From 6e79694a4c4407d43d53964c40ebd3012b0ee967 Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:13:12 -0800 Subject: [PATCH 01/40] refactor(docs): Add md/mdx linting via eslint and fix violations (#23168) Adds Markdown / MDX documentation linting via the following plugins: - [eslint-mdx](https://github.com/mdx-js/eslint-mdx) - [@docusaurus/eslint-plugin](https://docusaurus.io/docs/api/misc/@docusaurus/eslint-plugin) Also fixes violations in our docs. [AB#21860](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/21860) --- .prettierignore | 3 +- docs/.eslintignore | 4 + docs/.eslintrc.cjs | 135 +++-- docs/README.md | 20 +- docs/docs/build/releases-and-apitags.mdx | 2 +- docs/docs/glossary.mdx | 2 +- docs/docs/testing/telemetry.mdx | 8 +- docs/docs/testing/typed-telemetry.mdx | 2 +- docs/package.json | 7 +- docs/pnpm-lock.yaml | 523 ++++++++++++++++++ docs/versioned_docs/version-1/glossary.mdx | 2 +- .../version-1/release-notes.mdx | 4 +- .../version-1/testing/telemetry.mdx | 8 +- .../version-1/testing/typed-telemetry.mdx | 2 +- 14 files changed, 647 insertions(+), 75 deletions(-) create mode 100644 docs/.eslintignore diff --git a/.prettierignore b/.prettierignore index b20b3be38d18..5881ab3762bc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -80,7 +80,8 @@ packages/framework/data-object-base/es5 # Generated by policy-check packages/runtime/test-runtime-utils/src/assertionShortCodesMap.ts -# TODO: Investigate formatting options that support JSX syntax in .mdx files +# Prettier does not yet support mdx v3. +# See docs/**/*.mdx # Generated diff --git a/docs/.eslintignore b/docs/.eslintignore new file mode 100644 index 000000000000..05fb46a2a94b --- /dev/null +++ b/docs/.eslintignore @@ -0,0 +1,4 @@ +build +node_modules +docs/api +versioned_docs/*/api diff --git a/docs/.eslintrc.cjs b/docs/.eslintrc.cjs index c06e973f1d01..f97407e15823 100644 --- a/docs/.eslintrc.cjs +++ b/docs/.eslintrc.cjs @@ -4,73 +4,96 @@ */ module.exports = { - extends: [require.resolve("@fluidframework/eslint-config-fluid"), "prettier"], parserOptions: { - project: ["./tsconfig.json"], + ecmaVersion: "2022", }, - settings: { - react: { - version: "detect", - }, - }, - rules: { - // Required by Docusaurus for certain component exports. - "import/no-default-export": "off", - - "import/no-unassigned-import": [ - "error", - { - // Allow unassigned imports of css files. - allow: ["**/*.css"], + overrides: [ + // Rules for code + { + files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"], + extends: [require.resolve("@fluidframework/eslint-config-fluid"), "prettier"], + parserOptions: { + project: ["./tsconfig.json"], }, - ], - - "import/no-internal-modules": [ - "error", - { - allow: ["@docusaurus/**", "@site/**", "@theme/**"], + settings: { + react: { + version: "detect", + }, }, - ], + rules: { + // Required by Docusaurus for certain component exports. + "import/no-default-export": "off", - "import/no-unresolved": [ - "error", - { - ignore: ["^@docusaurus/", "^@theme/", "^@theme-original/"], - }, - ], + "import/no-unassigned-import": [ + "error", + { + // Allow unassigned imports of css files. + allow: ["**/*.css"], + }, + ], - // All dependencies in this package are dev - "import/no-extraneous-dependencies": [ - "error", - { - devDependencies: true, - }, - ], + "import/no-internal-modules": [ + "error", + { + allow: ["@docusaurus/**", "@site/**", "@theme/**"], + }, + ], - // Unfortunately, some of the import aliases supported by Docusaurus are not recognized by TSC / the eslint parser. - // So we have to disable some rules that enforce strong typing. - // Could be worth investigating if there's a way to make TSC aware of how the aliases are resolved, but until then, - // these rules are disabled. - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - }, - overrides: [ - { - // Test files - files: ["test/**/*"], - parserOptions: { - project: ["./test/tsconfig.json"], + "import/no-unresolved": [ + "error", + { + ignore: ["^@docusaurus/", "^@theme/", "^@theme-original/"], + }, + ], + + // All dependencies in this package are dev + "import/no-extraneous-dependencies": [ + "error", + { + devDependencies: true, + }, + ], + + // Unfortunately, some of the import aliases supported by Docusaurus are not recognized by TSC / the eslint parser. + // So we have to disable some rules that enforce strong typing. + // Could be worth investigating if there's a way to make TSC aware of how the aliases are resolved, but until then, + // these rules are disabled. + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", }, + overrides: [ + { + // Test file rule overrides + files: ["test/**/*"], + parserOptions: { + project: ["./test/tsconfig.json"], + }, + }, + { + // Config file tool overrides + files: ["docusaurus.config.ts", "playwright.config.ts", "infra/**/*"], + rules: { + "import/no-internal-modules": "off", + }, + }, + ], }, + + // Rules for .md/.mdx documents { - // Config files - files: ["docusaurus.config.ts", "playwright.config.ts", "infra/**/*"], + files: ["**/*.md", "**/*.mdx"], + // TODO: extend prettier plugin, once prettier supports MDX v3. + // See + extends: ["plugin:mdx/recommended"], + plugins: ["@docusaurus/eslint-plugin"], rules: { - "import/no-internal-modules": "off", - "import/no-nodejs-modules": "off", - "unicorn/no-process-exit": "off", + // See + "@docusaurus/no-html-links": "error", + + // See + "@docusaurus/prefer-docusaurus-heading": "error", }, }, ], diff --git a/docs/README.md b/docs/README.md index a03255e79b76..f961c5c61c72 100644 --- a/docs/README.md +++ b/docs/README.md @@ -137,6 +137,25 @@ The replacement syntax to use in `.mdx` files would be: (just like you would do in a JSX context!) +#### Custom Heading IDs + +In GitHub-flavored Markdown, you can assign a custom anchor ID to a heading by appending `{#}` to the heading text. +E.g., + +```markdown +# Foo {#bar} +``` + +Because curly braces are interpreted specially by JSX, this syntax doesn't work as is in `.mdx` documents. +Instead, you'll need to escape the opening brace to prevent MDX from attempting to process the syntax as JSX. +E.g., + +```markdown +# Foo \{#bar} +``` + +See the following Docusaurus issue for more context: . + ### Mermaid Docusaurus has built-in support for [mermaid](https://mermaid.js.org/) diagrams. @@ -182,7 +201,6 @@ The following npm scripts are supported in this directory: | Script | Description | |--------|-------------| | `build` | Build everything: the API documentation, the website, the tests, etc. | -| `prebuild:api-documentation` | Temporary workaround for AB#24394. Cleans up existing generated API documentation before generating new content. | | `build:api-documentation` | Download API model artifacts and generate API documentation. | | `prebuild:docusaurus` | Runs pre-site build metadata generation. | | `build:docusaurus` | Build the website with Docusaurus. | diff --git a/docs/docs/build/releases-and-apitags.mdx b/docs/docs/build/releases-and-apitags.mdx index 27263221ffde..d9eb8f7daad7 100644 --- a/docs/docs/build/releases-and-apitags.mdx +++ b/docs/docs/build/releases-and-apitags.mdx @@ -32,7 +32,7 @@ There are no guarantees for API breakages or Fluid document compatibility among These are done on demand to preview new APIs or try fixes for our partners. They are represented by the `dev` tag in npm There are no guarantees for API breakages or Fluid document compatibility among the release changes. Assume you will have to throw away containers generated by older release candidates -## API Support Levels {#api-support-levels} +## API Support Levels \{#api-support-levels} For packages that are part of the `@fluidframework` scope and the `fluid-framework` package, we use import paths to communicate the stability and guarantees associated with those APIs. diff --git a/docs/docs/glossary.mdx b/docs/docs/glossary.mdx index d7fbdeb02c3a..05a10352a17c 100644 --- a/docs/docs/glossary.mdx +++ b/docs/docs/glossary.mdx @@ -34,7 +34,7 @@ A Fluid container can be _attached_ or _detached_. A detached container is not c be loaded by other clients. Newly created containers begin in a detached state, which allows developers to add initial data if needed before attaching the container. Also see [Attached](#attached). -## Distributed data structures (DDSes) {#distributed-data-structures} +## Distributed data structures (DDSes) \{#distributed-data-structures} DDSes are the data structures Fluid Framework provides for storing collaborative data. As collaborators modify the data, the changes will be reflected to all other collaborators. diff --git a/docs/docs/testing/telemetry.mdx b/docs/docs/testing/telemetry.mdx index 7383966b9df1..dc772df7a0cf 100644 --- a/docs/docs/testing/telemetry.mdx +++ b/docs/docs/testing/telemetry.mdx @@ -14,7 +14,7 @@ your other telemetry, and route the event data in whatever way you need. The `ITelemetryBaseLogger` is an interface within the `@fluidframework/common-definitions` package. This interface can be implemented and passed into the service client's constructor via the `props` parameter. -All Fluid service clients (for example, [AzureClient][]) and [TinyliciousClient][])) allow passing a `logger?: ITelemetryBaseLogger` +All Fluid service clients (for example, [AzureClient][] and [TinyliciousClient][]) allow passing a `logger?: ITelemetryBaseLogger` into the service client props. Both `createContainer()` and `getContainer()` methods will then create an instance of the `logger`. `TinyliciousClientProps` interface definition takes an optional parameter `logger`. @@ -155,7 +155,7 @@ The Fluid Framework sends events in the following categories: ### EventName -This property contains a unique name for the event. The name may be namespaced, delimitted by a colon ':'. +This property contains a unique name for the event. The name may be namespaced, delimited by a colon ':'. Additionally, some event names (not the namespaces) contain underscores '\_', as a free-form subdivision of events into different related cases. Once common example is `foo_start`, `foo_end` and `foo_cancel` for performance events. @@ -263,6 +263,7 @@ async function start(): Promise { const id = await container.attach(); location.hash = id; } +} ``` Now, whenever a telemetry event is encountered, the custom `send()` method gets called and will print out the entire @@ -270,8 +271,7 @@ event object. The
-  ConsoleLogger sends telemetry events to the browser console for display. :::warning diff --git a/docs/docs/testing/typed-telemetry.mdx b/docs/docs/testing/typed-telemetry.mdx index b72ed208ad5e..f2af8a74df02 100644 --- a/docs/docs/testing/typed-telemetry.mdx +++ b/docs/docs/testing/typed-telemetry.mdx @@ -143,7 +143,7 @@ startTelemetry(telemetryConfig); You can now run the app and see the telemetry being printed on your console. -### Interpreting telemetry data {#telemetry_visualization} +### Interpreting telemetry data \{#telemetry_visualization} This section provides a set of Azure App Insights queries related to collaborative sessions within a Fluid Framework application. It is intended to be used with the telemetry generated from @fluidframework/fluid-telemetry package whose integration steps are outline above. diff --git a/docs/package.json b/docs/package.json index 247b2a5d5d90..12147dc69394 100644 --- a/docs/package.json +++ b/docs/package.json @@ -28,8 +28,8 @@ "clean:test": "rimraf --glob test-results", "clean:versions-json": "rimraf --glob ./versions.json", "download-doc-models": "node ./infra/download-doc-models.mjs", - "eslint": "eslint src test --format stylish", - "eslint:fix": "eslint src test --format stylish --fix", + "eslint": "eslint . --format stylish", + "eslint:fix": "eslint . --format stylish --fix", "format": "npm run prettier:fix", "generate-api-documentation": "dotenv -- node ./infra/generate-api-documentation.mjs", "generate-versions": "dotenv -- node ./infra/generate-versions.mjs", @@ -61,6 +61,7 @@ "devDependencies": { "@azure/static-web-apps-cli": "^2.0.1", "@docusaurus/core": "^3.6.2", + "@docusaurus/eslint-plugin": "^3.6.2", "@docusaurus/module-type-aliases": "^3.6.2", "@docusaurus/plugin-content-docs": "^3.6.2", "@docusaurus/preset-classic": "^3.6.2", @@ -85,6 +86,8 @@ "dotenv-cli": "^7.4.3", "eslint": "~8.55.0", "eslint-config-prettier": "~9.1.0", + "eslint-mdx": "^3.1.5", + "eslint-plugin-mdx": "^3.1.5", "fs-extra": "^11.2.0", "linkcheck-bin": "3.0.0-1", "lunr": "^2.3.9", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index d8ca0cd177a2..64b324145d83 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@docusaurus/core': specifier: ^3.6.2 version: 3.6.2(@mdx-js/react@3.1.0)(acorn@8.14.0)(debug@4.3.7)(eslint@8.55.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.4) + '@docusaurus/eslint-plugin': + specifier: ^3.6.2 + version: 3.6.2(eslint@8.55.0)(typescript@5.5.4) '@docusaurus/module-type-aliases': specifier: ^3.6.2 version: 3.6.2(acorn@8.14.0)(react-dom@18.3.1)(react@18.3.1) @@ -86,6 +89,12 @@ importers: eslint-config-prettier: specifier: ~9.1.0 version: 9.1.0(eslint@8.55.0) + eslint-mdx: + specifier: ^3.1.5 + version: 3.1.5(eslint@8.55.0) + eslint-plugin-mdx: + specifier: ^3.1.5 + version: 3.1.5(eslint@8.55.0) fs-extra: specifier: ^11.2.0 version: 11.2.0 @@ -2572,6 +2581,20 @@ packages: tslib: 2.8.1 dev: true + /@docusaurus/eslint-plugin@3.6.2(eslint@8.55.0)(typescript@5.5.4): + resolution: {integrity: sha512-QBsbQs1M+qOnwUiQoDVfkgqu1/S1YQMYP9zAlqrim9IXwfHc9KR6AVqW5A2WIA2IzWeeLMN4rlf/604HtEzZeg==} + engines: {node: '>=18.0'} + peerDependencies: + eslint: '>=6' + dependencies: + '@typescript-eslint/utils': 5.62.0(eslint@8.55.0)(typescript@5.5.4) + eslint: 8.55.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@docusaurus/logger@3.6.2: resolution: {integrity: sha512-1p4IQhhgLyIfsey4UAdAIW69aUE1Ei6O91Nsw30ryZeDWSG5dh4o3zaRGOLxfAX69Ac/yDm6YCwJOafUxL6Vxg==} engines: {node: '>=18.0'} @@ -3898,6 +3921,76 @@ packages: engines: {node: '>=12.4.0'} dev: true + /@npmcli/config@8.3.4: + resolution: {integrity: sha512-01rtHedemDNhUXdicU7s+QYz/3JyV5Naj84cvdXGH4mgCdL+agmSYaLF4LUG4vMCLzhBO8YtS0gPpH1FGvbgAw==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + '@npmcli/map-workspaces': 3.0.6 + '@npmcli/package-json': 5.2.1 + ci-info: 4.1.0 + ini: 4.1.3 + nopt: 7.2.1 + proc-log: 4.2.0 + semver: 7.6.3 + walk-up-path: 3.0.1 + transitivePeerDependencies: + - bluebird + dev: true + + /@npmcli/git@5.0.8: + resolution: {integrity: sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + '@npmcli/promise-spawn': 7.0.2 + ini: 4.1.3 + lru-cache: 10.4.3 + npm-pick-manifest: 9.1.0 + proc-log: 4.2.0 + promise-inflight: 1.0.1 + promise-retry: 2.0.1 + semver: 7.6.3 + which: 4.0.0 + transitivePeerDependencies: + - bluebird + dev: true + + /@npmcli/map-workspaces@3.0.6: + resolution: {integrity: sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + '@npmcli/name-from-folder': 2.0.0 + glob: 10.4.5 + minimatch: 9.0.5 + read-package-json-fast: 3.0.2 + dev: true + + /@npmcli/name-from-folder@2.0.0: + resolution: {integrity: sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /@npmcli/package-json@5.2.1: + resolution: {integrity: sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + '@npmcli/git': 5.0.8 + glob: 10.4.5 + hosted-git-info: 7.0.2 + json-parse-even-better-errors: 3.0.2 + normalize-package-data: 6.0.2 + proc-log: 4.2.0 + semver: 7.6.3 + transitivePeerDependencies: + - bluebird + dev: true + + /@npmcli/promise-spawn@7.0.2: + resolution: {integrity: sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + which: 4.0.0 + dev: true + /@oclif/core@4.0.33: resolution: {integrity: sha512-NoTDwJ2L/ywpsSjcN7jAAHf3m70Px4Yim2SJrm16r70XpnfbNOdlj1x0HEJ0t95gfD+p/y5uy+qPT/VXTh/1gw==} engines: {node: '>=18.0.0'} @@ -4079,6 +4172,11 @@ packages: dev: true optional: true + /@pkgr/core@0.1.1: + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dev: true + /@playwright/test@1.49.0: resolution: {integrity: sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==} engines: {node: '>=18'} @@ -4574,6 +4672,12 @@ packages: '@types/node': 22.9.1 dev: true + /@types/concat-stream@2.0.3: + resolution: {integrity: sha512-3qe4oQAPNwVNwK4C9c8u+VJqv9kez+2MR4qJpoPFfXtgxxif1QbFusvXzK0/Wra2VX07smostI2VMmJNSpZjuQ==} + dependencies: + '@types/node': 22.9.1 + dev: true + /@types/configstore@2.1.1: resolution: {integrity: sha512-YY+hm3afkDHeSM2rsFXxeZtu0garnusBWNG1+7MknmDWQHqcH2w21/xOU9arJUi8ch4qyFklidANLCu3ihhVwQ==} dev: true @@ -4898,6 +5002,10 @@ packages: '@types/node': 22.9.1 dev: true + /@types/is-empty@1.2.3: + resolution: {integrity: sha512-4J1l5d79hoIvsrKh5VUKVRA1aIdsOb10Hu5j3J2VfP/msDnfTdGPmNp2E1Wg+vs97Bktzo+MZePFFXSGoykYJw==} + dev: true + /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} dev: true @@ -5082,6 +5190,10 @@ packages: '@types/node': 22.9.1 dev: true + /@types/supports-color@8.1.3: + resolution: {integrity: sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==} + dev: true + /@types/tmp@0.0.33: resolution: {integrity: sha512-gVC1InwyVrO326wbBZw+AO3u2vRXz/iRWq9jYhpG4W8LXyIgDv3ZmcLQ5Q4Gs+gFMyqx+viFoFT+l3p61QFCmQ==} dev: true @@ -5202,6 +5314,14 @@ packages: '@typescript-eslint/visitor-keys': 5.59.11 dev: true + /@typescript-eslint/scope-manager@5.62.0: + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + dev: true + /@typescript-eslint/scope-manager@6.21.0: resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5243,6 +5363,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@typescript-eslint/types@5.62.0: + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@typescript-eslint/types@6.21.0: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5274,6 +5399,27 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.5.4): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.7(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.6.3 + tsutils: 3.21.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.5.4): resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5337,6 +5483,26 @@ packages: - typescript dev: true + /@typescript-eslint/utils@5.62.0(eslint@8.55.0)(typescript@5.5.4): + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.55.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.4) + eslint: 8.55.0 + eslint-scope: 5.1.1 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils@6.7.5(eslint@8.55.0)(typescript@5.5.4): resolution: {integrity: sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5364,6 +5530,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@5.62.0: + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@typescript-eslint/visitor-keys@6.21.0: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5498,6 +5672,11 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true + /abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -6395,6 +6574,11 @@ packages: engines: {node: '>=8'} dev: true + /ci-info@4.1.0: + resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + engines: {node: '>=8'} + dev: true + /clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -6624,6 +6808,16 @@ packages: typedarray: 0.0.6 dev: true + /concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + dev: true + /concurrently@7.6.0: resolution: {integrity: sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==} engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} @@ -7713,6 +7907,11 @@ packages: dequal: 2.0.3 dev: true + /diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + dev: true + /dill-cli@0.1.3(typescript@5.5.4): resolution: {integrity: sha512-8Ej9+I5SeYCXFcojLw+JIdH+rnzoYnvFMIqlfit5D5ByNqNfCHbLbN1++2mqKP7XyYbHs66U5+fFX9zx/pth5g==} engines: {node: '>=18.0.0'} @@ -7990,6 +8189,10 @@ packages: resolution: {integrity: sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==} dev: true + /err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + dev: true + /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -8226,6 +8429,32 @@ packages: - supports-color dev: true + /eslint-mdx@3.1.5(eslint@8.55.0): + resolution: {integrity: sha512-ynztX0k7CQ3iDL7fDEIeg3g0O/d6QPv7IBI9fdYLhXp5fAp0fi8X22xF/D3+Pk0f90R27uwqa1clHpay6t0l8Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + eslint: '>=8.0.0' + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint: 8.55.0 + espree: 9.6.1 + estree-util-visit: 2.0.0 + remark-mdx: 3.1.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + synckit: 0.9.2 + tslib: 2.8.1 + unified: 11.0.5 + unified-engine: 11.2.2 + unist-util-visit: 5.0.0 + uvu: 0.5.6 + vfile: 6.0.3 + transitivePeerDependencies: + - bluebird + - supports-color + dev: true + /eslint-module-utils@2.12.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.55.0): resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} engines: {node: '>=4'} @@ -8309,6 +8538,38 @@ packages: - supports-color dev: true + /eslint-plugin-markdown@3.0.1(eslint@8.55.0): + resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.55.0 + mdast-util-from-markdown: 0.8.5 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-mdx@3.1.5(eslint@8.55.0): + resolution: {integrity: sha512-lUE7tP7IrIRHU3gTtASDe5u4YM2SvQveYVJfuo82yn3MLh/B/v05FNySURCK4aIxIYF1QYo3IRemQG/lyQzpAg==} + engines: {node: '>=18.0.0'} + peerDependencies: + eslint: '>=8.0.0' + dependencies: + eslint: 8.55.0 + eslint-mdx: 3.1.5(eslint@8.55.0) + eslint-plugin-markdown: 3.0.1(eslint@8.55.0) + remark-mdx: 3.1.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + tslib: 2.8.1 + unified: 11.0.5 + vfile: 6.0.3 + transitivePeerDependencies: + - bluebird + - supports-color + dev: true + /eslint-plugin-promise@6.1.1(eslint@8.55.0): resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -9895,6 +10156,11 @@ packages: engines: {node: '>= 4'} dev: true + /ignore@6.0.2: + resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==} + engines: {node: '>= 4'} + dev: true + /image-size@1.1.1: resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==} engines: {node: '>=16.x'} @@ -9928,6 +10194,10 @@ packages: engines: {node: '>=8'} dev: true + /import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + dev: true + /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -9978,6 +10248,11 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} dev: true @@ -10171,6 +10446,10 @@ packages: hasBin: true dev: true + /is-empty@1.2.0: + resolution: {integrity: sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==} + dev: true + /is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -10468,6 +10747,11 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + dev: true + /isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} @@ -10600,6 +10884,11 @@ packages: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true + /json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /json-schema-library@9.3.5: resolution: {integrity: sha512-5eBDx7cbfs+RjylsVO+N36b0GOPtv78rfqgf2uON+uaHUIC62h63Y8pkV2ovKbaL4ZpQcHp21968x5nx/dFwqQ==} dependencies: @@ -10745,6 +11034,11 @@ packages: engines: {node: '>=6'} dev: true + /kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + /klona@2.0.6: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} @@ -10826,6 +11120,11 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true + /lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /linkcheck-bin@3.0.0-1: resolution: {integrity: sha512-Qj4ag9fCcEKMdMzwKQoRt1ZUQUxGgx8OoseWJ3O+4Ph21O2TBU9m8+tmRPXYFnvSJUYYE7/hfbI5hPHAb8RY3w==} engines: {node: '>=18.0.0'} @@ -10839,6 +11138,15 @@ packages: - supports-color dev: true + /load-plugin@6.0.3: + resolution: {integrity: sha512-kc0X2FEUZr145odl68frm+lMJuQ23+rTXYmR6TImqPtbpmXC4vVXbWKDQ9IzndA0HfyQamWfKLhzsqGSTxE63w==} + dependencies: + '@npmcli/config': 8.3.4 + import-meta-resolve: 4.1.0 + transitivePeerDependencies: + - bluebird + dev: true + /loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -12080,6 +12388,11 @@ packages: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} dev: true + /mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + /mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -12194,6 +12507,14 @@ packages: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} dev: true + /nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + abbrev: 2.0.0 + dev: true + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -12227,6 +12548,38 @@ packages: engines: {node: '>=14.16'} dev: true + /npm-install-checks@6.3.0: + resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + semver: 7.6.3 + dev: true + + /npm-normalize-package-bin@3.0.1: + resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /npm-package-arg@11.0.3: + resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + hosted-git-info: 7.0.2 + proc-log: 4.2.0 + semver: 7.6.3 + validate-npm-package-name: 5.0.1 + dev: true + + /npm-pick-manifest@9.1.0: + resolution: {integrity: sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + npm-install-checks: 6.3.0 + npm-normalize-package-bin: 3.0.1 + npm-package-arg: 11.0.3 + semver: 7.6.3 + dev: true + /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -12565,6 +12918,17 @@ packages: lines-and-columns: 1.2.4 dev: true + /parse-json@7.1.1: + resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} + engines: {node: '>=16'} + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 3.0.2 + lines-and-columns: 2.0.4 + type-fest: 3.13.1 + dev: true + /parse-json@8.1.0: resolution: {integrity: sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==} engines: {node: '>=18'} @@ -13617,10 +13981,32 @@ packages: engines: {node: '>=6'} dev: true + /proc-log@4.2.0: + resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true + /promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + dev: true + + /promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + dev: true + /promise@8.3.0: resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} dependencies: @@ -13928,6 +14314,14 @@ packages: loose-envify: 1.4.0 dev: true + /read-package-json-fast@3.0.2: + resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + json-parse-even-better-errors: 3.0.2 + npm-normalize-package-bin: 3.0.1 + dev: true + /read-package-up@11.0.0: resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} engines: {node: '>=18'} @@ -14384,6 +14778,11 @@ packages: engines: {node: '>=0.12'} dev: true + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + dev: true + /retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -14461,6 +14860,13 @@ packages: tslib: 2.8.1 dev: true + /sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: true + /safe-array-concat@1.1.2: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} @@ -15028,6 +15434,15 @@ packages: strip-ansi: 7.1.0 dev: true + /string-width@6.1.0: + resolution: {integrity: sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==} + engines: {node: '>=16'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 10.4.0 + strip-ansi: 7.1.0 + dev: true + /string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -15214,6 +15629,11 @@ packages: has-flag: 4.0.0 dev: true + /supports-color@9.4.0: + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} + dev: true + /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -15252,6 +15672,14 @@ packages: get-port: 3.2.0 dev: true + /synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + dev: true + /tapable@1.1.3: resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} engines: {node: '>=6'} @@ -15534,6 +15962,11 @@ packages: engines: {node: '>=12.20'} dev: true + /type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + dev: true + /type-fest@4.27.0: resolution: {integrity: sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==} engines: {node: '>=16'} @@ -15686,6 +16119,35 @@ packages: engines: {node: '>=18'} dev: true + /unified-engine@11.2.2: + resolution: {integrity: sha512-15g/gWE7qQl9tQ3nAEbMd5h9HV1EACtFs6N9xaRBZICoCwnNGbal1kOs++ICf4aiTdItZxU2s/kYWhW7htlqJg==} + dependencies: + '@types/concat-stream': 2.0.3 + '@types/debug': 4.1.12 + '@types/is-empty': 1.2.3 + '@types/node': 22.9.1 + '@types/unist': 3.0.3 + concat-stream: 2.0.0 + debug: 4.3.7(supports-color@8.1.1) + extend: 3.0.2 + glob: 10.4.5 + ignore: 6.0.2 + is-empty: 1.2.0 + is-plain-obj: 4.1.0 + load-plugin: 6.0.3 + parse-json: 7.1.1 + trough: 2.2.0 + unist-util-inspect: 8.1.0 + vfile: 6.0.3 + vfile-message: 4.0.2 + vfile-reporter: 8.1.1 + vfile-statistics: 3.0.0 + yaml: 2.6.1 + transitivePeerDependencies: + - bluebird + - supports-color + dev: true + /unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} dependencies: @@ -15717,6 +16179,12 @@ packages: crypto-random-string: 4.0.0 dev: true + /unist-util-inspect@8.1.0: + resolution: {integrity: sha512-mOlg8Mp33pR0eeFpo5d2902ojqFFOKMMG2hF8bmH7ZlhnmjFgh0NI3/ZDwdaBJNbvrS7LZFVrBVtIE9KZ9s7vQ==} + dependencies: + '@types/unist': 3.0.3 + dev: true + /unist-util-is@4.1.0: resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} dev: true @@ -15897,6 +16365,17 @@ packages: hasBin: true dev: true + /uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + dequal: 2.0.3 + diff: 5.2.0 + kleur: 4.1.5 + sade: 1.8.1 + dev: true + /valid-url@1.0.9: resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} dev: true @@ -15908,6 +16387,11 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /validator@13.12.0: resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} engines: {node: '>= 0.10'} @@ -15943,6 +16427,33 @@ packages: unist-util-stringify-position: 4.0.0 dev: true + /vfile-reporter@8.1.1: + resolution: {integrity: sha512-qxRZcnFSQt6pWKn3PAk81yLK2rO2i7CDXpy8v8ZquiEOMLSnPw6BMSi9Y1sUCwGGl7a9b3CJT1CKpnRF7pp66g==} + dependencies: + '@types/supports-color': 8.1.3 + string-width: 6.1.0 + supports-color: 9.4.0 + unist-util-stringify-position: 4.0.0 + vfile: 6.0.3 + vfile-message: 4.0.2 + vfile-sort: 4.0.0 + vfile-statistics: 3.0.0 + dev: true + + /vfile-sort@4.0.0: + resolution: {integrity: sha512-lffPI1JrbHDTToJwcq0rl6rBmkjQmMuXkAxsZPRS9DXbaJQvc642eCg6EGxcX2i1L+esbuhq+2l9tBll5v8AeQ==} + dependencies: + vfile: 6.0.3 + vfile-message: 4.0.2 + dev: true + + /vfile-statistics@3.0.0: + resolution: {integrity: sha512-/qlwqwWBWFOmpXujL/20P+Iuydil0rZZNglR+VNm6J0gpLHwuVM5s7g2TfVoswbXjZ4HuIhLMySEyIw5i7/D8w==} + dependencies: + vfile: 6.0.3 + vfile-message: 4.0.2 + dev: true + /vfile@4.2.1: resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} dependencies: @@ -16018,6 +16529,10 @@ packages: - debug dev: true + /walk-up-path@3.0.1: + resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} + dev: true + /watchpack@2.4.2: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} @@ -16313,6 +16828,14 @@ packages: isexe: 2.0.0 dev: true + /which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + isexe: 3.1.1 + dev: true + /widest-line@3.1.0: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} diff --git a/docs/versioned_docs/version-1/glossary.mdx b/docs/versioned_docs/version-1/glossary.mdx index 5bc7f3a4e0c2..0526a7b3af76 100644 --- a/docs/versioned_docs/version-1/glossary.mdx +++ b/docs/versioned_docs/version-1/glossary.mdx @@ -34,7 +34,7 @@ A Fluid container can be _attached_ or _detached_. A detached container is not c be loaded by other clients. Newly created containers begin in a detached state, which allows developers to add initial data if needed before attaching the container. Also see [Attached](#attached). -## Distributed data structures (DDSes) {#distributed-data-structures} +## Distributed data structures (DDSes) \{#distributed-data-structures} DDSes are the data structures Fluid Framework provides for storing collaborative data. As collaborators modify the data, the changes will be reflected to all other collaborators. diff --git a/docs/versioned_docs/version-1/release-notes.mdx b/docs/versioned_docs/version-1/release-notes.mdx index e406ea2292a9..ad495bfdb708 100644 --- a/docs/versioned_docs/version-1/release-notes.mdx +++ b/docs/versioned_docs/version-1/release-notes.mdx @@ -80,13 +80,13 @@ The AzureClient connection config has been updated to have distinct types to dif ```js // Previous Remote Syntax const connection = { - tenantId = "AZURE_TENANT_ID", + tenantId: "AZURE_TENANT_ID", //... } // Previous Local Syntax import { LOCAL_MODE_TENANT_ID } from "@fluidframework/azure-client"; const connection = { - tenantId = LOCAL_MODE_TENANT_ID, + tenantId: LOCAL_MODE_TENANT_ID, //... } ``` diff --git a/docs/versioned_docs/version-1/testing/telemetry.mdx b/docs/versioned_docs/version-1/testing/telemetry.mdx index 7383966b9df1..dc772df7a0cf 100644 --- a/docs/versioned_docs/version-1/testing/telemetry.mdx +++ b/docs/versioned_docs/version-1/testing/telemetry.mdx @@ -14,7 +14,7 @@ your other telemetry, and route the event data in whatever way you need. The `ITelemetryBaseLogger` is an interface within the `@fluidframework/common-definitions` package. This interface can be implemented and passed into the service client's constructor via the `props` parameter. -All Fluid service clients (for example, [AzureClient][]) and [TinyliciousClient][])) allow passing a `logger?: ITelemetryBaseLogger` +All Fluid service clients (for example, [AzureClient][] and [TinyliciousClient][]) allow passing a `logger?: ITelemetryBaseLogger` into the service client props. Both `createContainer()` and `getContainer()` methods will then create an instance of the `logger`. `TinyliciousClientProps` interface definition takes an optional parameter `logger`. @@ -155,7 +155,7 @@ The Fluid Framework sends events in the following categories: ### EventName -This property contains a unique name for the event. The name may be namespaced, delimitted by a colon ':'. +This property contains a unique name for the event. The name may be namespaced, delimited by a colon ':'. Additionally, some event names (not the namespaces) contain underscores '\_', as a free-form subdivision of events into different related cases. Once common example is `foo_start`, `foo_end` and `foo_cancel` for performance events. @@ -263,6 +263,7 @@ async function start(): Promise { const id = await container.attach(); location.hash = id; } +} ``` Now, whenever a telemetry event is encountered, the custom `send()` method gets called and will print out the entire @@ -270,8 +271,7 @@ event object. The
-  ConsoleLogger sends telemetry events to the browser console for display. :::warning diff --git a/docs/versioned_docs/version-1/testing/typed-telemetry.mdx b/docs/versioned_docs/version-1/testing/typed-telemetry.mdx index b72ed208ad5e..f2af8a74df02 100644 --- a/docs/versioned_docs/version-1/testing/typed-telemetry.mdx +++ b/docs/versioned_docs/version-1/testing/typed-telemetry.mdx @@ -143,7 +143,7 @@ startTelemetry(telemetryConfig); You can now run the app and see the telemetry being printed on your console. -### Interpreting telemetry data {#telemetry_visualization} +### Interpreting telemetry data \{#telemetry_visualization} This section provides a set of Azure App Insights queries related to collaborative sessions within a Fluid Framework application. It is intended to be used with the telemetry generated from @fluidframework/fluid-telemetry package whose integration steps are outline above. From 645a1a0824648991ceba153f44a53744f0f10f14 Mon Sep 17 00:00:00 2001 From: Ji Kim <111468570+jikim-msft@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:36:22 -0800 Subject: [PATCH 02/40] docs(changeset): Remove a period from a changeset (#23174) #### Description A header of the changeset created for `Tree` package change includes `.`. Removing it for conformity with other changesets. --- .changeset/dull-words-work.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/dull-words-work.md b/.changeset/dull-words-work.md index 8fe0c13e78e2..8bf9e28a5b80 100644 --- a/.changeset/dull-words-work.md +++ b/.changeset/dull-words-work.md @@ -6,6 +6,6 @@ "section": tree --- -Enables Revertible objects to be cloned using `RevertibleAlpha.clone()`. +Enables Revertible objects to be cloned using `RevertibleAlpha.clone()` Replaced `DisposableRevertible` with `RevertibleAlpha`. The new `RevertibleAlpha` interface extends `Revertible` and includes a `clone(branch: TreeBranch)` method to facilitate cloning a Revertible to a specified target branch. The source branch where the `RevertibleAlpha` was created must share revision logs with the target branch where the `RevertibleAlpha` is being cloned. If this condition is not met, the operation will throw an error. From 8dd510a5b0a41ad5cb4a17f189db7dc563a0397c Mon Sep 17 00:00:00 2001 From: Michael Zhen <112977307+zhenmichael@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:50:48 -0800 Subject: [PATCH 03/40] (docs): fix implementation issues with deployment and publish artifact pipeline (#23171) Corrected errors from previous imlementation of deploy-website, upload-json-steps, and publish-api-model-artifact pipelines. Also removed unused pipeline artifact downloads in upload-json-steps (only downloading from client build now). --- tools/pipelines/deploy-website.yml | 6 +-- .../pipelines/publish-api-model-artifact.yml | 12 +++--- .../pipelines/templates/upload-json-steps.yml | 39 +++---------------- 3 files changed, 12 insertions(+), 45 deletions(-) diff --git a/tools/pipelines/deploy-website.yml b/tools/pipelines/deploy-website.yml index 069bb9729751..8cf3b44c7e2c 100644 --- a/tools/pipelines/deploy-website.yml +++ b/tools/pipelines/deploy-website.yml @@ -47,10 +47,7 @@ variables: - name: repoToTrigger value: microsoft/FluidFramework - name: shouldRetainGuardianAssets - value: ${{ or( - eq(parameters.guardianAssetRetentionOverride, 'force'), - and(eq(parameters.guardianAssetRetentionOverride, 'default')) - )}} + value: ${{ eq(parameters.guardianAssetRetentionOverride, 'default') }} - name: deploymentToken ${{ if eq( parameters['deployEnvironment'], 'new' ) }}: value: "$(FLUID_WEBSITE_TORUS_API_TOKEN)" @@ -105,7 +102,6 @@ stages: - job: build_site displayName: 'Build website' - dependsOn: upload_json pool: Large-eastus2 steps: - checkout: self diff --git a/tools/pipelines/publish-api-model-artifact.yml b/tools/pipelines/publish-api-model-artifact.yml index 11e36f37dc95..5595ec4a1d90 100644 --- a/tools/pipelines/publish-api-model-artifact.yml +++ b/tools/pipelines/publish-api-model-artifact.yml @@ -66,7 +66,7 @@ variables: - name: shouldRetainGuardianAssets value: ${{ or( eq(parameters.guardianAssetRetentionOverride, 'force'), - and(eq(parameters.guardianAssetRetentionOverride, 'default')) + eq(parameters.guardianAssetRetentionOverride, 'default') )}} - name: deploymentToken ${{ if eq( parameters['deployEnvironment'], 'new' ) }}: @@ -128,12 +128,12 @@ stages: - deployment: upload_json displayName: 'Combine api-extractor JSON' - dependsOn: [] # run in parallel + dependsOn: check_branch_version environment: 'fluid-docs-env' pool: Large variables: - uploadAsLatestRelease: $[ stageDependencies.check_branch_version.outputs['check_branch_version.SetShouldDeploy.shouldDeploy'] ] - majorVersion: $[ stageDependencies.check_branch_version.outputs['check_branch_version.SetVersion.majorVersion'] ] + uploadAsLatestRelease: $[ dependencies.check_branch_version.outputs['SetShouldDeploy.shouldDeploy'] ] + majorVersion: $[ dependencies.check_branch_version.outputs['SetShouldDeploy.majorVersion'] ] strategy: runOnce: deploy: @@ -149,10 +149,10 @@ stages: # this stage runs depending on the check_branch_version stage and deployOverride parameter # the trigger is configured such that deploy-website runs using main branch resources # this ensures that the generated website is up-to-date with the latest changes -- stage: deploy docs +- stage: deploy displayName: 'Deploy website' pool: Small - dependsOn: ['build', 'guardian', 'check_branch_version'] + dependsOn: ['check_branch_version'] jobs: - job: deploy_site displayName: 'Deploy website' diff --git a/tools/pipelines/templates/upload-json-steps.yml b/tools/pipelines/templates/upload-json-steps.yml index ad3651bbbd7d..5fa81c46b6ab 100644 --- a/tools/pipelines/templates/upload-json-steps.yml +++ b/tools/pipelines/templates/upload-json-steps.yml @@ -16,8 +16,11 @@ parameters: # Determines if artifact should be published as latest-v*.tar.gz (for release branches) - name: uploadAsLatestRelease - type: boolean + type: string default: false + values: + - true + - false # Major version to upload as latest-v*.tar.gz - name: majorVersion @@ -29,42 +32,10 @@ steps: clean: true # Download the api-extractor outputs -- template: download-api-extractor-artifact.yml - parameters: - pipelineName: Build - common-definitions - branchName: ${{ variables['Build.SourceBranch'] }} -- template: download-api-extractor-artifact.yml - parameters: - pipelineName: Build - common-utils - branchName: ${{ variables['Build.SourceBranch'] }} -- template: download-api-extractor-artifact.yml - parameters: - pipelineName: Build - container-definitions - branchName: ${{ variables['Build.SourceBranch'] }} -- template: download-api-extractor-artifact.yml - parameters: - pipelineName: Build - core-interfaces - branchName: ${{ variables['Build.SourceBranch'] }} -- template: download-api-extractor-artifact.yml - parameters: - pipelineName: Build - driver-definitions - branchName: ${{ variables['Build.SourceBranch'] }} -- template: download-api-extractor-artifact.yml - parameters: - pipelineName: Build - protocol-definitions - branchName: ${{ variables['Build.SourceBranch'] }} -- template: download-api-extractor-artifact.yml - parameters: - pipelineName: Build - azure - branchName: ${{ variables['Build.SourceBranch'] }} - template: download-api-extractor-artifact.yml parameters: pipelineName: Build - client packages branchName: ${{ variables['Build.SourceBranch'] }} -- template: download-api-extractor-artifact.yml - parameters: - pipelineName: server-routerlicious - branchName: ${{ variables['Build.SourceBranch'] }} # Copy and merge the api-extractor outputs to a central location - task: CopyFiles@2 @@ -116,7 +87,7 @@ steps: inlineScript: | az storage blob upload -f '$(Pipeline.Workspace)/$(Build.SourceVersion).tar.gz' -c 'api-extractor-json' -n latest.tar.gz --account-name ${{ parameters.STORAGE_ACCOUNT }} --auth-mode login --overwrite --verbose -- ${{ if eq(parameters.uploadAsLatestRelease, true) }}: +- ${{ if eq(parameters.uploadAsLatestRelease, 'true') }}: - task: AzureCLI@2 displayName: 'Upload JSON as latest-v*.tar.gz' continueOnError: true From b0a067536bd66ca2d5ddc4d285c2590f29939c35 Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:31:30 -0800 Subject: [PATCH 04/40] fix(docs): Use no-cookies youtube link (#23175) The new website was flagged by our privacy team for embedding a youtube video without ensuring no cookies were collected. This updates the only currently embedded youtube video on the site to ensure this is not the case, adds a re-usable React component to avoid introducing such issues in the future, and adds best practices documentation to the docs README. --- docs/README.md | 17 ++++++++++++ docs/src/components/home/banner.tsx | 22 +++------------ docs/src/components/youtubeVideo.tsx | 41 ++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 docs/src/components/youtubeVideo.tsx diff --git a/docs/README.md b/docs/README.md index f961c5c61c72..25792af907fc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -189,6 +189,23 @@ E.g., +``` + ## Scripts The following npm scripts are supported in this directory: diff --git a/docs/src/components/home/banner.tsx b/docs/src/components/home/banner.tsx index 9e0869062579..52edf38f84e9 100644 --- a/docs/src/components/home/banner.tsx +++ b/docs/src/components/home/banner.tsx @@ -5,9 +5,11 @@ import React from "react"; +import { YoutubeVideo } from "@site/src/components/youtubeVideo"; + import "@site/src/css/home/banner.css"; -const videoSourceUrl = "https://www.youtube.com/embed/fjRfTdIYzWg"; +const videoEmbedId = "fjRfTdIYzWg"; /** * Homepage banner component. @@ -17,7 +19,7 @@ export function Banner(): React.ReactElement {
-
); @@ -34,19 +36,3 @@ function TitleBox(): React.ReactElement { ); } - -function Video(): React.ReactElement { - return ( -
- -
- ); -} diff --git a/docs/src/components/youtubeVideo.tsx b/docs/src/components/youtubeVideo.tsx new file mode 100644 index 000000000000..78bcce76b275 --- /dev/null +++ b/docs/src/components/youtubeVideo.tsx @@ -0,0 +1,41 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import React from "react"; + +/** + * {@link YoutubeVideo} component props. + */ +export interface YoutubeVideoProps { + /** + * Embed ID of the YouTube video. + */ + videoId: string; + + /** + * Optional class name to apply to the video container. + */ + className?: string; +} + +/** + * Renders a YouTube video, utilizing `youtube-nocookie.com` to ensure our privacy requirements are being met (i.e., no cookies). + */ +export function YoutubeVideo({ className, videoId }: YoutubeVideoProps): React.Element { + const videoSourceUrl = `https://www.youtube-nocookie.com/embed/${videoId}`; + return ( +
+ +
+ ); +} From ab91a56fb50bcdd3ce9ec4ac6dfea7af77eb83b1 Mon Sep 17 00:00:00 2001 From: Michael Zhen <112977307+zhenmichael@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:33:51 -0800 Subject: [PATCH 05/40] (docs): remove enum from upload-json-steps param (#23176) parameter has issues processing runtime variable inputs with enum. It seems that during compilation time, it takes the variable input as the literal string, which causes it to fail the enum true/false check. --- tools/pipelines/templates/upload-json-steps.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/pipelines/templates/upload-json-steps.yml b/tools/pipelines/templates/upload-json-steps.yml index 5fa81c46b6ab..fcb7cdfeecc9 100644 --- a/tools/pipelines/templates/upload-json-steps.yml +++ b/tools/pipelines/templates/upload-json-steps.yml @@ -18,9 +18,6 @@ parameters: - name: uploadAsLatestRelease type: string default: false - values: - - true - - false # Major version to upload as latest-v*.tar.gz - name: majorVersion From cea34d10d0f816335ab1b88b190940046ae7b696 Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:38:32 -0800 Subject: [PATCH 06/40] refactor(devtools): Mark interfaces as `@sealed` and `@system` as appropriate (#23165) APIs that were never intended for direct consumer use have been marked as `@system`. These are: - HasContainerKey And APIs that were not intended to be extended by consumers have been marked as `@sealed`. These are: - ContainerDevtoolsProps - DevtoolsProps - HasContainerKey - IDevtools Additionally, interface properties have been marked as `readonly`. Note that all of the affected APIs are `@beta`, so this change is not in violation of our breaking changes policy. And in practice, it is unlikely to affect any users of our APIs. --- .changeset/long-hats-smile.md | 24 +++++++++++++++++++ .../api-report/devtools-core.alpha.api.md | 4 ++-- .../api-report/devtools-core.beta.api.md | 4 ++-- .../devtools-core/src/CommonInterfaces.ts | 10 ++++---- .../devtools/api-report/devtools.alpha.api.md | 16 ++++++------- .../devtools/api-report/devtools.beta.api.md | 16 ++++++------- packages/tools/devtools/devtools/src/index.ts | 13 +++++++--- 7 files changed, 60 insertions(+), 27 deletions(-) create mode 100644 .changeset/long-hats-smile.md diff --git a/.changeset/long-hats-smile.md b/.changeset/long-hats-smile.md new file mode 100644 index 000000000000..3c42df45eabd --- /dev/null +++ b/.changeset/long-hats-smile.md @@ -0,0 +1,24 @@ +--- +"@fluidframework/devtools": minor +"@fluidframework/devtools-core": minor +--- +--- +"section": other +--- + +Mark APIs as `@sealed` and `@system` as appropriate, and make interface properties `readonly` + +APIs that were never intended for direct consumer use have been marked as `@system`. +These are: + +- HasContainerKey + +APIs that were not intended to be extended by consumers have been marked as `@sealed`. +These are: + +- ContainerDevtoolsProps +- DevtoolsProps +- HasContainerKey +- IDevtools + +Additionally, interface properties have been marked as `readonly`. diff --git a/packages/tools/devtools/devtools-core/api-report/devtools-core.alpha.api.md b/packages/tools/devtools/devtools-core/api-report/devtools-core.alpha.api.md index 14bca7db3e42..4d52956240fc 100644 --- a/packages/tools/devtools/devtools-core/api-report/devtools-core.alpha.api.md +++ b/packages/tools/devtools/devtools-core/api-report/devtools-core.alpha.api.md @@ -22,9 +22,9 @@ export interface FluidDevtoolsProps { logger?: IDevtoolsLogger; } -// @beta +// @beta @sealed export interface HasContainerKey { - containerKey: ContainerKey; + readonly containerKey: ContainerKey; } // @beta @sealed diff --git a/packages/tools/devtools/devtools-core/api-report/devtools-core.beta.api.md b/packages/tools/devtools/devtools-core/api-report/devtools-core.beta.api.md index 81133885b70f..cb293d74b575 100644 --- a/packages/tools/devtools/devtools-core/api-report/devtools-core.beta.api.md +++ b/packages/tools/devtools/devtools-core/api-report/devtools-core.beta.api.md @@ -10,9 +10,9 @@ export type ContainerKey = string; // @beta export function createDevtoolsLogger(baseLogger?: ITelemetryBaseLogger): IDevtoolsLogger; -// @beta +// @beta @sealed export interface HasContainerKey { - containerKey: ContainerKey; + readonly containerKey: ContainerKey; } // @beta @sealed diff --git a/packages/tools/devtools/devtools-core/src/CommonInterfaces.ts b/packages/tools/devtools/devtools-core/src/CommonInterfaces.ts index 1f1b25683483..c1d3ab7a1cde 100644 --- a/packages/tools/devtools/devtools-core/src/CommonInterfaces.ts +++ b/packages/tools/devtools/devtools-core/src/CommonInterfaces.ts @@ -8,22 +8,24 @@ * * @remarks Each Container registered with the Devtools must be assigned a unique `containerKey`. * - * @example + * @example "Canvas Container" * - * "Canvas Container" * @beta */ export type ContainerKey = string; /** * Common interface for data associated with a particular Container registered with the Devtools. + * + * @sealed + * @system * @beta */ export interface HasContainerKey { /** * {@inheritDoc ContainerKey} */ - containerKey: ContainerKey; + readonly containerKey: ContainerKey; } /** @@ -43,7 +45,7 @@ export interface HasFluidObjectId { /** * The ID of the Fluid object (DDS) associated with data or a request. */ - fluidObjectId: FluidObjectId; + readonly fluidObjectId: FluidObjectId; } /** diff --git a/packages/tools/devtools/devtools/api-report/devtools.alpha.api.md b/packages/tools/devtools/devtools/api-report/devtools.alpha.api.md index 9425407e09bb..43a48ef78798 100644 --- a/packages/tools/devtools/devtools/api-report/devtools.alpha.api.md +++ b/packages/tools/devtools/devtools/api-report/devtools.alpha.api.md @@ -4,9 +4,9 @@ ```ts -// @beta +// @beta @sealed export interface ContainerDevtoolsProps extends HasContainerKey { - container: IFluidContainer; + readonly container: IFluidContainer; } // @beta @@ -15,18 +15,18 @@ export type ContainerKey = string; // @beta export function createDevtoolsLogger(baseLogger?: ITelemetryBaseLogger): IDevtoolsLogger; -// @beta +// @beta @sealed export interface DevtoolsProps { - initialContainers?: ContainerDevtoolsProps[]; - logger?: IDevtoolsLogger; + readonly initialContainers?: ContainerDevtoolsProps[]; + readonly logger?: IDevtoolsLogger; } -// @beta +// @beta @sealed export interface HasContainerKey { - containerKey: ContainerKey; + readonly containerKey: ContainerKey; } -// @beta +// @beta @sealed export interface IDevtools extends IDisposable { closeContainerDevtools(id: string): void; registerContainerDevtools(props: ContainerDevtoolsProps): void; diff --git a/packages/tools/devtools/devtools/api-report/devtools.beta.api.md b/packages/tools/devtools/devtools/api-report/devtools.beta.api.md index 937707523dff..ac13928f9b02 100644 --- a/packages/tools/devtools/devtools/api-report/devtools.beta.api.md +++ b/packages/tools/devtools/devtools/api-report/devtools.beta.api.md @@ -4,9 +4,9 @@ ```ts -// @beta +// @beta @sealed export interface ContainerDevtoolsProps extends HasContainerKey { - container: IFluidContainer; + readonly container: IFluidContainer; } // @beta @@ -15,18 +15,18 @@ export type ContainerKey = string; // @beta export function createDevtoolsLogger(baseLogger?: ITelemetryBaseLogger): IDevtoolsLogger; -// @beta +// @beta @sealed export interface DevtoolsProps { - initialContainers?: ContainerDevtoolsProps[]; - logger?: IDevtoolsLogger; + readonly initialContainers?: ContainerDevtoolsProps[]; + readonly logger?: IDevtoolsLogger; } -// @beta +// @beta @sealed export interface HasContainerKey { - containerKey: ContainerKey; + readonly containerKey: ContainerKey; } -// @beta +// @beta @sealed export interface IDevtools extends IDisposable { closeContainerDevtools(id: string): void; registerContainerDevtools(props: ContainerDevtoolsProps): void; diff --git a/packages/tools/devtools/devtools/src/index.ts b/packages/tools/devtools/devtools/src/index.ts index 2f908cc6db1e..24729f963252 100644 --- a/packages/tools/devtools/devtools/src/index.ts +++ b/packages/tools/devtools/devtools/src/index.ts @@ -34,6 +34,8 @@ import { isInternalFluidContainer } from "@fluidframework/fluid-static/internal" /** * Properties for configuring {@link IDevtools}. + * + * @sealed * @beta */ export interface DevtoolsProps { @@ -47,27 +49,29 @@ export interface DevtoolsProps { * This is provided to the Devtools instance strictly to enable communicating supported / desired functionality with * external listeners. */ - logger?: IDevtoolsLogger; + readonly logger?: IDevtoolsLogger; /** * (optional) List of Containers to initialize the devtools with. * * @remarks Additional Containers can be registered with the Devtools via {@link IDevtools.registerContainerDevtools}. */ - initialContainers?: ContainerDevtoolsProps[]; + readonly initialContainers?: ContainerDevtoolsProps[]; // TODO: Add ability for customers to specify custom data visualizer overrides } /** * Properties for configuring Devtools for an individual {@link @fluidframework/fluid-static#IFluidContainer}. + * + * @sealed * @beta */ export interface ContainerDevtoolsProps extends HasContainerKey { /** * The Container to register with the Devtools. */ - container: IFluidContainer; + readonly container: IFluidContainer; // TODO: Add ability for customers to specify custom data visualizer overrides } @@ -84,6 +88,8 @@ export interface ContainerDevtoolsProps extends HasContainerKey { * The lifetime of the associated singleton is bound by that of the Window (globalThis), and it will be automatically * disposed of on Window unload. * If you wish to dispose of it earlier, you may call its {@link @fluidframework/core-interfaces#IDisposable.dispose} method. + * + * @sealed * @beta */ export interface IDevtools extends IDisposable { @@ -145,6 +151,7 @@ class Devtools implements IDevtools { * Initializes the Devtools singleton and returns a handle to it. * * @see {@link @fluidframework/devtools-core#initializeDevtoolsBase} + * * @beta */ export function initializeDevtools(props: DevtoolsProps): IDevtools { From 9a3fa55d9b9cff37f2298b46318de3e45ffc49d6 Mon Sep 17 00:00:00 2001 From: Mark Fields Date: Thu, 21 Nov 2024 17:56:37 -0800 Subject: [PATCH 07/40] Flaky Test: Small adjustment to observe test behavior in CI (#23178) The test is failing a lot against FRS, always at checking `counter1`. Next sprint I'll take time to repro locally, but for now I'm reordering the asserts to see if Containers 2/3 are also in bad shape. --- .../src/test/stashedOps.spec.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/test/test-end-to-end-tests/src/test/stashedOps.spec.ts b/packages/test/test-end-to-end-tests/src/test/stashedOps.spec.ts index fcf58276fea1..e8f801186cdb 100644 --- a/packages/test/test-end-to-end-tests/src/test/stashedOps.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/stashedOps.spec.ts @@ -2296,14 +2296,9 @@ describeCompat( // ContainerRuntime will use PSM and BatchTracker and it will play out like this: // - One will win the race and get their op sequenced first. // - Then the other will close with Forked Container Error when it sees that ack - with matching batchId but from a different client - // - All other clients (including the winner) will be tracking the batchId, and when it sees the duplicate from the loser, it will ignore it. + // - Each other client (including the winner) will be tracking the batchId, and when it sees the duplicate from the loser, it will close. await provider.ensureSynchronized(); - // Container1 is not used directly in this test, but is present and observing the session, - // so we can double-check eventual consistency - the container should have closed and the op should not have been duplicated - assert(container1.closed, "container1 should be closed"); - assert.strictEqual(counter1.value, incrementValue); - // Both containers will close with the correct value for the counter. // The container whose op is sequenced first will close with "Duplicate batch" error // when it sees the other container's batch come in. @@ -2311,8 +2306,25 @@ describeCompat( // when it sees the winner's batch come in. assert(container2.closed, "container2 should be closed"); assert(container3.closed, "container3 should be closed"); - assert.strictEqual(counter2.value, incrementValue); - assert.strictEqual(counter3.value, incrementValue); + assert.strictEqual( + counter2.value, + incrementValue, + "container2 should have incremented to 3 (at least locally)", + ); + assert.strictEqual( + counter3.value, + incrementValue, + "container3 should have incremented to 3 (at least locally)", + ); + + // Container1 is not used directly in this test, but is present and observing the session, + // so we can double-check eventual consistency - the container should have closed when processing the duplicate (after applying the first) + assert(container1.closed, "container1 should be closed"); + assert.strictEqual( + counter1.value, + incrementValue, + "container1 should have incremented to 3 before closing", + ); }, ); From f1c1c31b067976395362edd27e1da8e8ce8e51a7 Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:16:27 -0800 Subject: [PATCH 08/40] Add driver type information in service-client telemetry (#22964) This change adds `driver`, `driverEndpointName` and `testVariant` information to service-client telemetry. Rest of the field such as `clientType`, `loaderId` and `containerId` are not displayed as part of `Test_start` and `Test_end` event name [Execute]( https://dataexplorer.azure.com/clusters/kusto.aria.microsoft.com/databases/742fa5a288b045e5beab1a2b8e445a71?query=H4sIAAAAAAAAA1WPsU7DQAyG9z6FdRMgUItg4AHokIUpYj25dz5yKLEjn9OqEg%2FPoSal2Wz%2F3%2B%2FfnjgLg6SUA%2FnUTzn6lHAyGdCq4h82P3DqSAne0dAfptzHJkJmuHMvu7fX5527XyNGxT5wIOiwgAvILAZohqEDhCBsmJkU7FQj3Wa7hZWdjsQ3%2FrZu83uOroaIxuo7nGH%2FxzScxLe5glhCVUeVbwr2pHTh1tDj%2F3GfqBnZ5knUfCRtzyOtBjVylHy5ZBZCn%2BvGG7IXrEFNXPTltevk%2BsvcD1QKfi0dqYrOtRIW4V%2BVtJA1jQEAAA%3D%3D) union office_fluid_ffautomation_* | where Data_buildId in ("308410") | where Data_testName has "cannot attach a container twice" // | where Data_eventName has "Test_End" | order by EventInfo_Time asc | project-reorder EventInfo_Time, Data_testVariant, Data_driverType, Data_driverEndpointName, Data_clientType, Data_loaderId, Data_containerId, Data_eventName, Data_message, Data_error, Data_reason The `Test_End` event lacks `Data_driverType` and `Data_driverEndpointName` field, which are present in other events. Test run: https://dev.azure.com/fluidframework/internal/_build/results?buildId=308410&view=results [ADO Work Item](https://dev.azure.com/fluidframework/internal/_workitems/edit/8762) --- .../azure-client/package.json | 6 ++-- .../azure-client/src/test/.mocharc.cjs | 3 +- .../src/test/AzureClientFactory.ts | 28 +++++++++++++++-- .../end-to-end-tests/odsp-client/package.json | 5 ++-- .../odsp-client/{ => src/test}/.mocharc.cjs | 6 ++-- .../odsp-client/src/test/OdspClientFactory.ts | 30 +++++++++++++++++-- pnpm-lock.yaml | 6 ++++ 7 files changed, 72 insertions(+), 12 deletions(-) rename packages/service-clients/end-to-end-tests/odsp-client/{ => src/test}/.mocharc.cjs (61%) diff --git a/packages/service-clients/end-to-end-tests/azure-client/package.json b/packages/service-clients/end-to-end-tests/azure-client/package.json index a61dde7cf2a5..44a6cb856384 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/package.json +++ b/packages/service-clients/end-to-end-tests/azure-client/package.json @@ -31,11 +31,12 @@ "test": "npm run test:realsvc", "test:coverage": "c8 npm test", "test:realsvc": "npm run test:realsvc:tinylicious", - "test:realsvc:azure": "cross-env FLUID_CLIENT=azure npm run test:realsvc:azure:run", + "test:realsvc:azure": "cross-env FLUID_CLIENT=azure npm run test:realsvc:azure:run -- --driver=r11s --r11sEndpointName=frs", "test:realsvc:azure:run": "mocha --recursive \"lib/test/**/*.spec.*js\" --exit --timeout 20000 --config src/test/.mocharc.cjs", "test:realsvc:run": "mocha lib/test --config src/test/.mocharc.cjs", - "test:realsvc:tinylicious": "start-server-and-test start:tinylicious:test 7071 test:realsvc:azure:run", + "test:realsvc:tinylicious": "start-server-and-test start:tinylicious:test 7071 test:realsvc:tinylicious:run", "test:realsvc:tinylicious:report": "npm run test:realsvc:tinylicious", + "test:realsvc:tinylicious:run": "npm run test:realsvc:azure:run -- --driver=t9s", "test:realsvc:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:realsvc" }, "c8": { @@ -62,6 +63,7 @@ "@fluid-experimental/data-objects": "workspace:~", "@fluid-internal/client-utils": "workspace:~", "@fluid-internal/mocha-test-setup": "workspace:~", + "@fluid-private/test-version-utils": "workspace:~", "@fluidframework/aqueduct": "workspace:~", "@fluidframework/azure-client": "workspace:~", "@fluidframework/azure-client-legacy": "npm:@fluidframework/azure-client@^1.2.0", diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/.mocharc.cjs b/packages/service-clients/end-to-end-tests/azure-client/src/test/.mocharc.cjs index b8d46952ff9a..449ced77e6ec 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/.mocharc.cjs +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/.mocharc.cjs @@ -6,6 +6,7 @@ "use strict"; const packageDir = `${__dirname}/../..`; -const getFluidTestMochaConfig = require("@fluid-internal/mocha-test-setup/mocharc-common"); +const getFluidTestMochaConfig = require("@fluid-private/test-version-utils/mocharc-common"); const config = getFluidTestMochaConfig(packageDir); + module.exports = config; diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/AzureClientFactory.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/AzureClientFactory.ts index 46ae0e85e5d3..5ce448e91574 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/AzureClientFactory.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/AzureClientFactory.ts @@ -17,7 +17,11 @@ import { ITelemetryBaseLogger as ITelemetryBaseLoggerLegacy, } from "@fluidframework/azure-client-legacy"; import { IConfigProviderBase } from "@fluidframework/core-interfaces"; -import { MockLogger, createMultiSinkLogger } from "@fluidframework/telemetry-utils/internal"; +import { + MockLogger, + createChildLogger, + createMultiSinkLogger, +} from "@fluidframework/telemetry-utils/internal"; import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal"; import { default as Axios, AxiosResponse, type AxiosRequestConfig } from "axios"; import { v4 as uuid } from "uuid"; @@ -35,6 +39,16 @@ export function createAzureClient( configProvider?: IConfigProviderBase, scopes?: ScopeType[], ): AzureClient { + const args = process.argv.slice(2); + + const driverIndex = args.indexOf("--driver"); + const r11sEndpointNameIndex = args.indexOf("--r11sEndpointName"); + + // Get values associated with the flags + const driver = driverIndex === -1 ? undefined : args[driverIndex + 1]; + const r11sEndpointName = + r11sEndpointNameIndex === -1 ? undefined : args[r11sEndpointNameIndex + 1]; + const useAzure = process.env.FLUID_CLIENT === "azure"; const tenantId = useAzure ? (process.env.azure__fluid__relay__service__tenantId as string) @@ -73,9 +87,19 @@ export function createAzureClient( } return logger ?? testLogger; }; + + const createLogger = createChildLogger({ + logger: getLogger(), + properties: { + all: { + driverType: useAzure ? r11sEndpointName : driver, + driverEndpointName: driver, + }, + }, + }); return new AzureClient({ connection: connectionProps, - logger: getLogger(), + logger: createLogger, configProvider, }); } diff --git a/packages/service-clients/end-to-end-tests/odsp-client/package.json b/packages/service-clients/end-to-end-tests/odsp-client/package.json index 18ec2fe99771..3db1b908b8e4 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/package.json +++ b/packages/service-clients/end-to-end-tests/odsp-client/package.json @@ -29,8 +29,8 @@ "lint:fix": "fluid-build . --task eslint:fix --task format", "test": "npm run test:realsvc:odsp:run", "test:coverage": "c8 npm test", - "test:realsvc:odsp": "cross-env npm run test:realsvc:odsp:run", - "test:realsvc:odsp:run": "mocha --recursive \"lib/test/**/*.spec.*js\" --exit --timeout 20000", + "test:realsvc:odsp": "cross-env npm run test:realsvc:odsp:run -- --driver=odsp --odspEndpointName=odsp", + "test:realsvc:odsp:run": "mocha --recursive \"lib/test/**/*.spec.*js\" --exit --timeout 20000 --config src/test/.mocharc.cjs", "test:realsvc:run": "mocha --recursive \"lib/test/**/*.spec.*js\"", "test:realsvc:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:realsvc" }, @@ -56,6 +56,7 @@ }, "dependencies": { "@fluid-internal/mocha-test-setup": "workspace:~", + "@fluid-private/test-version-utils": "workspace:~", "@fluidframework/aqueduct": "workspace:~", "@fluidframework/container-definitions": "workspace:~", "@fluidframework/container-loader": "workspace:~", diff --git a/packages/service-clients/end-to-end-tests/odsp-client/.mocharc.cjs b/packages/service-clients/end-to-end-tests/odsp-client/src/test/.mocharc.cjs similarity index 61% rename from packages/service-clients/end-to-end-tests/odsp-client/.mocharc.cjs rename to packages/service-clients/end-to-end-tests/odsp-client/src/test/.mocharc.cjs index cddbf0e44d55..449ced77e6ec 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/.mocharc.cjs +++ b/packages/service-clients/end-to-end-tests/odsp-client/src/test/.mocharc.cjs @@ -5,8 +5,8 @@ "use strict"; -const getFluidTestMochaConfig = require("@fluid-internal/mocha-test-setup/mocharc-common"); - -const packageDir = __dirname; +const packageDir = `${__dirname}/../..`; +const getFluidTestMochaConfig = require("@fluid-private/test-version-utils/mocharc-common"); const config = getFluidTestMochaConfig(packageDir); + module.exports = config; diff --git a/packages/service-clients/end-to-end-tests/odsp-client/src/test/OdspClientFactory.ts b/packages/service-clients/end-to-end-tests/odsp-client/src/test/OdspClientFactory.ts index 82964ed939c9..d9da4750057f 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/src/test/OdspClientFactory.ts +++ b/packages/service-clients/end-to-end-tests/odsp-client/src/test/OdspClientFactory.ts @@ -8,7 +8,11 @@ import { type ITelemetryBaseLogger, } from "@fluidframework/core-interfaces"; import { OdspClient, OdspConnectionConfig } from "@fluidframework/odsp-client/internal"; -import { MockLogger, createMultiSinkLogger } from "@fluidframework/telemetry-utils/internal"; +import { + MockLogger, + createChildLogger, + createMultiSinkLogger, +} from "@fluidframework/telemetry-utils/internal"; import { OdspTestTokenProvider } from "./OdspTokenFactory.js"; @@ -128,6 +132,16 @@ export function createOdspClient( throw new Error("client id is missing"); } + const args = process.argv.slice(2); + + const driverIndex = args.indexOf("--driver"); + const odspEndpointNameIndex = args.indexOf("--odspEndpointName"); + + // Get values associated with the flags + const driverType = driverIndex === -1 ? undefined : args[driverIndex + 1]; + const driverEndpointName = + odspEndpointNameIndex === -1 ? undefined : args[odspEndpointNameIndex + 1]; + const credentials: IOdspCredentials = { clientId, ...creds, @@ -139,6 +153,7 @@ export function createOdspClient( driveId, filePath: "", }; + const getLogger = (): ITelemetryBaseLogger | undefined => { const testLogger = getTestLogger?.(); if (!logger && !testLogger) { @@ -149,9 +164,20 @@ export function createOdspClient( } return logger ?? testLogger; }; + + const createLogger = createChildLogger({ + logger: getLogger(), + properties: { + all: { + driverType, + driverEndpointName, + }, + }, + }); + return new OdspClient({ connection: connectionProps, - logger: getLogger(), + logger: createLogger, configProvider, }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1943c8c0d514..8cd489ca9d35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13007,6 +13007,9 @@ importers: '@fluid-internal/mocha-test-setup': specifier: workspace:~ version: link:../../../test/mocha-test-setup + '@fluid-private/test-version-utils': + specifier: workspace:~ + version: link:../../../test/test-version-utils '@fluidframework/aqueduct': specifier: workspace:~ version: link:../../../framework/aqueduct @@ -13143,6 +13146,9 @@ importers: '@fluid-internal/mocha-test-setup': specifier: workspace:~ version: link:../../../test/mocha-test-setup + '@fluid-private/test-version-utils': + specifier: workspace:~ + version: link:../../../test/test-version-utils '@fluidframework/aqueduct': specifier: workspace:~ version: link:../../../framework/aqueduct From cae07b5c8c7904184b5fbf8c677f302da19cc697 Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:14:38 -0800 Subject: [PATCH 09/40] Move events library to core-interfaces and client-utils (#23141) This change relocates event library from SharedTree to client-utils and core-interfaces. Old PR: https://github.com/microsoft/FluidFramework/pull/23037 [ADO#6595](https://dev.azure.com/fluidframework/internal/_workitems/edit/6595) --- .changeset/new-hats-learn.md | 11 ++ .../client-utils}/src/events/README.md | 0 .../client-utils}/src/events/emitter.ts | 94 ++++------- .../common/client-utils/src/events/index.ts | 6 + .../common/client-utils/src/indexBrowser.ts | 2 + packages/common/client-utils/src/indexNode.ts | 2 + .../src/test/mocha}/events.spec.ts | 155 ++++++++++++------ .../api-report/core-interfaces.beta.api.md | 17 ++ .../core-interfaces.legacy.alpha.api.md | 17 ++ .../core-interfaces.legacy.public.api.md | 17 ++ .../api-report/core-interfaces.public.api.md | 17 ++ packages/common/core-interfaces/src/events.ts | 1 - .../core-interfaces/src/events/README.md | 3 + .../core-interfaces/src/events/emitter.ts | 73 +++++++++ .../core-interfaces/src/events/index.ts | 18 ++ .../core-interfaces}/src/events/listeners.ts | 0 packages/common/core-interfaces/src/index.ts | 11 ++ .../dds/tree/api-report/tree.alpha.api.md | 17 +- packages/dds/tree/api-report/tree.beta.api.md | 17 +- .../tree/api-report/tree.legacy.alpha.api.md | 17 +- .../tree/api-report/tree.legacy.public.api.md | 17 +- .../dds/tree/api-report/tree.public.api.md | 17 +- packages/dds/tree/src/core/forest/forest.ts | 2 +- .../schema-stored/storedSchemaRepository.ts | 3 +- packages/dds/tree/src/core/tree/anchorSet.ts | 3 +- packages/dds/tree/src/events/index.ts | 19 --- packages/dds/tree/src/events/interop.ts | 38 ----- .../chunked-forest/chunkedForest.ts | 3 +- .../feature-libraries/flex-tree/context.ts | 2 +- .../object-forest/objectForest.ts | 3 +- packages/dds/tree/src/index.ts | 12 +- .../dds/tree/src/shared-tree-core/branch.ts | 3 +- .../tree/src/shared-tree-core/editManager.ts | 2 +- .../src/shared-tree/schematizingTreeView.ts | 12 +- .../dds/tree/src/shared-tree/sharedTree.ts | 13 +- .../dds/tree/src/shared-tree/treeCheckout.ts | 12 +- packages/dds/tree/src/simple-tree/api/tree.ts | 3 +- .../tree/src/simple-tree/api/treeNodeApi.ts | 2 +- .../src/simple-tree/core/treeNodeKernel.ts | 3 +- .../simple-tree/core/unhydratedFlexTree.ts | 3 +- .../test/shared-tree/schematizeTree.spec.ts | 2 +- packages/dds/tree/src/test/utils.ts | 6 +- 42 files changed, 403 insertions(+), 272 deletions(-) create mode 100644 .changeset/new-hats-learn.md rename packages/{dds/tree => common/client-utils}/src/events/README.md (100%) rename packages/{dds/tree => common/client-utils}/src/events/emitter.ts (65%) create mode 100644 packages/common/client-utils/src/events/index.ts rename packages/{dds/tree/src/test/events => common/client-utils/src/test/mocha}/events.spec.ts (75%) create mode 100644 packages/common/core-interfaces/src/events/README.md create mode 100644 packages/common/core-interfaces/src/events/emitter.ts create mode 100644 packages/common/core-interfaces/src/events/index.ts rename packages/{dds/tree => common/core-interfaces}/src/events/listeners.ts (100%) delete mode 100644 packages/dds/tree/src/events/index.ts delete mode 100644 packages/dds/tree/src/events/interop.ts diff --git a/.changeset/new-hats-learn.md b/.changeset/new-hats-learn.md new file mode 100644 index 000000000000..da3274c3073f --- /dev/null +++ b/.changeset/new-hats-learn.md @@ -0,0 +1,11 @@ +--- +"@fluidframework/core-interfaces": minor +"@fluidframework/tree": minor +--- +--- +"section": other +--- + +Relocating Events Library to `@fluidframework/core-interfaces` and `@fluid-internal/client-utils` + +The events library's types and interfaces are moved to `@fluidframework/core-interfaces`, while its implementation is relocated to `@fluid-internal/client-utils`. There are no changes to how the events library is used; the relocation simply organizes the library into more appropriate packages. This change has no impact on external consumers of Fluid. diff --git a/packages/dds/tree/src/events/README.md b/packages/common/client-utils/src/events/README.md similarity index 100% rename from packages/dds/tree/src/events/README.md rename to packages/common/client-utils/src/events/README.md diff --git a/packages/dds/tree/src/events/emitter.ts b/packages/common/client-utils/src/events/emitter.ts similarity index 65% rename from packages/dds/tree/src/events/emitter.ts rename to packages/common/client-utils/src/events/emitter.ts index f352b374a79d..9bb016ae9ea3 100644 --- a/packages/dds/tree/src/events/emitter.ts +++ b/packages/common/client-utils/src/events/emitter.ts @@ -3,68 +3,37 @@ * Licensed under the MIT License. */ -import { UsageError } from "@fluidframework/telemetry-utils/internal"; -import { getOrCreate } from "../util/index.js"; -import type { Listenable, Listeners, Off } from "./listeners.js"; +import type { + HasListeners, + IEmitter, + Listenable, + Listeners, + MapGetSet, + NoListenersCallback, + Off, +} from "@fluidframework/core-interfaces/internal"; /** - * Interface for an event emitter that can emit typed events to subscribed listeners. + * Retrieve a value from a map with the given key, or create a new entry if the key is not in the map. + * @param map - The map to query/update + * @param key - The key to lookup in the map + * @param defaultValue - a function which returns a default value. This is called and used to set an initial value for the given key in the map if none exists + * @returns either the existing value for the given key, or the newly-created value (the result of `defaultValue`) + * @internal */ -export interface IEmitter> { - /** - * Emits an event with the specified name and arguments, notifying all subscribers by calling their registered listener functions. - * @param eventName - the name of the event to fire - * @param args - the arguments passed to the event listener functions - */ - emit>( - eventName: K, - ...args: Parameters - ): void; - - /** - * Emits an event with the specified name and arguments, notifying all subscribers by calling their registered listener functions. - * It also collects the return values of all listeners into an array. - * - * Warning: This method should be used with caution. It deviates from the standard event-based integration pattern as creates substantial coupling between the emitter and its listeners. - * For the majority of use-cases it is recommended to use the standard {@link IEmitter.emit} functionality. - * @param eventName - the name of the event to fire - * @param args - the arguments passed to the event listener functions - * @returns An array of the return values of each listener, preserving the order listeners were called. - */ - emitAndCollect>( - eventName: K, - ...args: Parameters - ): ReturnType[]; -} - -/** - * Called when the last listener for `eventName` is removed. - * Useful for determining when to clean up resources related to detecting when the event might occurs. - */ -export type NoListenersCallback = ( - eventName: keyof Listeners, -) => void; - -/** - * Allows querying if an object has listeners. - * @sealed - */ -export interface HasListeners> { - /** - * When no `eventName` is provided, returns true iff there are any listeners. - * - * When `eventName` is provided, returns true iff there are listeners for that event. - * - * @remarks - * This can be used to know when its safe to cleanup data-structures which only exist to fire events for their listeners. - */ - hasListeners(eventName?: keyof Listeners): boolean; +function getOrCreate(map: MapGetSet, key: K, defaultValue: (key: K) => V): V { + let value = map.get(key); + if (value === undefined) { + value = defaultValue(key); + map.set(key, value); + } + return value; } /** * Provides an API for subscribing to and listening to events. * - * @remarks Classes wishing to emit events may either extend this class, compose over it, or expose it as a property of type {@link Listenable}. + * @remarks Classes wishing to emit events may either extend this class, compose over it, or expose it as a property of type {@link @fluidframework/core-interfaces#Listenable}. * * @example Extending this class * @@ -112,8 +81,9 @@ export interface HasListeners> { * } * } * ``` + * @internal */ -export class EventEmitter> +export class CustomEventEmitter> implements Listenable, HasListeners { protected readonly listeners = new Map< @@ -168,7 +138,7 @@ export class EventEmitter> const eventDescription = typeof eventName === "symbol" ? eventName.description : String(eventName.toString()); - throw new UsageError( + throw new Error( `Attempted to register the same listener object twice for event ${eventDescription}`, ); } @@ -189,7 +159,7 @@ export class EventEmitter> public hasListeners(eventName?: keyof TListeners): boolean { if (eventName === undefined) { - return this.listeners.size !== 0; + return this.listeners.size > 0; } return this.listeners.has(eventName); } @@ -197,9 +167,10 @@ export class EventEmitter> /** * This class exposes the constructor and the `emit` method of `EventEmitter`, elevating them from protected to public + * @internal */ class ComposableEventEmitter> - extends EventEmitter + extends CustomEventEmitter implements IEmitter { public constructor(noListeners?: NoListenersCallback) { @@ -222,10 +193,10 @@ class ComposableEventEmitter> } /** - * Create a {@link Listenable} that can be instructed to emit events via the {@link IEmitter} interface. + * Create a {@link @fluidframework/core-interfaces#Listenable} that can be instructed to emit events via the {@link @fluidframework/core-interfaces#IEmitter} interface. * - * A class can delegate handling {@link Listenable} to the returned value while using it to emit the events. - * See also {@link EventEmitter} which be used as a base class to implement {@link Listenable} via extension. + * A class can delegate handling {@link @fluidframework/core-interfaces#Listenable} to the returned value while using it to emit the events. + * See also CustomEventEmitter which be used as a base class to implement {@link @fluidframework/core-interfaces#Listenable} via extension. * @example Forwarding events to the emitter * ```typescript * interface MyEvents { @@ -248,6 +219,7 @@ class ComposableEventEmitter> * } * } * ``` + * @internal */ export function createEmitter( noListeners?: NoListenersCallback, diff --git a/packages/common/client-utils/src/events/index.ts b/packages/common/client-utils/src/events/index.ts new file mode 100644 index 000000000000..619a687f58f5 --- /dev/null +++ b/packages/common/client-utils/src/events/index.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export { createEmitter, CustomEventEmitter } from "./emitter.js"; diff --git a/packages/common/client-utils/src/indexBrowser.ts b/packages/common/client-utils/src/indexBrowser.ts index 758d66555e7c..5df89eb16ddc 100644 --- a/packages/common/client-utils/src/indexBrowser.ts +++ b/packages/common/client-utils/src/indexBrowser.ts @@ -26,3 +26,5 @@ export { TypedEventEmitter, type TypedEventTransform, } from "./typedEventEmitter.js"; + +export { createEmitter } from "./events/index.js"; diff --git a/packages/common/client-utils/src/indexNode.ts b/packages/common/client-utils/src/indexNode.ts index b9ee1e7d5088..d086833f428d 100644 --- a/packages/common/client-utils/src/indexNode.ts +++ b/packages/common/client-utils/src/indexNode.ts @@ -26,3 +26,5 @@ export { TypedEventEmitter, type TypedEventTransform, } from "./typedEventEmitter.js"; + +export { createEmitter } from "./events/index.js"; diff --git a/packages/dds/tree/src/test/events/events.spec.ts b/packages/common/client-utils/src/test/mocha/events.spec.ts similarity index 75% rename from packages/dds/tree/src/test/events/events.spec.ts rename to packages/common/client-utils/src/test/mocha/events.spec.ts index ca1b31bc3a08..14f67706c732 100644 --- a/packages/dds/tree/src/test/events/events.spec.ts +++ b/packages/common/client-utils/src/test/mocha/events.spec.ts @@ -5,14 +5,9 @@ import { strict as assert } from "node:assert"; -import { validateAssertionError } from "@fluidframework/test-runtime-utils/internal"; +import type { Listenable } from "@fluidframework/core-interfaces/internal"; -import { - EventEmitter, - createEmitter, - // eslint-disable-next-line import/no-internal-modules -} from "../../events/emitter.js"; -import type { Listenable } from "../../events/index.js"; +import { CustomEventEmitter, createEmitter } from "../../events/index.js"; interface TestEvents { open: () => void; @@ -20,7 +15,7 @@ interface TestEvents { compute: (input: string) => string; } -describe("EventEmitter", () => { +describe("CustomEventEmitter", () => { it("emits events", () => { const emitter = createEmitter(); const log: string[] = []; @@ -31,8 +26,8 @@ describe("EventEmitter", () => { it("emits events and collects their results", () => { const emitter = createEmitter(); - const listener1 = (arg: string) => arg.toUpperCase(); - const listener2 = (arg: string) => arg.toLowerCase(); + const listener1 = (arg: string): string => arg.toUpperCase(); + const listener2 = (arg: string): string => arg.toLowerCase(); emitter.on("compute", listener1); emitter.on("compute", listener2); const results = emitter.emitAndCollect("compute", "hello"); @@ -85,7 +80,7 @@ describe("EventEmitter", () => { it("deregisters events via off", () => { const emitter = createEmitter(); let error = false; - const listener = (e: boolean) => (error = e); + const listener = (e: boolean): boolean => (error = e); emitter.on("close", listener); emitter.off("close", listener); emitter.emit("close", true); @@ -112,8 +107,8 @@ describe("EventEmitter", () => { const emitter = createEmitter(); let opened = false; let closed = false; - const listenerOpen = () => (opened = true); - const listenerClosed = () => (closed = true); + const listenerOpen = (): boolean => (opened = true); + const listenerClosed = (): boolean => (closed = true); emitter.on("open", listenerOpen); emitter.on("close", listenerClosed); emitter.off("open", listenerOpen); @@ -129,7 +124,7 @@ describe("EventEmitter", () => { it("correctly handles multiple registrations for the same event", () => { const emitter = createEmitter(); let count: number; - const listener = () => (count += 1); + const listener = (): number => (count += 1); const off1 = emitter.on("open", listener); const off2 = emitter.on("open", () => listener()); @@ -148,37 +143,10 @@ describe("EventEmitter", () => { assert.strictEqual(count, 0); }); - it("errors on multiple registrations of the same listener", () => { - const emitter = createEmitter(); - let count = 0; - const listener = () => (count += 1); - emitter.on("open", listener); - assert.throws( - () => emitter.on("open", listener), - (e: Error) => validateAssertionError(e, /register.*twice.*open/), - ); - // If error is caught, the listener should still fire once for the first registration - emitter.emit("open"); - assert.strictEqual(count, 1); - }); - - it("includes symbol description in the error message on multiple registrations of the same listener", () => { - // This test ensures that symbol types are registered, error on double registration, and include the description of the symbol in the error message. - const eventSymbol = Symbol("TestEvent"); - const emitter = createEmitter<{ [eventSymbol]: () => void }>(); - const listener = () => {}; - emitter.on(eventSymbol, listener); - emitter.emit(eventSymbol); - assert.throws( - () => emitter.on(eventSymbol, listener), - (e: Error) => validateAssertionError(e, /register.*twice.*TestEvent/), - ); - }); - it("allows repeat deregistrations", () => { const emitter = createEmitter(); const deregister = emitter.on("open", () => {}); - const listenerB = () => {}; + const listenerB = (): void => {}; emitter.on("open", listenerB); deregister(); deregister(); @@ -284,28 +252,82 @@ describe("EventEmitter", () => { emitter.emit("open"); assert.deepEqual(log, ["A1", "B", "A2"]); }); + + it("errors on multiple registrations of the same listener", () => { + const emitter = createEmitter(); + let count = 0; + const listener = (): number => (count += 1); + emitter.on("open", listener); + assert.throws( + () => emitter.on("open", listener), + (e: Error) => validateAssertionError(e, /register.*twice.*open/), + ); + // If error is caught, the listener should still fire once for the first registration + emitter.emit("open"); + assert.strictEqual(count, 1); + }); + + it("includes symbol description in the error message on multiple registrations of the same listener", () => { + // This test ensures that symbol types are registered, error on double registration, and include the description of the symbol in the error message. + const eventSymbol = Symbol("TestEvent"); + const emitter = createEmitter<{ [eventSymbol]: () => void }>(); + const listener = (): void => {}; + emitter.on(eventSymbol, listener); + emitter.emit(eventSymbol); + assert.throws( + () => emitter.on(eventSymbol, listener), + (e: Error) => validateAssertionError(e, /register.*twice.*TestEvent/), + ); + }); }); -// The below classes correspond to the examples given in the doc comment of `EventEmitter` to ensure that they compile +/** + * The below classes correspond to the examples given in {@link CustomEventEmitter} to ensure that they compile. + * + * Provides an API for subscribing to and listening to events. + * + * @remarks Classes wishing to emit events may either extend this class, compose over it, or expose it as a property of type {@link @fluidframework/core-interfaces#Listenable}. + * + * Note: These are for testing only and should not be re-exported. + */ +/** + * A set of events with their handlers. + */ interface MyEvents { loaded: () => void; computed: () => number; } -class MyInheritanceClass extends EventEmitter { - private load() { +/** + * Example of extending {@link CustomEventEmitter}. + */ +export class MyInheritanceClass extends CustomEventEmitter { + private load(): number[] { this.emit("loaded"); const results: number[] = this.emitAndCollect("computed"); + return results; + } + + public triggerLoad(): void { + this.load(); } } -class MyCompositionClass implements Listenable { +/** + * Example of composing over {@link CustomEventEmitter}. + */ +export class MyCompositionClass implements Listenable { private readonly events = createEmitter(); - private load() { + private load(): number[] { this.events.emit("loaded"); const results: number[] = this.events.emitAndCollect("computed"); + return results; + } + + public triggerLoad(): void { + this.load(); } public on(eventName: K, listener: MyEvents[K]): () => void { @@ -317,13 +339,48 @@ class MyCompositionClass implements Listenable { } } -class MyExposingClass { +/** + * Example of exposing {@link CustomEventEmitter} as a property + */ +export class MyExposingClass { private readonly _events = createEmitter(); public readonly events: Listenable = this._events; - private load() { + private load(): number[] { this._events.emit("loaded"); const results: number[] = this._events.emitAndCollect("computed"); + return results; + } + public triggerLoad(): void { + this.load(); + } +} + +/** + * Validates that an error thrown by assert() function has the expected message. + * + * @param error - The error object thrown by `assert()` function. + * @param expectedErrorMessage - The message that the error object should match. + * @returns `true` if the message in the error object that was passed in matches the expected + * message. Otherwise it throws an error. + * + * @remarks + * Similar to {@link @fluidframework/test-runtime-utils#validateAssertionError}. + * + * @internal + */ +function validateAssertionError(error: Error, expectedErrorMsg: string | RegExp): boolean { + const actualMsg = error.message; + if ( + typeof expectedErrorMsg === "string" + ? actualMsg !== expectedErrorMsg + : !expectedErrorMsg.test(actualMsg) + ) { + // This throws an Error instead of an AssertionError because AssertionError would require a dependency on the + // node assert library, which we don't want to do for this library because it's used in the browser. + const message = `Unexpected assertion thrown\nActual: ${error.message}\nExpected: ${expectedErrorMsg}`; + throw new Error(message); } + return true; } diff --git a/packages/common/core-interfaces/api-report/core-interfaces.beta.api.md b/packages/common/core-interfaces/api-report/core-interfaces.beta.api.md index 1847c672c174..5c391b055f63 100644 --- a/packages/common/core-interfaces/api-report/core-interfaces.beta.api.md +++ b/packages/common/core-interfaces/api-report/core-interfaces.beta.api.md @@ -286,6 +286,9 @@ export interface IResponse { value: any; } +// @public +export type IsListener = TListener extends (...args: any[]) => void ? true : false; + // @public export interface ITelemetryBaseEvent extends ITelemetryBaseProperties { // (undocumented) @@ -305,6 +308,17 @@ export interface ITelemetryBaseProperties { [index: string]: TelemetryBaseEventPropertyType | Tagged; } +// @public @sealed +export interface Listenable { + off>(eventName: K, listener: TListeners[K]): void; + on>(eventName: K, listener: TListeners[K]): Off; +} + +// @public +export type Listeners = { + [P in (string | symbol) & keyof T as IsListener extends true ? P : never]: T[P]; +}; + // @public export const LogLevel: { readonly verbose: 10; @@ -315,6 +329,9 @@ export const LogLevel: { // @public export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; +// @public +export type Off = () => void; + // @public export type ReplaceIEventThisPlaceHolder = L extends any[] ? { [K in keyof L]: L[K] extends IEventThisPlaceHolder ? TThis : L[K]; diff --git a/packages/common/core-interfaces/api-report/core-interfaces.legacy.alpha.api.md b/packages/common/core-interfaces/api-report/core-interfaces.legacy.alpha.api.md index 41bcd993d0de..8adbe20f8ea7 100644 --- a/packages/common/core-interfaces/api-report/core-interfaces.legacy.alpha.api.md +++ b/packages/common/core-interfaces/api-report/core-interfaces.legacy.alpha.api.md @@ -335,6 +335,9 @@ export interface IResponse { value: any; } +// @public +export type IsListener = TListener extends (...args: any[]) => void ? true : false; + // @public export interface ITelemetryBaseEvent extends ITelemetryBaseProperties { // (undocumented) @@ -361,6 +364,17 @@ export interface IThrottlingWarning extends IErrorBase { readonly retryAfterSeconds: number; } +// @public @sealed +export interface Listenable { + off>(eventName: K, listener: TListeners[K]): void; + on>(eventName: K, listener: TListeners[K]): Off; +} + +// @public +export type Listeners = { + [P in (string | symbol) & keyof T as IsListener extends true ? P : never]: T[P]; +}; + // @public export const LogLevel: { readonly verbose: 10; @@ -371,6 +385,9 @@ export const LogLevel: { // @public export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; +// @public +export type Off = () => void; + // @public export type ReplaceIEventThisPlaceHolder = L extends any[] ? { [K in keyof L]: L[K] extends IEventThisPlaceHolder ? TThis : L[K]; diff --git a/packages/common/core-interfaces/api-report/core-interfaces.legacy.public.api.md b/packages/common/core-interfaces/api-report/core-interfaces.legacy.public.api.md index 3ad0eb78bf7e..685fcc6228a1 100644 --- a/packages/common/core-interfaces/api-report/core-interfaces.legacy.public.api.md +++ b/packages/common/core-interfaces/api-report/core-interfaces.legacy.public.api.md @@ -286,6 +286,9 @@ export interface IResponse { value: any; } +// @public +export type IsListener = TListener extends (...args: any[]) => void ? true : false; + // @public export interface ITelemetryBaseEvent extends ITelemetryBaseProperties { // (undocumented) @@ -305,6 +308,17 @@ export interface ITelemetryBaseProperties { [index: string]: TelemetryBaseEventPropertyType | Tagged; } +// @public @sealed +export interface Listenable { + off>(eventName: K, listener: TListeners[K]): void; + on>(eventName: K, listener: TListeners[K]): Off; +} + +// @public +export type Listeners = { + [P in (string | symbol) & keyof T as IsListener extends true ? P : never]: T[P]; +}; + // @public export const LogLevel: { readonly verbose: 10; @@ -315,6 +329,9 @@ export const LogLevel: { // @public export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; +// @public +export type Off = () => void; + // @public export type ReplaceIEventThisPlaceHolder = L extends any[] ? { [K in keyof L]: L[K] extends IEventThisPlaceHolder ? TThis : L[K]; diff --git a/packages/common/core-interfaces/api-report/core-interfaces.public.api.md b/packages/common/core-interfaces/api-report/core-interfaces.public.api.md index 3ad0eb78bf7e..685fcc6228a1 100644 --- a/packages/common/core-interfaces/api-report/core-interfaces.public.api.md +++ b/packages/common/core-interfaces/api-report/core-interfaces.public.api.md @@ -286,6 +286,9 @@ export interface IResponse { value: any; } +// @public +export type IsListener = TListener extends (...args: any[]) => void ? true : false; + // @public export interface ITelemetryBaseEvent extends ITelemetryBaseProperties { // (undocumented) @@ -305,6 +308,17 @@ export interface ITelemetryBaseProperties { [index: string]: TelemetryBaseEventPropertyType | Tagged; } +// @public @sealed +export interface Listenable { + off>(eventName: K, listener: TListeners[K]): void; + on>(eventName: K, listener: TListeners[K]): Off; +} + +// @public +export type Listeners = { + [P in (string | symbol) & keyof T as IsListener extends true ? P : never]: T[P]; +}; + // @public export const LogLevel: { readonly verbose: 10; @@ -315,6 +329,9 @@ export const LogLevel: { // @public export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; +// @public +export type Off = () => void; + // @public export type ReplaceIEventThisPlaceHolder = L extends any[] ? { [K in keyof L]: L[K] extends IEventThisPlaceHolder ? TThis : L[K]; diff --git a/packages/common/core-interfaces/src/events.ts b/packages/common/core-interfaces/src/events.ts index be7f01758225..1b9496b2e223 100644 --- a/packages/common/core-interfaces/src/events.ts +++ b/packages/common/core-interfaces/src/events.ts @@ -18,7 +18,6 @@ export interface IEvent { * * @eventProperty */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any (event: string, listener: (...args: any[]) => void); } diff --git a/packages/common/core-interfaces/src/events/README.md b/packages/common/core-interfaces/src/events/README.md new file mode 100644 index 000000000000..6ef320726ae6 --- /dev/null +++ b/packages/common/core-interfaces/src/events/README.md @@ -0,0 +1,3 @@ +# events + +This module contains interfaces and helpers for objects that emit and subscribe to events. diff --git a/packages/common/core-interfaces/src/events/emitter.ts b/packages/common/core-interfaces/src/events/emitter.ts new file mode 100644 index 000000000000..6f85c3af1673 --- /dev/null +++ b/packages/common/core-interfaces/src/events/emitter.ts @@ -0,0 +1,73 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { Listeners } from "./listeners.js"; + +/** + * Interface for an event emitter that can emit typed events to subscribed listeners. + * @internal + */ +export interface IEmitter> { + /** + * Emits an event with the specified name and arguments, notifying all subscribers by calling their registered listener functions. + * @param eventName - The name of the event to fire + * @param args - The arguments passed to the event listener functions + */ + emit>( + eventName: K, + ...args: Parameters + ): void; + + /** + * Emits an event with the specified name and arguments, notifying all subscribers by calling their registered listener functions. + * It also collects the return values of all listeners into an array. + * + * @remarks + * Warning: This method should be used with caution. It deviates from the standard event-based integration pattern as creates substantial coupling between the emitter and its listeners. + * For the majority of use-cases it is recommended to use the standard {@link IEmitter.emit} functionality. + * @param eventName - The name of the event to fire + * @param args - The arguments passed to the event listener functions + * @returns An array of the return values of each listener, preserving the order listeners were called. + */ + emitAndCollect>( + eventName: K, + ...args: Parameters + ): ReturnType[]; +} + +/** + * Called when the last listener for a given `eventName` is removed. + * @remarks + * Useful for determining when to clean up resources related to detecting when the event might occurs. + * @internal + */ +export type NoListenersCallback = ( + eventName: keyof Listeners, +) => void; + +/** + * Allows querying if an object has listeners. + * @sealed + * @internal + */ +export interface HasListeners> { + /** + * Determines whether or not any listeners are registered for the specified event name. + * + * @remarks + * If no event name is given, checks if *any* listeners are registered. + * This can be used to know when its safe to cleanup data-structures which only exist to fire events for their listeners. + */ + hasListeners(eventName?: keyof Listeners): boolean; +} + +/** + * Subset of Map interface including only the `get` and `set` methods. + * @internal + */ +export interface MapGetSet { + get(key: K): V | undefined; + set(key: K, value: V): void; +} diff --git a/packages/common/core-interfaces/src/events/index.ts b/packages/common/core-interfaces/src/events/index.ts new file mode 100644 index 000000000000..5902b650bfa1 --- /dev/null +++ b/packages/common/core-interfaces/src/events/index.ts @@ -0,0 +1,18 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export type { + HasListeners, + IEmitter, + MapGetSet, + NoListenersCallback, +} from "./emitter.js"; + +export type { + IsListener, + Listeners, + Listenable, + Off, +} from "./listeners.js"; diff --git a/packages/dds/tree/src/events/listeners.ts b/packages/common/core-interfaces/src/events/listeners.ts similarity index 100% rename from packages/dds/tree/src/events/listeners.ts rename to packages/common/core-interfaces/src/events/listeners.ts diff --git a/packages/common/core-interfaces/src/index.ts b/packages/common/core-interfaces/src/index.ts index 8d2d89346f2d..06796dab9223 100644 --- a/packages/common/core-interfaces/src/index.ts +++ b/packages/common/core-interfaces/src/index.ts @@ -48,3 +48,14 @@ export type { FluidObjectProviderKeys, FluidObject, FluidObjectKeys } from "./pr export type { ConfigTypes, IConfigProviderBase } from "./config.js"; export type { ISignalEnvelope } from "./messages.js"; export type { ErasedType } from "./erasedType.js"; + +export type { + HasListeners, + IEmitter, + IsListener, + Listeners, + Listenable, + MapGetSet, + NoListenersCallback, + Off, +} from "./events/index.js"; diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 519f1c3f4c90..317d99ac7dbe 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -311,8 +311,7 @@ declare namespace InternalTypes { } export { InternalTypes } -// @public -export type IsListener = TListener extends (...args: any[]) => void ? true : false; +export { IsListener } // @alpha export type IsUnion = T extends unknown ? [T2] extends [T] ? false : true : "error"; @@ -415,16 +414,9 @@ export interface JsonValidator { // @public export type LazyItem = Item | (() => Item); -// @public @sealed -export interface Listenable { - off>(eventName: K, listener: TListeners[K]): void; - on>(eventName: K, listener: TListeners[K]): Off; -} +export { Listenable } -// @public -export type Listeners = { - [P in (string | symbol) & keyof T as IsListener extends true ? P : never]: T[P]; -}; +export { Listeners } // @public @sealed export interface MakeNominal { @@ -479,8 +471,7 @@ type ObjectFromSchemaRecordUnsafe; }; -// @public -export type Off = () => void; +export { Off } // @alpha export interface ParseOptions { diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 8b3bb6d072ba..d2b12ce272ef 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -197,8 +197,7 @@ declare namespace InternalTypes { } export { InternalTypes } -// @public -export type IsListener = TListener extends (...args: any[]) => void ? true : false; +export { IsListener } // @public @sealed export class IterableTreeArrayContent implements Iterable { @@ -223,16 +222,9 @@ export interface ITreeViewConfiguration = Item | (() => Item); -// @public @sealed -export interface Listenable { - off>(eventName: K, listener: TListeners[K]): void; - on>(eventName: K, listener: TListeners[K]): Off; -} +export { Listenable } -// @public -export type Listeners = { - [P in (string | symbol) & keyof T as IsListener extends true ? P : never]: T[P]; -}; +export { Listeners } // @public @sealed export interface MakeNominal { @@ -284,8 +276,7 @@ type ObjectFromSchemaRecordUnsafe; }; -// @public -export type Off = () => void; +export { Off } // @public @sealed export interface ReadonlyArrayNode extends ReadonlyArray, Awaited> { diff --git a/packages/dds/tree/api-report/tree.legacy.alpha.api.md b/packages/dds/tree/api-report/tree.legacy.alpha.api.md index a15683a9d7ac..184c22baab00 100644 --- a/packages/dds/tree/api-report/tree.legacy.alpha.api.md +++ b/packages/dds/tree/api-report/tree.legacy.alpha.api.md @@ -197,8 +197,7 @@ declare namespace InternalTypes { } export { InternalTypes } -// @public -export type IsListener = TListener extends (...args: any[]) => void ? true : false; +export { IsListener } // @public @sealed export class IterableTreeArrayContent implements Iterable { @@ -223,16 +222,9 @@ export interface ITreeViewConfiguration = Item | (() => Item); -// @public @sealed -export interface Listenable { - off>(eventName: K, listener: TListeners[K]): void; - on>(eventName: K, listener: TListeners[K]): Off; -} +export { Listenable } -// @public -export type Listeners = { - [P in (string | symbol) & keyof T as IsListener extends true ? P : never]: T[P]; -}; +export { Listeners } // @public @sealed export interface MakeNominal { @@ -279,8 +271,7 @@ type ObjectFromSchemaRecordUnsafe; }; -// @public -export type Off = () => void; +export { Off } // @public @sealed export interface ReadonlyArrayNode extends ReadonlyArray, Awaited> { diff --git a/packages/dds/tree/api-report/tree.legacy.public.api.md b/packages/dds/tree/api-report/tree.legacy.public.api.md index 24baf14953b1..1306818b234b 100644 --- a/packages/dds/tree/api-report/tree.legacy.public.api.md +++ b/packages/dds/tree/api-report/tree.legacy.public.api.md @@ -197,8 +197,7 @@ declare namespace InternalTypes { } export { InternalTypes } -// @public -export type IsListener = TListener extends (...args: any[]) => void ? true : false; +export { IsListener } // @public @sealed export class IterableTreeArrayContent implements Iterable { @@ -223,16 +222,9 @@ export interface ITreeViewConfiguration = Item | (() => Item); -// @public @sealed -export interface Listenable { - off>(eventName: K, listener: TListeners[K]): void; - on>(eventName: K, listener: TListeners[K]): Off; -} +export { Listenable } -// @public -export type Listeners = { - [P in (string | symbol) & keyof T as IsListener extends true ? P : never]: T[P]; -}; +export { Listeners } // @public @sealed export interface MakeNominal { @@ -279,8 +271,7 @@ type ObjectFromSchemaRecordUnsafe; }; -// @public -export type Off = () => void; +export { Off } // @public @sealed export interface ReadonlyArrayNode extends ReadonlyArray, Awaited> { diff --git a/packages/dds/tree/api-report/tree.public.api.md b/packages/dds/tree/api-report/tree.public.api.md index 24baf14953b1..1306818b234b 100644 --- a/packages/dds/tree/api-report/tree.public.api.md +++ b/packages/dds/tree/api-report/tree.public.api.md @@ -197,8 +197,7 @@ declare namespace InternalTypes { } export { InternalTypes } -// @public -export type IsListener = TListener extends (...args: any[]) => void ? true : false; +export { IsListener } // @public @sealed export class IterableTreeArrayContent implements Iterable { @@ -223,16 +222,9 @@ export interface ITreeViewConfiguration = Item | (() => Item); -// @public @sealed -export interface Listenable { - off>(eventName: K, listener: TListeners[K]): void; - on>(eventName: K, listener: TListeners[K]): Off; -} +export { Listenable } -// @public -export type Listeners = { - [P in (string | symbol) & keyof T as IsListener extends true ? P : never]: T[P]; -}; +export { Listeners } // @public @sealed export interface MakeNominal { @@ -279,8 +271,7 @@ type ObjectFromSchemaRecordUnsafe; }; -// @public -export type Off = () => void; +export { Off } // @public @sealed export interface ReadonlyArrayNode extends ReadonlyArray, Awaited> { diff --git a/packages/dds/tree/src/core/forest/forest.ts b/packages/dds/tree/src/core/forest/forest.ts index 403dceac2f00..ac4112a5dd0a 100644 --- a/packages/dds/tree/src/core/forest/forest.ts +++ b/packages/dds/tree/src/core/forest/forest.ts @@ -5,7 +5,7 @@ import { assert } from "@fluidframework/core-utils/internal"; -import type { Listenable } from "../../events/index.js"; +import type { Listenable } from "@fluidframework/core-interfaces/internal"; import type { FieldKey, TreeStoredSchemaSubscription } from "../schema-stored/index.js"; import { type Anchor, diff --git a/packages/dds/tree/src/core/schema-stored/storedSchemaRepository.ts b/packages/dds/tree/src/core/schema-stored/storedSchemaRepository.ts index 1081a4ec15c0..2959ab0133f9 100644 --- a/packages/dds/tree/src/core/schema-stored/storedSchemaRepository.ts +++ b/packages/dds/tree/src/core/schema-stored/storedSchemaRepository.ts @@ -5,7 +5,8 @@ import { BTree } from "@tylerbu/sorted-btree-es6"; -import { type Listenable, createEmitter } from "../../events/index.js"; +import type { Listenable } from "@fluidframework/core-interfaces/internal"; +import { createEmitter } from "@fluid-internal/client-utils"; import { compareStrings } from "../../util/index.js"; import type { TreeNodeSchemaIdentifier } from "./format.js"; diff --git a/packages/dds/tree/src/core/tree/anchorSet.ts b/packages/dds/tree/src/core/tree/anchorSet.ts index 73c3b88e9e75..ce8893cabe68 100644 --- a/packages/dds/tree/src/core/tree/anchorSet.ts +++ b/packages/dds/tree/src/core/tree/anchorSet.ts @@ -7,7 +7,8 @@ import { assert } from "@fluidframework/core-utils/internal"; -import { type Listenable, createEmitter } from "../../events/index.js"; +import type { Listenable } from "@fluidframework/core-interfaces/internal"; +import { createEmitter } from "@fluid-internal/client-utils"; import { type Brand, type BrandedKey, diff --git a/packages/dds/tree/src/events/index.ts b/packages/dds/tree/src/events/index.ts deleted file mode 100644 index 2df3834f2974..000000000000 --- a/packages/dds/tree/src/events/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -export { - createEmitter, - EventEmitter, - type IEmitter, - type NoListenersCallback, - type HasListeners, -} from "./emitter.js"; - -export { - type Listeners, - type Listenable, - type Off, - type IsListener, -} from "./listeners.js"; diff --git a/packages/dds/tree/src/events/interop.ts b/packages/dds/tree/src/events/interop.ts deleted file mode 100644 index 5b1f24daabfd..000000000000 --- a/packages/dds/tree/src/events/interop.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import type { IEvent } from "@fluidframework/core-interfaces"; -import type { Listeners } from "./listeners.js"; -import type { UnionToIntersection } from "../util/index.js"; - -// TODO: this file is currently unused. Use it or remove it. - -/** - * Converts a {@link Listeners} type (i.e. the event registry for a {@link Listenable}) into a type consumable - * by an IEventProvider from `@fluidframework/core-interfaces`. - * @param E - the `Events` type to transform - * @param Target - an optional `IEvent` type that will be merged into the result along with the transformed `E` - * - * @example - * - * ```typescript - * interface MyEvents { - * load: (user: string, data: IUserData) => void; - * error: (errorCode: number) => void; - * } - * - * class MySharedObject extends SharedObject> { - * // ... - * } - * ``` - */ -export type TransformListeners< - TListeners extends Listeners, - TTarget extends IEvent = IEvent, -> = { - [P in keyof Listeners]: (event: P, listener: TListeners[P]) => void; -} extends Record - ? UnionToIntersection & TTarget - : never; diff --git a/packages/dds/tree/src/feature-libraries/chunked-forest/chunkedForest.ts b/packages/dds/tree/src/feature-libraries/chunked-forest/chunkedForest.ts index aa8a17e7b62f..5ef3ea95df9f 100644 --- a/packages/dds/tree/src/feature-libraries/chunked-forest/chunkedForest.ts +++ b/packages/dds/tree/src/feature-libraries/chunked-forest/chunkedForest.ts @@ -4,6 +4,8 @@ */ import { assert, oob } from "@fluidframework/core-utils/internal"; +import type { Listenable } from "@fluidframework/core-interfaces"; +import { createEmitter } from "@fluid-internal/client-utils"; import { type Anchor, @@ -28,7 +30,6 @@ import { mapCursorField, rootFieldKey, } from "../../core/index.js"; -import { createEmitter, type Listenable } from "../../events/index.js"; import { assertValidRange, brand, diff --git a/packages/dds/tree/src/feature-libraries/flex-tree/context.ts b/packages/dds/tree/src/feature-libraries/flex-tree/context.ts index 7a2f57f87f9f..c2906bfcb126 100644 --- a/packages/dds/tree/src/feature-libraries/flex-tree/context.ts +++ b/packages/dds/tree/src/feature-libraries/flex-tree/context.ts @@ -12,7 +12,7 @@ import { anchorSlot, moveToDetachedField, } from "../../core/index.js"; -import type { Listenable } from "../../events/index.js"; +import type { Listenable } from "@fluidframework/core-interfaces"; import { type IDisposable, disposeSymbol } from "../../util/index.js"; import type { NodeKeyManager } from "../node-key/index.js"; diff --git a/packages/dds/tree/src/feature-libraries/object-forest/objectForest.ts b/packages/dds/tree/src/feature-libraries/object-forest/objectForest.ts index ffacb467ef3c..75f4bc72a3d8 100644 --- a/packages/dds/tree/src/feature-libraries/object-forest/objectForest.ts +++ b/packages/dds/tree/src/feature-libraries/object-forest/objectForest.ts @@ -4,6 +4,8 @@ */ import { assert } from "@fluidframework/core-utils/internal"; +import { createEmitter } from "@fluid-internal/client-utils"; +import type { Listenable } from "@fluidframework/core-interfaces"; import { type Anchor, @@ -33,7 +35,6 @@ import { aboveRootPlaceholder, deepCopyMapTree, } from "../../core/index.js"; -import { createEmitter, type Listenable } from "../../events/index.js"; import { assertNonNegativeSafeInteger, assertValidIndex, diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index a7319cb689eb..dba817ab5f8a 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -34,12 +34,12 @@ export { } from "./core/index.js"; export { type Brand } from "./util/index.js"; -export { - type Listeners, - type IsListener, - type Listenable, - type Off, -} from "./events/index.js"; +export type { + Listeners, + IsListener, + Listenable, + Off, +} from "@fluidframework/core-interfaces"; export { TreeStatus, diff --git a/packages/dds/tree/src/shared-tree-core/branch.ts b/packages/dds/tree/src/shared-tree-core/branch.ts index 1ae01ddc481f..ebe58dbee025 100644 --- a/packages/dds/tree/src/shared-tree-core/branch.ts +++ b/packages/dds/tree/src/shared-tree-core/branch.ts @@ -21,7 +21,8 @@ import { tagRollbackInverse, type RebaseStatsWithDuration, } from "../core/index.js"; -import { createEmitter, type Listenable } from "../events/index.js"; +import type { Listenable } from "@fluidframework/core-interfaces"; +import { createEmitter } from "@fluid-internal/client-utils"; import { TransactionStack } from "./transactionStack.js"; import { fail, getLast, hasSome } from "../util/index.js"; diff --git a/packages/dds/tree/src/shared-tree-core/editManager.ts b/packages/dds/tree/src/shared-tree-core/editManager.ts index e4cd45958064..3d76415fa32d 100644 --- a/packages/dds/tree/src/shared-tree-core/editManager.ts +++ b/packages/dds/tree/src/shared-tree-core/editManager.ts @@ -4,6 +4,7 @@ */ import { assert } from "@fluidframework/core-utils/internal"; +import { createEmitter } from "@fluid-internal/client-utils"; import type { SessionId } from "@fluidframework/id-compressor"; import { BTree } from "@tylerbu/sorted-btree-es6"; @@ -41,7 +42,6 @@ import { minSequenceId, sequenceIdComparator, } from "./sequenceIdUtils.js"; -import { createEmitter } from "../events/index.js"; import { TelemetryEventBatcher, measure, diff --git a/packages/dds/tree/src/shared-tree/schematizingTreeView.ts b/packages/dds/tree/src/shared-tree/schematizingTreeView.ts index 1bf94a594809..bb49861c7276 100644 --- a/packages/dds/tree/src/shared-tree/schematizingTreeView.ts +++ b/packages/dds/tree/src/shared-tree/schematizingTreeView.ts @@ -3,6 +3,12 @@ * Licensed under the MIT License. */ +import type { + HasListeners, + IEmitter, + Listenable, +} from "@fluidframework/core-interfaces/internal"; +import { createEmitter } from "@fluid-internal/client-utils"; import { assert } from "@fluidframework/core-utils/internal"; import { UsageError } from "@fluidframework/telemetry-utils/internal"; @@ -12,12 +18,6 @@ import { Compatibility, type SchemaPolicy, } from "../core/index.js"; -import { - type HasListeners, - type IEmitter, - type Listenable, - createEmitter, -} from "../events/index.js"; import { type NodeKeyManager, defaultSchemaPolicy, diff --git a/packages/dds/tree/src/shared-tree/sharedTree.ts b/packages/dds/tree/src/shared-tree/sharedTree.ts index ad7bf615973a..b6899a2399a4 100644 --- a/packages/dds/tree/src/shared-tree/sharedTree.ts +++ b/packages/dds/tree/src/shared-tree/sharedTree.ts @@ -4,6 +4,12 @@ */ import { assert, unreachableCase } from "@fluidframework/core-utils/internal"; +import type { + HasListeners, + IEmitter, + Listenable, +} from "@fluidframework/core-interfaces/internal"; +import { createEmitter } from "@fluid-internal/client-utils"; import type { IChannelAttributes, IChannelFactory, @@ -26,12 +32,7 @@ import { makeDetachedFieldIndex, moveToDetachedField, } from "../core/index.js"; -import { - type HasListeners, - type IEmitter, - type Listenable, - createEmitter, -} from "../events/index.js"; + import { DetachedFieldIndexSummarizer, ForestSummarizer, diff --git a/packages/dds/tree/src/shared-tree/treeCheckout.ts b/packages/dds/tree/src/shared-tree/treeCheckout.ts index 2cf1e23bd644..263d7e5fe437 100644 --- a/packages/dds/tree/src/shared-tree/treeCheckout.ts +++ b/packages/dds/tree/src/shared-tree/treeCheckout.ts @@ -4,6 +4,12 @@ */ import { assert } from "@fluidframework/core-utils/internal"; +import type { + HasListeners, + IEmitter, + Listenable, +} from "@fluidframework/core-interfaces/internal"; +import { createEmitter } from "@fluid-internal/client-utils"; import type { IIdCompressor } from "@fluidframework/id-compressor"; import { UsageError, @@ -39,12 +45,6 @@ import { type RevertibleAlphaFactory, type RevertibleAlpha, } from "../core/index.js"; -import { - type HasListeners, - type IEmitter, - type Listenable, - createEmitter, -} from "../events/index.js"; import { type FieldBatchCodec, type TreeCompressionStrategy, diff --git a/packages/dds/tree/src/simple-tree/api/tree.ts b/packages/dds/tree/src/simple-tree/api/tree.ts index 370ac30f0403..4c049113504f 100644 --- a/packages/dds/tree/src/simple-tree/api/tree.ts +++ b/packages/dds/tree/src/simple-tree/api/tree.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import type { IFluidLoadable, IDisposable } from "@fluidframework/core-interfaces"; +import type { IFluidLoadable, IDisposable, Listenable } from "@fluidframework/core-interfaces"; import { UsageError } from "@fluidframework/telemetry-utils/internal"; import type { @@ -11,7 +11,6 @@ import type { RevertibleAlphaFactory, RevertibleFactory, } from "../../core/index.js"; -import type { Listenable } from "../../events/index.js"; import { type ImplicitFieldSchema, diff --git a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts index 2211d21212d5..fb71ba8a3e35 100644 --- a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts +++ b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts @@ -24,7 +24,7 @@ import { } from "../leafNodeSchema.js"; import { isFluidHandle } from "@fluidframework/runtime-utils/internal"; import { UsageError } from "@fluidframework/telemetry-utils/internal"; -import type { Off } from "../../events/index.js"; +import type { Off } from "@fluidframework/core-interfaces"; import { getKernel, isTreeNode, diff --git a/packages/dds/tree/src/simple-tree/core/treeNodeKernel.ts b/packages/dds/tree/src/simple-tree/core/treeNodeKernel.ts index 74c6482758a4..79864e8fe7dd 100644 --- a/packages/dds/tree/src/simple-tree/core/treeNodeKernel.ts +++ b/packages/dds/tree/src/simple-tree/core/treeNodeKernel.ts @@ -4,7 +4,8 @@ */ import { assert, Lazy } from "@fluidframework/core-utils/internal"; -import { createEmitter, type Listenable, type Off } from "../../events/index.js"; +import { createEmitter } from "@fluid-internal/client-utils"; +import type { Listenable, Off } from "@fluidframework/core-interfaces"; import type { TreeNode, Unhydrated } from "./types.js"; import { anchorSlot, diff --git a/packages/dds/tree/src/simple-tree/core/unhydratedFlexTree.ts b/packages/dds/tree/src/simple-tree/core/unhydratedFlexTree.ts index 7938be81b9e6..e77403f502da 100644 --- a/packages/dds/tree/src/simple-tree/core/unhydratedFlexTree.ts +++ b/packages/dds/tree/src/simple-tree/core/unhydratedFlexTree.ts @@ -4,6 +4,8 @@ */ import { assert, oob } from "@fluidframework/core-utils/internal"; +import { createEmitter } from "@fluid-internal/client-utils"; +import type { Listenable } from "@fluidframework/core-interfaces"; import { UsageError } from "@fluidframework/telemetry-utils/internal"; import { @@ -43,7 +45,6 @@ import { cursorForMapTreeNode, } from "../../feature-libraries/index.js"; import type { Context } from "./context.js"; -import { createEmitter, type Listenable } from "../../events/index.js"; interface UnhydratedTreeSequenceFieldEditBuilder extends SequenceFieldEditBuilder { diff --git a/packages/dds/tree/src/test/shared-tree/schematizeTree.spec.ts b/packages/dds/tree/src/test/shared-tree/schematizeTree.spec.ts index 972a2d60ecb5..7e0758018954 100644 --- a/packages/dds/tree/src/test/shared-tree/schematizeTree.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/schematizeTree.spec.ts @@ -38,7 +38,7 @@ import { // eslint-disable-next-line import/no-internal-modules } from "../../shared-tree/schematizeTree.js"; import { checkoutWithContent, validateViewConsistency } from "../utils.js"; -import type { Listenable } from "../../events/index.js"; +import type { Listenable } from "@fluidframework/core-interfaces"; import { SchemaFactory, ViewSchema, diff --git a/packages/dds/tree/src/test/utils.ts b/packages/dds/tree/src/test/utils.ts index 7d53459ada19..76a7bb911f88 100644 --- a/packages/dds/tree/src/test/utils.ts +++ b/packages/dds/tree/src/test/utils.ts @@ -5,6 +5,11 @@ import { strict as assert } from "node:assert"; +import type { + HasListeners, + IEmitter, + Listenable, +} from "@fluidframework/core-interfaces/internal"; import { createMockLoggerExt, type IMockLoggerExt, @@ -88,7 +93,6 @@ import { type RevertibleAlpha, type RevertibleAlphaFactory, } from "../core/index.js"; -import type { HasListeners, IEmitter, Listenable } from "../events/index.js"; import { typeboxValidator } from "../external-utilities/index.js"; import { type NodeKeyManager, From bea8dd53323a51abf2001eceb6b14e6e980b4397 Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:44:46 -0800 Subject: [PATCH 10/40] fix(devtools-view): Fix selectable menu section role (#23177) [Previous PR](https://github.com/microsoft/FluidFramework/pull/23093) attempted to fix this by setting the "button" role via style properties, but apparently this doesn't actually work. Confirmed that the role did not appear in the browser devtools view previously but does with the new pattern. Also updates styling to highlight selectable menu headers the same way we highlight sub-heading selections. --- .../EXTENSION_CHANGELOG.md | 4 ++ .../public/manifest.json | 2 +- .../devtools-view/src/DevtoolsView.tsx | 4 +- .../devtools-view/src/components/Menu.tsx | 43 +++++++++++++++---- .../devtools-view/src/test/Menu.test.tsx | 4 +- 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/tools/devtools/devtools-browser-extension/EXTENSION_CHANGELOG.md b/packages/tools/devtools/devtools-browser-extension/EXTENSION_CHANGELOG.md index 25f4588435aa..9081aab949f8 100644 --- a/packages/tools/devtools/devtools-browser-extension/EXTENSION_CHANGELOG.md +++ b/packages/tools/devtools/devtools-browser-extension/EXTENSION_CHANGELOG.md @@ -4,6 +4,10 @@ +# 1.0.2 + +Enhance selection styling in main menu of the devtools view and fix menu item roles for accessibility. + # 1.0.1 Update extension name to omit the " (preview)" postfix. diff --git a/packages/tools/devtools/devtools-browser-extension/public/manifest.json b/packages/tools/devtools/devtools-browser-extension/public/manifest.json index 54f6b046a5d0..5fcd04c2c012 100644 --- a/packages/tools/devtools/devtools-browser-extension/public/manifest.json +++ b/packages/tools/devtools/devtools-browser-extension/public/manifest.json @@ -3,7 +3,7 @@ "name": "Fluid Framework Developer Tools", "description": "Devtools extension for viewing live data about your Fluid application.", "author": "Microsoft", - "version": "1.0.1", + "version": "1.0.2", "action": { "default_icon": { "16": "icons/icon_16.png", diff --git a/packages/tools/devtools/devtools-view/src/DevtoolsView.tsx b/packages/tools/devtools/devtools-view/src/DevtoolsView.tsx index c62b23d69b32..a11514dba521 100644 --- a/packages/tools/devtools/devtools-view/src/DevtoolsView.tsx +++ b/packages/tools/devtools/devtools-view/src/DevtoolsView.tsx @@ -237,7 +237,9 @@ function _DevtoolsView(props: _DevtoolsViewProps): React.ReactElement { const { supportedFeatures } = props; const [containers, setContainers] = React.useState(); - const [menuSelection, setMenuSelection] = React.useState(); + const [menuSelection, setMenuSelection] = React.useState({ + type: "homeMenuSelection", + }); const messageRelay = useMessageRelay(); React.useEffect(() => { diff --git a/packages/tools/devtools/devtools-view/src/components/Menu.tsx b/packages/tools/devtools/devtools-view/src/components/Menu.tsx index 754d32ce69d4..d274c7fe7fe8 100644 --- a/packages/tools/devtools/devtools-view/src/components/Menu.tsx +++ b/packages/tools/devtools/devtools-view/src/components/Menu.tsx @@ -247,6 +247,11 @@ export interface MenuSectionButtonHeaderProps extends MenuSectionLabelHeaderProp * Button alt text. */ altText: string; + + /** + * Whether or not this selectable heading is the current selection. + */ + isActive: boolean; } const useMenuSectionButtonHeaderStyles = makeStyles({ @@ -256,7 +261,18 @@ const useMenuSectionButtonHeaderStyles = makeStyles({ flexDirection: "row", fontWeight: "bold", cursor: "pointer", - role: "button", + "&:hover": { + color: tokens.colorNeutralForeground1Hover, + backgroundColor: tokens.colorNeutralBackground1Hover, + }, + }, + active: { + color: tokens.colorNeutralForeground1Selected, + backgroundColor: tokens.colorNeutralBackground1Selected, + }, + inactive: { + color: tokens.colorNeutralForeground1, + backgroundColor: tokens.colorNeutralBackground1, }, }); @@ -266,8 +282,9 @@ const useMenuSectionButtonHeaderStyles = makeStyles({ export function MenuSectionButtonHeader( props: MenuSectionButtonHeaderProps, ): React.ReactElement { - const { label, icon, onClick, altText } = props; + const { label, icon, onClick, altText, isActive } = props; const styles = useMenuSectionButtonHeaderStyles(); + const style = mergeClasses(styles.root, isActive ? styles.active : styles.inactive); const handleKeyDown = (event: React.KeyboardEvent): void => { if ((event.key === "Enter" || event.key === " ") && onClick) { @@ -277,11 +294,12 @@ export function MenuSectionButtonHeader( return (
{label} {icon} @@ -353,16 +371,14 @@ export function MenuItem(props: MenuItemProps): React.ReactElement { */ export interface MenuProps { /** - * The current menu selection (if any). + * The current menu selection. */ - currentSelection?: MenuSelection | undefined; + currentSelection: MenuSelection; /** * Sets the menu selection to the specified value. - * - * @remarks Passing `undefined` clears the selection. */ - setSelection(newSelection: MenuSelection | undefined): void; + setSelection(newSelection: MenuSelection): void; /** * Set of features supported by the {@link @fluidframework/devtools-core#IFluidDevtools} @@ -491,7 +507,14 @@ export function Menu(props: MenuProps): React.ReactElement { menuSections.push( } + header={ + + } key="home-menu-section" />, } key="op-latency-menu-section" @@ -544,6 +568,7 @@ export function Menu(props: MenuProps): React.ReactElement { label="Settings" altText="Settings" onClick={onSettingsClicked} + isActive={currentSelection?.type === "settingsMenuSelection"} /> } key="settings-menu-section" diff --git a/packages/tools/devtools/devtools-view/src/test/Menu.test.tsx b/packages/tools/devtools/devtools-view/src/test/Menu.test.tsx index b86175d908dc..145d1fd457be 100644 --- a/packages/tools/devtools/devtools-view/src/test/Menu.test.tsx +++ b/packages/tools/devtools/devtools-view/src/test/Menu.test.tsx @@ -35,7 +35,9 @@ describe("Menu Accessibility Check", () => { }; }); const MenuWrapper: React.FC = () => { - const [menuSelection, setMenuSelection] = React.useState(); + const [menuSelection, setMenuSelection] = React.useState({ + type: "homeMenuSelection", + }); return ( From 51e66e8b81a5667b02d480fcdec978048fbcb895 Mon Sep 17 00:00:00 2001 From: Alex Villarreal <716334+alexvy86@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:56:20 -0600 Subject: [PATCH 11/40] refactor(build-tools): Add path to error message (#23182) ## Description Add details to error message so we get better info to troubleshoot if the error happens. Motivated by the fact that we're hitting this error when some build pipelines run in the LTS branch. --- build-tools/packages/build-cli/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-tools/packages/build-cli/src/config.ts b/build-tools/packages/build-cli/src/config.ts index 6cfa643d871b..29f1571fb569 100644 --- a/build-tools/packages/build-cli/src/config.ts +++ b/build-tools/packages/build-cli/src/config.ts @@ -364,7 +364,7 @@ export function getFlubConfig(configPath: string, noCache = false): FlubConfig { const config = configResult?.config as FlubConfig | undefined; if (config === undefined) { - throw new Error("No flub configuration found."); + throw new Error(`No flub configuration found (configPath='${configPath}').`); } // Only version 1 of the config is supported. If any other value is provided, throw an error. From 1b75f54f1a7e4ce636db0c777a61a525078ad852 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Fri, 22 Nov 2024 12:35:17 -0800 Subject: [PATCH 12/40] feat(build-infrastructure): Add selection and filtering APIs (#22866) This change adds selection and filtering logic to the build-infrastructure package. Much of this code currently exists in build-cli, but the implementation has changed slightly to accommodate the new interfaces. There are also new git-related APIs, which are used in the "changed since" selection/filter logic. None of the code is shared with existing implementation in build-cli. Once build-infrastructure is ready, the build-cli functionality will be replaced with the build-infra stuff. --------- Co-authored-by: Alex Villarreal <716334+alexvy86@users.noreply.github.com> --- .../api-report/build-infrastructure.api.md | 44 +- .../build-infrastructure/src/filter.ts | 302 +++++++++++ .../packages/build-infrastructure/src/git.ts | 210 ++++++++ .../build-infrastructure/src/index.ts | 15 +- .../src/test/filter.test.ts | 476 ++++++++++++++++++ .../build-infrastructure/src/test/git.test.ts | 136 ++++- 6 files changed, 1160 insertions(+), 23 deletions(-) create mode 100644 build-tools/packages/build-infrastructure/src/filter.ts create mode 100644 build-tools/packages/build-infrastructure/src/test/filter.test.ts diff --git a/build-tools/packages/build-infrastructure/api-report/build-infrastructure.api.md b/build-tools/packages/build-infrastructure/api-report/build-infrastructure.api.md index d8e59ef330dc..ea752787307e 100644 --- a/build-tools/packages/build-infrastructure/api-report/build-infrastructure.api.md +++ b/build-tools/packages/build-infrastructure/api-report/build-infrastructure.api.md @@ -13,23 +13,6 @@ import { SimpleGit } from 'simple-git'; // @public export type AdditionalPackageProps = Record | undefined; -// @public -export class BuildProject

implements IBuildProject

{ - constructor(searchPath: string, - upstreamRemotePartialUrl?: string | undefined); - protected readonly configFilePath: string; - readonly configuration: BuildProjectLayout; - getGitRepository(): Promise>; - getPackageReleaseGroup(pkg: Readonly

): Readonly; - get packages(): Map; - relativeToRepo(p: string): string; - get releaseGroups(): Map; - reload(): void; - readonly root: string; - readonly upstreamRemotePartialUrl?: string | undefined; - get workspaces(): Map; -} - // @public export const BUILDPROJECT_CONFIG_VERSION = 1; @@ -48,6 +31,9 @@ export interface BuildProjectLayout { // @public export function createPackageManager(name: PackageManagerName): IPackageManager; +// @public +export function findGitRootSync(cwd?: string): string; + // @public export interface FluidPackageJsonFields { pnpm?: { @@ -68,6 +54,24 @@ export function getBuildProjectConfig(searchPath: string, noCache?: boolean): { configFilePath: string; }; +// @public +export function getChangedSinceRef

(buildProject: IBuildProject

, ref: string, remote?: string): Promise<{ + files: string[]; + dirs: string[]; + workspaces: IWorkspace[]; + releaseGroups: IReleaseGroup[]; + packages: P[]; +}>; + +// @public +export function getFiles(git: SimpleGit, directory: string): Promise; + +// @public +export function getMergeBaseRemote(git: SimpleGit, branch: string, remote?: string, localRef?: string): Promise; + +// @public +export function getRemote(git: SimpleGit, partialUrl: string | undefined): Promise; + // @public export interface IBuildProject

extends Reloadable { configuration: BuildProjectLayout; @@ -238,6 +242,12 @@ export interface Reloadable { // @public export function setVersion(packages: IPackage[], version: SemVer): Promise; +// @public +export function updatePackageJsonFile(packagePath: string, packageTransformer: (json: J) => void): void; + +// @public +export function updatePackageJsonFileAsync(packagePath: string, packageTransformer: (json: J) => Promise): Promise; + // @public export interface WorkspaceDefinition { directory: string; diff --git a/build-tools/packages/build-infrastructure/src/filter.ts b/build-tools/packages/build-infrastructure/src/filter.ts new file mode 100644 index 000000000000..c247b4385a7e --- /dev/null +++ b/build-tools/packages/build-infrastructure/src/filter.ts @@ -0,0 +1,302 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import path from "node:path"; + +import mm from "micromatch"; + +import { getChangedSinceRef, getRemote } from "./git.js"; +import { type IBuildProject, IPackage } from "./types.js"; + +export const defaultSelectionKinds = ["dir", "all"] as const; + +/** + * A convenience type representing a glob string. + */ +export type GlobString = string; + +/** + * The criteria that should be used for selecting package-like objects from a collection. + */ +export interface PackageSelectionCriteria { + /** + * An array of workspaces whose packages are selected. All packages in the workspace _except_ the root package + * will be selected. To include workspace roots, use the `workspaceRoots` property. + * + * Values should either be complete workspace names or micromatch glob strings. To select all workspaces, use `"*"`. + * See https://www.npmjs.com/package/micromatch?activeTab=readme#extended-globbing for more details. + * + * Workspace names will be compared against all globs - if any match, the workspace will be selected. + */ + workspaces: (GlobString | string)[]; + + /** + * An array of workspaces whose root packages are selected. Only the roots of each workspace will be included. + * + * Values should either be complete workspace names or micromatch glob strings. To select all workspaces, use `"*"`. + * See https://www.npmjs.com/package/micromatch?activeTab=readme#extended-globbing for more details. + * + * Workspace names will be compared against all globs - if any match, the workspace will be selected. + */ + workspaceRoots: (GlobString | string)[]; + + /** + * An array of release groups whose packages are selected. All packages in the release group _except_ the root package + * will be selected. To include release group roots, use the `releaseGroupRoots` property. + * + * Values should either be complete release group names or micromatch glob strings. To select all release groups, use + * `"*"`. See https://www.npmjs.com/package/micromatch?activeTab=readme#extended-globbing for more details. + * + * Workspace names will be compared against all globs - if any match, the workspace will be selected. + */ + releaseGroups: (GlobString | string)[]; + + /** + * An array of release groups whose root packages are selected. Only the roots of each release group will be included. + * Rootless release groups will never be selected with this criteria. + * + * The reserved string "\*" will select all packages when included in one of the criteria. If used, the "\*" value is + * expected to be the only item in the selection array. + */ + releaseGroupRoots: (GlobString | string)[]; + + /** + * If set, only selects the single package in this directory. + */ + directory?: string | undefined; + + /** + * If set, only selects packages that have changes when compared with the branch of this name. + */ + changedSinceBranch?: string | undefined; +} + +/** + * A pre-defined {@link PackageSelectionCriteria} that selects all packages. + */ +export const AllPackagesSelectionCriteria: PackageSelectionCriteria = { + workspaces: ["*"], + workspaceRoots: ["*"], + releaseGroups: [], + releaseGroupRoots: [], + directory: undefined, + changedSinceBranch: undefined, +} as const; + +/** + * An empty {@link PackageSelectionCriteria} that selects no packages. + */ +export const EmptySelectionCriteria: PackageSelectionCriteria = { + workspaces: [], + workspaceRoots: [], + releaseGroups: [], + releaseGroupRoots: [], + directory: undefined, + changedSinceBranch: undefined, +} as const; + +/** + * The criteria that should be used for filtering package-like objects from a collection. + */ +export interface PackageFilterOptions { + /** + * If set, filters IN packages whose scope matches the strings provided. + */ + scope?: string[] | undefined; + + /** + * If set, filters OUT packages whose scope matches the strings provided. + */ + skipScope?: string[] | undefined; + + /** + * If set, filters private packages in/out. + */ + private: boolean | undefined; +} + +/** + * Selects packages from a BuildProject based on the selection criteria. + * + * @param buildProject - The BuildProject to select from. + * @param selection - The selection criteria to use to select packages. + * @returns A `Set` containing the selected packages. + */ +const selectPackagesFromRepo = async

( + buildProject: IBuildProject

, + selection: PackageSelectionCriteria, +): Promise> => { + const selected: Set

= new Set(); + + if (selection.changedSinceBranch !== undefined) { + const git = await buildProject.getGitRepository(); + const remote = await getRemote(git, buildProject.upstreamRemotePartialUrl); + if (remote === undefined) { + throw new Error(`Can't find a remote with ${buildProject.upstreamRemotePartialUrl}`); + } + const { packages } = await getChangedSinceRef( + buildProject, + selection.changedSinceBranch, + remote, + ); + addAllToSet(selected, packages); + } + + if (selection.directory !== undefined) { + const selectedAbsolutePath = path.join( + selection.directory === "." + ? process.cwd() + : path.resolve(buildProject.root, selection.directory), + ); + + const dirPackage = [...buildProject.packages.values()].find( + (p) => p.directory === selectedAbsolutePath, + ); + if (dirPackage === undefined) { + throw new Error(`Cannot find package with directory: ${selectedAbsolutePath}`); + } + selected.add(dirPackage); + return selected; + } + + // Select workspace and workspace root packages + for (const workspace of buildProject.workspaces.values()) { + if (selection.workspaces.length > 0 && mm.isMatch(workspace.name, selection.workspaces)) { + addAllToSet( + selected, + workspace.packages.filter((p) => !p.isWorkspaceRoot), + ); + } + + if ( + selection.workspaceRoots.length > 0 && + mm.isMatch(workspace.name, selection.workspaceRoots) + ) { + addAllToSet( + selected, + workspace.packages.filter((p) => p.isWorkspaceRoot), + ); + } + } + + // Select release group and release group root packages + for (const releaseGroup of buildProject.releaseGroups.values()) { + if ( + selection.releaseGroups.length > 0 && + mm.isMatch(releaseGroup.name, selection.releaseGroups) + ) { + addAllToSet( + selected, + releaseGroup.packages.filter((p) => !p.isReleaseGroupRoot), + ); + } + + if ( + selection.releaseGroupRoots.length > 0 && + mm.isMatch(releaseGroup.name, selection.releaseGroupRoots) + ) { + addAllToSet( + selected, + releaseGroup.packages.filter((p) => p.isReleaseGroupRoot), + ); + } + } + + return selected; +}; + +/** + * Selects packages from the BuildProject based on the selection criteria. The selected packages will be filtered by the + * filter criteria if provided. + * + * @param buildProject - The BuildProject. + * @param selection - The selection criteria to use to select packages. + * @param filter - An optional filter criteria to filter selected packages by. + * @returns An object containing the selected packages and the filtered packages. + */ +export async function selectAndFilterPackages

( + buildProject: IBuildProject

, + selection: PackageSelectionCriteria, + filter?: PackageFilterOptions, +): Promise<{ selected: P[]; filtered: P[] }> { + // Select the packages from the repo + const selected = [...(await selectPackagesFromRepo

(buildProject, selection))]; + + // Filter resulting list if needed + const filtered = filter === undefined ? selected : await filterPackages(selected, filter); + + return { selected, filtered }; +} + +/** + * Convenience type that contains only the properties of a package that are needed for filtering. + */ +export interface FilterablePackage { + name: string; + private?: boolean | undefined; +} + +/** + * Filters a list of packages by the filter criteria. + * + * @param packages - An array of packages to be filtered. + * @param filters - The filter criteria to filter the packages by. + * @typeParam T - The type of the package-like objects being filtered. + * @returns An array containing only the filtered items. + */ +export async function filterPackages( + packages: T[], + filters: PackageFilterOptions, +): Promise { + const filtered = packages.filter((pkg) => { + if (filters === undefined) { + return true; + } + + const isPrivate: boolean = pkg.private ?? false; + if (filters.private !== undefined && filters.private !== isPrivate) { + return false; + } + + const scopeIn = scopesToPrefix(filters?.scope); + const scopeOut = scopesToPrefix(filters?.skipScope); + + if (scopeIn !== undefined) { + let found = false; + for (const scope of scopeIn) { + found ||= pkg.name.startsWith(scope); + } + if (!found) { + return false; + } + } + if (scopeOut !== undefined) { + for (const scope of scopeOut) { + if (pkg.name.startsWith(scope) === true) { + return false; + } + } + } + return true; + }); + + return filtered; +} + +function scopesToPrefix(scopes: string[] | undefined): string[] | undefined { + return scopes === undefined ? undefined : scopes.map((s) => `${s}/`); +} + +/** + * Adds all the items of an iterable to a set. + * + * @param set - The set to which items will be added. + * @param iterable - The iterable containing items to add to the set. + */ +export function addAllToSet(set: Set, iterable: Iterable): void { + for (const item of iterable) { + set.add(item); + } +} diff --git a/build-tools/packages/build-infrastructure/src/git.ts b/build-tools/packages/build-infrastructure/src/git.ts index dd3d61286861..4cc8faa33dcb 100644 --- a/build-tools/packages/build-infrastructure/src/git.ts +++ b/build-tools/packages/build-infrastructure/src/git.ts @@ -3,9 +3,21 @@ * Licensed under the MIT License. */ +import path from "node:path"; + import execa from "execa"; +import readPkgUp from "read-pkg-up"; +import type { SimpleGit } from "simple-git"; import { NotInGitRepository } from "./errors.js"; +import type { + IBuildProject, + IPackage, + IReleaseGroup, + IWorkspace, + PackageName, +} from "./types.js"; +import { isPathUnder } from "./utils.js"; /** * Returns the absolute path to the nearest Git repository root found starting at `cwd`. @@ -43,3 +55,201 @@ export function findGitRootSync(cwd = process.cwd()): string { throw error; } } + +/** + * Get the merge base between the current HEAD and a remote branch. + * + * @param branch - The branch to compare against. + * @param remote - The remote to compare against. If this is undefined, then the local branch is compared with. + * @param localRef - The local ref to compare against. Defaults to HEAD. + * @returns The ref of the merge base between the current HEAD and the remote branch. + */ +export async function getMergeBaseRemote( + git: SimpleGit, + branch: string, + remote?: string, + localRef = "HEAD", +): Promise { + if (remote !== undefined) { + // make sure we have the latest remote refs + await git.fetch([remote]); + } + + const compareRef = remote === undefined ? branch : `refs/remotes/${remote}/${branch}`; + const base = await git.raw("merge-base", compareRef, localRef); + return base; +} + +/** + * Gets all the files that have changed when compared to another ref. Paths are relative to the root of the git + * repository. + * + * Note that newly added, unstaged files are NOT included. + */ +export async function getChangedFilesSinceRef( + git: SimpleGit, + ref: string, + remote?: string, +): Promise { + if (remote !== undefined) { + // make sure we have the latest remote refs + await git.fetch([remote]); + } + + // Find the merge base commit + const divergedAt = remote === undefined ? ref : await getMergeBaseRemote(git, ref, remote); + // Now we can find which files have changed + const changed = await git.diff([ + divergedAt, + "--name-only", + // Select all file change types except "broken" + // See https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203 + "--diff-filter=ACDMRTUX", + ]); + + const files = changed + .split("\n") + .filter((value) => value !== null && value !== undefined && value !== ""); + return files; +} + +/** + * Given an array of file paths, returns a deduplicated array of all of the directories those files are in. + */ +function filePathsToDirectories(files: string[]): string[] { + const dirs = new Set(files.map((f) => path.dirname(f))); + return [...dirs]; +} + +/** + * Gets the changed files, directories, release groups, and packages since the given ref. + * + * Returned paths are relative to the BuildProject root. + * + * @param buildProject - The BuildProject. + * @param ref - The ref to compare against. + * @param remote - The remote to compare against. + * @returns An object containing the changed files, directories, release groups, workspaces, and packages. Note that a + * package may appear in multiple groups. That is, if a single package in a release group is changed, the releaseGroups + * value will contain that group, and the packages value will contain only the single package. Also, if two packages are + * changed, each within separate release groups, the packages value will contain both packages, and the releaseGroups + * value will contain both release groups. + */ +export async function getChangedSinceRef

( + buildProject: IBuildProject

, + ref: string, + remote?: string, +): Promise<{ + files: string[]; + dirs: string[]; + workspaces: IWorkspace[]; + releaseGroups: IReleaseGroup[]; + packages: P[]; +}> { + const gitRoot = findGitRootSync(buildProject.root); + const git = await buildProject.getGitRepository(); + const filesRaw = await getChangedFilesSinceRef(git, ref, remote); + const files = filesRaw + .map( + // Make paths absolute + (filePath) => path.join(gitRoot, filePath), + ) + .filter((filePath) => { + // filter out changed paths that are not under the Fluid repo + // since only paths under the repo should be included + return isPathUnder(buildProject.root, filePath); + }) + .map((filePath) => buildProject.relativeToRepo(filePath)); + const dirs = filePathsToDirectories(files); + + const changedPackageNames = dirs + .map((dir) => { + const cwd = path.resolve(buildProject.root, dir); + return readPkgUp.sync({ cwd })?.packageJson.name; + }) + .filter((name): name is string => name !== undefined); + + const changedPackages = [...new Set(changedPackageNames)] + .map((name) => buildProject.packages.get(name as PackageName)) + .filter((pkg): pkg is P => pkg !== undefined); + + const changedReleaseGroups = [...new Set(changedPackages.map((pkg) => pkg.releaseGroup))] + .map((rg) => buildProject.releaseGroups.get(rg)) + .filter((rg): rg is IReleaseGroup => rg !== undefined); + + const changedWorkspaces = [...new Set(changedReleaseGroups.map((ws) => ws.workspace))]; + + return { + files, + dirs, + workspaces: changedWorkspaces, + releaseGroups: changedReleaseGroups, + packages: changedPackages, + }; +} + +/** + * Get a matching git remote name based on a partial URL to the remote repo. It will match the first remote that + * contains the partialUrl case insensitively. + * + * @param partialUrl - partial URL to match case insensitively + */ +export async function getRemote( + git: SimpleGit, + partialUrl: string | undefined, +): Promise { + if (partialUrl === undefined) { + return undefined; + } + + const lowerPartialUrl = partialUrl.toLowerCase(); + const remotes = await git.getRemotes(/* verbose */ true); + + for (const r of remotes) { + if (r.refs.fetch.toLowerCase().includes(lowerPartialUrl)) { + return r.name; + } + } +} + +/** + * Returns an array containing repo repo-relative paths to all the files in the provided directory. + * A given path will only be included once in the array; that is, there will be no duplicate paths. + * Note that this function excludes files that are deleted locally whether the deletion is staged or not. + * + * @param directory - A directory to filter the results by. Only files under this directory will be returned. To + * return all files in the repo use the value `"."`. + */ +export async function getFiles(git: SimpleGit, directory: string): Promise { + // Note that `--deduplicate` is not used here because it is not available until git version 2.31.0. + const results = await git.raw( + "ls-files", + // Includes cached (staged) files. + "--cached", + // Includes other (untracked) files that are not ignored. + "--others", + // Excludes files that are ignored by standard ignore rules. + "--exclude-standard", + // Shows the full path of the files relative to the repository root. + "--full-name", + directory, + ); + + // Deduplicate the list of files by building a Set. + // This includes paths to deleted, unstaged files, so we get the list of deleted files from git status and remove + // those from the full list. + const allFiles = new Set( + results + .split("\n") + .map((line) => line.trim()) + // filter out empty lines + .filter((line) => line !== ""), + ); + const status = await git.status(); + for (const deletedFile of status.deleted) { + allFiles.delete(deletedFile); + } + + // Files are already repo root-relative + return [...allFiles]; +} diff --git a/build-tools/packages/build-infrastructure/src/index.ts b/build-tools/packages/build-infrastructure/src/index.ts index 3a69dc484400..590b45613b9d 100644 --- a/build-tools/packages/build-infrastructure/src/index.ts +++ b/build-tools/packages/build-infrastructure/src/index.ts @@ -14,6 +14,10 @@ * @module default entrypoint */ +export { + getAllDependencies, + loadBuildProject, +} from "./buildProject.js"; export { type ReleaseGroupDefinition, type WorkspaceDefinition, @@ -26,11 +30,14 @@ export { } from "./config.js"; export { NotInGitRepository } from "./errors.js"; export { - BuildProject, - getAllDependencies, - loadBuildProject, -} from "./buildProject.js"; + getFiles, + findGitRootSync, + getMergeBaseRemote, + getRemote, + getChangedSinceRef, +} from "./git.js"; export { PackageBase } from "./package.js"; +export { updatePackageJsonFile, updatePackageJsonFileAsync } from "./packageJsonUtils.js"; export { createPackageManager } from "./packageManagers.js"; export type { AdditionalPackageProps, diff --git a/build-tools/packages/build-infrastructure/src/test/filter.test.ts b/build-tools/packages/build-infrastructure/src/test/filter.test.ts new file mode 100644 index 000000000000..846d6cf08647 --- /dev/null +++ b/build-tools/packages/build-infrastructure/src/test/filter.test.ts @@ -0,0 +1,476 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import chai, { assert, expect } from "chai"; +import assertArrays from "chai-arrays"; + +import { loadBuildProject } from "../buildProject.js"; +import { + AllPackagesSelectionCriteria, + EmptySelectionCriteria, + PackageFilterOptions, + PackageSelectionCriteria, + filterPackages, + selectAndFilterPackages, +} from "../filter.js"; +import type { IBuildProject, IPackage, WorkspaceName } from "../types.js"; + +import { testRepoRoot } from "./init.js"; + +// const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +chai.use(assertArrays); + +const EmptyFilter: PackageFilterOptions = { + private: undefined, + scope: undefined, + skipScope: undefined, +}; + +async function getBuildProject(): Promise { + const fluidRepo = loadBuildProject(testRepoRoot, "microsoft/FluidFramework"); + return fluidRepo; +} + +async function getMainWorkspacePackages(): Promise { + const fluidRepo = await getBuildProject(); + const packages = fluidRepo.workspaces.get("main" as WorkspaceName)?.packages; + assert(packages !== undefined); + return packages; +} + +describe("filterPackages", () => { + it("no filters", async () => { + const packages = await getMainWorkspacePackages(); + const filters = EmptyFilter; + + const actual = await filterPackages(packages, filters); + const names = actual.map((p) => p.name); + expect(names).to.be.equalTo([ + "main-release-group-root", + "@group2/pkg-d", + "@group2/pkg-e", + "@group3/pkg-f", + "@group3/pkg-g", + "pkg-a", + "pkg-b", + "@private/pkg-c", + "@shared/shared", + ]); + }); + + it("private=true", async () => { + const packages = await getMainWorkspacePackages(); + const filters = { ...EmptyFilter, private: true }; + const actual = await filterPackages(packages, filters); + const names = actual.map((p) => p.name); + expect(names).to.be.containingAllOf(["@private/pkg-c"]); + expect(names).to.be.ofSize(1); + }); + + it("private=false", async () => { + const packages = await getMainWorkspacePackages(); + const filters = { ...EmptyFilter, private: false }; + const actual = await filterPackages(packages, filters); + const names = actual.map((p) => p.name); + expect(names).to.be.equalTo([ + "main-release-group-root", + "@group2/pkg-d", + "@group2/pkg-e", + "@group3/pkg-f", + "@group3/pkg-g", + "pkg-a", + "pkg-b", + "@shared/shared", + ]); + }); + + it("multiple scopes", async () => { + const packages = await getMainWorkspacePackages(); + const filters: PackageFilterOptions = { + private: undefined, + scope: ["@shared", "@private"], + skipScope: undefined, + }; + const actual = await filterPackages(packages, filters); + const names = actual.map((p) => p.name); + expect(names).to.be.containingAllOf(["@shared/shared", "@private/pkg-c"]); + }); + + it("multiple skipScopes", async () => { + const packages = await getMainWorkspacePackages(); + const filters: PackageFilterOptions = { + ...EmptyFilter, + skipScope: ["@shared", "@private", "@group3"], + }; + const actual = await filterPackages(packages, filters); + const names = actual.map((p) => p.name); + expect(names).to.be.equalTo([ + "main-release-group-root", + "@group2/pkg-d", + "@group2/pkg-e", + "pkg-a", + "pkg-b", + ]); + }); + + it("scope and skipScope", async () => { + const packages = await getMainWorkspacePackages(); + const filters: PackageFilterOptions = { + ...EmptyFilter, + scope: ["@shared", "@private"], + skipScope: ["@shared"], + }; + const actual = await filterPackages(packages, filters); + const names = actual.map((p) => p.name); + expect(names).to.be.equalTo(["@private/pkg-c"]); + }); +}); + +describe("selectAndFilterPackages", () => { + const fluidRepoPromise = getBuildProject(); + + it("all, no filters", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions = AllPackagesSelectionCriteria; + const filter = EmptyFilter; + + const { selected } = await selectAndFilterPackages(fluidRepo, selectionOptions, filter); + const names = selected.map((p) => p.name).sort(); + + expect(names).to.be.equalTo([ + "@group2/pkg-d", + "@group2/pkg-e", + "@group3/pkg-f", + "@group3/pkg-g", + "@private/pkg-c", + "@shared/shared", + "main-release-group-root", + "other-pkg-a", + "other-pkg-b", + "pkg-a", + "pkg-b", + "second-release-group-root", + ]); + }); + + it("select directory", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + releaseGroups: ["main"], + releaseGroupRoots: [], + workspaces: [], + workspaceRoots: [], + directory: "second/packages/other-pkg-a", + changedSinceBranch: undefined, + }; + const filters: PackageFilterOptions = { + private: undefined, + scope: undefined, + skipScope: undefined, + }; + + const { selected, filtered } = await selectAndFilterPackages( + fluidRepo, + selectionOptions, + filters, + ); + expect(selected).to.be.ofSize(1); + expect(filtered).to.be.ofSize(1); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const pkg = filtered[0]!; + + expect(pkg.name).to.equal("other-pkg-a"); + expect(fluidRepo.relativeToRepo(pkg.directory)).to.equal("second/packages/other-pkg-a"); + }); + + describe("select release group", () => { + it("no filters", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + releaseGroups: ["main"], + }; + const filters: PackageFilterOptions = EmptyFilter; + + const { selected } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = selected.map((p) => p.name); + + expect(names).to.be.equalTo(["pkg-a", "pkg-b", "@private/pkg-c", "@shared/shared"]); + }); + + it("select release group root", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + releaseGroupRoots: ["main"], + }; + const filters: PackageFilterOptions = EmptyFilter; + + const { selected } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const dirs = selected.map((p) => fluidRepo.relativeToRepo(p.directory)); + + expect(selected.length).to.equal(1); + expect(dirs).to.be.containingAllOf([""]); + }); + + it("filter private", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + releaseGroups: ["main"], + }; + const filters: PackageFilterOptions = { + ...EmptyFilter, + private: true, + }; + + const { filtered } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = filtered.map((p) => p.name); + + expect(names).to.be.containingAllOf(["@private/pkg-c"]); + }); + + it("filter non-private", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + releaseGroups: ["main"], + }; + const filters: PackageFilterOptions = { + ...EmptyFilter, + private: false, + }; + + const { filtered } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = filtered.map((p) => p.name); + + expect(names).to.be.equalTo(["pkg-a", "pkg-b", "@shared/shared"]); + }); + + it("filter scopes", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + releaseGroups: ["main"], + }; + const filters: PackageFilterOptions = { + ...EmptyFilter, + scope: ["@shared"], + }; + + const { filtered } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = filtered.map((p) => p.name); + + expect(names).to.be.equalTo(["@shared/shared"]); + }); + + it("filter skipScopes", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + releaseGroups: ["main"], + }; + const filters: PackageFilterOptions = { + ...EmptyFilter, + skipScope: ["@shared", "@private"], + }; + + const { filtered } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = filtered.map((p) => p.name); + + expect(names).to.be.equalTo(["pkg-a", "pkg-b"]); + }); + }); + + describe("select workspace", () => { + it("all, no filters", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + workspaces: ["main"], + }; + const filters: PackageFilterOptions = EmptyFilter; + + const { selected } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = selected.map((p) => p.name); + + expect(names).to.be.equalTo([ + "@group2/pkg-d", + "@group2/pkg-e", + "@group3/pkg-f", + "@group3/pkg-g", + "pkg-a", + "pkg-b", + "@private/pkg-c", + "@shared/shared", + ]); + }); + + it("select workspace root at repo root", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + workspaceRoots: ["main"], + }; + const filters: PackageFilterOptions = EmptyFilter; + + const { selected } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const dirs = selected.map((p) => fluidRepo.relativeToRepo(p.directory)); + + expect(selected.length).to.equal(1); + expect(dirs).to.be.containingAllOf([""]); + }); + + it("select workspace root not at repo root", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + workspaceRoots: ["second"], + }; + const filters: PackageFilterOptions = EmptyFilter; + + const { selected } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const dirs = selected.map((p) => fluidRepo.relativeToRepo(p.directory)); + + expect(selected.length).to.equal(1); + expect(dirs).to.be.containingAllOf(["second"]); + }); + + it("filter private", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + workspaces: ["main"], + }; + const filters: PackageFilterOptions = { + private: true, + scope: undefined, + skipScope: undefined, + }; + + const { filtered } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = filtered.map((p) => p.name); + + expect(names).to.be.containingAllOf(["@private/pkg-c"]); + }); + + it("filter non-private", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + workspaces: ["main"], + }; + const filters: PackageFilterOptions = { + private: false, + scope: undefined, + skipScope: undefined, + }; + + const { filtered } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = filtered.map((p) => p.name); + + expect(names).to.be.equalTo([ + "@group2/pkg-d", + "@group2/pkg-e", + "@group3/pkg-f", + "@group3/pkg-g", + "pkg-a", + "pkg-b", + "@shared/shared", + ]); + }); + + it("filter scopes", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + workspaces: ["main"], + }; + const filters: PackageFilterOptions = { + private: undefined, + scope: ["@shared"], + skipScope: undefined, + }; + + const { filtered } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = filtered.map((p) => p.name); + + expect(names).to.be.equalTo(["@shared/shared"]); + }); + + it("filter skipScopes", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + workspaces: ["main"], + }; + const filters: PackageFilterOptions = { + private: undefined, + scope: undefined, + skipScope: ["@shared", "@private", "@group3"], + }; + + const { filtered } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = filtered.map((p) => p.name); + + expect(names).to.be.equalTo(["@group2/pkg-d", "@group2/pkg-e", "pkg-a", "pkg-b"]); + }); + }); + + describe("combination workspace and release group", () => { + const filters: PackageFilterOptions = EmptyFilter; + + it("selects workspace and disjoint release group", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + workspaces: ["second"], + releaseGroups: ["group2"], + }; + + const { filtered } = await selectAndFilterPackages(fluidRepo, selectionOptions, filters); + const names = filtered.map((p) => p.name); + + expect(names).to.be.equalTo([ + "other-pkg-a", + "other-pkg-b", + "@group2/pkg-d", + "@group2/pkg-e", + ]); + }); + }); + + it("selects all release groups", async () => { + const fluidRepo = await fluidRepoPromise; + const selectionOptions: PackageSelectionCriteria = { + ...EmptySelectionCriteria, + releaseGroups: ["*"], + }; + + const { filtered } = await selectAndFilterPackages( + fluidRepo, + selectionOptions, + EmptyFilter, + ); + const names = filtered.map((p) => p.name).sort(); + + expect(names).to.be.equalTo( + [ + "@group2/pkg-d", + "@group2/pkg-e", + "@group3/pkg-f", + "@group3/pkg-g", + "@private/pkg-c", + "@shared/shared", + "other-pkg-a", + "other-pkg-b", + "pkg-a", + "pkg-b", + ].sort(), + ); + }); +}); diff --git a/build-tools/packages/build-infrastructure/src/test/git.test.ts b/build-tools/packages/build-infrastructure/src/test/git.test.ts index 9b315e162274..4c17259265d2 100644 --- a/build-tools/packages/build-infrastructure/src/test/git.test.ts +++ b/build-tools/packages/build-infrastructure/src/test/git.test.ts @@ -4,15 +4,21 @@ */ import { strict as assert } from "node:assert"; +import { unlink } from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; +import { expect } from "chai"; +import { readJson, writeJson } from "fs-extra/esm"; import { describe, it } from "mocha"; +import { CleanOptions, simpleGit } from "simple-git"; +import { loadBuildProject } from "../buildProject.js"; import { NotInGitRepository } from "../errors.js"; -import { findGitRootSync } from "../git.js"; +import { findGitRootSync, getChangedSinceRef, getFiles, getRemote } from "../git.js"; +import type { PackageJson } from "../types.js"; -import { packageRootPath } from "./init.js"; +import { packageRootPath, testRepoRoot } from "./init.js"; describe("findGitRootSync", () => { it("finds root", () => { @@ -29,3 +35,129 @@ describe("findGitRootSync", () => { }, NotInGitRepository); }); }); + +describe("getRemote", () => { + const git = simpleGit(process.cwd()); + + it("finds upstream remote", async () => { + const actual = await getRemote(git, "microsoft/FluidFramework"); + expect(actual).not.to.be.undefined; + }); + + it("missing remote returns undefined", async () => { + const actual = await getRemote(git, "foo/bar"); + expect(actual).to.be.undefined; + }); +}); + +describe("getChangedSinceRef: local", () => { + const git = simpleGit(process.cwd()); + const repo = loadBuildProject(testRepoRoot); + + beforeEach(async () => { + // create a file + const newFile = path.join(testRepoRoot, "second/newFile.json"); + await writeJson(newFile, '{"foo": "bar"}'); + await git.add(newFile); + + // delete a file + await unlink(path.join(testRepoRoot, "packages/group3/pkg-f/src/index.mjs")); + + // edit a file + const pkgJson = path.join(testRepoRoot, "packages/group3/pkg-f/package.json"); + const json = (await readJson(pkgJson)) as PackageJson; + json.author = "edited field"; + await writeJson(pkgJson, json); + }); + + afterEach(async () => { + await git.reset(["HEAD", "--", testRepoRoot]); + await git.checkout(["HEAD", "--", testRepoRoot]); + await git.clean(CleanOptions.FORCE, [testRepoRoot]); + }); + + it("returns correct files", async () => { + const { files } = await getChangedSinceRef(repo, "HEAD"); + + expect(files).to.be.containingAllOf([ + "packages/group3/pkg-f/package.json", + "packages/group3/pkg-f/src/index.mjs", + "second/newFile.json", + ]); + expect(files).to.be.ofSize(3); + }); + + it("returns correct dirs", async () => { + const { dirs } = await getChangedSinceRef(repo, "HEAD"); + + expect(dirs).to.be.containingAllOf([ + "packages/group3/pkg-f", + "packages/group3/pkg-f/src", + "second", + ]); + expect(dirs).to.be.ofSize(3); + }); + + it("returns correct packages", async () => { + const { packages } = await getChangedSinceRef(repo, "HEAD"); + + expect(packages.map((p) => p.name)).to.be.containingAllOf([ + "@group3/pkg-f", + "second-release-group-root", + ]); + expect(packages).to.be.ofSize(2); + }); + + it("returns correct release groups", async () => { + const { releaseGroups } = await getChangedSinceRef(repo, "HEAD"); + + expect(releaseGroups.map((p) => p.name)).to.be.containingAllOf([ + "group3", + "second-release-group", + ]); + expect(releaseGroups).to.be.ofSize(2); + }); + + it("returns correct workspaces", async () => { + const { workspaces } = await getChangedSinceRef(repo, "HEAD"); + + expect(workspaces.map((p) => p.name)).to.be.containingAllOf(["main", "second"]); + expect(workspaces).to.be.ofSize(2); + }); +}); + +describe("getFiles", () => { + const git = simpleGit(process.cwd()); + const gitRoot = findGitRootSync(); + + it("correct files with clean working directory", async () => { + const actual = await getFiles(git, testRepoRoot); + console.debug(testRepoRoot, actual); + + expect(actual).to.be.containingAllOf( + [ + `${testRepoRoot}/.changeset/README.md`, + `${testRepoRoot}/.changeset/bump-main-group-minor.md`, + `${testRepoRoot}/.changeset/config.json`, + `${testRepoRoot}/fluidBuild.config.cjs`, + `${testRepoRoot}/package.json`, + `${testRepoRoot}/packages/group2/pkg-d/package.json`, + `${testRepoRoot}/packages/group2/pkg-e/package.json`, + `${testRepoRoot}/packages/group3/pkg-f/package.json`, + `${testRepoRoot}/packages/group3/pkg-f/src/index.mjs`, + `${testRepoRoot}/packages/group3/pkg-g/package.json`, + `${testRepoRoot}/packages/pkg-a/package.json`, + `${testRepoRoot}/packages/pkg-b/package.json`, + `${testRepoRoot}/packages/pkg-c/package.json`, + `${testRepoRoot}/packages/shared/package.json`, + `${testRepoRoot}/pnpm-lock.yaml`, + `${testRepoRoot}/pnpm-workspace.yaml`, + `${testRepoRoot}/second/package.json`, + `${testRepoRoot}/second/packages/other-pkg-a/package.json`, + `${testRepoRoot}/second/packages/other-pkg-b/package.json`, + `${testRepoRoot}/second/pnpm-lock.yaml`, + `${testRepoRoot}/second/pnpm-workspace.yaml`, + ].map((p) => path.relative(gitRoot, p)), + ); + }); +}); From 0ca4681da6207615d47fd2275da3697e97521b6b Mon Sep 17 00:00:00 2001 From: Mark Fields Date: Fri, 22 Nov 2024 13:07:15 -0800 Subject: [PATCH 13/40] fix(gc): fullGC flag for Autorecovery shouldn't be cleared before GC runs (#23179) When reviewing the telemetry for autorecovery, I found that a large number of documents see Autorecovery but not fullGC. This was unexpected since Autorecovery includes setting a flag that causes the next GC run to use fullGC. Upon closer inspection of a few sample sessions, I found that the flag was getting cleared before the next GC run. Likely due to unexpected case where summary triggers the load of the Tombstoned object, so the GC op and the summary Ack are racing. If the GC op wins, then the summary Ack clears the flag. This updates the logic to only clear the flag if GC actually ran with fullGC. --- .../src/gc/garbageCollection.ts | 45 +++++++++++++++-- .../src/gc/gcSummaryStateTracker.ts | 13 ----- .../src/test/gc/garbageCollection.spec.ts | 50 ++++++++++++++++--- .../src/test/gc/gcSummaryStateTracker.spec.ts | 34 ------------- 4 files changed, 84 insertions(+), 58 deletions(-) diff --git a/packages/runtime/container-runtime/src/gc/garbageCollection.ts b/packages/runtime/container-runtime/src/gc/garbageCollection.ts index e4b7cb1dc5c0..931427d56b1b 100644 --- a/packages/runtime/container-runtime/src/gc/garbageCollection.ts +++ b/packages/runtime/container-runtime/src/gc/garbageCollection.ts @@ -338,6 +338,43 @@ export class GarbageCollector implements IGarbageCollector { }); } + /** API for ensuring the correct auto-recovery mitigations */ + private readonly autoRecovery = (() => { + // This uses a hidden state machine for forcing fullGC as part of autorecovery, + // to regenerate the GC data for each node. + // + // Once fullGC has been requested, we need to wait until GC has run and the summary has been acked before clearing the state. + // + // States: + // - undefined: No need to run fullGC now. + // - "requested": FullGC requested, but GC has not yet run. Keep using fullGC until back to undefined. + // - "ran": FullGC ran, but the following summary has not yet been acked. Keep using fullGC until back to undefined. + // + // Transitions: + // - autoRecovery.requestFullGCOnNextRun :: [anything] --> "requested" + // - autoRecovery.onCompletedGCRun :: "requested" --> "ran" + // - autoRecovery.onSummaryAck :: "ran" --> undefined + let state: "requested" | "ran" | undefined; + return { + requestFullGCOnNextRun: () => { + state = "requested"; + }, + onCompletedGCRun: () => { + if (state === "requested") { + state = "ran"; + } + }, + onSummaryAck: () => { + if (state === "ran") { + state = undefined; + } + }, + useFullGC: () => { + return state !== undefined; + }, + }; + })(); + /** * Called during container initialization. Initializes the tombstone and deleted nodes state from the base snapshot. * Also, initializes the GC state including unreferenced nodes tracking if a current reference timestamp exists. @@ -460,9 +497,7 @@ export class GarbageCollector implements IGarbageCollector { telemetryContext?: ITelemetryContext, ): Promise { const fullGC = - options.fullGC ?? - (this.configs.runFullGC === true || - this.summaryStateTracker.autoRecovery.fullGCRequested()); + options.fullGC ?? (this.configs.runFullGC === true || this.autoRecovery.useFullGC()); // Add the options that are used to run GC to the telemetry context. telemetryContext?.setMultiple("fluid_GC", "Options", { @@ -521,6 +556,7 @@ export class GarbageCollector implements IGarbageCollector { await this.telemetryTracker.logPendingEvents(logger); // Update the state of summary state tracker from this run's stats. this.summaryStateTracker.updateStateFromGCRunStats(gcStats); + this.autoRecovery.onCompletedGCRun(); this.newReferencesSinceLastRun.clear(); this.completedRuns++; @@ -857,6 +893,7 @@ export class GarbageCollector implements IGarbageCollector { * Called to refresh the latest summary state. This happens when either a pending summary is acked. */ public async refreshLatestSummary(result: IRefreshSummaryResult): Promise { + this.autoRecovery.onSummaryAck(); return this.summaryStateTracker.refreshLatestSummary(result); } @@ -891,7 +928,7 @@ export class GarbageCollector implements IGarbageCollector { // In case the cause of the TombstoneLoaded event is incorrect GC Data (i.e. the object is actually reachable), // do fullGC on the next run to get a chance to repair (in the likely case the bug is not deterministic) - this.summaryStateTracker.autoRecovery.requestFullGCOnNextRun(); + this.autoRecovery.requestFullGCOnNextRun(); break; } default: diff --git a/packages/runtime/container-runtime/src/gc/gcSummaryStateTracker.ts b/packages/runtime/container-runtime/src/gc/gcSummaryStateTracker.ts index b64affb9a580..da6367ddf753 100644 --- a/packages/runtime/container-runtime/src/gc/gcSummaryStateTracker.ts +++ b/packages/runtime/container-runtime/src/gc/gcSummaryStateTracker.ts @@ -50,18 +50,6 @@ export class GCSummaryStateTracker { // to unreferenced or vice-versa. public updatedDSCountSinceLastSummary: number = 0; - /** API for ensuring the correct auto-recovery mitigations */ - public autoRecovery = { - requestFullGCOnNextRun: () => { - this.fullGCModeForAutoRecovery = true; - }, - fullGCRequested: () => { - return this.fullGCModeForAutoRecovery; - }, - }; - /** If true, the next GC run will do fullGC mode to regenerate the GC data for each node */ - private fullGCModeForAutoRecovery: boolean = false; - constructor( // Tells whether GC should run or not. private readonly configs: Pick< @@ -235,7 +223,6 @@ export class GCSummaryStateTracker { this.latestSummaryData = this.pendingSummaryData; this.pendingSummaryData = undefined; this.updatedDSCountSinceLastSummary = 0; - this.fullGCModeForAutoRecovery = false; } /** diff --git a/packages/runtime/container-runtime/src/test/gc/garbageCollection.spec.ts b/packages/runtime/container-runtime/src/test/gc/garbageCollection.spec.ts index 429555b17096..c796ac2873fa 100644 --- a/packages/runtime/container-runtime/src/test/gc/garbageCollection.spec.ts +++ b/packages/runtime/container-runtime/src/test/gc/garbageCollection.spec.ts @@ -78,9 +78,11 @@ type GcWithPrivates = IGarbageCollector & { { latestSummaryGCVersion: GCVersion; latestSummaryData: IGCSummaryTrackingData | undefined; - fullGCModeForAutoRecovery: boolean; } >; + readonly autoRecovery: { + useFullGC: () => boolean; + }; readonly telemetryTracker: GCTelemetryTracker; readonly mc: MonitoringContext; readonly sessionExpiryTimer: Omit & { defaultTimeout: number }; @@ -330,6 +332,29 @@ describe("Garbage Collection Tests", () => { }); }); + it("Private Autorecovery API", () => { + const autoRecovery: { + useFullGC: () => boolean; + requestFullGCOnNextRun: () => void; + onCompletedGCRun: () => void; + onSummaryAck: () => void; + } = createGarbageCollector().autoRecovery as any; + + assert.equal(autoRecovery.useFullGC(), false, "Expect false by default"); + + autoRecovery.requestFullGCOnNextRun(); + assert.equal(autoRecovery.useFullGC(), true, "Expect true after requesting full GC"); + + autoRecovery.onSummaryAck(); + assert.equal(autoRecovery.useFullGC(), true, "Expect true still after early Summary Ack"); + + autoRecovery.onCompletedGCRun(); + assert.equal(autoRecovery.useFullGC(), true, "Expect true still after full GC alone"); + + autoRecovery.onSummaryAck(); + assert.equal(autoRecovery.useFullGC(), false, "Expect false after post-GC Summary Ack"); + }); + describe("Tombstone and Sweep", () => { it("Tombstone then Delete", async () => { // Simple starting reference graph - root and two nodes @@ -441,13 +466,14 @@ describe("Garbage Collection Tests", () => { ); corruptedGCData.gcNodes["/"] = [nodes[1]]; - // getGCData set up to sometimes return the corrupted data + // getGCData set up to return the corrupted data unless fullGC is true gc = createGarbageCollector({ createParams: { gcOptions: { enableGCSweep: true } }, // Required to run AutoRecovery getGCData: async (fullGC?: boolean) => { return fullGC ? defaultGCData : corruptedGCData; }, }); + // These spies will let us monitor how each of these functions are called (or not) during runSweepPhase. // The original behavior of the function is preserved, but we can check how it was called. const spies = { @@ -527,22 +553,32 @@ describe("Garbage Collection Tests", () => { // Simulate a successful GC/Summary. // GC Data corruption should be fixed (nodes[0] should be referenced again) and autorecovery fullGC state should be reset spies.gc.runGC.resetHistory(); + // BUG FIX: Start with a spurious summary ack arriving before GC runs. It should not yet reset the autoRecovery state. + await gc.refreshLatestSummary({ + isSummaryTracked: true, + isSummaryNewer: false, + }); + assert( + gc.autoRecovery.useFullGC(), + "autoRecovery.useFullGC should still be true after spurious summary ack (haven't run GC yet)", + ); await gc.collectGarbage({}); assert( spies.gc.runGC.calledWith(/* fullGC: */ true), "runGC should be called with fullGC true", ); + // This matters in case GC runs but the summary fails. We'll need to run with fullGC again next time. assert( - gc.summaryStateTracker.fullGCModeForAutoRecovery, - "fullGCModeForAutoRecovery should NOT have been reset to false yet", + gc.autoRecovery.useFullGC(), + "autoRecovery.useFullGC should still be true after GC run but before summary ack", ); - await gc.summaryStateTracker.refreshLatestSummary({ + await gc.refreshLatestSummary({ isSummaryTracked: true, isSummaryNewer: false, }); assert( - !gc.summaryStateTracker.fullGCModeForAutoRecovery, - "fullGCModeForAutoRecovery should have been reset to false now", + !gc.autoRecovery.useFullGC(), + "autoRecovery.useFullGC should have been reset to false now", ); // Lastly, confirm that the node was successfully restored diff --git a/packages/runtime/container-runtime/src/test/gc/gcSummaryStateTracker.spec.ts b/packages/runtime/container-runtime/src/test/gc/gcSummaryStateTracker.spec.ts index b4b1c7785eb3..197732656746 100644 --- a/packages/runtime/container-runtime/src/test/gc/gcSummaryStateTracker.spec.ts +++ b/packages/runtime/container-runtime/src/test/gc/gcSummaryStateTracker.spec.ts @@ -13,47 +13,13 @@ import { import { GCSummaryStateTracker, - GCVersion, IGCStats, IGarbageCollectionState, gcStateBlobKey, nextGCVersion, } from "../../gc/index.js"; -type GCSummaryStateTrackerWithPrivates = Omit< - GCSummaryStateTracker, - "latestSummaryGCVersion" -> & { - latestSummaryGCVersion: GCVersion; -}; - describe("GCSummaryStateTracker tests", () => { - it("Autorecovery: requesting Full GC", async () => { - const tracker: GCSummaryStateTrackerWithPrivates = new GCSummaryStateTracker({ - gcAllowed: true, - gcVersionInBaseSnapshot: 1, - gcVersionInEffect: 1, - }) as any; - assert.equal(tracker.autoRecovery.fullGCRequested(), false, "Should be false by default"); - - tracker.autoRecovery.requestFullGCOnNextRun(); - - assert.equal( - tracker.autoRecovery.fullGCRequested(), - true, - "Should be true after requesting full GC", - ); - - // After the first summary succeeds (refreshLatestSummary called), the state should be reset. - await tracker.refreshLatestSummary({ isSummaryTracked: true, isSummaryNewer: true }); - - assert.equal( - tracker.autoRecovery.fullGCRequested(), - false, - "Should be false after Summary Ack", - ); - }); - /** * These tests validate that the GC data is written in summary incrementally. Basically, only parts of the GC * data that has changed since the last successful summary is re-written, rest is written as SummaryHandle. From 5b71dbe6b637e0e450a0ac8449ff35b8c0d581f8 Mon Sep 17 00:00:00 2001 From: Michael Zhen <112977307+zhenmichael@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:53:56 -0800 Subject: [PATCH 14/40] (docs): update pipeline agent pool (#23185) update pipeline agent pool to eastus2 for deploy-website and publish-api-artifact-model pipelines --- tools/pipelines/build-docs.yml | 4 ++-- tools/pipelines/deploy-website.yml | 2 +- tools/pipelines/publish-api-model-artifact.yml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/pipelines/build-docs.yml b/tools/pipelines/build-docs.yml index 42b89ecf2bcf..5c4b8936445f 100644 --- a/tools/pipelines/build-docs.yml +++ b/tools/pipelines/build-docs.yml @@ -91,7 +91,7 @@ stages: # also, the artifact will be uploaded as latest-v*.tar.gz where * is the major version - stage: check_branch_version displayName: 'Check Version Deployment Condition' - pool: Small + pool: Small-eastus2 jobs: - job: check_branch_version displayName: 'Check Version Deployment Condition' @@ -322,7 +322,7 @@ stages: - stage: deploy displayName: 'Deploy website' - pool: Small + pool: Small-eastus2 dependsOn: ['build', 'guardian'] variables: deployOverrideVar: ${{ parameters.deployOverride }} diff --git a/tools/pipelines/deploy-website.yml b/tools/pipelines/deploy-website.yml index 8cf3b44c7e2c..cbd664556c17 100644 --- a/tools/pipelines/deploy-website.yml +++ b/tools/pipelines/deploy-website.yml @@ -262,7 +262,7 @@ stages: - stage: deploy displayName: 'Deploy website' - pool: Small + pool: Small-eastus2 dependsOn: ['build', 'guardian'] variables: deployOverrideVar: ${{ parameters.deployOverride }} diff --git a/tools/pipelines/publish-api-model-artifact.yml b/tools/pipelines/publish-api-model-artifact.yml index 5595ec4a1d90..21c29c521704 100644 --- a/tools/pipelines/publish-api-model-artifact.yml +++ b/tools/pipelines/publish-api-model-artifact.yml @@ -91,7 +91,7 @@ stages: # also, the artifact will be uploaded as latest-v*.tar.gz where * is the major version - stage: check_branch_version displayName: 'Check Version Deployment Condition' - pool: Small + pool: Small-eastus2 jobs: - job: check_branch_version displayName: 'Check Version Deployment Condition' @@ -130,7 +130,7 @@ stages: displayName: 'Combine api-extractor JSON' dependsOn: check_branch_version environment: 'fluid-docs-env' - pool: Large + pool: Large-eastus2 variables: uploadAsLatestRelease: $[ dependencies.check_branch_version.outputs['SetShouldDeploy.shouldDeploy'] ] majorVersion: $[ dependencies.check_branch_version.outputs['SetShouldDeploy.majorVersion'] ] @@ -151,7 +151,7 @@ stages: # this ensures that the generated website is up-to-date with the latest changes - stage: deploy displayName: 'Deploy website' - pool: Small + pool: Small-eastus2 dependsOn: ['check_branch_version'] jobs: - job: deploy_site From 0e2ef75ce2c6a52aa3560c62815c9309f5815c2f Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:37:39 -0800 Subject: [PATCH 15/40] improvement(eslint-config-fluid): Update `@typescript-eslint/consistent-type-exports` auto-fix behavior (#23188) Updates auto-fix policy for `@typescript-eslint/consistent-type-exports` to prefer inline `type` annotations, rather than splitting exports into type-only and non-type-only groups. This makes it easier to tell at a glance how the auto-fix changes affect individual exports when a list of exports is large. It also makes it easier to detect issues in edge-cases where the the rule is applied incorrectly. E.g.: ```typescript export { type Foo, Bar } from "./baz.js"; ``` instead of: ```typescript export type { Foo } from "./baz.js"; export { Bar } from "./baz.js"; ``` [AB#22618](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/22618) --- common/build/eslint-config-fluid/CHANGELOG.md | 19 +++++++++++++++++++ common/build/eslint-config-fluid/package.json | 2 +- .../printed-configs/strict.json | 2 +- common/build/eslint-config-fluid/strict.js | 5 ++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/common/build/eslint-config-fluid/CHANGELOG.md b/common/build/eslint-config-fluid/CHANGELOG.md index 2d8d4bee4963..98b78e9bf257 100644 --- a/common/build/eslint-config-fluid/CHANGELOG.md +++ b/common/build/eslint-config-fluid/CHANGELOG.md @@ -1,5 +1,24 @@ # @fluidframework/eslint-config-fluid Changelog +## [5.5.2](https://github.com/microsoft/FluidFramework/releases/tag/eslint-config-fluid_v5.5.2) + +Update auto-fix policy for `@typescript-eslint/consistent-type-exports` to prefer inline `type` annotations, rather than splitting exports into type-only and non-type-only groups. +This makes it easier to tell at a glance how the auto-fix changes affect individual exports when a list of exports is large. +It also makes it easier to detect issues in edge-cases where the the rule is applied incorrectly. + +E.g.: + +```typescript +export { type Foo, Bar } from "./baz.js"; +``` + +instead of: + +```typescript +export type { Foo } from "./baz.js"; +export { Bar } from "./baz.js"; +``` + ## [5.5.1](https://github.com/microsoft/FluidFramework/releases/tag/eslint-config-fluid_v5.5.1) ### Disabled rules diff --git a/common/build/eslint-config-fluid/package.json b/common/build/eslint-config-fluid/package.json index 6946c1775ed9..dfc6ff61134a 100644 --- a/common/build/eslint-config-fluid/package.json +++ b/common/build/eslint-config-fluid/package.json @@ -1,6 +1,6 @@ { "name": "@fluidframework/eslint-config-fluid", - "version": "5.5.1", + "version": "5.5.2", "description": "Shareable ESLint config for the Fluid Framework", "homepage": "https://fluidframework.com", "repository": { diff --git a/common/build/eslint-config-fluid/printed-configs/strict.json b/common/build/eslint-config-fluid/printed-configs/strict.json index 68f5bcfacbce..5dd3dcbba129 100644 --- a/common/build/eslint-config-fluid/printed-configs/strict.json +++ b/common/build/eslint-config-fluid/printed-configs/strict.json @@ -106,7 +106,7 @@ "@typescript-eslint/consistent-type-exports": [ "error", { - "fixMixedExportsWithInlineTypeSpecifier": false + "fixMixedExportsWithInlineTypeSpecifier": true } ], "@typescript-eslint/consistent-type-imports": [ diff --git a/common/build/eslint-config-fluid/strict.js b/common/build/eslint-config-fluid/strict.js index 43727ddd4518..d90b75c78795 100644 --- a/common/build/eslint-config-fluid/strict.js +++ b/common/build/eslint-config-fluid/strict.js @@ -78,7 +78,10 @@ module.exports = { */ "@typescript-eslint/consistent-type-exports": [ "error", - { fixMixedExportsWithInlineTypeSpecifier: false }, + { + // Makes it easier to tell, at a glance, the impact of a change to individual exports. + fixMixedExportsWithInlineTypeSpecifier: true, + }, ], /** From d34877ec0db66aa6d8046935e6cb766ad9b86973 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Fri, 22 Nov 2024 15:18:52 -0800 Subject: [PATCH 16/40] Odsp: Automatically Rename Temp File (#21622) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Automatic File Renaming Currently, the process of handling detached blobs in Fluid involves several steps: 1. Users need to pass a detached blob storage interface to the loader. 2. Blobs are stored in this detached storage and later uploaded during the container attachment process. 3. The attachment process involves creating a 0-byte file, uploading blobs, and then uploading the summary. 4. The file is initially created with a temporary extension, and it is the user's responsibility to rename the file to its final name after the attachment process is complete. This step is not well-documented and can be complex for users. ## Motivations for Improvement The primary motivations for improving this process include: • Simplification for Users: By automating the file renaming process, users are relieved from the burden of managing the rename themselves, which is currently a source of confusion and errors. • Transparency: Automating the rename ensures that the process is transparent to users, making the use of detached blobs more seamless and reducing the need for extensive documentation and user intervention. • Error Reduction: Automating the rename reduces the risk of errors related to file naming, which can affect file discovery, sharing, and application logic that depends on specific file names. --- ...dsp-driver-definitions.legacy.alpha.api.md | 1 + .../src/resolvedUrl.ts | 5 ++ .../odsp-driver.legacy.alpha.api.md | 2 +- packages/drivers/odsp-driver/package.json | 9 ++- packages/drivers/odsp-driver/src/contracts.ts | 7 ++ .../odsp-driver/src/createFile/createFile.ts | 80 +++++++++++++++++-- .../src/createFile/createNewModule.ts | 2 +- .../drivers/odsp-driver/src/epochTracker.ts | 3 +- .../src/odspDocumentStorageManager.ts | 28 +++++++ .../src/test/createNewUtilsTests.spec.ts | 43 ++++++++++ .../validateOdspDriverPrevious.generated.ts | 2 + .../src/test/blobs.spec.ts | 24 ++++-- 12 files changed, 192 insertions(+), 14 deletions(-) diff --git a/packages/drivers/odsp-driver-definitions/api-report/odsp-driver-definitions.legacy.alpha.api.md b/packages/drivers/odsp-driver-definitions/api-report/odsp-driver-definitions.legacy.alpha.api.md index 45699779bda1..8730a54bbbb7 100644 --- a/packages/drivers/odsp-driver-definitions/api-report/odsp-driver-definitions.legacy.alpha.api.md +++ b/packages/drivers/odsp-driver-definitions/api-report/odsp-driver-definitions.legacy.alpha.api.md @@ -96,6 +96,7 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { isClpCompliantApp?: boolean; // (undocumented) odspResolvedUrl: true; + pendingRename?: string; shareLinkInfo?: ShareLinkInfoType; // (undocumented) summarizer: boolean; diff --git a/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts b/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts index 6b1a23096b27..7e01aaf216b2 100644 --- a/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts +++ b/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts @@ -122,6 +122,11 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { tokens: {}; fileName: string; + /** + * Used to track when a file was created with a temporary name. In that case this value will + * be the desired name, which the file is eventually renamed too. + */ + pendingRename?: string; summarizer: boolean; diff --git a/packages/drivers/odsp-driver/api-report/odsp-driver.legacy.alpha.api.md b/packages/drivers/odsp-driver/api-report/odsp-driver.legacy.alpha.api.md index 30cae08778c6..c5bb2e7780b5 100644 --- a/packages/drivers/odsp-driver/api-report/odsp-driver.legacy.alpha.api.md +++ b/packages/drivers/odsp-driver/api-report/odsp-driver.legacy.alpha.api.md @@ -54,7 +54,7 @@ export class EpochTracker implements IPersistedFileCache { } // @alpha (undocumented) -export type FetchType = "blob" | "createBlob" | "createFile" | "joinSession" | "ops" | "test" | "snapshotTree" | "treesLatest" | "uploadSummary" | "push" | "versions"; +export type FetchType = "blob" | "createBlob" | "createFile" | "joinSession" | "ops" | "test" | "snapshotTree" | "treesLatest" | "uploadSummary" | "push" | "versions" | "renameFile"; // @alpha (undocumented) export type FetchTypeInternal = FetchType | "cache"; diff --git a/packages/drivers/odsp-driver/package.json b/packages/drivers/odsp-driver/package.json index 4385de37fcd1..86081211cb6d 100644 --- a/packages/drivers/odsp-driver/package.json +++ b/packages/drivers/odsp-driver/package.json @@ -159,7 +159,14 @@ "typescript": "~5.4.5" }, "typeValidation": { - "broken": {}, + "broken": { + "TypeAlias_FetchTypeInternal": { + "backCompat": false + }, + "TypeAlias_FetchType": { + "backCompat": false + } + }, "entrypoint": "legacy" } } diff --git a/packages/drivers/odsp-driver/src/contracts.ts b/packages/drivers/odsp-driver/src/contracts.ts index a25fe6a10645..fcc1fc3e22fd 100644 --- a/packages/drivers/odsp-driver/src/contracts.ts +++ b/packages/drivers/odsp-driver/src/contracts.ts @@ -189,6 +189,13 @@ export interface ICreateFileResponse { "sharing"?: any; "sharingLink"?: string; "sharingLinkErrorReason"?: string; + "name": string; +} + +export interface IRenameFileResponse { + "@odata.context": string; + "id": string; + "name": string; } export interface IVersionedValueWithEpoch { diff --git a/packages/drivers/odsp-driver/src/createFile/createFile.ts b/packages/drivers/odsp-driver/src/createFile/createFile.ts index ab283dd23afd..f977500a4953 100644 --- a/packages/drivers/odsp-driver/src/createFile/createFile.ts +++ b/packages/drivers/odsp-driver/src/createFile/createFile.ts @@ -13,6 +13,7 @@ import { InstrumentedStorageTokenFetcher, OdspErrorTypes, ShareLinkInfoType, + type IOdspUrlParts, } from "@fluidframework/odsp-driver-definitions/internal"; import { ITelemetryLoggerExt, @@ -20,7 +21,7 @@ import { PerformanceEvent, } from "@fluidframework/telemetry-utils/internal"; -import { ICreateFileResponse } from "./../contracts.js"; +import { ICreateFileResponse, type IRenameFileResponse } from "./../contracts.js"; import { ClpCompliantAppHeader } from "./../contractsPublic.js"; import { createOdspUrl } from "./../createOdspUrl.js"; import { EpochTracker } from "./../epochTracker.js"; @@ -74,10 +75,18 @@ export async function createNewFluidFile( } let itemId: string; + let pendingRename: string | undefined; let summaryHandle: string = ""; let shareLinkInfo: ShareLinkInfoType | undefined; if (createNewSummary === undefined) { - itemId = await createNewEmptyFluidFile(getAuthHeader, newFileInfo, logger, epochTracker); + const content = await createNewEmptyFluidFile( + getAuthHeader, + newFileInfo, + logger, + epochTracker, + ); + itemId = content.itemId; + pendingRename = newFileInfo.filename; } else { const content = await createNewFluidFileFromSummary( getAuthHeader, @@ -103,6 +112,7 @@ export async function createNewFluidFile( fileEntry.resolvedUrl = odspResolvedUrl; odspResolvedUrl.shareLinkInfo = shareLinkInfo; + odspResolvedUrl.pendingRename = pendingRename; if (createNewSummary !== undefined && createNewCaching) { assert(summaryHandle !== undefined, 0x203 /* "Summary handle is undefined" */); @@ -178,9 +188,8 @@ export async function createNewEmptyFluidFile( newFileInfo: INewFileInfo, logger: ITelemetryLoggerExt, epochTracker: EpochTracker, -): Promise { +): Promise<{ itemId: string; fileName: string }> { const filePath = encodeFilePath(newFileInfo.filePath); - // add .tmp extension to empty file (host is expected to rename) const encodedFilename = encodeURIComponent(`${newFileInfo.filename}.tmp`); const initialUrl = `${getApiRoot(new URL(newFileInfo.siteUrl))}/drives/${ newFileInfo.driveId @@ -228,7 +237,68 @@ export async function createNewEmptyFluidFile( event.end({ ...fetchResponse.propsToLog, }); - return content.id; + return { itemId: content.id, fileName: content.name }; + }, + { end: true, cancel: "error" }, + ); + }); +} + +export async function renameEmptyFluidFile( + getAuthHeader: InstrumentedStorageTokenFetcher, + odspParts: IOdspUrlParts, + requestedFileName: string, + logger: ITelemetryLoggerExt, + epochTracker: EpochTracker, +): Promise { + const initialUrl = `${getApiRoot(new URL(odspParts.siteUrl))}/drives/${ + odspParts.driveId + }/items/${odspParts.itemId}?@name.conflictBehavior=rename`; + + return getWithRetryForTokenRefresh(async (options) => { + const url = initialUrl; + const method = "PATCH"; + const authHeader = await getAuthHeader( + { ...options, request: { url, method } }, + "renameFile", + ); + + return PerformanceEvent.timedExecAsync( + logger, + { eventName: "renameFile" }, + async (event) => { + const headers = getHeadersWithAuth(authHeader); + headers["Content-Type"] = "application/json"; + + const fetchResponse = await runWithRetry( + async () => + epochTracker.fetchAndParseAsJSON( + url, + { + body: JSON.stringify({ + name: requestedFileName, + }), + headers, + method: "PATCH", + }, + "renameFile", + ), + "renameFile", + logger, + ); + + const content = fetchResponse.content; + if (!content?.id) { + throw new NonRetryableError( + "ODSP RenameFile call returned no item ID (for empty file)", + OdspErrorTypes.incorrectServerResponse, + { driverVersion }, + ); + } + event.end({ + ...fetchResponse.propsToLog, + }); + return content; }, { end: true, cancel: "error" }, ); diff --git a/packages/drivers/odsp-driver/src/createFile/createNewModule.ts b/packages/drivers/odsp-driver/src/createFile/createNewModule.ts index 0690a35ab96b..8e5e1e60cc48 100644 --- a/packages/drivers/odsp-driver/src/createFile/createNewModule.ts +++ b/packages/drivers/odsp-driver/src/createFile/createNewModule.ts @@ -3,6 +3,6 @@ * Licensed under the MIT License. */ -export { createNewFluidFile } from "./createFile.js"; +export { createNewFluidFile, renameEmptyFluidFile } from "./createFile.js"; export { createNewContainerOnExistingFile } from "./createNewContainerOnExistingFile.js"; export { convertCreateNewSummaryTreeToTreeAndBlobs } from "./createNewUtils.js"; diff --git a/packages/drivers/odsp-driver/src/epochTracker.ts b/packages/drivers/odsp-driver/src/epochTracker.ts index 4c1feecc4d2f..a956da10698c 100644 --- a/packages/drivers/odsp-driver/src/epochTracker.ts +++ b/packages/drivers/odsp-driver/src/epochTracker.ts @@ -61,7 +61,8 @@ export type FetchType = | "treesLatest" | "uploadSummary" | "push" - | "versions"; + | "versions" + | "renameFile"; /** * @legacy diff --git a/packages/drivers/odsp-driver/src/odspDocumentStorageManager.ts b/packages/drivers/odsp-driver/src/odspDocumentStorageManager.ts index 6358656002b9..7921c6fa9916 100644 --- a/packages/drivers/odsp-driver/src/odspDocumentStorageManager.ts +++ b/packages/drivers/odsp-driver/src/odspDocumentStorageManager.ts @@ -43,6 +43,7 @@ import { ISnapshotCachedEntry2, IVersionedValueWithEpoch, } from "./contracts.js"; +import { useCreateNewModule } from "./createFile/index.js"; import { EpochTracker } from "./epochTracker.js"; import { ISnapshotRequestAndResponseOptions, @@ -769,6 +770,33 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase { 0x56e /* summary upload manager should have been initialized */, ); const id = await this.odspSummaryUploadManager.writeSummaryTree(summary, context); + const { pendingRename } = this.odspResolvedUrl; + if ( + pendingRename !== undefined && + this.config.getBoolean("Fluid.Driver.Odsp.disablePendingRename") !== true + ) { + // This is a temporary file, so we need to rename it to remove the .tmp extension + // This should only happen for the initial summary upload for a new file + assert( + context.ackHandle === undefined && + context.proposalHandle === undefined && + context.referenceSequenceNumber === 0, + "temporaryFileName should only be set for new file creation in the empty file create flow", + ); + + const renameResponse = await useCreateNewModule(this.logger, async (m) => + m.renameEmptyFluidFile( + this.getAuthHeader, + this.odspResolvedUrl, + pendingRename, + this.logger, + this.epochTracker, + ), + ); + this.odspResolvedUrl.pendingRename = undefined; + this.odspResolvedUrl.fileName = renameResponse.name; + } + return id; } diff --git a/packages/drivers/odsp-driver/src/test/createNewUtilsTests.spec.ts b/packages/drivers/odsp-driver/src/test/createNewUtilsTests.spec.ts index 2b36a7f105a9..cb1960dcb44c 100644 --- a/packages/drivers/odsp-driver/src/test/createNewUtilsTests.spec.ts +++ b/packages/drivers/odsp-driver/src/test/createNewUtilsTests.spec.ts @@ -181,6 +181,49 @@ describe("Create New Utils Tests", () => { await epochTracker.removeEntries().catch(() => {}); }); + it("createNewFluidFile with undefined summary and rename it", async () => { + const odspResolvedUrl = await useCreateNewModule(createChildLogger(), async (module) => + mockFetchOk( + async () => + module.createNewFluidFile( + async (_options) => "token", + newFileParams, + createChildLogger(), + undefined, + epochTracker, + fileEntry, + true /* createNewCaching */, + false /* forceAccessTokenViaAuthorizationHeader */, + ), + { itemId: "itemId1", id: "Summary handle" }, + { "x-fluid-epoch": "epoch1" }, + ), + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const snapshot = await epochTracker.get(createCacheSnapshotKey(odspResolvedUrl, false)); + assert(snapshot === undefined); + + assert(odspResolvedUrl.pendingRename === "filename"); + + const renameResponse = await useCreateNewModule(createChildLogger(), async (module) => + mockFetchOk( + async () => + module.renameEmptyFluidFile( + async (_options) => "token", + odspResolvedUrl, + odspResolvedUrl.pendingRename!, + createChildLogger(), + epochTracker, + ), + { id: "Summary handle", name: "filename" }, + { "x-fluid-epoch": "epoch1" }, + ), + ); + + assert(renameResponse.name === "filename"); + }); + it("Should cache converted summary during createNewContainerOnExistingFile", async () => { const existingFileParams: IExistingFileInfo = { type: "Existing", diff --git a/packages/drivers/odsp-driver/src/test/types/validateOdspDriverPrevious.generated.ts b/packages/drivers/odsp-driver/src/test/types/validateOdspDriverPrevious.generated.ts index 0c36d9f293e3..69a82817a31d 100644 --- a/packages/drivers/odsp-driver/src/test/types/validateOdspDriverPrevious.generated.ts +++ b/packages/drivers/odsp-driver/src/test/types/validateOdspDriverPrevious.generated.ts @@ -427,6 +427,7 @@ declare type old_as_current_for_TypeAlias_FetchType = requireAssignableTo, TypeOnly> /* @@ -445,6 +446,7 @@ declare type old_as_current_for_TypeAlias_FetchTypeInternal = requireAssignableT * typeValidation.broken: * "TypeAlias_FetchTypeInternal": {"backCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type current_as_old_for_TypeAlias_FetchTypeInternal = requireAssignableTo, TypeOnly> /* diff --git a/packages/test/test-end-to-end-tests/src/test/blobs.spec.ts b/packages/test/test-end-to-end-tests/src/test/blobs.spec.ts index a51370cc537e..104af7bcf784 100644 --- a/packages/test/test-end-to-end-tests/src/test/blobs.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/blobs.spec.ts @@ -71,9 +71,23 @@ const usageErrorMessage = "Empty file summary creation isn't supported in this d const containerCloseAndDisposeUsageErrors = [ { eventName: "fluid:telemetry:Container:ContainerClose", error: usageErrorMessage }, ]; -const ContainerCloseUsageError: ExpectedEvents = { +const ContainerStateEventsOrErrors: ExpectedEvents = { routerlicious: containerCloseAndDisposeUsageErrors, tinylicious: containerCloseAndDisposeUsageErrors, + odsp: [ + { + eventName: "fluid:telemetry:OdspDriver:createNewEmptyFile_end", + containerAttachState: "Detached", + }, + { + eventName: "fluid:telemetry:OdspDriver:uploadSummary_end", + containerAttachState: "Attaching", + }, + { + eventName: "fluid:telemetry:OdspDriver:renameFile_end", + containerAttachState: "Attaching", + }, + ], }; describeCompat("blobs", "FullCompat", (getTestObjectProvider, apis) => { @@ -444,7 +458,7 @@ function serializationTests({ for (const summarizeProtocolTree of [undefined, true, false]) { itExpects( `works in detached container. summarizeProtocolTree: ${summarizeProtocolTree}`, - ContainerCloseUsageError, + ContainerStateEventsOrErrors, async function () { const loader = provider.makeTestLoader({ ...testContainerConfig, @@ -611,7 +625,7 @@ function serializationTests({ itExpects( "redirect table saved in snapshot", - ContainerCloseUsageError, + ContainerStateEventsOrErrors, async function () { // test with and without offline load enabled const offlineCfg = { @@ -687,7 +701,7 @@ function serializationTests({ itExpects( "serialize/rehydrate then attach", - ContainerCloseUsageError, + ContainerStateEventsOrErrors, async function () { const loader = provider.makeTestLoader({ ...testContainerConfig, @@ -741,7 +755,7 @@ function serializationTests({ itExpects( "serialize/rehydrate multiple times then attach", - ContainerCloseUsageError, + ContainerStateEventsOrErrors, async function () { const loader = provider.makeTestLoader({ ...testContainerConfig, From 4e4b3e737a788dbd7dbb74c943db60bbd903640b Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Fri, 22 Nov 2024 15:19:17 -0800 Subject: [PATCH 17/40] feat(eslint-config-fluid): Add strict-biome config for use in conjunction with Biome linter (#22960) Adds a strict config to the shared eslint package that can be used in projects to use Biome for linting in conjunction with eslint. This config is the same as the strict config, but disables rules that have a Biome equivalent. To make it easier to see the differences, I also updated the strict config - this __will not__ be merged. --- common/build/eslint-config-fluid/CHANGELOG.md | 10 +- common/build/eslint-config-fluid/README.md | 7 + common/build/eslint-config-fluid/package.json | 6 +- .../build/eslint-config-fluid/pnpm-lock.yaml | 24 +- .../printed-configs/strict-biome.json | 2076 +++++++++++++++++ .../build/eslint-config-fluid/strict-biome.js | 20 + 6 files changed, 2131 insertions(+), 12 deletions(-) create mode 100644 common/build/eslint-config-fluid/printed-configs/strict-biome.json create mode 100644 common/build/eslint-config-fluid/strict-biome.js diff --git a/common/build/eslint-config-fluid/CHANGELOG.md b/common/build/eslint-config-fluid/CHANGELOG.md index 98b78e9bf257..aa2e7718b93b 100644 --- a/common/build/eslint-config-fluid/CHANGELOG.md +++ b/common/build/eslint-config-fluid/CHANGELOG.md @@ -1,6 +1,14 @@ # @fluidframework/eslint-config-fluid Changelog -## [5.5.2](https://github.com/microsoft/FluidFramework/releases/tag/eslint-config-fluid_v5.5.2) +## [5.6.0](https://github.com/microsoft/FluidFramework/releases/tag/eslint-config-fluid_v5.6.0) + +### New config for use with Biome linter + +A new strict-biome config is available that disables all rules that Biome's recommended config includes. +This config is intended to be used in projects that use both eslint and Biome for linting. +This config is considered experimental. + +### Auto-fix behavior change for @typescript-eslint/consistent-type-exports Update auto-fix policy for `@typescript-eslint/consistent-type-exports` to prefer inline `type` annotations, rather than splitting exports into type-only and non-type-only groups. This makes it easier to tell at a glance how the auto-fix changes affect individual exports when a list of exports is large. diff --git a/common/build/eslint-config-fluid/README.md b/common/build/eslint-config-fluid/README.md index f8c964c588fc..0d2b06e62000 100644 --- a/common/build/eslint-config-fluid/README.md +++ b/common/build/eslint-config-fluid/README.md @@ -24,6 +24,12 @@ In particular, use of this config is encouraged for libraries with public facing Imported via `@fluidframework/eslint-config-fluid/strict`. +### Strict-Biome + +A version of the "strict" config that disables rules that are supported by Biome's "recommended" lint config. +This config is intended to be used in projects that use both eslint and Biome for linting. +This config is considered experimental. + ## Changing the lint config If you want to change the shared lint config (that is, this package), you need to do the following: @@ -64,6 +70,7 @@ a diff to review as part of a PR -- just like we do with API reports for code ch | `print-config:react` | `eslint --config ./index.js --print-config ./src/file.tsx > ./printed-configs/react.json` | | `print-config:recommended` | `eslint --config ./recommended.js --print-config ./src/file.ts > ./printed-configs/recommended.json` | | `print-config:strict` | `eslint --config ./strict.js --print-config ./src/file.ts > ./printed-configs/strict.json` | +| `print-config:strict-biome` | `eslint --config ./strict-biome.js --print-config ./src/file.ts > ./printed-configs/strict-biome.json` | | `print-config:test` | Print the eslint config for test files (`eslint --config index.js --print-config src/test/file.ts`). | | `test` | `echo TODO: add tests in @fluidframework/eslint-config-fluid` | diff --git a/common/build/eslint-config-fluid/package.json b/common/build/eslint-config-fluid/package.json index dfc6ff61134a..506b2d58d638 100644 --- a/common/build/eslint-config-fluid/package.json +++ b/common/build/eslint-config-fluid/package.json @@ -1,6 +1,6 @@ { "name": "@fluidframework/eslint-config-fluid", - "version": "5.5.2", + "version": "5.6.0", "description": "Shareable ESLint config for the Fluid Framework", "homepage": "https://fluidframework.com", "repository": { @@ -24,17 +24,19 @@ "print-config:react": "eslint --config ./index.js --print-config ./src/file.tsx > ./printed-configs/react.json", "print-config:recommended": "eslint --config ./recommended.js --print-config ./src/file.ts > ./printed-configs/recommended.json", "print-config:strict": "eslint --config ./strict.js --print-config ./src/file.ts > ./printed-configs/strict.json", + "print-config:strict-biome": "eslint --config ./strict-biome.js --print-config ./src/file.ts > ./printed-configs/strict-biome.json", "print-config:test": "eslint --config ./index.js --print-config ./src/test/file.ts > ./printed-configs/test.json", "test": "echo TODO: add tests" }, "dependencies": { - "@fluid-internal/eslint-plugin-fluid": "^0.1.2", + "@fluid-internal/eslint-plugin-fluid": "^0.1.3", "@microsoft/tsdoc": "^0.14.2", "@rushstack/eslint-patch": "~1.4.0", "@rushstack/eslint-plugin": "~0.13.1", "@rushstack/eslint-plugin-security": "~0.7.1", "@typescript-eslint/eslint-plugin": "~6.7.5", "@typescript-eslint/parser": "~6.7.5", + "eslint-config-biome": "~1.9.3", "eslint-config-prettier": "~9.0.0", "eslint-import-resolver-typescript": "~3.6.3", "eslint-plugin-eslint-comments": "~3.2.0", diff --git a/common/build/eslint-config-fluid/pnpm-lock.yaml b/common/build/eslint-config-fluid/pnpm-lock.yaml index 185ed812581a..e038923c69ab 100644 --- a/common/build/eslint-config-fluid/pnpm-lock.yaml +++ b/common/build/eslint-config-fluid/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@fluid-internal/eslint-plugin-fluid': - specifier: ^0.1.2 - version: 0.1.2(eslint@8.55.0)(typescript@5.1.6) + specifier: ^0.1.3 + version: 0.1.3(eslint@8.55.0)(typescript@5.1.6) '@microsoft/tsdoc': specifier: ^0.14.2 version: 0.14.2 @@ -29,6 +29,9 @@ importers: '@typescript-eslint/parser': specifier: ~6.7.5 version: 6.7.5(eslint@8.55.0)(typescript@5.1.6) + eslint-config-biome: + specifier: ~1.9.3 + version: 1.9.3 eslint-config-prettier: specifier: ~9.0.0 version: 9.0.0(eslint@8.55.0) @@ -165,8 +168,8 @@ packages: resolution: {integrity: sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - /@fluid-internal/eslint-plugin-fluid@0.1.2(eslint@8.55.0)(typescript@5.1.6): - resolution: {integrity: sha512-E7LF4ukpCoyZcxpDUQz0edXsKllbh4m8NAdiug6sSI1KIIQFwtq5vvW3kQ0Op5xA9w10T6crfcvmuAzdP84UGg==} + /@fluid-internal/eslint-plugin-fluid@0.1.3(eslint@8.55.0)(typescript@5.1.6): + resolution: {integrity: sha512-l3MPEQ34lYP9QOIQ9vx9ncyvkYqR7ci7ZixxD4RdOmGBnAFOqipBjn5Je9AJfztQ3jWgcT3jiV+AVT3rBjc2Yw==} dependencies: '@microsoft/tsdoc': 0.14.2 '@typescript-eslint/parser': 6.21.0(eslint@8.55.0)(typescript@5.1.6) @@ -326,7 +329,7 @@ packages: resolution: {integrity: sha512-T0HO+VrU9VbLRiEx/kH4+gwGMHNMIGkp0Pok+p0I33saOOLyhfGvwOKQgvt2qkxzQEV2L5MtGB8EnW4r5d3CqQ==} dependencies: '@textlint/ast-node-types': 12.6.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.7 mdast-util-gfm-autolink-literal: 0.1.3 remark-footnotes: 3.0.0 remark-frontmatter: 3.0.0 @@ -498,7 +501,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.7 eslint: 8.55.0 typescript: 5.1.6 transitivePeerDependencies: @@ -617,11 +620,11 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.7 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.5.4 + semver: 7.6.3 ts-api-utils: 1.0.3(typescript@5.1.6) typescript: 5.1.6 transitivePeerDependencies: @@ -1137,7 +1140,6 @@ packages: optional: true dependencies: ms: 2.1.3 - dev: false /decamelize@4.0.0: resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} @@ -1392,6 +1394,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + /eslint-config-biome@1.9.3: + resolution: {integrity: sha512-Zrz6Z+Gtv1jqnfHsqvpwCSqclktvQF1OnyDAfDOvjzzck2c5Nw3crEHI2KLuH+LnNBttiPAb7Y7e8sF158sOgQ==} + dev: false + /eslint-config-prettier@9.0.0(eslint@8.55.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true diff --git a/common/build/eslint-config-fluid/printed-configs/strict-biome.json b/common/build/eslint-config-fluid/printed-configs/strict-biome.json new file mode 100644 index 000000000000..c6ef3b361b34 --- /dev/null +++ b/common/build/eslint-config-fluid/printed-configs/strict-biome.json @@ -0,0 +1,2076 @@ +{ + "env": { + "browser": true, + "es2024": false, + "es6": true, + "node": true + }, + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "ignorePatterns": [ + "**/packageVersion.ts" + ], + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": "latest", + "project": "./tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "eslint-comments", + "import", + "@fluid-internal/fluid", + "unused-imports", + "promise", + "jsdoc", + "@typescript-eslint", + "@rushstack/security", + "@rushstack", + "unicorn", + "tsdoc" + ], + "reportUnusedDisableDirectives": true, + "rules": { + "@babel/object-curly-spacing": [ + "off" + ], + "@babel/semi": [ + "off" + ], + "@eslint-community/eslint-plugin-mysticatea/no-this-in-static": [ + "off" + ], + "@fluid-internal/fluid/no-member-release-tags": [ + "error" + ], + "@fluid-internal/fluid/no-unchecked-record-access": [ + "error" + ], + "@rushstack/no-new-null": [ + "error" + ], + "@rushstack/typedef-var": [ + "off" + ], + "@typescript-eslint/adjacent-overload-signatures": [ + "error" + ], + "@typescript-eslint/array-type": [ + "error" + ], + "@typescript-eslint/await-thenable": [ + "error" + ], + "@typescript-eslint/ban-ts-comment": [ + "error" + ], + "@typescript-eslint/ban-tslint-comment": [ + "error" + ], + "@typescript-eslint/ban-types": [ + "off" + ], + "@typescript-eslint/block-spacing": [ + "off" + ], + "@typescript-eslint/brace-style": [ + "off" + ], + "@typescript-eslint/class-literal-property-style": [ + "error" + ], + "@typescript-eslint/comma-dangle": [ + "off", + "always-multiline" + ], + "@typescript-eslint/comma-spacing": [ + "off" + ], + "@typescript-eslint/consistent-generic-constructors": [ + "error" + ], + "@typescript-eslint/consistent-indexed-object-style": [ + "error" + ], + "@typescript-eslint/consistent-type-assertions": [ + "error", + { + "assertionStyle": "as", + "objectLiteralTypeAssertions": "never" + } + ], + "@typescript-eslint/consistent-type-definitions": [ + "error" + ], + "@typescript-eslint/consistent-type-exports": [ + "off", + { + "fixMixedExportsWithInlineTypeSpecifier": true + } + ], + "@typescript-eslint/consistent-type-imports": [ + "off", + { + "fixStyle": "separate-type-imports" + } + ], + "@typescript-eslint/default-param-last": [ + "off" + ], + "@typescript-eslint/dot-notation": [ + "off" + ], + "@typescript-eslint/explicit-function-return-type": [ + "error", + { + "allowExpressions": true, + "allowTypedFunctionExpressions": true, + "allowHigherOrderFunctions": true, + "allowDirectConstAssertionInArrowFunctions": true, + "allowConciseArrowFunctionExpressionsStartingWithVoid": false + } + ], + "@typescript-eslint/explicit-member-accessibility": [ + "off", + { + "accessibility": "explicit", + "overrides": { + "accessors": "explicit", + "constructors": "explicit", + "methods": "explicit", + "properties": "explicit", + "parameterProperties": "explicit" + } + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "error" + ], + "@typescript-eslint/func-call-spacing": [ + "off" + ], + "@typescript-eslint/indent": [ + "off" + ], + "@typescript-eslint/key-spacing": [ + "off" + ], + "@typescript-eslint/keyword-spacing": [ + "off" + ], + "@typescript-eslint/lines-around-comment": [ + 0 + ], + "@typescript-eslint/member-delimiter-style": [ + "off" + ], + "@typescript-eslint/member-ordering": [ + "off" + ], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "accessor", + "modifiers": [ + "private" + ], + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + } + ], + "@typescript-eslint/no-array-constructor": [ + "error" + ], + "@typescript-eslint/no-base-to-string": [ + "error" + ], + "@typescript-eslint/no-confusing-non-null-assertion": [ + "error" + ], + "@typescript-eslint/no-dupe-class-members": [ + "off" + ], + "@typescript-eslint/no-duplicate-enum-values": [ + "error" + ], + "@typescript-eslint/no-duplicate-type-constituents": [ + "off" + ], + "@typescript-eslint/no-dynamic-delete": [ + "error" + ], + "@typescript-eslint/no-empty-function": [ + "off" + ], + "@typescript-eslint/no-empty-interface": [ + "off" + ], + "@typescript-eslint/no-explicit-any": [ + "off", + { + "ignoreRestArgs": true + } + ], + "@typescript-eslint/no-extra-non-null-assertion": [ + "off" + ], + "@typescript-eslint/no-extra-parens": [ + "off" + ], + "@typescript-eslint/no-extra-semi": [ + "off" + ], + "@typescript-eslint/no-extraneous-class": [ + "off" + ], + "@typescript-eslint/no-floating-promises": [ + "error" + ], + "@typescript-eslint/no-for-in-array": [ + "error" + ], + "@typescript-eslint/no-implied-eval": [ + "error" + ], + "@typescript-eslint/no-import-type-side-effects": [ + "off" + ], + "@typescript-eslint/no-inferrable-types": [ + "off" + ], + "@typescript-eslint/no-invalid-this": [ + "off" + ], + "@typescript-eslint/no-invalid-void-type": [ + "off" + ], + "@typescript-eslint/no-loss-of-precision": [ + "off" + ], + "@typescript-eslint/no-magic-numbers": [ + "off" + ], + "@typescript-eslint/no-misused-new": [ + "off" + ], + "@typescript-eslint/no-misused-promises": [ + "error" + ], + "@typescript-eslint/no-namespace": [ + "error" + ], + "@typescript-eslint/no-non-null-asserted-optional-chain": [ + "error" + ], + "@typescript-eslint/no-non-null-assertion": [ + "off" + ], + "@typescript-eslint/no-redeclare": [ + "off" + ], + "@typescript-eslint/no-redundant-type-constituents": [ + "error" + ], + "@typescript-eslint/no-require-imports": [ + "error" + ], + "@typescript-eslint/no-shadow": [ + "error", + { + "hoist": "all", + "ignoreTypeValueShadow": true + } + ], + "@typescript-eslint/no-this-alias": [ + "off" + ], + "@typescript-eslint/no-throw-literal": [ + "error" + ], + "@typescript-eslint/no-unnecessary-qualifier": [ + "error" + ], + "@typescript-eslint/no-unnecessary-type-arguments": [ + "off" + ], + "@typescript-eslint/no-unnecessary-type-assertion": [ + "error" + ], + "@typescript-eslint/no-unnecessary-type-constraint": [ + "off" + ], + "@typescript-eslint/no-unsafe-argument": [ + "error" + ], + "@typescript-eslint/no-unsafe-assignment": [ + "error" + ], + "@typescript-eslint/no-unsafe-call": [ + "error" + ], + "@typescript-eslint/no-unsafe-declaration-merging": [ + "off" + ], + "@typescript-eslint/no-unsafe-enum-comparison": [ + "error" + ], + "@typescript-eslint/no-unsafe-member-access": [ + "error" + ], + "@typescript-eslint/no-unsafe-return": [ + "error" + ], + "@typescript-eslint/no-unused-expressions": [ + "error" + ], + "@typescript-eslint/no-unused-vars": [ + "off" + ], + "@typescript-eslint/no-use-before-define": [ + "off" + ], + "@typescript-eslint/no-useless-constructor": [ + "off" + ], + "@typescript-eslint/no-useless-empty-export": [ + "off" + ], + "@typescript-eslint/no-useless-template-literals": [ + "off" + ], + "@typescript-eslint/no-var-requires": [ + "error" + ], + "@typescript-eslint/non-nullable-type-assertion-style": [ + "off" + ], + "@typescript-eslint/object-curly-spacing": [ + "off" + ], + "@typescript-eslint/prefer-as-const": [ + "off" + ], + "@typescript-eslint/prefer-enum-initializers": [ + "off" + ], + "@typescript-eslint/prefer-for-of": [ + "error" + ], + "@typescript-eslint/prefer-function-type": [ + "off" + ], + "@typescript-eslint/prefer-includes": [ + "error" + ], + "@typescript-eslint/prefer-literal-enum-member": [ + "off" + ], + "@typescript-eslint/prefer-namespace-keyword": [ + "off" + ], + "@typescript-eslint/prefer-nullish-coalescing": [ + "error" + ], + "@typescript-eslint/prefer-optional-chain": [ + "off" + ], + "@typescript-eslint/prefer-readonly": [ + "error" + ], + "@typescript-eslint/prefer-string-starts-ends-with": [ + "error" + ], + "@typescript-eslint/promise-function-async": [ + "error" + ], + "@typescript-eslint/quotes": [ + 0, + "double", + { + "allowTemplateLiterals": true, + "avoidEscape": true + } + ], + "@typescript-eslint/require-await": [ + "off" + ], + "@typescript-eslint/restrict-plus-operands": [ + "error" + ], + "@typescript-eslint/restrict-template-expressions": [ + "off" + ], + "@typescript-eslint/return-await": [ + "error" + ], + "@typescript-eslint/semi": [ + "off", + "always" + ], + "@typescript-eslint/space-before-blocks": [ + "off" + ], + "@typescript-eslint/space-before-function-paren": [ + "off", + { + "anonymous": "never", + "asyncArrow": "always", + "named": "never" + } + ], + "@typescript-eslint/space-infix-ops": [ + "off" + ], + "@typescript-eslint/strict-boolean-expressions": [ + "error" + ], + "@typescript-eslint/triple-slash-reference": [ + "error" + ], + "@typescript-eslint/type-annotation-spacing": [ + "off" + ], + "@typescript-eslint/typedef": [ + "off" + ], + "@typescript-eslint/unbound-method": [ + "error", + { + "ignoreStatic": true + } + ], + "@typescript-eslint/unified-signatures": [ + "off" + ], + "array-bracket-newline": [ + "off" + ], + "array-bracket-spacing": [ + "off" + ], + "array-element-newline": [ + "off" + ], + "arrow-body-style": [ + "off" + ], + "arrow-parens": [ + "off", + "always" + ], + "arrow-spacing": [ + "off" + ], + "babel/object-curly-spacing": [ + "off" + ], + "babel/quotes": [ + 0 + ], + "babel/semi": [ + "off" + ], + "block-spacing": [ + "off" + ], + "brace-style": [ + "off" + ], + "camelcase": [ + "off" + ], + "capitalized-comments": [ + "off" + ], + "comma-dangle": [ + "off" + ], + "comma-spacing": [ + "off" + ], + "comma-style": [ + "off" + ], + "complexity": [ + "off" + ], + "computed-property-spacing": [ + "off" + ], + "constructor-super": [ + "off" + ], + "curly": [ + 0 + ], + "default-case": [ + "error" + ], + "default-case-last": [ + "off" + ], + "default-param-last": [ + "off" + ], + "dot-location": [ + "off" + ], + "dot-notation": [ + "off" + ], + "eol-last": [ + "off" + ], + "eqeqeq": [ + "off", + "smart" + ], + "eslint-comments/disable-enable-pair": [ + "error", + { + "allowWholeFile": true + } + ], + "eslint-comments/no-aggregating-enable": [ + "error" + ], + "eslint-comments/no-duplicate-disable": [ + "error" + ], + "eslint-comments/no-unlimited-disable": [ + "error" + ], + "eslint-comments/no-unused-enable": [ + "error" + ], + "eslint-plugin-mysticatea/no-this-in-static": [ + "off" + ], + "flowtype/boolean-style": [ + "off" + ], + "flowtype/delimiter-dangle": [ + "off" + ], + "flowtype/generic-spacing": [ + "off" + ], + "flowtype/object-type-curly-spacing": [ + "off" + ], + "flowtype/object-type-delimiter": [ + "off" + ], + "flowtype/quotes": [ + "off" + ], + "flowtype/semi": [ + "off" + ], + "flowtype/space-after-type-colon": [ + "off" + ], + "flowtype/space-before-generic-bracket": [ + "off" + ], + "flowtype/space-before-type-colon": [ + "off" + ], + "flowtype/union-intersection-spacing": [ + "off" + ], + "for-direction": [ + "off" + ], + "func-call-spacing": [ + "off" + ], + "function-call-argument-newline": [ + "off" + ], + "function-paren-newline": [ + "off" + ], + "generator-star": [ + "off" + ], + "generator-star-spacing": [ + "off" + ], + "getter-return": [ + "off" + ], + "guard-for-in": [ + "error" + ], + "id-match": [ + "error" + ], + "implicit-arrow-linebreak": [ + "off" + ], + "import/default": [ + 2 + ], + "import/export": [ + 2 + ], + "import/named": [ + "off" + ], + "import/namespace": [ + 2 + ], + "import/no-default-export": [ + "error" + ], + "import/no-deprecated": [ + "error" + ], + "import/no-duplicates": [ + 1 + ], + "import/no-extraneous-dependencies": [ + "error" + ], + "import/no-internal-modules": [ + "error", + { + "allow": [ + "@fluid-example/*/internal", + "@fluid-experimental/*/internal", + "@fluid-internal/*/internal", + "@fluid-private/*/internal", + "@fluid-tools/*/internal", + "@fluidframework/*/internal", + "@fluid-experimental/**", + "*/index.js" + ] + } + ], + "import/no-named-as-default": [ + 1 + ], + "import/no-named-as-default-member": [ + 1 + ], + "import/no-nodejs-modules": [ + "error" + ], + "import/no-unassigned-import": [ + "error" + ], + "import/no-unresolved": [ + 2, + { + "caseSensitive": true, + "caseSensitiveStrict": false + } + ], + "import/no-unused-modules": [ + "error" + ], + "import/order": [ + "error", + { + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": false, + "orderImportKind": "ignore" + }, + "distinctGroup": true, + "warnOnUnassignedImports": false + } + ], + "indent": [ + "off" + ], + "indent-legacy": [ + "off" + ], + "jest/max-nested-describe": [ + "off" + ], + "jest/no-duplicate-hooks": [ + "off" + ], + "jest/no-export": [ + "off" + ], + "jest/no-focused-tests": [ + "off" + ], + "jsdoc/check-access": [ + "error" + ], + "jsdoc/check-examples": [ + "off" + ], + "jsdoc/check-indentation": [ + "error" + ], + "jsdoc/check-line-alignment": [ + "warn" + ], + "jsdoc/check-tag-names": [ + "off" + ], + "jsdoc/empty-tags": [ + "error" + ], + "jsdoc/multiline-blocks": [ + "error", + { + "noSingleLineBlocks": true + } + ], + "jsdoc/no-bad-blocks": [ + "error" + ], + "jsdoc/require-asterisk-prefix": [ + "error" + ], + "jsdoc/require-description": [ + "error", + { + "checkConstructors": false, + "checkGetters": true, + "checkSetters": true + } + ], + "jsdoc/require-hyphen-before-param-description": [ + "error" + ], + "jsdoc/require-jsdoc": [ + "error", + { + "publicOnly": true, + "enableFixer": false, + "require": { + "ArrowFunctionExpression": true, + "ClassDeclaration": true, + "ClassExpression": true, + "FunctionDeclaration": true, + "FunctionExpression": true, + "MethodDefinition": false + }, + "contexts": [ + "TSEnumDeclaration", + "TSInterfaceDeclaration", + "TSTypeAliasDeclaration", + "VariableDeclaration" + ], + "checkConstructors": true, + "checkGetters": true, + "checkSetters": true, + "exemptEmptyConstructors": false, + "exemptEmptyFunctions": false, + "fixerMessage": "" + } + ], + "jsdoc/require-param-description": [ + "error" + ], + "jsdoc/require-returns-description": [ + "error" + ], + "jsx-a11y/alt-text": [ + "off" + ], + "jsx-a11y/anchor-has-content": [ + "off" + ], + "jsx-a11y/anchor-is-valid": [ + "off" + ], + "jsx-a11y/aria-activedescendant-has-tabindex": [ + "off" + ], + "jsx-a11y/aria-props": [ + "off" + ], + "jsx-a11y/aria-proptypes": [ + "off" + ], + "jsx-a11y/aria-role": [ + "off" + ], + "jsx-a11y/aria-unsupported-elements": [ + "off" + ], + "jsx-a11y/click-events-have-key-events": [ + "off" + ], + "jsx-a11y/heading-has-content": [ + "off" + ], + "jsx-a11y/html-has-lang": [ + "off" + ], + "jsx-a11y/iframe-has-title": [ + "off" + ], + "jsx-a11y/img-redundant-alt": [ + "off" + ], + "jsx-a11y/interactive-supports-focus": [ + "off" + ], + "jsx-a11y/label-has-associated-control": [ + "off" + ], + "jsx-a11y/lang": [ + "off" + ], + "jsx-a11y/media-has-caption": [ + "off" + ], + "jsx-a11y/mouse-events-have-key-events": [ + "off" + ], + "jsx-a11y/no-access-key": [ + "off" + ], + "jsx-a11y/no-aria-hidden-on-focusable": [ + "off" + ], + "jsx-a11y/no-autofocus": [ + "off" + ], + "jsx-a11y/no-distracting-elements": [ + "off" + ], + "jsx-a11y/no-interactive-element-to-noninteractive-role": [ + "off" + ], + "jsx-a11y/no-noninteractive-element-to-interactive-role": [ + "off" + ], + "jsx-a11y/no-noninteractive-tabindex": [ + "off" + ], + "jsx-a11y/no-redundant-roles": [ + "off" + ], + "jsx-a11y/prefer-tag-over-role": [ + "off" + ], + "jsx-a11y/role-has-required-aria-props": [ + "off" + ], + "jsx-a11y/role-supports-aria-props": [ + "off" + ], + "jsx-a11y/scope": [ + "off" + ], + "jsx-a11y/tabindex-no-positive": [ + "off" + ], + "jsx-quotes": [ + "off" + ], + "key-spacing": [ + "off" + ], + "keyword-spacing": [ + "off" + ], + "linebreak-style": [ + "off" + ], + "lines-around-comment": [ + 0 + ], + "max-classes-per-file": [ + "off" + ], + "max-len": [ + 0, + { + "code": 120, + "ignoreTrailingComments": true, + "ignoreUrls": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true, + "ignoreRegExpLiterals": true + } + ], + "max-lines": [ + "off" + ], + "max-statements-per-line": [ + "off" + ], + "multiline-ternary": [ + "off" + ], + "new-parens": [ + "off" + ], + "newline-per-chained-call": [ + "off" + ], + "no-array-constructor": [ + "off" + ], + "no-arrow-condition": [ + "off" + ], + "no-async-promise-executor": [ + "off" + ], + "no-bitwise": [ + "error" + ], + "no-caller": [ + "error" + ], + "no-case-declarations": [ + "off" + ], + "no-class-assign": [ + "off" + ], + "no-comma-dangle": [ + "off" + ], + "no-compare-neg-zero": [ + "off" + ], + "no-cond-assign": [ + "off" + ], + "no-confusing-arrow": [ + 0 + ], + "no-const-assign": [ + "off" + ], + "no-constant-condition": [ + "off" + ], + "no-constructor-return": [ + "off" + ], + "no-control-regex": [ + "off" + ], + "no-debugger": [ + "off" + ], + "no-delete-var": [ + "off" + ], + "no-dupe-args": [ + "off" + ], + "no-dupe-class-members": [ + "off" + ], + "no-dupe-else-if": [ + "off" + ], + "no-dupe-keys": [ + "off" + ], + "no-duplicate-case": [ + "off" + ], + "no-duplicate-imports": [ + "off" + ], + "no-else-return": [ + "off" + ], + "no-empty": [ + "off" + ], + "no-empty-character-class": [ + "off" + ], + "no-empty-function": [ + "off" + ], + "no-empty-pattern": [ + "off" + ], + "no-eval": [ + "off" + ], + "no-ex-assign": [ + "off" + ], + "no-extra-boolean-cast": [ + "off" + ], + "no-extra-label": [ + "off" + ], + "no-extra-parens": [ + "off" + ], + "no-extra-semi": [ + "off" + ], + "no-fallthrough": [ + "off" + ], + "no-floating-decimal": [ + "off" + ], + "no-func-assign": [ + "off" + ], + "no-global-assign": [ + "off" + ], + "no-implied-eval": [ + "off" + ], + "no-import-assign": [ + "off" + ], + "no-inner-declarations": [ + "off" + ], + "no-invalid-regexp": [ + "error" + ], + "no-invalid-this": [ + "off" + ], + "no-irregular-whitespace": [ + "error" + ], + "no-label-var": [ + "off" + ], + "no-labels": [ + "off" + ], + "no-lone-blocks": [ + "off" + ], + "no-loss-of-precision": [ + "off" + ], + "no-magic-numbers": [ + "off" + ], + "no-misleading-character-class": [ + "off" + ], + "no-mixed-operators": [ + 0 + ], + "no-mixed-spaces-and-tabs": [ + "off" + ], + "no-multi-spaces": [ + "off", + { + "ignoreEOLComments": true + } + ], + "no-multi-str": [ + "off" + ], + "no-multiple-empty-lines": [ + "off", + { + "max": 1, + "maxBOF": 0, + "maxEOF": 0 + } + ], + "no-negated-condition": [ + "off" + ], + "no-nested-ternary": [ + "off" + ], + "no-new-func": [ + "error" + ], + "no-new-native-nonconstructor": [ + "off" + ], + "no-new-symbol": [ + "off" + ], + "no-new-wrappers": [ + "error" + ], + "no-nonoctal-decimal-escape": [ + "off" + ], + "no-obj-calls": [ + "off" + ], + "no-octal": [ + "error" + ], + "no-octal-escape": [ + "error" + ], + "no-param-reassign": [ + "off" + ], + "no-prototype-builtins": [ + "off" + ], + "no-redeclare": [ + "off" + ], + "no-regex-spaces": [ + "off" + ], + "no-reserved-keys": [ + "off" + ], + "no-restricted-syntax": [ + "error", + { + "selector": "ExportAllDeclaration", + "message": "Exporting * is not permitted. You should export only named items you intend to export." + }, + "ForInStatement" + ], + "no-return-assign": [ + "off" + ], + "no-self-assign": [ + "off" + ], + "no-self-compare": [ + "off" + ], + "no-sequences": [ + "off" + ], + "no-setter-return": [ + "off" + ], + "no-shadow": [ + "off" + ], + "no-shadow-restricted-names": [ + "off" + ], + "no-space-before-semi": [ + "off" + ], + "no-spaced-func": [ + "off" + ], + "no-sparse-arrays": [ + "off" + ], + "no-tabs": [ + 0 + ], + "no-template-curly-in-string": [ + "error" + ], + "no-this-before-super": [ + "off" + ], + "no-throw-literal": [ + "off" + ], + "no-trailing-spaces": [ + "off" + ], + "no-undef": [ + "off" + ], + "no-undef-init": [ + "error" + ], + "no-underscore-dangle": [ + "off" + ], + "no-unexpected-multiline": [ + 0 + ], + "no-unneeded-ternary": [ + "off" + ], + "no-unreachable": [ + "off" + ], + "no-unsafe-finally": [ + "off" + ], + "no-unsafe-negation": [ + "off" + ], + "no-unsafe-optional-chaining": [ + "off" + ], + "no-unused-expressions": [ + "off" + ], + "no-unused-labels": [ + "off" + ], + "no-unused-vars": [ + "off" + ], + "no-use-before-define": [ + "off" + ], + "no-useless-backreference": [ + "error" + ], + "no-useless-catch": [ + "off" + ], + "no-useless-computed-key": [ + "off" + ], + "no-useless-constructor": [ + "off" + ], + "no-useless-escape": [ + "off" + ], + "no-useless-rename": [ + "off" + ], + "no-var": [ + "off" + ], + "no-void": [ + "error" + ], + "no-whitespace-before-property": [ + "off" + ], + "no-with": [ + "off" + ], + "no-wrap-func": [ + "off" + ], + "nonblock-statement-body-position": [ + "off" + ], + "object-curly-newline": [ + "off" + ], + "object-curly-spacing": [ + "off" + ], + "object-property-newline": [ + "off" + ], + "object-shorthand": [ + "error" + ], + "one-var": [ + "off", + "never" + ], + "one-var-declaration-per-line": [ + "off" + ], + "operator-linebreak": [ + "off" + ], + "padded-blocks": [ + "off", + "never" + ], + "padding-line-between-statements": [ + "off", + { + "blankLine": "always", + "prev": "*", + "next": "return" + } + ], + "prefer-arrow-callback": [ + "off" + ], + "prefer-const": [ + "off" + ], + "prefer-exponentiation-operator": [ + "off" + ], + "prefer-numeric-literals": [ + "off" + ], + "prefer-object-spread": [ + "error" + ], + "prefer-promise-reject-errors": [ + "error" + ], + "prefer-regex-literals": [ + "off" + ], + "prefer-rest-params": [ + "off" + ], + "prefer-spread": [ + "error" + ], + "prefer-template": [ + "off" + ], + "promise/param-names": [ + "warn" + ], + "quote-props": [ + "off", + "consistent-as-needed" + ], + "quotes": [ + 0 + ], + "radix": [ + "error" + ], + "react-hooks/exhaustive-deps": [ + "off" + ], + "react/button-has-type": [ + "off" + ], + "react/jsx-child-element-spacing": [ + "off" + ], + "react/jsx-closing-bracket-location": [ + "off" + ], + "react/jsx-closing-tag-location": [ + "off" + ], + "react/jsx-curly-newline": [ + "off" + ], + "react/jsx-curly-spacing": [ + "off" + ], + "react/jsx-equals-spacing": [ + "off" + ], + "react/jsx-first-prop-new-line": [ + "off" + ], + "react/jsx-indent": [ + "off" + ], + "react/jsx-indent-props": [ + "off" + ], + "react/jsx-key": [ + "off" + ], + "react/jsx-max-props-per-line": [ + "off" + ], + "react/jsx-newline": [ + "off" + ], + "react/jsx-no-comment-textnodes": [ + "off" + ], + "react/jsx-no-duplicate-props": [ + "off" + ], + "react/jsx-no-target-blank": [ + "off" + ], + "react/jsx-no-useless-fragment": [ + "off" + ], + "react/jsx-one-expression-per-line": [ + "off" + ], + "react/jsx-props-no-multi-spaces": [ + "off" + ], + "react/jsx-space-before-closing": [ + "off" + ], + "react/jsx-tag-spacing": [ + "off" + ], + "react/jsx-wrap-multilines": [ + "off" + ], + "react/no-array-index-key": [ + "off" + ], + "react/no-children-prop": [ + "off" + ], + "react/no-danger": [ + "off" + ], + "react/no-danger-with-children": [ + "off" + ], + "react/void-dom-elements-no-children": [ + "off" + ], + "require-atomic-updates": [ + "error" + ], + "require-await": [ + "off" + ], + "require-yield": [ + "off" + ], + "rest-spread-spacing": [ + "off" + ], + "semi": [ + "off" + ], + "semi-spacing": [ + "off" + ], + "semi-style": [ + "off" + ], + "simple-import-sort/imports": [ + "off" + ], + "sonarjs/prefer-while": [ + "off" + ], + "space-after-function-name": [ + "off" + ], + "space-after-keywords": [ + "off" + ], + "space-before-blocks": [ + "off" + ], + "space-before-function-paren": [ + "off" + ], + "space-before-function-parentheses": [ + "off" + ], + "space-before-keywords": [ + "off" + ], + "space-in-brackets": [ + "off" + ], + "space-in-parens": [ + "off", + "never" + ], + "space-infix-ops": [ + "off" + ], + "space-return-throw-case": [ + "off" + ], + "space-unary-ops": [ + "off" + ], + "space-unary-word-ops": [ + "off" + ], + "spaced-comment": [ + "error", + "always", + { + "block": { + "markers": [ + "!" + ], + "balanced": true + } + } + ], + "standard/array-bracket-even-spacing": [ + "off" + ], + "standard/computed-property-even-spacing": [ + "off" + ], + "standard/object-curly-even-spacing": [ + "off" + ], + "stylistic/jsx-self-closing-comp": [ + "off" + ], + "switch-colon-spacing": [ + "off" + ], + "template-curly-spacing": [ + "off" + ], + "template-tag-spacing": [ + "off" + ], + "tsdoc/syntax": [ + "error" + ], + "unicorn/better-regex": [ + "error" + ], + "unicorn/catch-error-name": [ + "error" + ], + "unicorn/consistent-destructuring": [ + "error" + ], + "unicorn/consistent-function-scoping": [ + "error" + ], + "unicorn/custom-error-definition": [ + "off" + ], + "unicorn/empty-brace-spaces": [ + "off" + ], + "unicorn/error-message": [ + "error" + ], + "unicorn/escape-case": [ + "error" + ], + "unicorn/expiring-todo-comments": [ + "off" + ], + "unicorn/explicit-length-check": [ + "error" + ], + "unicorn/filename-case": [ + "error", + { + "cases": { + "camelCase": true, + "pascalCase": true + } + } + ], + "unicorn/import-style": [ + "error" + ], + "unicorn/new-for-builtins": [ + "off" + ], + "unicorn/no-abusive-eslint-disable": [ + "error" + ], + "unicorn/no-array-callback-reference": [ + "error" + ], + "unicorn/no-array-for-each": [ + "off" + ], + "unicorn/no-array-method-this-argument": [ + "error" + ], + "unicorn/no-array-push-push": [ + "error" + ], + "unicorn/no-array-reduce": [ + "error" + ], + "unicorn/no-await-expression-member": [ + "error" + ], + "unicorn/no-console-spaces": [ + "error" + ], + "unicorn/no-document-cookie": [ + "error" + ], + "unicorn/no-empty-file": [ + "error" + ], + "unicorn/no-for-loop": [ + "off" + ], + "unicorn/no-hex-escape": [ + "error" + ], + "unicorn/no-instanceof-array": [ + "off" + ], + "unicorn/no-invalid-remove-event-listener": [ + "error" + ], + "unicorn/no-keyword-prefix": [ + "off" + ], + "unicorn/no-lonely-if": [ + "error" + ], + "unicorn/no-negated-condition": [ + "error" + ], + "unicorn/no-nested-ternary": [ + "off" + ], + "unicorn/no-new-array": [ + "error" + ], + "unicorn/no-new-buffer": [ + "error" + ], + "unicorn/no-null": [ + "error" + ], + "unicorn/no-object-as-default-parameter": [ + "error" + ], + "unicorn/no-process-exit": [ + "error" + ], + "unicorn/no-static-only-class": [ + "off" + ], + "unicorn/no-thenable": [ + "off" + ], + "unicorn/no-this-assignment": [ + "error" + ], + "unicorn/no-typeof-undefined": [ + "off" + ], + "unicorn/no-unnecessary-await": [ + "error" + ], + "unicorn/no-unreadable-array-destructuring": [ + "error" + ], + "unicorn/no-unreadable-iife": [ + "error" + ], + "unicorn/no-unused-properties": [ + "off" + ], + "unicorn/no-useless-fallback-in-spread": [ + "error" + ], + "unicorn/no-useless-length-check": [ + "error" + ], + "unicorn/no-useless-promise-resolve-reject": [ + "error" + ], + "unicorn/no-useless-spread": [ + "off" + ], + "unicorn/no-useless-switch-case": [ + "off" + ], + "unicorn/no-useless-undefined": [ + "off" + ], + "unicorn/no-zero-fractions": [ + "error" + ], + "unicorn/number-literal-case": [ + "off" + ], + "unicorn/numeric-separators-style": [ + "error", + { + "onlyIfContainsSeparator": true + } + ], + "unicorn/prefer-add-event-listener": [ + "error" + ], + "unicorn/prefer-array-find": [ + "error" + ], + "unicorn/prefer-array-flat": [ + "error" + ], + "unicorn/prefer-array-flat-map": [ + "off" + ], + "unicorn/prefer-array-index-of": [ + "error" + ], + "unicorn/prefer-array-some": [ + "error" + ], + "unicorn/prefer-at": [ + "off" + ], + "unicorn/prefer-blob-reading-methods": [ + "error" + ], + "unicorn/prefer-code-point": [ + "error" + ], + "unicorn/prefer-date-now": [ + "error" + ], + "unicorn/prefer-default-parameters": [ + "error" + ], + "unicorn/prefer-dom-node-append": [ + "error" + ], + "unicorn/prefer-dom-node-dataset": [ + "error" + ], + "unicorn/prefer-dom-node-remove": [ + "error" + ], + "unicorn/prefer-dom-node-text-content": [ + "error" + ], + "unicorn/prefer-event-target": [ + "off" + ], + "unicorn/prefer-export-from": [ + "error" + ], + "unicorn/prefer-includes": [ + "error" + ], + "unicorn/prefer-json-parse-buffer": [ + "off" + ], + "unicorn/prefer-keyboard-event-key": [ + "error" + ], + "unicorn/prefer-logical-operator-over-ternary": [ + "error" + ], + "unicorn/prefer-math-trunc": [ + "error" + ], + "unicorn/prefer-modern-dom-apis": [ + "error" + ], + "unicorn/prefer-modern-math-apis": [ + "error" + ], + "unicorn/prefer-module": [ + "error" + ], + "unicorn/prefer-native-coercion-functions": [ + "error" + ], + "unicorn/prefer-negative-index": [ + "error" + ], + "unicorn/prefer-node-protocol": [ + "off" + ], + "unicorn/prefer-number-properties": [ + "off" + ], + "unicorn/prefer-object-from-entries": [ + "error" + ], + "unicorn/prefer-optional-catch-binding": [ + "error" + ], + "unicorn/prefer-prototype-methods": [ + "error" + ], + "unicorn/prefer-query-selector": [ + "error" + ], + "unicorn/prefer-reflect-apply": [ + "error" + ], + "unicorn/prefer-regexp-test": [ + "error" + ], + "unicorn/prefer-set-has": [ + "error" + ], + "unicorn/prefer-set-size": [ + "error" + ], + "unicorn/prefer-spread": [ + "error" + ], + "unicorn/prefer-string-replace-all": [ + "off" + ], + "unicorn/prefer-string-slice": [ + "error" + ], + "unicorn/prefer-string-starts-ends-with": [ + "error" + ], + "unicorn/prefer-string-trim-start-end": [ + "error" + ], + "unicorn/prefer-switch": [ + "error" + ], + "unicorn/prefer-ternary": [ + "error" + ], + "unicorn/prefer-top-level-await": [ + "error" + ], + "unicorn/prefer-type-error": [ + "error" + ], + "unicorn/prevent-abbreviations": [ + "off" + ], + "unicorn/relative-url-style": [ + "error" + ], + "unicorn/require-array-join-separator": [ + "error" + ], + "unicorn/require-number-to-fixed-digits-argument": [ + "error" + ], + "unicorn/require-post-message-target-origin": [ + "off" + ], + "unicorn/string-content": [ + "off" + ], + "unicorn/switch-case-braces": [ + "error" + ], + "unicorn/template-indent": [ + 0 + ], + "unicorn/text-encoding-identifier-case": [ + "error" + ], + "unicorn/throw-new-error": [ + "error" + ], + "unused-imports/no-unused-imports": [ + "error" + ], + "use-isnan": [ + "off" + ], + "valid-typeof": [ + "off" + ], + "vue/array-bracket-newline": [ + "off" + ], + "vue/array-bracket-spacing": [ + "off" + ], + "vue/array-element-newline": [ + "off" + ], + "vue/arrow-spacing": [ + "off" + ], + "vue/block-spacing": [ + "off" + ], + "vue/block-tag-newline": [ + "off" + ], + "vue/brace-style": [ + "off" + ], + "vue/comma-dangle": [ + "off" + ], + "vue/comma-spacing": [ + "off" + ], + "vue/comma-style": [ + "off" + ], + "vue/dot-location": [ + "off" + ], + "vue/func-call-spacing": [ + "off" + ], + "vue/html-closing-bracket-newline": [ + "off" + ], + "vue/html-closing-bracket-spacing": [ + "off" + ], + "vue/html-end-tags": [ + "off" + ], + "vue/html-indent": [ + "off" + ], + "vue/html-quotes": [ + "off" + ], + "vue/html-self-closing": [ + 0 + ], + "vue/key-spacing": [ + "off" + ], + "vue/keyword-spacing": [ + "off" + ], + "vue/max-attributes-per-line": [ + "off" + ], + "vue/max-len": [ + 0 + ], + "vue/multiline-html-element-content-newline": [ + "off" + ], + "vue/multiline-ternary": [ + "off" + ], + "vue/mustache-interpolation-spacing": [ + "off" + ], + "vue/no-extra-parens": [ + "off" + ], + "vue/no-multi-spaces": [ + "off" + ], + "vue/no-spaces-around-equal-signs-in-attribute": [ + "off" + ], + "vue/object-curly-newline": [ + "off" + ], + "vue/object-curly-spacing": [ + "off" + ], + "vue/object-property-newline": [ + "off" + ], + "vue/operator-linebreak": [ + "off" + ], + "vue/quote-props": [ + "off" + ], + "vue/script-indent": [ + "off" + ], + "vue/singleline-html-element-content-newline": [ + "off" + ], + "vue/space-in-parens": [ + "off" + ], + "vue/space-infix-ops": [ + "off" + ], + "vue/space-unary-ops": [ + "off" + ], + "vue/template-curly-spacing": [ + "off" + ], + "wrap-iife": [ + "off" + ], + "wrap-regex": [ + "off" + ], + "yield-star-spacing": [ + "off" + ], + "yoda": [ + "off" + ] + }, + "settings": { + "import/extensions": [ + ".ts", + ".tsx", + ".d.ts", + ".js", + ".jsx", + ".jsx" + ], + "import/external-module-folders": [ + "node_modules", + "node_modules/@types" + ], + "import/parsers": { + "@typescript-eslint/parser": [ + ".ts", + ".tsx", + ".d.ts", + ".tsx" + ] + }, + "import/resolver": { + "typescript": { + "extensions": [ + ".ts", + ".tsx", + ".d.ts", + ".js", + ".jsx" + ], + "conditionNames": [ + "allow-ff-test-exports", + "types", + "import", + "esm2020", + "es2020", + "es2015", + "require", + "node", + "node-addons", + "browser", + "default" + ] + }, + "node": { + "extensions": [ + ".ts", + ".cts", + ".mts", + ".tsx", + ".js", + ".jsx" + ] + } + }, + "jsdoc": { + "mode": "typescript", + "tagNamePreference": { + "arg": { + "message": "Please use @param instead of @arg.", + "replacement": "param" + }, + "argument": { + "message": "Please use @param instead of @argument.", + "replacement": "param" + }, + "return": { + "message": "Please use @returns instead of @return.", + "replacement": "returns" + } + } + } + } +} diff --git a/common/build/eslint-config-fluid/strict-biome.js b/common/build/eslint-config-fluid/strict-biome.js new file mode 100644 index 000000000000..081a6bafaed5 --- /dev/null +++ b/common/build/eslint-config-fluid/strict-biome.js @@ -0,0 +1,20 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * "Strict" Biome-compatible eslint configuration. + * + * This configuration is the same as the "strict" config, but disables rules that are handled by biome, which allows + * projects to use both biome and eslint without conflicting rules. + */ +module.exports = { + env: { + browser: true, + es6: true, + es2024: false, + node: true, + }, + extends: ["./strict.js", "biome"], +}; From ba9d715a30b0cb55cf91281fded2e046c7fc1ca7 Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:43:15 -0800 Subject: [PATCH 18/40] refactor(api-markdown-documenter): Update dependency on `@fluidframework/eslint-config-fluid` and fix violations (#23190) Most of the changes are import style changes auto-fixed via `eslint --fix`. --- tools/api-markdown-documenter/.eslintrc.cjs | 3 + .../api-markdown-documenter.alpha.api.md | 14 +- .../api-markdown-documenter.beta.api.md | 14 +- .../api-markdown-documenter.public.api.md | 14 +- tools/api-markdown-documenter/package.json | 2 +- tools/api-markdown-documenter/pnpm-lock.yaml | 223 +++++++++++++----- .../src/ConfigurationBase.ts | 2 +- .../src/FileSystemConfiguration.ts | 2 +- .../src/LintApiModel.ts | 4 +- .../api-markdown-documenter/src/LoadModel.ts | 4 +- .../api-markdown-documenter/src/RenderHtml.ts | 6 +- .../src/RenderMarkdown.ts | 6 +- .../ApiItemTransformUtilities.ts | 13 +- .../api-item-transforms/TransformApiItem.ts | 5 +- .../api-item-transforms/TransformApiModel.ts | 14 +- .../TsdocNodeTransforms.ts | 9 +- .../src/api-item-transforms/Utilities.ts | 11 +- .../configuration/Configuration.ts | 5 +- .../configuration/TransformationOptions.ts | 43 ++-- .../CreateDefaultLayout.ts | 6 +- .../TransformApiClass.ts | 2 +- .../TransformApiEntryPoint.ts | 7 +- .../TransformApiEnum.ts | 4 +- .../TransformApiFunctionLike.ts | 6 +- .../TransformApiInterface.ts | 4 +- .../TransformApiItemWithoutChildren.ts | 6 +- .../TransformApiModel.ts | 2 +- .../TransformApiModuleLike.ts | 4 +- .../TransformApiNamespace.ts | 1 + .../api-item-transforms/helpers/Helpers.ts | 7 +- .../helpers/TableHelpers.ts | 3 +- .../test/Transformation.test.ts | 2 +- .../documentation-domain-to-html/ToHtml.ts | 4 +- .../TransformationContext.ts | 3 +- .../documentation-domain-to-html/Utilities.ts | 4 +- .../configuration/Configuration.ts | 5 +- .../configuration/Transformation.ts | 3 +- .../BlockQuoteToHtml.ts | 1 + .../default-transformations/CodeSpanToHtml.ts | 1 + .../FencedCodeBlockToHtml.ts | 1 + .../default-transformations/HeadingToHtml.ts | 1 + .../default-transformations/LinkToHtml.ts | 1 + .../OrderedListToHtml.ts | 1 + .../ParagraphToHtml.ts | 1 + .../PlainTextToHtml.ts | 2 + .../default-transformations/SectionToHtml.ts | 3 +- .../default-transformations/SpanToHtml.ts | 2 +- .../TableCellToHtml.ts | 3 +- .../default-transformations/TableRowToHtml.ts | 1 + .../default-transformations/TableToHtml.ts | 3 +- .../UnorderedListToHtml.ts | 1 + .../test/BlockQuoteToHtml.test.ts | 2 + .../test/CodeSpanToHtml.test.ts | 2 + .../test/CustomNodeTypeToHtml.test.ts | 2 + .../test/DocumentToHtml.test.ts | 1 + .../test/FencedCodeBlockToHtml.test.ts | 2 + .../test/HeadingToHtml.test.ts | 1 + .../test/HierarchicalSectionToHtml.test.ts | 1 + .../test/HorizontalRuleToHtml.test.ts | 1 + .../test/LineBreakToHtml.test.ts | 1 + .../test/LinkToHtml.test.ts | 1 + .../test/OrderedListToHtml.test.ts | 1 + .../test/ParagraphToHtml.test.ts | 2 + .../test/PlainTextToHtml.test.ts | 2 + .../test/SpanToHtml.test.ts | 2 + .../test/TableToHtml.test.ts | 2 + .../test/UnorderedListToHtml.test.ts | 2 + .../test/Utilities.ts | 5 +- .../src/documentation-domain/DocumentNode.ts | 5 +- .../src/documentation-domain/HeadingNode.ts | 3 +- .../HorizontalRuleNode.ts | 2 +- .../src/documentation-domain/LineBreakNode.ts | 2 +- .../src/documentation-domain/LinkNode.ts | 3 +- .../src/documentation-domain/SectionNode.ts | 2 +- .../src/documentation-domain/SpanNode.ts | 2 +- .../src/documentation-domain/TableNode.ts | 2 +- .../src/documentation-domain/TableRowNode.ts | 2 +- .../src/documentation-domain/Utilities.ts | 2 +- tools/api-markdown-documenter/src/index.ts | 4 +- .../src/renderers/html-renderer/Render.ts | 2 +- .../src/renderers/markdown-renderer/Render.ts | 3 +- .../markdown-renderer/RenderContext.ts | 1 + .../renderers/markdown-renderer/Utilities.ts | 9 +- .../configuration/Configuration.ts | 5 +- .../default-renderers/RenderHeading.ts | 3 +- .../test/RenderBlockQuote.test.ts | 1 + .../test/RenderCodeSpan.test.ts | 1 + .../test/RenderCustomNodeType.test.ts | 5 +- .../test/RenderFencedCodeBlock.test.ts | 1 + .../test/RenderHeading.test.ts | 1 + .../test/RenderHierarchicalSection.test.ts | 1 + .../test/RenderHorizontalRule.test.ts | 1 + .../test/RenderLineBreak.test.ts | 1 + .../markdown-renderer/test/RenderLink.test.ts | 1 + .../test/RenderOrderedList.test.ts | 1 + .../test/RenderParagraph.test.ts | 1 + .../test/RenderPlainText.test.ts | 3 +- .../markdown-renderer/test/RenderSpan.test.ts | 1 + .../test/RenderTable.test.ts | 1 + .../test/RenderTableCell.test.ts | 1 + .../test/RenderUnorderedList.test.ts | 1 + .../src/test/EndToEndTests.ts | 4 +- .../src/test/HtmlEndToEnd.test.ts | 3 +- .../src/test/MarkdownEndToEnd.test.ts | 3 +- .../src/utilities/ApiItemUtilities.ts | 3 +- 105 files changed, 394 insertions(+), 212 deletions(-) diff --git a/tools/api-markdown-documenter/.eslintrc.cjs b/tools/api-markdown-documenter/.eslintrc.cjs index 8c6e738090ae..e00ef28da411 100644 --- a/tools/api-markdown-documenter/.eslintrc.cjs +++ b/tools/api-markdown-documenter/.eslintrc.cjs @@ -9,6 +9,9 @@ module.exports = { project: ["./tsconfig.json"], }, rules: { + // Too many false positives with array access + "@fluid-internal/fluid/no-unchecked-record-access": "off", + // Rule is reported in a lot of places where it would be invalid to follow the suggested pattern "@typescript-eslint/class-literal-property-style": "off", diff --git a/tools/api-markdown-documenter/api-report/api-markdown-documenter.alpha.api.md b/tools/api-markdown-documenter/api-report/api-markdown-documenter.alpha.api.md index 498ebeb632cc..2e3855e19de0 100644 --- a/tools/api-markdown-documenter/api-report/api-markdown-documenter.alpha.api.md +++ b/tools/api-markdown-documenter/api-report/api-markdown-documenter.alpha.api.md @@ -5,15 +5,15 @@ ```ts import { ApiCallSignature } from '@microsoft/api-extractor-model'; -import { ApiClass } from '@microsoft/api-extractor-model'; +import type { ApiClass } from '@microsoft/api-extractor-model'; import { ApiConstructor } from '@microsoft/api-extractor-model'; import { ApiConstructSignature } from '@microsoft/api-extractor-model'; import { ApiEntryPoint } from '@microsoft/api-extractor-model'; -import { ApiEnum } from '@microsoft/api-extractor-model'; -import { ApiEnumMember } from '@microsoft/api-extractor-model'; +import type { ApiEnum } from '@microsoft/api-extractor-model'; +import type { ApiEnumMember } from '@microsoft/api-extractor-model'; import { ApiFunction } from '@microsoft/api-extractor-model'; import { ApiIndexSignature } from '@microsoft/api-extractor-model'; -import { ApiInterface } from '@microsoft/api-extractor-model'; +import type { ApiInterface } from '@microsoft/api-extractor-model'; import { ApiItem } from '@microsoft/api-extractor-model'; import { ApiItemKind } from '@microsoft/api-extractor-model'; import { ApiMethod } from '@microsoft/api-extractor-model'; @@ -21,9 +21,9 @@ import { ApiMethodSignature } from '@microsoft/api-extractor-model'; import { ApiModel } from '@microsoft/api-extractor-model'; import { ApiNamespace } from '@microsoft/api-extractor-model'; import { ApiPackage } from '@microsoft/api-extractor-model'; -import { ApiPropertyItem } from '@microsoft/api-extractor-model'; -import { ApiTypeAlias } from '@microsoft/api-extractor-model'; -import { ApiVariable } from '@microsoft/api-extractor-model'; +import type { ApiPropertyItem } from '@microsoft/api-extractor-model'; +import type { ApiTypeAlias } from '@microsoft/api-extractor-model'; +import type { ApiVariable } from '@microsoft/api-extractor-model'; import type { Data } from 'unist'; import { DocNode } from '@microsoft/tsdoc'; import { DocSection } from '@microsoft/tsdoc'; diff --git a/tools/api-markdown-documenter/api-report/api-markdown-documenter.beta.api.md b/tools/api-markdown-documenter/api-report/api-markdown-documenter.beta.api.md index 34ab71b8d77d..8f7c96b2e2a3 100644 --- a/tools/api-markdown-documenter/api-report/api-markdown-documenter.beta.api.md +++ b/tools/api-markdown-documenter/api-report/api-markdown-documenter.beta.api.md @@ -5,15 +5,15 @@ ```ts import { ApiCallSignature } from '@microsoft/api-extractor-model'; -import { ApiClass } from '@microsoft/api-extractor-model'; +import type { ApiClass } from '@microsoft/api-extractor-model'; import { ApiConstructor } from '@microsoft/api-extractor-model'; import { ApiConstructSignature } from '@microsoft/api-extractor-model'; import { ApiEntryPoint } from '@microsoft/api-extractor-model'; -import { ApiEnum } from '@microsoft/api-extractor-model'; -import { ApiEnumMember } from '@microsoft/api-extractor-model'; +import type { ApiEnum } from '@microsoft/api-extractor-model'; +import type { ApiEnumMember } from '@microsoft/api-extractor-model'; import { ApiFunction } from '@microsoft/api-extractor-model'; import { ApiIndexSignature } from '@microsoft/api-extractor-model'; -import { ApiInterface } from '@microsoft/api-extractor-model'; +import type { ApiInterface } from '@microsoft/api-extractor-model'; import { ApiItem } from '@microsoft/api-extractor-model'; import { ApiItemKind } from '@microsoft/api-extractor-model'; import { ApiMethod } from '@microsoft/api-extractor-model'; @@ -21,9 +21,9 @@ import { ApiMethodSignature } from '@microsoft/api-extractor-model'; import { ApiModel } from '@microsoft/api-extractor-model'; import { ApiNamespace } from '@microsoft/api-extractor-model'; import { ApiPackage } from '@microsoft/api-extractor-model'; -import { ApiPropertyItem } from '@microsoft/api-extractor-model'; -import { ApiTypeAlias } from '@microsoft/api-extractor-model'; -import { ApiVariable } from '@microsoft/api-extractor-model'; +import type { ApiPropertyItem } from '@microsoft/api-extractor-model'; +import type { ApiTypeAlias } from '@microsoft/api-extractor-model'; +import type { ApiVariable } from '@microsoft/api-extractor-model'; import type { Data } from 'unist'; import { DocNode } from '@microsoft/tsdoc'; import { DocSection } from '@microsoft/tsdoc'; diff --git a/tools/api-markdown-documenter/api-report/api-markdown-documenter.public.api.md b/tools/api-markdown-documenter/api-report/api-markdown-documenter.public.api.md index f386d60d1611..e4cdc109cce6 100644 --- a/tools/api-markdown-documenter/api-report/api-markdown-documenter.public.api.md +++ b/tools/api-markdown-documenter/api-report/api-markdown-documenter.public.api.md @@ -5,15 +5,15 @@ ```ts import { ApiCallSignature } from '@microsoft/api-extractor-model'; -import { ApiClass } from '@microsoft/api-extractor-model'; +import type { ApiClass } from '@microsoft/api-extractor-model'; import { ApiConstructor } from '@microsoft/api-extractor-model'; import { ApiConstructSignature } from '@microsoft/api-extractor-model'; import { ApiEntryPoint } from '@microsoft/api-extractor-model'; -import { ApiEnum } from '@microsoft/api-extractor-model'; -import { ApiEnumMember } from '@microsoft/api-extractor-model'; +import type { ApiEnum } from '@microsoft/api-extractor-model'; +import type { ApiEnumMember } from '@microsoft/api-extractor-model'; import { ApiFunction } from '@microsoft/api-extractor-model'; import { ApiIndexSignature } from '@microsoft/api-extractor-model'; -import { ApiInterface } from '@microsoft/api-extractor-model'; +import type { ApiInterface } from '@microsoft/api-extractor-model'; import { ApiItem } from '@microsoft/api-extractor-model'; import { ApiItemKind } from '@microsoft/api-extractor-model'; import { ApiMethod } from '@microsoft/api-extractor-model'; @@ -21,9 +21,9 @@ import { ApiMethodSignature } from '@microsoft/api-extractor-model'; import { ApiModel } from '@microsoft/api-extractor-model'; import { ApiNamespace } from '@microsoft/api-extractor-model'; import { ApiPackage } from '@microsoft/api-extractor-model'; -import { ApiPropertyItem } from '@microsoft/api-extractor-model'; -import { ApiTypeAlias } from '@microsoft/api-extractor-model'; -import { ApiVariable } from '@microsoft/api-extractor-model'; +import type { ApiPropertyItem } from '@microsoft/api-extractor-model'; +import type { ApiTypeAlias } from '@microsoft/api-extractor-model'; +import type { ApiVariable } from '@microsoft/api-extractor-model'; import type { Data } from 'unist'; import { DocNode } from '@microsoft/tsdoc'; import { DocSection } from '@microsoft/tsdoc'; diff --git a/tools/api-markdown-documenter/package.json b/tools/api-markdown-documenter/package.json index 7e65e87269ce..0317e3b1c831 100644 --- a/tools/api-markdown-documenter/package.json +++ b/tools/api-markdown-documenter/package.json @@ -88,7 +88,7 @@ "@fluid-tools/markdown-magic": "file:../markdown-magic", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.44.0", - "@fluidframework/eslint-config-fluid": "^5.1.0", + "@fluidframework/eslint-config-fluid": "^5.5.1", "@microsoft/api-extractor": "^7.45.1", "@types/chai": "^4.3.4", "@types/hast": "^3.0.4", diff --git a/tools/api-markdown-documenter/pnpm-lock.yaml b/tools/api-markdown-documenter/pnpm-lock.yaml index 9d619b030589..c6c5f7c6fab4 100644 --- a/tools/api-markdown-documenter/pnpm-lock.yaml +++ b/tools/api-markdown-documenter/pnpm-lock.yaml @@ -55,8 +55,8 @@ importers: specifier: ^0.44.0 version: 0.44.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.1.0 - version: 5.2.0(eslint@8.55.0)(typescript@5.1.6) + specifier: ^5.5.1 + version: 5.5.1(eslint@8.55.0)(typescript@5.1.6) '@microsoft/api-extractor': specifier: ^7.45.1 version: 7.45.1(@types/node@18.15.11) @@ -218,12 +218,12 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@fluid-internal/eslint-plugin-fluid@0.1.1(eslint@8.55.0)(typescript@5.1.6): - resolution: {integrity: sha512-7CNeAjn81BPvq/BKc1nQo/6HUZXg4KUAglFuCX6HFCnpGPrDLdm7cdkrGyA1tExB1EGnCAPFzVNbSqSYcwJnag==} + /@fluid-internal/eslint-plugin-fluid@0.1.3(eslint@8.55.0)(typescript@5.1.6): + resolution: {integrity: sha512-l3MPEQ34lYP9QOIQ9vx9ncyvkYqR7ci7ZixxD4RdOmGBnAFOqipBjn5Je9AJfztQ3jWgcT3jiV+AVT3rBjc2Yw==} dependencies: '@microsoft/tsdoc': 0.14.2 - '@typescript-eslint/parser': 6.7.5(eslint@8.55.0)(typescript@5.1.6) - ts-morph: 20.0.0 + '@typescript-eslint/parser': 6.21.0(eslint@8.55.0)(typescript@5.1.6) + ts-morph: 22.0.0 transitivePeerDependencies: - eslint - supports-color @@ -315,10 +315,10 @@ packages: '@fluidframework/protocol-definitions': 3.2.0 dev: true - /@fluidframework/eslint-config-fluid@5.2.0(eslint@8.55.0)(typescript@5.1.6): - resolution: {integrity: sha512-FGAt7QIm//36j+ZiiIAj1+LZ6NYP2UJLwE5NT70ztgjl90jaCEWZUgoGUgPUWB9CpTmVZYo1+dGWOSsMLHidJA==} + /@fluidframework/eslint-config-fluid@5.5.1(eslint@8.55.0)(typescript@5.1.6): + resolution: {integrity: sha512-rbHvimalOIyLd6nsK5uWJQylxwkztJc0yWKOrop8rrxgMR9dSuGnjJXcjhijZ0p1+zHbZ325+udoDFWw2HJZRQ==} dependencies: - '@fluid-internal/eslint-plugin-fluid': 0.1.1(eslint@8.55.0)(typescript@5.1.6) + '@fluid-internal/eslint-plugin-fluid': 0.1.3(eslint@8.55.0)(typescript@5.1.6) '@microsoft/tsdoc': 0.14.2 '@rushstack/eslint-patch': 1.4.0 '@rushstack/eslint-plugin': 0.13.1(eslint@8.55.0)(typescript@5.1.6) @@ -326,13 +326,13 @@ packages: '@typescript-eslint/eslint-plugin': 6.7.5(@typescript-eslint/parser@6.7.5)(eslint@8.55.0)(typescript@5.1.6) '@typescript-eslint/parser': 6.7.5(eslint@8.55.0)(typescript@5.1.6) eslint-config-prettier: 9.0.0(eslint@8.55.0) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.7.5)(eslint-plugin-i@2.29.0)(eslint@8.55.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.7.5)(eslint-plugin-i@2.29.1)(eslint@8.55.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.55.0) - eslint-plugin-import: /eslint-plugin-i@2.29.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + eslint-plugin-import: /eslint-plugin-i@2.29.1(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-typescript@3.6.3)(eslint@8.55.0) eslint-plugin-jsdoc: 46.8.2(eslint@8.55.0) eslint-plugin-promise: 6.1.1(eslint@8.55.0) eslint-plugin-react: 7.33.2(eslint@8.55.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.55.0) + eslint-plugin-react-hooks: 4.6.2(eslint@8.55.0) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-unicorn: 48.0.1(eslint@8.55.0) eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@6.7.5)(eslint@8.55.0) @@ -340,6 +340,7 @@ packages: - eslint - eslint-import-resolver-node - eslint-import-resolver-webpack + - eslint-plugin-import-x - supports-color - typescript dev: true @@ -544,6 +545,11 @@ packages: fastq: 1.15.0 dev: true + /@nolyfill/is-core-module@1.0.39: + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + dev: true + /@oclif/core@4.0.20: resolution: {integrity: sha512-N1RKI/nteiEbd/ilyv/W1tz82SEAeXeQ5oZpdU/WgLrDilHH7cicrT/NBLextJJhH3QTC+1oan0Z5vNzkX6lGA==} engines: {node: '>=18.0.0'} @@ -819,15 +825,6 @@ packages: - supports-color dev: true - /@ts-morph/common@0.21.0: - resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==} - dependencies: - fast-glob: 3.3.2 - minimatch: 7.4.6 - mkdirp: 2.1.6 - path-browserify: 1.0.1 - dev: true - /@ts-morph/common@0.23.0: resolution: {integrity: sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==} dependencies: @@ -988,7 +985,7 @@ packages: '@typescript-eslint/type-utils': 6.7.5(eslint@8.55.0)(typescript@5.1.6) '@typescript-eslint/utils': 6.7.5(eslint@8.55.0)(typescript@5.1.6) '@typescript-eslint/visitor-keys': 6.7.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.2.4 @@ -1013,6 +1010,27 @@ packages: - typescript dev: true + /@typescript-eslint/parser@6.21.0(eslint@8.55.0)(typescript@5.1.6): + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.6(supports-color@8.1.1) + eslint: 8.55.0 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@6.7.5(eslint@8.55.0)(typescript@5.1.6): resolution: {integrity: sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1027,7 +1045,7 @@ packages: '@typescript-eslint/types': 6.7.5 '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.1.6) '@typescript-eslint/visitor-keys': 6.7.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) eslint: 8.55.0 typescript: 5.1.6 transitivePeerDependencies: @@ -1042,6 +1060,14 @@ packages: '@typescript-eslint/visitor-keys': 5.59.11 dev: true + /@typescript-eslint/scope-manager@6.21.0: + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + dev: true + /@typescript-eslint/scope-manager@6.7.5: resolution: {integrity: sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1075,6 +1101,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@typescript-eslint/types@6.21.0: + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/types@6.7.5: resolution: {integrity: sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1101,6 +1132,28 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.1.6): + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.6(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.0 + ts-api-utils: 1.0.3(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@6.7.5(typescript@5.1.6): resolution: {integrity: sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1169,6 +1222,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@6.21.0: + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@typescript-eslint/visitor-keys@6.7.5: resolution: {integrity: sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1651,10 +1712,6 @@ packages: engines: {node: '>=0.8'} dev: true - /code-block-writer@12.0.0: - resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} - dev: true - /code-block-writer@13.0.1: resolution: {integrity: sha512-c5or4P6erEA69TxaxTNcHUNcIn+oyxSRTOWV+pSYF+z4epXqNvwvJ70XPGjPNgue83oAFAPBRQYwpAJ/Hpe/Sg==} dev: true @@ -2194,21 +2251,28 @@ packages: - supports-color dev: true - /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.7.5)(eslint-plugin-i@2.29.0)(eslint@8.55.0): - resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + /eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.7.5)(eslint-plugin-i@2.29.1)(eslint@8.55.0): + resolution: {integrity: sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '*' eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true dependencies: - debug: 4.3.4(supports-color@8.1.1) + '@nolyfill/is-core-module': 1.0.39 + debug: 4.3.6(supports-color@8.1.1) enhanced-resolve: 5.15.0 eslint: 8.55.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) - eslint-plugin-import: /eslint-plugin-i@2.29.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-typescript@3.6.3)(eslint@8.55.0) + eslint-plugin-import: /eslint-plugin-i@2.29.1(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-typescript@3.6.3)(eslint@8.55.0) fast-glob: 3.3.2 - get-tsconfig: 4.7.2 - is-core-module: 2.13.1 + get-tsconfig: 4.8.1 + is-bun-module: 1.2.1 is-glob: 4.0.3 transitivePeerDependencies: - '@typescript-eslint/parser' @@ -2217,7 +2281,36 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0): + /eslint-module-utils@2.12.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-typescript@3.6.3)(eslint@8.55.0): + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.7.5(eslint@8.55.0)(typescript@5.1.6) + debug: 3.2.7 + eslint: 8.55.0 + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.7.5)(eslint-plugin-i@2.29.1)(eslint@8.55.0) + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.55.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -2242,7 +2335,7 @@ packages: debug: 3.2.7 eslint: 8.55.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.7.5)(eslint-plugin-i@2.29.0)(eslint@8.55.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.7.5)(eslint-plugin-i@2.29.1)(eslint@8.55.0) transitivePeerDependencies: - supports-color dev: true @@ -2276,21 +2369,20 @@ packages: ignore: 5.2.4 dev: true - /eslint-plugin-i@2.29.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0): - resolution: {integrity: sha512-slGeTS3GQzx9267wLJnNYNO8X9EHGsc75AKIAFvnvMYEcTJKotPKL1Ru5PIGVHIVet+2DsugePWp8Oxpx8G22w==} + /eslint-plugin-i@2.29.1(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-typescript@3.6.3)(eslint@8.55.0): + resolution: {integrity: sha512-ORizX37MelIWLbMyqI7hi8VJMf7A0CskMmYkB+lkCX3aF4pkGV7kwx5bSEb4qx7Yce2rAf9s34HqDRPjGRZPNQ==} engines: {node: '>=12'} peerDependencies: eslint: ^7.2.0 || ^8 dependencies: - debug: 3.2.7 - doctrine: 2.1.0 + debug: 4.3.6(supports-color@8.1.1) + doctrine: 3.0.0 eslint: 8.55.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.55.0) get-tsconfig: 4.7.2 is-glob: 4.0.3 minimatch: 3.1.2 - resolve: 1.22.8 semver: 7.6.0 transitivePeerDependencies: - '@typescript-eslint/parser' @@ -2308,7 +2400,7 @@ packages: '@es-joy/jsdoccomment': 0.40.1 are-docs-informative: 0.0.2 comment-parser: 1.4.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint: 8.55.0 esquery: 1.5.0 @@ -2328,8 +2420,8 @@ packages: eslint: 8.55.0 dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.55.0): - resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + /eslint-plugin-react-hooks@4.6.2(eslint@8.55.0): + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 @@ -2788,6 +2880,12 @@ packages: resolve-pkg-maps: 1.0.0 dev: true + /get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /git-hooks-list@1.0.3: resolution: {integrity: sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ==} dev: true @@ -3321,6 +3419,12 @@ packages: builtin-modules: 3.3.0 dev: true + /is-bun-module@1.2.1: + resolution: {integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==} + dependencies: + semver: 7.6.3 + dev: true + /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -4060,15 +4164,15 @@ packages: brace-expansion: 2.0.1 dev: true - /minimatch@7.4.6: - resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} - engines: {node: '>=10'} + /minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 dev: true - /minimatch@8.0.4: - resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 @@ -4111,12 +4215,6 @@ packages: hasBin: true dev: true - /mkdirp@2.1.6: - resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} - engines: {node: '>=10'} - hasBin: true - dev: true - /mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} @@ -4810,6 +4908,12 @@ packages: lru-cache: 6.0.0 dev: true + /semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + dev: true + /serialize-javascript@6.0.0: resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} dependencies: @@ -5210,13 +5314,6 @@ packages: engines: {node: '>=14.13.1'} dev: true - /ts-morph@20.0.0: - resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==} - dependencies: - '@ts-morph/common': 0.21.0 - code-block-writer: 12.0.0 - dev: true - /ts-morph@22.0.0: resolution: {integrity: sha512-M9MqFGZREyeb5fTl6gNHKZLqBQA0TjA1lea+CR48R8EBTDuWrNqW6ccC5QvjNR4s6wDumD3LTCjOFSp9iwlzaw==} dependencies: diff --git a/tools/api-markdown-documenter/src/ConfigurationBase.ts b/tools/api-markdown-documenter/src/ConfigurationBase.ts index 316ce2303529..c400af41aa96 100644 --- a/tools/api-markdown-documenter/src/ConfigurationBase.ts +++ b/tools/api-markdown-documenter/src/ConfigurationBase.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { type Logger } from "./Logging.js"; +import type { Logger } from "./Logging.js"; /** * Common base interface for configuration interfaces. diff --git a/tools/api-markdown-documenter/src/FileSystemConfiguration.ts b/tools/api-markdown-documenter/src/FileSystemConfiguration.ts index b0095d0644eb..2265a661cdfb 100644 --- a/tools/api-markdown-documenter/src/FileSystemConfiguration.ts +++ b/tools/api-markdown-documenter/src/FileSystemConfiguration.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { type NewlineKind } from "@rushstack/node-core-library"; +import type { NewlineKind } from "@rushstack/node-core-library"; /** * Configuration for interacting with the file-system. diff --git a/tools/api-markdown-documenter/src/LintApiModel.ts b/tools/api-markdown-documenter/src/LintApiModel.ts index 25dd68e98599..bce345e7b5c4 100644 --- a/tools/api-markdown-documenter/src/LintApiModel.ts +++ b/tools/api-markdown-documenter/src/LintApiModel.ts @@ -4,6 +4,7 @@ */ import { fail, strict as assert } from "node:assert"; + import { ApiDocumentedItem, type ApiItem, @@ -20,9 +21,10 @@ import { DocNodeContainer, DocNodeKind, } from "@microsoft/tsdoc"; + +import type { ConfigurationBase } from "./ConfigurationBase.js"; import { defaultConsoleLogger } from "./Logging.js"; import { resolveSymbolicReference } from "./utilities/index.js"; -import type { ConfigurationBase } from "./ConfigurationBase.js"; /** * {@link lintApiModel} configuration. diff --git a/tools/api-markdown-documenter/src/LoadModel.ts b/tools/api-markdown-documenter/src/LoadModel.ts index ca89a222efd6..2e0fad5a877c 100644 --- a/tools/api-markdown-documenter/src/LoadModel.ts +++ b/tools/api-markdown-documenter/src/LoadModel.ts @@ -12,11 +12,11 @@ import { ApiModel, type IResolveDeclarationReferenceResult, } from "@microsoft/api-extractor-model"; -import { type DocComment, type DocInheritDocTag } from "@microsoft/tsdoc"; +import type { DocComment, DocInheritDocTag } from "@microsoft/tsdoc"; import { FileSystem } from "@rushstack/node-core-library"; -import { defaultConsoleLogger, type Logger } from "./Logging.js"; import type { ConfigurationBase } from "./ConfigurationBase.js"; +import { defaultConsoleLogger, type Logger } from "./Logging.js"; /** * {@link loadModel} options. diff --git a/tools/api-markdown-documenter/src/RenderHtml.ts b/tools/api-markdown-documenter/src/RenderHtml.ts index ce978f4af474..5b501a538cf3 100644 --- a/tools/api-markdown-documenter/src/RenderHtml.ts +++ b/tools/api-markdown-documenter/src/RenderHtml.ts @@ -7,14 +7,14 @@ import * as Path from "node:path"; import { FileSystem, NewlineKind } from "@rushstack/node-core-library"; +import type { FileSystemConfiguration } from "./FileSystemConfiguration.js"; +import type { Logger } from "./Logging.js"; import { type ApiItemTransformationConfiguration, transformApiModel, } from "./api-item-transforms/index.js"; -import { type DocumentNode } from "./documentation-domain/index.js"; -import { type Logger } from "./Logging.js"; +import type { DocumentNode } from "./documentation-domain/index.js"; import { type RenderDocumentAsHtmlConfig, renderDocumentAsHtml } from "./renderers/index.js"; -import { type FileSystemConfiguration } from "./FileSystemConfiguration.js"; /** * Renders the provided model and its contents, and writes each document to a file on disk. diff --git a/tools/api-markdown-documenter/src/RenderMarkdown.ts b/tools/api-markdown-documenter/src/RenderMarkdown.ts index b821248a11fa..38a5f57c19d2 100644 --- a/tools/api-markdown-documenter/src/RenderMarkdown.ts +++ b/tools/api-markdown-documenter/src/RenderMarkdown.ts @@ -7,14 +7,14 @@ import * as Path from "node:path"; import { FileSystem, NewlineKind } from "@rushstack/node-core-library"; +import type { FileSystemConfiguration } from "./FileSystemConfiguration.js"; +import type { Logger } from "./Logging.js"; import { type ApiItemTransformationConfiguration, transformApiModel, } from "./api-item-transforms/index.js"; -import { type DocumentNode } from "./documentation-domain/index.js"; -import { type Logger } from "./Logging.js"; +import type { DocumentNode } from "./documentation-domain/index.js"; import { type MarkdownRenderConfiguration, renderDocumentAsMarkdown } from "./renderers/index.js"; -import { type FileSystemConfiguration } from "./FileSystemConfiguration.js"; /** * Renders the provided model and its contents, and writes each document to a file on disk. diff --git a/tools/api-markdown-documenter/src/api-item-transforms/ApiItemTransformUtilities.ts b/tools/api-markdown-documenter/src/api-item-transforms/ApiItemTransformUtilities.ts index ffe8dddf3d1b..9df83ab4b962 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/ApiItemTransformUtilities.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/ApiItemTransformUtilities.ts @@ -7,13 +7,14 @@ import * as Path from "node:path"; import { type ApiItem, ApiItemKind, ReleaseTag } from "@microsoft/api-extractor-model"; -import { type Heading } from "../Heading.js"; -import { type Link } from "../Link.js"; +import type { Heading } from "../Heading.js"; +import type { Link } from "../Link.js"; import { getQualifiedApiItemName, getReleaseTag } from "../utilities/index.js"; -import { - type ApiItemTransformationConfiguration, - type DocumentBoundaries, - type HierarchyBoundaries, + +import type { + ApiItemTransformationConfiguration, + DocumentBoundaries, + HierarchyBoundaries, } from "./configuration/index.js"; /** diff --git a/tools/api-markdown-documenter/src/api-item-transforms/TransformApiItem.ts b/tools/api-markdown-documenter/src/api-item-transforms/TransformApiItem.ts index 17f305bdc748..23b83edc960a 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/TransformApiItem.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/TransformApiItem.ts @@ -23,10 +23,11 @@ import { type ApiVariable, } from "@microsoft/api-extractor-model"; -import { type DocumentNode, type SectionNode } from "../documentation-domain/index.js"; +import type { DocumentNode, SectionNode } from "../documentation-domain/index.js"; + import { doesItemRequireOwnDocument, shouldItemBeIncluded } from "./ApiItemTransformUtilities.js"; import { createDocument } from "./Utilities.js"; -import { type ApiItemTransformationConfiguration } from "./configuration/index.js"; +import type { ApiItemTransformationConfiguration } from "./configuration/index.js"; import { createBreadcrumbParagraph, wrapInSection } from "./helpers/index.js"; /** diff --git a/tools/api-markdown-documenter/src/api-item-transforms/TransformApiModel.ts b/tools/api-markdown-documenter/src/api-item-transforms/TransformApiModel.ts index 80f446cd7a4a..f8820f8a1e8f 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/TransformApiModel.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/TransformApiModel.ts @@ -3,22 +3,18 @@ * Licensed under the MIT License. */ -import { - type ApiEntryPoint, - type ApiItem, - type ApiModel, - type ApiPackage, -} from "@microsoft/api-extractor-model"; +import type { ApiEntryPoint, ApiItem, ApiModel, ApiPackage } from "@microsoft/api-extractor-model"; + +import type { DocumentNode, SectionNode } from "../documentation-domain/index.js"; -import { type DocumentNode, type SectionNode } from "../documentation-domain/index.js"; +import { doesItemRequireOwnDocument, shouldItemBeIncluded } from "./ApiItemTransformUtilities.js"; +import { apiItemToDocument, apiItemToSections } from "./TransformApiItem.js"; import { createDocument } from "./Utilities.js"; import { type ApiItemTransformationConfiguration, getApiItemTransformationConfigurationWithDefaults, } from "./configuration/index.js"; -import { doesItemRequireOwnDocument, shouldItemBeIncluded } from "./ApiItemTransformUtilities.js"; import { createBreadcrumbParagraph, createEntryPointList, wrapInSection } from "./helpers/index.js"; -import { apiItemToDocument, apiItemToSections } from "./TransformApiItem.js"; /** * Renders the provided model and its contents to a series of {@link DocumentNode}s. diff --git a/tools/api-markdown-documenter/src/api-item-transforms/TsdocNodeTransforms.ts b/tools/api-markdown-documenter/src/api-item-transforms/TsdocNodeTransforms.ts index 34c12a1e4736..e757f3cc9374 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/TsdocNodeTransforms.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/TsdocNodeTransforms.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { type ApiItem } from "@microsoft/api-extractor-model"; +import type { ApiItem } from "@microsoft/api-extractor-model"; import { type DocCodeSpan, type DocDeclarationReference, @@ -20,7 +20,8 @@ import { type DocHtmlStartTag, } from "@microsoft/tsdoc"; -import { type Link } from "../Link.js"; +import type { ConfigurationBase } from "../ConfigurationBase.js"; +import type { Link } from "../Link.js"; import { CodeSpanNode, type DocumentationNode, @@ -34,9 +35,9 @@ import { SingleLineSpanNode, SpanNode, } from "../documentation-domain/index.js"; -import { type ConfigurationBase } from "../ConfigurationBase.js"; + import { getTsdocNodeTransformationOptions } from "./Utilities.js"; -import { type ApiItemTransformationConfiguration } from "./configuration/index.js"; +import type { ApiItemTransformationConfiguration } from "./configuration/index.js"; /** * Library of transformations from {@link https://github.com/microsoft/tsdoc/blob/main/tsdoc/src/nodes/DocNode.ts| DocNode}s diff --git a/tools/api-markdown-documenter/src/api-item-transforms/Utilities.ts b/tools/api-markdown-documenter/src/api-item-transforms/Utilities.ts index 8200a7d49411..057c13e95e86 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/Utilities.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/Utilities.ts @@ -4,19 +4,20 @@ */ import type { ApiItem } from "@microsoft/api-extractor-model"; -import { type DocDeclarationReference } from "@microsoft/tsdoc"; +import type { DocDeclarationReference } from "@microsoft/tsdoc"; +import type { Link } from "../Link.js"; import { DocumentNode, type SectionNode } from "../documentation-domain/index.js"; -import { type Link } from "../Link.js"; +import { resolveSymbolicReference } from "../utilities/index.js"; + import { getDocumentPathForApiItem, getLinkForApiItem, shouldItemBeIncluded, } from "./ApiItemTransformUtilities.js"; -import { type TsdocNodeTransformOptions } from "./TsdocNodeTransforms.js"; -import { type ApiItemTransformationConfiguration } from "./configuration/index.js"; +import type { TsdocNodeTransformOptions } from "./TsdocNodeTransforms.js"; +import type { ApiItemTransformationConfiguration } from "./configuration/index.js"; import { wrapInSection } from "./helpers/index.js"; -import { resolveSymbolicReference } from "../utilities/index.js"; /** * Creates a {@link DocumentNode} representing the provided API item. diff --git a/tools/api-markdown-documenter/src/api-item-transforms/configuration/Configuration.ts b/tools/api-markdown-documenter/src/api-item-transforms/configuration/Configuration.ts index e50467bc074e..cb157378b441 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/configuration/Configuration.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/configuration/Configuration.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. */ -import { type ApiModel } from "@microsoft/api-extractor-model"; +import type { ApiModel } from "@microsoft/api-extractor-model"; -import { type ConfigurationBase } from "../../ConfigurationBase.js"; +import type { ConfigurationBase } from "../../ConfigurationBase.js"; import { defaultConsoleLogger } from "../../Logging.js"; + import { type DocumentationSuiteOptions, getDocumentationSuiteOptionsWithDefaults, diff --git a/tools/api-markdown-documenter/src/api-item-transforms/configuration/TransformationOptions.ts b/tools/api-markdown-documenter/src/api-item-transforms/configuration/TransformationOptions.ts index f2c464e90414..b04d2c56e0f0 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/configuration/TransformationOptions.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/configuration/TransformationOptions.ts @@ -3,30 +3,31 @@ * Licensed under the MIT License. */ -import { - type ApiCallSignature, - type ApiClass, - type ApiConstructSignature, - type ApiConstructor, - type ApiEntryPoint, - type ApiEnum, - type ApiEnumMember, - type ApiFunction, - type ApiIndexSignature, - type ApiInterface, - type ApiItem, - type ApiMethod, - type ApiMethodSignature, - type ApiModel, - type ApiNamespace, - type ApiPropertyItem, - type ApiTypeAlias, - type ApiVariable, +import type { + ApiCallSignature, + ApiClass, + ApiConstructSignature, + ApiConstructor, + ApiEntryPoint, + ApiEnum, + ApiEnumMember, + ApiFunction, + ApiIndexSignature, + ApiInterface, + ApiItem, + ApiMethod, + ApiMethodSignature, + ApiModel, + ApiNamespace, + ApiPropertyItem, + ApiTypeAlias, + ApiVariable, } from "@microsoft/api-extractor-model"; -import { type SectionNode } from "../../documentation-domain/index.js"; +import type { SectionNode } from "../../documentation-domain/index.js"; import * as DefaultTransformationImplementations from "../default-implementations/index.js"; -import { type ApiItemTransformationConfiguration } from "./Configuration.js"; + +import type { ApiItemTransformationConfiguration } from "./Configuration.js"; /** * Signature for a function which generates one or more {@link SectionNode}s describing an diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/CreateDefaultLayout.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/CreateDefaultLayout.ts index 942d514af7af..b6b34de3b7f1 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/CreateDefaultLayout.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/CreateDefaultLayout.ts @@ -5,9 +5,10 @@ import { type ApiItem, ReleaseTag } from "@microsoft/api-extractor-model"; -import { type SectionNode } from "../../documentation-domain/index.js"; +import type { SectionNode } from "../../documentation-domain/index.js"; +import { getReleaseTag } from "../../utilities/index.js"; import { doesItemRequireOwnDocument, getHeadingForApiItem } from "../ApiItemTransformUtilities.js"; -import { type ApiItemTransformationConfiguration } from "../configuration/index.js"; +import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; import { alphaWarningSpan, betaWarningSpan, @@ -20,7 +21,6 @@ import { createThrowsSection, wrapInSection, } from "../helpers/index.js"; -import { getReleaseTag } from "../../utilities/index.js"; /** * Default content layout for all API items. diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiClass.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiClass.ts index e6efd8f70c9c..768e22836749 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiClass.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiClass.ts @@ -16,9 +16,9 @@ import { import type { SectionNode } from "../../documentation-domain/index.js"; import { ApiModifier, getScopedMemberNameForDiagnostics, isStatic } from "../../utilities/index.js"; +import { filterChildMembers } from "../ApiItemTransformUtilities.js"; import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; import { createChildDetailsSection, createMemberTables } from "../helpers/index.js"; -import { filterChildMembers } from "../ApiItemTransformUtilities.js"; /** * Default documentation transform for `Class` items. diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiEntryPoint.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiEntryPoint.ts index d5a1d38badb4..dcb9d2a0b764 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiEntryPoint.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiEntryPoint.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. */ -import { type ApiEntryPoint, type ApiItem } from "@microsoft/api-extractor-model"; +import type { ApiEntryPoint, ApiItem } from "@microsoft/api-extractor-model"; + +import type { SectionNode } from "../../documentation-domain/index.js"; +import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; -import { type SectionNode } from "../../documentation-domain/index.js"; -import { type ApiItemTransformationConfiguration } from "../configuration/index.js"; import { transformApiModuleLike } from "./TransformApiModuleLike.js"; /** diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiEnum.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiEnum.ts index 11971f66445d..70439a510bf8 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiEnum.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiEnum.ts @@ -11,10 +11,10 @@ import { } from "@microsoft/api-extractor-model"; import type { DocumentationNode, SectionNode } from "../../documentation-domain/index.js"; +import { getScopedMemberNameForDiagnostics } from "../../utilities/index.js"; +import { filterChildMembers } from "../ApiItemTransformUtilities.js"; import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; import { createMemberTables, wrapInSection } from "../helpers/index.js"; -import { filterChildMembers } from "../ApiItemTransformUtilities.js"; -import { getScopedMemberNameForDiagnostics } from "../../utilities/index.js"; /** * Default documentation transform for `Enum` items. diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiFunctionLike.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiFunctionLike.ts index b45dfe07cd6d..15a8134e9601 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiFunctionLike.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiFunctionLike.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. */ -import { type SectionNode } from "../../documentation-domain/index.js"; -import { type ApiFunctionLike } from "../../utilities/index.js"; -import { type ApiItemTransformationConfiguration } from "../configuration/index.js"; +import type { SectionNode } from "../../documentation-domain/index.js"; +import type { ApiFunctionLike } from "../../utilities/index.js"; +import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; import { createParametersSection, createReturnsSection } from "../helpers/index.js"; /** diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiInterface.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiInterface.ts index 563395255a4d..caded49e4a5a 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiInterface.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiInterface.ts @@ -15,10 +15,10 @@ import { } from "@microsoft/api-extractor-model"; import type { SectionNode } from "../../documentation-domain/index.js"; +import { getScopedMemberNameForDiagnostics } from "../../utilities/index.js"; +import { filterChildMembers } from "../ApiItemTransformUtilities.js"; import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; import { createChildDetailsSection, createMemberTables } from "../helpers/index.js"; -import { filterChildMembers } from "../ApiItemTransformUtilities.js"; -import { getScopedMemberNameForDiagnostics } from "../../utilities/index.js"; /** * Default documentation transform for `Interface` items. diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiItemWithoutChildren.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiItemWithoutChildren.ts index e2886a3749ff..594c9d00e25f 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiItemWithoutChildren.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiItemWithoutChildren.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. */ -import { type ApiItem } from "@microsoft/api-extractor-model"; +import type { ApiItem } from "@microsoft/api-extractor-model"; -import { type SectionNode } from "../../documentation-domain/index.js"; -import { type ApiItemTransformationConfiguration } from "../configuration/index.js"; +import type { SectionNode } from "../../documentation-domain/index.js"; +import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; /** * Default transformation helper for rendering item kinds that do not have children. diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiModel.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiModel.ts index 79e086612677..cb572c05448b 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiModel.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiModel.ts @@ -6,7 +6,7 @@ import { ApiItemKind, type ApiModel } from "@microsoft/api-extractor-model"; import { ParagraphNode, SectionNode, SpanNode } from "../../documentation-domain/index.js"; -import { type ApiItemTransformationConfiguration } from "../configuration/index.js"; +import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; import { createTableWithHeading } from "../helpers/index.js"; /** diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiModuleLike.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiModuleLike.ts index 4f8942ad7482..c74976e54db2 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiModuleLike.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiModuleLike.ts @@ -17,10 +17,10 @@ import { import type { SectionNode } from "../../documentation-domain/index.js"; import type { ApiModuleLike } from "../../utilities/index.js"; +import { getScopedMemberNameForDiagnostics } from "../../utilities/index.js"; +import { filterItems } from "../ApiItemTransformUtilities.js"; import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; import { createChildDetailsSection, createMemberTables } from "../helpers/index.js"; -import { filterItems } from "../ApiItemTransformUtilities.js"; -import { getScopedMemberNameForDiagnostics } from "../../utilities/index.js"; /** * Default documentation transform for module-like API items (packages, namespaces). diff --git a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiNamespace.ts b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiNamespace.ts index 1a4775004f60..89c2521bc51d 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiNamespace.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/default-implementations/TransformApiNamespace.ts @@ -7,6 +7,7 @@ import type { ApiItem, ApiNamespace } from "@microsoft/api-extractor-model"; import type { SectionNode } from "../../documentation-domain/index.js"; import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; + import { transformApiModuleLike } from "./TransformApiModuleLike.js"; /** diff --git a/tools/api-markdown-documenter/src/api-item-transforms/helpers/Helpers.ts b/tools/api-markdown-documenter/src/api-item-transforms/helpers/Helpers.ts index 9b570a577c73..5865ecfed0e5 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/helpers/Helpers.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/helpers/Helpers.ts @@ -29,7 +29,8 @@ import { type DocSection, } from "@microsoft/tsdoc"; -import { type Heading } from "../../Heading.js"; +import type { Heading } from "../../Heading.js"; +import type { Logger } from "../../Logging.js"; import { type DocumentationNode, DocumentationNodeType, @@ -46,7 +47,6 @@ import { SpanNode, UnorderedListNode, } from "../../documentation-domain/index.js"; -import { type Logger } from "../../Logging.js"; import { type ApiFunctionLike, injectSeparator, @@ -65,7 +65,8 @@ import { } from "../ApiItemTransformUtilities.js"; import { transformTsdocSection } from "../TsdocNodeTransforms.js"; import { getTsdocNodeTransformationOptions } from "../Utilities.js"; -import { type ApiItemTransformationConfiguration } from "../configuration/index.js"; +import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; + import { createParametersSummaryTable, createTypeParametersSummaryTable } from "./TableHelpers.js"; /** diff --git a/tools/api-markdown-documenter/src/api-item-transforms/helpers/TableHelpers.ts b/tools/api-markdown-documenter/src/api-item-transforms/helpers/TableHelpers.ts index 530820599053..ab4cd1dce559 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/helpers/TableHelpers.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/helpers/TableHelpers.ts @@ -39,7 +39,8 @@ import { import { getLinkForApiItem } from "../ApiItemTransformUtilities.js"; import { transformTsdocSection } from "../TsdocNodeTransforms.js"; import { getTsdocNodeTransformationOptions } from "../Utilities.js"; -import { type ApiItemTransformationConfiguration } from "../configuration/index.js"; +import type { ApiItemTransformationConfiguration } from "../configuration/index.js"; + import { createExcerptSpanWithHyperlinks } from "./Helpers.js"; /** diff --git a/tools/api-markdown-documenter/src/api-item-transforms/test/Transformation.test.ts b/tools/api-markdown-documenter/src/api-item-transforms/test/Transformation.test.ts index 07c3da53f896..00a8bddeb3f1 100644 --- a/tools/api-markdown-documenter/src/api-item-transforms/test/Transformation.test.ts +++ b/tools/api-markdown-documenter/src/api-item-transforms/test/Transformation.test.ts @@ -38,12 +38,12 @@ import { } from "../../documentation-domain/index.js"; import { getHeadingForApiItem } from "../ApiItemTransformUtilities.js"; import { apiItemToSections } from "../TransformApiItem.js"; +import { transformApiModel } from "../TransformApiModel.js"; import { type ApiItemTransformationConfiguration, getApiItemTransformationConfigurationWithDefaults, } from "../configuration/index.js"; import { betaWarningSpan, wrapInSection } from "../helpers/index.js"; -import { transformApiModel } from "../TransformApiModel.js"; /** * Sample "default" configuration. diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/ToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/ToHtml.ts index 8a59ac849cb8..de82c97be25a 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/ToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/ToHtml.ts @@ -5,12 +5,14 @@ import type { Root as HastRoot, Nodes as HastTree } from "hast"; import { h } from "hastscript"; + import type { DocumentNode, DocumentationNode } from "../documentation-domain/index.js"; -import type { TransformationConfig } from "./configuration/index.js"; + import { createTransformationContext, type TransformationContext, } from "./TransformationContext.js"; +import type { TransformationConfig } from "./configuration/index.js"; /** * Generates an HTML AST from the provided {@link DocumentNode}. diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/TransformationContext.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/TransformationContext.ts index af0d21046616..9ad66f6d4f53 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/TransformationContext.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/TransformationContext.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. */ -import type { TextFormatting } from "../documentation-domain/index.js"; import { defaultConsoleLogger, type Logger } from "../Logging.js"; +import type { TextFormatting } from "../documentation-domain/index.js"; + import { defaultTransformations, type TransformationConfig, diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/Utilities.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/Utilities.ts index 00d657c46be6..02e22caf36ec 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/Utilities.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/Utilities.ts @@ -9,9 +9,11 @@ */ import type { Element as HastElement } from "hast"; import { h } from "hastscript"; + import type { DocumentationNode } from "../index.js"; -import type { TransformationContext } from "./TransformationContext.js"; + import { documentationNodesToHtml } from "./ToHtml.js"; +import type { TransformationContext } from "./TransformationContext.js"; /** * An HTML tag and its (optional) attributes. diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/configuration/Configuration.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/configuration/Configuration.ts index a247dddfc753..37c5d6052753 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/configuration/Configuration.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/configuration/Configuration.ts @@ -4,9 +4,10 @@ */ import type { ConfigurationBase } from "../../ConfigurationBase.js"; -import type { TextFormatting } from "../../documentation-domain/index.js"; import { defaultConsoleLogger } from "../../Logging.js"; -import { type Transformations } from "./Transformation.js"; +import type { TextFormatting } from "../../documentation-domain/index.js"; + +import type { Transformations } from "./Transformation.js"; /** * Configuration for transforming {@link DocumentationNode}s to HTML. diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/configuration/Transformation.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/configuration/Transformation.ts index bbb8c49fb4ec..0323526c74c9 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/configuration/Transformation.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/configuration/Transformation.ts @@ -5,6 +5,7 @@ import type { Nodes as HastNodes } from "hast"; import { h } from "hastscript"; + import { DocumentationNodeType, type DocumentationNode, @@ -23,6 +24,7 @@ import { type TableRowNode, type UnorderedListNode, } from "../../documentation-domain/index.js"; +import type { TransformationContext } from "../TransformationContext.js"; import { blockQuoteToHtml, codeSpanToHtml, @@ -39,7 +41,6 @@ import { tableRowToHtml, unorderedListToHtml, } from "../default-transformations/index.js"; -import type { TransformationContext } from "../TransformationContext.js"; /** * Configuration for transforming {@link DocumentationNode}s to {@link https://github.com/syntax-tree/hast | hast}, diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/BlockQuoteToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/BlockQuoteToHtml.ts index 0928a30d7e5d..c8d31e7a3bd8 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/BlockQuoteToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/BlockQuoteToHtml.ts @@ -8,6 +8,7 @@ * Licensed under the MIT License. */ import type { Element as HastElement } from "hast"; + import type { BlockQuoteNode } from "../../index.js"; import type { TransformationContext } from "../TransformationContext.js"; import { transformChildrenUnderTag } from "../Utilities.js"; diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/CodeSpanToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/CodeSpanToHtml.ts index 79f2465bc7f8..70705c21ef72 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/CodeSpanToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/CodeSpanToHtml.ts @@ -8,6 +8,7 @@ import type { Nodes as HastTree } from "hast"; import type { CodeSpanNode } from "../../index.js"; import type { TransformationContext } from "../TransformationContext.js"; import { transformChildrenUnderTag } from "../Utilities.js"; + import { applyFormatting } from "./Utilities.js"; /** diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/FencedCodeBlockToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/FencedCodeBlockToHtml.ts index 021949c36331..dff11b391b8a 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/FencedCodeBlockToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/FencedCodeBlockToHtml.ts @@ -8,6 +8,7 @@ * Licensed under the MIT License. */ import type { Element as HastElement } from "hast"; + import type { FencedCodeBlockNode } from "../../documentation-domain/index.js"; import type { TransformationContext } from "../TransformationContext.js"; import { transformChildrenUnderTag } from "../Utilities.js"; diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/HeadingToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/HeadingToHtml.ts index a8d5fa6f2a15..e06bb7c3c301 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/HeadingToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/HeadingToHtml.ts @@ -9,6 +9,7 @@ */ import type { Element as HastElement, Nodes as HastNodes } from "hast"; import { h } from "hastscript"; + import type { HeadingNode } from "../../documentation-domain/index.js"; import type { TransformationContext } from "../TransformationContext.js"; import { transformChildrenUnderTag } from "../Utilities.js"; diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/LinkToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/LinkToHtml.ts index 17eb3835800f..75134ba0ef7e 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/LinkToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/LinkToHtml.ts @@ -8,6 +8,7 @@ * Licensed under the MIT License. */ import type { Element as HastElement } from "hast"; + import type { LinkNode } from "../../documentation-domain/index.js"; import type { TransformationContext } from "../TransformationContext.js"; import { transformChildrenUnderTag } from "../Utilities.js"; diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/OrderedListToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/OrderedListToHtml.ts index 272264caa7c6..6443d2e7d541 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/OrderedListToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/OrderedListToHtml.ts @@ -9,6 +9,7 @@ */ import type { Element as HastElement } from "hast"; import { h } from "hastscript"; + import type { OrderedListNode } from "../../documentation-domain/index.js"; import type { TransformationContext } from "../TransformationContext.js"; import { transformListChildren } from "../Utilities.js"; diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/ParagraphToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/ParagraphToHtml.ts index 01947cad68bd..8ca5fede24ac 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/ParagraphToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/ParagraphToHtml.ts @@ -8,6 +8,7 @@ * Licensed under the MIT License. */ import type { Element as HastElement } from "hast"; + import type { ParagraphNode } from "../../documentation-domain/index.js"; import type { TransformationContext } from "../TransformationContext.js"; import { transformChildrenUnderTag } from "../Utilities.js"; diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/PlainTextToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/PlainTextToHtml.ts index 4b6e00825bbe..05516bc246fd 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/PlainTextToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/PlainTextToHtml.ts @@ -8,8 +8,10 @@ import "hast-util-raw"; import type { Nodes as HastTree } from "hast"; + import type { PlainTextNode } from "../../documentation-domain/index.js"; import type { TransformationContext } from "../TransformationContext.js"; + import { applyFormatting } from "./Utilities.js"; /** diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/SectionToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/SectionToHtml.ts index b54897f7d057..c525f7ced223 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/SectionToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/SectionToHtml.ts @@ -9,9 +9,10 @@ */ import type { Element as HastElement, Nodes as HastNodes } from "hast"; import { h } from "hastscript"; + import type { SectionNode } from "../../documentation-domain/index.js"; -import type { TransformationContext } from "../TransformationContext.js"; import { documentationNodeToHtml, documentationNodesToHtml } from "../ToHtml.js"; +import type { TransformationContext } from "../TransformationContext.js"; /** * Transform a {@link SectionNode} to HTML. diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/SpanToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/SpanToHtml.ts index 90dccc2289a1..d4e2455855cb 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/SpanToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/SpanToHtml.ts @@ -7,8 +7,8 @@ import type { Element as HastElement } from "hast"; import { h } from "hastscript"; import type { SpanNode } from "../../documentation-domain/index.js"; -import type { TransformationContext } from "../TransformationContext.js"; import { documentationNodesToHtml } from "../ToHtml.js"; +import type { TransformationContext } from "../TransformationContext.js"; /** * Transform a {@link SpanNode} to HTML. diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableCellToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableCellToHtml.ts index 62a4c2ed6f41..b60cd0e161bf 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableCellToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableCellToHtml.ts @@ -8,9 +8,10 @@ * Licensed under the MIT License. */ import type { Element as HastElement } from "hast"; + import { TableCellKind, type TableCellNode } from "../../documentation-domain/index.js"; -import { transformChildrenUnderTag } from "../Utilities.js"; import type { TransformationContext } from "../TransformationContext.js"; +import { transformChildrenUnderTag } from "../Utilities.js"; /** * Transform a {@link TableCellNode} to HTML. diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableRowToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableRowToHtml.ts index 2784bc903c67..86dc06587168 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableRowToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableRowToHtml.ts @@ -8,6 +8,7 @@ * Licensed under the MIT License. */ import type { Element as HastElement } from "hast"; + import type { TableRowNode } from "../../documentation-domain/index.js"; import type { TransformationContext } from "../TransformationContext.js"; import { transformChildrenUnderTag, type HtmlTag } from "../Utilities.js"; diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableToHtml.ts index 2811e41c0cba..9ab2298d559b 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/TableToHtml.ts @@ -9,9 +9,10 @@ */ import type { Element as HastElement } from "hast"; import { h } from "hastscript"; + import type { TableNode } from "../../documentation-domain/index.js"; -import { transformChildrenUnderTag } from "../Utilities.js"; import type { TransformationContext } from "../TransformationContext.js"; +import { transformChildrenUnderTag } from "../Utilities.js"; /** * Transform a {@link TableNode} to HTML. diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/UnorderedListToHtml.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/UnorderedListToHtml.ts index 2f8d0b22fdd2..5e8b757877a9 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/UnorderedListToHtml.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/default-transformations/UnorderedListToHtml.ts @@ -9,6 +9,7 @@ */ import type { Element as HastElement } from "hast"; import { h } from "hastscript"; + import type { UnorderedListNode } from "../../documentation-domain/index.js"; import type { TransformationContext } from "../TransformationContext.js"; import { transformListChildren } from "../Utilities.js"; diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/BlockQuoteToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/BlockQuoteToHtml.test.ts index 977808cd357f..13281e009cee 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/BlockQuoteToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/BlockQuoteToHtml.test.ts @@ -4,7 +4,9 @@ */ import { h } from "hastscript"; + import { BlockQuoteNode, LineBreakNode, PlainTextNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("BlockQuote HTML rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/CodeSpanToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/CodeSpanToHtml.test.ts index 88bdf23972bd..8272bb2c0ada 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/CodeSpanToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/CodeSpanToHtml.test.ts @@ -8,7 +8,9 @@ * Licensed under the MIT License. */ import { h } from "hastscript"; + import { CodeSpanNode, PlainTextNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("CodeSpan HTML rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/CustomNodeTypeToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/CustomNodeTypeToHtml.test.ts index d0e95612dd35..cf5ba7012d76 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/CustomNodeTypeToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/CustomNodeTypeToHtml.test.ts @@ -9,8 +9,10 @@ */ import { expect } from "chai"; import type { Nodes as HastNodes } from "hast"; + import { DocumentationLiteralNodeBase } from "../../documentation-domain/index.js"; import type { TransformationContext } from "../TransformationContext.js"; + import { testTransformation } from "./Utilities.js"; /** diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/DocumentToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/DocumentToHtml.test.ts index c0db1a117f93..b234d837fbc9 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/DocumentToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/DocumentToHtml.test.ts @@ -5,6 +5,7 @@ import { expect } from "chai"; import { h } from "hastscript"; + import { DocumentNode, HeadingNode, diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/FencedCodeBlockToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/FencedCodeBlockToHtml.test.ts index 6459bf635f94..817b69d6f4a1 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/FencedCodeBlockToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/FencedCodeBlockToHtml.test.ts @@ -4,11 +4,13 @@ */ import { h } from "hastscript"; + import { FencedCodeBlockNode, LineBreakNode, PlainTextNode, } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; const brElement = h("br"); diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HeadingToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HeadingToHtml.test.ts index b895e4ace39b..7ec15145bcc8 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HeadingToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HeadingToHtml.test.ts @@ -10,6 +10,7 @@ import { h } from "hastscript"; import { HeadingNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("HeadingNode -> Html", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HierarchicalSectionToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HierarchicalSectionToHtml.test.ts index 99f251120cd5..4aaefd283538 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HierarchicalSectionToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HierarchicalSectionToHtml.test.ts @@ -15,6 +15,7 @@ import { ParagraphNode, SectionNode, } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("HierarchicalSection HTML rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HorizontalRuleToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HorizontalRuleToHtml.test.ts index 7b5adf2d75b8..6434efb6f2c4 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HorizontalRuleToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/HorizontalRuleToHtml.test.ts @@ -10,6 +10,7 @@ import { h } from "hastscript"; import { HorizontalRuleNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; it("HorizontalRule HTML rendering test", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/LineBreakToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/LineBreakToHtml.test.ts index cd6e67b78bbc..dc1e5208d526 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/LineBreakToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/LineBreakToHtml.test.ts @@ -10,6 +10,7 @@ import { h } from "hastscript"; import { LineBreakNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; it("LineBreak HTML rendering test", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/LinkToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/LinkToHtml.test.ts index 848f66e599c7..a860ff86f87c 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/LinkToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/LinkToHtml.test.ts @@ -10,6 +10,7 @@ import { h } from "hastscript"; import { LinkNode, PlainTextNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("Link HTML rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/OrderedListToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/OrderedListToHtml.test.ts index 0194fdc0798f..af32ac906ca6 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/OrderedListToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/OrderedListToHtml.test.ts @@ -10,6 +10,7 @@ import { h } from "hastscript"; import { OrderedListNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("OrderedListNode HTML rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/ParagraphToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/ParagraphToHtml.test.ts index f50853105161..02e49a572ea5 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/ParagraphToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/ParagraphToHtml.test.ts @@ -8,7 +8,9 @@ * Licensed under the MIT License. */ import { h } from "hastscript"; + import { ParagraphNode, PlainTextNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("ParagraphNode HTML rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/PlainTextToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/PlainTextToHtml.test.ts index d8e5f760d4a0..0be5d77134a0 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/PlainTextToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/PlainTextToHtml.test.ts @@ -4,7 +4,9 @@ */ import type { Nodes as HastTree } from "hast"; + import { PlainTextNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("PlainText to HTML transformation tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/SpanToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/SpanToHtml.test.ts index 174c47fb1db8..e2426a843537 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/SpanToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/SpanToHtml.test.ts @@ -4,12 +4,14 @@ */ import { h } from "hastscript"; + import { LineBreakNode, PlainTextNode, SpanNode, type TextFormatting, } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("Span to HTML transformation tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/TableToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/TableToHtml.test.ts index 82fe8c7809dc..ee792b4926df 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/TableToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/TableToHtml.test.ts @@ -8,6 +8,7 @@ * Licensed under the MIT License. */ import { h } from "hastscript"; + import { TableBodyCellNode, TableBodyRowNode, @@ -15,6 +16,7 @@ import { TableHeaderRowNode, TableNode, } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("Table HTML rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/UnorderedListToHtml.test.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/UnorderedListToHtml.test.ts index 1fc2bbff4817..6079586d504d 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/UnorderedListToHtml.test.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/UnorderedListToHtml.test.ts @@ -4,7 +4,9 @@ */ import { h } from "hastscript"; + import { UnorderedListNode } from "../../documentation-domain/index.js"; + import { assertTransformation } from "./Utilities.js"; describe("UnorderedListNode HTML rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/Utilities.ts b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/Utilities.ts index 3a7275c38d6f..fb569575a3ce 100644 --- a/tools/api-markdown-documenter/src/documentation-domain-to-html/test/Utilities.ts +++ b/tools/api-markdown-documenter/src/documentation-domain-to-html/test/Utilities.ts @@ -5,10 +5,11 @@ import { expect } from "chai"; import type { Nodes as HastNodes } from "hast"; + import type { DocumentationNode } from "../../documentation-domain/index.js"; -import { createTransformationContext } from "../TransformationContext.js"; -import { type TransformationConfig } from "../configuration/index.js"; import { documentationNodeToHtml } from "../ToHtml.js"; +import { createTransformationContext } from "../TransformationContext.js"; +import type { TransformationConfig } from "../configuration/index.js"; /** * Tests transforming an individual {@link DocumentationNode} to HTML. diff --git a/tools/api-markdown-documenter/src/documentation-domain/DocumentNode.ts b/tools/api-markdown-documenter/src/documentation-domain/DocumentNode.ts index c56c66365f86..c52f119346a5 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/DocumentNode.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/DocumentNode.ts @@ -5,9 +5,10 @@ import type { Parent as UnistParent } from "unist"; -import { type ApiItem } from "../index.js"; +import type { ApiItem } from "../index.js"; + import { DocumentationNodeType } from "./DocumentationNodeType.js"; -import { type SectionNode } from "./SectionNode.js"; +import type { SectionNode } from "./SectionNode.js"; /** * {@link DocumentNode} construction properties. diff --git a/tools/api-markdown-documenter/src/documentation-domain/HeadingNode.ts b/tools/api-markdown-documenter/src/documentation-domain/HeadingNode.ts index f31c008a738f..bbc109b3036c 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/HeadingNode.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/HeadingNode.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import { type Heading } from "../Heading.js"; +import type { Heading } from "../Heading.js"; + import { DocumentationParentNodeBase, type MultiLineDocumentationNode, diff --git a/tools/api-markdown-documenter/src/documentation-domain/HorizontalRuleNode.ts b/tools/api-markdown-documenter/src/documentation-domain/HorizontalRuleNode.ts index 503d063968e1..f17ae151307d 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/HorizontalRuleNode.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/HorizontalRuleNode.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { type MultiLineDocumentationNode } from "./DocumentationNode.js"; +import type { MultiLineDocumentationNode } from "./DocumentationNode.js"; import { DocumentationNodeType } from "./DocumentationNodeType.js"; /** diff --git a/tools/api-markdown-documenter/src/documentation-domain/LineBreakNode.ts b/tools/api-markdown-documenter/src/documentation-domain/LineBreakNode.ts index b434e93c4d52..73fb4a3f4fb2 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/LineBreakNode.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/LineBreakNode.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { type MultiLineDocumentationNode } from "./DocumentationNode.js"; +import type { MultiLineDocumentationNode } from "./DocumentationNode.js"; import { DocumentationNodeType } from "./DocumentationNodeType.js"; /** diff --git a/tools/api-markdown-documenter/src/documentation-domain/LinkNode.ts b/tools/api-markdown-documenter/src/documentation-domain/LinkNode.ts index 9770a556ba00..1ed1fff117ab 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/LinkNode.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/LinkNode.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import { type Link, type UrlTarget } from "../Link.js"; +import type { Link, UrlTarget } from "../Link.js"; + import { DocumentationParentNodeBase, type SingleLineDocumentationNode, diff --git a/tools/api-markdown-documenter/src/documentation-domain/SectionNode.ts b/tools/api-markdown-documenter/src/documentation-domain/SectionNode.ts index cf4a1e657aab..489664683e44 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/SectionNode.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/SectionNode.ts @@ -9,7 +9,7 @@ import { type MultiLineDocumentationNode, } from "./DocumentationNode.js"; import { DocumentationNodeType } from "./DocumentationNodeType.js"; -import { type HeadingNode } from "./HeadingNode.js"; +import type { HeadingNode } from "./HeadingNode.js"; /** * Represents a hierarchically nested section. diff --git a/tools/api-markdown-documenter/src/documentation-domain/SpanNode.ts b/tools/api-markdown-documenter/src/documentation-domain/SpanNode.ts index fd2ffd8c6e50..78ee0b583e02 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/SpanNode.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/SpanNode.ts @@ -10,7 +10,7 @@ import { } from "./DocumentationNode.js"; import { DocumentationNodeType } from "./DocumentationNodeType.js"; import { PlainTextNode } from "./PlainTextNode.js"; -import { type TextFormatting } from "./TextFormatting.js"; +import type { TextFormatting } from "./TextFormatting.js"; import { createNodesFromPlainText } from "./Utilities.js"; // TODO: Rename to "FormattedSpan" - this doesn't really correspond to a "span" in a traditional sense. diff --git a/tools/api-markdown-documenter/src/documentation-domain/TableNode.ts b/tools/api-markdown-documenter/src/documentation-domain/TableNode.ts index 8e54dc16f16c..620ae0b8e6ad 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/TableNode.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/TableNode.ts @@ -8,7 +8,7 @@ import { type MultiLineDocumentationNode, } from "./DocumentationNode.js"; import { DocumentationNodeType } from "./DocumentationNodeType.js"; -import { type TableBodyRowNode, type TableHeaderRowNode } from "./TableRowNode.js"; +import type { TableBodyRowNode, TableHeaderRowNode } from "./TableRowNode.js"; // TODOs: // - Support alignment properties in Table, TableRow and TableCell (inherit pattern for resolution) diff --git a/tools/api-markdown-documenter/src/documentation-domain/TableRowNode.ts b/tools/api-markdown-documenter/src/documentation-domain/TableRowNode.ts index 2b3156a81ead..5d095a5ef80a 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/TableRowNode.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/TableRowNode.ts @@ -5,7 +5,7 @@ import { DocumentationParentNodeBase } from "./DocumentationNode.js"; import { DocumentationNodeType } from "./DocumentationNodeType.js"; -import { type TableCellNode, type TableHeaderCellNode } from "./TableCellNode.js"; +import type { TableCellNode, TableHeaderCellNode } from "./TableCellNode.js"; /** * Kind of Table Row. diff --git a/tools/api-markdown-documenter/src/documentation-domain/Utilities.ts b/tools/api-markdown-documenter/src/documentation-domain/Utilities.ts index 71939e8f4d03..52c44cd7e3a7 100644 --- a/tools/api-markdown-documenter/src/documentation-domain/Utilities.ts +++ b/tools/api-markdown-documenter/src/documentation-domain/Utilities.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { type DocumentationNode, type SingleLineDocumentationNode } from "./DocumentationNode.js"; +import type { DocumentationNode, SingleLineDocumentationNode } from "./DocumentationNode.js"; import { LineBreakNode } from "./LineBreakNode.js"; import { PlainTextNode } from "./PlainTextNode.js"; diff --git a/tools/api-markdown-documenter/src/index.ts b/tools/api-markdown-documenter/src/index.ts index c79ab543f4ca..5e336ff66f46 100644 --- a/tools/api-markdown-documenter/src/index.ts +++ b/tools/api-markdown-documenter/src/index.ts @@ -77,7 +77,7 @@ export { // #region Scoped exports // This pattern is required to scope the utilities in a way that API-Extractor supports. -/* eslint-disable unicorn/prefer-export-from */ +/* eslint-disable import/order, unicorn/prefer-export-from */ // Export `ApiItem`-related utilities import * as ApiItemUtilities from "./ApiItemUtilitiesModule.js"; @@ -124,7 +124,7 @@ export { MarkdownRenderer, }; -/* eslint-enable unicorn/prefer-export-from */ +/* eslint-enable import/order, unicorn/prefer-export-from */ // #endregion diff --git a/tools/api-markdown-documenter/src/renderers/html-renderer/Render.ts b/tools/api-markdown-documenter/src/renderers/html-renderer/Render.ts index 69cd012317b0..fd7a4fe4606a 100644 --- a/tools/api-markdown-documenter/src/renderers/html-renderer/Render.ts +++ b/tools/api-markdown-documenter/src/renderers/html-renderer/Render.ts @@ -7,11 +7,11 @@ import type { Root as HastRoot, Nodes as HastTree } from "hast"; import { format } from "hast-util-format"; import { toHtml as toHtmlString } from "hast-util-to-html"; +import type { DocumentNode } from "../../documentation-domain/index.js"; import { documentToHtml, type TransformationConfig, } from "../../documentation-domain-to-html/index.js"; -import type { DocumentNode } from "../../documentation-domain/index.js"; /** * Configuration for rendering HTML. diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/Render.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/Render.ts index 2cb395309c41..e60590125f5c 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/Render.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/Render.ts @@ -5,8 +5,9 @@ import type { DocumentNode, DocumentationNode } from "../../documentation-domain/index.js"; import { DocumentWriter } from "../DocumentWriter.js"; -import { type RenderConfiguration, defaultRenderers } from "./configuration/index.js"; + import { type RenderContext, getContextWithDefaults } from "./RenderContext.js"; +import { type RenderConfiguration, defaultRenderers } from "./configuration/index.js"; /** * Renders a {@link DocumentNode} as Markdown, and returns the resulting file contents as a `string`. diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/RenderContext.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/RenderContext.ts index e4ad71dbcce6..7166050d47cc 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/RenderContext.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/RenderContext.ts @@ -4,6 +4,7 @@ */ import type { TextFormatting } from "../../documentation-domain/index.js"; + import type { Renderers } from "./configuration/index.js"; /** diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/Utilities.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/Utilities.ts index 44be63787742..c78885364a9d 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/Utilities.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/Utilities.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. */ -import { type DocumentationNode } from "../../documentation-domain/index.js"; -import { type DocumentWriter } from "../DocumentWriter.js"; -import { renderHtml } from "../html-renderer/index.js"; -import { type RenderContext as MarkdownRenderContext } from "./RenderContext.js"; +import type { DocumentationNode } from "../../documentation-domain/index.js"; import { documentationNodeToHtml } from "../../documentation-domain-to-html/index.js"; +import type { DocumentWriter } from "../DocumentWriter.js"; +import { renderHtml } from "../html-renderer/index.js"; + +import type { RenderContext as MarkdownRenderContext } from "./RenderContext.js"; /** * Renders the provided {@link DocumentationNode} using HTML syntax. diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/configuration/Configuration.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/configuration/Configuration.ts index 5830ad3f4137..d97838080b55 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/configuration/Configuration.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/configuration/Configuration.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. */ -import { type ConfigurationBase } from "../../../ConfigurationBase.js"; +import type { ConfigurationBase } from "../../../ConfigurationBase.js"; import { defaultConsoleLogger } from "../../../Logging.js"; -import { type Renderers } from "./RenderOptions.js"; + +import type { Renderers } from "./RenderOptions.js"; /** * Configuration for Markdown rendering of generated documentation contents. diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/default-renderers/RenderHeading.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/default-renderers/RenderHeading.ts index dd9e4122856d..8368e60f988c 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/default-renderers/RenderHeading.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/default-renderers/RenderHeading.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. */ -import { type HeadingNode } from "../../../documentation-domain/index.js"; +import type { HeadingNode } from "../../../documentation-domain/index.js"; import type { DocumentWriter } from "../../DocumentWriter.js"; import { renderNodes } from "../Render.js"; import type { RenderContext } from "../RenderContext.js"; import { renderNodeWithHtmlSyntax } from "../Utilities.js"; + import { escapeTextForMarkdown } from "./RenderPlainText.js"; /** diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderBlockQuote.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderBlockQuote.test.ts index 8e5653eeae79..0729fd7ef325 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderBlockQuote.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderBlockQuote.test.ts @@ -10,6 +10,7 @@ import { LineBreakNode, PlainTextNode, } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("BlockQuote Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderCodeSpan.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderCodeSpan.test.ts index edcc939ace7e..f479b5df9ed4 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderCodeSpan.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderCodeSpan.test.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import { CodeSpanNode, PlainTextNode } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("CodeSpan Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderCustomNodeType.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderCustomNodeType.test.ts index fe163168f243..0e6b09e4d5dd 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderCustomNodeType.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderCustomNodeType.test.ts @@ -6,8 +6,9 @@ import { expect } from "chai"; import { DocumentationLiteralNodeBase } from "../../../documentation-domain/index.js"; -import { type DocumentWriter } from "../../DocumentWriter.js"; -import { type RenderContext } from "../RenderContext.js"; +import type { DocumentWriter } from "../../DocumentWriter.js"; +import type { RenderContext } from "../RenderContext.js"; + import { testRender } from "./Utilities.js"; /** diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderFencedCodeBlock.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderFencedCodeBlock.test.ts index af085f338900..cdb98c5d5c3e 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderFencedCodeBlock.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderFencedCodeBlock.test.ts @@ -10,6 +10,7 @@ import { LineBreakNode, PlainTextNode, } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("FencedCodeBlock Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHeading.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHeading.test.ts index b91e55c4aa27..638f4beb4d25 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHeading.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHeading.test.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import { HeadingNode } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("Heading Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHierarchicalSection.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHierarchicalSection.test.ts index 3e72e739037f..3f0e63496fb0 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHierarchicalSection.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHierarchicalSection.test.ts @@ -11,6 +11,7 @@ import { ParagraphNode, SectionNode, } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("HierarchicalSection Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHorizontalRule.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHorizontalRule.test.ts index b03dbedbd591..9de7a6934efc 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHorizontalRule.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderHorizontalRule.test.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import { HorizontalRuleNode } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("HorizontalRule Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderLineBreak.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderLineBreak.test.ts index a2613bef8851..49059edba596 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderLineBreak.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderLineBreak.test.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import { LineBreakNode } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("LineBreak Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderLink.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderLink.test.ts index 8493aca0d913..7470fda72a59 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderLink.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderLink.test.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import { LinkNode, PlainTextNode } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("Link Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderOrderedList.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderOrderedList.test.ts index 44851a9784d4..426d7ae73362 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderOrderedList.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderOrderedList.test.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import { OrderedListNode } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("OrderedListNode Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderParagraph.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderParagraph.test.ts index 95c56beb6359..5919b9d35861 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderParagraph.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderParagraph.test.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import { ParagraphNode, PlainTextNode } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("ParagraphNode Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderPlainText.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderPlainText.test.ts index 313f09b0124f..fcaf64111030 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderPlainText.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderPlainText.test.ts @@ -6,7 +6,8 @@ import { expect } from "chai"; import { PlainTextNode } from "../../../documentation-domain/index.js"; -import { type RenderContext } from "../RenderContext.js"; +import type { RenderContext } from "../RenderContext.js"; + import { testRender } from "./Utilities.js"; describe("PlainText Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderSpan.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderSpan.test.ts index 7385703966b2..bd948567d013 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderSpan.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderSpan.test.ts @@ -11,6 +11,7 @@ import { SpanNode, type TextFormatting, } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("Span Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderTable.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderTable.test.ts index fe129cd1f256..f68e1b0263a2 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderTable.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderTable.test.ts @@ -12,6 +12,7 @@ import { TableHeaderRowNode, TableNode, } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("Table Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderTableCell.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderTableCell.test.ts index c999aa0b2b0b..dfb5fa74e2f9 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderTableCell.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderTableCell.test.ts @@ -13,6 +13,7 @@ import { TableBodyCellNode, TableHeaderCellNode, } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("Table Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderUnorderedList.test.ts b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderUnorderedList.test.ts index 75974f50101f..c481a828727c 100644 --- a/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderUnorderedList.test.ts +++ b/tools/api-markdown-documenter/src/renderers/markdown-renderer/test/RenderUnorderedList.test.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import { UnorderedListNode } from "../../../documentation-domain/index.js"; + import { testRender } from "./Utilities.js"; describe("UnorderedListNode Markdown rendering tests", () => { diff --git a/tools/api-markdown-documenter/src/test/EndToEndTests.ts b/tools/api-markdown-documenter/src/test/EndToEndTests.ts index 5d1a5097145d..26a71239774d 100644 --- a/tools/api-markdown-documenter/src/test/EndToEndTests.ts +++ b/tools/api-markdown-documenter/src/test/EndToEndTests.ts @@ -9,13 +9,13 @@ import type { ApiModel } from "@microsoft/api-extractor-model"; import { FileSystem } from "@rushstack/node-core-library"; import { expect } from "chai"; import { compare } from "dir-compare"; +import type { Suite } from "mocha"; +import { loadModel } from "../LoadModel.js"; import { transformApiModel, type ApiItemTransformationConfiguration, } from "../api-item-transforms/index.js"; -import type { Suite } from "mocha"; -import { loadModel } from "../LoadModel.js"; import type { DocumentNode } from "../documentation-domain/index.js"; /** diff --git a/tools/api-markdown-documenter/src/test/HtmlEndToEnd.test.ts b/tools/api-markdown-documenter/src/test/HtmlEndToEnd.test.ts index 3b2c142d643a..6a28afe7b443 100644 --- a/tools/api-markdown-documenter/src/test/HtmlEndToEnd.test.ts +++ b/tools/api-markdown-documenter/src/test/HtmlEndToEnd.test.ts @@ -9,13 +9,14 @@ import { fileURLToPath } from "node:url"; import { ApiItemKind, ReleaseTag } from "@microsoft/api-extractor-model"; import { FileSystem, NewlineKind } from "@rushstack/node-core-library"; +import type { DocumentNode } from "../documentation-domain/index.js"; import { type RenderDocumentAsHtmlConfig, renderDocumentAsHtml } from "../renderers/index.js"; + import { endToEndTests, type ApiModelTestOptions, type EndToEndTestConfig, } from "./EndToEndTests.js"; -import type { DocumentNode } from "../documentation-domain/index.js"; const dirname = Path.dirname(fileURLToPath(import.meta.url)); diff --git a/tools/api-markdown-documenter/src/test/MarkdownEndToEnd.test.ts b/tools/api-markdown-documenter/src/test/MarkdownEndToEnd.test.ts index d8078be416df..40d9dfda0a7d 100644 --- a/tools/api-markdown-documenter/src/test/MarkdownEndToEnd.test.ts +++ b/tools/api-markdown-documenter/src/test/MarkdownEndToEnd.test.ts @@ -9,13 +9,14 @@ import { fileURLToPath } from "node:url"; import { ApiItemKind, ReleaseTag } from "@microsoft/api-extractor-model"; import { FileSystem, NewlineKind } from "@rushstack/node-core-library"; +import type { DocumentNode } from "../documentation-domain/index.js"; import { type MarkdownRenderConfiguration, renderDocumentAsMarkdown } from "../renderers/index.js"; + import { endToEndTests, type ApiModelTestOptions, type EndToEndTestConfig, } from "./EndToEndTests.js"; -import type { DocumentNode } from "../documentation-domain/index.js"; const dirname = Path.dirname(fileURLToPath(import.meta.url)); diff --git a/tools/api-markdown-documenter/src/utilities/ApiItemUtilities.ts b/tools/api-markdown-documenter/src/utilities/ApiItemUtilities.ts index ce202bc59e44..e3f126aeef47 100644 --- a/tools/api-markdown-documenter/src/utilities/ApiItemUtilities.ts +++ b/tools/api-markdown-documenter/src/utilities/ApiItemUtilities.ts @@ -34,7 +34,8 @@ import { TSDocTagDefinition, } from "@microsoft/tsdoc"; import { PackageName } from "@rushstack/node-core-library"; -import { type Logger } from "../Logging.js"; + +import type { Logger } from "../Logging.js"; /** * This module contains general `ApiItem`-related types and utilities. From 4561fef182e69febca62be4fbcc5df55680e8e2e Mon Sep 17 00:00:00 2001 From: Noah Encke <78610362+noencke@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:50:38 -0800 Subject: [PATCH 19/40] Expose squash method on SharedTreeBranch (#23170) ## Description This exposes the ability to directly remove or squash commits on the head of a SharedTreeBranch. Currently this functionality is used when aborting or committing transactions. In addition to being a nice refactor in general, this will assist with future efforts to remove the notion of transactionality from branches. --- packages/dds/tree/src/core/rebase/utils.ts | 2 +- .../dds/tree/src/shared-tree-core/branch.ts | 141 +++++++++++------- .../src/test/shared-tree-core/branch.spec.ts | 73 +++++++++ 3 files changed, 161 insertions(+), 55 deletions(-) diff --git a/packages/dds/tree/src/core/rebase/utils.ts b/packages/dds/tree/src/core/rebase/utils.ts index e14630794095..d91382873df6 100644 --- a/packages/dds/tree/src/core/rebase/utils.ts +++ b/packages/dds/tree/src/core/rebase/utils.ts @@ -484,7 +484,7 @@ export function findAncestor( * @param descendant - a descendant. If an empty `path` array is included, it will be populated * with the chain of ancestry for `descendant` from most distant to closest (not including the ancestor found by `predicate`, * but otherwise including `descendant`). - * @param predicate - a function which will be evaluated on every ancestor of `descendant` until it returns true. + * @param predicate - a function which will be evaluated on `descendant` and then ancestor of `descendant` (in ascending order) until it returns true. * @returns the closest ancestor of `descendant` that satisfies `predicate`, or `undefined` if no such ancestor exists. * * @example diff --git a/packages/dds/tree/src/shared-tree-core/branch.ts b/packages/dds/tree/src/shared-tree-core/branch.ts index ebe58dbee025..a3ee5292fcd0 100644 --- a/packages/dds/tree/src/shared-tree-core/branch.ts +++ b/packages/dds/tree/src/shared-tree-core/branch.ts @@ -25,7 +25,7 @@ import type { Listenable } from "@fluidframework/core-interfaces"; import { createEmitter } from "@fluid-internal/client-utils"; import { TransactionStack } from "./transactionStack.js"; -import { fail, getLast, hasSome } from "../util/index.js"; +import { getLast, hasSome } from "../util/index.js"; /** * Describes a change to a `SharedTreeBranch`. Various operations can mutate the head of the branch; @@ -319,26 +319,8 @@ export class SharedTreeBranch { return undefined; } - // Squash the changes and make the squash commit the new head of this branch - const squashedChange = this.changeFamily.rebaser.compose(commits); - const revision = this.mintRevisionTag(); - - const newHead = mintCommit(startCommit, { - revision, - change: this.changeFamily.rebaser.changeRevision(squashedChange, revision), - }); - - const changeEvent = { - type: "replace", - change: undefined, - removedCommits: commits, - newCommits: [newHead], - } as const; - - this.#events.emit("beforeChange", changeEvent); - this.head = newHead; - this.#events.emit("afterChange", changeEvent); - return [commits, newHead]; + const squashedCommits = this.squashAfter(startCommit); + return [squashedCommits, this.head]; } /** @@ -352,42 +334,12 @@ export class SharedTreeBranch { abortedCommits: GraphCommit[], ] { this.assertNotDisposed(); - const [startCommit, commits] = this.popTransaction(); + const [startCommit] = this.popTransaction(); this.editor.exitTransaction(); - this.#events.emit("transactionAborted", this.transactions.size === 0); - if (!hasSome(commits)) { - this.#events.emit("transactionRolledBack", this.transactions.size === 0); - return [undefined, []]; - } - - const inverses: TaggedChange[] = []; - for (let i = commits.length - 1; i >= 0; i--) { - const revision = this.mintRevisionTag(); - const commit = - commits[i] ?? fail("This wont run because we are iterating through commits"); - const inverse = this.changeFamily.rebaser.changeRevision( - this.changeFamily.rebaser.invert(commit, true, revision), - revision, - commit.revision, - ); - - inverses.push(tagRollbackInverse(inverse, revision, commit.revision)); - } - const change = - inverses.length > 0 ? this.changeFamily.rebaser.compose(inverses) : undefined; - - const changeEvent = { - type: "remove", - change: change === undefined ? undefined : makeAnonChange(change), - removedCommits: commits, - } as const; - - this.#events.emit("beforeChange", changeEvent); - this.head = startCommit; - this.#events.emit("afterChange", changeEvent); + const [taggedChange, removedCommits] = this.removeAfter(startCommit); this.#events.emit("transactionRolledBack", this.transactions.size === 0); - return [change, commits]; + return [taggedChange?.change, removedCommits]; } /** @@ -493,6 +445,87 @@ export class SharedTreeBranch { return rebaseResult; } + /** + * Remove a range of commits from this branch. + * @param commit - All commits after (but not including) this commit will be removed. + * @returns The net change to this branch and the commits that were removed from this branch. + */ + public removeAfter( + commit: GraphCommit, + ): [change: TaggedChange | undefined, removedCommits: GraphCommit[]] { + if (commit === this.head) { + return [undefined, []]; + } + + const removedCommits: GraphCommit[] = []; + const inverses: TaggedChange[] = []; + findAncestor([this.head, removedCommits], (c) => { + // TODO: Pull this side effect out if/when more diverse ancestry walking helpers are available + if (c !== commit) { + const revision = this.mintRevisionTag(); + const inverse = this.changeFamily.rebaser.changeRevision( + this.changeFamily.rebaser.invert(c, true, revision), + revision, + c.revision, + ); + + inverses.push(tagRollbackInverse(inverse, revision, c.revision)); + return false; + } + + return true; + }); + assert(hasSome(removedCommits), "Commit must be in the branch's ancestry"); + + const change = makeAnonChange(this.changeFamily.rebaser.compose(inverses)); + const changeEvent = { + type: "remove", + change, + removedCommits, + } as const; + + this.#events.emit("beforeChange", changeEvent); + this.head = commit; + this.#events.emit("afterChange", changeEvent); + return [change, removedCommits]; + } + + /** + * Replace a range of commits on this branch with a single commit composed of equivalent changes. + * @param commit - All commits after (but not including) this commit will be squashed. + * @returns The commits that were squashed and removed from this branch. + * @remarks The commits after `commit` will be removed from this branch, and the squash commit will become the new head of this branch. + * The change event emitted by this operation will have a `change` property that is undefined, since no net change occurred. + */ + public squashAfter(commit: GraphCommit): GraphCommit[] { + if (commit === this.head) { + return []; + } + + const removedCommits: GraphCommit[] = []; + findAncestor([this.head, removedCommits], (c) => c === commit); + assert(hasSome(removedCommits), "Commit must be in the branch's ancestry"); + + const squashedChange = this.changeFamily.rebaser.compose(removedCommits); + const revision = this.mintRevisionTag(); + const newHead = mintCommit(commit, { + revision, + change: this.changeFamily.rebaser.changeRevision(squashedChange, revision), + }); + + const changeEvent = { + type: "replace", + change: undefined, + removedCommits, + newCommits: [newHead], + } as const; + + this.#events.emit("beforeChange", changeEvent); + this.head = newHead; + this.#events.emit("afterChange", changeEvent); + return removedCommits; + } + /** * Apply all the divergent changes on the given branch to this branch. * diff --git a/packages/dds/tree/src/test/shared-tree-core/branch.spec.ts b/packages/dds/tree/src/test/shared-tree-core/branch.spec.ts index 1daec4e6240a..d6ab75a58ffa 100644 --- a/packages/dds/tree/src/test/shared-tree-core/branch.spec.ts +++ b/packages/dds/tree/src/test/shared-tree-core/branch.spec.ts @@ -529,6 +529,79 @@ describe("Branches", () => { assertDisposed(() => fork.merge(branch)); }); + it("can remove commits", () => { + const branch = create(); + const originalHead = branch.getHead(); + const tag1 = change(branch); + const tag2 = change(branch); + assertHistory(branch, tag1, tag2); + branch.removeAfter(originalHead); + assert.equal(branch.getHead(), originalHead); + }); + + it("emit correct change events after a remove", () => { + let removeEventCount = 0; + const branch = create(({ type }) => { + if (type === "remove") { + removeEventCount += 1; + } + }); + const originalHead = branch.getHead(); + change(branch); + change(branch); + assert.equal(removeEventCount, 0); + branch.removeAfter(originalHead); + assert.equal(removeEventCount, 2); + }); + + it("can squash commits", () => { + const branch = create(); + const originalHead = branch.getHead(); + const tag1 = change(branch); + const tag2 = change(branch); + assertHistory(branch, tag1, tag2); + branch.squashAfter(originalHead); + assert.equal(branch.getHead().parent?.revision, originalHead.revision); + }); + + it("emit correct change events during and after squashing", () => { + let replaceEventCount = 0; + const branch = create(({ type }) => { + if (type === "replace") { + replaceEventCount += 1; + } + }); + const originalHead = branch.getHead(); + change(branch); + change(branch); + assert.equal(replaceEventCount, 0); + branch.squashAfter(originalHead); + assert.equal(replaceEventCount, 2); + }); + + it("do not emit a change event after squashing no commits", () => { + let changeEventCount = 0; + const branch = create(() => { + changeEventCount += 1; + }); + branch.squashAfter(branch.getHead()); + assert.equal(changeEventCount, 0); + }); + + it("emit a change event after squashing only a single commit", () => { + // TODO#25379: It might be nice to _not_ emit an event in this case (and not actually replace the head) + // as an optimization, but code affecting op submission and transactions relies on the current behavior for now. + let changeEventCount = 0; + const branch = create(() => { + changeEventCount += 1; + }); + const originalHead = branch.getHead(); + change(branch); + changeEventCount = 0; + branch.squashAfter(originalHead); + assert.equal(changeEventCount, 2); + }); + it("correctly report whether they are in the middle of a transaction", () => { // Create a branch and test `isTransacting()` during two transactions, one nested within the other const branch = create(); From c025b53f3b007117c0338f3badb34d14b6175f92 Mon Sep 17 00:00:00 2001 From: Pranshu <9313077+pk-pranshu@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:59:19 -0800 Subject: [PATCH 20/40] =?UTF-8?q?banner=20to=20call=20out=20deprecated=20S?= =?UTF-8?q?ingalManager=20and=20point=20to=20new=20presence=E2=80=A6=20(#2?= =?UTF-8?q?3194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description A banner to announce that Signaler and SignalManager are now deprecated. Pointing users to the new Presence page. --------- Co-authored-by: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> --- docs/docs/concepts/signals.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/docs/concepts/signals.mdx b/docs/docs/concepts/signals.mdx index 80fb455c9d42..d7446bd3e8a1 100644 --- a/docs/docs/concepts/signals.mdx +++ b/docs/docs/concepts/signals.mdx @@ -3,6 +3,16 @@ title: Signals and Signaler sidebar_position: 6 --- +:::warning + +Signaler/SignalManager are now deprecated and no longer supported by Fluid team. + +Please checkout our new Presence offering [here](../build/presence). + +**We highly recommend moving to the new Presence APIs** + +::: + When using DDSes, data is sequenced and stored within the Fluid container to achieve synchronized shared state. For scenarios that involve shared persisted data, DDSes provide an effective way to communicate data so that it is retained in the container. However, there could be many scenarios where we need to communicate data that is short-lived, in which the ordering and storage of said information would be wasteful and unnecessary. For instance, displaying the currently selected object of each user is an example of short-lived information in which the past data is mostly irrelevant. Signals provide an appropriate channel for transmitting transient data, since the information that is communicated via signals is not retained in the container. Signals are not guaranteed to be ordered on delivery relative to other signals and ops, but they still provide a useful communication channel for impermanent and short-lived data. From 10388f38d4cee0de2ca7a8eb7105f4abb7209435 Mon Sep 17 00:00:00 2001 From: Kian Thompson <102998837+kian-thompson@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:32:09 -0800 Subject: [PATCH 21/40] Skip flaky SharedString e2e test for ODSP and r11s (#23187) This test is observed to be somewhat flaky in the e2e pipeline. There isn't much to be done for this test as it's testing simple behavior, and the flakiness is most likely due to server performance at the time of the pipeline run. Since the test is simply verifying end-to-end flow (loader -> runtime -> DDS) for a specific feature, we can skip it for the specifically flaky drivers. [AB#12905](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/12905) [AB#13656](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/13656) Data point side note when running the test 1000 times for specific drivers: - ODSP 0/1000 failed - FRS 2/1000 failed --- .../src/test/sharedStringEndToEndTests.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/test/test-end-to-end-tests/src/test/sharedStringEndToEndTests.spec.ts b/packages/test/test-end-to-end-tests/src/test/sharedStringEndToEndTests.spec.ts index 613b1d7f4a55..31e85adaf53c 100644 --- a/packages/test/test-end-to-end-tests/src/test/sharedStringEndToEndTests.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/sharedStringEndToEndTests.spec.ts @@ -188,7 +188,11 @@ describeCompat("SharedString grouped batching", "NoCompat", (getTestObjectProvid await provider.ensureSynchronized(); }); - it("can load summarized grouped batch", async () => { + it("can load summarized grouped batch", async function () { + // We've seen flakiness in ODSP and r11s. This test is verifying SharedString logic regardless of what service handles the ops/summary. + if (!["local", "tinylicious", "t9s"].includes(provider.driver.type)) { + this.skip(); + } const container1 = await provider.makeTestContainer(groupedBatchingContainerConfig); const dataObject1 = (await container1.getEntryPoint()) as ITestFluidObject; const sharedString1 = await dataObject1.getSharedObject(stringId); From eac1018f1f11a26dc106f9168ced48cd88982364 Mon Sep 17 00:00:00 2001 From: Mark Fields Date: Fri, 22 Nov 2024 16:46:24 -0800 Subject: [PATCH 22/40] feat(odsp-driver): Add logging property if doc is in dogfood or MSIT server environments (#23186) Sometimes we are investigating a live site incident with our 1P customers, and it would be helpful to know which service environment (SharePoint "Farm") the document belongs to, since different settings or code can be deployed to these independently. We can get a reasonable proxy of this by inspecting the domain of the URL against some known patterns. We are computing/logging this for trees-latest and create-new events - should typically be once per session. --- .../odsp-driver/src/createFile/createFile.ts | 10 ++- .../src/createFile/createNewUtils.ts | 4 +- .../drivers/odsp-driver/src/fetchSnapshot.ts | 5 ++ .../drivers/odsp-driver/src/odspUrlHelper.ts | 18 +++++ .../src/test/odspUrlHelper.spec.ts | 72 ++++++++++++++++++- 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/packages/drivers/odsp-driver/src/createFile/createFile.ts b/packages/drivers/odsp-driver/src/createFile/createFile.ts index f977500a4953..b89c5fc1385e 100644 --- a/packages/drivers/odsp-driver/src/createFile/createFile.ts +++ b/packages/drivers/odsp-driver/src/createFile/createFile.ts @@ -27,7 +27,7 @@ import { createOdspUrl } from "./../createOdspUrl.js"; import { EpochTracker } from "./../epochTracker.js"; import { getHeadersWithAuth } from "./../getUrlAndHeadersWithAuth.js"; import { OdspDriverUrlResolver } from "./../odspDriverUrlResolver.js"; -import { getApiRoot } from "./../odspUrlHelper.js"; +import { checkForKnownServerFarmType, getApiRoot } from "./../odspUrlHelper.js"; import { INewFileInfo, buildOdspShareLinkReqParams, @@ -202,10 +202,16 @@ export async function createNewEmptyFluidFile( { ...options, request: { url, method } }, "CreateNewFile", ); + const internalFarmType = checkForKnownServerFarmType(newFileInfo.siteUrl); return PerformanceEvent.timedExecAsync( logger, - { eventName: "createNewEmptyFile" }, + { + eventName: "createNewEmptyFile", + details: { + internalFarmType, + }, + }, async (event) => { const headers = getHeadersWithAuth(authHeader); headers["Content-Type"] = "application/json"; diff --git a/packages/drivers/odsp-driver/src/createFile/createNewUtils.ts b/packages/drivers/odsp-driver/src/createFile/createNewUtils.ts index 62f34971fbab..6c153cc4a57a 100644 --- a/packages/drivers/odsp-driver/src/createFile/createNewUtils.ts +++ b/packages/drivers/odsp-driver/src/createFile/createNewUtils.ts @@ -32,6 +32,7 @@ import { } from "./../contracts.js"; import { EpochTracker, FetchType } from "./../epochTracker.js"; import { getHeadersWithAuth } from "./../getUrlAndHeadersWithAuth.js"; +import { checkForKnownServerFarmType } from "./../odspUrlHelper.js"; import { getWithRetryForTokenRefresh, maxUmpPostBodySize } from "./../odspUtils.js"; import { runWithRetry } from "./../retryUtils.js"; @@ -222,11 +223,12 @@ export async function createNewFluidContainerCore(args: { fetchType, validateResponseCallback, } = args; + const internalFarmType = checkForKnownServerFarmType(initialUrl); return getWithRetryForTokenRefresh(async (options) => { return PerformanceEvent.timedExecAsync( logger, - { eventName: telemetryName }, + { eventName: telemetryName, details: { internalFarmType } }, async (event) => { const snapshotBody = JSON.stringify(containerSnapshot); let url: string; diff --git a/packages/drivers/odsp-driver/src/fetchSnapshot.ts b/packages/drivers/odsp-driver/src/fetchSnapshot.ts index 963477ac02b2..c1de311e75ec 100644 --- a/packages/drivers/odsp-driver/src/fetchSnapshot.ts +++ b/packages/drivers/odsp-driver/src/fetchSnapshot.ts @@ -47,6 +47,7 @@ import { EpochTracker } from "./epochTracker.js"; import { getQueryString } from "./getQueryString.js"; import { getHeadersWithAuth } from "./getUrlAndHeadersWithAuth.js"; import { convertOdspSnapshotToSnapshotTreeAndBlobs } from "./odspSnapshotParser.js"; +import { checkForKnownServerFarmType } from "./odspUrlHelper.js"; import { IOdspResponse, fetchAndParseAsJSONHelper, @@ -297,6 +298,7 @@ async function fetchLatestSnapshotCore( return getWithRetryForTokenRefresh(async (tokenFetchOptions) => { const fetchSnapshotForLoadingGroup = isSnapshotFetchForLoadingGroup(loadingGroupIds); const eventName = fetchSnapshotForLoadingGroup ? "TreesLatestForGroup" : "TreesLatest"; + const internalFarmType = checkForKnownServerFarmType(odspResolvedUrl.siteUrl); const perfEvent = { eventName, @@ -304,6 +306,9 @@ async function fetchLatestSnapshotCore( shareLinkPresent: odspResolvedUrl.shareLinkInfo?.sharingLinkToRedeem !== undefined, isSummarizer: odspResolvedUrl.summarizer, redeemFallbackEnabled: enableRedeemFallback, + details: { + internalFarmType, + }, }; if (snapshotOptions !== undefined) { for (const [key, value] of Object.entries(snapshotOptions)) { diff --git a/packages/drivers/odsp-driver/src/odspUrlHelper.ts b/packages/drivers/odsp-driver/src/odspUrlHelper.ts index 4ed1fab50af7..2d651362987b 100644 --- a/packages/drivers/odsp-driver/src/odspUrlHelper.ts +++ b/packages/drivers/odsp-driver/src/odspUrlHelper.ts @@ -126,3 +126,21 @@ export async function getOdspUrlParts(url: URL): Promise { describe("hasOdcOrigin", () => { @@ -176,4 +181,69 @@ describe("odspUrlHelper", () => { ); }); }); + + describe("checkForKnownServerFarmType", () => { + it("SPDF", () => { + assert.equal( + checkForKnownServerFarmType("https://microsoft.sharepoint-df.com/path?query=string"), + "SPDF", + ); + // Actual create-new-file URL (with IDs scrubbed) + assert.equal( + checkForKnownServerFarmType( + "https://microsoft.sharepoint-df.com/_api/v2.1/drives/b!ci..bo/items/root:%2FLoopAppData/Untitled.loop:/opStream/snapshots/snapshot?ump=1", + ), + "SPDF", + ); + // Actual trees-latest URL (with IDs scrubbed) + assert.equal( + checkForKnownServerFarmType( + "https://microsoft-my.sharepoint-df.com/_api/v2.1/drives/b!AG..wb/items/01..NV/opStream/snapshots/trees/latest?ump=1", + ), + "SPDF", + ); + assert.equal( + checkForKnownServerFarmType("https://foo.sharepoint-df.com/path?query=string"), + "SPDF", + ); + assert.equal( + checkForKnownServerFarmType("https://sharepoint-df.com/path?query=string"), + undefined, + ); + assert.equal( + checkForKnownServerFarmType("https://foo.not-sharepoint-df.com/path?query=string"), + undefined, + ); + }); + it("MSIT", () => { + assert.equal( + checkForKnownServerFarmType("https://microsoft.sharepoint.com/path?query=string"), + "MSIT", + ); + assert.equal( + checkForKnownServerFarmType("https://microsoft-my.sharepoint.com/path?query=string"), + "MSIT", + ); + assert.equal( + checkForKnownServerFarmType( + "https://microsoft-my.not.sharepoint.com/path?query=string", + ), + undefined, + ); + assert.equal( + checkForKnownServerFarmType("https://microsoft.foo.com/path?query=string"), + undefined, + ); + }); + it("other", () => { + assert.equal( + checkForKnownServerFarmType("https://foo.com/path?query=string"), + undefined, + ); + assert.throws( + () => checkForKnownServerFarmType("NOT A URL"), + "expected it to throw with invalid input", + ); + }); + }); }); From 12051a0cfd3cda62df51672d9ff189b9f763ff1c Mon Sep 17 00:00:00 2001 From: Alex Villarreal <716334+alexvy86@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:43:06 -0600 Subject: [PATCH 23/40] refactor: Use assert.equal() to see values when test fails (#23172) ## Description The `Includes ack'd ids in summary` test sometimes fails against routerlicious but the error output only says `false !== true` so we can't tell the actual value of the number that is being compared to 1. This PR updates a few asserts in that file so they'll give us the actual value instead of just `expected true, got false` in the output when the test fails. --- .../src/test/idCompressor.spec.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/test/test-end-to-end-tests/src/test/idCompressor.spec.ts b/packages/test/test-end-to-end-tests/src/test/idCompressor.spec.ts index 89255a38073c..601afb693476 100644 --- a/packages/test/test-end-to-end-tests/src/test/idCompressor.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/idCompressor.spec.ts @@ -889,12 +889,14 @@ describeCompat("IdCompressor Summaries", "NoCompat", (getTestObjectProvider, com const { summaryTree } = await summarizeNow(summarizer); const summaryStats = getCompressorSummaryStats(summaryTree); - assert( - summaryStats.sessionCount === 1, + assert.equal( + summaryStats.sessionCount, + 1, "Should have a local session as all ids are ack'd", ); - assert( - summaryStats.clusterCount === 1, + assert.equal( + summaryStats.clusterCount, + 1, "Should have a local cluster as all ids are ack'd", ); }); @@ -921,12 +923,14 @@ describeCompat("IdCompressor Summaries", "NoCompat", (getTestObjectProvider, com const { summaryTree } = await summarizeNow(summarizer1); const summaryStats = getCompressorSummaryStats(summaryTree); - assert( - summaryStats.sessionCount === 1, + assert.equal( + summaryStats.sessionCount, + 1, "Should have a local session as all ids are ack'd", ); - assert( - summaryStats.clusterCount === 1, + assert.equal( + summaryStats.clusterCount, + 1, "Should have a local cluster as all ids are ack'd", ); @@ -1011,8 +1015,9 @@ describeCompat("IdCompressor Summaries", "NoCompat", (getTestObjectProvider, com ); // Test assumption - assert( - entryPoint2._runtime.attachState === AttachState.Detached, + assert.equal( + entryPoint2._runtime.attachState, + AttachState.Detached, "data store is detached", ); @@ -1028,12 +1033,9 @@ describeCompat("IdCompressor Summaries", "NoCompat", (getTestObjectProvider, com // attached data store. await ds2.trySetAlias("foo"); - assert( - // For some reason TSC gets it wrong - it assumes that attachState is constant and that assert above - // established it's AttachState.Detached, so this comparison is useless. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - entryPoint2._runtime.attachState === AttachState.Attached, + assert.equal( + entryPoint2._runtime.attachState, + AttachState.Attached, "data store is detached", ); From dbaca6e6789f3b68e99f61a1b4ba50fbeb2cf7c4 Mon Sep 17 00:00:00 2001 From: Alex Villarreal <716334+alexvy86@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:27:21 -0600 Subject: [PATCH 24/40] fix(examples): Fix tests in external-data example so they don't fail in Node20 (#23189) ## Description Fixes the tests of the external-data example so they succeed in Node20. Also simplifies that package a bit by removing an unnecessary split of the webpack config and removing the unused `stream-http` dependency. I also tried to simplify further by replacing node-fetch with Node's native fetch but that led to different test failures. Not urgent so I left it alone. --- examples/external-data/package.json | 4 +- .../tests/customerService.test.ts | 8 ++ examples/external-data/webpack.config.cjs | 84 ++++++++----------- examples/external-data/webpack.dev.cjs | 9 -- examples/external-data/webpack.prod.cjs | 9 -- pnpm-lock.yaml | 19 ----- 6 files changed, 45 insertions(+), 88 deletions(-) delete mode 100644 examples/external-data/webpack.dev.cjs delete mode 100644 examples/external-data/webpack.prod.cjs diff --git a/examples/external-data/package.json b/examples/external-data/package.json index 5581b2536951..4327614870a9 100644 --- a/examples/external-data/package.json +++ b/examples/external-data/package.json @@ -112,7 +112,6 @@ "puppeteer": "^23.6.0", "rimraf": "^4.4.0", "start-server-and-test": "^2.0.3", - "stream-http": "^3.2.0", "supertest": "^3.4.2", "tinylicious": "^5.0.0", "ts-jest": "^29.1.1", @@ -120,8 +119,7 @@ "typescript": "~5.4.5", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", - "webpack-dev-server": "~4.15.2", - "webpack-merge": "^6.0.1" + "webpack-dev-server": "~4.15.2" }, "fluid": { "browser": { diff --git a/examples/external-data/tests/customerService.test.ts b/examples/external-data/tests/customerService.test.ts index 24676896628b..e3785a233827 100644 --- a/examples/external-data/tests/customerService.test.ts +++ b/examples/external-data/tests/customerService.test.ts @@ -154,6 +154,14 @@ describe("mock-customer-service", () => { await closeServer(_externalDataService); await closeServer(_customerService); + + // Something about shutting down the servers after each test and then starting new ones on the same ports before + // running the next test is causing issues where the second test to run gets an "other side closed" message when + // it tries to issue its first request to the services. This does not happen on Node18 but does on Node20. + // I couldn't figure out why, but letting the JS turn end here before the test runs seems to fix it. + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); }); // We have omitted `@types/supertest` due to cross-package build issue. diff --git a/examples/external-data/webpack.config.cjs b/examples/external-data/webpack.config.cjs index e43c35ccc4ea..0dd2447512c3 100644 --- a/examples/external-data/webpack.config.cjs +++ b/examples/external-data/webpack.config.cjs @@ -4,56 +4,44 @@ */ const path = require("path"); -const { merge } = require("webpack-merge"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const webpack = require("webpack"); -module.exports = (env) => { - const isProduction = env?.production; - - return merge( - { - entry: { - start: "./src/start.ts", - }, - resolve: { - extensionAlias: { - ".js": [".ts", ".tsx", ".js", ".cjs", ".mjs"], - }, - extensions: [".ts", ".tsx", ".js", ".cjs", ".mjs"], - fallback: { - http: require.resolve("stream-http"), - fs: false, - path: false, - stream: false, - }, - }, - module: { - rules: [ - { - test: /\.tsx?$/, - loader: "ts-loader", - }, - ], - }, - output: { - filename: "[name].bundle.js", - path: path.resolve(__dirname, "dist"), - library: "[name]", - // https://github.com/webpack/webpack/issues/5767 - // https://github.com/webpack/webpack/issues/7939 - devtoolNamespace: "fluid-example/app-integration-external-data", - libraryTarget: "umd", - }, - plugins: [ - new webpack.ProvidePlugin({ - process: "process/browser.js", - }), - new HtmlWebpackPlugin({ - template: "./src/index.html", - }), - ], +module.exports = { + entry: { + start: "./src/start.ts", + }, + resolve: { + extensionAlias: { + ".js": [".ts", ".tsx", ".js", ".cjs", ".mjs"], }, - isProduction ? require("./webpack.prod.cjs") : require("./webpack.dev.cjs"), - ); + extensions: [".ts", ".tsx", ".js", ".cjs", ".mjs"], + }, + module: { + rules: [ + { + test: /\.tsx?$/, + loader: "ts-loader", + }, + ], + }, + output: { + filename: "[name].bundle.js", + path: path.resolve(__dirname, "dist"), + library: "[name]", + // https://github.com/webpack/webpack/issues/5767 + // https://github.com/webpack/webpack/issues/7939 + devtoolNamespace: "fluid-example/app-integration-external-data", + libraryTarget: "umd", + }, + plugins: [ + new webpack.ProvidePlugin({ + process: "process/browser.js", + }), + new HtmlWebpackPlugin({ + template: "./src/index.html", + }), + ], + mode: "development", + devtool: "inline-source-map", }; diff --git a/examples/external-data/webpack.dev.cjs b/examples/external-data/webpack.dev.cjs deleted file mode 100644 index c5b13a83bad8..000000000000 --- a/examples/external-data/webpack.dev.cjs +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -module.exports = { - mode: "development", - devtool: "inline-source-map", -}; diff --git a/examples/external-data/webpack.prod.cjs b/examples/external-data/webpack.prod.cjs deleted file mode 100644 index 2273d173d964..000000000000 --- a/examples/external-data/webpack.prod.cjs +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -module.exports = { - mode: "production", - devtool: "source-map", -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8cd489ca9d35..828dd251d7be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4211,9 +4211,6 @@ importers: start-server-and-test: specifier: ^2.0.3 version: 2.0.8 - stream-http: - specifier: ^3.2.0 - version: 3.2.0 supertest: specifier: ^3.4.2 version: 3.4.2 @@ -4238,9 +4235,6 @@ importers: webpack-dev-server: specifier: ~4.15.2 version: 4.15.2(webpack-cli@5.1.4)(webpack@5.95.0) - webpack-merge: - specifier: ^6.0.1 - version: 6.0.1 examples/service-clients/azure-client/external-controller: dependencies: @@ -26320,10 +26314,6 @@ packages: engines: {node: '>=6'} dev: true - /builtin-status-codes@3.0.0: - resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} - dev: true - /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -37586,15 +37576,6 @@ packages: dependencies: duplexer: 0.1.2 - /stream-http@3.2.0: - resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} - dependencies: - builtin-status-codes: 3.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - xtend: 4.0.2 - dev: true - /stream-to-pull-stream@1.7.3: resolution: {integrity: sha512-6sNyqJpr5dIOQdgNy/xcDWwDuzAsAwVzhzrWlAPAQ7Lkjx/rv0wgvxEyKwTq6FmNd5rjTrELt/CLmaSw7crMGg==} dependencies: From fcacb6460213bb5e03f7fc81eafef8d94ae11b1a Mon Sep 17 00:00:00 2001 From: MarioJGMsoft Date: Mon, 25 Nov 2024 10:23:27 -0800 Subject: [PATCH 25/40] Remove WRITE usages of compatDetails on ContainerRuntime GC ops (#22795) ## Description Fixes: [AB#19718](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/19718) Follow-on to [#17024](https://dev.azure.com/fluidframework/internal/_workitems/edit/17024/) We need to look at Kusto first to confirm our theory that the code to read both the GC op types ("Sweep" and "TombstoneLoaded") has sufficiently saturated. "Sweep" was [added](https://github.com/microsoft/FluidFramework/commit/b3b6cd4e09659521e11cf57d5663c48d2ce7e9c1) in 2.0.0-internal.7.4.0 "TombstoneLoaded" was [added](https://github.com/microsoft/FluidFramework/commit/dd9ff1665bfb5f10ff59e02ce4caa2b674f4de47) in 2.0.0-rc.2.0.0 We do see sessions on RC.1, but I believe they're all Whiteboard, which doesn't see any "TombstoneLoaded" events which trigger those ops. We can write a query to confirm with high confidence, then proceed. ## Reviewer Guidance The goal of this PR is to remove all uses of compatDetails that are no longer necessary, please let me know if I missed any or if there are some that can't be removed yet. --- .../container-runtime/src/containerRuntime.ts | 1 - .../src/gc/garbageCollection.ts | 7 -- .../runtime/container-runtime/src/index.ts | 3 - .../container-runtime/src/messageTypes.ts | 53 +------------ .../src/opLifecycle/remoteMessageProcessor.ts | 7 +- .../src/pendingStateManager.ts | 5 +- .../src/test/containerRuntime.spec.ts | 77 ++----------------- .../src/test/gc/garbageCollection.spec.ts | 65 +++------------- .../src/test/pendingStateManager.spec.ts | 45 +---------- .../src/test/batching.spec.ts | 53 ------------- 10 files changed, 24 insertions(+), 292 deletions(-) diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index 97f771a9005e..d51252a6bcb6 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -4613,7 +4613,6 @@ export class ContainerRuntime this.channelCollection.rollback(type, contents, localOpMetadata); break; default: - // Don't check message.compatDetails because this is for rolling back a local op so the type will be known throw new Error(`Can't rollback ${type}`); } } diff --git a/packages/runtime/container-runtime/src/gc/garbageCollection.ts b/packages/runtime/container-runtime/src/gc/garbageCollection.ts index 931427d56b1b..ef5da9a762a6 100644 --- a/packages/runtime/container-runtime/src/gc/garbageCollection.ts +++ b/packages/runtime/container-runtime/src/gc/garbageCollection.ts @@ -745,13 +745,9 @@ export class GarbageCollector implements IGarbageCollector { deletedNodeIds: sweepReadyDSAndBlobs, }; - // Its fine for older clients to ignore this op because it doesn't have any functional impact. This op - // is an optimization to ensure that all clients are in sync when it comes to deleted nodes to prevent their - // accidental usage. The clients will sync without the delete op too but it may take longer. const containerGCMessage: ContainerRuntimeGCMessage = { type: ContainerMessageType.GC, contents, - compatDetails: { behavior: "Ignore" }, // DEPRECATED: For temporary back compat only }; this.submitMessage(containerGCMessage); return; @@ -1073,15 +1069,12 @@ export class GarbageCollector implements IGarbageCollector { return; } - // Use compat behavior "Ignore" since this is an optimization to opportunistically protect - // objects from deletion, so it's fine for older clients to ignore this op. const containerGCMessage: ContainerRuntimeGCMessage = { type: ContainerMessageType.GC, contents: { type: GarbageCollectionMessageType.TombstoneLoaded, nodePath, }, - compatDetails: { behavior: "Ignore" }, // DEPRECATED: For temporary back compat only }; this.submitMessage(containerGCMessage); } diff --git a/packages/runtime/container-runtime/src/index.ts b/packages/runtime/container-runtime/src/index.ts index fb7123b7ca16..d33facd99561 100644 --- a/packages/runtime/container-runtime/src/index.ts +++ b/packages/runtime/container-runtime/src/index.ts @@ -26,9 +26,6 @@ export { } from "./containerRuntime.js"; export { ContainerMessageType, - IContainerRuntimeMessageCompatDetails, - CompatModeBehavior, - RecentlyAddedContainerRuntimeMessageDetails, UnknownContainerRuntimeMessage, } from "./messageTypes.js"; export { IBlobManagerLoadInfo } from "./blobManager/index.js"; diff --git a/packages/runtime/container-runtime/src/messageTypes.ts b/packages/runtime/container-runtime/src/messageTypes.ts index b8bbfac2fb23..19704e238162 100644 --- a/packages/runtime/container-runtime/src/messageTypes.ts +++ b/packages/runtime/container-runtime/src/messageTypes.ts @@ -58,29 +58,6 @@ export enum ContainerMessageType { GC = "GC", } -/** - * How should an older client handle an unrecognized remote op type? - * - * @deprecated The utility of a mechanism to handle unknown messages is outweighed by the nuance required to get it right. - * @internal - */ -export type CompatModeBehavior = - /** Ignore the op. It won't be persisted if this client summarizes */ - | "Ignore" - /** Fail processing immediately. (The container will close) */ - | "FailToProcess"; - -/** - * All the info an older client would need to know how to handle an unrecognized remote op type - * - * @deprecated The utility of a mechanism to handle unknown messages is outweighed by the nuance required to get it right. - * @internal - */ -export interface IContainerRuntimeMessageCompatDetails { - /** How should an older client handle an unrecognized remote op type? */ - behavior: CompatModeBehavior; -} - /** * The unpacked runtime message / details to be handled or dispatched by the ContainerRuntime. * Message type are differentiated via a `type` string and contain different contents depending on their type. @@ -88,27 +65,11 @@ export interface IContainerRuntimeMessageCompatDetails { * IMPORTANT: when creating one to be serialized, set the properties in the order they appear here. * This way stringified values can be compared. */ -type TypedContainerRuntimeMessage< - TType extends ContainerMessageType, - TContents, - TUSedCompatDetails extends boolean = false, -> = { +interface TypedContainerRuntimeMessage { /** Type of the op, within the ContainerRuntime's domain */ type: TType; /** Domain-specific contents, interpreted according to the type */ contents: TContents; -} & (TUSedCompatDetails extends true - ? Partial - : { compatDetails?: undefined }); - -/** - * Additional details expected for any recently added message. - * @deprecated The utility of a mechanism to handle unknown messages is outweighed by the nuance required to get it right. - * @internal - */ -export interface RecentlyAddedContainerRuntimeMessageDetails { - /** Info describing how to handle this op in case the type is unrecognized (default: fail to process) */ - compatDetails: IContainerRuntimeMessageCompatDetails; } export type ContainerRuntimeDataStoreOpMessage = TypedContainerRuntimeMessage< @@ -145,8 +106,7 @@ export type ContainerRuntimeIdAllocationMessage = TypedContainerRuntimeMessage< >; export type ContainerRuntimeGCMessage = TypedContainerRuntimeMessage< ContainerMessageType.GC, - GarbageCollectionMessage, - true // TUsedCompatDetails + GarbageCollectionMessage >; export type ContainerRuntimeDocumentSchemaMessage = TypedContainerRuntimeMessage< ContainerMessageType.DocumentSchemaChange, @@ -157,8 +117,7 @@ export type ContainerRuntimeDocumentSchemaMessage = TypedContainerRuntimeMessage * Represents an unrecognized TypedContainerRuntimeMessage, e.g. a message from a future version of the container runtime. * @internal */ -export interface UnknownContainerRuntimeMessage - extends Partial { +export interface UnknownContainerRuntimeMessage { /** Invalid type of the op, within the ContainerRuntime's domain. This value should never exist at runtime. * This is useful for type narrowing but should never be used as an actual message type at runtime. * Actual value will not be "__unknown...", but the type `Exclude` is not supported. @@ -222,9 +181,3 @@ export type InboundSequencedContainerRuntimeMessage = Omit< "type" | "contents" > & InboundContainerRuntimeMessage; - -/** A [loose] InboundSequencedContainerRuntimeMessage that is recent and may contain compat details. - * It exists solely to to provide access to those details. - */ -export type InboundSequencedRecentlyAddedContainerRuntimeMessage = ISequencedDocumentMessage & - Partial; diff --git a/packages/runtime/container-runtime/src/opLifecycle/remoteMessageProcessor.ts b/packages/runtime/container-runtime/src/opLifecycle/remoteMessageProcessor.ts index 210dc689b3b9..e963c2ef1f20 100644 --- a/packages/runtime/container-runtime/src/opLifecycle/remoteMessageProcessor.ts +++ b/packages/runtime/container-runtime/src/opLifecycle/remoteMessageProcessor.ts @@ -13,7 +13,6 @@ import { ContainerMessageType, type InboundContainerRuntimeMessage, type InboundSequencedContainerRuntimeMessage, - type InboundSequencedRecentlyAddedContainerRuntimeMessage, } from "../messageTypes.js"; import { asBatchMetadata } from "../metadata.js"; @@ -259,7 +258,7 @@ export function ensureContentsDeserialized(mutableMessage: ISequencedDocumentMes * * The return type illustrates the assumption that the message param * becomes a InboundSequencedContainerRuntimeMessage by the time the function returns - * (but there is no runtime validation of the 'type' or 'compatDetails' values). + * (but there is no runtime validation of the 'type'). */ function unpack(message: ISequencedDocumentMessage): InboundSequencedContainerRuntimeMessage { // We assume the contents is an InboundContainerRuntimeMessage (the message is "packed") @@ -270,10 +269,6 @@ function unpack(message: ISequencedDocumentMessage): InboundSequencedContainerRu messageUnpacked.type = contents.type; messageUnpacked.contents = contents.contents; - if ("compatDetails" in contents) { - (messageUnpacked as InboundSequencedRecentlyAddedContainerRuntimeMessage).compatDetails = - contents.compatDetails; - } return messageUnpacked; } diff --git a/packages/runtime/container-runtime/src/pendingStateManager.ts b/packages/runtime/container-runtime/src/pendingStateManager.ts index 4d5e60b1c399..1d9b54d50b7e 100644 --- a/packages/runtime/container-runtime/src/pendingStateManager.ts +++ b/packages/runtime/container-runtime/src/pendingStateManager.ts @@ -105,9 +105,9 @@ function isEmptyBatchPendingMessage(message: IPendingMessageFromStash): boolean function buildPendingMessageContent(message: InboundSequencedContainerRuntimeMessage): string { // IMPORTANT: Order matters here, this must match the order of the properties used // when submitting the message. - const { type, contents, compatDetails }: InboundContainerRuntimeMessage = message; + const { type, contents }: InboundContainerRuntimeMessage = message; // Any properties that are not defined, won't be emitted by stringify. - return JSON.stringify({ type, contents, compatDetails }); + return JSON.stringify({ type, contents }); } function typesOfKeys(obj: T): Record { @@ -126,7 +126,6 @@ function scrubAndStringify( // For these known/expected keys, we can either drill in (for contents) // or just use the value as-is (since it's not personal info) scrubbed.contents = message.contents && typesOfKeys(message.contents); - scrubbed.compatDetails = message.compatDetails; scrubbed.type = message.type; return JSON.stringify(scrubbed); diff --git a/packages/runtime/container-runtime/src/test/containerRuntime.spec.ts b/packages/runtime/container-runtime/src/test/containerRuntime.spec.ts index df96ca058cec..85c1abfe7046 100644 --- a/packages/runtime/container-runtime/src/test/containerRuntime.spec.ts +++ b/packages/runtime/container-runtime/src/test/containerRuntime.spec.ts @@ -71,10 +71,8 @@ import { } from "../containerRuntime.js"; import { ContainerMessageType, - type ContainerRuntimeGCMessage, type InboundSequencedContainerRuntimeMessage, type OutboundContainerRuntimeMessage, - type RecentlyAddedContainerRuntimeMessageDetails, type UnknownContainerRuntimeMessage, } from "../messageTypes.js"; import type { BatchMessage, InboundMessageResult } from "../opLifecycle/index.js"; @@ -90,12 +88,6 @@ import { type IRefreshSummaryAckOptions, } from "../summary/index.js"; -// Type test: -const outboundMessage: OutboundContainerRuntimeMessage = - {} as unknown as OutboundContainerRuntimeMessage; -// @ts-expect-error Outbound type should not include compat behavior -(() => {})(outboundMessage.compatDetails); - function submitDataStoreOp( runtime: Pick, id: string, @@ -1129,7 +1121,7 @@ describe("Runtime", () => { ); }); - describe("[DEPRECATED] Future op type compatibility", () => { + describe("Unrecognized types not supported", () => { let containerRuntime: ContainerRuntime; beforeEach(async () => { containerRuntime = await ContainerRuntime.loadRuntime({ @@ -1144,37 +1136,6 @@ describe("Runtime", () => { }); }); - it("can submit op compat behavior (temporarily still available for GC op)", async () => { - // Create a container runtime type where the submit method is public. This makes it easier to test - // submission and processing of ops. The other option is to send data store or alias ops whose - // processing requires creation of data store context and runtime as well. - type ContainerRuntimeWithSubmit = Omit & { - submit( - containerRuntimeMessage: OutboundContainerRuntimeMessage, - localOpMetadata: unknown, - metadata: Record | undefined, - ): void; - }; - const containerRuntimeWithSubmit = - containerRuntime as unknown as ContainerRuntimeWithSubmit; - - const gcMessageWithDeprecatedCompatDetails: ContainerRuntimeGCMessage = { - type: ContainerMessageType.GC, - contents: { type: "Sweep", deletedNodeIds: [] }, - compatDetails: { behavior: "Ignore" }, - }; - - assert.doesNotThrow( - () => - containerRuntimeWithSubmit.submit( - gcMessageWithDeprecatedCompatDetails, - undefined, - undefined, - ), - "Cannot submit container runtime message with compatDetails", - ); - }); - /** Overwrites channelCollection property and exposes private submit function with modified typing */ function patchContainerRuntime(): Omit & { submit: (containerRuntimeMessage: UnknownContainerRuntimeMessage) => void; @@ -1203,7 +1164,7 @@ describe("Runtime", () => { return patched; } - it("Op with unrecognized type and 'Ignore' compat behavior is ignored by resubmit", async () => { + it("Op with unrecognized type is ignored by resubmit", async () => { const patchedContainerRuntime = patchContainerRuntime(); changeConnectionState(patchedContainerRuntime, false, mockClientId); @@ -1213,7 +1174,6 @@ describe("Runtime", () => { patchedContainerRuntime.submit({ type: "FUTURE_TYPE" as any, contents: "3", - compatDetails: { behavior: "Ignore" }, // This op should be ignored by resubmit }); submitDataStoreOp(patchedContainerRuntime, "4", "test"); @@ -1231,12 +1191,10 @@ describe("Runtime", () => { ); }); - it("process remote op with unrecognized type and 'Ignore' compat behavior", async () => { - const futureRuntimeMessage: RecentlyAddedContainerRuntimeMessageDetails & - Record = { + it("process remote op with unrecognized type", async () => { + const futureRuntimeMessage: Record = { type: "FROM_THE_FUTURE", contents: "Hello", - compatDetails: { behavior: "Ignore" }, }; const packedOp: Omit< @@ -1253,35 +1211,10 @@ describe("Runtime", () => { () => containerRuntime.process(packedOp as ISequencedDocumentMessage, false /* local */), (error: IErrorBase) => error.errorType === ContainerErrorTypes.dataProcessingError, - "Ops with unrecognized type and 'Ignore' compat behavior should fail to process", - ); - }); - - it("process remote op with unrecognized type and no compat behavior", async () => { - const futureRuntimeMessage = { - type: "FROM_THE_FUTURE", - contents: "Hello", - }; - - const packedOp: Omit< - ISequencedDocumentMessage, - "term" | "clientSequenceNumber" | "referenceSequenceNumber" | "timestamp" - > = { - contents: JSON.stringify(futureRuntimeMessage), - type: MessageType.Operation, - sequenceNumber: 123, - clientId: "someClientId", - minimumSequenceNumber: 0, - }; - assert.throws( - () => - containerRuntime.process(packedOp as ISequencedDocumentMessage, false /* local */), - (error: IErrorBase) => error.errorType === ContainerErrorTypes.dataProcessingError, - "Ops with unrecognized type and no specified compat behavior should fail to process", + "Ops with unrecognized type should fail to process", ); }); }); - describe("Supports mixin classes", () => { it("new loadRuntime method works", async () => { const makeMixin = ( diff --git a/packages/runtime/container-runtime/src/test/gc/garbageCollection.spec.ts b/packages/runtime/container-runtime/src/test/gc/garbageCollection.spec.ts index c796ac2873fa..7e9fbcc9f7b6 100644 --- a/packages/runtime/container-runtime/src/test/gc/garbageCollection.spec.ts +++ b/packages/runtime/container-runtime/src/test/gc/garbageCollection.spec.ts @@ -526,7 +526,6 @@ describe("Garbage Collection Tests", () => { type: GarbageCollectionMessageType.TombstoneLoaded, nodePath: nodes[0], }, - compatDetails: { behavior: "Ignore" }, } satisfies ContainerRuntimeGCMessage, "submitted message not as expected", ); @@ -2217,61 +2216,21 @@ describe("Garbage Collection Tests", () => { assert.strictEqual(garbageCollector.deletedNodes.size, 0, "Expecting 0 deleted nodes"); }); - describe("Future GC op type compatibility", () => { + it("process remote op with unrecognized type", async () => { + const garbageCollector = createGarbageCollector(); const gcMessageFromFuture: Record = { type: "FUTURE_MESSAGE", hello: "HELLO", }; - - let garbageCollector: IGarbageCollector; - beforeEach(async () => { - garbageCollector = createGarbageCollector({ - createParams: { gcOptions: { enableGCSweep: true } }, - }); - }); - - it("can submit GC op compat behavior", async () => { - const gcWithPrivates = garbageCollector as GcWithPrivates; - const containerRuntimeGCMessage: Omit & { - type: string; - contents: any; - } = { - type: ContainerMessageType.GC, - contents: gcMessageFromFuture, - compatDetails: { behavior: "Ignore" }, - }; - - assert.doesNotThrow( - () => - gcWithPrivates.submitMessage(containerRuntimeGCMessage as ContainerRuntimeGCMessage), - "Cannot submit GC message with compatDetails", - ); - }); - - it("process remote op with unrecognized type and 'Ignore' compat behavior", async () => { - assert.throws( - () => - garbageCollector.processMessages( - [gcMessageFromFuture as unknown as GarbageCollectionMessage], - Date.now(), - false /* local */, - ), - (error: IErrorBase) => error.errorType === ContainerErrorTypes.dataProcessingError, - "Garbage collection message of unknown type FROM_THE_FUTURE", - ); - }); - - it("process remote op with unrecognized type and no compat behavior", async () => { - assert.throws( - () => - garbageCollector.processMessages( - [gcMessageFromFuture as unknown as GarbageCollectionMessage], - Date.now(), - false /* local */, - ), - (error: IErrorBase) => error.errorType === ContainerErrorTypes.dataProcessingError, - "Garbage collection message of unknown type FROM_THE_FUTURE", - ); - }); + assert.throws( + () => + garbageCollector.processMessages( + [gcMessageFromFuture as unknown as GarbageCollectionMessage], + Date.now(), + false /* local */, + ), + (error: IErrorBase) => error.errorType === ContainerErrorTypes.dataProcessingError, + "Garbage collection message of unknown type FUTURE_MESSAGE", + ); }); }); diff --git a/packages/runtime/container-runtime/src/test/pendingStateManager.spec.ts b/packages/runtime/container-runtime/src/test/pendingStateManager.spec.ts index 5615f41a9561..82e2032ae41f 100644 --- a/packages/runtime/container-runtime/src/test/pendingStateManager.spec.ts +++ b/packages/runtime/container-runtime/src/test/pendingStateManager.spec.ts @@ -13,11 +13,7 @@ import { import { MockLogger2, createChildLogger } from "@fluidframework/telemetry-utils/internal"; import Deque from "double-ended-queue"; -import type { - InboundSequencedContainerRuntimeMessage, - RecentlyAddedContainerRuntimeMessageDetails, - UnknownContainerRuntimeMessage, -} from "../messageTypes.js"; +import type { InboundSequencedContainerRuntimeMessage } from "../messageTypes.js"; import { BatchManager, BatchMessage, @@ -604,45 +600,6 @@ describe("Pending State Manager", () => { assert.deepStrictEqual(pendingStateManager.initialMessages.toArray(), messages); }); }); - - describe("Future op compat behavior", () => { - it("pending op roundtrip", async () => { - const pendingStateManager = createPendingStateManager([]); - const futureRuntimeMessage: Pick & - RecentlyAddedContainerRuntimeMessageDetails = { - type: "FROM_THE_FUTURE", - contents: "Hello", - compatDetails: { behavior: "FailToProcess" }, - }; - - pendingStateManager.onFlushBatch( - [ - { - contents: JSON.stringify(futureRuntimeMessage), - referenceSequenceNumber: 0, - }, - ], - 1, - ); - const inboundMessage = futureRuntimeMessage as ISequencedDocumentMessage & - UnknownContainerRuntimeMessage; - pendingStateManager.processInboundMessages( - { - type: "fullBatch", - messages: [inboundMessage], - batchStart: { - batchStartCsn: 1 /* batchStartCsn */, - batchId: undefined, - clientId: "clientId", - keyMessage: inboundMessage, - }, - length: 1, - groupedBatch: false, - }, - true /* local */, - ); - }); - }); }); describe("replayPendingStates", () => { diff --git a/packages/test/test-end-to-end-tests/src/test/batching.spec.ts b/packages/test/test-end-to-end-tests/src/test/batching.spec.ts index 75969e0c8aff..629ee3fd957a 100644 --- a/packages/test/test-end-to-end-tests/src/test/batching.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/batching.spec.ts @@ -157,59 +157,6 @@ describeCompat("Flushing ops", "NoCompat", (getTestObjectProvider, apis) => { await provider.ensureSynchronized(); } - it("[DEPRECATED] can send and receive a batch specifying compatDetails", async () => { - await setupContainers({ - flushMode: FlushMode.TurnBased, - compressionOptions: { - minimumBatchSizeInBytes: 10, - compressionAlgorithm: CompressionAlgorithms.lz4, - }, - enableGroupedBatching: true, - chunkSizeInBytes: 100, - }); - const futureOpSubmitter2 = dataObject2.context.containerRuntime as unknown as { - submit: (containerRuntimeMessage: any) => void; - }; - const dataObject1BatchMessages: ISequencedDocumentMessage[] = []; - const dataObject2BatchMessages: ISequencedDocumentMessage[] = []; - setupBatchMessageListener(dataObject1, dataObject1BatchMessages); - setupBatchMessageListener(dataObject2, dataObject2BatchMessages); - - // Submit two ops: a FluidDataStoreOp and a GC op - dataObject2map1.set("key1", "value1"); - futureOpSubmitter2.submit({ - type: ContainerMessageType.GC, - contents: { - type: "TombstoneLoaded", - nodePath: "/", - }, - compatDetails: { behavior: "Ignore" }, // This op should be ignored when processed - }); - - // Wait for the ops to get flushed and processed. - await provider.ensureSynchronized(); - - // Neither container should have closed - assert.equal(container1.closed, false, "Container1 should not have closed"); - assert.equal(container2.closed, false, "Container2 should not have closed"); - - // Both ops should have reached both containers - assert.deepEqual( - dataObject1BatchMessages - .filter((m) => m.type !== ContainerMessageType.ChunkedOp) // Don't worry about ChunkedOps, not sure how many there will be - .map((m) => m.type), - [ContainerMessageType.FluidDataStoreOp, ContainerMessageType.GC], - "Unexpected op types received (dataObject1)", - ); - assert.deepEqual( - dataObject2BatchMessages - .filter((m) => m.type !== ContainerMessageType.ChunkedOp) // Don't worry about ChunkedOps, not sure how many there will be - .map((m) => m.type), - [ContainerMessageType.FluidDataStoreOp, ContainerMessageType.GC], - "Unexpected op types received (dataObject2)", - ); - }); - it("Can't set up a container with Immediate Mode and Offline Load", async () => { await assert.rejects( setupContainers({ flushMode: FlushMode.Immediate }), From c23b2190d9f2fdb5fcd1371cc63063a9e4ade6fb Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 25 Nov 2024 10:46:50 -0800 Subject: [PATCH 26/40] build(eslint-config-fluid): Bump version to 5.7.0 (#23193) Bumps to the next minor release since 5.6 has been released. --- common/build/eslint-config-fluid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/build/eslint-config-fluid/package.json b/common/build/eslint-config-fluid/package.json index 506b2d58d638..3e71462bfd2a 100644 --- a/common/build/eslint-config-fluid/package.json +++ b/common/build/eslint-config-fluid/package.json @@ -1,6 +1,6 @@ { "name": "@fluidframework/eslint-config-fluid", - "version": "5.6.0", + "version": "5.7.0", "description": "Shareable ESLint config for the Fluid Framework", "homepage": "https://fluidframework.com", "repository": { From 1a4e6bdda995aecd549f88064fb0aac859ef15fd Mon Sep 17 00:00:00 2001 From: Noah Encke <78610362+noencke@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:03:30 -0800 Subject: [PATCH 27/40] Move internal transaction API out of SharedTreeBranch and into TreeCheckout (#23195) ## Description There is no inherent reason why SharedTreeBranches need to know about transactions, and the fact that they do entangles the various layers of our system in a way that isn't necessary. This PR moves the transaction API out of SharedTreeBranch and into TreeCheckout. Checkouts now own all state necessary to run transactions, and branches are mutated strictly in terms of their commits, with no transient state. One upshot of this refactor is that SharedTreeCore also no longer knows about transactions, since it does not know about TreeCheckouts. This required moving some of the logic in SharedTreeCore down into SharedTree. Many SharedTreeBranch unit tests that are no longer relevant have been deleted, and those that remain conceptually relevant have been rewritten against TreeCheckout. --- .../dds/tree/src/shared-tree-core/branch.ts | 177 +-------- .../src/shared-tree-core/sharedTreeCore.ts | 73 +--- .../dds/tree/src/shared-tree/sharedTree.ts | 53 +++ .../dds/tree/src/shared-tree/treeCheckout.ts | 202 ++++++++-- .../src/test/shared-tree-core/branch.spec.ts | 359 +----------------- .../shared-tree-core/sharedTreeCore.spec.ts | 50 +-- .../tree/src/test/shared-tree-core/utils.ts | 42 +- .../src/test/shared-tree/sharedTree.spec.ts | 23 ++ .../src/test/shared-tree/treeCheckout.spec.ts | 31 ++ 9 files changed, 341 insertions(+), 669 deletions(-) diff --git a/packages/dds/tree/src/shared-tree-core/branch.ts b/packages/dds/tree/src/shared-tree-core/branch.ts index a3ee5292fcd0..517607726d70 100644 --- a/packages/dds/tree/src/shared-tree-core/branch.ts +++ b/packages/dds/tree/src/shared-tree-core/branch.ts @@ -24,8 +24,7 @@ import { import type { Listenable } from "@fluidframework/core-interfaces"; import { createEmitter } from "@fluid-internal/client-utils"; -import { TransactionStack } from "./transactionStack.js"; -import { getLast, hasSome } from "../util/index.js"; +import { hasSome } from "../util/index.js"; /** * Describes a change to a `SharedTreeBranch`. Various operations can mutate the head of the branch; @@ -126,34 +125,6 @@ export interface SharedTreeBranchEvents { readonly #events = createEmitter>(); public readonly events: Listenable> = this.#events; public readonly editor: TEditor; - private readonly transactions = new TransactionStack(); - /** - * After pushing a starting revision to the transaction stack, this branch might be rebased - * over commits which are children of that starting revision. When the transaction is committed, - * those rebased-over commits should not be included in the transaction's squash commit, even though - * they exist between the starting revision and the final commit within the transaction. - * - * Whenever `rebaseOnto` is called during a transaction, this map is augmented with an entry from the - * original merge-base to the new merge-base. - * - * This state need only be retained for the lifetime of the transaction. - * - * TODO: This strategy might need to be revisited when adding better support for async transactions. - * Since: - * - * 1. Transactionality is guaranteed primarily by squashing at commit time - * 2. Branches may be rebased with an ongoing transaction - * - * a rebase operation might invalidate only a portion of a transaction's commits, thus defeating the - * purpose of transactionality. - * - * AB#6483 and children items track this work. - */ - private readonly initialTransactionRevToRebasedRev = new Map(); private disposed = false; private readonly unsubscribeBranchTrimmer?: () => void; /** @@ -231,11 +178,12 @@ export class SharedTreeBranch { } /** - * Sets the head of this branch. Emits no change events. + * Sets the head of this branch. + * @remarks This is a "manual override" of sorts, for when the branch needs to be set to a certain state without going through the usual flow of edits. + * This might be necessary as a performance optimization, or to prevent parts of the system updating incorrectly (this method emits no change events!). */ public setHead(head: GraphCommit): void { this.assertNotDisposed(); - assert(!this.isTransacting(), 0x685 /* Cannot set head during a transaction */); this.head = head; } @@ -279,104 +227,6 @@ export class SharedTreeBranch { return this.head; } - /** - * Begin a transaction on this branch. If the transaction is committed via {@link commitTransaction}, - * all commits made since this call will be squashed into a single head commit. - */ - public startTransaction(): void { - this.assertNotDisposed(); - const forks = new Set>(); - const onDisposeUnSubscribes: (() => void)[] = []; - const onForkUnSubscribe = onForkTransitive(this, (fork) => { - forks.add(fork); - onDisposeUnSubscribes.push(fork.events.on("dispose", () => forks.delete(fork))); - }); - this.transactions.push(this.head.revision, () => { - forks.forEach((fork) => fork.dispose()); - onDisposeUnSubscribes.forEach((unsubscribe) => unsubscribe()); - onForkUnSubscribe(); - }); - this.editor.enterTransaction(); - this.#events.emit("transactionStarted", this.transactions.size === 1); - } - - /** - * Commit the current transaction. There must be a transaction in progress that was begun via {@link startTransaction}. - * If there are commits in the current transaction, they will be squashed into a new single head commit. - * @returns the commits that were squashed and the new squash commit if a squash occurred, otherwise `undefined`. - * @remarks If the transaction had no changes applied during its lifetime, then no squash occurs (i.e. this method is a no-op). - * Even if the transaction contained only one change, it will still be replaced with an (equivalent) squash change. - */ - public commitTransaction(): - | [squashedCommits: GraphCommit[], newCommit: GraphCommit] - | undefined { - this.assertNotDisposed(); - const [startCommit, commits] = this.popTransaction(); - this.editor.exitTransaction(); - - this.#events.emit("transactionCommitted", this.transactions.size === 0); - if (!hasSome(commits)) { - return undefined; - } - - const squashedCommits = this.squashAfter(startCommit); - return [squashedCommits, this.head]; - } - - /** - * Cancel the current transaction. There must be a transaction in progress that was begun via - * {@link startTransaction}. All commits made during the transaction will be removed. - * @returns the change to this branch resulting in the removal of the commits, and a list of the - * commits that were removed. - */ - public abortTransaction(): [ - change: TChange | undefined, - abortedCommits: GraphCommit[], - ] { - this.assertNotDisposed(); - const [startCommit] = this.popTransaction(); - this.editor.exitTransaction(); - this.#events.emit("transactionAborted", this.transactions.size === 0); - const [taggedChange, removedCommits] = this.removeAfter(startCommit); - this.#events.emit("transactionRolledBack", this.transactions.size === 0); - return [taggedChange?.change, removedCommits]; - } - - /** - * True iff this branch is in the middle of a transaction that was begin via {@link startTransaction} - */ - public isTransacting(): boolean { - return this.transactions.size !== 0; - } - - private popTransaction(): [GraphCommit, GraphCommit[]] { - const { startRevision: startRevisionOriginal } = this.transactions.pop(); - let startRevision = startRevisionOriginal; - - for ( - let r: RevisionTag | undefined = startRevision; - r !== undefined; - r = this.initialTransactionRevToRebasedRev.get(startRevision) - ) { - startRevision = r; - } - - if (!this.isTransacting()) { - this.initialTransactionRevToRebasedRev.clear(); - } - - const commits: GraphCommit[] = []; - const startCommit = findAncestor( - [this.head, commits], - (c) => c.revision === startRevision, - ); - assert( - startCommit !== undefined, - 0x593 /* Expected branch to be ahead of transaction start revision */, - ); - return [startCommit, commits]; - } - /** * Spawn a new branch that is based off of the current state of this branch. * @param commit - The commit to base the new branch off of. Defaults to the head of this branch. @@ -421,14 +271,6 @@ export class SharedTreeBranch { assert(hasSome(targetCommits), "Expected commit(s) for a non no-op rebase"); const newCommits = targetCommits.concat(sourceCommits); - - if (this.isTransacting()) { - const src = targetCommits[0].parent?.revision; - const dst = getLast(targetCommits).revision; - if (src !== undefined && dst !== undefined) { - this.initialTransactionRevToRebasedRev.set(src, dst); - } - } const changeEvent = { type: "replace", get change() { @@ -538,10 +380,6 @@ export class SharedTreeBranch { ): [change: TChange, newCommits: GraphCommit[]] | undefined { this.assertNotDisposed(); branch.assertNotDisposed(); - assert( - !branch.isTransacting(), - 0x597 /* Branch may not be merged while transaction is in progress */, - ); if (branch === this) { return undefined; @@ -616,10 +454,6 @@ export class SharedTreeBranch { return; } - while (this.isTransacting()) { - this.abortTransaction(); - } - this.unsubscribeBranchTrimmer?.(); this.disposed = true; @@ -640,8 +474,7 @@ export class SharedTreeBranch { * The deregister function has undefined behavior if called more than once. */ // Branches are invariant over TChange -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function onForkTransitive>( +export function onForkTransitive }>( branch: T, onFork: (fork: T) => void, ): () => void { diff --git a/packages/dds/tree/src/shared-tree-core/sharedTreeCore.ts b/packages/dds/tree/src/shared-tree-core/sharedTreeCore.ts index 016e0a4e73a8..c9811360f64c 100644 --- a/packages/dds/tree/src/shared-tree-core/sharedTreeCore.ts +++ b/packages/dds/tree/src/shared-tree-core/sharedTreeCore.ts @@ -45,7 +45,7 @@ import { breakingClass, } from "../util/index.js"; -import { type SharedTreeBranch, getChangeReplaceType } from "./branch.js"; +import { getChangeReplaceType, type SharedTreeBranch } from "./branch.js"; import { EditManager, minimumPossibleSequenceNumber } from "./editManager.js"; import { makeEditManagerCodec } from "./editManagerCodecs.js"; import type { SeqNumber } from "./editManagerFormat.js"; @@ -56,7 +56,7 @@ import { type ChangeEnricherReadonlyCheckout, NoOpChangeEnricher } from "./chang import type { ResubmitMachine } from "./resubmitMachine.js"; import { DefaultResubmitMachine } from "./defaultResubmitMachine.js"; import { BranchCommitEnricher } from "./branchCommitEnricher.js"; -import { createChildLogger, UsageError } from "@fluidframework/telemetry-utils/internal"; +import { createChildLogger } from "@fluidframework/telemetry-utils/internal"; // TODO: Organize this to be adjacent to persisted types. const summarizablesTreeKey = "indexes"; @@ -183,24 +183,7 @@ export class SharedTreeCore this.mintRevisionTag, rebaseLogger, ); - this.editManager.localBranch.events.on("transactionStarted", () => { - if (this.detachedRevision === undefined) { - // It is currently forbidden to attach during a transaction, so transaction state changes can be ignored until after attaching. - this.commitEnricher.startTransaction(); - } - }); - this.editManager.localBranch.events.on("transactionAborted", () => { - if (this.detachedRevision === undefined) { - // It is currently forbidden to attach during a transaction, so transaction state changes can be ignored until after attaching. - this.commitEnricher.abortTransaction(); - } - }); - this.editManager.localBranch.events.on("transactionCommitted", () => { - if (this.detachedRevision === undefined) { - // It is currently forbidden to attach during a transaction, so transaction state changes can be ignored until after attaching. - this.commitEnricher.commitTransaction(); - } - }); + this.editManager.localBranch.events.on("beforeChange", (change) => { if (this.detachedRevision === undefined) { // Commit enrichment is only necessary for changes that will be submitted as ops, and changes issued while detached are not submitted. @@ -208,20 +191,12 @@ export class SharedTreeCore } }); this.editManager.localBranch.events.on("afterChange", (change) => { - // We do not submit ops for changes that are part of a transaction. - if (!this.getLocalBranch().isTransacting()) { - if ( - change.type === "append" || - (change.type === "replace" && getChangeReplaceType(change) === "transactionCommit") - ) { - for (const commit of change.newCommits) { - this.submitCommit( - this.detachedRevision !== undefined - ? commit - : this.commitEnricher.enrich(commit), - this.schemaAndPolicy, - ); - } + if ( + change.type === "append" || + (change.type === "replace" && getChangeReplaceType(change) === "transactionCommit") + ) { + for (const commit of change.newCommits) { + this.submitCommit(commit, this.schemaAndPolicy); } } }); @@ -338,22 +313,21 @@ export class SharedTreeCore * @returns the submitted commit. This is undefined if the underlying `SharedObject` is not attached, * and may differ from `commit` due to enrichments like detached tree refreshers. */ - - private submitCommit( + protected submitCommit( commit: GraphCommit, schemaAndPolicy: ClonableSchemaAndPolicy, isResubmit = false, ): void { - assert( - // Edits should not be submitted until all transactions finish - !this.getLocalBranch().isTransacting() || isResubmit, - 0x68b /* Unexpected edit submitted during transaction */, - ); assert( this.isAttached() === (this.detachedRevision === undefined), 0x95a /* Detached revision should only be set when not attached */, ); + const enrichedCommit = + this.detachedRevision === undefined && !isResubmit + ? this.commitEnricher.enrich(commit) + : commit; + // Edits submitted before the first attach are treated as sequenced because they will be included // in the attach summary that is uploaded to the service. // Until this attach workflow happens, this instance essentially behaves as a centralized data structure. @@ -361,7 +335,7 @@ export class SharedTreeCore const newRevision: SeqNumber = brand((this.detachedRevision as number) + 1); this.detachedRevision = newRevision; this.editManager.addSequencedChange( - { ...commit, sessionId: this.editManager.localSessionId }, + { ...enrichedCommit, sessionId: this.editManager.localSessionId }, newRevision, this.detachedRevision, ); @@ -370,7 +344,7 @@ export class SharedTreeCore } const message = this.messageCodec.encode( { - commit, + commit: enrichedCommit, sessionId: this.editManager.localSessionId, }, { @@ -383,7 +357,7 @@ export class SharedTreeCore schema: schemaAndPolicy.schema.clone(), policy: schemaAndPolicy.policy, }); - this.resubmitMachine.onCommitSubmitted(commit); + this.resubmitMachine.onCommitSubmitted(enrichedCommit); } protected processCore( @@ -416,13 +390,6 @@ export class SharedTreeCore protected onDisconnect(): void {} protected override didAttach(): void { - if (this.getLocalBranch().isTransacting()) { - // Attaching during a transaction is not currently supported. - // At least part of of the system is known to not handle this case correctly - commit enrichment - and there may be others. - throw new UsageError( - "Cannot attach while a transaction is in progress. Commit or abort the transaction before attaching.", - ); - } if (this.detachedRevision !== undefined) { this.detachedRevision = undefined; } @@ -461,10 +428,6 @@ export class SharedTreeCore } protected applyStashedOp(content: JsonCompatibleReadOnly): void { - assert( - !this.getLocalBranch().isTransacting(), - 0x674 /* Unexpected transaction is open while applying stashed ops */, - ); // Empty context object is passed in, as our decode function is schema-agnostic. const { commit: { revision, change }, diff --git a/packages/dds/tree/src/shared-tree/sharedTree.ts b/packages/dds/tree/src/shared-tree/sharedTree.ts index b6899a2399a4..298db89486ac 100644 --- a/packages/dds/tree/src/shared-tree/sharedTree.ts +++ b/packages/dds/tree/src/shared-tree/sharedTree.ts @@ -311,6 +311,25 @@ export class SharedTree breaker: this.breaker, }, ); + + this.checkout.events.on("transactionStarted", () => { + if (this.isAttached()) { + // It is currently forbidden to attach during a transaction, so transaction state changes can be ignored until after attaching. + this.commitEnricher.startTransaction(); + } + }); + this.checkout.events.on("transactionAborted", () => { + if (this.isAttached()) { + // It is currently forbidden to attach during a transaction, so transaction state changes can be ignored until after attaching. + this.commitEnricher.abortTransaction(); + } + }); + this.checkout.events.on("transactionCommitted", () => { + if (this.isAttached()) { + // It is currently forbidden to attach during a transaction, so transaction state changes can be ignored until after attaching. + this.commitEnricher.commitTransaction(); + } + }); } @throwIfBroken @@ -350,6 +369,40 @@ export class SharedTree this.checkout.setTipRevisionForLoadedData(this.trunkHeadRevision); this._events.emit("afterBatch"); } + + protected override submitCommit( + ...args: Parameters< + SharedTreeCore["submitCommit"] + > + ): void { + // We do not submit ops for changes that are part of a transaction. + if (!this.checkout.isTransacting()) { + super.submitCommit(...args); + } + } + + protected override didAttach(): void { + if (this.checkout.isTransacting()) { + // Attaching during a transaction is not currently supported. + // At least part of of the system is known to not handle this case correctly - commit enrichment - and there may be others. + throw new UsageError( + "Cannot attach while a transaction is in progress. Commit or abort the transaction before attaching.", + ); + } + super.didAttach(); + } + + protected override applyStashedOp( + ...args: Parameters< + SharedTreeCore["applyStashedOp"] + > + ): void { + assert( + !this.checkout.isTransacting(), + 0x674 /* Unexpected transaction is open while applying stashed ops */, + ); + super.applyStashedOp(...args); + } } /** diff --git a/packages/dds/tree/src/shared-tree/treeCheckout.ts b/packages/dds/tree/src/shared-tree/treeCheckout.ts index 263d7e5fe437..483c7f48594d 100644 --- a/packages/dds/tree/src/shared-tree/treeCheckout.ts +++ b/packages/dds/tree/src/shared-tree/treeCheckout.ts @@ -44,6 +44,8 @@ import { visitDelta, type RevertibleAlphaFactory, type RevertibleAlpha, + type GraphCommit, + findAncestor, } from "../core/index.js"; import { type FieldBatchCodec, @@ -56,7 +58,9 @@ import { } from "../feature-libraries/index.js"; import { SharedTreeBranch, + TransactionStack, getChangeReplaceType, + onForkTransitive, type SharedTreeBranchChange, } from "../shared-tree-core/index.js"; import { @@ -106,6 +110,31 @@ export interface CheckoutEvents { * this change is not revertible. */ changed(data: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void; + + /** + * Fired after a new transaction is started. + */ + transactionStarted(): void; + + /** + * Fired after the current transaction is aborted. + */ + transactionAborted(): void; + + /** + * Fired after the current transaction is committed. + */ + transactionCommitted(): void; + + /** + * Fired when a new branch is created from this checkout. + */ + fork(branch: ITreeCheckout): void; + + /** + * Fired when the checkout is disposed. + */ + dispose(): void; } /** @@ -289,10 +318,7 @@ export function createTreeCheckout( ); const events = args?.events ?? createEmitter(); - const transaction = new Transaction(branch); - return new TreeCheckout( - transaction, branch, false, changeFamily, @@ -353,26 +379,21 @@ export interface ITransaction { } class Transaction implements ITransaction { - public constructor( - private readonly branch: SharedTreeBranch, - ) {} + public constructor(private readonly checkout: TreeCheckout) {} public start(): void { - this.branch.startTransaction(); - this.branch.editor.enterTransaction(); + this.checkout.startTransaction(); } public commit(): TransactionResult.Commit { - this.branch.commitTransaction(); - this.branch.editor.exitTransaction(); + this.checkout.commitTransaction(); return TransactionResult.Commit; } public abort(): TransactionResult.Abort { - this.branch.abortTransaction(); - this.branch.editor.exitTransaction(); + this.checkout.abortTransaction(); return TransactionResult.Abort; } public inProgress(): boolean { - return this.branch.isTransacting(); + return this.checkout.isTransacting(); } } @@ -407,6 +428,32 @@ export class TreeCheckout implements ITreeCheckoutFork { private readonly views = new Set>(); + public readonly transaction: ITransaction; + private readonly transactions = new TransactionStack(); + /** + * After pushing a starting revision to the transaction stack, this branch might be rebased + * over commits which are children of that starting revision. When the transaction is committed, + * those rebased-over commits should not be included in the transaction's squash commit, even though + * they exist between the starting revision and the final commit within the transaction. + * + * Whenever `rebaseOnto` is called during a transaction, this map is augmented with an entry from the + * original merge-base to the new merge-base. + * + * This state need only be retained for the lifetime of the transaction. + * + * TODO: This strategy might need to be revisited when adding better support for async transactions. + * Since: + * + * 1. Transactionality is guaranteed primarily by squashing at commit time + * 2. Branches may be rebased with an ongoing transaction + * + * a rebase operation might invalidate only a portion of a transaction's commits, thus defeating the + * purpose of transactionality. + * + * AB#6483 and children items track this work. + */ + private readonly initialTransactionRevToRebasedRev = new Map(); + /** * Set of revertibles maintained for automatic disposal */ @@ -434,7 +481,6 @@ export class TreeCheckout implements ITreeCheckoutFork { public static readonly revertTelemetryEventName = "RevertRevertible"; public constructor( - public readonly transaction: ITransaction, private readonly _branch: SharedTreeBranch, /** True if and only if this checkout is for a forked branch and not the "main branch" of the tree. */ public readonly isBranch: boolean, @@ -456,21 +502,7 @@ export class TreeCheckout implements ITreeCheckoutFork { private readonly logger?: ITelemetryLoggerExt, private readonly breaker: Breakable = new Breakable("TreeCheckout"), ) { - // when a transaction is started, take a snapshot of the current state of removed roots - _branch.events.on("transactionStarted", () => { - this.removedRootsSnapshots.push(this.removedRoots.clone()); - }); - // when a transaction is committed, the latest snapshot of removed roots can be discarded - _branch.events.on("transactionCommitted", () => { - this.removedRootsSnapshots.pop(); - }); - // after a transaction is rolled back, revert removed roots back to the latest snapshot - _branch.events.on("transactionRolledBack", () => { - const snapshot = this.removedRootsSnapshots.pop(); - assert(snapshot !== undefined, 0x9ae /* a snapshot for removed roots does not exist */); - this.removedRoots = snapshot; - }); - + this.transaction = new Transaction(this); // We subscribe to `beforeChange` rather than `afterChange` here because it's possible that the change is invalid WRT our forest. // For example, a bug in the editor might produce a malformed change object and thus applying the change to the forest will throw an error. // In such a case we will crash here, preventing the change from being added to the commit graph, and preventing `afterChange` from firing. @@ -530,7 +562,7 @@ export class TreeCheckout implements ITreeCheckoutFork { _branch.events.on("afterChange", (event) => { // The following logic allows revertibles to be generated for the change. // Currently only appends (including merges) and transaction commits are supported. - if (!_branch.isTransacting()) { + if (!this.isTransacting()) { if ( event.type === "append" || (event.type === "replace" && getChangeReplaceType(event) === "transactionCommit") @@ -724,6 +756,88 @@ export class TreeCheckout implements ITreeCheckoutFork { return this.forest.anchors.locate(anchor); } + // #region Transactions + + public isTransacting(): boolean { + return this.transactions.size !== 0; + } + + public startTransaction(): void { + this.checkNotDisposed(); + + const forks = new Set(); + const onDisposeUnSubscribes: (() => void)[] = []; + const onForkUnSubscribe = onForkTransitive(this, (fork) => { + forks.add(fork); + onDisposeUnSubscribes.push(fork.events.on("dispose", () => forks.delete(fork))); + }); + this.transactions.push(this._branch.getHead().revision, () => { + forks.forEach((fork) => fork.dispose()); + onDisposeUnSubscribes.forEach((unsubscribe) => unsubscribe()); + onForkUnSubscribe(); + }); + this._branch.editor.enterTransaction(); + // When a transaction is started, take a snapshot of the current state of removed roots + this.events.emit("transactionStarted"); + this.removedRootsSnapshots.push(this.removedRoots.clone()); + } + + public abortTransaction(): void { + this.checkNotDisposed(); + const [startCommit] = this.popTransaction(); + this._branch.editor.exitTransaction(); + this.events.emit("transactionAborted"); + this._branch.removeAfter(startCommit); + // After a transaction is rolled back, revert removed roots back to the latest snapshot + const snapshot = this.removedRootsSnapshots.pop(); + assert(snapshot !== undefined, 0x9ae /* a snapshot for removed roots does not exist */); + this.removedRoots = snapshot; + } + + public commitTransaction(): void { + this.checkNotDisposed(); + const [startCommit, commits] = this.popTransaction(); + this._branch.editor.exitTransaction(); + this.events.emit("transactionCommitted"); + // When a transaction is committed, the latest snapshot of removed roots can be discarded + this.removedRootsSnapshots.pop(); + if (!hasSome(commits)) { + return undefined; + } + + this._branch.squashAfter(startCommit); + } + + private popTransaction(): [GraphCommit, GraphCommit[]] { + const { startRevision: startRevisionOriginal } = this.transactions.pop(); + let startRevision = startRevisionOriginal; + + for ( + let r: RevisionTag | undefined = startRevision; + r !== undefined; + r = this.initialTransactionRevToRebasedRev.get(startRevision) + ) { + startRevision = r; + } + + if (!this.isTransacting()) { + this.initialTransactionRevToRebasedRev.clear(); + } + + const commits: GraphCommit[] = []; + const startCommit = findAncestor( + [this._branch.getHead(), commits], + (c) => c.revision === startRevision, + ); + assert( + startCommit !== undefined, + 0x593 /* Expected branch to be ahead of transaction start revision */, + ); + return [startCommit, commits]; + } + + // #endregion Transactions + public branch(): TreeCheckout { this.checkNotDisposed( "The parent branch has already been disposed and can no longer create new branches.", @@ -732,9 +846,7 @@ export class TreeCheckout implements ITreeCheckoutFork { const branch = this._branch.fork(); const storedSchema = this.storedSchema.clone(); const forest = this.forest.clone(storedSchema, anchors); - const transaction = new Transaction(branch); - return new TreeCheckout( - transaction, + const checkout = new TreeCheckout( branch, true, this.changeFamily, @@ -748,6 +860,8 @@ export class TreeCheckout implements ITreeCheckoutFork { this.logger, this.breaker, ); + this.events.emit("fork", checkout); + return checkout; } public rebase(checkout: TreeCheckout): void { @@ -758,14 +872,24 @@ export class TreeCheckout implements ITreeCheckoutFork { "The source of the branch rebase has been disposed and cannot be rebased.", ); assert( - !checkout.transaction.inProgress(), + !checkout.isTransacting(), 0x9af /* A view cannot be rebased while it has a pending transaction */, ); assert( checkout.isBranch, 0xa5d /* The main branch cannot be rebased onto another branch. */, ); - checkout._branch.rebaseOnto(this._branch); + + const result = checkout._branch.rebaseOnto(this._branch); + if (result !== undefined && this.isTransacting()) { + const { targetCommits } = result.commits; + // If `targetCommits` were empty, then `result` would be undefined and we couldn't reach here + assert(hasSome(targetCommits), "Expected target commits to be non-empty"); + const src = targetCommits[0].parent?.revision; + assert(src !== undefined, "Expected parent to be defined"); + const dst = getLast(targetCommits).revision; + this.initialTransactionRevToRebasedRev.set(src, dst); + } } public rebaseOnto(checkout: ITreeCheckout): void { @@ -785,7 +909,7 @@ export class TreeCheckout implements ITreeCheckoutFork { "The source of the branch merge has been disposed and cannot be merged.", ); assert( - !this.transaction.inProgress(), + !this.isTransacting(), 0x9b0 /* Views cannot be merged into a view while it has a pending transaction */, ); while (checkout.transaction.inProgress()) { @@ -812,11 +936,15 @@ export class TreeCheckout implements ITreeCheckoutFork { "The branch has already been disposed and cannot be disposed again.", ); this.disposed = true; + while (this.isTransacting()) { + this.abortTransaction(); + } this.purgeRevertibles(); this._branch.dispose(); for (const view of this.views) { view.dispose(); } + this.events.emit("dispose"); } public getRemovedRoots(): [string | number | undefined, number, JsonableTree][] { @@ -857,7 +985,7 @@ export class TreeCheckout implements ITreeCheckoutFork { } private revertRevertible(revision: RevisionTag, kind: CommitKind): RevertMetrics { - if (this._branch.isTransacting()) { + if (this.isTransacting()) { throw new UsageError("Undo is not yet supported during transactions."); } diff --git a/packages/dds/tree/src/test/shared-tree-core/branch.spec.ts b/packages/dds/tree/src/test/shared-tree-core/branch.spec.ts index d6ab75a58ffa..12a73eb6e07a 100644 --- a/packages/dds/tree/src/test/shared-tree-core/branch.spec.ts +++ b/packages/dds/tree/src/test/shared-tree-core/branch.spec.ts @@ -263,77 +263,6 @@ describe("Branches", () => { assert.equal(changeEventCount, 2); }); - it("emit correct change events during and after committing a transaction", () => { - // Create a branch and count the change events emitted - let changeEventCount = 0; - let replaceEventCount = 0; - const branch = create(({ type }) => { - if (type === "append") { - changeEventCount += 1; - } else if (type === "replace") { - replaceEventCount += 1; - } - }); - // Begin a transaction - branch.startTransaction(); - // Ensure that the correct change is emitted when applying changes in a transaction - change(branch); - assert.equal(changeEventCount, 2); - change(branch); - assert.equal(changeEventCount, 4); - assert.equal(replaceEventCount, 0); - // Commit the transaction. No change event should be emitted since the commits, though squashed, are still equivalent - branch.commitTransaction(); - assert.equal(changeEventCount, 4); - assert.equal(replaceEventCount, 2); - }); - - it("do not emit a change event after committing an empty transaction", () => { - // Create a branch and count the change events emitted - let changeEventCount = 0; - const branch = create(() => { - changeEventCount += 1; - }); - // Start and immediately abort a transaction - branch.startTransaction(); - branch.commitTransaction(); - assert.equal(changeEventCount, 0); - }); - - it("emit a change event after aborting a transaction", () => { - // Create a branch and count the change events emitted - let changeEventCount = 0; - const branch = create(({ type }) => { - if (type === "remove") { - changeEventCount += 1; - } - }); - // Begin a transaction - branch.startTransaction(); - // Apply a couple of changes to the branch - change(branch); - change(branch); - // Ensure the the correct number of change events have been emitted so far - assert.equal(changeEventCount, 0); - // Abort the transaction. A new change event should be emitted since the state rolls back to before the transaction - branch.abortTransaction(); - assert.equal(changeEventCount, 2); - }); - - it("do not emit a change event after aborting an empty transaction", () => { - // Create a branch and count the change events emitted - let changeEventCount = 0; - const branch = create(({ type }) => { - if (type === "remove") { - changeEventCount += 1; - } - }); - // Start and immediately abort a transaction - branch.startTransaction(); - branch.abortTransaction(); - assert.equal(changeEventCount, 0); - }); - it("emit a fork event after forking", () => { let fork: DefaultBranch | undefined; const branch = create(); @@ -351,166 +280,11 @@ describe("Branches", () => { assert.equal(disposed, true); }); - for (const withCommits of [true, false]) { - const [withCommitsTitle, potentiallyAddCommit] = withCommits - ? ["(with commits)", change] - : ["(without commits)", () => {}]; - it(`emit a transactionStarted event after a new transaction scope is opened ${withCommitsTitle}`, () => { - const branch = create(); - const log: boolean[] = []; - branch.events.on("transactionStarted", (isOuterTransaction) => { - log.push(isOuterTransaction); - }); - branch.startTransaction(); - { - assert.deepEqual(log, [true]); - potentiallyAddCommit(branch); - branch.startTransaction(); - { - assert.deepEqual(log, [true, false]); - potentiallyAddCommit(branch); - } - branch.abortTransaction(); - potentiallyAddCommit(branch); - branch.startTransaction(); - { - assert.deepEqual(log, [true, false, false]); - potentiallyAddCommit(branch); - } - branch.abortTransaction(); - } - branch.abortTransaction(); - }); - - it(`emit a transactionAborted event after a transaction scope is aborted ${withCommitsTitle}`, () => { - const branch = create(); - const log: boolean[] = []; - branch.events.on("transactionAborted", (isOuterTransaction) => { - log.push(isOuterTransaction); - }); - branch.startTransaction(); - { - potentiallyAddCommit(branch); - branch.startTransaction(); - { - potentiallyAddCommit(branch); - assert.deepEqual(log, []); - } - branch.abortTransaction(); - assert.deepEqual(log, [false]); - potentiallyAddCommit(branch); - branch.startTransaction(); - { - potentiallyAddCommit(branch); - } - branch.abortTransaction(); - assert.deepEqual(log, [false, false]); - potentiallyAddCommit(branch); - } - branch.abortTransaction(); - assert.deepEqual(log, [false, false, true]); - }); - - it(`emit a transactionCommitted event after a new transaction scope is committed ${withCommitsTitle}`, () => { - const branch = create(); - const log: boolean[] = []; - branch.events.on("transactionCommitted", (isOuterTransaction) => { - log.push(isOuterTransaction); - }); - branch.startTransaction(); - { - potentiallyAddCommit(branch); - branch.startTransaction(); - { - potentiallyAddCommit(branch); - assert.deepEqual(log, []); - } - branch.commitTransaction(); - assert.deepEqual(log, [false]); - potentiallyAddCommit(branch); - branch.startTransaction(); - { - potentiallyAddCommit(branch); - } - branch.commitTransaction(); - assert.deepEqual(log, [false, false]); - potentiallyAddCommit(branch); - } - branch.commitTransaction(); - assert.deepEqual(log, [false, false, true]); - }); - - it(`emit a transactionRolledBack event after a transaction scope is rolled back ${withCommitsTitle}`, () => { - const branch = create(); - const log: boolean[] = []; - branch.events.on("transactionRolledBack", (isOuterTransaction) => { - log.push(isOuterTransaction); - }); - branch.startTransaction(); - { - potentiallyAddCommit(branch); - branch.startTransaction(); - { - potentiallyAddCommit(branch); - assert.deepEqual(log, []); - } - branch.abortTransaction(); - assert.deepEqual(log, [false]); - potentiallyAddCommit(branch); - branch.startTransaction(); - { - potentiallyAddCommit(branch); - } - branch.abortTransaction(); - assert.deepEqual(log, [false, false]); - potentiallyAddCommit(branch); - } - branch.abortTransaction(); - assert.deepEqual(log, [false, false, true]); - }); - } - it("can be read after disposal", () => { const branch = create(); branch.dispose(); - // These methods are valid to call after disposal + // Getting the head is valid after disposal branch.getHead(); - branch.isTransacting(); - }); - - describe("do not include rebased-over changes in a transaction", () => { - it("when the transaction started on a commit known only to the local branch", () => { - const branch = create(); - const fork = branch.fork(); - - change(branch); - - change(fork); - fork.startTransaction(); - change(fork); - change(fork); - fork.rebaseOnto(branch); - const [commits] = fork.commitTransaction() ?? [[]]; - assert.equal(commits.length, 2); - }); - - it("when the transaction started on the commit the branch forked from", () => { - // i.e., the branch was created via .fork() and immediately started a transaction before any - // changes were applied. - const branch = create(); - const fork = branch.fork(); - - change(branch); - - fork.startTransaction(); - change(fork); - change(fork); - fork.rebaseOnto(branch); - change(branch); - fork.rebaseOnto(branch); - const [commits] = fork.commitTransaction() ?? [[]]; - assert.equal(commits.length, 2); - }); }); it("cannot be mutated after disposal", () => { @@ -522,10 +296,6 @@ describe("Branches", () => { assertDisposed(() => branch.fork()); assertDisposed(() => branch.rebaseOnto(fork)); assertDisposed(() => branch.merge(branch.fork())); - assertDisposed(() => branch.startTransaction()); - assertDisposed(() => branch.commitTransaction()); - assertDisposed(() => branch.abortTransaction()); - assertDisposed(() => branch.abortTransaction()); assertDisposed(() => fork.merge(branch)); }); @@ -602,133 +372,6 @@ describe("Branches", () => { assert.equal(changeEventCount, 2); }); - it("correctly report whether they are in the middle of a transaction", () => { - // Create a branch and test `isTransacting()` during two transactions, one nested within the other - const branch = create(); - assert.equal(branch.isTransacting(), false); - branch.startTransaction(); - assert.equal(branch.isTransacting(), true); - branch.startTransaction(); - assert.equal(branch.isTransacting(), true); - branch.abortTransaction(); - assert.equal(branch.isTransacting(), true); - branch.commitTransaction(); - assert.equal(branch.isTransacting(), false); - }); - - it("squash their commits when committing a transaction", () => { - // Create a new branch and start a transaction - const branch = create(); - branch.startTransaction(); - // Apply two changes to it - const tag1 = change(branch); - const tag2 = change(branch); - // Ensure that the commits are in the correct order with the correct tags - assertHistory(branch, tag1, tag2); - // Commit the transaction and ensure that there is now only one commit on the branch - branch.commitTransaction(); - assert.equal(branch.getHead().parent?.revision, nullRevisionTag); - }); - - it("rollback their commits when aborting a transaction", () => { - // Create a new branch and apply one change before starting a transaction - const branch = create(); - const tag1 = change(branch); - branch.startTransaction(); - // Apply two more changes to it - const tag2 = change(branch); - const tag3 = change(branch); - // Ensure that the commits are in the correct order with the correct tags - assertHistory(branch, tag1, tag2, tag3); - // Abort the transaction and ensure that there is now only one commit on the branch - branch.abortTransaction(); - assert.equal(branch.getHead().revision, tag1); - }); - - it("allow transactions to nest", () => { - // Create a new branch and open three transactions, applying one change in each - const branch = create(); - branch.startTransaction(); - change(branch); - branch.startTransaction(); - change(branch); - branch.startTransaction(); - change(branch); - // Commit the inner transaction, but abort the middle transaction so the inner one is moot - branch.commitTransaction(); - branch.abortTransaction(); - // Ensure that the branch has only one commit on it - assert.equal(branch.getHead().parent?.revision, nullRevisionTag); - // Abort the last transaction as well, and ensure that the branch has no commits on it - branch.abortTransaction(); - assert.equal(branch.getHead().revision, nullRevisionTag); - }); - - describe("all nested forks and transactions are disposed and aborted when transaction is", () => { - const setUpNestedForks = (rootBranch: DefaultBranch) => { - change(rootBranch); - rootBranch.startTransaction(); - const fork1 = rootBranch.fork(); - change(rootBranch); - rootBranch.startTransaction(); - const fork2 = rootBranch.fork(); - change(rootBranch); - const fork3 = rootBranch.fork(); - change(fork3); - const fork4 = fork3.fork(); - change(fork3); - fork3.startTransaction(); - change(fork3); - const fork5 = fork3.fork(); - - return { - disposedForks: [fork2, fork3, fork4, fork5], - notDisposedForks: [fork1], - }; - }; - - const assertNestedForks = (nestedForks: { - disposedForks: readonly DefaultBranch[]; - notDisposedForks: readonly DefaultBranch[]; - }) => { - nestedForks.disposedForks.forEach((fork) => { - assertDisposed(() => fork.fork()); - assert.equal(fork.isTransacting(), false); - }); - nestedForks.notDisposedForks.forEach((fork) => assertNotDisposed(() => fork.fork())); - }; - - it("commited", () => { - const rootBranch = create(); - const nestedForks = setUpNestedForks(rootBranch); - rootBranch.commitTransaction(); - - assert.equal(rootBranch.isTransacting(), true); - assertNestedForks(nestedForks); - - rootBranch.commitTransaction(); - assertNestedForks({ - disposedForks: nestedForks.notDisposedForks, - notDisposedForks: [], - }); - }); - - it("aborted", () => { - const rootBranch = create(); - const nestedForks = setUpNestedForks(rootBranch); - rootBranch.abortTransaction(); - - assert.equal(rootBranch.isTransacting(), true); - assertNestedForks(nestedForks); - - rootBranch.abortTransaction(); - assertNestedForks({ - disposedForks: nestedForks.notDisposedForks, - notDisposedForks: [], - }); - }); - }); - describe("transitive fork event", () => { /** Creates forks at various "depths" and returns the number of forks created */ function forkTransitive(forkable: T): number { diff --git a/packages/dds/tree/src/test/shared-tree-core/sharedTreeCore.spec.ts b/packages/dds/tree/src/test/shared-tree-core/sharedTreeCore.spec.ts index 50cbf073d8d4..cd78248fdb18 100644 --- a/packages/dds/tree/src/test/shared-tree-core/sharedTreeCore.spec.ts +++ b/packages/dds/tree/src/test/shared-tree-core/sharedTreeCore.spec.ts @@ -26,7 +26,6 @@ import { MockFluidDataStoreRuntime, MockSharedObjectServices, MockStorage, - validateAssertionError, } from "@fluidframework/test-runtime-utils/internal"; import { @@ -195,27 +194,6 @@ describe("SharedTreeCore", () => { assert.equal(getTrunkLength(tree), 6 - 3); }); - it("can complete a transaction that spans trunk eviction", () => { - const runtime = new MockFluidDataStoreRuntime({ idCompressor: createIdCompressor() }); - const tree = new TestSharedTreeCore(runtime); - const factory = new MockContainerRuntimeFactory(); - factory.createContainerRuntime(runtime); - tree.connect({ - deltaConnection: runtime.createDeltaConnection(), - objectStorage: new MockStorage(), - }); - - changeTree(tree); - factory.processAllMessages(); - assert.equal(getTrunkLength(tree), 1); - const branch1 = tree.getLocalBranch().fork(); - branch1.startTransaction(); - changeTree(tree); - changeTree(tree); - factory.processAllMessages(); - branch1.commitTransaction(); - }); - it("evicts trunk commits only when no branches have them in their ancestry", () => { const runtime = new MockFluidDataStoreRuntime({ idCompressor: createIdCompressor() }); const tree = new TestSharedTreeCore(runtime); @@ -548,7 +526,7 @@ describe("SharedTreeCore", () => { deltaConnection: dataStoreRuntime1.createDeltaConnection(), objectStorage: new MockStorage(), }); - tree.getLocalBranch().startTransaction(); + tree.startTransaction(); assert.equal(enricher.enrichmentLog.length, 0); changeTree(tree); assert.equal(enricher.enrichmentLog.length, 1); @@ -556,7 +534,7 @@ describe("SharedTreeCore", () => { changeTree(tree); assert.equal(enricher.enrichmentLog.length, 2); assert.equal(enricher.enrichmentLog[1].input, tree.getLocalBranch().getHead().change); - tree.getLocalBranch().commitTransaction(); + tree.commitTransaction(); assert.equal(enricher.enrichmentLog.length, 2); assert.equal(machine.submissionLog.length, 1); assert.notEqual(machine.submissionLog[0], tree.getLocalBranch().getHead().change); @@ -575,12 +553,12 @@ describe("SharedTreeCore", () => { deltaConnection: dataStoreRuntime1.createDeltaConnection(), objectStorage: new MockStorage(), }); - tree.getLocalBranch().startTransaction(); + tree.startTransaction(); assert.equal(enricher.enrichmentLog.length, 0); changeTree(tree); assert.equal(enricher.enrichmentLog.length, 1); assert.equal(enricher.enrichmentLog[0].input, tree.getLocalBranch().getHead().change); - tree.getLocalBranch().abortTransaction(); + tree.abortTransaction(); assert.equal(enricher.enrichmentLog.length, 1); assert.equal(machine.submissionLog.length, 0); }); @@ -620,26 +598,6 @@ describe("SharedTreeCore", () => { }); }); - it("throws an error if attaching during a transaction", () => { - const tree = createTree([]); - const containerRuntimeFactory = new MockContainerRuntimeFactoryForReconnection(); - const dataStoreRuntime1 = new MockFluidDataStoreRuntime({ - idCompressor: createIdCompressor(), - }); - containerRuntimeFactory.createContainerRuntime(dataStoreRuntime1); - tree.getLocalBranch().startTransaction(); - assert.throws( - () => { - tree.connect({ - deltaConnection: dataStoreRuntime1.createDeltaConnection(), - objectStorage: new MockStorage(), - }); - }, - (e: Error) => - validateAssertionError(e, /Cannot attach while a transaction is in progress/), - ); - }); - function isSummaryTree(summaryObject: SummaryObject): summaryObject is ISummaryTree { return summaryObject.type === SummaryType.Tree; } diff --git a/packages/dds/tree/src/test/shared-tree-core/utils.ts b/packages/dds/tree/src/test/shared-tree-core/utils.ts index ce73135d8cdf..de85955fbbfb 100644 --- a/packages/dds/tree/src/test/shared-tree-core/utils.ts +++ b/packages/dds/tree/src/test/shared-tree-core/utils.ts @@ -10,7 +10,11 @@ import type { import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils/internal"; import type { ICodecOptions } from "../../codec/index.js"; -import { RevisionTagCodec, TreeStoredSchemaRepository } from "../../core/index.js"; +import { + RevisionTagCodec, + TreeStoredSchemaRepository, + type GraphCommit, +} from "../../core/index.js"; import { typeboxValidator } from "../../external-utilities/index.js"; import { DefaultChangeFamily, @@ -44,6 +48,8 @@ export class TestSharedTreeCore extends SharedTreeCore; + public constructor( runtime: IFluidDataStoreRuntime = new MockFluidDataStoreRuntime({ idCompressor: testIdCompressor, @@ -86,4 +92,38 @@ export class TestSharedTreeCore extends SharedTreeCore { return super.getLocalBranch(); } + + protected override submitCommit( + ...args: Parameters["submitCommit"]> + ): void { + // We do not submit ops for changes that are part of a transaction. + if (this.transactionStart === undefined) { + super.submitCommit(...args); + } + } + + public startTransaction(): void { + assert( + this.transactionStart === undefined, + "Transaction already started. TestSharedTreeCore does not support nested transactions.", + ); + this.transactionStart = this.getLocalBranch().getHead(); + this.commitEnricher.startTransaction(); + } + + public abortTransaction(): void { + assert(this.transactionStart !== undefined, "No transaction to abort."); + const start = this.transactionStart; + this.transactionStart = undefined; + this.commitEnricher.abortTransaction(); + this.getLocalBranch().removeAfter(start); + } + + public commitTransaction(): void { + assert(this.transactionStart !== undefined, "No transaction to commit."); + const start = this.transactionStart; + this.transactionStart = undefined; + this.commitEnricher.commitTransaction(); + this.getLocalBranch().squashAfter(start); + } } diff --git a/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts b/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts index 825b444f9b28..407bd8bb93e2 100644 --- a/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts @@ -12,6 +12,7 @@ import { MockContainerRuntimeFactory, MockFluidDataStoreRuntime, MockStorage, + validateAssertionError, } from "@fluidframework/test-runtime-utils/internal"; import { type ITestFluidObject, @@ -2096,6 +2097,28 @@ describe("SharedTree", () => { ); }); + it("throws an error if attaching during a transaction", () => { + const sharedTreeFactory = new SharedTreeFactory(); + const runtime = new MockFluidDataStoreRuntime({ idCompressor: createIdCompressor() }); + const tree = sharedTreeFactory.create(runtime, "tree"); + const runtimeFactory = new MockContainerRuntimeFactory(); + runtimeFactory.createContainerRuntime(runtime); + const view = tree.viewWith(new TreeViewConfiguration({ schema: StringArray })); + view.initialize([]); + assert.throws( + () => { + Tree.runTransaction(view, () => { + tree.connect({ + deltaConnection: runtime.createDeltaConnection(), + objectStorage: new MockStorage(), + }); + }); + }, + (e: Error) => + validateAssertionError(e, /Cannot attach while a transaction is in progress/), + ); + }); + it("breaks on exceptions", () => { const tree = treeTestFactory(); const sf = new SchemaFactory("test"); diff --git a/packages/dds/tree/src/test/shared-tree/treeCheckout.spec.ts b/packages/dds/tree/src/test/shared-tree/treeCheckout.spec.ts index 3e5c429d9e3a..86ec5fba5081 100644 --- a/packages/dds/tree/src/test/shared-tree/treeCheckout.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/treeCheckout.spec.ts @@ -801,6 +801,37 @@ describe("sharedTreeView", () => { }, { skip: true }, ); + + itView("dispose branches created during the transaction", ({ view, tree }) => { + const branchA = tree.branch(); + view.checkout.transaction.start(); + const branchB = tree.branch(); + view.checkout.transaction.start(); + const branchC = tree.branch(); + assert.equal(branchA.disposed, false); + assert.equal(branchB.disposed, false); + assert.equal(branchC.disposed, false); + view.checkout.transaction.abort(); + assert.equal(branchA.disposed, false); + assert.equal(branchB.disposed, false); + assert.equal(branchC.disposed, true); + view.checkout.transaction.commit(); + assert.equal(branchA.disposed, false); + assert.equal(branchB.disposed, true); + assert.equal(branchC.disposed, true); + }); + + itView("statuses are reported correctly", ({ view }) => { + assert.equal(view.checkout.isTransacting(), false); + view.checkout.transaction.start(); + assert.equal(view.checkout.isTransacting(), true); + view.checkout.transaction.start(); + assert.equal(view.checkout.isTransacting(), true); + view.checkout.transaction.commit(); + assert.equal(view.checkout.isTransacting(), true); + view.checkout.transaction.abort(); + assert.equal(view.checkout.isTransacting(), false); + }); }); describe("disposal", () => { From e71e434f6af93982170808638108c3095d26ec9e Mon Sep 17 00:00:00 2001 From: Abram Sanderson Date: Mon, 25 Nov 2024 12:07:21 -0800 Subject: [PATCH 28/40] fix(@fluid-experimental/tree): Export undo-redo constructs (#23201) ## Description Exports the intended undo-redo surface from experimental SharedTree. Resolves #23028 Co-authored-by: Abram Sanderson --- .../api-report/experimental-tree.alpha.api.md | 18 ++++++++++ experimental/dds/tree/src/UndoRedoHandler.ts | 34 +++++++++++++++++++ experimental/dds/tree/src/index.ts | 2 ++ 3 files changed, 54 insertions(+) diff --git a/experimental/dds/tree/api-report/experimental-tree.alpha.api.md b/experimental/dds/tree/api-report/experimental-tree.alpha.api.md index 7cdd78746bed..48bd439fe1a0 100644 --- a/experimental/dds/tree/api-report/experimental-tree.alpha.api.md +++ b/experimental/dds/tree/api-report/experimental-tree.alpha.api.md @@ -419,6 +419,12 @@ export interface InternalizedChange { InternalChangeBrand: '2cae1045-61cf-4ef7-a6a3-8ad920cb7ab3'; } +// @alpha +export interface IRevertible { + discard(): any; + revert(): any; +} + // @alpha export interface ISharedTreeEvents extends ISharedObjectEvents { // (undocumented) @@ -427,6 +433,11 @@ export interface ISharedTreeEvents extends ISharedObjectEvents { (event: 'appliedSequencedEdit', listener: SequencedEditAppliedHandler): any; } +// @alpha +export interface IUndoConsumer { + pushToCurrentOperation(revertible: IRevertible): any; +} + // @alpha export type LocalCompressedId = number & { readonly LocalCompressedId: '6fccb42f-e2a4-4243-bd29-f13d12b9c6d1'; @@ -721,6 +732,13 @@ export interface SharedTreeSummaryBase { readonly version: WriteFormat; } +// @alpha +export class SharedTreeUndoRedoHandler { + constructor(stackManager: IUndoConsumer); + attachTree(tree: SharedTree): void; + detachTree(tree: SharedTree): void; +} + // @alpha export enum Side { // (undocumented) diff --git a/experimental/dds/tree/src/UndoRedoHandler.ts b/experimental/dds/tree/src/UndoRedoHandler.ts index 525842c0aaea..e369c4c6cfaa 100644 --- a/experimental/dds/tree/src/UndoRedoHandler.ts +++ b/experimental/dds/tree/src/UndoRedoHandler.ts @@ -11,25 +11,59 @@ import { EditCommittedEventArguments, SharedTree } from './SharedTree.js'; // TODO: We temporarily duplicate these contracts from 'framework/undo-redo' to unblock development // while we decide on the correct layering for undo. +/** + * A revertible change + * + * @alpha + */ export interface IRevertible { + /** + * Revert the change + */ revert(); + + /** + * Discard the change, freeing any associated resources. + */ discard(); } +/** + * A consumer of revertible changes. + * + * This interface is typically implemented by a stack which may optionally aggregate multiple + * changes into one operation. + * + * @alpha + */ export interface IUndoConsumer { + /** + * Push a revertible to the current operation. Invoked for each change on undo consumers subscribed to a SharedTree. + */ pushToCurrentOperation(revertible: IRevertible); } /** * A shared tree undo redo handler that will add revertible local tree changes to the provided * undo redo stack manager + * + * @alpha */ export class SharedTreeUndoRedoHandler { constructor(private readonly stackManager: IUndoConsumer) {} + /** + * Attach a shared tree to this handler. Each edit from the tree will invoke `this.stackManager`'s + * {@link IUndoConsumer.pushToCurrentOperation} method with an associated {@link IRevertible}. + */ public attachTree(tree: SharedTree) { tree.on(SharedTreeEvent.EditCommitted, this.treeDeltaHandler); } + + /** + * Detach a shared tree from this handler. Edits from the tree will no longer cause `this.stackManager`'s + * {@link IUndoConsumer.pushToCurrentOperation} to be called. + */ public detachTree(tree: SharedTree) { tree.off(SharedTreeEvent.EditCommitted, this.treeDeltaHandler); } diff --git a/experimental/dds/tree/src/index.ts b/experimental/dds/tree/src/index.ts index 8d4d1dcc01fb..135758ec77b5 100644 --- a/experimental/dds/tree/src/index.ts +++ b/experimental/dds/tree/src/index.ts @@ -188,3 +188,5 @@ export { SharedTreeShim, SharedTreeShimFactory, } from './migration-shim/index.js'; + +export { IRevertible, IUndoConsumer, SharedTreeUndoRedoHandler } from './UndoRedoHandler.js'; From 651a64cd7753a75b1e60e0fb3f1ad949ad3238f3 Mon Sep 17 00:00:00 2001 From: Shubhangi Date: Mon, 25 Nov 2024 16:38:38 -0800 Subject: [PATCH 29/40] Adding pause/resume functionality for document lambdas (#23138) ## Description Recently we added circuit breaker and pause/resume for scriptorium, i.e. partition lambdas. With this PR, I am adding the pause/resume functionality for document lambdas. It will be triggered by a document lambda during various circuit breaker states (pause during open, and resume during close). Currently no doc lambda is using circuit breaker, so this is just to add functionality which will not be triggered until we implement circuit breaker in any doc lambda. Important changes: - Added pause/resume listeners for the event emitted by documentContext. - Since there can be multiple docContexts, we pause the kafka partition at the lowest offset out of them, so that no messages are missed during resume. - On resume, we start from the lowest offset and send to the contextManager and respective docContext. The offsets which were already processed successfully (if any) are skipped during this resume. - Remaining offsets are then processed as usual by the respective lambdas. (Lambdas should make sure to reset any state needed on pause, such that it can reprocess the failed messages on resume) ## Reviewer Guidance - Tested it locally by hardcoding context.pause() and resume() from deli lambda and making sure that ops are not lost on resume. Tested it with a single and multiple docPartitions. - This feature shouldnt have any impact until we implement circuit breaker in any doc lambda and trigger pause/resume from there. --------- Co-authored-by: Shubhangi Agarwal Co-authored-by: Shubhangi Agarwal --- .../src/document-router/contextManager.ts | 81 +++++++++++++++++-- .../src/document-router/documentContext.ts | 45 +++++++++-- .../src/document-router/documentLambda.ts | 81 ++++++++++++++++++- .../src/document-router/documentPartition.ts | 43 ++++++++++ .../src/kafka-service/partition.ts | 25 ++++-- .../src/kafka-service/partitionManager.ts | 4 +- .../src/kafka-service/runner.ts | 2 +- .../lambdas/src/scriptorium/lambda.ts | 2 +- .../packages/services-core/src/lambdas.ts | 7 +- 9 files changed, 264 insertions(+), 26 deletions(-) diff --git a/server/routerlicious/packages/lambdas-driver/src/document-router/contextManager.ts b/server/routerlicious/packages/lambdas-driver/src/document-router/contextManager.ts index 1f5606c1abe9..558ca9c13aef 100644 --- a/server/routerlicious/packages/lambdas-driver/src/document-router/contextManager.ts +++ b/server/routerlicious/packages/lambdas-driver/src/document-router/contextManager.ts @@ -38,6 +38,9 @@ export class DocumentContextManager extends EventEmitter { private closed = false; + private headUpdatedAfterResume = false; // used to track whether the head has been updated after a resume event, so that we allow moving out of order only once during resume. + private tailUpdatedAfterResume = false; // used to track whether the tail has been updated after a resume event, so that we allow moving out of order only once during resume. + constructor(private readonly partitionContext: IContext) { super(); } @@ -65,6 +68,22 @@ export class DocumentContextManager extends EventEmitter { Lumberjack.verbose("Emitting error from contextManager, context error event."); this.emit("error", error, errorData); }); + context.addListener("pause", (offset: number, reason?: any) => { + // Find the lowest offset of all contexts and emit pause + let lowestOffset = offset; + for (const docContext of this.contexts) { + if (docContext.head.offset < lowestOffset) { + lowestOffset = docContext.head.offset; + } + } + this.headUpdatedAfterResume = false; // reset this flag when we pause + this.tailUpdatedAfterResume = false; // reset this flag when we pause + Lumberjack.info("Emitting pause from contextManager", { lowestOffset, offset, reason }); + this.emit("pause", lowestOffset, offset, reason); + }); + context.addListener("resume", () => { + this.emit("resume"); + }); return context; } @@ -78,11 +97,44 @@ export class DocumentContextManager extends EventEmitter { } /** - * Updates the head to the new offset. The head offset will not be updated if it stays the same or moves backwards. + * Updates the head to the new offset. The head offset will not be updated if it stays the same or moves backwards, except if the resumeBackToOffset is specified. + * resumeBackToOffset is specified during resume after a lambda pause (eg: circuit breaker) * @returns True if the head was updated, false if it was not. */ - public setHead(head: IQueuedMessage) { - if (head.offset > this.head.offset) { + public setHead(head: IQueuedMessage, resumeBackToOffset?: number | undefined) { + if (head.offset > this.head.offset || head.offset === resumeBackToOffset) { + // If head is moving backwards + if (head.offset <= this.head.offset) { + if (head.offset <= this.lastCheckpoint.offset) { + Lumberjack.info( + "Not updating contextManager head since new head's offset is <= last checkpoint, returning early", + { + newHeadOffset: head.offset, + currentHeadOffset: this.head.offset, + lastCheckpointOffset: this.lastCheckpoint.offset, + }, + ); + return false; + } + if (this.headUpdatedAfterResume) { + Lumberjack.warning( + "ContextManager head is moving backwards again after a previous move backwards. This is unexpected.", + { resumeBackToOffset, currentHeadOffset: this.head.offset }, + ); + return false; + } + Lumberjack.info( + "Allowing the contextManager head to move to the specified offset", + { resumeBackToOffset, currentHeadOffset: this.head.offset }, + ); + } + if (!this.headUpdatedAfterResume && resumeBackToOffset !== undefined) { + Lumberjack.info("Setting headUpdatedAfterResume to true", { + resumeBackToOffset, + currentHeadOffset: this.head.offset, + }); + this.headUpdatedAfterResume = true; + } this.head = head; return true; } @@ -90,12 +142,29 @@ export class DocumentContextManager extends EventEmitter { return false; } - public setTail(tail: IQueuedMessage) { + public setTail(tail: IQueuedMessage, resumeBackToOffset?: number | undefined) { assert( - tail.offset > this.tail.offset && tail.offset <= this.head.offset, - `${tail.offset} > ${this.tail.offset} && ${tail.offset} <= ${this.head.offset}`, + (tail.offset > this.tail.offset || + (tail.offset === resumeBackToOffset && !this.tailUpdatedAfterResume)) && + tail.offset <= this.head.offset, + `Tail offset ${tail.offset} must be greater than the current tail offset ${this.tail.offset} or equal to the resume offset ${resumeBackToOffset} if not yet resumed (tailUpdatedAfterResume: ${this.tailUpdatedAfterResume}), and less than or equal to the head offset ${this.head.offset}.`, ); + if (tail.offset <= this.tail.offset) { + Lumberjack.info("Allowing the contextManager tail to move to the specified offset.", { + resumeBackToOffset, + currentTailOffset: this.tail.offset, + }); + } + + if (!this.tailUpdatedAfterResume && resumeBackToOffset !== undefined) { + Lumberjack.info("Setting tailUpdatedAfterResume to true", { + resumeBackToOffset, + currentTailOffset: this.tail.offset, + }); + this.tailUpdatedAfterResume = true; + } + this.tail = tail; this.updateCheckpoint(); } diff --git a/server/routerlicious/packages/lambdas-driver/src/document-router/documentContext.ts b/server/routerlicious/packages/lambdas-driver/src/document-router/documentContext.ts index c500da64b121..613ba2c8ab03 100644 --- a/server/routerlicious/packages/lambdas-driver/src/document-router/documentContext.ts +++ b/server/routerlicious/packages/lambdas-driver/src/document-router/documentContext.ts @@ -27,6 +27,8 @@ export class DocumentContext extends EventEmitter implements IContext { private closed = false; private contextError = undefined; + public headUpdatedAfterResume = false; // used to track whether the head has been updated after a resume event, so that we allow moving out of order only once during resume. + constructor( private readonly routingKey: IRoutingKey, head: IQueuedMessage, @@ -59,20 +61,51 @@ export class DocumentContext extends EventEmitter implements IContext { /** * Updates the head offset for the context. */ - public setHead(head: IQueuedMessage) { + public setHead(head: IQueuedMessage, resumeBackToOffset?: number | undefined) { assert( - head.offset > this.head.offset, - `${head.offset} > ${this.head.offset} ` + - `(${head.topic}, ${head.partition}, ${this.routingKey.tenantId}/${this.routingKey.documentId})`, + head.offset > this.head.offset || + (head.offset === resumeBackToOffset && !this.headUpdatedAfterResume), + `Head offset ${head.offset} must be greater than the current head offset ${this.head.offset} or equal to the resume offset ${resumeBackToOffset} if not yet resumed (headUpdatedAfterResume: ${this.headUpdatedAfterResume}). Topic ${head.topic}, partition ${head.partition}, tenantId ${this.routingKey.tenantId}, documentId ${this.routingKey.documentId}.`, ); + // If head is moving backwards + if (head.offset <= this.head.offset) { + if (head.offset <= this.tailInternal.offset) { + Lumberjack.info( + "Not updating documentContext head since new head's offset is <= last checkpoint offset (tailInternal), returning early", + { + newHeadOffset: head.offset, + currentHeadOffset: this.head.offset, + tailInternalOffset: this.tailInternal.offset, + documentId: this.routingKey.documentId, + }, + ); + return false; + } + Lumberjack.info("Allowing the document context head to move to the specified offset", { + resumeBackToOffset, + currentHeadOffset: this.head.offset, + documentId: this.routingKey.documentId, + }); + } + // When moving back to a state where head and tail differ we set the tail to be the old head, as in the // constructor, to make tail represent the inclusive top end of the checkpoint range. if (!this.hasPendingWork()) { this.tailInternal = this.getLatestTail(); } + if (!this.headUpdatedAfterResume && resumeBackToOffset !== undefined) { + Lumberjack.info("Setting headUpdatedAfterResume to true", { + resumeBackToOffset, + currentHeadOffset: this.head.offset, + documentId: this.routingKey.documentId, + }); + this.headUpdatedAfterResume = true; + } + this.headInternal = head; + return true; } public checkpoint(message: IQueuedMessage, restartOnCheckpointFailure?: boolean) { @@ -85,8 +118,7 @@ export class DocumentContext extends EventEmitter implements IContext { assert( offset > this.tail.offset && offset <= this.head.offset, - `${offset} > ${this.tail.offset} && ${offset} <= ${this.head.offset} ` + - `(${message.topic}, ${message.partition}, ${this.routingKey.tenantId}/${this.routingKey.documentId})`, + `Checkpoint offset ${offset} must be greater than the current tail offset ${this.tail.offset} and less than or equal to the head offset ${this.head.offset}. Topic ${message.topic}, partition ${message.partition}, tenantId ${this.routingKey.tenantId}, documentId ${this.routingKey.documentId}.`, ); // Update the tail and broadcast the checkpoint @@ -111,6 +143,7 @@ export class DocumentContext extends EventEmitter implements IContext { } public pause(offset: number, reason?: any) { + this.headUpdatedAfterResume = false; // reset this flag when we pause this.emit("pause", offset, reason); } diff --git a/server/routerlicious/packages/lambdas-driver/src/document-router/documentLambda.ts b/server/routerlicious/packages/lambdas-driver/src/document-router/documentLambda.ts index e6de570c154c..3fdd0f431998 100644 --- a/server/routerlicious/packages/lambdas-driver/src/document-router/documentLambda.ts +++ b/server/routerlicious/packages/lambdas-driver/src/document-router/documentLambda.ts @@ -33,6 +33,12 @@ export class DocumentLambda implements IPartitionLambda { private activityCheckTimer: NodeJS.Timeout | undefined; + private reprocessRange: { startOffset: number | undefined; endOffset: number | undefined } = { + startOffset: undefined, + endOffset: undefined, + }; + private reprocessingOffset: number | undefined; + constructor( private readonly factory: IPartitionLambdaFactory, private readonly context: IContext, @@ -45,6 +51,20 @@ export class DocumentLambda implements IPartitionLambda { ); context.error(error, errorData); }); + this.contextManager.on( + "pause", + (lowestOffset: number, pausedAtOffset: number, reason?: any) => { + // Emit pause at the lowest offset out of all document partitions + // This is important for ensuring that we don't miss any messages + // And store the reprocessRange so that we can allow contextManager to move back to it when it resumes + // It will move back to the first offset which was not checkpointed from this range + this.storeReprocessRange(lowestOffset, pausedAtOffset); + context.pause(lowestOffset, reason); + }, + ); + this.contextManager.on("resume", () => { + context.resume(); + }); this.activityCheckTimer = setInterval( this.inactivityCheck.bind(this), documentLambdaServerConfiguration.partitionActivityCheckInterval, @@ -55,18 +75,30 @@ export class DocumentLambda implements IPartitionLambda { * {@inheritDoc IPartitionLambda.handler} */ public handler(message: IQueuedMessage): undefined { - if (!this.contextManager.setHead(message)) { + this.reprocessingOffset = this.isOffsetWithinReprocessRange(message.offset) + ? message.offset + : undefined; + if (!this.contextManager.setHead(message, this.reprocessingOffset)) { this.context.log?.warn( "Unexpected head offset. " + `head offset: ${this.contextManager.getHeadOffset()}, message offset: ${ message.offset }`, ); + // update reprocessRange to avoid reprocessing the same message again + if (this.reprocessingOffset !== undefined) { + this.updateReprocessRange(this.reprocessingOffset); + } return undefined; } this.handlerCore(message); - this.contextManager.setTail(message); + this.contextManager.setTail(message, this.reprocessingOffset); + + // update reprocessRange to avoid reprocessing the same message again + if (this.reprocessingOffset !== undefined) { + this.updateReprocessRange(this.reprocessingOffset); + } return undefined; } @@ -86,6 +118,45 @@ export class DocumentLambda implements IPartitionLambda { this.documents.clear(); } + public pause(offset: number): void { + for (const [, partition] of this.documents) { + partition.pause(offset); + } + } + + public resume(): void { + for (const [, partition] of this.documents) { + partition.resume(); + } + } + + private storeReprocessRange(lowestOffset: number, pausedAtoffset: number) { + this.reprocessRange = { + startOffset: lowestOffset, + endOffset: pausedAtoffset, + }; + } + + private isOffsetWithinReprocessRange(offset: number) { + return ( + this.reprocessRange.startOffset !== undefined && + this.reprocessRange.endOffset !== undefined && + offset >= this.reprocessRange.startOffset && + offset <= this.reprocessRange.endOffset + ); + } + + private updateReprocessRange(reprocessedOffset: number) { + this.reprocessRange.startOffset = reprocessedOffset + 1; + if ( + this.reprocessRange.endOffset && + this.reprocessRange.endOffset < this.reprocessRange.startOffset + ) { + // reset since all messages in the reprocess range have been processed + this.reprocessRange = { startOffset: undefined, endOffset: undefined }; + } + } + private handlerCore(message: IQueuedMessage): void { const boxcar = extractBoxcar(message); if (!boxcar.documentId || !boxcar.tenantId) { @@ -114,9 +185,11 @@ export class DocumentLambda implements IPartitionLambda { ); this.documents.set(routingKey, document); } else { - // SetHead assumes it will always receive increasing offsets. So we need to split the creation case + // SetHead assumes it will always receive increasing offsets (except reprocessing during pause/resume). So we need to split the creation case // from the update case. - document.context.setHead(message); + if (!document.context.setHead(message, this.reprocessingOffset)) { + return; // if head not updated, it means it doesnt need to be processed, return early + } } // Forward the message to the document queue and then resolve the promise to begin processing more messages diff --git a/server/routerlicious/packages/lambdas-driver/src/document-router/documentPartition.ts b/server/routerlicious/packages/lambdas-driver/src/document-router/documentPartition.ts index a1a46a12ccbf..34076ec55e7e 100644 --- a/server/routerlicious/packages/lambdas-driver/src/document-router/documentPartition.ts +++ b/server/routerlicious/packages/lambdas-driver/src/document-router/documentPartition.ts @@ -22,6 +22,7 @@ export class DocumentPartition { private lambda: IPartitionLambda | undefined; private corrupt = false; private closed = false; + private paused = false; private activityTimeoutTime: number | undefined; private readonly restartOnErrorNames: string[] = []; @@ -94,6 +95,7 @@ export class DocumentPartition { restart: true, tenantId: this.tenantId, documentId: this.documentId, + errorLabel: "docPartition:lambdaFactory.create", }); } else { // There is no need to pass the message to be checkpointed to markAsCorrupt(). @@ -201,4 +203,45 @@ export class DocumentPartition { this.activityTimeoutTime = activityTime !== undefined ? activityTime : cacluatedActivityTimeout; } + + public pause(offset: number) { + if (this.paused) { + Lumberjack.warning("Doc partition already paused, returning early.", { + ...getLumberBaseProperties(this.documentId, this.tenantId), + offset, + }); + return; + } + this.paused = true; + + this.q.pause(); + this.q.remove(() => true); // flush all the messages in the queue since kafka consumer will resume from last successful offset + + if (this.lambda?.pause) { + this.lambda.pause(offset); + } + Lumberjack.info("Doc partition paused", { + ...getLumberBaseProperties(this.documentId, this.tenantId), + offset, + }); + } + + public resume() { + if (!this.paused) { + Lumberjack.warning("Doc partition already resumed, returning early.", { + ...getLumberBaseProperties(this.documentId, this.tenantId), + }); + return; + } + this.paused = false; + + this.q.resume(); + + if (this.lambda?.resume) { + this.lambda.resume(); + } + Lumberjack.info("Doc partition resumed", { + ...getLumberBaseProperties(this.documentId, this.tenantId), + }); + } } diff --git a/server/routerlicious/packages/lambdas-driver/src/kafka-service/partition.ts b/server/routerlicious/packages/lambdas-driver/src/kafka-service/partition.ts index 7f2c0a81c77d..1c1c4b0ebbfd 100644 --- a/server/routerlicious/packages/lambdas-driver/src/kafka-service/partition.ts +++ b/server/routerlicious/packages/lambdas-driver/src/kafka-service/partition.ts @@ -107,6 +107,13 @@ export class Partition extends EventEmitter { return; } + if (this.paused) { + Lumberjack.info("Partition is paused, skipping pushing message to queue", { + partitionId: this.id, + messageOffset: rawMessage.offset, + }); + return; + } this.q.push(rawMessage).catch((error) => { Lumberjack.error("Error pushing raw message to queue in partition", undefined, error); }); @@ -144,9 +151,12 @@ export class Partition extends EventEmitter { this.removeAllListeners(); } - public pause(): void { + public pause(offset: number): void { if (this.paused) { - Lumberjack.info(`Partition already paused, returning early.`, { partitionId: this.id }); + Lumberjack.warning(`Partition already paused, returning early.`, { + partitionId: this.id, + offset, + }); return; } this.paused = true; @@ -155,14 +165,14 @@ export class Partition extends EventEmitter { this.q.remove(() => true); // flush all the messages in the queue since kafka consumer will resume from last successful offset if (this.lambda?.pause) { - this.lambda.pause(); + this.lambda.pause(offset); } - Lumberjack.info(`Partition paused`, { partitionId: this.id }); + Lumberjack.info(`Partition paused`, { partitionId: this.id, offset }); } public resume(): void { if (!this.paused) { - Lumberjack.info(`Partition already resumed, returning early.`, { + Lumberjack.warning(`Partition already resumed, returning early.`, { partitionId: this.id, }); return; @@ -170,6 +180,11 @@ export class Partition extends EventEmitter { this.paused = false; this.q.resume(); + + if (this.lambda?.resume) { + // needed for documentLambdas + this.lambda.resume(); + } Lumberjack.info(`Partition resumed`, { partitionId: this.id }); } diff --git a/server/routerlicious/packages/lambdas-driver/src/kafka-service/partitionManager.ts b/server/routerlicious/packages/lambdas-driver/src/kafka-service/partitionManager.ts index 68d690ad7ae7..eb43edaa04bb 100644 --- a/server/routerlicious/packages/lambdas-driver/src/kafka-service/partitionManager.ts +++ b/server/routerlicious/packages/lambdas-driver/src/kafka-service/partitionManager.ts @@ -122,10 +122,10 @@ export class PartitionManager extends EventEmitter { this.removeAllListeners(); } - public pause(partitionId: number): void { + public pause(partitionId: number, offset: number): void { const partition = this.partitions.get(partitionId); if (partition) { - partition.pause(); + partition.pause(offset); } else { throw new Error(`PartitionId ${partitionId} not found for pause`); } diff --git a/server/routerlicious/packages/lambdas-driver/src/kafka-service/runner.ts b/server/routerlicious/packages/lambdas-driver/src/kafka-service/runner.ts index e5748981e9e9..ac753138054d 100644 --- a/server/routerlicious/packages/lambdas-driver/src/kafka-service/runner.ts +++ b/server/routerlicious/packages/lambdas-driver/src/kafka-service/runner.ts @@ -151,7 +151,7 @@ export class KafkaRunner implements IRunner { const seekTimeout = this.config?.get("kafka:seekTimeoutAfterPause") ?? 1000; await this.consumer.pauseFetching(partitionId, seekTimeout, offset); } - this.partitionManager?.pause(partitionId); + this.partitionManager?.pause(partitionId, offset); } public async resume(partitionId: number): Promise { diff --git a/server/routerlicious/packages/lambdas/src/scriptorium/lambda.ts b/server/routerlicious/packages/lambdas/src/scriptorium/lambda.ts index a353650df711..72e61dea465a 100644 --- a/server/routerlicious/packages/lambdas/src/scriptorium/lambda.ts +++ b/server/routerlicious/packages/lambdas/src/scriptorium/lambda.ts @@ -187,7 +187,7 @@ export class ScriptoriumLambda implements IPartitionLambda { this.dbCircuitBreaker?.shutdown(); } - public pause(): void { + public pause(offset: number): void { this.current.clear(); this.pending.clear(); this.pendingMetric = undefined; diff --git a/server/routerlicious/packages/services-core/src/lambdas.ts b/server/routerlicious/packages/services-core/src/lambdas.ts index 02e8eaee4a97..e211623bf1ec 100644 --- a/server/routerlicious/packages/services-core/src/lambdas.ts +++ b/server/routerlicious/packages/services-core/src/lambdas.ts @@ -133,7 +133,12 @@ export interface IPartitionLambda { /** * Pauses the lambda. It should clear any pending work. */ - pause?(): void; + pause?(offset: number): void; + + /** + * Resumes the lambda. This is relevant for documentLambda to resume the documentPartition queueus. + */ + resume?(): void; } /** From fa4a02e5c9be838dfb267649b076c1355ee2b109 Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:25:21 -0800 Subject: [PATCH 30/40] docs(odsp-driver): comment added to explain ternary for batched signals back compat (#23200) ## Description In #22749, we added `targetClientId` check to make sure only the specified client receives the targeted signal. We added a conditional check to make sure individual ISignalMessage's should be passed when there's one element. This done for signal-based layer combination compat testing to pass, as old loaders don't handle batched signals. --- .../drivers/odsp-driver/src/odspDocumentDeltaConnection.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/drivers/odsp-driver/src/odspDocumentDeltaConnection.ts b/packages/drivers/odsp-driver/src/odspDocumentDeltaConnection.ts index c6fbfaca84e8..7c185dc64dfd 100644 --- a/packages/drivers/odsp-driver/src/odspDocumentDeltaConnection.ts +++ b/packages/drivers/odsp-driver/src/odspDocumentDeltaConnection.ts @@ -699,6 +699,10 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection { ); if (filteredMsgs.length > 0) { + // This ternary is needed for signal-based layer compat tests to pass, + // specifically the layer version combination where you have an old loader and the most recent driver layer. + // Old loader doesn't send or receive batched signals (ISignalMessage[]), + // so only individual ISignalMessage's should be passed when there's one element for backcompat. listener(filteredMsgs.length === 1 ? filteredMsgs[0] : filteredMsgs, documentId); } }, From f7be9651daeba09853627c0953e5969a60674ce3 Mon Sep 17 00:00:00 2001 From: Jason Hartman Date: Tue, 26 Nov 2024 10:42:11 -0800 Subject: [PATCH 31/40] feat(client-presence): use common event support (#23196) - Use event types from core-interfaces and implementation from client-utils. - Add `off` event deregistration support. - Rename types following the updates that had been made to "core-interfaces" events. --- .changeset/slick-badgers-go.md | 12 + .../common/client-utils/src/events/emitter.ts | 2 +- .../core-interfaces/src/events/listeners.ts | 6 +- .../presence/api-report/presence.alpha.api.md | 35 +- packages/framework/presence/package.json | 5 +- .../framework/presence/src/cjs/package.json | 1 - .../framework/presence/src/events/events.ts | 329 ------------------ .../presence/src/exposedUtilityTypes.ts | 10 +- packages/framework/presence/src/index.ts | 9 +- .../presence/src/latestMapValueManager.ts | 7 +- .../presence/src/latestValueManager.ts | 7 +- .../presence/src/notificationsManager.ts | 75 ++-- packages/framework/presence/src/presence.ts | 5 +- .../framework/presence/src/presenceManager.ts | 4 +- .../framework/presence/src/systemWorkspace.ts | 3 +- .../src/test/notificationsManager.spec.ts | 5 +- pnpm-lock.yaml | 3 + 17 files changed, 108 insertions(+), 410 deletions(-) create mode 100644 .changeset/slick-badgers-go.md delete mode 100644 packages/framework/presence/src/events/events.ts diff --git a/.changeset/slick-badgers-go.md b/.changeset/slick-badgers-go.md new file mode 100644 index 000000000000..5d1de7f83fb6 --- /dev/null +++ b/.changeset/slick-badgers-go.md @@ -0,0 +1,12 @@ +--- +"@fluidframework/presence": minor +--- +--- +"section": feature +--- + +`off` event deregistration pattern now supported + +Event subscriptions within `@fluidframework/presence` may now use `off` to deregister event listeners, including initial listeners provided to `Notifications`. + +Some type names have shifted within the API though no consumers are expected to be using those types directly. The most visible rename is `NotificationSubscribable` to `NotificationListenable`. Other shifts are to use types now exported thru `@fluidframework/core-interfaces` where the most notable is `ISubscribable` that is now `Listenable`. diff --git a/packages/common/client-utils/src/events/emitter.ts b/packages/common/client-utils/src/events/emitter.ts index 9bb016ae9ea3..d0901128ac9d 100644 --- a/packages/common/client-utils/src/events/emitter.ts +++ b/packages/common/client-utils/src/events/emitter.ts @@ -204,7 +204,7 @@ class ComposableEventEmitter> * } * * class MyClass implements Listenable { - * private readonly events = createEmitterMinimal(); + * private readonly events = createEmitter(); * * private load(): void { * this.events.emit("loaded"); diff --git a/packages/common/core-interfaces/src/events/listeners.ts b/packages/common/core-interfaces/src/events/listeners.ts index d87d6d3b6060..d5fda44bfbdb 100644 --- a/packages/common/core-interfaces/src/events/listeners.ts +++ b/packages/common/core-interfaces/src/events/listeners.ts @@ -35,14 +35,14 @@ export type Listeners = { * @param TListeners - All the {@link Listeners | events} that this subscribable supports * * @privateRemarks - * `EventEmitter` can be used as a base class to implement this via extension. + * {@link @fluid-internal/client-utils#CustomEventEmitter} can be used as a base class to implement this via extension. * ```ts - * type MyEventEmitter = IEventEmitter<{ + * type MyEventEmitter = IEmitter<{ * load: (user: string, data: IUserData) => void; * error: (errorCode: number) => void; * }> * ``` - * {@link createEmitter} can help implement this interface via delegation. + * {@link @fluid-internal/client-utils#createEmitter} can help implement this interface via delegation. * * @sealed @public */ diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index bca4af4c8beb..1ef5850b7cd7 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -37,7 +37,7 @@ export const ExperimentalPresenceManager: SharedObjectKind; + readonly events: Listenable; getAttendee(clientId: ClientConnectionId | ClientSessionId): ISessionClient; getAttendees(): ReadonlySet; getMyself(): ISessionClient; @@ -89,7 +89,7 @@ export interface LatestMapValueManager>; clientValues(): IterableIterator>; readonly controls: BroadcastControls; - readonly events: ISubscribable>; + readonly events: Listenable>; readonly local: ValueMap; } @@ -123,7 +123,7 @@ export interface LatestValueManager { clientValue(client: ISessionClient): LatestValueData; clientValues(): IterableIterator>; readonly controls: BroadcastControls; - readonly events: ISubscribable>; + readonly events: Listenable>; get local(): InternalUtilityTypes.FullyReadonly>; set local(value: JsonSerializable & JsonDeserialized); } @@ -141,19 +141,25 @@ export interface LatestValueMetadata { } // @alpha @sealed -export interface NotificationEmitter> { - broadcast>(notificationName: K, ...args: Parameters): void; - unicast>(notificationName: K, targetClient: ISessionClient, ...args: Parameters): void; +export interface NotificationEmitter> { + broadcast>(notificationName: K, ...args: Parameters): void; + unicast>(notificationName: K, targetClient: ISessionClient, ...args: Parameters): void; +} + +// @alpha @sealed +export interface NotificationListenable> { + off>(notificationName: K, listener: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void): void; + on>(notificationName: K, listener: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void): Off; } // @alpha -export function Notifications, Key extends string = string>(initialSubscriptions: Partial>): InternalTypes.ManagerFactory, NotificationsManager>; +export function Notifications, Key extends string = string>(initialSubscriptions: Partial>): InternalTypes.ManagerFactory, NotificationsManager>; // @alpha @sealed -export interface NotificationsManager> { +export interface NotificationsManager> { readonly emit: NotificationEmitter; - readonly events: ISubscribable; - readonly notifications: NotificationSubscribable; + readonly events: Listenable; + readonly notifications: NotificationListenable; } // @alpha @sealed (undocumented) @@ -163,13 +169,8 @@ export interface NotificationsManagerEvents { } // @alpha @sealed -export interface NotificationSubscribable> { - on>(notificationName: K, listener: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void): () => void; -} - -// @alpha @sealed -export type NotificationSubscriptions> = { - [K in string & keyof InternalUtilityTypes.NotificationEvents]: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void; +export type NotificationSubscriptions> = { + [K in string & keyof InternalUtilityTypes.NotificationListeners]: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void; }; // @alpha @sealed (undocumented) diff --git a/packages/framework/presence/package.json b/packages/framework/presence/package.json index 8337e3260c8b..9a73ecdaaf4d 100644 --- a/packages/framework/presence/package.json +++ b/packages/framework/presence/package.json @@ -31,10 +31,6 @@ "import": "./lib/core-interfaces/index.js", "require": "./dist/core-interfaces/index.js" }, - "./internal/events": { - "import": "./lib/events/events.js", - "require": "./dist/events/events.js" - }, "./internal/exposedInternalTypes": { "import": "./lib/exposedInternalTypes.js", "require": "./dist/exposedInternalTypes.js" @@ -112,6 +108,7 @@ "temp-directory": "nyc/.nyc_output" }, "dependencies": { + "@fluid-internal/client-utils": "workspace:~", "@fluidframework/container-definitions": "workspace:~", "@fluidframework/container-loader": "workspace:~", "@fluidframework/container-runtime-definitions": "workspace:~", diff --git a/packages/framework/presence/src/cjs/package.json b/packages/framework/presence/src/cjs/package.json index 4436120b395c..95a877235f93 100644 --- a/packages/framework/presence/src/cjs/package.json +++ b/packages/framework/presence/src/cjs/package.json @@ -8,7 +8,6 @@ }, "./internal/container-definitions/internal": "./container-definitions/index.js", "./internal/core-interfaces": "./core-interfaces/index.js", - "./internal/events": "./events/events.js", "./internal/exposedInternalTypes": "./exposedInternalTypes.js", "./internal/exposedUtilityTypes": "./exposedUtilityTypes.js" } diff --git a/packages/framework/presence/src/events/events.ts b/packages/framework/presence/src/events/events.ts deleted file mode 100644 index b427b50c13c6..000000000000 --- a/packages/framework/presence/src/events/events.ts +++ /dev/null @@ -1,329 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * This file is a clone of the `events.ts` file from the `@fluidframework/tree` package. - * `@public` APIs have been changed to `@alpha` and some lint defects from more strict rules - * have been fixed or suppressed. - */ - -import type { IEvent } from "@fluidframework/core-interfaces"; -import { assert } from "@fluidframework/core-utils/internal"; - -function fail(message: string): never { - throw new Error(message); -} - -/** - * Retrieve a value from a map with the given key, or create a new entry if the key is not in the map. - * @param map - The map to query/update - * @param key - The key to lookup in the map - * @param defaultValue - a function which returns a default value. This is called and used to set an initial value for the given key in the map if none exists - * @returns either the existing value for the given key, or the newly-created value (the result of `defaultValue`) - */ -function getOrCreate(map: Map, key: K, defaultValue: (key: K) => V): V { - let value = map.get(key); - if (value === undefined) { - value = defaultValue(key); - map.set(key, value); - } - return value; -} - -/** - * Convert a union of types to an intersection of those types. Useful for `TransformEvents`. - */ -export type UnionToIntersection = (T extends any ? (k: T) => unknown : never) extends ( - k: infer U, -) => unknown - ? U - : never; - -/** - * `true` iff the given type is an acceptable shape for an event - * @alpha - */ -export type IsEvent = Event extends (...args: any[]) => any ? true : false; - -/** - * Used to specify the kinds of events emitted by an {@link ISubscribable}. - * - * @remarks - * - * Any object type is a valid {@link Events}, but only the event-like properties of that - * type will be included. - * - * @example - * - * ```typescript - * interface MyEvents { - * load: (user: string, data: IUserData) => void; - * error: (errorCode: number) => void; - * } - * ``` - * - * @alpha - */ -export type Events = { - [P in (string | symbol) & keyof E as IsEvent extends true ? P : never]: E[P]; -}; - -/** - * Converts an `Events` type (i.e. the event registry for an {@link ISubscribable}) into a type consumable - * by an IEventProvider from `@fluidframework/core-interfaces`. - * @param E - the `Events` type to transform - * @param Target - an optional `IEvent` type that will be merged into the result along with the transformed `E` - * - * @example - * - * ```typescript - * interface MyEvents { - * load: (user: string, data: IUserData) => void; - * error: (errorCode: number) => void; - * } - * - * class MySharedObject extends SharedObject> { - * // ... - * } - * ``` - */ -export type TransformEvents, Target extends IEvent = IEvent> = { - [P in keyof Events]: (event: P, listener: E[P]) => void; -} extends Record - ? UnionToIntersection & Target - : never; - -/** - * An object which allows the registration of listeners so that subscribers can be notified when an event happens. - * - * `EventEmitter` can be used as a base class to implement this via extension. - * @param E - All the events that this emitter supports - * @example - * ```ts - * type MyEventEmitter = IEventEmitter<{ - * load: (user: string, data: IUserData) => void; - * error: (errorCode: number) => void; - * }> - * ``` - * @privateRemarks - * {@link createEmitter} can help implement this interface via delegation. - * - * @alpha - */ -export interface ISubscribable> { - /** - * Register an event listener. - * @param eventName - the name of the event - * @param listener - the handler to run when the event is fired by the emitter - * @returns a function which will deregister the listener when run. This function has undefined behavior - * if called more than once. - */ - on>(eventName: K, listener: E[K]): () => void; -} - -/** - * Interface for an event emitter that can emit typed events to subscribed listeners. - * @internal - */ -export interface IEmitter> { - /** - * Emits an event with the specified name and arguments, notifying all subscribers by calling their registered listener functions. - * @param eventName - the name of the event to fire - * @param args - the arguments passed to the event listener functions - */ - emit>(eventName: K, ...args: Parameters): void; - - /** - * Emits an event with the specified name and arguments, notifying all subscribers by calling their registered listener functions. - * It also collects the return values of all listeners into an array. - * - * Warning: This method should be used with caution. It deviates from the standard event-based integration pattern as creates substantial coupling between the emitter and its listeners. - * For the majority of use-cases it is recommended to use the standard {@link IEmitter.emit} functionality. - * @param eventName - the name of the event to fire - * @param args - the arguments passed to the event listener functions - * @returns An array of the return values of each listener, preserving the order listeners were called. - */ - emitAndCollect>( - eventName: K, - ...args: Parameters - ): ReturnType[]; -} - -/** - * Create an {@link ISubscribable} that can be instructed to emit events via the {@link IEmitter} interface. - * - * A class can delegate handling {@link ISubscribable} to the returned value while using it to emit the events. - * See also `EventEmitter` which be used as a base class to implement {@link ISubscribable} via extension. - * @internal - */ -export function createEmitter>( - noListeners?: NoListenersCallback, -): ISubscribable & IEmitter & HasListeners { - return new ComposableEventEmitter(noListeners); -} - -/** - * Called when the last listener for `eventName` is removed. - * Useful for determining when to clean up resources related to detecting when the event might occurs. - * @internal - */ -export type NoListenersCallback> = (eventName: keyof Events) => void; - -/** - * @internal - */ -export interface HasListeners> { - /** - * When no `eventName` is provided, returns true iff there are any listeners. - * - * When `eventName` is provided, returns true iff there are listeners for that event. - * - * @remarks - * This can be used to know when its safe to cleanup data-structures which only exist to fire events for their listeners. - */ - hasListeners(eventName?: keyof Events): boolean; -} - -/** - * Provides an API for subscribing to and listening to events. - * - * @remarks Classes wishing to emit events may either extend this class or compose over it. - * - * @example Extending this class - * - * ```typescript - * interface MyEvents { - * "loaded": () => void; - * } - * - * class MyClass extends EventEmitter { - * private load() { - * this.emit("loaded"); - * } - * } - * ``` - * - * @example Composing over this class - * - * ```typescript - * class MyClass implements ISubscribable { - * private readonly events = EventEmitter.create(); - * - * private load() { - * this.events.emit("loaded"); - * } - * - * public on(eventName: K, listener: MyEvents[K]): () => void { - * return this.events.on(eventName, listener); - * } - * } - * ``` - */ -export class EventEmitter> implements ISubscribable, HasListeners { - // TODO: because the inner data-structure here is a set, adding the same callback twice does not error, - // but only calls it once, and unsubscribing will stop calling it all together. - // This is surprising since it makes subscribing and unsubscribing not inverses (but instead both idempotent). - // This might be desired, but if so the documentation should indicate it. - private readonly listeners = new Map any>>(); - - // Because this is protected and not public, calling this externally (not from a subclass) makes sending events to the constructed instance impossible. - // Instead, use the static `create` function to get an instance which allows emitting events. - protected constructor(private readonly noListeners?: NoListenersCallback) {} - - protected emit>(eventName: K, ...args: Parameters): void { - const listeners = this.listeners.get(eventName); - if (listeners !== undefined) { - const argArray: unknown[] = args; // TODO: Current TS (4.5.5) cannot spread `args` into `listener()`, but future versions (e.g. 4.8.4) can. - // This explicitly copies listeners so that new listeners added during this call to emit will not receive this event. - for (const listener of listeners) { - // If listener has been unsubscribed while invoking other listeners, skip it. - if (listeners.has(listener)) { - listener(...argArray); - } - } - } - } - - protected emitAndCollect>( - eventName: K, - ...args: Parameters - ): ReturnType[] { - const listeners = this.listeners.get(eventName); - if (listeners !== undefined) { - const argArray: unknown[] = args; - const resultArray: ReturnType[] = []; - for (const listener of listeners.values()) { - // listner return type is any to enable this.listeners to be a Map - // of Sets rather than a Record with tracked (known) return types. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - resultArray.push(listener(...argArray)); - } - return resultArray; - } - return []; - } - - /** - * Register an event listener. - * @param eventName - the name of the event - * @param listener - the handler to run when the event is fired by the emitter - * @returns a function which will deregister the listener when run. - * This function will error if called more than once. - * @privateRemarks - * TODO: - * invoking the returned callback can error even if its only called once if the same listener was provided to two calls to "on". - * This behavior is not documented and its unclear if its a bug or not: see note on listeners. - */ - public on>(eventName: K, listener: E[K]): () => void { - getOrCreate(this.listeners, eventName, () => new Set()).add(listener); - return () => this.off(eventName, listener); - } - - private off>(eventName: K, listener: E[K]): void { - const listeners = - this.listeners.get(eventName) ?? - // TODO: consider making this (and assert below) a usage error since it can be triggered by users of the public API: maybe separate those use cases somehow? - fail("Event has no listeners. Event deregistration functions may only be invoked once."); - assert( - listeners.delete(listener), - 0xa30 /* Listener does not exist. Event deregistration functions may only be invoked once. */, - ); - if (listeners.size === 0) { - this.listeners.delete(eventName); - this.noListeners?.(eventName); - } - } - - public hasListeners(eventName?: keyof Events): boolean { - if (eventName === undefined) { - return this.listeners.size > 0; - } - return this.listeners.has(eventName); - } -} - -// This class exposes the constructor and the `emit` method of `EventEmitter`, elevating them from protected to public -class ComposableEventEmitter> - extends EventEmitter - implements IEmitter -{ - public constructor(noListeners?: NoListenersCallback) { - super(noListeners); - } - - public override emit>( - eventName: K, - ...args: Parameters - ): void { - return super.emit(eventName, ...args); - } - - public override emitAndCollect>( - eventName: K, - ...args: Parameters - ): ReturnType[] { - return super.emitAndCollect(eventName, ...args); - } -} diff --git a/packages/framework/presence/src/exposedUtilityTypes.ts b/packages/framework/presence/src/exposedUtilityTypes.ts index 67eeff1e4dda..66dbfe3a8b74 100644 --- a/packages/framework/presence/src/exposedUtilityTypes.ts +++ b/packages/framework/presence/src/exposedUtilityTypes.ts @@ -32,7 +32,7 @@ export namespace InternalUtilityTypes { * * @system */ - export type IsNotificationEvent = Event extends (...args: infer P) => void + export type IsNotificationListener = Event extends (...args: infer P) => void ? CoreInternalUtilityTypes.IfSameType< P, JsonSerializable

& JsonDeserialized

, @@ -42,11 +42,11 @@ export namespace InternalUtilityTypes { : false; /** - * Used to specify the kinds of notifications emitted by a {@link NotificationSubscribable}. + * Used to specify the kinds of notifications emitted by a {@link NotificationListenable}. * * @remarks * - * Any object type is a valid NotificationEvents, but only the notification-like + * Any object type is a valid NotificationListeners, but only the notification-like * properties of that type will be included. * * @example @@ -60,8 +60,8 @@ export namespace InternalUtilityTypes { * * @system */ - export type NotificationEvents = { - [P in string & keyof E as IsNotificationEvent extends true ? P : never]: E[P]; + export type NotificationListeners = { + [P in string & keyof E as IsNotificationListener extends true ? P : never]: E[P]; }; /** diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 8b0bd528e88d..c5b225fbc89a 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -18,13 +18,6 @@ // JsonSerializable, // } from "@fluidframework/presence/internal/core-interfaces"; -// If desired these are the "required" types from events. -// export type { -// Events, -// IsEvent, -// ISubscribable, -// } from "@fluidframework/presence/internal/events"; - export type { ClientConnectionId } from "./baseTypes.js"; export type { @@ -80,7 +73,7 @@ export type { export { type NotificationEmitter, - type NotificationSubscribable, + type NotificationListenable, type NotificationSubscriptions, Notifications, type NotificationsManager, diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 7851e268172f..13c5992c4aba 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. */ +import { createEmitter } from "@fluid-internal/client-utils"; +import type { Listenable } from "@fluidframework/core-interfaces"; + import type { BroadcastControls, BroadcastControlSettings } from "./broadcastControls.js"; import { OptionalBroadcastControl } from "./broadcastControls.js"; import type { ValueManager } from "./internalTypes.js"; @@ -19,8 +22,6 @@ import type { JsonDeserialized, JsonSerializable, } from "@fluidframework/presence/internal/core-interfaces"; -import type { ISubscribable } from "@fluidframework/presence/internal/events"; -import { createEmitter } from "@fluidframework/presence/internal/events"; import type { InternalTypes } from "@fluidframework/presence/internal/exposedInternalTypes"; import type { InternalUtilityTypes } from "@fluidframework/presence/internal/exposedUtilityTypes"; @@ -280,7 +281,7 @@ export interface LatestMapValueManager>; + readonly events: Listenable>; /** * Controls for management of sending updates. diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 4a5cd1bb5c7e..6d2c73fbe94d 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. */ +import { createEmitter } from "@fluid-internal/client-utils"; +import type { Listenable } from "@fluidframework/core-interfaces"; + import type { BroadcastControls, BroadcastControlSettings } from "./broadcastControls.js"; import { OptionalBroadcastControl } from "./broadcastControls.js"; import type { ValueManager } from "./internalTypes.js"; @@ -16,8 +19,6 @@ import type { JsonDeserialized, JsonSerializable, } from "@fluidframework/presence/internal/core-interfaces"; -import type { ISubscribable } from "@fluidframework/presence/internal/events"; -import { createEmitter } from "@fluidframework/presence/internal/events"; import type { InternalTypes } from "@fluidframework/presence/internal/exposedInternalTypes"; import type { InternalUtilityTypes } from "@fluidframework/presence/internal/exposedUtilityTypes"; @@ -47,7 +48,7 @@ export interface LatestValueManager { /** * Events for Latest value manager. */ - readonly events: ISubscribable>; + readonly events: Listenable>; /** * Controls for management of sending updates. diff --git a/packages/framework/presence/src/notificationsManager.ts b/packages/framework/presence/src/notificationsManager.ts index dd00c0cc423a..b1cb84744eec 100644 --- a/packages/framework/presence/src/notificationsManager.ts +++ b/packages/framework/presence/src/notificationsManager.ts @@ -3,14 +3,15 @@ * Licensed under the MIT License. */ +import { createEmitter } from "@fluid-internal/client-utils"; +import type { Listeners, Listenable, Off } from "@fluidframework/core-interfaces"; + import type { ValueManager } from "./internalTypes.js"; import type { ISessionClient } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; import type { JsonTypeWith } from "@fluidframework/presence/internal/core-interfaces"; -import type { Events, ISubscribable } from "@fluidframework/presence/internal/events"; -import { createEmitter } from "@fluidframework/presence/internal/events"; import type { InternalTypes } from "@fluidframework/presence/internal/exposedInternalTypes"; import type { InternalUtilityTypes } from "@fluidframework/presence/internal/exposedUtilityTypes"; @@ -38,23 +39,43 @@ export interface NotificationsManagerEvents { * @sealed * @alpha */ -export interface NotificationSubscribable< - E extends InternalUtilityTypes.NotificationEvents, +export interface NotificationListenable< + TListeners extends InternalUtilityTypes.NotificationListeners, > { /** * Register a notification listener. * @param notificationName - the name of the notification - * @param listener - the handler to run when the notification is received from other client - * @returns a function which will deregister the listener when run. This function - * has undefined behavior if called more than once. + * @param listener - The listener function to run when the notification is fired. + * @returns A {@link @fluidframework/core-interfaces#Off | function} which will deregister the listener when called. + * Calling the deregistration function more than once will have no effect. + * + * Listeners may also be deregistered by passing the listener to {@link NotificationListenable.off | off()}. + * @remarks Registering the exact same `listener` object for the same notification more than once will throw an error. + * If registering the same listener for the same notification multiple times is desired, consider using a wrapper function for the second subscription. + */ + on>( + notificationName: K, + listener: ( + sender: ISessionClient, + ...args: InternalUtilityTypes.JsonDeserializedParameters + ) => void, + ): Off; + + /** + * Deregister notification listener. + * @param notificationName - The name of the notification. + * @param listener - The listener function to remove from the current set of notification listeners. + * @remarks If `listener` is not currently registered, this method will have no effect. + * + * Listeners may also be deregistered by calling the {@link @fluidframework/core-interfaces#Off | deregistration function} returned when they are {@link NotificationListenable.on | registered}. */ - on>( + off>( notificationName: K, listener: ( sender: ISessionClient, - ...args: InternalUtilityTypes.JsonDeserializedParameters + ...args: InternalUtilityTypes.JsonDeserializedParameters ) => void, - ): () => void; + ): void; } /** @@ -63,8 +84,10 @@ export interface NotificationSubscribable< * @sealed * @alpha */ -export type NotificationSubscriptions> = { - [K in string & keyof InternalUtilityTypes.NotificationEvents]: ( +export type NotificationSubscriptions< + E extends InternalUtilityTypes.NotificationListeners, +> = { + [K in string & keyof InternalUtilityTypes.NotificationListeners]: ( sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters ) => void; @@ -76,13 +99,13 @@ export type NotificationSubscriptions> { +export interface NotificationEmitter> { /** * Emits a notification with the specified name and arguments, notifying all clients. * @param notificationName - the name of the notification to fire * @param args - the arguments sent with the notification */ - broadcast>( + broadcast>( notificationName: K, ...args: Parameters ): void; @@ -93,7 +116,7 @@ export interface NotificationEmitter>( + unicast>( notificationName: K, targetClient: ISessionClient, ...args: Parameters @@ -109,11 +132,13 @@ export interface NotificationEmitter> { +export interface NotificationsManager< + T extends InternalUtilityTypes.NotificationListeners, +> { /** * Events for Notifications manager. */ - readonly events: ISubscribable; + readonly events: Listenable; /** * Send notifications to other clients. @@ -123,7 +148,7 @@ export interface NotificationsManager; + readonly notifications: NotificationListenable; } /** @@ -133,7 +158,7 @@ export interface NotificationsManager(o: Partial>) => K[]; class NotificationsManagerImpl< - T extends InternalUtilityTypes.NotificationEvents, + T extends InternalUtilityTypes.NotificationListeners, Key extends string, > implements NotificationsManager, @@ -174,12 +199,10 @@ class NotificationsManagerImpl< }; // Workaround for types - private readonly notificationsInternal = - // @ts-expect-error TODO - createEmitter>(); + private readonly notificationsInternal = createEmitter>(); // @ts-expect-error TODO - public readonly notifications: NotificationSubscribable = this.notificationsInternal; + public readonly notifications: NotificationListenable = this.notificationsInternal; public constructor( private readonly key: Key, @@ -193,7 +216,7 @@ class NotificationsManagerImpl< for (const subscriptionName of recordKeys(initialSubscriptions)) { // Lingering Event typing issues with Notifications specialization requires // this cast. The only thing that really matters is that name is a string. - const name = subscriptionName as keyof Events>; + const name = subscriptionName as keyof Listeners>; const value = initialSubscriptions[subscriptionName]; // This check should not be needed while using exactOptionalPropertyTypes, but // typescript appears to ignore that with Partial<>. Good to be defensive @@ -209,7 +232,7 @@ class NotificationsManagerImpl< _received: number, value: InternalTypes.ValueRequiredState, ): void { - const eventName = value.value.name as keyof Events>; + const eventName = value.value.name as keyof Listeners>; if (this.notificationsInternal.hasListeners(eventName)) { // Without schema validation, we don't know that the args are the correct type. // For now we assume the user is sending the correct types and there is no corruption along the way. @@ -238,7 +261,7 @@ class NotificationsManagerImpl< * @alpha */ export function Notifications< - T extends InternalUtilityTypes.NotificationEvents, + T extends InternalUtilityTypes.NotificationListeners, Key extends string = string, >( initialSubscriptions: Partial>, diff --git a/packages/framework/presence/src/presence.ts b/packages/framework/presence/src/presence.ts index 27a9b33569ec..50037481c066 100644 --- a/packages/framework/presence/src/presence.ts +++ b/packages/framework/presence/src/presence.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import type { Listenable } from "@fluidframework/core-interfaces"; import type { SessionId } from "@fluidframework/id-compressor"; import type { ClientConnectionId } from "./baseTypes.js"; @@ -15,8 +16,6 @@ import type { PresenceWorkspaceAddress, } from "./types.js"; -import type { ISubscribable } from "@fluidframework/presence/internal/events"; - /** * A Fluid client session identifier. * @@ -166,7 +165,7 @@ export interface IPresence { /** * Events for Notifications manager. */ - readonly events: ISubscribable; + readonly events: Listenable; /** * Get all attendees in the session. diff --git a/packages/framework/presence/src/presenceManager.ts b/packages/framework/presence/src/presenceManager.ts index 9125dde82739..fb6b449fccfa 100644 --- a/packages/framework/presence/src/presenceManager.ts +++ b/packages/framework/presence/src/presenceManager.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. */ +import { createEmitter } from "@fluid-internal/client-utils"; +import type { IEmitter } from "@fluidframework/core-interfaces/internal"; import { createSessionId } from "@fluidframework/id-compressor/internal"; import type { ITelemetryLoggerExt, @@ -33,8 +35,6 @@ import type { IContainerExtension, IExtensionMessage, } from "@fluidframework/presence/internal/container-definitions/internal"; -import type { IEmitter } from "@fluidframework/presence/internal/events"; -import { createEmitter } from "@fluidframework/presence/internal/events"; /** * Portion of the container extension requirements ({@link IContainerExtension}) that are delegated to presence manager. diff --git a/packages/framework/presence/src/systemWorkspace.ts b/packages/framework/presence/src/systemWorkspace.ts index dfc6f983be7b..86d975cbecd9 100644 --- a/packages/framework/presence/src/systemWorkspace.ts +++ b/packages/framework/presence/src/systemWorkspace.ts @@ -4,6 +4,7 @@ */ import type { IAudience } from "@fluidframework/container-definitions"; +import type { IEmitter } from "@fluidframework/core-interfaces/internal"; import { assert } from "@fluidframework/core-utils/internal"; import type { ClientConnectionId } from "./baseTypes.js"; @@ -18,8 +19,6 @@ import { SessionClientStatus } from "./presence.js"; import type { PresenceStatesInternal } from "./presenceStates.js"; import type { PresenceStates, PresenceStatesSchema } from "./types.js"; -import type { IEmitter } from "@fluidframework/presence/internal/events"; - /** * The system workspace's datastore structure. * diff --git a/packages/framework/presence/src/test/notificationsManager.spec.ts b/packages/framework/presence/src/test/notificationsManager.spec.ts index 7c66ff521007..1789143cc95d 100644 --- a/packages/framework/presence/src/test/notificationsManager.spec.ts +++ b/packages/framework/presence/src/test/notificationsManager.spec.ts @@ -346,7 +346,7 @@ describe("Presence", () => { assert(unattendedEventCalled, "unattendedEvent not called"); }); - it.skip("raises `unattendedEvent` event when recognized notification is received without listeners", async () => { + it("raises `unattendedEvent` event when recognized notification is received without listeners", async () => { let unattendedEventCalled = false; function newIdEventHandler(client: ISessionClient, id: number): void { @@ -376,8 +376,7 @@ describe("Presence", () => { unattendedEventCalled = true; }); - // TODO: Internal Event implementation needs updated. See https://github.com/microsoft/FluidFramework/pull/23046. - // testEvents.notifications.off("newId", newIdEventHandler); + testEvents.notifications.off("newId", newIdEventHandler); // Processing this signal should trigger the testEvents.newId event listeners presence.processSignal( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 828dd251d7be..12fe1b7c343c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11440,6 +11440,9 @@ importers: packages/framework/presence: dependencies: + '@fluid-internal/client-utils': + specifier: workspace:~ + version: link:../../common/client-utils '@fluidframework/container-definitions': specifier: workspace:~ version: link:../../common/container-definitions From abde76d8decbaf2cde8aac68b3fa061a0fe75d92 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Tue, 26 Nov 2024 13:24:26 -0800 Subject: [PATCH 32/40] feat(presence): Add support for signal batching (#23075) ## Summary of changes Presence updates are grouped together and throttled to prevent flooding the network with messages when presence values are rapidly updated. This means the presence infrastructure will not immediately broadcast updates but will broadcast them after a configurable delay. The `allowableUpdateLatencyMs` property configures how long a local update may be delayed under normal circumstances, enabling batching with other updates. The default `allowableUpdateLatencyMs` is **60 milliseconds** but may be (1) specified during configuration of a States Workspace or Value Manager and/or (2) updated later using the `controls` member of Workspace or Value Manager. States Workspace configuration applies when a Value Manager does not have its own setting. Notifications are never queued; they effectively always have an `allowableUpdateLatencyMs` of 0. However, they may be batched with other updates that were already queued. Note that due to throttling, clients receiving updates may not see updates for all values set by another. For example, with `Latest*ValueManagers`, the only value sent is the value at the time the outgoing batched message is sent. Previous values set by the client will not be broadcast or seen by other clients. ### Example You can configure the batching and throttling behavior using the `allowableUpdateLatencyMs` property as in the following example: ```ts // Configure a states workspace const stateWorkspace = presence.getStates("app:v1states", { // This value manager has an allowable latency of 100ms. position: Latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }), // This value manager uses the workspace default. count: Latest({ num: 0 }), }, // Specify the default for all value managers in this workspace to 200ms, // overriding the default value of 60ms. { allowableUpdateLatencyMs: 200 } ); // Temporarily set count updates to send as soon as possible const countState = stateWorkspace.props.count; countState.controls.allowableUpdateLatencyMs = 0; countState.local = { num: 5000 }; // Reset the update latency to the workspace default countState.controls.allowableUpdateLatencyMs = undefined; ``` ## Test cases 1. Signals send immediately when allowable latency is 0 1. Signals are batched when they are received within the send deadline (send deadline is set to now + allowable latency iff a new message is queued; i.e. a message was not previously queued - this is the first message to be put in the queue) 1. Signals from LVMs with different allowable latencies are batched correctly 1. Queued signal is sent with immediate signal (i.e. if a signal is queued and an immediate message comes in, it should merge and send immediately) 1. Multiple workspaces; messages from multiple workspaces should be batched and queued 1. Include sending notifications - they should be sent immediately The test cases themselves include inline comments with the time and expected deadline for each operation. Signals are also numbered inline to make it easier to match up with the expected signals. ## Known gaps LatestMapValueManager is not tested. --------- Co-authored-by: Jason Hartman --- .changeset/shy-files-jam.md | 45 + packages/framework/presence/README.md | 39 +- .../presence/src/presenceDatastoreManager.ts | 159 +++- .../framework/presence/src/presenceStates.ts | 7 +- .../presence/src/test/batching.spec.ts | 898 ++++++++++++++++++ .../presence/src/test/timerManager.spec.ts | 116 +++ .../framework/presence/src/timerManager.ts | 68 ++ 7 files changed, 1307 insertions(+), 25 deletions(-) create mode 100644 .changeset/shy-files-jam.md create mode 100644 packages/framework/presence/src/test/batching.spec.ts create mode 100644 packages/framework/presence/src/test/timerManager.spec.ts create mode 100644 packages/framework/presence/src/timerManager.ts diff --git a/.changeset/shy-files-jam.md b/.changeset/shy-files-jam.md new file mode 100644 index 000000000000..eabca628460e --- /dev/null +++ b/.changeset/shy-files-jam.md @@ -0,0 +1,45 @@ +--- +"@fluidframework/presence": minor +--- +--- +"section": feature +--- + +Presence updates are now batched and throttled + +Presence updates are grouped together and throttled to prevent flooding the network with messages when presence values are rapidly updated. This means the presence infrastructure will not immediately broadcast updates but will broadcast them after a configurable delay. + +The `allowableUpdateLatencyMs` property configures how long a local update may be delayed under normal circumstances, enabling batching with other updates. The default `allowableUpdateLatencyMs` is **60 milliseconds** but may be (1) specified during configuration of a [States Workspace](https://github.com/microsoft/FluidFramework/tree/main/packages/framework/presence#value-managers#states-workspace) or [Value Manager](https://github.com/microsoft/FluidFramework/tree/main/packages/framework/presence#value-managers#value-managers) and/or (2) updated later using the `controls` member of Workspace or Value Manager. [States Workspace](https://github.com/microsoft/FluidFramework/tree/main/packages/framework/presence#value-managers#states-workspace) configuration applies when a Value Manager does not have its own setting. + +Notifications are never queued; they effectively always have an `allowableUpdateLatencyMs` of 0. However, they may be batched with other updates that were already queued. + +Note that due to throttling, clients receiving updates may not see updates for all values set by another. For example, +with `Latest*ValueManagers`, the only value sent is the value at the time the outgoing batched message is sent. Previous +values set by the client will not be broadcast or seen by other clients. + +#### Example + +You can configure the batching and throttling behavior using the `allowableUpdateLatencyMs` property as in the following example: + +```ts +// Configure a states workspace +const stateWorkspace = presence.getStates("app:v1states", + { + // This value manager has an allowable latency of 100ms. + position: Latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }), + // This value manager uses the workspace default. + count: Latest({ num: 0 }), + }, + // Specify the default for all value managers in this workspace to 200ms, + // overriding the default value of 60ms. + { allowableUpdateLatencyMs: 200 } +); + +// Temporarily set count updates to send as soon as possible +const countState = stateWorkspace.props.count; +countState.controls.allowableUpdateLatencyMs = 0; +countState.local = { num: 5000 }; + +// Reset the update latency to the workspace default +countState.controls.allowableUpdateLatencyMs = undefined; +``` diff --git a/packages/framework/presence/README.md b/packages/framework/presence/README.md index 5561b5b4946f..7c608267719e 100644 --- a/packages/framework/presence/README.md +++ b/packages/framework/presence/README.md @@ -128,9 +128,44 @@ Notifications API is partially implemented. All messages are always broadcast ev Notifications are fundamentally unreliable at this time as there are no built-in acknowledgements nor retained state. To prevent most common loss of notifications, always check for connection before sending. -### Throttling +### Throttling/batching -Throttling is not yet implemented. `BroadcastControls` exists in the API to provide control over throttling of value updates, but throttling is not yet implemented. It is recommended that `BroadcastControls.allowableUpdateLatencyMs` use is considered and specified to light up once support is added. +Presence updates are grouped together and throttled to prevent flooding the network with messages when presence values are rapidly updated. This means the presence infrastructure will not immediately broadcast updates but will broadcast them after a configurable delay. + +The `allowableUpdateLatencyMs` property configures how long a local update may be delayed under normal circumstances, enabling batching with other updates. The default `allowableUpdateLatencyMs` is **60 milliseconds** but may be (1) specified during configuration of a [States Workspace](#states-workspace) or [Value Manager](#value-managers) and/or (2) updated later using the `controls` member of Workspace or Value Manager. [States Workspace](#states-workspace) configuration applies when a Value Manager does not have its own setting. + +Notifications are never queued; they effectively always have an `allowableUpdateLatencyMs` of 0. However, they may be batched with other updates that were already queued. + +Note that due to throttling, clients receiving updates may not see updates for all values set by another. For example, +with `Latest*ValueManagers`, the only value sent is the value at the time the outgoing batched message is sent. Previous +values set by the client will not be broadcast or seen by other clients. + +#### Example + +You can configure the batching and throttling behavior using the `allowableUpdateLatencyMs` property as in the following example: + +```ts +// Configure a states workspace +const stateWorkspace = presence.getStates("app:v1states", + { + // This value manager has an allowable latency of 100ms. + position: Latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }), + // This value manager uses the workspace default. + count: Latest({ num: 0 }), + }, + // Specify the default for all value managers in this workspace to 200ms, + // overriding the default value of 60ms. + { allowableUpdateLatencyMs: 200 } +); + +// Temporarily set count updates to send as soon as possible +const countState = stateWorkspace.props.count; +countState.controls.allowableUpdateLatencyMs = 0; +countState.local = { num: 5000 }; + +// Reset the update latency to the workspace default +countState.controls.allowableUpdateLatencyMs = undefined; +``` diff --git a/packages/framework/presence/src/presenceDatastoreManager.ts b/packages/framework/presence/src/presenceDatastoreManager.ts index 5b07f9bc716a..e5140fb9e568 100644 --- a/packages/framework/presence/src/presenceDatastoreManager.ts +++ b/packages/framework/presence/src/presenceDatastoreManager.ts @@ -9,6 +9,7 @@ import type { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/intern import type { ClientConnectionId } from "./baseTypes.js"; import type { BroadcastControlSettings } from "./broadcastControls.js"; +import { brandedObjectEntries } from "./internalTypes.js"; import type { IEphemeralRuntime } from "./internalTypes.js"; import type { ClientSessionId, ISessionClient } from "./presence.js"; import type { @@ -17,8 +18,13 @@ import type { PresenceStatesInternal, ValueElementMap, } from "./presenceStates.js"; -import { createPresenceStates, mergeUntrackedDatastore } from "./presenceStates.js"; +import { + createPresenceStates, + mergeUntrackedDatastore, + mergeValueDirectory, +} from "./presenceStates.js"; import type { SystemWorkspaceDatastore } from "./systemWorkspace.js"; +import { TimerManager } from "./timerManager.js"; import type { PresenceStates, PresenceStatesSchema, @@ -93,6 +99,43 @@ export interface PresenceDatastoreManager { processSignal(message: IExtensionMessage, local: boolean): void; } +function mergeGeneralDatastoreMessageContent( + base: GeneralDatastoreMessageContent | undefined, + newData: GeneralDatastoreMessageContent, +): GeneralDatastoreMessageContent { + // This function-local "datastore" will hold the merged message data. + const queueDatastore = base ?? {}; + + // Merge the current data with the existing data, if any exists. + // Iterate over the current message data; individual items are workspaces. + for (const [workspaceName, workspaceData] of Object.entries(newData)) { + // Initialize the merged data as the queued datastore entry for the workspace. + // Since the key might not exist, create an empty object in that case. It will + // be set explicitly after the loop. + const mergedData = queueDatastore[workspaceName] ?? {}; + + // Iterate over each value manager and its data, merging it as needed. + for (const valueManagerKey of Object.keys(workspaceData)) { + for (const [clientSessionId, value] of brandedObjectEntries( + workspaceData[valueManagerKey], + )) { + mergedData[valueManagerKey] ??= {}; + const oldData = mergedData[valueManagerKey][clientSessionId]; + mergedData[valueManagerKey][clientSessionId] = mergeValueDirectory( + oldData, + value, + 0, // local values do not need a time shift + ); + } + } + + // Store the merged data in the function-local queue workspace. The whole contents of this + // datastore will be sent as the message data. + queueDatastore[workspaceName] = mergedData; + } + return queueDatastore; +} + /** * Manages singleton datastore for all Presence. */ @@ -101,7 +144,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { private averageLatency = 0; private returnedMessages = 0; private refreshBroadcastRequested = false; - + private readonly timer = new TimerManager(); private readonly workspaces = new Map< string, PresenceWorkspaceEntry @@ -162,27 +205,17 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { return; } - const clientConnectionId = this.runtime.clientId; - assert(clientConnectionId !== undefined, 0xa59 /* Client connected without clientId */); - const currentClientToSessionValueState = - this.datastore["system:presence"].clientToSessionId[clientConnectionId]; - const updates: GeneralDatastoreMessageContent[InternalWorkspaceAddress] = {}; for (const [key, value] of Object.entries(states)) { updates[key] = { [this.clientSessionId]: value }; } - this.localUpdate({ - // Always send current connection mapping for some resiliency against - // lost signals. This ensures that client session id found in `updates` - // (which is this client's client session id) is always represented in - // system workspace of recipient clients. - "system:presence": { - clientToSessionId: { - [clientConnectionId]: { ...currentClientToSessionValueState }, - }, + + this.enqueueMessage( + { + [internalWorkspaceAddress]: updates, }, - [internalWorkspaceAddress]: updates, - }); + options, + ); }; const entry = createPresenceStates( @@ -200,14 +233,96 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { return entry.public; } - private localUpdate(data: DatastoreMessageContent): void { - const content = { + /** + * The combined contents of all queued updates. Will be undefined when no messages are queued. + */ + private queuedData: GeneralDatastoreMessageContent | undefined; + + /** + * Enqueues a new message to be sent. The message may be queued or may be sent immediately depending on the state of + * the send timer, other messages in the queue, the configured allowed latency, etc. + */ + private enqueueMessage( + data: GeneralDatastoreMessageContent, + options: RuntimeLocalUpdateOptions, + ): void { + // Merging the message with any queued messages effectively queues the message. + // It is OK to queue all incoming messages as long as when we send, we send the queued data. + this.queuedData = mergeGeneralDatastoreMessageContent(this.queuedData, data); + + const { allowableUpdateLatencyMs } = options; + const now = Date.now(); + const thisMessageDeadline = now + allowableUpdateLatencyMs; + + if ( + // If the timer has not expired, we can short-circuit because the timer will fire + // and cover this update. In other words, queuing this will be fast enough to + // meet its deadline, because a timer is already scheduled to fire before its deadline. + !this.timer.hasExpired() && + // If the deadline for this message is later than the overall send deadline, then + // we can exit early since a timer will take care of sending it. + thisMessageDeadline >= this.timer.expireTime + ) { + return; + } + + // Either we need to send this message immediately, or we need to schedule a timer + // to fire at the send deadline that will take care of it. + + // Note that timeoutInMs === allowableUpdateLatency, but the calculation is done this way for clarity. + const timeoutInMs = thisMessageDeadline - now; + const scheduleForLater = timeoutInMs > 0; + + if (scheduleForLater) { + // Schedule the queued messages to be sent at the updateDeadline + this.timer.setTimeout(this.sendQueuedMessage.bind(this), timeoutInMs); + } else { + this.sendQueuedMessage(); + } + } + + /** + * Send any queued signal immediately. Does nothing if no message is queued. + */ + private sendQueuedMessage(): void { + this.timer.clearTimeout(); + + if (this.queuedData === undefined) { + return; + } + + // Check for connectivity before sending updates. + if (!this.runtime.connected) { + // Clear the queued data since we're disconnected. We don't want messages + // to queue infinitely while disconnected. + this.queuedData = undefined; + return; + } + + const clientConnectionId = this.runtime.clientId; + assert(clientConnectionId !== undefined, 0xa59 /* Client connected without clientId */); + const currentClientToSessionValueState = + this.datastore["system:presence"].clientToSessionId[clientConnectionId]; + + const newMessage = { sendTimestamp: Date.now(), avgLatency: this.averageLatency, // isComplete: false, - data, + data: { + // Always send current connection mapping for some resiliency against + // lost signals. This ensures that client session id found in `updates` + // (which is this client's client session id) is always represented in + // system workspace of recipient clients. + "system:presence": { + clientToSessionId: { + [clientConnectionId]: { ...currentClientToSessionValueState }, + }, + }, + ...this.queuedData, + }, } satisfies DatastoreUpdateMessage["content"]; - this.runtime.submitSignal(datastoreUpdateMessageType, content); + this.queuedData = undefined; + this.runtime.submitSignal(datastoreUpdateMessageType, newMessage); } private broadcastAllKnownState(): void { diff --git a/packages/framework/presence/src/presenceStates.ts b/packages/framework/presence/src/presenceStates.ts index 4172a46bda01..d3e492d3e4fb 100644 --- a/packages/framework/presence/src/presenceStates.ts +++ b/packages/framework/presence/src/presenceStates.ts @@ -146,7 +146,12 @@ function isValueDirectory< return "items" in value; } -function mergeValueDirectory< +/** + * Merge a value directory. + * + * @internal + */ +export function mergeValueDirectory< T, TValueState extends | InternalTypes.ValueRequiredState diff --git a/packages/framework/presence/src/test/batching.spec.ts b/packages/framework/presence/src/test/batching.spec.ts new file mode 100644 index 000000000000..b45274e7d50d --- /dev/null +++ b/packages/framework/presence/src/test/batching.spec.ts @@ -0,0 +1,898 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { EventAndErrorTrackingLogger } from "@fluidframework/test-utils/internal"; +import { describe, it, after, afterEach, before, beforeEach } from "mocha"; +import { useFakeTimers, type SinonFakeTimers } from "sinon"; + +import { Latest, Notifications, type PresenceNotifications } from "../index.js"; +import type { createPresenceManager } from "../presenceManager.js"; + +import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; +import { assertFinalExpectations, prepareConnectedPresence } from "./testUtils.js"; + +describe("Presence", () => { + describe("batching", () => { + let runtime: MockEphemeralRuntime; + let logger: EventAndErrorTrackingLogger; + const initialTime = 1000; + let clock: SinonFakeTimers; + let presence: ReturnType; + + before(async () => { + clock = useFakeTimers(); + }); + + beforeEach(() => { + logger = new EventAndErrorTrackingLogger(); + runtime = new MockEphemeralRuntime(logger); + + // Note that while the initialTime is set to 1000, the prepareConnectedPresence call advances + // it to 1010 so all tests start at that time. + clock.setSystemTime(initialTime); + + // Set up the presence connection. + presence = prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); + }); + + afterEach(() => { + // Tick the clock forward by a large amount before resetting it + // in case there are lingering queued signals or timers + clock.tick(1000); + clock.reset(); + }); + + after(() => { + clock.restore(); + }); + + describe("LatestValueManager", () => { + it("sends signal immediately when allowable latency is 0", async () => { + runtime.signalsExpected.push( + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1010, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 0, + "timestamp": 1010, + "value": { + "num": 0, + }, + }, + }, + }, + }, + }, + ], + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1020, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 1, + "timestamp": 1020, + "value": { + "num": 42, + }, + }, + }, + }, + }, + }, + ], + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1020, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 2, + "timestamp": 1020, + "value": { + "num": 84, + }, + }, + }, + }, + }, + }, + ], + ); + + // Configure a state workspace + // SIGNAL #1 - intial data is sent immediately + const stateWorkspace = presence.getStates("name:testStateWorkspace", { + count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), + }); + + const { count } = stateWorkspace.props; + + clock.tick(10); // Time is now 1020 + + // SIGNAL #2 + count.local = { num: 42 }; + + // SIGNAL #3 + count.local = { num: 84 }; + + assertFinalExpectations(runtime, logger); + }); + + it("sets timer for default allowableUpdateLatency", async () => { + runtime.signalsExpected.push([ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1070, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 0, + "timestamp": 1010, + "value": { + "num": 0, + }, + }, + }, + }, + }, + }, + ]); + + // Configure a state workspace + presence.getStates("name:testStateWorkspace", { + count: Latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), + }); // will be queued; deadline is now 1070 + + // SIGNAL #1 + // The deadline timer will fire at time 1070 and send a single + // signal with the value from the last signal (num=0). + + clock.tick(100); // Time is now 1110 + }); + + it("batches signals sent within default allowableUpdateLatency", async () => { + runtime.signalsExpected.push( + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1070, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 3, + "timestamp": 1060, + "value": { + "num": 22, + }, + }, + }, + }, + }, + }, + ], + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1150, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 6, + "timestamp": 1140, + "value": { + "num": 90, + }, + }, + }, + }, + }, + }, + ], + ); + + // Configure a state workspace + const stateWorkspace = presence.getStates("name:testStateWorkspace", { + count: Latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), + }); // will be queued; deadline is now 1070 + + const { count } = stateWorkspace.props; + + clock.tick(10); // Time is now 1020 + count.local = { num: 12 }; // will be queued; deadline remains 1070 + + clock.tick(10); // Time is now 1030 + count.local = { num: 34 }; // will be queued; deadline remains 1070 + + clock.tick(30); // Time is now 1060 + count.local = { num: 22 }; // will be queued; deadline remains 1070 + + // SIGNAL #1 + // The deadline timer will fire at time 1070 and send a single + // signal with the value from the last signal (num=22). + + // It's necessary to tick the timer beyond the deadline so the timer will fire. + clock.tick(20); // Time is now 1080 + + clock.tick(10); // Time is now 1090 + count.local = { num: 56 }; // will be queued; deadline is set to 1150 + + clock.tick(40); // Time is now 1130 + count.local = { num: 78 }; // will be queued; deadline remains 1150 + + clock.tick(10); // Time is now 1140 + count.local = { num: 90 }; // will be queued; deadline remains 1150 + + // SIGNAL #2 + // The deadline timer will fire at time 1150 and send a single + // signal with the value from the last signal (num=90). + + // It's necessary to tick the timer beyond the deadline so the timer will fire. + clock.tick(30); // Time is now 1180 + }); + + it("batches signals sent within a specified allowableUpdateLatency", async () => { + runtime.signalsExpected.push( + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1110, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 2, + "timestamp": 1100, + "value": { + "num": 34, + }, + }, + }, + }, + }, + }, + ], + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1240, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 5, + "timestamp": 1220, + "value": { + "num": 90, + }, + }, + }, + }, + }, + }, + ], + ); + + // Configure a state workspace + const stateWorkspace = presence.getStates("name:testStateWorkspace", { + count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + }); + + const { count } = stateWorkspace.props; + + clock.tick(10); // Time is now 1020 + count.local = { num: 12 }; // will be queued; deadline is set to 1120 + + clock.tick(80); // Time is now 1100 + count.local = { num: 34 }; // will be queued; deadline remains 1120 + + // SIGNAL #1 + // The deadline timer will fire at time 1120 and send a single + // signal with the value from the last signal (num=34). + + // It's necessary to tick the timer beyond the deadline so the timer will fire. + clock.tick(30); // Time is now 1130 + + clock.tick(10); // Time is now 1140 + count.local = { num: 56 }; // will be queued; deadline is set to 1240 + + clock.tick(40); // Time is now 1180 + count.local = { num: 78 }; // will be queued; deadline remains 1240 + + clock.tick(40); // Time is now 1220 + count.local = { num: 90 }; // will be queued; deadline remains 1240 + + // SIGNAL #2 + // The deadline timer will fire at time 1240 and send a single + // signal with the value from the last signal (num=90). + + // It's necessary to tick the timer beyond the deadline so the timer will fire. + clock.tick(30); // Time is now 1250 + }); + + it("queued signal is sent immediately with immediate update message", async () => { + runtime.signalsExpected.push( + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1010, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 0, + "timestamp": 1010, + "value": { + "num": 0, + }, + }, + }, + "immediateUpdate": { + "sessionId-2": { + "rev": 0, + "timestamp": 1010, + "value": { + "num": 0, + }, + }, + }, + }, + }, + }, + ], + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1110, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 2, + "timestamp": 1100, + "value": { + "num": 34, + }, + }, + }, + "immediateUpdate": { + "sessionId-2": { + "rev": 1, + "timestamp": 1110, + "value": { + "num": 56, + }, + }, + }, + }, + }, + }, + ], + ); + + // Configure a state workspace + // SIGNAL #1 - this signal is not queued because it contains a value manager with a latency of 0, + // so the initial data will be sent immediately. + const stateWorkspace = presence.getStates("name:testStateWorkspace", { + count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + immediateUpdate: Latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), + }); + + const { count, immediateUpdate } = stateWorkspace.props; + + clock.tick(10); // Time is now 1020 + count.local = { num: 12 }; // will be queued; deadline is set to 1120 + + clock.tick(80); // Time is now 1100 + count.local = { num: 34 }; // will be queued; deadline remains 1120 + + clock.tick(10); // Time is now 1110 + + // SIGNAL #2 + // The following update should cause the queued signals to be merged with this immediately-sent + // signal with the value from the last signal (num=34). + immediateUpdate.local = { num: 56 }; + }); + + it("batches signals with different allowed latencies", async () => { + runtime.signalsExpected.push( + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1060, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 2, + "timestamp": 1050, + "value": { + "num": 34, + }, + }, + }, + "note": { + "sessionId-2": { + "rev": 1, + "timestamp": 1020, + "value": { + "message": "will be queued", + }, + }, + }, + }, + }, + }, + ], + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1110, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { "rev": 0, "timestamp": 1000, "value": "sessionId-2" }, + }, + }, + "s:name:testStateWorkspace": { + "note": { + "sessionId-2": { + "rev": 2, + "timestamp": 1060, + "value": { "message": "final message" }, + }, + }, + }, + }, + }, + ], + ); + + // Configure a state workspace + const stateWorkspace = presence.getStates("name:testStateWorkspace", { + count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + note: Latest({ message: "" }, { allowableUpdateLatencyMs: 50 }), + }); // will be queued, deadline is set to 1060 + + const { count, note } = stateWorkspace.props; + + clock.tick(10); // Time is now 1020 + note.local = { message: "will be queued" }; // will be queued, deadline remains 1060 + count.local = { num: 12 }; // will be queued; deadline remains 1060 + + clock.tick(30); // Time is now 1050 + count.local = { num: 34 }; // will be queued; deadline remains 1060 + + // SIGNAL #1 + // At time 1060, the deadline timer will fire and send a single signal with the value + // from both workspaces (num=34, message="will be queued"). + + clock.tick(10); // Time is now 1060 + note.local = { message: "final message" }; // will be queued; deadline is 1110 + + // SIGNAL #2 + // At time 1110, the deadline timer will fire and send a single signal with the value + // from the note workspace (message="final message"). + + // It's necessary to tick the timer beyond the deadline so the timer will fire. + clock.tick(100); // Time is now 1160 + }); + + it("batches signals from multiple workspaces", async () => { + runtime.signalsExpected.push([ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1070, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 2, + "timestamp": 1050, + "value": { + "num": 34, + }, + }, + }, + }, + "s:name:testStateWorkspace2": { + "note": { + "sessionId-2": { + "rev": 2, + "timestamp": 1060, + "value": { + "message": "final message", + }, + }, + }, + }, + }, + }, + ]); + + // Configure two state workspaces + const stateWorkspace = presence.getStates("name:testStateWorkspace", { + count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + }); // will be queued, deadline is 1110 + + const stateWorkspace2 = presence.getStates("name:testStateWorkspace2", { + note: Latest({ message: "" }, { allowableUpdateLatencyMs: 60 }), + }); // will be queued, deadline is 1070 + + const { count } = stateWorkspace.props; + const { note } = stateWorkspace2.props; + + clock.tick(10); // Time is now 1020 + note.local = { message: "will be queued" }; // will be queued, deadline is 1070 + count.local = { num: 12 }; // will be queued; deadline remains 1070 + + clock.tick(30); // Time is now 1050 + count.local = { num: 34 }; // will be queued; deadline remains 1070 + + clock.tick(10); // Time is now 1060 + note.local = { message: "final message" }; // will be queued; deadline remains 1070 + + // SIGNAL #1 + // The deadline timer will fire at time 1070 and send a single + // signal with the values from the most recent workspace updates (num=34, message="final message"). + + // It's necessary to tick the timer beyond the deadline so the timer will fire. + clock.tick(30); // Time is now 1090 + }); + }); + + describe("NotificationsManager", () => { + it("notification signals are sent immediately", async () => { + runtime.signalsExpected.push( + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1050, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { "rev": 0, "timestamp": 1000, "value": "sessionId-2" }, + }, + }, + "n:name:testNotificationWorkspace": { + "testEvents": { + "sessionId-2": { + "rev": 0, + "timestamp": 0, + "value": { "name": "newId", "args": [77] }, + "ignoreUnmonitored": true, + }, + }, + }, + }, + }, + ], + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1060, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { "rev": 0, "timestamp": 1000, "value": "sessionId-2" }, + }, + }, + "n:name:testNotificationWorkspace": { + "testEvents": { + "sessionId-2": { + "rev": 0, + "timestamp": 0, + "value": { "name": "newId", "args": [88] }, + "ignoreUnmonitored": true, + }, + }, + }, + }, + }, + ], + ); + + // Configure a notifications workspace + // eslint-disable-next-line @typescript-eslint/ban-types + const notificationsWorkspace: PresenceNotifications<{}> = presence.getNotifications( + "name:testNotificationWorkspace", + {}, + ); + + notificationsWorkspace.add( + "testEvents", + Notifications< + // Below explicit generic specification should not be required. + { + newId: (id: number) => void; + }, + "testEvents" + >( + // A default handler is not required + {}, + ), + ); + + const { testEvents } = notificationsWorkspace.props; + + clock.tick(40); // Time is now 1050 + + // SIGNAL #1 + testEvents.emit.broadcast("newId", 77); + + clock.tick(10); // Time is now 1060 + + // SIGNAL #2 + testEvents.emit.broadcast("newId", 88); + }); + + it("notification signals cause queued messages to be sent immediately", async () => { + runtime.signalsExpected.push( + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1060, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "s:name:testStateWorkspace": { + "count": { + "sessionId-2": { + "rev": 3, + "timestamp": 1040, + "value": { + "num": 56, + }, + }, + }, + }, + "n:name:testNotificationWorkspace": { + "testEvents": { + "sessionId-2": { + "rev": 0, + "timestamp": 0, + "value": { + "name": "newId", + "args": [99], + }, + "ignoreUnmonitored": true, + }, + }, + }, + }, + }, + ], + [ + "Pres:DatastoreUpdate", + { + "sendTimestamp": 1090, + "avgLatency": 10, + "data": { + "system:presence": { + "clientToSessionId": { + "client2": { + "rev": 0, + "timestamp": 1000, + "value": "sessionId-2", + }, + }, + }, + "n:name:testNotificationWorkspace": { + "testEvents": { + "sessionId-2": { + "rev": 0, + "timestamp": 0, + "value": { + "name": "newId", + "args": [111], + }, + "ignoreUnmonitored": true, + }, + }, + }, + }, + }, + ], + ); + + // Configure a state workspace + const stateWorkspace = presence.getStates("name:testStateWorkspace", { + count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + }); // will be queued, deadline is 1110 + + // eslint-disable-next-line @typescript-eslint/ban-types + const notificationsWorkspace: PresenceNotifications<{}> = presence.getNotifications( + "name:testNotificationWorkspace", + {}, + ); + + notificationsWorkspace.add( + "testEvents", + Notifications< + // Below explicit generic specification should not be required. + { + newId: (id: number) => void; + }, + "testEvents" + >( + // A default handler is not required + {}, + ), + ); + + const { count } = stateWorkspace.props; + const { testEvents } = notificationsWorkspace.props; + + testEvents.notifications.on("newId", (client, newId) => { + // do nothing + }); + + clock.tick(10); // Time is now 1020 + count.local = { num: 12 }; // will be queued, deadline remains 1110 + + clock.tick(10); // Time is now 1030 + count.local = { num: 34 }; // will be queued, deadline remains 1110 + + clock.tick(10); // Time is now 1040 + count.local = { num: 56 }; // will be queued, deadline remains 1110 + + clock.tick(20); // Time is now 1060 + + // SIGNAL #1 + // The notification below will cause an immediate broadcast of the queued signal + // along with the notification signal. + testEvents.emit.broadcast("newId", 99); + + clock.tick(30); // Time is now 1090 + + // SIGNAL #2 + // Immediate broadcast of the notification signal. + testEvents.emit.broadcast("newId", 111); + }); + }); + }); +}); diff --git a/packages/framework/presence/src/test/timerManager.spec.ts b/packages/framework/presence/src/test/timerManager.spec.ts new file mode 100644 index 000000000000..2bc19cea6fc2 --- /dev/null +++ b/packages/framework/presence/src/test/timerManager.spec.ts @@ -0,0 +1,116 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import assert from "node:assert"; + +import { describe, it, after, afterEach, before, beforeEach } from "mocha"; +import { useFakeTimers, type SinonFakeTimers, spy } from "sinon"; + +import { TimerManager } from "../timerManager.js"; + +describe("TimerManager", () => { + const initialTime = 1000; + let clock: SinonFakeTimers; + + before(async () => { + clock = useFakeTimers(); + }); + + beforeEach(() => { + clock.setSystemTime(initialTime); + }); + + afterEach(() => { + clock.reset(); + }); + + after(() => { + clock.restore(); + }); + + it("fires on time", () => { + const timer = new TimerManager(); + const handler = spy(() => assert.strictEqual(Date.now(), 1100)); + timer.setTimeout(handler, 100); + + clock.tick(50); + assert.strictEqual(handler.callCount, 0); + assert.strictEqual(timer.hasExpired(), false); + + clock.tick(100); + assert.strictEqual(handler.callCount, 1); + assert.strictEqual(timer.hasExpired(), true); + }); + + it("expire time is based on timeout", () => { + const timer = new TimerManager(); + const handler = spy(() => undefined); + timer.setTimeout(handler, 50); + + assert.strictEqual(timer.expireTime, 1050); + assert.strictEqual(timer.hasExpired(), false); + }); + + it("does not fire if cleared before timeout", () => { + const timer = new TimerManager(); + const handler = spy(() => undefined); + timer.setTimeout(handler, 100); + + clock.tick(50); + timer.clearTimeout(); + + assert.strictEqual(handler.callCount, 0); + assert.strictEqual(timer.hasExpired(), true); + }); + + it("does not fire on old timeout when new timeout set", () => { + const timer = new TimerManager(); + const handler = spy(() => assert.strictEqual(Date.now(), 1150)); + timer.setTimeout(handler, 100); + assert.strictEqual(timer.expireTime, 1100); + + clock.tick(50); + timer.setTimeout(handler, 100); + assert.strictEqual(timer.expireTime, 1150); + + // advance to time 1120 - after the original timer should have fired, but before the new one. + clock.tick(70); + assert.strictEqual(handler.callCount, 0); + + // Advance past timeout + clock.tick(50); + + assert.strictEqual(timer.hasExpired(), true); + assert.strictEqual(handler.callCount, 1); + }); + + it("fires correctly on reuse", () => { + const timer = new TimerManager(); + const handler = spy(() => assert(Date.now() === 1100)); + timer.setTimeout(handler, 100); + + clock.tick(200); + const handler2 = spy(() => assert.strictEqual(Date.now(), 1300)); + timer.setTimeout(handler2, 100); + + clock.tick(200); + assert.strictEqual(handler.callCount, 1); + assert.strictEqual(handler2.callCount, 1); + }); + + it("multiple timers", () => { + const timer = new TimerManager(); + const timer2 = new TimerManager(); + const handler = spy(() => {}); + const handler2 = spy(() => {}); + + timer.setTimeout(handler, 100); + timer2.setTimeout(handler2, 50); + clock.tick(200); + + assert.strictEqual(handler.callCount, 1); + assert.strictEqual(handler2.callCount, 1); + }); +}); diff --git a/packages/framework/presence/src/timerManager.ts b/packages/framework/presence/src/timerManager.ts new file mode 100644 index 000000000000..988fa0c9ada2 --- /dev/null +++ b/packages/framework/presence/src/timerManager.ts @@ -0,0 +1,68 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Wrapper around setTimeout to track whether the timeout has expired or not. + */ +export class TimerManager { + private _timeoutId: number | undefined; + private _startTime = 0; + + public get startTime(): number { + return this._startTime; + } + + private _delay: number = 0; + + public get delay(): number { + return this._delay; + } + + private _expired: boolean = true; + + /** + * Whether the timer has expired or not. + * + * @returns True if the timer has expired; false otherwise. + */ + public hasExpired(): boolean { + return this._expired; + } + + /** + * Schedules a callback to be triggered after a delay. + * + * @param callback - A callback to execute after a delay. + * @param delay - The time to wait before executing the callback, in milliseconds. + */ + public setTimeout(callback: () => void, delay: number): void { + this.clearTimeout(); // Clear any existing timeout + this._startTime = Date.now(); + this._delay = delay; + this._expired = false; + this._timeoutId = setTimeout(() => { + this._expired = true; + callback(); + }, delay); + } + + /** + * Clear any pending timer. Also marks the timer as expired. + */ + public clearTimeout(): void { + if (this._timeoutId !== undefined) { + clearTimeout(this._timeoutId); + this._timeoutId = undefined; + this._expired = true; + } + } + + /** + * The time when this timer will expire/trigger. If the timer has expired, returns 0. + */ + public get expireTime(): number { + return this.hasExpired() ? 0 : this.startTime + this.delay; + } +} From 09b7299e1cbf1d800d4bea2bef6b7d0bc657ddb6 Mon Sep 17 00:00:00 2001 From: Zach Newton Date: Tue, 26 Nov 2024 15:54:59 -0800 Subject: [PATCH 33/40] refactor(routerlicious): use strictNullChecks (#23054) ## Description Setting TSConfig `strictNullChecks: true` can help avoid numerous runtime errors by catching unhandled `null`/`undefined` cases at compiletime. R11s has had this disabled for a very long time, and the code has not handled nulls very well because of it. This PR ## Breaking Changes Many types now have `| null` or `| undefined` added to reflect the actual, implemented return/param values. Behavior should not be changing, but types are. ## Reviewer Guidance This ended up being a large PR. I tried to keep the changes focused purely to handling undefined and null explicitly. This ended up requiring that I add a lot of `| null` and `eslint-ignore-next-line @rushstack/no-new-null`. These should be switched to `| undefined` with explicit conversion of `null -> undefined` where needed, but I did not want to rock the boat too much. --- .../.changeset/silly-loops-melt.md | 20 ++ .../packages/gitresources/tsconfig.json | 1 + .../packages/kafka-orderer/tsconfig.json | 1 + .../src/document-router/documentLambda.ts | 6 +- .../src/document-router/lambdaFactory.ts | 8 +- ...teServerLambdasDriverPrevious.generated.ts | 4 +- .../packages/lambdas-driver/tsconfig.json | 1 + .../packages/lambdas/src/copier/lambda.ts | 5 + .../lambdas/src/deli/lambdaFactory.ts | 7 +- .../lambdas/src/scribe/checkpointManager.ts | 4 +- .../lambdas/src/scribe/summaryReader.ts | 8 + .../lambdas/src/scribe/summaryWriter.ts | 10 +- .../packages/local-server/tsconfig.json | 1 + .../src/localKafkaSubscription.ts | 46 ++-- .../memory-orderer/src/localOrderManager.ts | 2 +- .../memory-orderer/src/localOrderer.ts | 26 ++- .../src/localOrdererConnection.ts | 2 +- .../memory-orderer/src/nodeManager.ts | 13 +- .../packages/memory-orderer/src/pubsub.ts | 25 ++- .../packages/memory-orderer/src/remoteNode.ts | 16 +- .../memory-orderer/src/reservationManager.ts | 6 +- .../packages/memory-orderer/tsconfig.json | 2 +- .../packages/protocol-base/tsconfig.json | 1 + .../packages/routerlicious-base/package.json | 3 +- .../src/alfred/routes/api/api.ts | 16 +- .../src/alfred/routes/api/deltas.ts | 26 +-- .../src/alfred/routes/api/documents.ts | 151 +++++++++---- .../routerlicious-base/src/alfred/runner.ts | 10 +- .../src/alfred/runnerFactory.ts | 13 +- .../src/nexus/ordererManager.ts | 49 ++-- .../routerlicious-base/src/nexus/runner.ts | 12 +- .../src/nexus/runnerFactory.ts | 15 +- .../routerlicious-base/src/riddler/api.ts | 24 +- .../src/riddler/mongoTenantRepository.ts | 5 +- .../routerlicious-base/src/riddler/runner.ts | 12 +- .../src/riddler/runnerFactory.ts | 13 +- .../src/riddler/tenantManager.ts | 64 +++++- .../src/test/alfred/api.spec.ts | 53 ++--- .../src/test/nexus/io.spec.ts | 8 +- .../src/test/riddler/api.spec.ts | 2 +- ...rverRouterliciousBasePrevious.generated.ts | 1 + .../src/utils/documentRouter.ts | 9 +- .../src/utils/sessionHelper.ts | 8 +- .../packages/routerlicious-base/tsconfig.json | 2 +- .../packages/routerlicious/src/deli/index.ts | 13 +- .../routerlicious/src/scribe/index.ts | 11 +- .../routerlicious/src/scriptorium/index.ts | 11 +- .../packages/routerlicious/tsconfig.json | 2 +- .../api-report/server-services-client.api.md | 36 +-- .../packages/services-client/package.json | 6 +- .../packages/services-client/src/array.ts | 3 + .../packages/services-client/src/error.ts | 2 +- .../services-client/src/gitManager.ts | 30 ++- .../packages/services-client/src/historian.ts | 7 +- .../services-client/src/restLessClient.ts | 3 +- .../services-client/src/restWrapper.ts | 6 +- .../packages/services-client/src/storage.ts | 5 +- .../services-client/src/storageUtils.ts | 10 +- ...eServerServicesClientPrevious.generated.ts | 1 + .../src/wholeSummaryUploadManager.ts | 4 +- .../packages/services-client/tsconfig.json | 2 +- .../packages/services-core/package.json | 12 + .../packages/services-core/src/cache.ts | 3 +- .../services-core/src/checkpointService.ts | 33 +-- .../services-core/src/combinedProducer.ts | 4 +- .../packages/services-core/src/database.ts | 19 +- .../packages/services-core/src/document.ts | 5 +- .../services-core/src/documentManager.ts | 3 +- .../packages/services-core/src/http.ts | 2 +- .../packages/services-core/src/index.ts | 1 + .../packages/services-core/src/lambdas.ts | 14 +- .../packages/services-core/src/messages.ts | 2 +- .../packages/services-core/src/mongo.ts | 3 + .../src/mongoCheckpointRepository.ts | 3 +- .../services-core/src/mongoDatabaseManager.ts | 10 +- .../src/mongoDocumentRepository.ts | 3 +- .../services-core/src/runWithRetry.ts | 84 ++++--- ...ateServerServicesCorePrevious.generated.ts | 4 + .../packages/services-core/src/throttler.ts | 7 +- .../packages/services-core/tsconfig.json | 2 +- .../src/kafkaNodeConsumer.ts | 13 +- .../src/kafkaNodeProducer.ts | 18 +- .../services-ordering-kafkanode/tsconfig.json | 2 +- .../src/zookeeperClient.ts | 6 +- .../services-ordering-zookeeper/tsconfig.json | 2 +- .../services-shared/src/runnerUtils.ts | 6 +- .../packages/services-shared/src/storage.ts | 11 +- .../packages/services-shared/src/webServer.ts | 10 +- .../packages/services-shared/tsconfig.json | 1 + .../services-utils/src/throttlerMiddleware.ts | 11 +- .../packages/services/package.json | 3 + .../packages/services/src/deltaManager.ts | 2 +- .../packages/services/src/documentManager.ts | 25 ++- .../packages/services/src/messageReceiver.ts | 11 +- .../packages/services/src/messageSender.ts | 12 +- .../mongoError/index.ts | 210 ++++++++++++------ .../mongoNetworkError/index.ts | 10 +- .../packages/services/src/mongodb.ts | 12 +- .../packages/services/src/redis.ts | 3 +- .../redisThrottleAndUsageStorageManager.ts | 2 +- .../packages/services/src/secretManager.ts | 2 +- .../services/src/storageNameRetriever.ts | 2 +- ...alidateServerServicesPrevious.generated.ts | 1 + .../packages/services/src/throttler.ts | 7 +- .../packages/services/src/throttlerHelper.ts | 26 +-- .../packages/services/tsconfig.json | 2 +- .../packages/test-utils/src/messageFactory.ts | 8 +- .../packages/test-utils/src/testCache.ts | 6 +- .../test-utils/src/testClientManager.ts | 14 +- .../test-utils/src/testDocumentStorage.ts | 4 +- .../packages/test-utils/src/testHistorian.ts | 122 +++++----- .../packages/test-utils/src/testKafka.ts | 2 +- .../packages/test-utils/src/testPublisher.ts | 2 +- .../test-utils/src/testTenantManager.ts | 5 +- .../src/testThrottleAndUsageStorageManager.ts | 2 +- .../test-utils/src/testThrottlerHelper.ts | 4 +- .../packages/test-utils/tsconfig.json | 2 +- .../packages/tinylicious/src/app.ts | 2 +- .../tinylicious/src/resourcesFactory.ts | 2 +- .../tinylicious/src/routes/ordering/deltas.ts | 7 +- .../src/routes/ordering/documents.ts | 10 +- .../tinylicious/src/routes/ordering/index.ts | 5 +- .../src/routes/storage/git/blobs.ts | 6 +- .../src/routes/storage/git/commits.ts | 4 +- .../src/routes/storage/git/refs.ts | 10 +- .../src/routes/storage/git/tags.ts | 4 +- .../src/routes/storage/git/trees.ts | 4 +- .../src/routes/storage/repository/commits.ts | 6 +- .../src/routes/storage/repository/contents.ts | 4 +- .../src/routes/storage/repository/headers.ts | 4 +- .../packages/tinylicious/src/runner.ts | 21 +- .../tinylicious/src/services/inMemorydb.ts | 11 +- .../tinylicious/src/services/levelDb.ts | 2 +- .../src/services/levelDbCollection.ts | 19 +- .../tinylicious/src/services/tenantManager.ts | 5 +- .../packages/tinylicious/src/utils.ts | 4 +- .../packages/tinylicious/tsconfig.json | 2 +- 137 files changed, 1115 insertions(+), 675 deletions(-) create mode 100644 server/routerlicious/.changeset/silly-loops-melt.md diff --git a/server/routerlicious/.changeset/silly-loops-melt.md b/server/routerlicious/.changeset/silly-loops-melt.md new file mode 100644 index 000000000000..3eeece02e734 --- /dev/null +++ b/server/routerlicious/.changeset/silly-loops-melt.md @@ -0,0 +1,20 @@ +--- +"@fluidframework/server-lambdas": major +"@fluidframework/server-lambdas-driver": major +"@fluidframework/server-memory-orderer": major +"@fluidframework/server-routerlicious": major +"@fluidframework/server-routerlicious-base": major +"@fluidframework/server-services": major +"@fluidframework/server-services-client": major +"@fluidframework/server-services-core": major +"@fluidframework/server-services-ordering-kafkanode": major +"@fluidframework/server-services-ordering-zookeeper": major +"@fluidframework/server-services-shared": major +"@fluidframework/server-services-utils": major +"@fluidframework/server-test-utils": major +"tinylicious": major +--- + +Types altered to account for undefined and null values + +Many types updated to reflect implementations that can return null or undefined, but did not call that out in type definitions. Internal functionality only changed to handle existing null/undefined cases that are now known at compiletime. diff --git a/server/routerlicious/packages/gitresources/tsconfig.json b/server/routerlicious/packages/gitresources/tsconfig.json index 4c4c04fe6144..16b23ae76c75 100644 --- a/server/routerlicious/packages/gitresources/tsconfig.json +++ b/server/routerlicious/packages/gitresources/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["dist", "node_modules"], "compilerOptions": { + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", }, diff --git a/server/routerlicious/packages/kafka-orderer/tsconfig.json b/server/routerlicious/packages/kafka-orderer/tsconfig.json index 4c4c04fe6144..16b23ae76c75 100644 --- a/server/routerlicious/packages/kafka-orderer/tsconfig.json +++ b/server/routerlicious/packages/kafka-orderer/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["dist", "node_modules"], "compilerOptions": { + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", }, diff --git a/server/routerlicious/packages/lambdas-driver/src/document-router/documentLambda.ts b/server/routerlicious/packages/lambdas-driver/src/document-router/documentLambda.ts index 3fdd0f431998..3a1b87718d9a 100644 --- a/server/routerlicious/packages/lambdas-driver/src/document-router/documentLambda.ts +++ b/server/routerlicious/packages/lambdas-driver/src/document-router/documentLambda.ts @@ -22,6 +22,7 @@ import { ITicketedSignalMessage, RawOperationType, IRawOperationMessage, + isCompleteBoxcarMessage, } from "@fluidframework/server-services-core"; import { getLumberBaseProperties, Lumberjack } from "@fluidframework/server-services-telemetry"; import { DocumentContextManager } from "./contextManager"; @@ -159,8 +160,9 @@ export class DocumentLambda implements IPartitionLambda { private handlerCore(message: IQueuedMessage): void { const boxcar = extractBoxcar(message); - if (!boxcar.documentId || !boxcar.tenantId) { - return; + if (!isCompleteBoxcarMessage(boxcar)) { + // If the boxcar is not complete, it cannot be routed correctly. + return undefined; } // Stash the parsed value for down stream lambdas diff --git a/server/routerlicious/packages/lambdas-driver/src/document-router/lambdaFactory.ts b/server/routerlicious/packages/lambdas-driver/src/document-router/lambdaFactory.ts index edd91151a871..e3b4fcfefc76 100644 --- a/server/routerlicious/packages/lambdas-driver/src/document-router/lambdaFactory.ts +++ b/server/routerlicious/packages/lambdas-driver/src/document-router/lambdaFactory.ts @@ -14,9 +14,13 @@ import { import { DocumentLambda } from "./documentLambda"; /** + * @typeParam TConfig - The configuration type for the lambdas created by this factory * @internal */ -export class DocumentLambdaFactory extends EventEmitter implements IPartitionLambdaFactory { +export class DocumentLambdaFactory + extends EventEmitter + implements IPartitionLambdaFactory +{ constructor( private readonly documentLambdaFactory: IPartitionLambdaFactory, private readonly documentLambdaServerConfiguration: IDocumentLambdaServerConfiguration, @@ -29,7 +33,7 @@ export class DocumentLambdaFactory extends EventEmitter implements IPartitionLam }); } - public async create(config: undefined, context: IContext): Promise { + public async create(config: TConfig, context: IContext): Promise { return new DocumentLambda( this.documentLambdaFactory, context, diff --git a/server/routerlicious/packages/lambdas-driver/src/test/types/validateServerLambdasDriverPrevious.generated.ts b/server/routerlicious/packages/lambdas-driver/src/test/types/validateServerLambdasDriverPrevious.generated.ts index a7bbf4c4919e..6c099648479a 100644 --- a/server/routerlicious/packages/lambdas-driver/src/test/types/validateServerLambdasDriverPrevious.generated.ts +++ b/server/routerlicious/packages/lambdas-driver/src/test/types/validateServerLambdasDriverPrevious.generated.ts @@ -56,7 +56,7 @@ use_old_ClassDeclaration_DocumentContext( declare function get_old_ClassDeclaration_DocumentLambdaFactory(): TypeOnly; declare function use_current_ClassDeclaration_DocumentLambdaFactory( - use: TypeOnly): void; + use: TypeOnly>): void; use_current_ClassDeclaration_DocumentLambdaFactory( get_old_ClassDeclaration_DocumentLambdaFactory()); @@ -66,7 +66,7 @@ use_current_ClassDeclaration_DocumentLambdaFactory( * "ClassDeclaration_DocumentLambdaFactory": {"backCompat": false} */ declare function get_current_ClassDeclaration_DocumentLambdaFactory(): - TypeOnly; + TypeOnly>; declare function use_old_ClassDeclaration_DocumentLambdaFactory( use: TypeOnly): void; use_old_ClassDeclaration_DocumentLambdaFactory( diff --git a/server/routerlicious/packages/lambdas-driver/tsconfig.json b/server/routerlicious/packages/lambdas-driver/tsconfig.json index 9f03e2b6e02c..bd48d16467b2 100644 --- a/server/routerlicious/packages/lambdas-driver/tsconfig.json +++ b/server/routerlicious/packages/lambdas-driver/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["src/test/**/*"], "compilerOptions": { + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "composite": true, diff --git a/server/routerlicious/packages/lambdas/src/copier/lambda.ts b/server/routerlicious/packages/lambdas/src/copier/lambda.ts index 26c89b950b56..3ae147ef3092 100644 --- a/server/routerlicious/packages/lambdas/src/copier/lambda.ts +++ b/server/routerlicious/packages/lambdas/src/copier/lambda.ts @@ -11,6 +11,7 @@ import { IPartitionLambda, IRawOperationMessage, IRawOperationMessageBatch, + isCompleteBoxcarMessage, } from "@fluidframework/server-services-core"; /** @@ -33,6 +34,10 @@ export class CopierLambda implements IPartitionLambda { public handler(message: IQueuedMessage): undefined { // Extract batch of raw ops from Kafka message: const boxcar = extractBoxcar(message); + if (!isCompleteBoxcarMessage(boxcar)) { + // If the boxcar is not complete, it cannot be routed correctly. + return undefined; + } const batch = boxcar.contents; const topic = `${boxcar.tenantId}/${boxcar.documentId}`; diff --git a/server/routerlicious/packages/lambdas/src/deli/lambdaFactory.ts b/server/routerlicious/packages/lambdas/src/deli/lambdaFactory.ts index de5d9bf4db03..0d4131cc9fb6 100644 --- a/server/routerlicious/packages/lambdas/src/deli/lambdaFactory.ts +++ b/server/routerlicious/packages/lambdas/src/deli/lambdaFactory.ts @@ -86,15 +86,16 @@ export class DeliLambdaFactory }; let gitManager: IGitManager; - let document: IDocument; + let document: IDocument | undefined; try { // Lookup the last sequence number stored // TODO - is this storage specific to the orderer in place? Or can I generalize the output context? - document = await this.documentRepository.readOne({ documentId, tenantId }); + document = + (await this.documentRepository.readOne({ documentId, tenantId })) ?? undefined; // Check if the document was deleted prior. - if (!isDocumentValid(document)) { + if (document === undefined || !isDocumentValid(document)) { // (Old, from tanviraumi:) Temporary guard against failure until we figure out what causing this to trigger. // Document sessions can be joined (via Alfred) after a document is functionally deleted. const errorMessage = `Received attempt to connect to a missing/deleted document.`; diff --git a/server/routerlicious/packages/lambdas/src/scribe/checkpointManager.ts b/server/routerlicious/packages/lambdas/src/scribe/checkpointManager.ts index dc1acc34901f..bf335e46c887 100644 --- a/server/routerlicious/packages/lambdas/src/scribe/checkpointManager.ts +++ b/server/routerlicious/packages/lambdas/src/scribe/checkpointManager.ts @@ -31,7 +31,7 @@ export class CheckpointManager implements ICheckpointManager { private readonly documentId: string, private readonly documentRepository: IDocumentRepository, private readonly opCollection: ICollection, - private readonly deltaService: IDeltaService, + private readonly deltaService: IDeltaService | undefined, private readonly getDeltasViaAlfred: boolean, private readonly verifyLastOpPersistence: boolean, private readonly checkpointService: ICheckpointService, @@ -51,7 +51,7 @@ export class CheckpointManager implements ICheckpointManager { markAsCorrupt: boolean = false, ): Promise { const isLocal = isLocalCheckpoint(noActiveClients, globalCheckpointOnly); - if (this.getDeltasViaAlfred) { + if (this.getDeltasViaAlfred && this.deltaService !== undefined) { if (pending.length > 0 && this.verifyLastOpPersistence) { // Verify that the last pending op has been persisted to op storage // If it is, we can checkpoint diff --git a/server/routerlicious/packages/lambdas/src/scribe/summaryReader.ts b/server/routerlicious/packages/lambdas/src/scribe/summaryReader.ts index cef4259e1189..9f5da673f6f1 100644 --- a/server/routerlicious/packages/lambdas/src/scribe/summaryReader.ts +++ b/server/routerlicious/packages/lambdas/src/scribe/summaryReader.ts @@ -59,6 +59,7 @@ export class SummaryReader implements ISummaryReader { try { let wholeFlatSummary: IWholeFlatSummary | undefined; try { + // Attempt to fetch the latest summary by "latest" ID wholeFlatSummary = await requestWithRetry( async () => this.summaryStorage.getSummary(LatestSummaryId), "readWholeSummary_getSummary", @@ -76,6 +77,7 @@ export class SummaryReader implements ISummaryReader { } if (!wholeFlatSummary) { + // If fetching by "latest" ID fails, attempt to fetch the latest summary by explicit ref const existingRef = await requestWithRetry( async () => this.summaryStorage.getRef(encodeURIComponent(this.documentId)), "readWholeSummary_getRef", @@ -83,6 +85,9 @@ export class SummaryReader implements ISummaryReader { shouldRetryNetworkError, this.maxRetriesOnError, ); + if (!existingRef) { + throw new Error("Could not find a ref for the document."); + } wholeFlatSummary = await requestWithRetry( async () => this.summaryStorage.getSummary(existingRef.object.sha), "readWholeSummary_getSummary", @@ -166,6 +171,9 @@ export class SummaryReader implements ISummaryReader { shouldRetryNetworkError, this.maxRetriesOnError, ); + if (!existingRef) { + throw new Error("Could not find a ref for the document."); + } const [attributesContent, scribeContent, deliContent, opsContent] = await Promise.all([ requestWithRetry( diff --git a/server/routerlicious/packages/lambdas/src/scribe/summaryWriter.ts b/server/routerlicious/packages/lambdas/src/scribe/summaryWriter.ts index 34a9b8156bd7..212a6aaae0ab 100644 --- a/server/routerlicious/packages/lambdas/src/scribe/summaryWriter.ts +++ b/server/routerlicious/packages/lambdas/src/scribe/summaryWriter.ts @@ -57,8 +57,8 @@ export class SummaryWriter implements ISummaryWriter { private readonly tenantId: string, private readonly documentId: string, private readonly summaryStorage: IGitManager, - private readonly deltaService: IDeltaService, - private readonly opStorage: ICollection, + private readonly deltaService: IDeltaService | undefined, + private readonly opStorage: ICollection | undefined, private readonly enableWholeSummaryUpload: boolean, private readonly lastSummaryMessages: ISequencedDocumentMessage[], private readonly getDeltasViaAlfred: boolean, @@ -732,7 +732,7 @@ export class SummaryWriter implements ISummaryWriter { } private async retrieveOps(gt: number, lt: number): Promise { - if (this.getDeltasViaAlfred) { + if (this.getDeltasViaAlfred && this.deltaService !== undefined) { return this.deltaService.getDeltas( "", this.tenantId, @@ -743,6 +743,10 @@ export class SummaryWriter implements ISummaryWriter { ); } + if (this.opStorage === undefined) { + return []; + } + const query = { "documentId": this.documentId, "tenantId": this.tenantId, diff --git a/server/routerlicious/packages/local-server/tsconfig.json b/server/routerlicious/packages/local-server/tsconfig.json index 9f03e2b6e02c..bd48d16467b2 100644 --- a/server/routerlicious/packages/local-server/tsconfig.json +++ b/server/routerlicious/packages/local-server/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["src/test/**/*"], "compilerOptions": { + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "composite": true, diff --git a/server/routerlicious/packages/memory-orderer/src/localKafkaSubscription.ts b/server/routerlicious/packages/memory-orderer/src/localKafkaSubscription.ts index 3db6dcc363d6..2dba1d4a01a5 100644 --- a/server/routerlicious/packages/memory-orderer/src/localKafkaSubscription.ts +++ b/server/routerlicious/packages/memory-orderer/src/localKafkaSubscription.ts @@ -51,29 +51,31 @@ export class LocalKafkaSubscription extends EventEmitter { const message = this.queue.get(this.queueOffset); - try { - this.processing = true; - - const optionalPromise = this.subscriber.process(message); - if (optionalPromise !== undefined) { - await optionalPromise; + if (message !== undefined) { + try { + this.processing = true; + + const optionalPromise = this.subscriber.process(message); + if (optionalPromise !== undefined) { + await optionalPromise; + } + + this.queueOffset++; + + this.emit("processed", this.queueOffset); + } catch (ex) { + // Lambda failed to process the message + this.subscriber.context.error(ex, { restart: false }); + + this.retryTimer = setTimeout(() => { + this.retryTimer = undefined; + this.process().catch((e) => this.handleProcessError(e)); + }, 500); + + return; + } finally { + this.processing = false; } - - this.queueOffset++; - - this.emit("processed", this.queueOffset); - } catch (ex) { - // Lambda failed to process the message - this.subscriber.context.error(ex, { restart: false }); - - this.retryTimer = setTimeout(() => { - this.retryTimer = undefined; - this.process().catch((e) => this.handleProcessError(e)); - }, 500); - - return; - } finally { - this.processing = false; } // Process the next one diff --git a/server/routerlicious/packages/memory-orderer/src/localOrderManager.ts b/server/routerlicious/packages/memory-orderer/src/localOrderManager.ts index fe711460d91e..30c4db0ef23d 100644 --- a/server/routerlicious/packages/memory-orderer/src/localOrderManager.ts +++ b/server/routerlicious/packages/memory-orderer/src/localOrderManager.ts @@ -14,7 +14,7 @@ import { IConcreteNode, IConcreteNodeFactory, IReservationManager } from "./inte */ export class LocalOrderManager { private readonly localOrderers = new Map>(); - private localNodeP: Promise; + private localNodeP!: Promise; constructor( private readonly nodeFactory: IConcreteNodeFactory, diff --git a/server/routerlicious/packages/memory-orderer/src/localOrderer.ts b/server/routerlicious/packages/memory-orderer/src/localOrderer.ts index 8d81749b7ed8..fc91f292b2f3 100644 --- a/server/routerlicious/packages/memory-orderer/src/localOrderer.ts +++ b/server/routerlicious/packages/memory-orderer/src/localOrderer.ts @@ -51,7 +51,13 @@ const DefaultScribe: IScribe = { lastClientSummaryHead: undefined, logOffset: -1, minimumSequenceNumber: -1, - protocolState: undefined, + protocolState: { + members: [], + minimumSequenceNumber: 0, + proposals: [], + sequenceNumber: 0, + values: [], + }, sequenceNumber: -1, lastSummarySequenceNumber: 0, validParentSummaries: undefined, @@ -143,8 +149,8 @@ export class LocalOrderer implements IOrderer { ); } - public rawDeltasKafka: LocalKafka; - public deltasKafka: LocalKafka; + public rawDeltasKafka!: LocalKafka; + public deltasKafka!: LocalKafka; public scriptoriumLambda: LocalLambdaController | undefined; public moiraLambda: LocalLambdaController | undefined; @@ -241,7 +247,9 @@ export class LocalOrderer implements IOrderer { this.scriptoriumContext, async (lambdaSetup, context) => { const deltasCollection = await lambdaSetup.deltaCollectionP(); - return new ScriptoriumLambda(deltasCollection, context, undefined, undefined); + return new ScriptoriumLambda(deltasCollection, context, undefined, async () => + Promise.resolve(), + ); }, ); @@ -343,6 +351,10 @@ export class LocalOrderer implements IOrderer { () => -1, ); + if (!this.gitManager) { + throw new Error("Git manager is required to start scribe lambda."); + } + const summaryReader = new SummaryReader( this.tenantId, this.documentId, @@ -355,7 +367,7 @@ export class LocalOrderer implements IOrderer { this.tenantId, this.documentId, this.gitManager, - null /* deltaService */, + undefined /* deltaService */, scribeMessagesCollection, false /* enableWholeSummaryUpload */, latestSummary.messages, @@ -374,7 +386,7 @@ export class LocalOrderer implements IOrderer { this.documentId, documentRepository, scribeMessagesCollection, - null /* deltaService */, + undefined /* deltaService */, false /* getDeltasViaAlfred */, false /* verifyLastOpPersistence */, checkpointService, @@ -400,7 +412,7 @@ export class LocalOrderer implements IOrderer { true, true, true, - this.details.value.isEphemeralContainer, + this.details.value.isEphemeralContainer ?? false, checkpointService.getLocalCheckpointEnabled(), maxPendingCheckpointMessagesLength, ); diff --git a/server/routerlicious/packages/memory-orderer/src/localOrdererConnection.ts b/server/routerlicious/packages/memory-orderer/src/localOrdererConnection.ts index b2d30725394a..f82f42b8f22d 100644 --- a/server/routerlicious/packages/memory-orderer/src/localOrdererConnection.ts +++ b/server/routerlicious/packages/memory-orderer/src/localOrdererConnection.ts @@ -129,7 +129,7 @@ export class LocalOrdererConnection implements IOrdererConnection { }); } - const boxcar: IBoxcarMessage = { + const boxcar: Required = { contents: messages, documentId: this.documentId, tenantId: this.tenantId, diff --git a/server/routerlicious/packages/memory-orderer/src/nodeManager.ts b/server/routerlicious/packages/memory-orderer/src/nodeManager.ts index af467c95abe4..20cf2242676c 100644 --- a/server/routerlicious/packages/memory-orderer/src/nodeManager.ts +++ b/server/routerlicious/packages/memory-orderer/src/nodeManager.ts @@ -42,16 +42,17 @@ export class NodeManager extends EventEmitter { /** * Loads the given remote node with the provided ID */ - // eslint-disable-next-line @typescript-eslint/promise-function-async - public loadRemote(id: string): Promise { + public async loadRemote(id: string): Promise { // Return immediately if have the resolved value - if (this.nodes.has(id)) { - return Promise.resolve(this.nodes.get(id)); + const existingNode = this.nodes.get(id); + if (existingNode !== undefined) { + return existingNode; } // Otherwise return a promise for the node - if (this.pendingNodes.has(id)) { - return this.pendingNodes.get(id); + const existingPendingNode = this.pendingNodes.get(id); + if (existingPendingNode !== undefined) { + return existingPendingNode; } // Otherwise load in the information diff --git a/server/routerlicious/packages/memory-orderer/src/pubsub.ts b/server/routerlicious/packages/memory-orderer/src/pubsub.ts index 4cbdaad1e13c..fee00ba4e19d 100644 --- a/server/routerlicious/packages/memory-orderer/src/pubsub.ts +++ b/server/routerlicious/packages/memory-orderer/src/pubsub.ts @@ -70,25 +70,28 @@ export class PubSub implements IPubSub { } const subscriptions = this.topics.get(topic); - if (!subscriptions.has(subscriber.id)) { - subscriptions.set(subscriber.id, { subscriber, count: 0 }); + if (!subscriptions?.has(subscriber.id)) { + subscriptions?.set(subscriber.id, { subscriber, count: 0 }); + } + const subscription = subscriptions?.get(subscriber.id); + if (subscription) { + subscription.count++; } - - subscriptions.get(subscriber.id).count++; } public unsubscribe(topic: string, subscriber: ISubscriber): void { assert(this.topics.has(topic)); const subscriptions = this.topics.get(topic); - assert(subscriptions.has(subscriber.id)); - const details = subscriptions.get(subscriber.id); - details.count--; - if (details.count === 0) { - subscriptions.delete(subscriber.id); + assert(subscriptions?.has(subscriber.id)); + const details = subscriptions?.get(subscriber.id); + if (details !== undefined) { + details.count--; + if (details.count === 0) { + subscriptions?.delete(subscriber.id); + } } - - if (subscriptions.size === 0) { + if (subscriptions?.size === 0) { this.topics.delete(topic); } } diff --git a/server/routerlicious/packages/memory-orderer/src/remoteNode.ts b/server/routerlicious/packages/memory-orderer/src/remoteNode.ts index 8683e3d3782e..d8a4bb1c9447 100644 --- a/server/routerlicious/packages/memory-orderer/src/remoteNode.ts +++ b/server/routerlicious/packages/memory-orderer/src/remoteNode.ts @@ -111,12 +111,12 @@ export class RemoteNode extends EventEmitter implements IConcreteNode { // Connect to the given remote node const db = await mongoManager.getDatabase(); const nodeCollection = db.collection(nodeCollectionName); - const details = await nodeCollection.findOne({ _id: id }); + const details = (await nodeCollection.findOne({ _id: id })) ?? undefined; const socket = - details.expiration >= Date.now() + details !== undefined && details.expiration >= Date.now() ? await Socket.connect(details.address, id) - : null; + : undefined; const node = new RemoteNode(id, socket); return node; @@ -142,11 +142,11 @@ export class RemoteNode extends EventEmitter implements IConcreteNode { private constructor( private readonly _id: string, - private readonly socket: Socket, + private readonly socket: Socket | undefined, ) { super(); - this.socket.on("message", (message) => { + this.socket?.on("message", (message) => { switch (message.type) { case "op": this.route(message.payload as IOpMessage); @@ -175,7 +175,7 @@ export class RemoteNode extends EventEmitter implements IConcreteNode { if (!this.topicMap.has(fullId)) { this.topicMap.set(fullId, []); } - this.topicMap.get(fullId).push(socketConnection); + this.topicMap.get(fullId)?.push(socketConnection); pendingConnect.deferred.resolve(socketConnection); break; @@ -197,7 +197,7 @@ export class RemoteNode extends EventEmitter implements IConcreteNode { } public send(cid: number, type: string, payload: any) { - this.socket.send({ + this.socket?.send({ cid, payload, type: type as any, @@ -226,7 +226,7 @@ export class RemoteNode extends EventEmitter implements IConcreteNode { private route(message: IOpMessage) { const sockets = this.topicMap.get(message.topic); - for (const socket of sockets) { + for (const socket of sockets ?? []) { socket.emit(message.op, message.data[0], ...message.data.slice(1)); } } diff --git a/server/routerlicious/packages/memory-orderer/src/reservationManager.ts b/server/routerlicious/packages/memory-orderer/src/reservationManager.ts index 992fc234b516..fa28110e1a99 100644 --- a/server/routerlicious/packages/memory-orderer/src/reservationManager.ts +++ b/server/routerlicious/packages/memory-orderer/src/reservationManager.ts @@ -37,7 +37,7 @@ export class ReservationManager extends EventEmitter implements IReservationMana // Reservation can be null (first time), expired, or existing and within the time window if (reservation === null) { - await this.makeReservation(node, key, null, reservations); + await this.makeReservation(node, key, undefined, reservations); return node; } else { const remoteNode = await this.nodeTracker.loadRemote(reservation.node); @@ -53,12 +53,12 @@ export class ReservationManager extends EventEmitter implements IReservationMana private async makeReservation( node: IConcreteNode, key: string, - existing: IReservation, + existing: IReservation | undefined, collection: ICollection, ): Promise { const newReservation: IReservation = { _id: key, node: node.id }; - await (existing + await (existing !== undefined ? collection.update({ _id: key, node: existing.node }, newReservation, null) : collection.insertOne(newReservation)); } diff --git a/server/routerlicious/packages/memory-orderer/tsconfig.json b/server/routerlicious/packages/memory-orderer/tsconfig.json index 58195bcd4eae..bd48d16467b2 100644 --- a/server/routerlicious/packages/memory-orderer/tsconfig.json +++ b/server/routerlicious/packages/memory-orderer/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["src/test/**/*"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "composite": true, diff --git a/server/routerlicious/packages/protocol-base/tsconfig.json b/server/routerlicious/packages/protocol-base/tsconfig.json index 9f03e2b6e02c..bd48d16467b2 100644 --- a/server/routerlicious/packages/protocol-base/tsconfig.json +++ b/server/routerlicious/packages/protocol-base/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["src/test/**/*"], "compilerOptions": { + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "composite": true, diff --git a/server/routerlicious/packages/routerlicious-base/package.json b/server/routerlicious/packages/routerlicious-base/package.json index 836b5126e72e..b1e5d6838d4b 100644 --- a/server/routerlicious/packages/routerlicious-base/package.json +++ b/server/routerlicious/packages/routerlicious-base/package.json @@ -141,7 +141,8 @@ "forwardCompat": false }, "ClassDeclaration_RiddlerResources": { - "forwardCompat": false + "forwardCompat": false, + "backCompat": false } } } diff --git a/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/api.ts b/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/api.ts index 0f12c030e42d..9cf14c46b6df 100644 --- a/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/api.ts +++ b/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/api.ts @@ -57,7 +57,7 @@ export function create( const router: Router = Router(); const tenantThrottleOptions: Partial = { - throttleIdPrefix: (req) => getParam(req.params, "tenantId"), + throttleIdPrefix: (req) => req.params.tenantId, throttleIdSuffix: Constants.alfredRestThrottleIdSuffix, }; const generalTenantThrottler = tenantThrottlers.get(Constants.generalRestCallThrottleIdPrefix); @@ -69,8 +69,8 @@ export function create( ); function handlePatchRootSuccess(request: Request, opBuilder: (request: Request) => any[]) { - const tenantId = getParam(request.params, "tenantId"); - const documentId = getParam(request.params, "id"); + const tenantId = request.params.tenantId; + const documentId = request.params.id; const clientId = (sillyname() as string).toLowerCase().split(" ").join("-"); sendJoin(tenantId, documentId, clientId, producer); sendOp(request, tenantId, documentId, clientId, producer, opBuilder); @@ -96,7 +96,7 @@ export function create( throttle(generalTenantThrottler, winston, tenantThrottleOptions), // eslint-disable-next-line @typescript-eslint/no-misused-promises async (request, response) => { - const tenantId = getParam(request.params, "tenantId"); + const tenantId = request.params.tenantId; const bearerAuthToken = request?.header("Authorization"); if (!bearerAuthToken) { response.status(400).send(`Missing Authorization header in the request.`); @@ -147,7 +147,7 @@ export function create( throttle(generalTenantThrottler, winston, tenantThrottleOptions), // eslint-disable-next-line @typescript-eslint/no-misused-promises async (request, response) => { - const tenantId = getParam(request.params, "tenantId"); + const tenantId = request.params.tenantId; const blobData = request.body as IBlobData; // TODO: why is this contacting external blob storage? const externalHistorianUrl = config.get("worker:blobStorageUrl") as string; @@ -174,8 +174,8 @@ export function create( verifyStorageToken(tenantManager, config), // eslint-disable-next-line @typescript-eslint/no-misused-promises async (request, response) => { - const tenantId = getParam(request.params, "tenantId"); - const documentId = getParam(request.params, "id"); + const tenantId = request.params.tenantId; + const documentId = request.params.id; const signalContent = request?.body?.signalContent; if (!isValidSignalEnvelope(signalContent)) { response @@ -209,7 +209,7 @@ export function create( function mapSetBuilder(request: Request): any[] { const reqOps = request.body as IMapSetOperation[]; - const ops = []; + const ops: ReturnType[] = []; for (const reqOp of reqOps) { ops.push(craftMapSet(reqOp)); } diff --git a/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/deltas.ts b/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/deltas.ts index 31175c2cb8d4..a51f746120d4 100644 --- a/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/deltas.ts +++ b/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/deltas.ts @@ -71,7 +71,7 @@ export function create( revokedTokenChecker, }; - function stringToSequenceNumber(value: any): number { + function stringToSequenceNumber(value: any): number | undefined { if (typeof value !== "string") { return undefined; } @@ -91,13 +91,14 @@ export function create( (request, response, next) => { const from = stringToSequenceNumber(request.query.from); const to = stringToSequenceNumber(request.query.to); - const tenantId = getParam(request.params, "tenantId") || appTenants[0].id; + const tenantId = request.params.tenantId || appTenants[0].id; + const documentId = request.params.id; // Query for the deltas and return a filtered version of just the operations field const deltasP = deltaService.getDeltasFromSummaryAndStorage( deltasCollectionName, tenantId, - getParam(request.params, "id"), + documentId, from, to, ); @@ -115,14 +116,11 @@ export function create( throttle(generalTenantThrottler, winston, tenantThrottleOptions), verifyStorageToken(tenantManager, config, defaultTokenValidationOptions), (request, response, next) => { - const tenantId = getParam(request.params, "tenantId") || appTenants[0].id; + const tenantId = request.params.tenantId || appTenants[0].id; + const documentId = request.params.id; // Query for the raw deltas (no from/to since we want all of them) - const deltasP = deltaService.getDeltas( - rawDeltasCollectionName, - tenantId, - getParam(request.params, "id"), - ); + const deltasP = deltaService.getDeltas(rawDeltasCollectionName, tenantId, documentId); handleResponse(deltasP, response, undefined, 500); }, @@ -146,26 +144,26 @@ export function create( ), verifyStorageToken(tenantManager, config, defaultTokenValidationOptions), (request, response, next) => { + const documentId = request.params.id; let from = stringToSequenceNumber(request.query.from); let to = stringToSequenceNumber(request.query.to); if (from === undefined && to === undefined) { from = 0; to = from + getDeltasRequestMaxOpsRange + 1; - } else if (to === undefined) { + } else if (to === undefined && from !== undefined) { to = from + getDeltasRequestMaxOpsRange + 1; - } else if (from === undefined) { + } else if (from === undefined && to !== undefined) { from = Math.max(0, to - getDeltasRequestMaxOpsRange - 1); } - const tenantId = getParam(request.params, "tenantId") || appTenants[0].id; - // eslint-disable-next-line @typescript-eslint/no-base-to-string + const tenantId = request.params.tenantId || appTenants[0].id; const caller = request.query.caller?.toString(); // Query for the deltas and return a filtered version of just the operations field const deltasP = deltaService.getDeltas( deltasCollectionName, tenantId, - getParam(request.params, "id"), + documentId, from, to, caller, diff --git a/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/documents.ts b/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/documents.ts index 3dc6b4df697b..270a98731383 100644 --- a/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/documents.ts +++ b/server/routerlicious/packages/routerlicious-base/src/alfred/routes/api/documents.ts @@ -30,7 +30,7 @@ import { validateRequestParams, handleResponse, } from "@fluidframework/server-services"; -import { Router } from "express"; +import { Request, Router } from "express"; import winston from "winston"; import { convertFirstSummaryWholeSummaryTreeToSummaryTree, @@ -52,6 +52,82 @@ import { Constants, getSession, StageTrace } from "../../../utils"; import { IDocumentDeleteService } from "../../services"; import type { RequestHandler } from "express-serve-static-core"; +/** + * Response body shape for modern clients that can handle object responses. + * @internal + */ +interface ICreateDocumentResponseBody { + /** + * The id of the created document. + */ + readonly id: string; + /** + * The access token for the created document. + * When this is provided, the client should use this token to connect to the document. + * Otherwise, if not provided, the client will need to generate a new token using the provided document id. + * @privateRemarks TODO: This is getting generated multiple times. We should generate it once and reuse it. + */ + readonly token?: string; + /** + * The session information for the created document. + * When this is provided, the client should use this session information to connect to the document in the correct location. + * Otherwise, if not provided, the client will need to discover the correct location using the getSession API or continue using the + * original location used when creating the document. + */ + readonly session?: ISession; +} + +async function generateCreateDocumentResponseBody( + request: Request, + tenantManager: ITenantManager, + documentId: string, + tenantId: string, + generateToken: boolean, + enableDiscovery: boolean, + sessionInfo: { + externalOrdererUrl: string; + externalHistorianUrl: string; + externalDeltaStreamUrl: string; + messageBrokerId?: string; + }, +): Promise { + const authorizationHeader = request.header("Authorization"); + let newDocumentAccessToken: string | undefined; + if (generateToken && authorizationHeader !== undefined) { + // Generate creation token given a jwt from header + const tokenRegex = /Basic (.+)/; + const tokenMatch = tokenRegex.exec(authorizationHeader); + const token = tokenMatch !== null ? tokenMatch[1] : undefined; + if (token === undefined) { + throw new NetworkError(400, "Authorization header is missing or malformed"); + } + const tenantKey = await tenantManager.getKey(tenantId); + newDocumentAccessToken = getCreationToken(token, tenantKey, documentId); + } + let newDocumentSession: ISession | undefined; + if (enableDiscovery) { + // Session information + const session: ISession = { + ordererUrl: sessionInfo.externalOrdererUrl, + historianUrl: sessionInfo.externalHistorianUrl, + deltaStreamUrl: sessionInfo.externalDeltaStreamUrl, + // Indicate to consumer that session was newly created. + isSessionAlive: false, + isSessionActive: false, + }; + // if undefined and added directly to the session object - will be serialized as null in mongo which is undesirable + if (sessionInfo.messageBrokerId) { + session.messageBrokerId = sessionInfo.messageBrokerId; + } + newDocumentSession = session; + } + return { + id: documentId, + token: newDocumentAccessToken, + session: newDocumentSession, + }; +} + export function create( storage: IDocumentStorage, appTenants: IAlfredTenant[], @@ -139,11 +215,10 @@ export function create( throttle(generalTenantThrottler, winston, tenantThrottleOptions), verifyStorageToken(tenantManager, config, defaultTokenValidationOptions), (request, response, next) => { + const tenantId = request.params.tenantId; + const documentId = request.params.id; const documentP = storage - .getDocument( - getParam(request.params, "tenantId") || appTenants[0].id, - getParam(request.params, "id"), - ) + .getDocument(tenantId ?? appTenants[0].id, documentId) .then((document) => { if (!document || document.scheduledDeletionTime) { throw new NetworkError(404, "Document not found."); @@ -182,7 +257,7 @@ export function create( // eslint-disable-next-line @typescript-eslint/no-misused-promises async (request, response, next) => { // Tenant and document - const tenantId = getParam(request.params, "tenantId"); + const tenantId = request.params.tenantId; // If enforcing server generated document id, ignore id parameter const id = enforceServerGeneratedDocumentId ? uuid() @@ -228,34 +303,24 @@ export function create( // TODO: remove condition once old drivers are phased out and all clients can handle object response const clientAcceptsObjectResponse = enableDiscovery === true || generateToken === true; if (clientAcceptsObjectResponse) { - const responseBody = { id, token: undefined, session: undefined }; - if (generateToken) { - // Generate creation token given a jwt from header - const authorizationHeader = request.header("Authorization"); - const tokenRegex = /Basic (.+)/; - const tokenMatch = tokenRegex.exec(authorizationHeader); - const token = tokenMatch[1]; - const tenantKey = await tenantManager.getKey(tenantId); - responseBody.token = getCreationToken(token, tenantKey, id); - } - if (enableDiscovery) { - // Session information - const session: ISession = { - ordererUrl: externalOrdererUrl, - historianUrl: externalHistorianUrl, - deltaStreamUrl: externalDeltaStreamUrl, - // Indicate to consumer that session was newly created. - isSessionAlive: false, - isSessionActive: false, - }; - // if undefined and added directly to the session object - will be serialized as null in mongo which is undesirable - if (messageBrokerId) { - session.messageBrokerId = messageBrokerId; - } - responseBody.session = session; - } + const generateResponseBodyP = generateCreateDocumentResponseBody( + request, + tenantManager, + id, + tenantId, + generateToken, + enableDiscovery, + { + externalOrdererUrl, + externalHistorianUrl, + externalDeltaStreamUrl, + messageBrokerId, + }, + ); handleResponse( - createP.then(() => responseBody), + Promise.all([createP, generateResponseBodyP]).then( + ([, responseBody]) => responseBody, + ), response, undefined, undefined, @@ -312,8 +377,8 @@ export function create( verifyStorageTokenForGetSession(tenantManager, config, defaultTokenValidationOptions), // eslint-disable-next-line @typescript-eslint/no-misused-promises async (request, response, next) => { - const documentId = getParam(request.params, "id"); - const tenantId = getParam(request.params, "tenantId"); + const documentId = request.params.id; + const tenantId = request.params.tenantId; const lumberjackProperties = getLumberBaseProperties(documentId, tenantId); const getSessionMetric: Lumber = Lumberjack.newLumberMetric( @@ -361,8 +426,8 @@ export function create( verifyStorageToken(tenantManager, config, defaultTokenValidationOptions), // eslint-disable-next-line @typescript-eslint/no-misused-promises async (request, response, next) => { - const documentId = getParam(request.params, "id"); - const tenantId = getParam(request.params, "tenantId"); + const documentId = request.params.id; + const tenantId = request.params.tenantId; const lumberjackProperties = getLumberBaseProperties(documentId, tenantId); Lumberjack.info(`Received document delete request.`, lumberjackProperties); @@ -382,8 +447,8 @@ export function create( verifyStorageToken(tenantManager, config, defaultTokenValidationOptions), // eslint-disable-next-line @typescript-eslint/no-misused-promises async (request, response, next) => { - const documentId = getParam(request.params, "id"); - const tenantId = getParam(request.params, "tenantId"); + const documentId = request.params.id; + const tenantId = request.params.tenantId; const lumberjackProperties = getLumberBaseProperties(documentId, tenantId); Lumberjack.info(`Received token revocation request.`, lumberjackProperties); @@ -401,6 +466,14 @@ export function create( request, response, ).correlationId; + if (!correlationId) { + return handleResponse( + Promise.reject( + new NetworkError(400, `Missing correlationId in request headers.`), + ), + response, + ); + } const options: IRevokeTokenOptions = { correlationId, }; diff --git a/server/routerlicious/packages/routerlicious-base/src/alfred/runner.ts b/server/routerlicious/packages/routerlicious-base/src/alfred/runner.ts index d9728fe25ec1..57d085c28cd9 100644 --- a/server/routerlicious/packages/routerlicious-base/src/alfred/runner.ts +++ b/server/routerlicious/packages/routerlicious-base/src/alfred/runner.ts @@ -35,8 +35,8 @@ import { IDocumentDeleteService } from "./services"; * @internal */ export class AlfredRunner implements IRunner { - private server: IWebServer; - private runningDeferred: Deferred; + private server?: IWebServer; + private runningDeferred?: Deferred; private stopped: boolean = false; private readonly runnerMetric = Lumberjack.newLumberMetric(LumberEventName.AlfredRunner); @@ -106,7 +106,7 @@ export class AlfredRunner implements IRunner { } } else { // Create an HTTP server with a blank request listener - this.server = this.serverFactory.create(null); + this.server = this.serverFactory.create(undefined); } const httpServer = this.server.httpServer; @@ -182,8 +182,8 @@ export class AlfredRunner implements IRunner { * Event listener for HTTP server "listening" event. */ private onListening() { - const addr = this.server.httpServer.address(); - const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; + const addr = this.server?.httpServer?.address(); + const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr?.port}`; winston.info(`Listening on ${bind}`); Lumberjack.info(`Listening on ${bind}`); } diff --git a/server/routerlicious/packages/routerlicious-base/src/alfred/runnerFactory.ts b/server/routerlicious/packages/routerlicious-base/src/alfred/runnerFactory.ts index 04f7492caf41..713c9f9c029f 100644 --- a/server/routerlicious/packages/routerlicious-base/src/alfred/runnerFactory.ts +++ b/server/routerlicious/packages/routerlicious-base/src/alfred/runnerFactory.ts @@ -153,7 +153,12 @@ export class AlfredResourcesFactory implements core.IResourcesFactory resources.startupCheck, resources.tokenRevocationManager, resources.revokedTokenChecker, - null, + undefined, resources.clusterDrainingChecker, resources.enableClientIPLogging, resources.readinessCheck, diff --git a/server/routerlicious/packages/routerlicious-base/src/nexus/ordererManager.ts b/server/routerlicious/packages/routerlicious-base/src/nexus/ordererManager.ts index 41ce5596272f..f252a4d10118 100644 --- a/server/routerlicious/packages/routerlicious-base/src/nexus/ordererManager.ts +++ b/server/routerlicious/packages/routerlicious-base/src/nexus/ordererManager.ts @@ -140,7 +140,7 @@ export class OrdererManager implements IOrdererManager { "decrement", ); - if (ordererConnectionCount <= 0) { + if (ordererConnectionCount !== undefined && ordererConnectionCount <= 0) { this.startCleanupTimer(tenantId, documentId); } } @@ -150,7 +150,7 @@ export class OrdererManager implements IOrdererManager { documentId: string, operation: "increment" | "decrement", ordererType?: "kafka" | "local", - ): Promise { + ): Promise { if (!this.options.enableConnectionCleanup) { return; } @@ -168,7 +168,8 @@ export class OrdererManager implements IOrdererManager { this.ordererConnectionCountMap.set(ordererId, 0); } const ordererConnectionCount = - this.ordererConnectionCountMap.get(ordererId) + (operation === "increment" ? 1 : -1); + (this.ordererConnectionCountMap.get(ordererId) ?? 0) + + (operation === "increment" ? 1 : -1); this.ordererConnectionCountMap.set(ordererId, ordererConnectionCount); return ordererConnectionCount; @@ -183,30 +184,32 @@ export class OrdererManager implements IOrdererManager { documentId: string, ): Promise<"kafka" | "local"> { const ordererId = this.getOrdererConnectionMapKey(tenantId, documentId); - if (!this.ordererConnectionTypeMap.has(ordererId)) { - if (!this.globalDbEnabled) { - const messageMetaData = { documentId, tenantId }; - Lumberjack.info(`Global db is disabled, checking orderer URL`, messageMetaData); - const tenant = await this.tenantManager.getTenant(tenantId, documentId); - - Lumberjack.info( - `tenant orderer: ${JSON.stringify(tenant.orderer)}`, - getLumberBaseProperties(documentId, tenantId), - ); + const cachedOrdererConnectionType = this.ordererConnectionTypeMap.get(ordererId); + if (cachedOrdererConnectionType !== undefined) { + return cachedOrdererConnectionType; + } + if (!this.globalDbEnabled) { + const messageMetaData = { documentId, tenantId }; + Lumberjack.info(`Global db is disabled, checking orderer URL`, messageMetaData); + const tenant = await this.tenantManager.getTenant(tenantId, documentId); + + Lumberjack.info( + `tenant orderer: ${JSON.stringify(tenant.orderer)}`, + getLumberBaseProperties(documentId, tenantId), + ); - if (tenant.orderer.url !== this.ordererUrl) { - Lumberjack.error(`Invalid ordering service endpoint`, { messageMetaData }); - throw new Error("Invalid ordering service endpoint"); - } + if (tenant.orderer.url !== this.ordererUrl) { + Lumberjack.error(`Invalid ordering service endpoint`, { messageMetaData }); + throw new Error("Invalid ordering service endpoint"); + } - if (tenant.orderer.type !== "kafka") { - this.ordererConnectionTypeMap.set(ordererId, "local"); - } + if (tenant.orderer.type !== "kafka") { + this.ordererConnectionTypeMap.set(ordererId, "local"); + return "local"; } - this.ordererConnectionTypeMap.set(ordererId, "kafka"); } - - return this.ordererConnectionTypeMap.get(ordererId); + this.ordererConnectionTypeMap.set(ordererId, "kafka"); + return "kafka"; } private async cleanupOrdererConnection(tenantId: string, documentId: string): Promise { diff --git a/server/routerlicious/packages/routerlicious-base/src/nexus/runner.ts b/server/routerlicious/packages/routerlicious-base/src/nexus/runner.ts index b42cc04aa541..53d7664941cb 100644 --- a/server/routerlicious/packages/routerlicious-base/src/nexus/runner.ts +++ b/server/routerlicious/packages/routerlicious-base/src/nexus/runner.ts @@ -35,8 +35,8 @@ import * as app from "./app"; import { runnerHttpServerStop } from "@fluidframework/server-services-shared"; export class NexusRunner implements IRunner { - private server: IWebServer; - private runningDeferred: Deferred; + private server?: IWebServer; + private runningDeferred?: Deferred; private stopped: boolean = false; private readonly runnerMetric = Lumberjack.newLumberMetric(LumberEventName.NexusRunner); @@ -93,6 +93,10 @@ export class NexusRunner implements IRunner { "usage:signalUsageCountingEnabled", ); + if (!this.server.webSocketServer) { + throw new Error("WebSocket server is not initialized"); + } + // Register all the socket.io stuff configureWebSocketServices( this.server.webSocketServer, @@ -239,8 +243,8 @@ export class NexusRunner implements IRunner { * Event listener for HTTP server "listening" event. */ private onListening() { - const addr = this.server.httpServer.address(); - const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; + const addr = this.server?.httpServer?.address(); + const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr?.port}`; winston.info(`Listening on ${bind}`); Lumberjack.info(`Listening on ${bind}`); } diff --git a/server/routerlicious/packages/routerlicious-base/src/nexus/runnerFactory.ts b/server/routerlicious/packages/routerlicious-base/src/nexus/runnerFactory.ts index 47c9853265fd..ed432e3b25c0 100644 --- a/server/routerlicious/packages/routerlicious-base/src/nexus/runnerFactory.ts +++ b/server/routerlicious/packages/routerlicious-base/src/nexus/runnerFactory.ts @@ -189,7 +189,7 @@ export class NexusResourcesFactory implements core.IResourcesFactory { - const tenantId = getParam(request.params, "id"); + const tenantId = request.params.id; const includeDisabledTenant = getIncludeDisabledFlag(request); const token = request.body.token; const claims = decode(token) as ITokenClaims; @@ -70,7 +70,7 @@ export function create( * Retrieves details for the given tenant */ router.get("/tenants/:id", (request, response) => { - const tenantId = getParam(request.params, "id"); + const tenantId = request.params.id; const includeDisabledTenant = getIncludeDisabledFlag(request); const tenantP = manager.getTenant(tenantId, includeDisabledTenant); handleResponse(tenantP, response); @@ -89,7 +89,7 @@ export function create( * Retrieves the api key for the tenant */ router.get("/tenants/:id/keys", (request, response) => { - const tenantId = getParam(request.params, "id"); + const tenantId = request.params.id; const includeDisabledTenant = getIncludeDisabledFlag(request); const tenantP = manager.getTenantKeys(tenantId, includeDisabledTenant); handleResponse(tenantP, response); @@ -99,7 +99,8 @@ export function create( * Updates the storage provider for the given tenant */ router.put("/tenants/:id/storage", (request, response) => { - const storageP = manager.updateStorage(getParam(request.params, "id"), request.body); + const tenantId = request.params.id; + const storageP = manager.updateStorage(tenantId, request.body); handleResponse(storageP, response); }); @@ -107,7 +108,8 @@ export function create( * Updates the orderer for the given tenant */ router.put("/tenants/:id/orderer", (request, response) => { - const storageP = manager.updateOrderer(getParam(request.params, "id"), request.body); + const tenantId = request.params.id; + const storageP = manager.updateOrderer(tenantId, request.body); handleResponse(storageP, response); }); @@ -115,7 +117,7 @@ export function create( * Updates the customData for the given tenant */ router.put("/tenants/:id/customData", (request, response) => { - const tenantId = getParam(request.params, "id"); + const tenantId = request.params.id; const customDataP = manager.updateCustomData(tenantId, request.body); handleResponse(customDataP, response); }); @@ -124,7 +126,7 @@ export function create( * Refreshes the key for the given tenant */ router.put("/tenants/:id/key", (request, response) => { - const tenantId = getParam(request.params, "id"); + const tenantId = request.params.id; const keyName = request.body.keyName as string; const refreshKeyP = manager.refreshTenantKey(tenantId, keyName); handleResponse(refreshKeyP, response); @@ -134,7 +136,7 @@ export function create( * Creates a new tenant */ router.post("/tenants/:id?", (request, response) => { - const tenantId = getParam(request.params, "id") || getRandomName("-"); + const tenantId = request.params.id ?? getRandomName("-"); const tenantStorage: ITenantStorage = request.body.storage ? request.body.storage : null; const tenantOrderer: ITenantOrderer = request.body.orderer ? request.body.orderer : null; const tenantCustomData: ITenantCustomData = request.body.customData @@ -153,11 +155,11 @@ export function create( * Deletes a tenant */ router.delete("/tenants/:id", (request, response) => { - const tenantId = getParam(request.params, "id"); + const tenantId = request.params.id; const scheduledDeletionTimeStr = request.body.scheduledDeletionTime; const scheduledDeletionTime = scheduledDeletionTimeStr ? new Date(scheduledDeletionTimeStr) - : null; + : undefined; const tenantP = manager.deleteTenant(tenantId, scheduledDeletionTime); handleResponse(tenantP, response); }); diff --git a/server/routerlicious/packages/routerlicious-base/src/riddler/mongoTenantRepository.ts b/server/routerlicious/packages/routerlicious-base/src/riddler/mongoTenantRepository.ts index 541b8e4405c1..76c389991067 100644 --- a/server/routerlicious/packages/routerlicious-base/src/riddler/mongoTenantRepository.ts +++ b/server/routerlicious/packages/routerlicious-base/src/riddler/mongoTenantRepository.ts @@ -30,7 +30,8 @@ export interface ITenantRepository { * @param options - optional. If set, provide customized options to the implementations * @returns The value of the query in the database. */ - findOne(query: any, options?: any): Promise; + // eslint-disable-next-line @rushstack/no-new-null + findOne(query: any, options?: any): Promise; /** * Finds the query in the database. If it exists, update the value to set. @@ -63,7 +64,7 @@ export class MongoTenantRepository implements ITenantRepository { async find(query: any, sort: any, limit?: number, skip?: number): Promise { return this.collection.find(query, sort, limit, skip); } - async findOne(query: any, options?: any): Promise { + async findOne(query: any, options?: any): Promise { return this.collection.findOne(query, options); } async update(filter: any, set: any, addToSet: any, options?: any): Promise { diff --git a/server/routerlicious/packages/routerlicious-base/src/riddler/runner.ts b/server/routerlicious/packages/routerlicious-base/src/riddler/runner.ts index 85f8cda07c4b..ac5a7181495c 100644 --- a/server/routerlicious/packages/routerlicious-base/src/riddler/runner.ts +++ b/server/routerlicious/packages/routerlicious-base/src/riddler/runner.ts @@ -24,8 +24,8 @@ import { ITenantRepository } from "./mongoTenantRepository"; * @internal */ export class RiddlerRunner implements IRunner { - private server: IWebServer; - private runningDeferred: Deferred; + private server?: IWebServer; + private runningDeferred?: Deferred; private stopped: boolean = false; private readonly runnerMetric = Lumberjack.newLumberMetric(LumberEventName.RiddlerRunner); @@ -51,7 +51,7 @@ export class RiddlerRunner implements IRunner { public start(): Promise { this.runningDeferred = new Deferred(); - const usingClusterModule: boolean | undefined = this.config.get("riddler:useNodeCluster"); + const usingClusterModule: boolean | undefined = this.config?.get("riddler:useNodeCluster"); // Don't include application logic in primary thread when Node.js cluster module is enabled. const includeAppLogic = !(cluster.isPrimary && usingClusterModule); @@ -75,7 +75,7 @@ export class RiddlerRunner implements IRunner { this.server = this.serverFactory.create(riddler); } else { - this.server = this.serverFactory.create(null); + this.server = this.serverFactory.create(undefined); } const httpServer = this.server.httpServer; @@ -149,8 +149,8 @@ export class RiddlerRunner implements IRunner { * Event listener for HTTP server "listening" event. */ private onListening() { - const addr = this.server.httpServer.address(); - const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; + const addr = this.server?.httpServer?.address(); + const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr?.port}`; Lumberjack.info(`Listening on ${bind}`); } } diff --git a/server/routerlicious/packages/routerlicious-base/src/riddler/runnerFactory.ts b/server/routerlicious/packages/routerlicious-base/src/riddler/runnerFactory.ts index 8f6ebafb2437..c5e9cd990918 100644 --- a/server/routerlicious/packages/routerlicious-base/src/riddler/runnerFactory.ts +++ b/server/routerlicious/packages/routerlicious-base/src/riddler/runnerFactory.ts @@ -15,6 +15,7 @@ import { IRunner, IRunnerFactory, IWebServerFactory, + IReadinessCheck, } from "@fluidframework/server-services-core"; import * as utils from "@fluidframework/server-services-utils"; import { Provider } from "nconf"; @@ -24,7 +25,6 @@ import { RiddlerRunner } from "./runner"; import { ITenantDocument } from "./tenantManager"; import { IRiddlerResourcesCustomizations } from "./customizations"; import { ITenantRepository, MongoTenantRepository } from "./mongoTenantRepository"; -import { IReadinessCheck } from "@fluidframework/server-services-core"; import { StartupCheck } from "@fluidframework/server-services-shared"; /** @@ -48,7 +48,7 @@ export class RiddlerResources implements IResources { public readonly riddlerStorageRequestMetricIntervalMs: number, public readonly tenantKeyGenerator: utils.ITenantKeyGenerator, public readonly startupCheck: IReadinessCheck, - public readonly cache: RedisCache, + public readonly cache?: RedisCache, public readonly readinessCheck?: IReadinessCheck, ) { const httpServerConfig: services.IHttpServerConfig = config.get("system:httpServer"); @@ -76,7 +76,7 @@ export class RiddlerResourcesFactory implements IResourcesFactory { // Cache connection const redisConfig = config.get("redisForTenantCache"); - let cache: RedisCache; + let cache: RedisCache | undefined; if (redisConfig) { const redisParams = { expireAfterSeconds: redisConfig.keyExpireAfterSeconds as number | undefined, @@ -114,7 +114,12 @@ export class RiddlerResourcesFactory implements IResourcesFactory { await this.tenantRepository.update({ _id: tenantId }, { orderer }, null); - return (await this.getTenantDocument(tenantId)).orderer; + const tenantDocument = await this.getTenantDocument(tenantId); + + if (tenantDocument === undefined) { + Lumberjack.error("Could not find tenantId after updating orderer.", { + [BaseTelemetryProperties.tenantId]: tenantId, + }); + throw new NetworkError(404, `Could not find updated tenant: ${tenantId}`); + } + + return tenantDocument.orderer; } /** @@ -366,7 +384,13 @@ export class TenantManager { this.tenantRepository.update({ _id: tenantId }, { customData }, null), ); const tenantDocument = await this.getTenantDocument(tenantId, true); - if (tenantDocument.disabled) { + if (tenantDocument === undefined) { + Lumberjack.error("Could not find tenantId after updating custom data.", { + [BaseTelemetryProperties.tenantId]: tenantId, + }); + throw new NetworkError(404, `Could not find updated tenant: ${tenantId}`); + } + if (tenantDocument.disabled === true) { Lumberjack.info("Updated custom data of a disabled tenant", { [BaseTelemetryProperties.tenantId]: tenantId, }); @@ -474,7 +498,7 @@ export class TenantManager { key2: tenantKey2, }; } catch (error) { - Lumberjack.error(`Error getting tenant keys.`, error); + Lumberjack.error(`Error getting tenant keys.`, lumberProperties, error); throw error; } } @@ -488,6 +512,12 @@ export class TenantManager { } const tenantDocument = await this.getTenantDocument(tenantId, false); + if (tenantDocument === undefined) { + Lumberjack.error(`Could not find tenantId when refreshing tenant key.`, { + [BaseTelemetryProperties.tenantId]: tenantId, + }); + throw new NetworkError(404, `Could not find tenantId: ${tenantId}`); + } const newTenantKey = this.tenantKeyGenerator.generateTenantKey(); const encryptionKeyVersion = tenantDocument.customData?.encryptionKeyVersion; @@ -649,12 +679,12 @@ export class TenantManager { private async getTenantDocument( tenantId: string, includeDisabledTenant = false, - ): Promise { + ): Promise { const found = await this.runWithDatabaseRequestCounter(async () => this.tenantRepository.findOne({ _id: tenantId }), ); if (!found || (found.disabled && !includeDisabledTenant)) { - return null; + return undefined; } this.attachDefaultsToTenantDocument(found); @@ -799,7 +829,7 @@ export class TenantManager { } } - private async getKeyFromCache(tenantId: string): Promise { + private async getKeyFromCache(tenantId: string): Promise { try { const cachedKey = await this.runWithCacheRequestCounter( async () => this.cache?.get(`tenantKeys:${tenantId}`), @@ -814,9 +844,13 @@ export class TenantManager { FetchTenantKeyMetric.RetrieveFromCacheSucess, ); } - return cachedKey; + return cachedKey ?? undefined; } catch (error) { - Lumberjack.error(`Error trying to retreive tenant keys from the cache.`, error); + Lumberjack.error( + `Error trying to retreive tenant keys from the cache.`, + { [BaseTelemetryProperties.tenantId]: tenantId }, + error, + ); this.fetchTenantKeyApiCounter.incrementCounter( FetchTenantKeyMetric.RetrieveFromCacheError, ); @@ -825,9 +859,15 @@ export class TenantManager { } private async deleteKeyFromCache(tenantId: string): Promise { - return this.runWithCacheRequestCounter( - async () => this.cache?.delete(`tenantKeys:${tenantId}`), - ); + return this.runWithCacheRequestCounter(async () => { + if (this.cache?.delete === undefined) { + Lumberjack.warning("Cache delete method is not implemented.", { + [BaseTelemetryProperties.tenantId]: tenantId, + }); + return false; + } + return this.cache.delete(`tenantKeys:${tenantId}`); + }); } private async setKeyInCache(tenantId: string, value: IEncryptedTenantKeys): Promise { diff --git a/server/routerlicious/packages/routerlicious-base/src/test/alfred/api.spec.ts b/server/routerlicious/packages/routerlicious-base/src/test/alfred/api.spec.ts index 7b3b8b5b198d..b396daea032e 100644 --- a/server/routerlicious/packages/routerlicious-base/src/test/alfred/api.spec.ts +++ b/server/routerlicious/packages/routerlicious-base/src/test/alfred/api.spec.ts @@ -191,8 +191,8 @@ describe("Routerlicious", () => { defaultDocumentRepository, defaultDocumentDeleteService, startupCheck, - null, - null, + undefined, + undefined, defaultCollaborationSessionEventEmitter, undefined, undefined, @@ -204,12 +204,13 @@ describe("Routerlicious", () => { const assertThrottle = async ( url: string, - token: string | (() => string), - body: any, + token: string | (() => string) | undefined, + body: any | undefined, method: "get" | "post" | "patch" = "get", limit: number = limitTenant, ): Promise => { - const tokenProvider = typeof token === "function" ? token : () => token; + const tokenProvider = + typeof token === "function" ? token : () => token ?? "no-token"; for (let i = 0; i < limit; i++) { // we're not interested in making the requests succeed with 200s, so just assert that not 429 await supertest[method](url) @@ -227,29 +228,29 @@ describe("Routerlicious", () => { describe("/api/v1", () => { it("/ping", async () => { - await assertThrottle("/api/v1/ping", null, null); + await assertThrottle("/api/v1/ping", undefined, undefined); }); it("/tenants/:tenantid/accesstoken", async () => { await assertThrottle( `/api/v1/tenants/${appTenant1.id}/accesstoken`, "Bearer 12345", // Dummy bearer token - null, + undefined, "post", ); }); it("/:tenantId/:id/root", async () => { await assertThrottle( `/api/v1/${appTenant1.id}/${document1._id}/root`, - null, - null, + undefined, + undefined, "patch", ); }); it("/:tenantId/:id/blobs", async () => { await assertThrottle( `/api/v1/${appTenant1.id}/${document1._id}/blobs`, - null, - null, + undefined, + undefined, "post", ); }); @@ -260,12 +261,12 @@ describe("Routerlicious", () => { await assertThrottle( `/documents/${appTenant2.id}/${document1._id}`, tenantToken2, - null, + undefined, ); await assertThrottle( `/documents/${appTenant1.id}/${document1._id}`, tenantToken1, - null, + undefined, ); await supertest .get(`/documents/${appTenant1.id}/${document1._id}`) @@ -290,12 +291,12 @@ describe("Routerlicious", () => { await assertThrottle( `/deltas/raw/${appTenant2.id}/${document1._id}`, tenantToken2, - null, + undefined, ); await assertThrottle( `/deltas/raw/${appTenant1.id}/${document1._id}`, tenantToken1, - null, + undefined, ); await supertest .get(`/deltas/raw/${appTenant1.id}/${document1._id}`) @@ -306,7 +307,7 @@ describe("Routerlicious", () => { await assertThrottle( `/deltas/${appTenant1.id}/${document1._id}`, tenantToken1, - null, + undefined, "get", limitGetDeltas, ); @@ -319,12 +320,12 @@ describe("Routerlicious", () => { await assertThrottle( `/deltas/v1/${appTenant2.id}/${document1._id}`, tenantToken2, - null, + undefined, ); await assertThrottle( `/deltas/v1/${appTenant1.id}/${document1._id}`, tenantToken1, - null, + undefined, ); await supertest .get(`/deltas/v1/${appTenant1.id}/${document1._id}`) @@ -335,12 +336,12 @@ describe("Routerlicious", () => { await assertThrottle( `/deltas/${appTenant2.id}/${document1._id}/v1`, tenantToken2, - null, + undefined, ); await assertThrottle( `/deltas/${appTenant1.id}/${document1._id}/v1`, tenantToken1, - null, + undefined, ); await supertest .get(`/deltas/${appTenant1.id}/${document1._id}/v1`) @@ -407,8 +408,8 @@ describe("Routerlicious", () => { defaultDocumentRepository, defaultDocumentDeleteService, startupCheck, - null, - null, + undefined, + undefined, defaultCollaborationSessionEventEmitter, undefined, undefined, @@ -616,8 +617,8 @@ describe("Routerlicious", () => { defaultDocumentRepository, defaultDocumentDeleteService, startupCheck, - null, - null, + undefined, + undefined, defaultCollaborationSessionEventEmitter, undefined, undefined, @@ -971,8 +972,8 @@ describe("Routerlicious", () => { defaultDocumentRepository, defaultDocumentDeleteService, startupCheck, - null, - null, + undefined, + undefined, defaultCollaborationSessionEventEmitter, undefined, undefined, diff --git a/server/routerlicious/packages/routerlicious-base/src/test/nexus/io.spec.ts b/server/routerlicious/packages/routerlicious-base/src/test/nexus/io.spec.ts index 8991a98f9898..89cbbfffe79f 100644 --- a/server/routerlicious/packages/routerlicious-base/src/test/nexus/io.spec.ts +++ b/server/routerlicious/packages/routerlicious-base/src/test/nexus/io.spec.ts @@ -23,7 +23,7 @@ import { import { KafkaOrdererFactory } from "@fluidframework/server-kafka-orderer"; import { LocalWebSocket, LocalWebSocketServer } from "@fluidframework/server-local-server"; import { configureWebSocketServices } from "@fluidframework/server-lambdas"; -import { PubSub } from "@fluidframework/server-memory-orderer"; +import { LocalOrderManager, PubSub } from "@fluidframework/server-memory-orderer"; import * as services from "@fluidframework/server-services"; import { generateToken } from "@fluidframework/server-services-utils"; import { @@ -154,7 +154,7 @@ describe("Routerlicious", () => { false, url, testTenantManager, - null, + null as unknown as LocalOrderManager, kafkaOrderer, ); @@ -1010,7 +1010,7 @@ describe("Routerlicious", () => { }); // generate a batch of messages const generateMessageBatch = (size: number): IDocumentMessage[] => { - const batch = []; + const batch: IDocumentMessage[] = []; for (let b = 0; b < size; b++) { const message = messageFactory.createDocumentMessage(); batch.push(message); @@ -1123,7 +1123,7 @@ Submitted Messages: ${JSON.stringify(messages, undefined, 2)}`, false, url, testTenantManager, - null, + null as unknown as LocalOrderManager, kafkaOrderer, ); diff --git a/server/routerlicious/packages/routerlicious-base/src/test/riddler/api.spec.ts b/server/routerlicious/packages/routerlicious-base/src/test/riddler/api.spec.ts index d1a482f40784..34bf3d0ba7d1 100644 --- a/server/routerlicious/packages/routerlicious-base/src/test/riddler/api.spec.ts +++ b/server/routerlicious/packages/routerlicious-base/src/test/riddler/api.spec.ts @@ -28,7 +28,7 @@ class TestSecretManager implements ISecretManager { constructor(private readonly encryptionKey: string) {} public getLatestKeyVersion(): EncryptionKeyVersion { - return undefined; + return EncryptionKeyVersion.key2022; } public decryptSecret(encryptedSecret: string): string { diff --git a/server/routerlicious/packages/routerlicious-base/src/test/types/validateServerRouterliciousBasePrevious.generated.ts b/server/routerlicious/packages/routerlicious-base/src/test/types/validateServerRouterliciousBasePrevious.generated.ts index 4f78ba8220de..63ff785c823a 100644 --- a/server/routerlicious/packages/routerlicious-base/src/test/types/validateServerRouterliciousBasePrevious.generated.ts +++ b/server/routerlicious/packages/routerlicious-base/src/test/types/validateServerRouterliciousBasePrevious.generated.ts @@ -529,6 +529,7 @@ declare function get_current_ClassDeclaration_RiddlerResources(): declare function use_old_ClassDeclaration_RiddlerResources( use: TypeOnly): void; use_old_ClassDeclaration_RiddlerResources( + // @ts-expect-error compatibility expected to be broken get_current_ClassDeclaration_RiddlerResources()); /* diff --git a/server/routerlicious/packages/routerlicious-base/src/utils/documentRouter.ts b/server/routerlicious/packages/routerlicious-base/src/utils/documentRouter.ts index 2e5af6a2a25f..861bf7a390e3 100644 --- a/server/routerlicious/packages/routerlicious-base/src/utils/documentRouter.ts +++ b/server/routerlicious/packages/routerlicious-base/src/utils/documentRouter.ts @@ -6,6 +6,7 @@ import { DocumentLambdaFactory } from "@fluidframework/server-lambdas-driver"; import { DefaultServiceConfiguration, + IPartitionLambdaConfig, IPartitionLambdaFactory, } from "@fluidframework/server-services-core"; import nconf from "nconf"; @@ -22,16 +23,16 @@ export interface IPlugin { create( config: nconf.Provider, customizations?: Record, - ): Promise; + ): Promise>; } /** * @internal */ -export async function createDocumentRouter( +export async function createDocumentRouter( config: nconf.Provider, customizations?: Record, -): Promise { +): Promise> { const pluginConfig = config.get("documentLambda") as string | object; const plugin = // eslint-disable-next-line @typescript-eslint/no-require-imports (typeof pluginConfig === "object" ? pluginConfig : require(pluginConfig)) as IPlugin; @@ -39,5 +40,5 @@ export async function createDocumentRouter( // Factory used to create document lambda processors const factory = await plugin.create(config, customizations); - return new DocumentLambdaFactory(factory, DefaultServiceConfiguration.documentLambda); + return new DocumentLambdaFactory(factory, DefaultServiceConfiguration.documentLambda); } diff --git a/server/routerlicious/packages/routerlicious-base/src/utils/sessionHelper.ts b/server/routerlicious/packages/routerlicious-base/src/utils/sessionHelper.ts index ad984b7aec7f..c91f11844f7a 100644 --- a/server/routerlicious/packages/routerlicious-base/src/utils/sessionHelper.ts +++ b/server/routerlicious/packages/routerlicious-base/src/utils/sessionHelper.ts @@ -207,7 +207,7 @@ async function updateExistingSession( `The document with isSessionAlive as false does not exist`, lumberjackProperties, ); - const doc: IDocument = await runWithRetry( + const doc = await runWithRetry( async () => documentRepository.readOne({ tenantId, @@ -221,7 +221,7 @@ async function updateExistingSession( undefined /* shouldIgnoreError */, (error) => true /* shouldRetry */, ); - if (!doc && !doc.session) { + if (!doc?.session) { Lumberjack.error( `Error running getSession from document: ${JSON.stringify(doc)}`, lumberjackProperties, @@ -287,8 +287,8 @@ export async function getSession( ): Promise { const baseLumberjackProperties = getLumberBaseProperties(documentId, tenantId); - const document: IDocument = await documentRepository.readOne({ tenantId, documentId }); - if (!document || document.scheduledDeletionTime !== undefined) { + const document = await documentRepository.readOne({ tenantId, documentId }); + if (!document || document?.scheduledDeletionTime !== undefined) { connectionTrace?.stampStage("DocumentDoesNotExist"); throw new NetworkError(404, "Document is deleted and cannot be accessed."); } diff --git a/server/routerlicious/packages/routerlicious-base/tsconfig.json b/server/routerlicious/packages/routerlicious-base/tsconfig.json index 71c4f9f9f3d5..7dfc976aeba3 100644 --- a/server/routerlicious/packages/routerlicious-base/tsconfig.json +++ b/server/routerlicious/packages/routerlicious-base/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["dist", "node_modules"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "types": ["mocha"], diff --git a/server/routerlicious/packages/routerlicious/src/deli/index.ts b/server/routerlicious/packages/routerlicious/src/deli/index.ts index e4ac613d6e00..fd502361219f 100644 --- a/server/routerlicious/packages/routerlicious/src/deli/index.ts +++ b/server/routerlicious/packages/routerlicious/src/deli/index.ts @@ -24,7 +24,7 @@ import { export async function deliCreate( config: Provider, customizations?: Record, -): Promise { +): Promise> { const kafkaEndpoint = config.get("kafka:lib:endpoint"); const kafkaLibrary = config.get("kafka:lib:name"); const kafkaProducerPollIntervalMs = config.get("kafka:lib:producerPollIntervalMs"); @@ -86,17 +86,22 @@ export async function deliCreate( core.DefaultServiceConfiguration.deli.ephemeralContainerSoftDeleteTimeInMs = ephemeralContainerSoftDeleteTimeInMs; - let globalDb: core.IDb; + let globalDb: core.IDb | undefined; if (globalDbEnabled) { const globalDbReconnect = (config.get("mongo:globalDbReconnect") as boolean) ?? false; - const globalDbManager = new core.MongoManager(factory, globalDbReconnect, null, true); + const globalDbManager = new core.MongoManager( + factory, + globalDbReconnect, + undefined /* reconnectDelayMs */, + true /* global */, + ); globalDb = await globalDbManager.getDatabase(); } const operationsDbManager = new core.MongoManager(factory, false); const operationsDb = await operationsDbManager.getDatabase(); - const db: core.IDb = globalDbEnabled ? globalDb : operationsDb; + const db: core.IDb = globalDbEnabled && globalDb !== undefined ? globalDb : operationsDb; // eslint-disable-next-line @typescript-eslint/await-thenable const collection = await db.collection(documentsCollectionName); diff --git a/server/routerlicious/packages/routerlicious/src/scribe/index.ts b/server/routerlicious/packages/routerlicious/src/scribe/index.ts index cbbc3895174a..94efe7df3bba 100644 --- a/server/routerlicious/packages/routerlicious/src/scribe/index.ts +++ b/server/routerlicious/packages/routerlicious/src/scribe/index.ts @@ -29,7 +29,7 @@ import { Provider } from "nconf"; export async function scribeCreate( config: Provider, customizations?: Record, -): Promise { +): Promise> { // Access config values const globalDbEnabled = config.get("mongo:globalDbEnabled") as boolean; const documentsCollectionName = config.get("mongo:collectionNames:documents"); @@ -87,7 +87,12 @@ export async function scribeCreate( let globalDb; if (globalDbEnabled) { const globalDbReconnect = (config.get("mongo:globalDbReconnect") as boolean) ?? false; - const globalDbMongoManager = new MongoManager(factory, globalDbReconnect, null, true); + const globalDbMongoManager = new MongoManager( + factory, + globalDbReconnect, + undefined /* reconnectDelayMs */, + true /* global */, + ); globalDb = await globalDbMongoManager.getDatabase(); } @@ -124,7 +129,7 @@ export async function scribeCreate( ); } - if (mongoExpireAfterSeconds > 0) { + if (mongoExpireAfterSeconds > 0 && scribeDeltas.createTTLIndex !== undefined) { await (createCosmosDBIndexes ? scribeDeltas.createTTLIndex({ _ts: 1 }, mongoExpireAfterSeconds) : scribeDeltas.createTTLIndex({ mongoTimestamp: 1 }, mongoExpireAfterSeconds)); diff --git a/server/routerlicious/packages/routerlicious/src/scriptorium/index.ts b/server/routerlicious/packages/routerlicious/src/scriptorium/index.ts index d9d51fe100fa..f58d4a15574a 100644 --- a/server/routerlicious/packages/routerlicious/src/scriptorium/index.ts +++ b/server/routerlicious/packages/routerlicious/src/scriptorium/index.ts @@ -77,7 +77,7 @@ export async function create( ); } - if (mongoExpireAfterSeconds > 0) { + if (mongoExpireAfterSeconds > 0 && opCollection.createTTLIndex !== undefined) { await (createCosmosDBIndexes ? opCollection.createTTLIndex({ _ts: 1 }, mongoExpireAfterSeconds) : opCollection.createTTLIndex({ mongoTimestamp: 1 }, mongoExpireAfterSeconds)); @@ -88,7 +88,12 @@ export async function create( // Database connection for global db if enabled if (globalDbEnabled) { const globalDbReconnect = (config.get("mongo:globalDbReconnect") as boolean) ?? false; - const globalDbMongoManager = new MongoManager(factory, globalDbReconnect, null, true); + const globalDbMongoManager = new MongoManager( + factory, + globalDbReconnect, + undefined /* reconnectDelayMs */, + true /* global */, + ); globalDb = await globalDbMongoManager.getDatabase(); } const documentsCollectionDb = globalDbEnabled ? globalDb : operationsDb; @@ -100,7 +105,7 @@ export async function create( // Required for checkpoint service const checkpointRepository = new MongoCheckpointRepository( operationsDb.collection(checkpointsCollectionName), - undefined /* checkpoint type */, + "scriptorium" /* checkpoint type */, ); const isLocalCheckpointEnabled = config.get("checkpoints: localCheckpointEnabled"); diff --git a/server/routerlicious/packages/routerlicious/tsconfig.json b/server/routerlicious/packages/routerlicious/tsconfig.json index f8b4ceaf8144..196fe944f886 100644 --- a/server/routerlicious/packages/routerlicious/tsconfig.json +++ b/server/routerlicious/packages/routerlicious/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["src/test/**/*"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "composite": true, diff --git a/server/routerlicious/packages/services-client/api-report/server-services-client.api.md b/server/routerlicious/packages/services-client/api-report/server-services-client.api.md index 7b3452dbd0be..84f772f8cfaf 100644 --- a/server/routerlicious/packages/services-client/api-report/server-services-client.api.md +++ b/server/routerlicious/packages/services-client/api-report/server-services-client.api.md @@ -25,7 +25,7 @@ import { SummaryObject } from '@fluidframework/protocol-definitions'; // @internal (undocumented) export class BasicRestWrapper extends RestWrapper { - constructor(baseurl?: string, defaultQueryString?: Record, maxBodyLength?: number, maxContentLength?: number, defaultHeaders?: RawAxiosRequestHeaders, axios?: AxiosInstance, refreshDefaultQueryString?: () => Record, refreshDefaultHeaders?: () => RawAxiosRequestHeaders, getCorrelationId?: () => string | undefined, getTelemetryContextProperties?: () => Record | undefined); + constructor(baseurl?: string, defaultQueryString?: Record, maxBodyLength?: number, maxContentLength?: number, defaultHeaders?: RawAxiosRequestHeaders, axios?: AxiosInstance, refreshDefaultQueryString?: (() => Record) | undefined, refreshDefaultHeaders?: (() => RawAxiosRequestHeaders) | undefined, getCorrelationId?: (() => string | undefined) | undefined, getTelemetryContextProperties?: (() => Record | undefined) | undefined); // (undocumented) protected request(requestConfig: AxiosRequestConfig, statusCode: number, canRetry?: boolean): Promise; } @@ -152,7 +152,7 @@ export class GitManager implements IGitManager { // (undocumented) getRawUrl(sha: string): string; // (undocumented) - getRef(ref: string): Promise; + getRef(ref: string): Promise; // (undocumented) getSummary(sha: string): Promise; getTree(root: string, recursive?: boolean): Promise; @@ -306,7 +306,7 @@ export interface IGitManager { // (undocumented) getRawUrl(sha: string): string; // (undocumented) - getRef(ref: string): Promise; + getRef(ref: string): Promise; // (undocumented) getSummary(sha: string): Promise; // (undocumented) @@ -344,7 +344,7 @@ export interface IGitService { // (undocumented) getContent(path: string, ref: string): Promise; // (undocumented) - getRef(ref: string): Promise; + getRef(ref: string): Promise; // (undocumented) getRefs(): Promise; // (undocumented) @@ -572,24 +572,24 @@ export class NetworkError extends Error { constructor( code: number, message: string, - canRetry?: boolean, - isFatal?: boolean, - retryAfterMs?: number, - source?: string, - internalErrorCode?: InternalErrorCode); + canRetry?: boolean | undefined, + isFatal?: boolean | undefined, + retryAfterMs?: number | undefined, + source?: string | undefined, + internalErrorCode?: InternalErrorCode | undefined); // @public - readonly canRetry?: boolean; + readonly canRetry?: boolean | undefined; // @public readonly code: number; get details(): INetworkErrorDetails | string; - readonly internalErrorCode?: InternalErrorCode; + readonly internalErrorCode?: InternalErrorCode | undefined; // @public - readonly isFatal?: boolean; - readonly retryAfter: number; + readonly isFatal?: boolean | undefined; + readonly retryAfter?: number; // @public - readonly retryAfterMs?: number; + readonly retryAfterMs?: number | undefined; // @public - readonly source?: string; + readonly source?: string | undefined; toJSON(): INetworkErrorDetails & { code: number; }; @@ -615,15 +615,15 @@ export enum RestLessFieldNames { // @internal (undocumented) export abstract class RestWrapper { - constructor(baseurl?: string, defaultQueryString?: Record, maxBodyLength?: number, maxContentLength?: number); + constructor(baseurl?: string | undefined, defaultQueryString?: Record, maxBodyLength?: number, maxContentLength?: number); // (undocumented) - protected readonly baseurl?: string; + protected readonly baseurl?: string | undefined; // (undocumented) protected defaultQueryString: Record; // (undocumented) delete(url: string, queryString?: Record, headers?: RawAxiosRequestHeaders, additionalOptions?: Partial>): Promise; // (undocumented) - protected generateQueryString(queryStringValues: Record): string; + protected generateQueryString(queryStringValues: Record | undefined): string; // (undocumented) get(url: string, queryString?: Record, headers?: RawAxiosRequestHeaders, additionalOptions?: Partial>): Promise; // (undocumented) diff --git a/server/routerlicious/packages/services-client/package.json b/server/routerlicious/packages/services-client/package.json index 067913c0d834..1019998d4dc5 100644 --- a/server/routerlicious/packages/services-client/package.json +++ b/server/routerlicious/packages/services-client/package.json @@ -95,6 +95,10 @@ "typescript": "~5.1.6" }, "typeValidation": { - "broken": {} + "broken": { + "ClassDeclaration_NetworkError": { + "backCompat": false + } + } } } diff --git a/server/routerlicious/packages/services-client/src/array.ts b/server/routerlicious/packages/services-client/src/array.ts index a2ad71b85b4a..a083c010d7e2 100644 --- a/server/routerlicious/packages/services-client/src/array.ts +++ b/server/routerlicious/packages/services-client/src/array.ts @@ -100,6 +100,9 @@ export function mergeKArrays(arrays: T[][], comparator: (a: T, b: T) => numbe const mergedResult: T[] = []; while (heap.size > 0) { const node = heap.pop(); + if (node === undefined) { + continue; + } mergedResult.push(node.value); const nextIndex = node.index + 1; if (nextIndex < arrays[node.row].length) { diff --git a/server/routerlicious/packages/services-client/src/error.ts b/server/routerlicious/packages/services-client/src/error.ts index f2b11cdad114..eeff1bffdde1 100644 --- a/server/routerlicious/packages/services-client/src/error.ts +++ b/server/routerlicious/packages/services-client/src/error.ts @@ -76,7 +76,7 @@ export class NetworkError extends Error { * Value representing the time in seconds that should be waited before retrying. * TODO: remove in favor of retryAfterMs once driver supports retryAfterMs. */ - public readonly retryAfter: number; + public readonly retryAfter?: number; constructor( /** diff --git a/server/routerlicious/packages/services-client/src/gitManager.ts b/server/routerlicious/packages/services-client/src/gitManager.ts index 20511ce49561..c638b88e2496 100644 --- a/server/routerlicious/packages/services-client/src/gitManager.ts +++ b/server/routerlicious/packages/services-client/src/gitManager.ts @@ -43,9 +43,10 @@ export class GitManager implements IGitManager { } public async getCommit(sha: string): Promise { - if (this.commitCache.has(sha)) { + const cachedCommit = this.commitCache.get(sha); + if (cachedCommit !== undefined) { debug(`Cache hit on ${sha}`); - return this.commitCache.get(sha); + return cachedCommit; } return this.historian.getCommit(sha); @@ -58,9 +59,10 @@ export class GitManager implements IGitManager { let sha = shaOrRef; // See if the sha is really a ref and convert - if (this.refCache.has(shaOrRef)) { + const cachedRef = this.refCache.get(shaOrRef); + if (cachedRef !== undefined) { debug(`Commit cache hit on ${shaOrRef}`); - sha = this.refCache.get(shaOrRef); + sha = cachedRef; // Delete refcache after first use this.refCache.delete(shaOrRef); @@ -72,8 +74,10 @@ export class GitManager implements IGitManager { } // See if the commit sha is hashed and return it if so - if (this.commitCache.has(sha)) { - const commit = this.commitCache.get(sha); + const cachedCommit = this.commitCache.get(sha); + if (cachedCommit !== undefined) { + debug(`Commit cache hit on ${sha}`); + const commit = cachedCommit; return [ { commit: { @@ -98,18 +102,20 @@ export class GitManager implements IGitManager { * Reads the object with the given ID. We defer to the client implementation to do the actual read. */ public async getTree(root: string, recursive = true): Promise { - if (this.treeCache.has(root)) { + const cachedTree = this.treeCache.get(root); + if (cachedTree !== undefined) { debug(`Tree cache hit on ${root}`); - return this.treeCache.get(root); + return cachedTree; } return this.historian.getTree(root, recursive); } public async getBlob(sha: string): Promise { - if (this.blobCache.has(sha)) { + const cachedBlob = this.blobCache.get(sha); + if (cachedBlob !== undefined) { debug(`Blob cache hit on ${sha}`); - return this.blobCache.get(sha); + return cachedBlob; } return this.historian.getBlob(sha); @@ -167,8 +173,8 @@ export class GitManager implements IGitManager { return this.historian.getSummary(sha); } - public async getRef(ref: string): Promise { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + // eslint-disable-next-line @rushstack/no-new-null + public async getRef(ref: string): Promise { return this.historian.getRef(`heads/${ref}`).catch((error) => { if (error === 400 || error === 404) { return null; diff --git a/server/routerlicious/packages/services-client/src/historian.ts b/server/routerlicious/packages/services-client/src/historian.ts index 585d8e307a5c..5fdff9110e99 100644 --- a/server/routerlicious/packages/services-client/src/historian.ts +++ b/server/routerlicious/packages/services-client/src/historian.ts @@ -40,12 +40,13 @@ export const getAuthorizationTokenFromCredentials = (credentials: ICredentials): export class Historian implements IHistorian { private readonly defaultQueryString: Record = {}; private readonly cacheBust: boolean; + private readonly restWrapper: RestWrapper; constructor( public endpoint: string, private readonly historianApi: boolean, disableCache: boolean, - private readonly restWrapper?: RestWrapper, + restWrapper?: RestWrapper, ) { if (disableCache && this.historianApi) { this.defaultQueryString.disableCache = disableCache; @@ -54,9 +55,7 @@ export class Historian implements IHistorian { this.cacheBust = disableCache; } - if (this.restWrapper === undefined) { - this.restWrapper = new BasicRestWrapper(this.endpoint); - } + this.restWrapper = restWrapper ?? new BasicRestWrapper(this.endpoint); } public async getHeader(sha: string): Promise { diff --git a/server/routerlicious/packages/services-client/src/restLessClient.ts b/server/routerlicious/packages/services-client/src/restLessClient.ts index b6f1062e6a4b..15b7f3f0ff59 100644 --- a/server/routerlicious/packages/services-client/src/restLessClient.ts +++ b/server/routerlicious/packages/services-client/src/restLessClient.ts @@ -52,7 +52,8 @@ export class RestLessClient { if ( newRequest.data && - ["post", "put", "patch"].includes(newRequest.method?.toLowerCase()) + newRequest.method !== undefined && + ["post", "put", "patch"].includes(newRequest.method.toLowerCase()) ) { const stringifiedBody = JSON.stringify(newRequest.data); body.append(RestLessFieldNames.Body, stringifiedBody); diff --git a/server/routerlicious/packages/services-client/src/restWrapper.ts b/server/routerlicious/packages/services-client/src/restWrapper.ts index e60d70987255..41be513ea3e2 100644 --- a/server/routerlicious/packages/services-client/src/restWrapper.ts +++ b/server/routerlicious/packages/services-client/src/restWrapper.ts @@ -125,7 +125,9 @@ export abstract class RestWrapper { protected abstract request(options: AxiosRequestConfig, statusCode: number): Promise; - protected generateQueryString(queryStringValues: Record) { + protected generateQueryString( + queryStringValues: Record | undefined, + ) { if (this.defaultQueryString || queryStringValues) { const queryStringRecord = { ...this.defaultQueryString, ...queryStringValues }; @@ -223,7 +225,7 @@ export class BasicRestWrapper extends RestWrapper { const retryConfig = { ...requestConfig }; retryConfig.headers = this.generateHeaders( retryConfig.headers, - options.headers[CorrelationIdHeaderName] as string, + options.headers?.[CorrelationIdHeaderName], ); this.request(retryConfig, statusCode, false).then(resolve).catch(reject); diff --git a/server/routerlicious/packages/services-client/src/storage.ts b/server/routerlicious/packages/services-client/src/storage.ts index 4adcf5a07761..51139fa379ff 100644 --- a/server/routerlicious/packages/services-client/src/storage.ts +++ b/server/routerlicious/packages/services-client/src/storage.ts @@ -73,7 +73,7 @@ export interface IGitService { getCommit(sha: string): Promise; createCommit(commit: git.ICreateCommitParams): Promise; getRefs(): Promise; - getRef(ref: string): Promise; + getRef(ref: string): Promise; createRef(params: git.ICreateRefParams): Promise; updateRef(ref: string, params: git.IPatchRefParams): Promise; deleteRef(ref: string): Promise; @@ -117,7 +117,8 @@ export interface IGitManager { createGitTree(params: git.ICreateTreeParams): Promise; createTree(files: api.ITree): Promise; createCommit(commit: git.ICreateCommitParams): Promise; - getRef(ref: string): Promise; + // eslint-disable-next-line @rushstack/no-new-null + getRef(ref: string): Promise; createRef(branch: string, sha: string): Promise; upsertRef(branch: string, commitSha: string): Promise; write( diff --git a/server/routerlicious/packages/services-client/src/storageUtils.ts b/server/routerlicious/packages/services-client/src/storageUtils.ts index 09557bf7ef95..e9a9e5fe5b8f 100644 --- a/server/routerlicious/packages/services-client/src/storageUtils.ts +++ b/server/routerlicious/packages/services-client/src/storageUtils.ts @@ -140,7 +140,7 @@ export function convertSummaryTreeToWholeSummaryTree( throw new Error(`Invalid tree entry for ${summaryObject.type}`); } - wholeSummaryTree.entries.push(entry); + wholeSummaryTree.entries?.push(entry); } return wholeSummaryTree; @@ -226,7 +226,7 @@ export function convertWholeFlatSummaryToSnapshotTreeAndBlobs( * @returns Whether the value is of IWholeSummaryTreeEntry type */ function isWholeSummaryTreeValueEntry(obj: any): obj is IWholeSummaryTreeValueEntry { - return obj && typeof obj === "object" && "value" in obj; + return typeof obj === "object" && obj !== null && "value" in obj; } /** @@ -235,7 +235,7 @@ function isWholeSummaryTreeValueEntry(obj: any): obj is IWholeSummaryTreeValueEn * @returns Whether the value is of IWholeSummaryBlob type */ function isWholeSummaryBlob(obj: unknown): obj is IWholeSummaryBlob { - return obj && typeof obj === "object" && "content" in obj; + return typeof obj === "object" && obj !== null && "content" in obj; } /** @@ -244,7 +244,7 @@ function isWholeSummaryBlob(obj: unknown): obj is IWholeSummaryBlob { * @returns Whether the value is of IWholeSummaryBlob type */ function isWholeSummaryTree(obj: any): obj is IWholeSummaryTree { - return obj && typeof obj === "object" && "type" in obj; + return typeof obj === "object" && obj !== null && "type" in obj; } /** @@ -258,7 +258,7 @@ export function convertFirstSummaryWholeSummaryTreeToSummaryTree( unreferenced?: true | undefined, ): ISummaryTree { const tree: { [path: string]: SummaryObject } = {}; - for (const entry of wholeSummaryTree.entries) { + for (const entry of wholeSummaryTree.entries ?? []) { switch (entry.type) { case "blob": { assert(isWholeSummaryTreeValueEntry(entry), "Invalid entry type"); diff --git a/server/routerlicious/packages/services-client/src/test/types/validateServerServicesClientPrevious.generated.ts b/server/routerlicious/packages/services-client/src/test/types/validateServerServicesClientPrevious.generated.ts index 282da6bf3249..344800399eda 100644 --- a/server/routerlicious/packages/services-client/src/test/types/validateServerServicesClientPrevious.generated.ts +++ b/server/routerlicious/packages/services-client/src/test/types/validateServerServicesClientPrevious.generated.ts @@ -1053,6 +1053,7 @@ declare function get_current_ClassDeclaration_NetworkError(): declare function use_old_ClassDeclaration_NetworkError( use: TypeOnly): void; use_old_ClassDeclaration_NetworkError( + // @ts-expect-error compatibility expected to be broken get_current_ClassDeclaration_NetworkError()); /* diff --git a/server/routerlicious/packages/services-client/src/wholeSummaryUploadManager.ts b/server/routerlicious/packages/services-client/src/wholeSummaryUploadManager.ts index b7c8cb565dab..0856c821bd8e 100644 --- a/server/routerlicious/packages/services-client/src/wholeSummaryUploadManager.ts +++ b/server/routerlicious/packages/services-client/src/wholeSummaryUploadManager.ts @@ -48,8 +48,8 @@ export class WholeSummaryUploadManager implements ISummaryUploadManager { type === "channel" ? ".app" : "", ); const snapshotPayload: IWholeSummaryPayload = { - entries: snapshotTree.entries, - message: undefined, + entries: snapshotTree.entries ?? [], + message: `${type} summary upload`, sequenceNumber, type, }; diff --git a/server/routerlicious/packages/services-client/tsconfig.json b/server/routerlicious/packages/services-client/tsconfig.json index 58195bcd4eae..bd48d16467b2 100644 --- a/server/routerlicious/packages/services-client/tsconfig.json +++ b/server/routerlicious/packages/services-client/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["src/test/**/*"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "composite": true, diff --git a/server/routerlicious/packages/services-core/package.json b/server/routerlicious/packages/services-core/package.json index 1da48245b511..884ef0d4392d 100644 --- a/server/routerlicious/packages/services-core/package.json +++ b/server/routerlicious/packages/services-core/package.json @@ -79,6 +79,18 @@ }, "ClassDeclaration_MongoManager": { "forwardCompat": false + }, + "ClassDeclaration_TokenRevokedError": { + "backCompat": false + }, + "ClassDeclaration_TokenRevocationError": { + "backCompat": false + }, + "InterfaceDeclaration_IBoxcarMessage": { + "backCompat": false + }, + "InterfaceDeclaration_IWebServer": { + "backCompat": false } } } diff --git a/server/routerlicious/packages/services-core/src/cache.ts b/server/routerlicious/packages/services-core/src/cache.ts index 717723c0ea14..6cf92b05cbb4 100644 --- a/server/routerlicious/packages/services-core/src/cache.ts +++ b/server/routerlicious/packages/services-core/src/cache.ts @@ -11,7 +11,8 @@ export interface ICache { /** * Retrieves the cached entry for the given key. Or null if it doesn't exist. */ - get(key: string): Promise; + // eslint-disable-next-line @rushstack/no-new-null + get(key: string): Promise; /** * Sets a cache value diff --git a/server/routerlicious/packages/services-core/src/checkpointService.ts b/server/routerlicious/packages/services-core/src/checkpointService.ts index f99186cbac5a..d94a4816aa3b 100644 --- a/server/routerlicious/packages/services-core/src/checkpointService.ts +++ b/server/routerlicious/packages/services-core/src/checkpointService.ts @@ -172,7 +172,7 @@ export class CheckpointService implements ICheckpointService { } } - async clearCheckpoint( + public async clearCheckpoint( documentId: string, tenantId: string, service: DocumentLambda, @@ -199,12 +199,12 @@ export class CheckpointService implements ICheckpointService { })); } - async restoreFromCheckpoint( + public async restoreFromCheckpoint( documentId: string, tenantId: string, service: DocumentLambda, document: IDocument, - ) { + ): Promise { let lastCheckpoint: IDeliState | IScribe | undefined; let isLocalCheckpoint = false; let localLogOffset: number | undefined; @@ -225,20 +225,21 @@ export class CheckpointService implements ICheckpointService { if (!this.localCheckpointEnabled || !this.checkpointRepository) { // If we cannot checkpoint locally, use document lastCheckpoint = parseCheckpointString(document[service]); - globalLogOffset = lastCheckpoint.logOffset; - globalSequenceNumber = lastCheckpoint.sequenceNumber; + globalLogOffset = lastCheckpoint?.logOffset; + globalSequenceNumber = lastCheckpoint?.sequenceNumber; checkpointSource = "defaultGlobalCollection"; } else { // Search checkpoints collection for checkpoint - const checkpoint: ICheckpoint | undefined = await this.checkpointRepository - .getCheckpoint(documentId, tenantId) - .catch((error) => { - Lumberjack.error( - `Error retrieving local checkpoint`, - getLumberBaseProperties(documentId, tenantId), - ); - return undefined; - }); + const checkpoint: ICheckpoint | undefined = + (await this.checkpointRepository + .getCheckpoint(documentId, tenantId) + .catch((error) => { + Lumberjack.error( + `Error retrieving local checkpoint`, + getLumberBaseProperties(documentId, tenantId), + ); + return undefined; + })) ?? undefined; const localCheckpoint: IDeliState | IScribe | undefined = parseCheckpointString( checkpoint?.[service], @@ -310,11 +311,11 @@ export class CheckpointService implements ICheckpointService { restoreFromCheckpointMetric.error( `Error restoring checkpoint from database. Last checkpoint not found.`, ); - return; + throw new Error("Could not restore checkpoint: Last checkpoint not found."); } } - async getLatestCheckpoint( + public async getLatestCheckpoint( tenantId: string, documentId: string, activeClients?: boolean, diff --git a/server/routerlicious/packages/services-core/src/combinedProducer.ts b/server/routerlicious/packages/services-core/src/combinedProducer.ts index c33396f355cf..17f84b6255b6 100644 --- a/server/routerlicious/packages/services-core/src/combinedProducer.ts +++ b/server/routerlicious/packages/services-core/src/combinedProducer.ts @@ -35,7 +35,7 @@ export class CombinedProducer implements IProducer { public async send(messages: T[], tenantId: string, documentId: string): Promise { if (this.parallel) { // parallelly - const sendP = []; + const sendP: Promise[] = []; for (const producer of this.producers) { sendP.push(producer.send(messages, tenantId, documentId)); } @@ -49,7 +49,7 @@ export class CombinedProducer implements IProducer { } public async close(): Promise { - const closeP = []; + const closeP: Promise[] = []; for (const producer of this.producers) { closeP.push(producer.close()); } diff --git a/server/routerlicious/packages/services-core/src/database.ts b/server/routerlicious/packages/services-core/src/database.ts index 504b27a86014..5366e97aea34 100644 --- a/server/routerlicious/packages/services-core/src/database.ts +++ b/server/routerlicious/packages/services-core/src/database.ts @@ -31,16 +31,16 @@ export interface IDatabaseManager { * Retrieves the delta collection */ getDeltaCollection( - tenantId: string, - documentId: string, + tenantId: string | undefined, + documentId: string | undefined, ): Promise>; /** * Scribe deltas collection */ getScribeDeltaCollection( - tenantId: string, - documentId: string, + tenantId: string | undefined, + documentId: string | undefined, ): Promise>; } @@ -52,7 +52,8 @@ export interface IDocumentRepository { /** * Retrieves a document from the database */ - readOne(filter: any): Promise; + // eslint-disable-next-line @rushstack/no-new-null + readOne(filter: any): Promise; /** * Update one document in the database @@ -99,7 +100,8 @@ export interface ICheckpointRepository { /** * Retrieves a checkpoint from the database */ - getCheckpoint(documentId: string, tenantId: string): Promise; + // eslint-disable-next-line @rushstack/no-new-null + getCheckpoint(documentId: string, tenantId: string): Promise; /** * Writes a checkpoint to the database @@ -155,7 +157,8 @@ export interface ICollection { * @param options - optional. If set, provide customized options to the implementations * @returns The value of the query in the database. */ - findOne(query: any, options?: any): Promise; + // eslint-disable-next-line @rushstack/no-new-null + findOne(query: any, options?: any): Promise; /** * @returns All values in the database. @@ -284,7 +287,7 @@ export interface IDb { * @param name - collection name * @param dbName - database name where collection located */ - collection(name: string, dbName?: string): ICollection; + collection(name: string, dbName?: string): ICollection; /** * Removes a collection or view from the database. diff --git a/server/routerlicious/packages/services-core/src/document.ts b/server/routerlicious/packages/services-core/src/document.ts index 31739e76aadd..6c6c4bc32ff0 100644 --- a/server/routerlicious/packages/services-core/src/document.ts +++ b/server/routerlicious/packages/services-core/src/document.ts @@ -37,11 +37,12 @@ export interface IDocumentStaticProperties { * @internal */ export interface IDocumentStorage { - getDocument(tenantId: string, documentId: string): Promise; + // eslint-disable-next-line @rushstack/no-new-null + getDocument(tenantId: string, documentId: string): Promise; getOrCreateDocument(tenantId: string, documentId: string): Promise; - getLatestVersion(tenantId: string, documentId: string): Promise; + getLatestVersion(tenantId: string, documentId: string): Promise; getVersions(tenantId: string, documentId: string, count: number): Promise; diff --git a/server/routerlicious/packages/services-core/src/documentManager.ts b/server/routerlicious/packages/services-core/src/documentManager.ts index 701683edea44..fac721e3770e 100644 --- a/server/routerlicious/packages/services-core/src/documentManager.ts +++ b/server/routerlicious/packages/services-core/src/documentManager.ts @@ -16,7 +16,8 @@ export interface IDocumentManager { * @param documentId - The document ID for the document to be read * @returns - An IDocument object containing properties with the document's data */ - readDocument(tenantId: string, documentId: string): Promise; + // eslint-disable-next-line @rushstack/no-new-null + readDocument(tenantId: string, documentId: string): Promise; /** * Reads only the static data for a specific document, using a cache of the data to do so potentially faster than readDocument. diff --git a/server/routerlicious/packages/services-core/src/http.ts b/server/routerlicious/packages/services-core/src/http.ts index 1f982b598765..82251f43157c 100644 --- a/server/routerlicious/packages/services-core/src/http.ts +++ b/server/routerlicious/packages/services-core/src/http.ts @@ -46,7 +46,7 @@ export interface IWebServer { /** * Web socket interface */ - webSocketServer: IWebSocketServer; + webSocketServer: IWebSocketServer | undefined; /** * HTTP server interface diff --git a/server/routerlicious/packages/services-core/src/index.ts b/server/routerlicious/packages/services-core/src/index.ts index a8228e85b4cc..05afe4648054 100644 --- a/server/routerlicious/packages/services-core/src/index.ts +++ b/server/routerlicious/packages/services-core/src/index.ts @@ -56,6 +56,7 @@ export { } from "./http"; export { extractBoxcar, + isCompleteBoxcarMessage, IContext, IContextErrorData, ILogger, diff --git a/server/routerlicious/packages/services-core/src/lambdas.ts b/server/routerlicious/packages/services-core/src/lambdas.ts index e211623bf1ec..a28846c7fea1 100644 --- a/server/routerlicious/packages/services-core/src/lambdas.ts +++ b/server/routerlicious/packages/services-core/src/lambdas.ts @@ -170,6 +170,16 @@ export interface IPartitionLambdaConfig { documentId: string; } +/** + * Whether the boxcar message includes the optional Routing Key fields. + * @internal + */ +export function isCompleteBoxcarMessage( + boxcar: IBoxcarMessage, +): boxcar is Required { + return boxcar.documentId !== undefined && boxcar.tenantId !== undefined; +} + /** * @internal */ @@ -187,8 +197,8 @@ export function extractBoxcar(message: IQueuedMessage): IBoxcarMessage { if (!parsedMessage) { return { contents: [], - documentId: null, - tenantId: null, + documentId: undefined, + tenantId: undefined, type: BoxcarType, }; } diff --git a/server/routerlicious/packages/services-core/src/messages.ts b/server/routerlicious/packages/services-core/src/messages.ts index 87a111da3191..47168415e96c 100644 --- a/server/routerlicious/packages/services-core/src/messages.ts +++ b/server/routerlicious/packages/services-core/src/messages.ts @@ -195,7 +195,7 @@ export interface ISequencedOperationMessage extends ITicketedMessage { /** * @internal */ -export interface IBoxcarMessage extends ITicketedMessage { +export interface IBoxcarMessage extends IMessage, Partial { // The type of the message type: typeof BoxcarType; diff --git a/server/routerlicious/packages/services-core/src/mongo.ts b/server/routerlicious/packages/services-core/src/mongo.ts index 266e4acdc642..71235ab67dbf 100644 --- a/server/routerlicious/packages/services-core/src/mongo.ts +++ b/server/routerlicious/packages/services-core/src/mongo.ts @@ -25,6 +25,9 @@ export class MongoManager { this.databaseP = this.connect(this.global); this.healthCheck = async (): Promise => { const database = await this.databaseP; + if (database.healthCheck === undefined) { + return; + } return database.healthCheck(); }; } diff --git a/server/routerlicious/packages/services-core/src/mongoCheckpointRepository.ts b/server/routerlicious/packages/services-core/src/mongoCheckpointRepository.ts index f1e02ff9ad66..d5eb1b9431cb 100644 --- a/server/routerlicious/packages/services-core/src/mongoCheckpointRepository.ts +++ b/server/routerlicious/packages/services-core/src/mongoCheckpointRepository.ts @@ -17,7 +17,8 @@ export class MongoCheckpointRepository implements ICheckpointRepository { private readonly checkpointType: string, ) {} - async getCheckpoint(documentId: string, tenantId: string): Promise { + // eslint-disable-next-line @rushstack/no-new-null + async getCheckpoint(documentId: string, tenantId: string): Promise { const pointReadFilter = this.composePointReadFilter(documentId, tenantId); return this.collection.findOne(pointReadFilter); } diff --git a/server/routerlicious/packages/services-core/src/mongoDatabaseManager.ts b/server/routerlicious/packages/services-core/src/mongoDatabaseManager.ts index bc963234465c..72a7298907b7 100644 --- a/server/routerlicious/packages/services-core/src/mongoDatabaseManager.ts +++ b/server/routerlicious/packages/services-core/src/mongoDatabaseManager.ts @@ -38,20 +38,20 @@ export class MongoDatabaseManager implements IDatabaseManager { } public async getDeltaCollection( - tenantId: string, - documentId: string, + tenantId: string | undefined, + documentId: string | undefined, ): Promise> { return this.getCollection(this.deltasCollectionName); } public async getScribeDeltaCollection( - tenantId: string, - documentId: string, + tenantId: string | undefined, + documentId: string | undefined, ): Promise> { return this.getCollection(this.scribeDeltasCollectionName); } - private async getCollection(name: string) { + private async getCollection(name: string) { const db = name === this.documentsCollectionName && this.globalDbEnabled ? await this.globalDbMongoManager.getDatabase() diff --git a/server/routerlicious/packages/services-core/src/mongoDocumentRepository.ts b/server/routerlicious/packages/services-core/src/mongoDocumentRepository.ts index ae9b079e2127..d08ae42d0a19 100644 --- a/server/routerlicious/packages/services-core/src/mongoDocumentRepository.ts +++ b/server/routerlicious/packages/services-core/src/mongoDocumentRepository.ts @@ -12,7 +12,8 @@ import { IDocument } from "./document"; export class MongoDocumentRepository implements IDocumentRepository { constructor(private readonly collection: ICollection) {} - async readOne(filter: any): Promise { + // eslint-disable-next-line @rushstack/no-new-null + async readOne(filter: any): Promise { return this.collection.findOne(filter); } diff --git a/server/routerlicious/packages/services-core/src/runWithRetry.ts b/server/routerlicious/packages/services-core/src/runWithRetry.ts index 91d119afb15d..6be76c7882d9 100644 --- a/server/routerlicious/packages/services-core/src/runWithRetry.ts +++ b/server/routerlicious/packages/services-core/src/runWithRetry.ts @@ -10,7 +10,7 @@ import { LumberEventName, Lumberjack, } from "@fluidframework/server-services-telemetry"; -import { NetworkError } from "@fluidframework/server-services-client"; +import { isNetworkError, NetworkError } from "@fluidframework/server-services-client"; /** * Executes a given API while providing support to retry on failures, ignore failures, and taking action on error. @@ -41,19 +41,18 @@ export async function runWithRetry( onErrorFn?: (error) => void, telemetryEnabled = false, shouldLogInitialSuccessVerbose = false, -): Promise { - let result: T | undefined; +): Promise { let retryCount = 0; let success = false; - let metric: Lumber; - let metricError; + let metric: Lumber | undefined; + let latestResultError: unknown; if (telemetryEnabled) { metric = Lumberjack.newLumberMetric(LumberEventName.RunWithRetry, telemetryProperties); } try { - do { + while (retryCount < maxRetries || maxRetries === -1) { try { - result = await api(); + const result = await api(); success = true; if (retryCount >= 1) { Lumberjack.info( @@ -61,11 +60,12 @@ export async function runWithRetry( telemetryProperties, ); } + return result; } catch (error) { if (onErrorFn !== undefined) { onErrorFn(error); } - metricError = error; + latestResultError = error; Lumberjack.error( `Error running ${callName}: retryCount ${retryCount}`, telemetryProperties, @@ -85,22 +85,13 @@ export async function runWithRetry( // if maxRetries is -1, we retry indefinitely // unless shouldRetry returns false at some point. if (maxRetries !== -1 && retryCount >= maxRetries) { - Lumberjack.error( - `Error after retrying ${retryCount} times, rejecting`, - telemetryProperties, - error, - ); - // Needs to be a full rejection here - throw error; } const intervalMs = calculateIntervalMs(error, retryCount, retryAfterMs); await delay(intervalMs); retryCount++; } - } while (!success); - - return result; + } } finally { if (telemetryEnabled && metric) { metric.setProperty("retryCount", retryCount); @@ -116,10 +107,18 @@ export async function runWithRetry( metric.success("runWithRetry succeeded"); } } else { - metric.error("runWithRetry failed", metricError); + metric.error("runWithRetry failed", latestResultError); } } } + + Lumberjack.error( + `Error after retrying ${retryCount} times, rejecting`, + telemetryProperties, + latestResultError, + ); + // Needs to be a full rejection here + throw latestResultError; } /** @@ -158,18 +157,19 @@ export async function requestWithRetry( onErrorFn?: (error) => void, telemetryEnabled = false, ): Promise { - let result: T; let retryCount = 0; let success = false; - let metric: Lumber; - let metricError; + let metric: Lumber | undefined; + let latestResultError: unknown; if (telemetryEnabled) { metric = Lumberjack.newLumberMetric(LumberEventName.RequestWithRetry, telemetryProperties); } try { - do { + // if maxRetries is -1, we retry indefinitely + // unless shouldRetry returns false at some point. + while (retryCount < maxRetries || maxRetries === -1) { try { - result = await request(); + const result = await request(); success = true; if (retryCount >= 1) { Lumberjack.info( @@ -177,10 +177,12 @@ export async function requestWithRetry( telemetryProperties, ); } - } catch (error) { + return result; + } catch (error: unknown) { if (onErrorFn !== undefined) { onErrorFn(error); } + latestResultError = error; Lumberjack.error( `Error running ${callName}: retryCount ${retryCount}`, telemetryProperties, @@ -194,17 +196,6 @@ export async function requestWithRetry( ); throw error; } - // if maxRetries is -1, we retry indefinitely - // unless shouldRetry returns false at some point. - if (maxRetries !== -1 && retryCount >= maxRetries) { - Lumberjack.error( - `Error after retrying ${retryCount} times, rejecting`, - telemetryProperties, - error, - ); - // Needs to be a full rejection here - throw error; - } // TODO: if error is a NetworkError, we should respect NetworkError.retryAfter // or NetworkError.retryAfterMs @@ -212,9 +203,7 @@ export async function requestWithRetry( await delay(intervalMs); retryCount++; } - } while (!success); - - return result; + } } finally { if (telemetryEnabled && metric) { metric.setProperty("retryCount", retryCount); @@ -224,10 +213,17 @@ export async function requestWithRetry( if (success) { metric.success("requestWithRetry succeeded"); } else { - metric.error("requestWithRetry failed", metricError); + metric.error("requestWithRetry failed", latestResultError); } } } + Lumberjack.error( + `Error after retrying ${retryCount} times, rejecting`, + telemetryProperties, + latestResultError, + ); + // Needs to be a full rejection here + throw latestResultError; } /** @@ -259,12 +255,8 @@ export function calculateRetryIntervalForNetworkError( numRetries: number, retryAfterInterval: number, ): number { - if ( - error instanceof Error && - error?.name === "NetworkError" && - (error as NetworkError).retryAfterMs - ) { - return (error as NetworkError).retryAfterMs; + if (isNetworkError(error) && error.retryAfterMs !== undefined) { + return error.retryAfterMs; } return retryAfterInterval * 2 ** numRetries; } diff --git a/server/routerlicious/packages/services-core/src/test/types/validateServerServicesCorePrevious.generated.ts b/server/routerlicious/packages/services-core/src/test/types/validateServerServicesCorePrevious.generated.ts index 12acd7254ac1..9a087af3847f 100644 --- a/server/routerlicious/packages/services-core/src/test/types/validateServerServicesCorePrevious.generated.ts +++ b/server/routerlicious/packages/services-core/src/test/types/validateServerServicesCorePrevious.generated.ts @@ -334,6 +334,7 @@ declare function get_current_InterfaceDeclaration_IBoxcarMessage(): declare function use_old_InterfaceDeclaration_IBoxcarMessage( use: TypeOnly): void; use_old_InterfaceDeclaration_IBoxcarMessage( + // @ts-expect-error compatibility expected to be broken get_current_InterfaceDeclaration_IBoxcarMessage()); /* @@ -2834,6 +2835,7 @@ declare function get_current_InterfaceDeclaration_IWebServer(): declare function use_old_InterfaceDeclaration_IWebServer( use: TypeOnly): void; use_old_InterfaceDeclaration_IWebServer( + // @ts-expect-error compatibility expected to be broken get_current_InterfaceDeclaration_IWebServer()); /* @@ -3435,6 +3437,7 @@ declare function get_current_ClassDeclaration_TokenRevocationError(): declare function use_old_ClassDeclaration_TokenRevocationError( use: TypeOnly): void; use_old_ClassDeclaration_TokenRevocationError( + // @ts-expect-error compatibility expected to be broken get_current_ClassDeclaration_TokenRevocationError()); /* @@ -3459,6 +3462,7 @@ declare function get_current_ClassDeclaration_TokenRevokedError(): declare function use_old_ClassDeclaration_TokenRevokedError( use: TypeOnly): void; use_old_ClassDeclaration_TokenRevokedError( + // @ts-expect-error compatibility expected to be broken get_current_ClassDeclaration_TokenRevokedError()); /* diff --git a/server/routerlicious/packages/services-core/src/throttler.ts b/server/routerlicious/packages/services-core/src/throttler.ts index 233755bea417..6696496f197e 100644 --- a/server/routerlicious/packages/services-core/src/throttler.ts +++ b/server/routerlicious/packages/services-core/src/throttler.ts @@ -55,7 +55,7 @@ export interface IThrottleAndUsageStorageManager { /** * Get throttling metrics for the given id. */ - getThrottlingMetric(id: string): Promise; + getThrottlingMetric(id: string): Promise; /** * Store throttling metrics and usage data for the given id. @@ -75,7 +75,7 @@ export interface IThrottleAndUsageStorageManager { /** * Get usage data for given id. */ - getUsageData(id: string): Promise; + getUsageData(id: string): Promise; } /** @@ -96,8 +96,9 @@ export interface IThrottlerHelper { /** * Retrieve most recent throttle status for given id. + * @returns Throttle status if found, otherwise undefined if given id is not already tracked for throttling. */ - getThrottleStatus(id: string): Promise; + getThrottleStatus(id: string): Promise; } /** diff --git a/server/routerlicious/packages/services-core/tsconfig.json b/server/routerlicious/packages/services-core/tsconfig.json index af1ec25def3f..8d9ec5943da7 100644 --- a/server/routerlicious/packages/services-core/tsconfig.json +++ b/server/routerlicious/packages/services-core/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["dist", "node_modules"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "types": [ diff --git a/server/routerlicious/packages/services-ordering-kafkanode/src/kafkaNodeConsumer.ts b/server/routerlicious/packages/services-ordering-kafkanode/src/kafkaNodeConsumer.ts index de3910dd778f..54a8df70e1d3 100644 --- a/server/routerlicious/packages/services-ordering-kafkanode/src/kafkaNodeConsumer.ts +++ b/server/routerlicious/packages/services-ordering-kafkanode/src/kafkaNodeConsumer.ts @@ -25,17 +25,17 @@ const defaultReconnectDelay = 5000; * @internal */ export class KafkaNodeConsumer implements IConsumer { - private client: kafka.KafkaClient; - private consumerGroup: kafka.ConsumerGroup; + private client!: kafka.KafkaClient; + private consumerGroup!: kafka.ConsumerGroup; private readonly events = new EventEmitter(); - private readonly zookeeper: IZookeeperClient; + private readonly zookeeper?: IZookeeperClient; constructor( private readonly clientOptions: kafka.KafkaClientOptions, clientId: string, public readonly groupId: string, public readonly topic: string, - private readonly zookeeperEndpoint?: string, + zookeeperEndpoint?: string, private readonly topicPartitions?: number, private readonly topicReplicationFactor?: number, private readonly reconnectDelay: number = defaultReconnectDelay, @@ -90,7 +90,7 @@ export class KafkaNodeConsumer implements IConsumer { public async close(): Promise { await util.promisify(((callback) => this.consumerGroup.close(false, callback)) as any)(); await util.promisify(((callback) => this.client.close(callback)) as any)(); - if (this.zookeeperEndpoint) { + if (this.zookeeper) { this.zookeeper.close(); } } @@ -128,7 +128,8 @@ export class KafkaNodeConsumer implements IConsumer { // Close the client if it exists if (this.client) { this.client.close(); - this.client = undefined; + // This gets reassigned immediately in `this.connect()` + this.client = undefined as unknown as kafka.KafkaClient; } this.events.emit("error", error); diff --git a/server/routerlicious/packages/services-ordering-kafkanode/src/kafkaNodeProducer.ts b/server/routerlicious/packages/services-ordering-kafkanode/src/kafkaNodeProducer.ts index bd81cf1234fe..d209fbe4b40b 100644 --- a/server/routerlicious/packages/services-ordering-kafkanode/src/kafkaNodeProducer.ts +++ b/server/routerlicious/packages/services-ordering-kafkanode/src/kafkaNodeProducer.ts @@ -24,9 +24,9 @@ import { ensureTopics } from "./kafkaTopics"; */ export class KafkaNodeProducer implements IProducer { private readonly messages = new Map(); - private client: kafka.KafkaClient; - private producer: kafka.Producer; - private sendPending: NodeJS.Immediate; + private client!: kafka.KafkaClient; + private producer!: kafka.Producer; + private sendPending?: NodeJS.Immediate; private readonly events = new EventEmitter(); private connecting = false; private connected = false; @@ -60,10 +60,13 @@ export class KafkaNodeProducer implements IProducer { const key = `${tenantId}/${documentId}`; // Get the list of boxcars for the given key - if (!this.messages.has(key)) { - this.messages.set(key, [new PendingBoxcar(tenantId, documentId)]); + const existingBoxcars = this.messages.get(key); + const boxcars: IPendingBoxcar[] = existingBoxcars ?? [ + new PendingBoxcar(tenantId, documentId), + ]; + if (!existingBoxcars) { + this.messages.set(key, boxcars); } - const boxcars = this.messages.get(key); // Create a new boxcar if necessary (will only happen when not connected) if (boxcars[boxcars.length - 1].messages.length + messages.length >= this.maxBatchSize) { @@ -230,7 +233,8 @@ export class KafkaNodeProducer implements IProducer { // Close the client if it exists if (this.client) { this.client.close(); - this.client = undefined; + // This gets re-assigned immediately in `this.connect()` + this.client = undefined as unknown as kafka.KafkaClient; } this.connecting = this.connected = false; diff --git a/server/routerlicious/packages/services-ordering-kafkanode/tsconfig.json b/server/routerlicious/packages/services-ordering-kafkanode/tsconfig.json index 584c4992074c..bbec5265e18c 100644 --- a/server/routerlicious/packages/services-ordering-kafkanode/tsconfig.json +++ b/server/routerlicious/packages/services-ordering-kafkanode/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["dist", "node_modules"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "skipLibCheck": true, diff --git a/server/routerlicious/packages/services-ordering-zookeeper/src/zookeeperClient.ts b/server/routerlicious/packages/services-ordering-zookeeper/src/zookeeperClient.ts index 85c9c666fc2d..c7e096047877 100644 --- a/server/routerlicious/packages/services-ordering-zookeeper/src/zookeeperClient.ts +++ b/server/routerlicious/packages/services-ordering-zookeeper/src/zookeeperClient.ts @@ -13,7 +13,7 @@ import type ZooKeeper from "zookeeper"; * @internal */ export class ZookeeperClient implements IZookeeperClient { - private client: ZooKeeper; + private client!: ZooKeeper; constructor(private readonly url: string) { this.connect(); @@ -32,7 +32,9 @@ export class ZookeeperClient implements IZookeeperClient { if (this.client) { this.client.removeAllListeners(); this.client.close(); - this.client = undefined; + // This is necessary to make sure the client is not reused. + // If accessed after close, it will correctly throw a fatal type error. + this.client = undefined as unknown as ZooKeeper; } } diff --git a/server/routerlicious/packages/services-ordering-zookeeper/tsconfig.json b/server/routerlicious/packages/services-ordering-zookeeper/tsconfig.json index f78b04545c31..16b23ae76c75 100644 --- a/server/routerlicious/packages/services-ordering-zookeeper/tsconfig.json +++ b/server/routerlicious/packages/services-ordering-zookeeper/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["dist", "node_modules"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", }, diff --git a/server/routerlicious/packages/services-shared/src/runnerUtils.ts b/server/routerlicious/packages/services-shared/src/runnerUtils.ts index 02d53371d315..9f7c72a2acc0 100644 --- a/server/routerlicious/packages/services-shared/src/runnerUtils.ts +++ b/server/routerlicious/packages/services-shared/src/runnerUtils.ts @@ -13,8 +13,8 @@ import { Deferred } from "@fluidframework/common-utils"; * @internal */ export async function runnerHttpServerStop( - server: IWebServer, - runningDeferredPromise: Deferred, + server: IWebServer | undefined, + runningDeferredPromise: Deferred | undefined, runnerServerCloseTimeoutMs: number, runnerMetric: Lumber, caller: string | undefined, @@ -27,7 +27,7 @@ export async function runnerHttpServerStop( try { runnerMetric.setProperties(runnerMetricProperties); // Close the underlying server and then resolve the runner once closed - await promiseTimeout(runnerServerCloseTimeoutMs, server.close()); + await promiseTimeout(runnerServerCloseTimeoutMs, server?.close() ?? Promise.resolve()); if (caller === "uncaughtException") { runningDeferredPromise?.reject({ uncaughtException: serializeError(uncaughtException), diff --git a/server/routerlicious/packages/services-shared/src/storage.ts b/server/routerlicious/packages/services-shared/src/storage.ts index ec481107ed12..2c05da2c3473 100644 --- a/server/routerlicious/packages/services-shared/src/storage.ts +++ b/server/routerlicious/packages/services-shared/src/storage.ts @@ -50,14 +50,15 @@ export class DocumentStorage implements IDocumentStorage { private readonly tenantManager: ITenantManager, private readonly enableWholeSummaryUpload: boolean, private readonly opsCollection: ICollection, - private readonly storageNameAssigner: IStorageNameAllocator, + private readonly storageNameAssigner: IStorageNameAllocator | undefined, private readonly ephemeralDocumentTTLSec: number = 60 * 60 * 24, // 24 hours in seconds ) {} /** * Retrieves database details for the given document */ - public async getDocument(tenantId: string, documentId: string): Promise { + // eslint-disable-next-line @rushstack/no-new-null + public async getDocument(tenantId: string, documentId: string): Promise { return this.documentRepository.readOne({ tenantId, documentId }); } @@ -314,10 +315,10 @@ export class DocumentStorage implements IDocumentStorage { } } - public async getLatestVersion(tenantId: string, documentId: string): Promise { + public async getLatestVersion(tenantId: string, documentId: string): Promise { const versions = await this.getVersions(tenantId, documentId, 1); if (!versions.length) { - return null as unknown as ICommit; + return null; } const latest = versions[0]; @@ -359,7 +360,7 @@ export class DocumentStorage implements IDocumentStorage { cache: { blobs: [], commits: [], - refs: { [documentId]: null as unknown as string }, + refs: {}, trees: [], }, code: null as unknown as string, diff --git a/server/routerlicious/packages/services-shared/src/webServer.ts b/server/routerlicious/packages/services-shared/src/webServer.ts index 111b33ecd3b7..de62dbf86f91 100644 --- a/server/routerlicious/packages/services-shared/src/webServer.ts +++ b/server/routerlicious/packages/services-shared/src/webServer.ts @@ -52,7 +52,7 @@ export class HttpServer implements core.IHttpServer { export class WebServer implements core.IWebServer { constructor( public httpServer: HttpServer, - public webSocketServer: core.IWebSocketServer, + public webSocketServer: core.IWebSocketServer | undefined, ) {} /** @@ -132,7 +132,7 @@ export class BasicWebServerFactory implements core.IWebServerFactory { const server = createAndConfigureHttpServer(requestListener, this.httpServerConfig); const httpServer = new HttpServer(server); - return new WebServer(httpServer, null as unknown as core.IWebSocketServer); + return new WebServer(httpServer, undefined); } } @@ -186,7 +186,7 @@ class NullHttpServer implements core.IHttpServer { } class NullWebServer implements core.IWebServer { public readonly httpServer: NullHttpServer = new NullHttpServer(); - public webSocketServer: core.IWebSocketServer = null as unknown as core.IWebSocketServer; + public webSocketServer: core.IWebSocketServer | undefined = undefined; /** * Closes the web server @@ -225,7 +225,7 @@ export class NodeClusterWebServerFactory implements core.IWebServerFactory { } const httpServer = this.initializeWorkerThread(requestListener); - return new WebServer(new HttpServer(httpServer), null as unknown as core.IWebSocketServer); + return new WebServer(new HttpServer(httpServer), undefined); } protected initializePrimaryThread(): void { @@ -397,7 +397,7 @@ export class SocketIoNodeClusterWebServerFactory extends NodeClusterWebServerFac setupMaster(server, { loadBalancingMethod: "least-connection", // either "random", "round-robin" or "least-connection" }); - return new WebServer(new HttpServer(server), null as unknown as core.IWebSocketServer); + return new WebServer(new HttpServer(server), undefined); } // Create a worker thread HTTP server and attach socket.io server to it. const httpServer = this.initializeWorkerThread(requestListener); diff --git a/server/routerlicious/packages/services-shared/tsconfig.json b/server/routerlicious/packages/services-shared/tsconfig.json index 9f03e2b6e02c..bd48d16467b2 100644 --- a/server/routerlicious/packages/services-shared/tsconfig.json +++ b/server/routerlicious/packages/services-shared/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["src/test/**/*"], "compilerOptions": { + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "composite": true, diff --git a/server/routerlicious/packages/services-utils/src/throttlerMiddleware.ts b/server/routerlicious/packages/services-utils/src/throttlerMiddleware.ts index 2cc54b863682..afff18394233 100644 --- a/server/routerlicious/packages/services-utils/src/throttlerMiddleware.ts +++ b/server/routerlicious/packages/services-utils/src/throttlerMiddleware.ts @@ -74,11 +74,20 @@ function noopMiddleware(req: Request, res: Response, next: NextFunction) { * @internal */ export function throttle( - throttler: IThrottler, + throttler?: IThrottler, logger?: ILogger, options?: Partial, isHttpUsageCountingEnabled: boolean = false, ): RequestHandler { + if (!throttler) { + Lumberjack.warning( + "Throttle middleware created without a throttler: Replacing with no-op middleware.", + { + [CommonProperties.telemetryGroupName]: "throttling", + }, + ); + return noopMiddleware; + } const throttleOptions = { ...defaultThrottleMiddlewareOptions, ...options, diff --git a/server/routerlicious/packages/services/package.json b/server/routerlicious/packages/services/package.json index 933436fd9151..7d3701356ae3 100644 --- a/server/routerlicious/packages/services/package.json +++ b/server/routerlicious/packages/services/package.json @@ -112,6 +112,9 @@ "broken": { "ClassDeclaration_MongoDb": { "forwardCompat": false + }, + "ClassDeclaration_WebServer": { + "backCompat": false } } } diff --git a/server/routerlicious/packages/services/src/deltaManager.ts b/server/routerlicious/packages/services/src/deltaManager.ts index 2695087536e5..8efeeefd6905 100644 --- a/server/routerlicious/packages/services/src/deltaManager.ts +++ b/server/routerlicious/packages/services/src/deltaManager.ts @@ -33,7 +33,7 @@ export class DeltaManager implements IDeltaService { const restWrapper = await this.getBasicRestWrapper(tenantId, documentId, baseUrl); const resultP = restWrapper.get( `/deltas/${tenantId}/${documentId}`, - { from, to, caller }, + { from, to, caller: caller ?? "Unknown" }, ); return resultP; } diff --git a/server/routerlicious/packages/services/src/documentManager.ts b/server/routerlicious/packages/services/src/documentManager.ts index 232b55f2eca2..64fd190d0b37 100644 --- a/server/routerlicious/packages/services/src/documentManager.ts +++ b/server/routerlicious/packages/services/src/documentManager.ts @@ -36,14 +36,15 @@ export class DocumentManager implements IDocumentManager { } } - public async readDocument(tenantId: string, documentId: string): Promise { + // eslint-disable-next-line @rushstack/no-new-null + public async readDocument(tenantId: string, documentId: string): Promise { // Retrieve the document const restWrapper = await this.getBasicRestWrapper(tenantId, documentId); const document: IDocument = await restWrapper.get( `/documents/${tenantId}/${documentId}`, ); if (!document) { - return undefined; + return null; } if (this.documentStaticDataCache) { @@ -68,13 +69,14 @@ export class DocumentManager implements IDocumentManager { "Falling back to database after attempting to read cached static document data, because the DocumentManager cache is undefined.", getLumberBaseProperties(documentId, tenantId), ); - const document: IDocument = await this.readDocument(tenantId, documentId); + const document = (await this.readDocument(tenantId, documentId)) ?? undefined; return document as IDocumentStaticProperties | undefined; } // Retrieve cached static document props const staticPropsKey: string = DocumentManager.getDocumentStaticKey(documentId); - const staticPropsStr: string = await this.documentStaticDataCache.get(staticPropsKey); + const staticPropsStr: string | undefined = + (await this.documentStaticDataCache.get(staticPropsKey)) ?? undefined; // If there are no cached static document props, fetch the document from the database if (!staticPropsStr) { @@ -82,7 +84,14 @@ export class DocumentManager implements IDocumentManager { "Falling back to database after attempting to read cached static document data.", getLumberBaseProperties(documentId, tenantId), ); - const document: IDocument = await this.readDocument(tenantId, documentId); + const document = await this.readDocument(tenantId, documentId); + if (!document) { + Lumberjack.warning( + "Fallback to database failed, document not found.", + getLumberBaseProperties(documentId, tenantId), + ); + return undefined; + } return DocumentManager.getStaticPropsFromDoc(document); } @@ -101,6 +110,12 @@ export class DocumentManager implements IDocumentManager { ); return; } + if (this.documentStaticDataCache.delete === undefined) { + Lumberjack.error( + "Cannot purge document static properties cache, because the cache does not have a delete function.", + ); + return; + } const staticPropsKey: string = DocumentManager.getDocumentStaticKey(documentId); await this.documentStaticDataCache.delete(staticPropsKey); diff --git a/server/routerlicious/packages/services/src/messageReceiver.ts b/server/routerlicious/packages/services/src/messageReceiver.ts index 37906f3b18ab..b5566e1f07e3 100644 --- a/server/routerlicious/packages/services/src/messageReceiver.ts +++ b/server/routerlicious/packages/services/src/messageReceiver.ts @@ -16,8 +16,8 @@ import { Lumberjack } from "@fluidframework/server-services-telemetry"; class RabbitmqReceiver implements ITaskMessageReceiver { private readonly events = new EventEmitter(); private readonly rabbitmqConnectionString: string; - private connection: amqp.Connection; - private channel: amqp.Channel; + private connection: amqp.Connection | undefined; + private channel: amqp.Channel | undefined; constructor( private readonly rabbitmqConfig: any, @@ -39,6 +39,9 @@ class RabbitmqReceiver implements ITaskMessageReceiver { .consume( this.taskQueueName, (msgBuffer) => { + if (msgBuffer === null) { + return; + } const msgString = msgBuffer.content.toString(); const msg = JSON.parse(msgString) as ITaskMessage; this.events.emit("message", msg); @@ -64,8 +67,8 @@ class RabbitmqReceiver implements ITaskMessageReceiver { } public async close() { - const closeChannelP = this.channel.close(); - const closeConnectionP = this.connection.close(); + const closeChannelP = this.channel?.close(); + const closeConnectionP = this.connection?.close(); await Promise.all([closeChannelP, closeConnectionP]); } } diff --git a/server/routerlicious/packages/services/src/messageSender.ts b/server/routerlicious/packages/services/src/messageSender.ts index 824c64200724..d1269a2cb128 100644 --- a/server/routerlicious/packages/services/src/messageSender.ts +++ b/server/routerlicious/packages/services/src/messageSender.ts @@ -21,8 +21,8 @@ class RabbitmqTaskSender implements ITaskMessageSender { private readonly events = new EventEmitter(); private readonly rabbitmqConnectionString: string; private readonly taskQueues: string[]; - private connection: amqp.Connection; - private channel: amqp.Channel; + private connection: amqp.Connection | undefined; + private channel: amqp.Channel | undefined; constructor(rabbitmqConfig: any, config: any) { this.rabbitmqConnectionString = rabbitmqConfig.connectionString; @@ -34,7 +34,7 @@ class RabbitmqTaskSender implements ITaskMessageSender { this.channel = await this.connection.createChannel(); // Assert task queues. - const queuePromises = []; + const queuePromises: ReturnType[] = []; for (const queue of this.taskQueues) { queuePromises.push(this.channel.assertQueue(queue, { durable: true })); } @@ -48,7 +48,7 @@ class RabbitmqTaskSender implements ITaskMessageSender { } public sendTask(queueName: string, message: ITaskMessage) { - this.channel.sendToQueue(queueName, Buffer.from(JSON.stringify(message)), { + this.channel?.sendToQueue(queueName, Buffer.from(JSON.stringify(message)), { persistent: false, }); } @@ -59,8 +59,8 @@ class RabbitmqTaskSender implements ITaskMessageSender { } public async close() { - const closeChannelP = this.channel.close(); - const closeConnectionP = this.connection.close(); + const closeChannelP = this.channel?.close(); + const closeConnectionP = this.connection?.close(); await Promise.all([closeChannelP, closeConnectionP]); } } diff --git a/server/routerlicious/packages/services/src/mongoExceptionRetryRules/mongoError/index.ts b/server/routerlicious/packages/services/src/mongoExceptionRetryRules/mongoError/index.ts index 56c322169c2f..e9831f8d6d13 100644 --- a/server/routerlicious/packages/services/src/mongoExceptionRetryRules/mongoError/index.ts +++ b/server/routerlicious/packages/services/src/mongoExceptionRetryRules/mongoError/index.ts @@ -14,11 +14,15 @@ class InternalErrorRule extends BaseMongoExceptionRetryRule { super("InternalErrorRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( + typeof error === "object" && + error !== null && + "code" in error && error.code === 1 && - error.codeName && - (error.codeName as string) === InternalErrorRule.codeName + "codeName" in error && + typeof error.codeName === "string" && + error.codeName === InternalErrorRule.codeName ); } } @@ -31,11 +35,15 @@ class InternalBulkWriteErrorRule extends BaseMongoExceptionRetryRule { super("InternalBulkWriteErrorRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( + typeof error === "object" && + error !== null && + "code" in error && error.code === 1 && - error.name && - (error.name as string).includes(InternalBulkWriteErrorRule.errorName) + "name" in error && + typeof error.name === "string" && + error.name.includes(InternalBulkWriteErrorRule.errorName) ); } } @@ -69,14 +77,17 @@ class NoConnectionAvailableRule extends BaseMongoExceptionRetryRule { super("NoConnectionAvailableRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { // TODO: This timed out actually included two different messages: // 1. Retries due to rate limiting: False. // 2. Retries due to rate limiting: True. // We might need to split this into two different rules after consult with DB team. return ( - error.message && - (error.message as string).startsWith(NoConnectionAvailableRule.messagePrefix) + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" && + error.message.startsWith(NoConnectionAvailableRule.messagePrefix) ); } @@ -107,7 +118,13 @@ class NoPrimaryInReplicasetRule extends BaseMongoExceptionRetryRule { // 1. Retries due to rate limiting: False. // 2. Retries due to rate limiting: True. // We might need to split this into two different rules after consult with DB team. - return error.message && (error.message as string) === NoPrimaryInReplicasetRule.message; + return ( + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" && + error.message === NoPrimaryInReplicasetRule.message + ); } } @@ -121,11 +138,14 @@ class PoolDestroyedRule extends BaseMongoExceptionRetryRule { super("PoolDestroyedRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( - error.message && - ((error.message as string) === PoolDestroyedRule.message1 || - (error.message as string) === PoolDestroyedRule.message2) + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" && + (error.message === PoolDestroyedRule.message1 || + error.message === PoolDestroyedRule.message2) ); } } @@ -140,11 +160,15 @@ class RequestSizeLargeRule extends BaseMongoExceptionRetryRule { super("RequestSizeLargeRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( + typeof error === "object" && + error !== null && + "code" in error && error.code === 16 && - error.errmsg && - (error.errmsg as string).startsWith(RequestSizeLargeRule.errorMsgPrefix) + "errmsg" in error && + typeof error.errmsg === "string" && + error.errmsg.startsWith(RequestSizeLargeRule.errorMsgPrefix) ); } } @@ -158,11 +182,15 @@ class RequestTimedNoRateLimitInfo extends BaseMongoExceptionRetryRule { super("RequestTimedNoRateLimitInfo", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( + typeof error === "object" && + error !== null && + "code" in error && error.code === 50 && - error.errmsg && - (error.errmsg as string) === RequestTimedNoRateLimitInfo.errmsg + "errmsg" in error && + typeof error.errmsg === "string" && + error.errmsg === RequestTimedNoRateLimitInfo.errmsg ); } } @@ -177,11 +205,15 @@ class RequestTimedOutWithHttpInfo extends BaseMongoExceptionRetryRule { super("RequestTimedOutWithHttpInfo", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( + typeof error === "object" && + error !== null && + "code" in error && error.code === 50 && - error.errmsg && - (error.errmsg as string).startsWith(RequestTimedOutWithHttpInfo.errmsgPrefix) + "errmsg" in error && + typeof error.errmsg === "string" && + error.errmsg.startsWith(RequestTimedOutWithHttpInfo.errmsgPrefix) ); } } @@ -196,12 +228,18 @@ class RequestTimedOutWithRateLimitTrue extends BaseMongoExceptionRetryRule { super("RequestTimedOutWithRateLimitTrue", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( + typeof error === "object" && + error !== null && + "code" in error && error.code === 50 && - error.errmsg && - (error.codeName as string) === RequestTimedOutWithRateLimitTrue.codeName && - (error.errmsg as string) === RequestTimedOutWithRateLimitTrue.errorMsg + "errmsg" in error && + typeof error.errmsg === "string" && + error.errmsg === RequestTimedOutWithRateLimitTrue.errorMsg && + "codeName" in error && + typeof error.codeName === "string" && + error.codeName === RequestTimedOutWithRateLimitTrue.codeName ); } } @@ -216,12 +254,18 @@ class RequestTimedOutWithRateLimitFalse extends BaseMongoExceptionRetryRule { super("RequestTimedOutWithRateLimitFalse", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( + typeof error === "object" && + error !== null && + "code" in error && error.code === 50 && - error.errmsg && - (error.codeName as string) === RequestTimedOutWithRateLimitFalse.codeName && - (error.errmsg as string) === RequestTimedOutWithRateLimitFalse.errorMsg + "errmsg" in error && + typeof error.errmsg === "string" && + "codeName" in error && + typeof error.codeName === "string" && + error.codeName === RequestTimedOutWithRateLimitFalse.codeName && + error.errmsg === RequestTimedOutWithRateLimitFalse.errorMsg ); } } @@ -234,11 +278,15 @@ class RequestTimedOutBulkWriteErrorRule extends BaseMongoExceptionRetryRule { super("RequestTimedOutBulkWriteErrorRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( + typeof error === "object" && + error !== null && + "code" in error && error.code === 50 && - error.name && - (error.name as string).includes(RequestTimedOutBulkWriteErrorRule.errorName) + "name" in error && + typeof error.name === "string" && + error.name.includes(RequestTimedOutBulkWriteErrorRule.errorName) ); } } @@ -251,8 +299,14 @@ class ConnectionPoolClearedErrorRule extends BaseMongoExceptionRetryRule { super("ConnectionPoolClearedErrorRule", retryRuleOverride); } - public match(error: any): boolean { - return error.name && (error.name as string) === ConnectionPoolClearedErrorRule.errorName; + public match(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "name" in error && + typeof error.name === "string" && + error.name === ConnectionPoolClearedErrorRule.errorName + ); } } @@ -266,12 +320,18 @@ class ServiceUnavailableRule extends BaseMongoExceptionRetryRule { super("ServiceUnavailableRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( - (error.code === 1 && - error.errorDetails && - (error.errorDetails as string).includes(ServiceUnavailableRule.errorDetails)) || - (error.errmsg && (error.errmsg as string).includes(ServiceUnavailableRule.errorDetails)) + typeof error === "object" && + error !== null && + (("code" in error && + error.code === 1 && + "errorDetails" in error && + typeof error.errorDetails === "string" && + error.errorDetails.includes(ServiceUnavailableRule.errorDetails)) || + ("errmsg" in error && + typeof error.errmsg === "string" && + error.errmsg.includes(ServiceUnavailableRule.errorDetails))) ); } } @@ -286,9 +346,13 @@ class TopologyDestroyed extends BaseMongoExceptionRetryRule { super("TopologyDestroyed", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( - error.message && (error.message as string).toLowerCase() === TopologyDestroyed.message + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" && + error.message.toLowerCase() === TopologyDestroyed.message ); } } @@ -302,11 +366,15 @@ class UnauthorizedRule extends BaseMongoExceptionRetryRule { super("UnauthorizedRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( + typeof error === "object" && + error !== null && + "code" in error && error.code === 13 && - error.codeName && - (error.codeName as string) === UnauthorizedRule.codeName + "codeName" in error && + typeof error.codeName == "string" && + error.codeName === UnauthorizedRule.codeName ); } } @@ -320,9 +388,13 @@ class ConnectionClosedMongoErrorRule extends BaseMongoExceptionRetryRule { super("ConnectionClosedMongoErrorRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( - error.message && /^connection .+ closed$/.test(error.message as string) === true // matches any message of format "connection closed" + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" && + /^connection .+ closed$/.test(error.message) === true // matches any message of format "connection closed" ); } } @@ -335,12 +407,16 @@ class ConnectionTimedOutBulkWriteErrorRule extends BaseMongoExceptionRetryRule { super("ConnectionTimedOutBulkWriteErrorRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( - error.name && - (error.name as string) === ConnectionTimedOutBulkWriteErrorRule.errorName && - error.message && - /^connection .*timed out$/.test(error.message as string) === true + typeof error === "object" && + error !== null && + "name" in error && + typeof error.name === "string" && + error.name === ConnectionTimedOutBulkWriteErrorRule.errorName && + "message" in error && + typeof error.message === "string" && + /^connection .*timed out$/.test(error.message) === true ); } } @@ -353,12 +429,16 @@ class NetworkTimedOutErrorRule extends BaseMongoExceptionRetryRule { super("NetworkTimedOutErrorRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( - error.name && - (error.name as string) === NetworkTimedOutErrorRule.errorName && - error.message && - /^connection .*timed out$/.test(error.message as string) === true + typeof error === "object" && + error !== null && + "name" in error && + typeof error.name === "string" && + error.name === NetworkTimedOutErrorRule.errorName && + "message" in error && + typeof error.message === "string" && + /^connection .*timed out$/.test(error.message) === true ); } } @@ -371,12 +451,16 @@ class MongoServerSelectionErrorRule extends BaseMongoExceptionRetryRule { super("MongoServerSelectionErrorRule", retryRuleOverride); } - public match(error: any): boolean { + public match(error: unknown): boolean { return ( - error.name && - (error.name as string) === MongoServerSelectionErrorRule.errorName && - error.message && - /^connection .*closed$/.test(error.message as string) === true + typeof error === "object" && + error !== null && + "name" in error && + typeof error.name === "string" && + error.name === MongoServerSelectionErrorRule.errorName && + "message" in error && + typeof error.message === "string" && + /^connection .*closed$/.test(error.message) === true ); } } diff --git a/server/routerlicious/packages/services/src/mongoExceptionRetryRules/mongoNetworkError/index.ts b/server/routerlicious/packages/services/src/mongoExceptionRetryRules/mongoNetworkError/index.ts index 925b3af00c1c..b7e558bb191c 100644 --- a/server/routerlicious/packages/services/src/mongoExceptionRetryRules/mongoNetworkError/index.ts +++ b/server/routerlicious/packages/services/src/mongoExceptionRetryRules/mongoNetworkError/index.ts @@ -14,7 +14,8 @@ class MongoNetworkTransientTransactionError extends BaseMongoExceptionRetryRule public match(error: any): boolean { return ( - error.errorLabels?.length && + Array.isArray(error.errorLabels) && + error.errorLabels.length > 0 && (error.errorLabels as string[]).includes("TransientTransactionError") ); } @@ -29,7 +30,8 @@ class MongoNetworkConnectionClosedError extends BaseMongoExceptionRetryRule { public match(error: any): boolean { return ( - error.message && /^connection .+ closed$/.test(error.message as string) === true // matches any message of format "connection closed" + typeof error.message === "string" && + /^connection .+ closed$/.test(error.message as string) === true // matches any message of format "connection closed" ); } } @@ -45,8 +47,8 @@ class MongoNetworkSocketDisconnectedError extends BaseMongoExceptionRetryRule { public match(error: any): boolean { return ( - error.message && - (error.message as string) === MongoNetworkSocketDisconnectedError.errorMessage + typeof error.message === "string" && + error.message === MongoNetworkSocketDisconnectedError.errorMessage ); } } diff --git a/server/routerlicious/packages/services/src/mongodb.ts b/server/routerlicious/packages/services/src/mongodb.ts index 5d155cb85343..6574cf4e4f9a 100644 --- a/server/routerlicious/packages/services/src/mongodb.ts +++ b/server/routerlicious/packages/services/src/mongodb.ts @@ -9,6 +9,7 @@ import * as core from "@fluidframework/server-services-core"; import { AggregationCursor, Collection, + Document, FindOneAndUpdateOptions, FindOptions, MongoClient, @@ -60,7 +61,7 @@ const errorResponseKeysAllowList = new Set([ /** * @internal */ -export class MongoCollection implements core.ICollection, core.IRetryable { +export class MongoCollection implements core.ICollection, core.IRetryable { private readonly apiCounter = new InMemoryApiCounters(); private readonly failedApiCounterSuffix = ".Failed"; private consecutiveFailedCount = 0; @@ -110,8 +111,9 @@ export class MongoCollection implements core.ICollection, core.IRetryable return this.requestWithRetry(req, "MongoCollection.find", query); } - public async findOne(query: object, options?: FindOptions): Promise { - const req: () => Promise = async () => this.collection.findOne(query, options); + // eslint-disable-next-line @rushstack/no-new-null + public async findOne(query: object, options?: FindOptions): Promise { + const req: () => Promise = async () => this.collection.findOne(query, options); return this.requestWithRetry( req, // request "MongoCollection.findOne", // callerName @@ -564,7 +566,7 @@ export class MongoDb implements core.IDb { this.client.on(event, listener); } - public collection(name: string, dbName = "admin"): core.ICollection { + public collection(name: string, dbName = "admin"): core.ICollection { const collection = this.client.db(dbName).collection(name); return new MongoCollection( collection, @@ -763,7 +765,7 @@ export class MongoDbFactory implements core.IDbFactory { } const connection = await MongoClient.connect( - global ? this.globalDbEndpoint : this.operationsDbEndpoint, + global && this.globalDbEndpoint ? this.globalDbEndpoint : this.operationsDbEndpoint, options, ); for (const monitoringEvent of this.dbMonitoringEventsList) { diff --git a/server/routerlicious/packages/services/src/redis.ts b/server/routerlicious/packages/services/src/redis.ts index 8705d50e5a98..a60b0c698742 100644 --- a/server/routerlicious/packages/services/src/redis.ts +++ b/server/routerlicious/packages/services/src/redis.ts @@ -45,7 +45,8 @@ export class RedisCache implements ICache { } } - public async get(key: string): Promise { + // eslint-disable-next-line @rushstack/no-new-null + public async get(key: string): Promise { try { // eslint-disable-next-line @typescript-eslint/return-await return this.redisClientConnectionManager.getRedisClient().get(this.getKey(key)); diff --git a/server/routerlicious/packages/services/src/redisThrottleAndUsageStorageManager.ts b/server/routerlicious/packages/services/src/redisThrottleAndUsageStorageManager.ts index 0f27695712d8..27bbc6793680 100644 --- a/server/routerlicious/packages/services/src/redisThrottleAndUsageStorageManager.ts +++ b/server/routerlicious/packages/services/src/redisThrottleAndUsageStorageManager.ts @@ -112,7 +112,7 @@ export class RedisThrottleAndUsageStorageManager implements IThrottleAndUsageSto await this.redisClientConnectionManager.getRedisClient().lpush(id, usageDataString); } - public async getUsageData(id: string): Promise { + public async getUsageData(id: string): Promise { const usageDataString = await this.redisClientConnectionManager.getRedisClient().rpop(id); if (usageDataString) { return JSON.parse(usageDataString) as IUsageData; diff --git a/server/routerlicious/packages/services/src/secretManager.ts b/server/routerlicious/packages/services/src/secretManager.ts index 32efddd956b1..836a68e7b343 100644 --- a/server/routerlicious/packages/services/src/secretManager.ts +++ b/server/routerlicious/packages/services/src/secretManager.ts @@ -12,7 +12,7 @@ import * as core from "@fluidframework/server-services-core"; */ export class SecretManager implements core.ISecretManager { public getLatestKeyVersion(): core.EncryptionKeyVersion { - return undefined; + return core.EncryptionKeyVersion.key2022; } public decryptSecret( diff --git a/server/routerlicious/packages/services/src/storageNameRetriever.ts b/server/routerlicious/packages/services/src/storageNameRetriever.ts index 98af91901f6a..17710966e8f3 100644 --- a/server/routerlicious/packages/services/src/storageNameRetriever.ts +++ b/server/routerlicious/packages/services/src/storageNameRetriever.ts @@ -13,6 +13,6 @@ export class StorageNameRetriever implements IStorageNameRetriever { public constructor() {} public async get(tenantId: string, documentId: string): Promise { - return undefined; + return "Unknown"; } } diff --git a/server/routerlicious/packages/services/src/test/types/validateServerServicesPrevious.generated.ts b/server/routerlicious/packages/services/src/test/types/validateServerServicesPrevious.generated.ts index aa3061292e0b..ded46b77cf30 100644 --- a/server/routerlicious/packages/services/src/test/types/validateServerServicesPrevious.generated.ts +++ b/server/routerlicious/packages/services/src/test/types/validateServerServicesPrevious.generated.ts @@ -862,6 +862,7 @@ declare function get_current_ClassDeclaration_WebServer(): declare function use_old_ClassDeclaration_WebServer( use: TypeOnly): void; use_old_ClassDeclaration_WebServer( + // @ts-expect-error compatibility expected to be broken get_current_ClassDeclaration_WebServer()); /* diff --git a/server/routerlicious/packages/services/src/throttler.ts b/server/routerlicious/packages/services/src/throttler.ts index bb0b2e69198d..45279a6123ff 100644 --- a/server/routerlicious/packages/services/src/throttler.ts +++ b/server/routerlicious/packages/services/src/throttler.ts @@ -171,8 +171,11 @@ export class Throttler implements IThrottler { } const lastThrottleUpdateTime = this.lastThrottleUpdateAtMap.get(id); - if (now - lastThrottleUpdateTime > this.minThrottleIntervalInMs) { - const countDelta = this.countDeltaMap.get(id); + if ( + lastThrottleUpdateTime !== undefined && + now - lastThrottleUpdateTime > this.minThrottleIntervalInMs + ) { + const countDelta = this.countDeltaMap.get(id) ?? 0; this.lastThrottleUpdateAtMap.set(id, now); this.countDeltaMap.set(id, 0); const messageMetaData = { diff --git a/server/routerlicious/packages/services/src/throttlerHelper.ts b/server/routerlicious/packages/services/src/throttlerHelper.ts index bc26c21b3850..09a00bca44d9 100644 --- a/server/routerlicious/packages/services/src/throttlerHelper.ts +++ b/server/routerlicious/packages/services/src/throttlerHelper.ts @@ -30,17 +30,17 @@ export class ThrottlerHelper implements IThrottlerHelper { usageData?: IUsageData, ): Promise { const now = Date.now(); - let throttlingMetric = await this.throttleAndUsageStorageManager.getThrottlingMetric(id); - if (!throttlingMetric) { - // start a throttling metric with 1 operation burst limit's worth of tokens - throttlingMetric = { - count: this.operationBurstLimit, - lastCoolDownAt: now, - throttleStatus: false, - throttleReason: undefined, - retryAfterInMs: 0, - }; - } + const defaultThrottlingMetric: IThrottlingMetrics = { + count: this.operationBurstLimit, + lastCoolDownAt: now, + throttleStatus: false, + throttleReason: "", + retryAfterInMs: 0, + }; + + const throttlingMetric: IThrottlingMetrics = + (await this.throttleAndUsageStorageManager.getThrottlingMetric(id)) ?? + defaultThrottlingMetric; // Exit early if already throttled and no chance of being unthrottled const retryAfterInMs = this.getRetryAfterInMs(throttlingMetric, now); @@ -95,8 +95,8 @@ export class ThrottlerHelper implements IThrottlerHelper { private async setThrottlingMetricAndUsageData( id: string, throttlingMetric: IThrottlingMetrics, - usageStorageId: string, - usageData: IUsageData, + usageStorageId?: string, + usageData?: IUsageData, ) { await (usageStorageId && usageData ? this.throttleAndUsageStorageManager.setThrottlingMetricAndUsageData( diff --git a/server/routerlicious/packages/services/tsconfig.json b/server/routerlicious/packages/services/tsconfig.json index 71c4f9f9f3d5..7dfc976aeba3 100644 --- a/server/routerlicious/packages/services/tsconfig.json +++ b/server/routerlicious/packages/services/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["dist", "node_modules"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "types": ["mocha"], diff --git a/server/routerlicious/packages/test-utils/src/messageFactory.ts b/server/routerlicious/packages/test-utils/src/messageFactory.ts index bd13b8f917b0..ca14b57895fe 100644 --- a/server/routerlicious/packages/test-utils/src/messageFactory.ts +++ b/server/routerlicious/packages/test-utils/src/messageFactory.ts @@ -35,8 +35,8 @@ export class KafkaMessageFactory { public topic = "test", partitions = 1, private readonly stringify = true, - private readonly tenantId: string = null, - private readonly documentId: string = null, + private readonly tenantId: string | undefined = undefined, + private readonly documentId: string | undefined = undefined, ) { for (let i = 0; i < partitions; i++) { this.offsets.push(0); @@ -121,7 +121,9 @@ export class MessageFactory { details: { capabilities: { interactive: true }, }, - user: null, + user: { + id: "test-user", + }, }, }; const operation: IDocumentSystemMessage = { diff --git a/server/routerlicious/packages/test-utils/src/testCache.ts b/server/routerlicious/packages/test-utils/src/testCache.ts index 769a5e00c2c6..bf40303b2102 100644 --- a/server/routerlicious/packages/test-utils/src/testCache.ts +++ b/server/routerlicious/packages/test-utils/src/testCache.ts @@ -21,13 +21,15 @@ export class TestCache implements ICache { return result; } public async incr(key: string): Promise { - let val = parseInt(this.map.get(key), 10) ?? 0; + const strVal = this.map.get(key); + let val = strVal ? parseInt(strVal, 10) : 0; val += 1; this.map.set(key, val.toString()); return val; } public async decr(key: string): Promise { - let val = parseInt(this.map.get(key), 10) ?? 0; + const strVal = this.map.get(key); + let val = strVal ? parseInt(strVal, 10) : 0; val -= 1; this.map.set(key, val.toString()); return val; diff --git a/server/routerlicious/packages/test-utils/src/testClientManager.ts b/server/routerlicious/packages/test-utils/src/testClientManager.ts index d06ca5f6e1ea..9f35c37301dd 100644 --- a/server/routerlicious/packages/test-utils/src/testClientManager.ts +++ b/server/routerlicious/packages/test-utils/src/testClientManager.ts @@ -21,11 +21,11 @@ export class TestClientManager implements IClientManager { if (!this.clients.has(tenantId)) { this.clients.set(tenantId, new Map()); } - if (!this.clients.get(tenantId).has(documentId)) { - this.clients.get(tenantId).set(documentId, new Map()); + if (!this.clients.get(tenantId)?.has(documentId)) { + this.clients.get(tenantId)?.set(documentId, new Map()); } - this.clients.get(tenantId).get(documentId).set(clientId, details); + this.clients.get(tenantId)?.get(documentId)?.set(clientId, details); } public async removeClient( @@ -33,15 +33,15 @@ export class TestClientManager implements IClientManager { documentId: string, clientId: string, ): Promise { - if (this.clients.has(tenantId) && this.clients.get(tenantId).has(documentId)) { - this.clients.get(tenantId).get(documentId).delete(clientId); + if (this.clients.has(tenantId) && this.clients.get(tenantId)?.has(documentId)) { + this.clients.get(tenantId)?.get(documentId)?.delete(clientId); } } public async getClients(tenantId: string, documentId: string): Promise { const signalClients: ISignalClient[] = []; - if (this.clients.has(tenantId) && this.clients.get(tenantId).has(documentId)) { - for (const [clientId, client] of this.clients.get(tenantId).get(documentId)) { + if (this.clients.has(tenantId) && this.clients.get(tenantId)?.has(documentId)) { + for (const [clientId, client] of this.clients.get(tenantId)?.get(documentId) ?? []) { signalClients.push({ clientId, client, diff --git a/server/routerlicious/packages/test-utils/src/testDocumentStorage.ts b/server/routerlicious/packages/test-utils/src/testDocumentStorage.ts index 0e4015d5c2df..03d2f43921cc 100644 --- a/server/routerlicious/packages/test-utils/src/testDocumentStorage.ts +++ b/server/routerlicious/packages/test-utils/src/testDocumentStorage.ts @@ -50,7 +50,7 @@ export class TestDocumentStorage implements IDocumentStorage { /** * Retrieves database details for the given document */ - public async getDocument(tenantId: string, documentId: string): Promise { + public async getDocument(tenantId: string, documentId: string): Promise { const collection = await this.databaseManager.getDocumentCollection(); return collection.findOne({ documentId, tenantId }); } @@ -179,7 +179,7 @@ export class TestDocumentStorage implements IDocumentStorage { public async getLatestVersion(tenantId: string, documentId: string): Promise { const versions = await this.getVersions(tenantId, documentId, 1); if (!versions.length) { - return null; + throw new Error("No versions found"); } const latest = versions[0]; diff --git a/server/routerlicious/packages/test-utils/src/testHistorian.ts b/server/routerlicious/packages/test-utils/src/testHistorian.ts index ca1e6e02d2fa..d19a0a196a02 100644 --- a/server/routerlicious/packages/test-utils/src/testHistorian.ts +++ b/server/routerlicious/packages/test-utils/src/testHistorian.ts @@ -29,6 +29,7 @@ import { IWholeFlatSummary, IWholeSummaryPayload, IWriteSummaryResponse, + NetworkError, } from "@fluidframework/server-services-client"; import { ICollection, IDb } from "@fluidframework/server-services-core"; import { v4 as uuid } from "uuid"; @@ -109,6 +110,9 @@ export class TestHistorian implements IHistorian { throw new Error("blob ID is undefined"); } const blob = await this.blobs.findOne({ _id: sha }); + if (!blob) { + throw new NetworkError(404, "Blob not found"); + } return { content: IsoBuffer.from( @@ -117,7 +121,8 @@ export class TestHistorian implements IHistorian { ).toString("base64"), encoding: "base64", sha: blob._id, - size: blob.content !== undefined ? blob.content.length : blob.value?.content.length, + size: + blob.content !== undefined ? blob.content.length : blob.value?.content.length ?? -1, url: "", }; } @@ -148,7 +153,7 @@ export class TestHistorian implements IHistorian { } public async getCommits(sha: string, count: number): Promise { - const commit = await this.getCommit(sha); + const commit = await this.getCommit(sha).catch(() => undefined); return commit ? [ { @@ -171,27 +176,28 @@ export class TestHistorian implements IHistorian { let commit = await this.commits.findOne({ _id: sha }); if (!commit) { const ref = await this.getRef(`refs/heads/${sha}`); - if (ref !== undefined) { + if (ref !== undefined && ref !== null) { commit = await this.commits.findOne({ _id: ref.object.sha }); } } - if (commit) { - return { - author: {} as Partial as IAuthor, - committer: {} as Partial as ICommitter, - message: commit.message ?? commit.value?.message, - parents: - commit.parents !== undefined - ? commit.parents.map((p) => ({ sha: p, url: "" })) - : commit.value?.parents.map((p) => ({ sha: p, url: "" })), - sha: commit._id, - tree: { - sha: commit.tree ?? commit.value?.tree, - url: "", - }, - url: "", - }; + if (!commit) { + throw new NetworkError(404, "Commit not found"); } + return { + author: {} as Partial as IAuthor, + committer: {} as Partial as ICommitter, + message: commit.message ?? commit.value?.message, + parents: + (commit.parents !== undefined + ? commit.parents.map((p) => ({ sha: p, url: "" })) + : commit.value?.parents.map((p) => ({ sha: p, url: "" }))) ?? [], + sha: commit._id, + tree: { + sha: commit.tree ?? commit.value?.tree, + url: "", + }, + url: "", + }; } public async createCommit(commit: ICreateCommitParams): Promise { @@ -216,26 +222,31 @@ export class TestHistorian implements IHistorian { throw new Error("Not Supported"); } - public async getRef(ref: string): Promise { + public async getRef(ref: string): Promise { const _id = ref.startsWith("refs/") ? ref.substr(5) : ref; const val = await this.refs.findOne({ _id }); - if (val) { - return { - ref: val.ref ?? val.value?.ref, - url: "", - object: { - sha: val.sha ?? val.value?.sha, - url: "", - type: "", - }, - }; + if (!val) { + return null; } + return { + ref: val.ref ?? val.value?.ref, + url: "", + object: { + sha: val.sha ?? val.value?.sha, + url: "", + type: "", + }, + }; } public async createRef(params: ICreateRefParams): Promise { const _id = params.ref.startsWith("refs/") ? params.ref.substr(5) : params.ref; await this.refs.insertOne({ _id, ...params, value: params }); - return this.getRef(params.ref); + const newRefFromStorage = await this.getRef(params.ref); + if (newRefFromStorage === null) { + throw new Error("Newly created ref not found in storage."); + } + return newRefFromStorage; } public async updateRef(ref: string, params: IPatchRefParams): Promise { @@ -243,7 +254,11 @@ export class TestHistorian implements IHistorian { await (params.force ? this.refs.upsert({ _id }, { sha: params.sha, ref }, {}) : this.refs.update({ _id }, { sha: params.sha, ref }, {})); - return this.getRef(ref); + const newRefFromStorage = await this.getRef(ref); + if (newRefFromStorage === null) { + throw new Error("Newly created ref not found in storage."); + } + return newRefFromStorage; } public async deleteRef(ref: string): Promise { @@ -274,31 +289,32 @@ export class TestHistorian implements IHistorian { public async getTreeHelper(sha: string, recursive: boolean, path: string = ""): Promise { const tree = await this.trees.findOne({ _id: sha }); - if (tree) { - const finalTree: ITree = { - sha: tree._id, + if (!tree) { + throw new NetworkError(404, "Tree not found"); + } + const finalTree: ITree = { + sha: tree._id, + url: "", + tree: [], + }; + for (const entry of tree.tree ?? tree.value?.tree ?? []) { + const entryPath: string = path === "" ? entry.path : `${path}/${entry.path}`; + const treeEntry: ITreeEntry = { + mode: entry.mode, + path: entryPath, + sha: entry.sha, + size: 0, + type: entry.type, url: "", - tree: [], }; - for (const entry of tree.tree ?? tree.value?.tree ?? []) { - const entryPath: string = path === "" ? entry.path : `${path}/${entry.path}`; - const treeEntry: ITreeEntry = { - mode: entry.mode, - path: entryPath, - sha: entry.sha, - size: 0, - type: entry.type, - url: "", - }; - finalTree.tree.push(treeEntry); - if (entry.type === "tree" && recursive) { - const childTree = await this.getTreeHelper(entry.sha, recursive, entryPath); - if (childTree) { - finalTree.tree = finalTree.tree.concat(childTree.tree); - } + finalTree.tree.push(treeEntry); + if (entry.type === "tree" && recursive) { + const childTree = await this.getTreeHelper(entry.sha, recursive, entryPath); + if (childTree) { + finalTree.tree = finalTree.tree.concat(childTree.tree); } } - return finalTree; } + return finalTree; } } diff --git a/server/routerlicious/packages/test-utils/src/testKafka.ts b/server/routerlicious/packages/test-utils/src/testKafka.ts index 5acd865133aa..603f4876bf37 100644 --- a/server/routerlicious/packages/test-utils/src/testKafka.ts +++ b/server/routerlicious/packages/test-utils/src/testKafka.ts @@ -18,7 +18,7 @@ import { TestContext } from "./testContext"; */ export class TestConsumer implements IConsumer { private readonly emitter = new EventEmitter(); - private pausedQueue: string[] = null; + private pausedQueue: string[] | null = null; private failOnCommit = false; // Leverage the context code for storing and tracking an offset diff --git a/server/routerlicious/packages/test-utils/src/testPublisher.ts b/server/routerlicious/packages/test-utils/src/testPublisher.ts index 537e7f05857c..d833222c8962 100644 --- a/server/routerlicious/packages/test-utils/src/testPublisher.ts +++ b/server/routerlicious/packages/test-utils/src/testPublisher.ts @@ -25,7 +25,7 @@ export class TestTopic implements ITopic { this.events.set(event, []); } - this.events.get(event).push({ args, event }); + this.events.get(event)?.push({ args, event }); } public getEvents(key: string) { diff --git a/server/routerlicious/packages/test-utils/src/testTenantManager.ts b/server/routerlicious/packages/test-utils/src/testTenantManager.ts index 21cb0f6d9a3f..c077daf94b12 100644 --- a/server/routerlicious/packages/test-utils/src/testTenantManager.ts +++ b/server/routerlicious/packages/test-utils/src/testTenantManager.ts @@ -40,7 +40,10 @@ export class TestTenant implements ITenant { return { historianUrl: this.historianUrl, internalHistorianUrl: this.historianUrl, - credentials: null, + credentials: { + user: "test", + password: "test", + }, owner: this.owner, repository: this.repository, url: this.url, diff --git a/server/routerlicious/packages/test-utils/src/testThrottleAndUsageStorageManager.ts b/server/routerlicious/packages/test-utils/src/testThrottleAndUsageStorageManager.ts index 04f57a68f7c4..3339e104330a 100644 --- a/server/routerlicious/packages/test-utils/src/testThrottleAndUsageStorageManager.ts +++ b/server/routerlicious/packages/test-utils/src/testThrottleAndUsageStorageManager.ts @@ -21,7 +21,7 @@ export class TestThrottleAndUsageStorageManager implements IThrottleAndUsageStor this.throttlingCache[id] = throttleMetric; } - async getThrottlingMetric(id: string): Promise { + async getThrottlingMetric(id: string): Promise { return this.throttlingCache[id]; } diff --git a/server/routerlicious/packages/test-utils/src/testThrottlerHelper.ts b/server/routerlicious/packages/test-utils/src/testThrottlerHelper.ts index 428ad675317f..ff432fb334d0 100644 --- a/server/routerlicious/packages/test-utils/src/testThrottlerHelper.ts +++ b/server/routerlicious/packages/test-utils/src/testThrottlerHelper.ts @@ -54,7 +54,7 @@ export class TestThrottlerHelper implements IThrottlerHelper { } else { throttlingMetrics.throttleStatus = false; throttlingMetrics.retryAfterInMs = 0; - throttlingMetrics.throttleReason = undefined; + throttlingMetrics.throttleReason = ""; } // update stored throttling metric @@ -63,7 +63,7 @@ export class TestThrottlerHelper implements IThrottlerHelper { return this.getThrottlerResponseFromThrottlingMetrics(throttlingMetrics); } - public async getThrottleStatus(id: string): Promise { + public async getThrottleStatus(id: string): Promise { const throttlingMetrics = this.throttleStorage[id]; if (!throttlingMetrics) { diff --git a/server/routerlicious/packages/test-utils/tsconfig.json b/server/routerlicious/packages/test-utils/tsconfig.json index 58195bcd4eae..bd48d16467b2 100644 --- a/server/routerlicious/packages/test-utils/tsconfig.json +++ b/server/routerlicious/packages/test-utils/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["src/test/**/*"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "composite": true, diff --git a/server/routerlicious/packages/tinylicious/src/app.ts b/server/routerlicious/packages/tinylicious/src/app.ts index d60b102767c4..bac84b4e009e 100644 --- a/server/routerlicious/packages/tinylicious/src/app.ts +++ b/server/routerlicious/packages/tinylicious/src/app.ts @@ -34,7 +34,7 @@ export function create( storage: IDocumentStorage, mongoManager: MongoManager, // eslint-disable-next-line import/no-deprecated - collaborationSessionEventEmitter: TypedEventEmitter, + collaborationSessionEventEmitter: TypedEventEmitter | undefined, ) { // Maximum REST request size const requestSize = config.get("alfred:restJsonSize"); diff --git a/server/routerlicious/packages/tinylicious/src/resourcesFactory.ts b/server/routerlicious/packages/tinylicious/src/resourcesFactory.ts index 722a016ff2b9..b5ce6e82a41d 100644 --- a/server/routerlicious/packages/tinylicious/src/resourcesFactory.ts +++ b/server/routerlicious/packages/tinylicious/src/resourcesFactory.ts @@ -49,7 +49,7 @@ export class TinyliciousResourcesFactory implements IResourcesFactory { const from = queryParamToNumber(request.query.from); const to = queryParamToNumber(request.query.to); - const tenantId = getParam(request.params, "tenantId"); + const tenantId = request.params.tenantId; + const documentId = request.params.id; // Query for the deltas and return a filtered version of just the operations field const deltasP = getDeltas( mongoManager, deltasCollectionName, tenantId, - getParam(request.params, "id"), + documentId, from, to, ); diff --git a/server/routerlicious/packages/tinylicious/src/routes/ordering/documents.ts b/server/routerlicious/packages/tinylicious/src/routes/ordering/documents.ts index 4c1cf0875446..0ae063e43c87 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/ordering/documents.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/ordering/documents.ts @@ -11,16 +11,14 @@ import { import { Router } from "express"; import { v4 as uuid } from "uuid"; import winston from "winston"; -import { getParam } from "../../utils"; export function create(storage: IDocumentStorage): Router { const router: Router = Router(); router.get("/:tenantId?/:id", (request, response) => { - const documentP = storage.getDocument( - getParam(request.params, "tenantId"), - getParam(request.params, "id"), - ); + const tenantId = request.params.tenantId ?? "fluid"; + const documentId = request.params.id; + const documentP = storage.getDocument(tenantId, documentId); documentP.then( (document) => { response.status(200).json(document); @@ -36,7 +34,7 @@ export function create(storage: IDocumentStorage): Router { */ router.post("/:tenantId", (request, response, next) => { // Tenant and document - const tenantId = getParam(request.params, "tenantId"); + const tenantId = request.params.tenantId; const id = request.body.id || uuid(); // Summary information diff --git a/server/routerlicious/packages/tinylicious/src/routes/ordering/index.ts b/server/routerlicious/packages/tinylicious/src/routes/ordering/index.ts index e3281afb2449..4f77d567de16 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/ordering/index.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/ordering/index.ts @@ -14,7 +14,6 @@ import { import { IDocumentStorage, MongoManager } from "@fluidframework/server-services-core"; import { Router } from "express"; import { Provider } from "nconf"; -import { getParam } from "../../utils"; import * as deltas from "./deltas"; import * as documents from "./documents"; @@ -36,8 +35,8 @@ export function create( * Passes on content to all clients in a collaboration session happening on the document via means of signal. */ router.post("/:tenantId/:id/broadcast-signal", (request, response) => { - const tenantId = getParam(request.params, "tenantId"); - const documentId = getParam(request.params, "id"); + const tenantId = request.params.tenantId; + const documentId = request.params.id; const signalContent = request?.body?.signalContent; if (!isValidSignalEnvelope(signalContent)) { response diff --git a/server/routerlicious/packages/tinylicious/src/routes/storage/git/blobs.ts b/server/routerlicious/packages/tinylicious/src/routes/storage/git/blobs.ts index bd6d22c74639..05b2144902d1 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/storage/git/blobs.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/storage/git/blobs.ts @@ -65,7 +65,7 @@ export function create(store: nconf.Provider): Router { const blobP = createBlob( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.body, ); @@ -80,7 +80,7 @@ export function create(store: nconf.Provider): Router { const blobP = getBlob( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params.sha, useCache, ); @@ -97,7 +97,7 @@ export function create(store: nconf.Provider): Router { const blobP = getBlob( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params.sha, useCache, ); diff --git a/server/routerlicious/packages/tinylicious/src/routes/storage/git/commits.ts b/server/routerlicious/packages/tinylicious/src/routes/storage/git/commits.ts index fa14099d7bb2..bf82880b153e 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/storage/git/commits.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/storage/git/commits.ts @@ -95,7 +95,7 @@ export function create(store: nconf.Provider): Router { const commitP = createCommit( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.body, ); @@ -107,7 +107,7 @@ export function create(store: nconf.Provider): Router { const commitP = getCommit( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params.sha, useCache, ); diff --git a/server/routerlicious/packages/tinylicious/src/routes/storage/git/refs.ts b/server/routerlicious/packages/tinylicious/src/routes/storage/git/refs.ts index 75f77cae2171..6f8141f0b0f1 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/storage/git/refs.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/storage/git/refs.ts @@ -106,7 +106,7 @@ export function create(store: nconf.Provider): Router { const router: Router = Router(); router.get("/repos/:ignored?/:tenantId/git/refs", (request, response) => { - const refsP = getRefs(store, request.params.tenantId, request.get("Authorization")); + const refsP = getRefs(store, request.params.tenantId, request.get("Authorization") ?? ""); utils.handleResponse(refsP, response, false); }); @@ -115,7 +115,7 @@ export function create(store: nconf.Provider): Router { const refP = getRef( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params[0], ); @@ -126,7 +126,7 @@ export function create(store: nconf.Provider): Router { const refP = createRef( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.body, ); @@ -137,7 +137,7 @@ export function create(store: nconf.Provider): Router { const refP = updateRef( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params[0], request.body, ); @@ -149,7 +149,7 @@ export function create(store: nconf.Provider): Router { const refP = deleteRef( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params[0], ); diff --git a/server/routerlicious/packages/tinylicious/src/routes/storage/git/tags.ts b/server/routerlicious/packages/tinylicious/src/routes/storage/git/tags.ts index 6eed44615e96..504def74d21d 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/storage/git/tags.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/storage/git/tags.ts @@ -33,7 +33,7 @@ export function create(store: nconf.Provider): Router { const tagP = createTag( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.body, ); @@ -44,7 +44,7 @@ export function create(store: nconf.Provider): Router { const tagP = getTag( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params[0], ); diff --git a/server/routerlicious/packages/tinylicious/src/routes/storage/git/trees.ts b/server/routerlicious/packages/tinylicious/src/routes/storage/git/trees.ts index 6eada9866bae..553295534bb2 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/storage/git/trees.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/storage/git/trees.ts @@ -102,7 +102,7 @@ export function create(store: nconf.Provider): Router { const treeP = createTree( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.body, ); @@ -114,7 +114,7 @@ export function create(store: nconf.Provider): Router { const treeP = getTree( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params.sha, request.query.recursive === "1", useCache, diff --git a/server/routerlicious/packages/tinylicious/src/routes/storage/repository/commits.ts b/server/routerlicious/packages/tinylicious/src/routes/storage/repository/commits.ts index caaea97fd47c..1a19816d8915 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/storage/repository/commits.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/storage/repository/commits.ts @@ -62,9 +62,9 @@ export function create(store: nconf.Provider): Router { const commitsP = getCommits( store, request.params.tenantId, - request.get("Authorization"), - queryParamToString(request.query.sha), - queryParamToNumber(request.query.count), + request.get("Authorization") ?? "", + queryParamToString(request.query.sha) ?? "", + queryParamToNumber(request.query.count) ?? 1, ); utils.handleResponse(commitsP, response, false); diff --git a/server/routerlicious/packages/tinylicious/src/routes/storage/repository/contents.ts b/server/routerlicious/packages/tinylicious/src/routes/storage/repository/contents.ts index 8b62de00a12c..5e05c8d706a4 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/storage/repository/contents.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/storage/repository/contents.ts @@ -37,9 +37,9 @@ export function create(store: nconf.Provider): Router { const contentP = getContent( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params[0], - queryParamToString(request.query.ref), + queryParamToString(request.query.ref) ?? "", ); utils.handleResponse(contentP, response, false); diff --git a/server/routerlicious/packages/tinylicious/src/routes/storage/repository/headers.ts b/server/routerlicious/packages/tinylicious/src/routes/storage/repository/headers.ts index 7331c0129f50..bb189dd9534c 100644 --- a/server/routerlicious/packages/tinylicious/src/routes/storage/repository/headers.ts +++ b/server/routerlicious/packages/tinylicious/src/routes/storage/repository/headers.ts @@ -36,7 +36,7 @@ export function create(store: nconf.Provider): Router { const headerP = getHeader( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params.sha, useCache, ); @@ -49,7 +49,7 @@ export function create(store: nconf.Provider): Router { const headerP = getTree( store, request.params.tenantId, - request.get("Authorization"), + request.get("Authorization") ?? "", request.params.sha, useCache, ); diff --git a/server/routerlicious/packages/tinylicious/src/runner.ts b/server/routerlicious/packages/tinylicious/src/runner.ts index fd7cb6540e14..3ba33b285e10 100644 --- a/server/routerlicious/packages/tinylicious/src/runner.ts +++ b/server/routerlicious/packages/tinylicious/src/runner.ts @@ -29,7 +29,7 @@ export class TinyliciousRunner implements IRunner { private server?: IWebServer; // eslint-disable-next-line import/no-deprecated - private runningDeferred: Deferred; + private runningDeferred?: Deferred; constructor( private readonly serverFactory: IWebServerFactory, @@ -71,6 +71,9 @@ export class TinyliciousRunner implements IRunner { this.server = this.serverFactory.create(alfred); const httpServer = this.server.httpServer; + if (!this.server.webSocketServer) { + throw new Error("WebSocket server is not initialized"); + } configureWebSocketServices( this.server.webSocketServer /* webSocketServer */, @@ -111,17 +114,17 @@ export class TinyliciousRunner implements IRunner { // Close the underlying server and then resolve the runner once closed this.server.close().then( () => { - this.runningDeferred.resolve(); + this.runningDeferred?.resolve(); }, (error) => { - this.runningDeferred.reject(error); + this.runningDeferred?.reject(error); }, ); } else { - this.runningDeferred.resolve(); + this.runningDeferred?.resolve(); } - return this.runningDeferred.promise; + return this.runningDeferred?.promise ?? Promise.resolve(); } /** @@ -154,10 +157,10 @@ export class TinyliciousRunner implements IRunner { // Handle specific listen errors with friendly messages switch (error.code) { case "EACCES": - this.runningDeferred.reject(`${bind} requires elevated privileges`); + this.runningDeferred?.reject(`${bind} requires elevated privileges`); break; case "EADDRINUSE": - this.runningDeferred.reject(`${bind} is already in use`); + this.runningDeferred?.reject(`${bind} is already in use`); break; default: throw error; @@ -168,8 +171,8 @@ export class TinyliciousRunner implements IRunner { * Event listener for HTTP server "listening" event. */ private onListening() { - const addr = this.server.httpServer.address(); - const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; + const addr = this.server?.httpServer?.address(); + const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr?.port}`; winston.info(`Listening on ${bind}`); } } diff --git a/server/routerlicious/packages/tinylicious/src/services/inMemorydb.ts b/server/routerlicious/packages/tinylicious/src/services/inMemorydb.ts index a7c3655e66fb..ae18bec9f5a3 100644 --- a/server/routerlicious/packages/tinylicious/src/services/inMemorydb.ts +++ b/server/routerlicious/packages/tinylicious/src/services/inMemorydb.ts @@ -15,12 +15,13 @@ export class InMemoryDb extends EventEmitter implements IDb { } public collection(name: string): ICollection { - if (!this.collections.has(name)) { - const collection = new Collection(); - this.collections.set(name, collection); + const existingCollection = this.collections.get(name); + if (existingCollection) { + return existingCollection as ICollection; } - - return this.collections.get(name); + const collection = new Collection(); + this.collections.set(name, collection); + return collection as ICollection; } public async dropCollection(name: string): Promise { diff --git a/server/routerlicious/packages/tinylicious/src/services/levelDb.ts b/server/routerlicious/packages/tinylicious/src/services/levelDb.ts index 7b6a537812b9..1c31f17032b0 100644 --- a/server/routerlicious/packages/tinylicious/src/services/levelDb.ts +++ b/server/routerlicious/packages/tinylicious/src/services/levelDb.ts @@ -30,7 +30,7 @@ export class LevelDb extends EventEmitter implements IDb { public collection(name: string): ICollection { const collectionDb = this.db.sublevel(name); - return new Collection(collectionDb, this.getProperty(name)); + return new Collection(collectionDb, this.getProperty(name)) as ICollection; } public async dropCollection(name: string): Promise { diff --git a/server/routerlicious/packages/tinylicious/src/services/levelDbCollection.ts b/server/routerlicious/packages/tinylicious/src/services/levelDbCollection.ts index a9fe9826c53e..9e810358853e 100644 --- a/server/routerlicious/packages/tinylicious/src/services/levelDbCollection.ts +++ b/server/routerlicious/packages/tinylicious/src/services/levelDbCollection.ts @@ -60,7 +60,8 @@ export class Collection implements ICollection { return readStream(this.db.createValueStream()); } - public findOne(query: any): Promise { + // eslint-disable-next-line @rushstack/no-new-null + public findOne(query: any): Promise { return this.findOneInternal(query); } @@ -103,7 +104,7 @@ export class Collection implements ICollection { } public async insertMany(values: any[], ordered: boolean): Promise { - const batchValues = []; + const batchValues: { type: "put"; key: string; value: any }[] = []; values.forEach((value) => { batchValues.push({ type: "put", @@ -141,9 +142,12 @@ export class Collection implements ICollection { return value; } - private async findOneInternal(query: any): Promise { + private async findOneInternal(query: any): Promise { const values = await this.findInternal(query); - return values.length > 0 ? values[0] : null; + if (values.length <= 0) { + return null; + } + return values[0]; } // Generate an insertion key for a value based on index structure. @@ -157,7 +161,7 @@ export class Collection implements ICollection { return v; } - const values = []; + const values: any[] = []; this.property.indexes.forEach((key) => { const innerValue = getValueByKey(value, key); // Leveldb does lexicographic comparison. We need to encode a number for numeric comparison. @@ -171,7 +175,7 @@ export class Collection implements ICollection { const isRange = this.property.limit !== undefined; const indexes = this.property.indexes; const indexLen = isRange ? indexes.length - 1 : indexes.length; - const queryValues = []; + const queryValues: any[] = []; for (let i = 0; i < indexLen; ++i) { const queryValue = query[indexes[i]]; if (queryValue !== undefined) { @@ -181,7 +185,8 @@ export class Collection implements ICollection { } } const key = queryValues.join("!"); - if (isRange) { + // Property limit check is redundant with `isRange` value, but it helps with type checking. + if (isRange && this.property.limit !== undefined) { const rangeKey = indexes[indexes.length - 1]; const from = query[rangeKey] && query[rangeKey].$gt > 0 ? Number(query[rangeKey].$gt) + 1 : 1; diff --git a/server/routerlicious/packages/tinylicious/src/services/tenantManager.ts b/server/routerlicious/packages/tinylicious/src/services/tenantManager.ts index ccf13c13aa5a..4a902b39e8f4 100644 --- a/server/routerlicious/packages/tinylicious/src/services/tenantManager.ts +++ b/server/routerlicious/packages/tinylicious/src/services/tenantManager.ts @@ -52,7 +52,10 @@ export class TinyliciousTenant implements ITenant { return { historianUrl: this.historianUrl, internalHistorianUrl: this.historianUrl, - credentials: null, + credentials: { + user: "tinylicious", + password: "", + }, owner: this.owner, repository: this.repository, url: this.url, diff --git a/server/routerlicious/packages/tinylicious/src/utils.ts b/server/routerlicious/packages/tinylicious/src/utils.ts index e4f07c22a008..3771ed1136ca 100644 --- a/server/routerlicious/packages/tinylicious/src/utils.ts +++ b/server/routerlicious/packages/tinylicious/src/utils.ts @@ -13,7 +13,7 @@ export function getParam(params: Params, key: string) { * Helper function to convert Request's query param to a number. * @param value - The value to be converted to number. */ -export function queryParamToNumber(value: any): number { +export function queryParamToNumber(value: any): number | undefined { if (typeof value !== "string") { return undefined; } @@ -25,7 +25,7 @@ export function queryParamToNumber(value: any): number { * Helper function to convert Request's query param to a string. * @param value - The value to be converted to number. */ -export function queryParamToString(value: any): string { +export function queryParamToString(value: any): string | undefined { if (typeof value !== "string") { return undefined; } diff --git a/server/routerlicious/packages/tinylicious/tsconfig.json b/server/routerlicious/packages/tinylicious/tsconfig.json index 58195bcd4eae..bd48d16467b2 100644 --- a/server/routerlicious/packages/tinylicious/tsconfig.json +++ b/server/routerlicious/packages/tinylicious/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@fluidframework/build-common/ts-common-config.json", "exclude": ["src/test/**/*"], "compilerOptions": { - "strictNullChecks": false, + "strictNullChecks": true, "rootDir": "./src", "outDir": "./dist", "composite": true, From d3d350a35d16ec90bb8fb15dd4a4e3f4a8b23f9c Mon Sep 17 00:00:00 2001 From: Noah Encke <78610362+noencke@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:14:34 -0800 Subject: [PATCH 34/40] Refactor SharedTree transaction internals (#23207) ## Description This PR does various refactors and cleanup to the code that manages transactions. * `ITransaction` and `Transaction` have been deleted and superseded by the new `Transactor` interface and `TransactionStack` implementation. * The new `TransactionStack` invokes callbacks when a transaction is being pushed to or popped from the stack, which greatly simplifies the structure of the instantiator (e.g. in `TreeCheckout`). * Added unit tests for `TransactionStack` * The new `Transactor` interface provides a much more precise specification for when it fires its events. * Removal of old/dead code --- .../dds/tree/src/shared-tree-core/index.ts | 11 +- .../tree/src/shared-tree-core/transaction.ts | 165 ++++++++++ .../src/shared-tree-core/transactionStack.ts | 45 --- packages/dds/tree/src/shared-tree/index.ts | 2 - .../dds/tree/src/shared-tree/sharedTree.ts | 12 +- .../dds/tree/src/shared-tree/treeCheckout.ts | 281 ++++-------------- .../shared-tree-core/sharedTreeCore.spec.ts | 8 +- .../test/shared-tree-core/transaction.spec.ts | 152 ++++++++++ .../tree/src/test/shared-tree-core/utils.ts | 53 ++-- .../shared-tree/fuzz/fuzzEditGenerators.ts | 6 +- .../test/shared-tree/fuzz/fuzzEditReducers.ts | 2 +- .../test/shared-tree/schematizeTree.spec.ts | 4 +- .../src/test/shared-tree/treeCheckout.spec.ts | 14 +- packages/dds/tree/src/test/utils.ts | 4 +- packages/dds/tree/src/util/index.ts | 1 - .../dds/tree/src/util/transactionResult.ts | 19 -- 16 files changed, 423 insertions(+), 356 deletions(-) create mode 100644 packages/dds/tree/src/shared-tree-core/transaction.ts delete mode 100644 packages/dds/tree/src/shared-tree-core/transactionStack.ts create mode 100644 packages/dds/tree/src/test/shared-tree-core/transaction.spec.ts delete mode 100644 packages/dds/tree/src/util/transactionResult.ts diff --git a/packages/dds/tree/src/shared-tree-core/index.ts b/packages/dds/tree/src/shared-tree-core/index.ts index 1d8b6da96347..37d14a52a347 100644 --- a/packages/dds/tree/src/shared-tree-core/index.ts +++ b/packages/dds/tree/src/shared-tree-core/index.ts @@ -11,6 +11,15 @@ export { getChangeReplaceType, } from "./branch.js"; +export { + TransactionResult, + type Transactor, + type TransactionEvents, + TransactionStack, + type OnPush, + type OnPop, +} from "./transaction.js"; + export { type ExplicitCoreCodecVersions, SharedTreeCore, @@ -28,8 +37,6 @@ export { NoOpChangeEnricher, } from "./changeEnricher.js"; -export { TransactionStack } from "./transactionStack.js"; - export { makeEditManagerCodec } from "./editManagerCodecs.js"; export { EditManagerSummarizer } from "./editManagerSummarizer.js"; export { diff --git a/packages/dds/tree/src/shared-tree-core/transaction.ts b/packages/dds/tree/src/shared-tree-core/transaction.ts new file mode 100644 index 000000000000..9019e791269f --- /dev/null +++ b/packages/dds/tree/src/shared-tree-core/transaction.ts @@ -0,0 +1,165 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { createEmitter } from "@fluid-internal/client-utils"; +import type { IDisposable, Listenable } from "@fluidframework/core-interfaces"; +import { UsageError } from "@fluidframework/telemetry-utils/internal"; + +/** + * Describes the result of a transaction. + * Transactions may either succeed and commit, or fail and abort. + */ +export enum TransactionResult { + /** + * Indicates the transaction failed. + */ + Abort, + /** + * Indicates the transaction succeeded. + */ + Commit, +} + +/** + * A simple API for managing transactions. + */ +export interface Transactor { + /** + * Start a new transaction. + * If a transaction is already in progress when this new transaction starts, then this transaction will be "nested" inside of it, + * i.e. the outer transaction will still be in progress after this new transaction is committed or aborted. + * + * @remarks - Asynchronous transactions are not supported on the root checkout, + * since it is always kept up-to-date with the latest remote edits and the results of this rebasing (which might invalidate + * the transaction) is not visible to the application author. + * Instead, + * + * 1. fork the root checkout + * 2. run the transaction on the fork + * 3. merge the fork back into the root checkout + * + * @privateRemarks - There is currently no enforcement that asynchronous transactions don't happen on the root checkout. + * AB#6488 tracks adding some enforcement to make it more clear to application authors that this is not supported. + */ + start(): void; + /** + * Close this transaction by squashing its edits and committing them as a single edit. + * If this is the root checkout and there are no ongoing transactions remaining, the squashed edit will be submitted to Fluid. + */ + commit(): void; + /** + * Close this transaction and revert the state of the tree to what it was before this transaction began. + */ + abort(): void; + /** + * True if there is at least one transaction currently in progress on this view, otherwise false. + */ + isInProgress(): boolean; + /** + * Provides events for changes in transaction progress. + */ + events: Listenable; +} + +export interface TransactionEvents { + /** + * Raised just after a transaction has begun. + * @remarks When this event fires, {@link Transactor.isInProgress} will be true because the transaction has already begun. + */ + started(): void; + /** + * Raised just before a transaction is aborted. + * @remarks When this event fires, {@link Transactor.isInProgress} will still be true because the transaction has not yet ended. + */ + aborting(): void; + /** + * Raised just before a transaction is committed. + * @remarks When this event fires, {@link Transactor.isInProgress} will still be true because the transaction has not yet ended. + */ + committing(): void; +} + +/** + * A function that will be called when a transaction is pushed to the {@link TransactionStack | stack}. + * @remarks This function may return {@link OnPop | its complement} - another function that will be called when the transaction is popped from the stack. + * This function runs just before the transaction begins, so if this is the beginning of an outermost (not nested) transaction then {@link Transactor.isInProgress} will be false during its execution. + */ +export type OnPush = () => OnPop | void; + +/** + * A function that will be called when a transaction is popped from the {@link TransactionStack | stack}. + * @remarks This function runs just after the transaction ends, so if this is the end of an outermost (not nested) transaction then {@link Transactor.isInProgress} will be false during its execution. + */ +export type OnPop = (result: TransactionResult) => void; + +/** + * An implementation of {@link Transactor} that uses a stack to manage transactions. + * @remarks Using a stack allows transactions to nest - i.e. an inner transaction may be started while an outer transaction is already in progress. + */ +export class TransactionStack implements Transactor, IDisposable { + readonly #stack: (OnPop | void)[] = []; + readonly #onPush?: () => OnPop | void; + + readonly #events = createEmitter(); + public get events(): Listenable { + return this.#events; + } + + #disposed = false; + public get disposed(): boolean { + return this.#disposed; + } + + /** + * Construct a new {@link TransactionStack}. + * @param onPush - A {@link OnPush | function} that will be called when a transaction begins. + */ + public constructor(onPush?: () => OnPop | void) { + this.#onPush = onPush; + } + + public isInProgress(): boolean { + this.ensureNotDisposed(); + return this.#stack.length > 0; + } + + public start(): void { + this.ensureNotDisposed(); + this.#stack.push(this.#onPush?.()); + this.#events.emit("started"); + } + + public commit(): void { + this.ensureNotDisposed(); + if (!this.isInProgress()) { + throw new UsageError("No transaction to commit"); + } + this.#events.emit("committing"); + this.#stack.pop()?.(TransactionResult.Commit); + } + + public abort(): void { + this.ensureNotDisposed(); + if (!this.isInProgress()) { + throw new UsageError("No transaction to abort"); + } + this.#events.emit("aborting"); + this.#stack.pop()?.(TransactionResult.Abort); + } + + public dispose(): void { + this.ensureNotDisposed(); + while (this.isInProgress()) { + this.abort(); + } + this.#disposed = true; + } + + private ensureNotDisposed(): void { + if (this.disposed) { + throw new UsageError("Transactor is disposed"); + } + } +} diff --git a/packages/dds/tree/src/shared-tree-core/transactionStack.ts b/packages/dds/tree/src/shared-tree-core/transactionStack.ts deleted file mode 100644 index 812777049b3f..000000000000 --- a/packages/dds/tree/src/shared-tree-core/transactionStack.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import type { RevisionTag } from "../core/index.js"; -import { fail } from "../util/index.js"; - -/** - * A helper class that organizes the state needed for managing nesting transactions. - */ -export class TransactionStack { - private readonly stack: { - startRevision: RevisionTag; - dispose: () => void; - }[] = []; - - /** - * The number of transactions currently ongoing. - */ - public get size(): number { - return this.stack.length; - } - - /** - * Pushes a new transaction onto the stack. That transaction becomes the current transaction. - * @param startRevision - the revision of the latest commit when this transaction begins - * @param disposables - an optional collection of disposable data to release after finishing a transaction - */ - public push(startRevision: RevisionTag, dispose: () => void): void { - this.stack.push({ startRevision, dispose }); - } - - /** - * Ends the current transaction. Fails if there is currently no ongoing transaction. - * @returns The revision that the closed transaction began on. - */ - public pop(): { - startRevision: RevisionTag; - } { - const transaction = this.stack.pop() ?? fail("No transaction is currently in progress"); - transaction.dispose(); - return transaction; - } -} diff --git a/packages/dds/tree/src/shared-tree/index.ts b/packages/dds/tree/src/shared-tree/index.ts index 40ae259e06d8..b018db6448d6 100644 --- a/packages/dds/tree/src/shared-tree/index.ts +++ b/packages/dds/tree/src/shared-tree/index.ts @@ -22,9 +22,7 @@ export { createTreeCheckout, TreeCheckout, type ITreeCheckout, - runSynchronous, type CheckoutEvents, - type ITransaction, type ITreeCheckoutFork, type BranchableTree, type TreeBranchFork, diff --git a/packages/dds/tree/src/shared-tree/sharedTree.ts b/packages/dds/tree/src/shared-tree/sharedTree.ts index 298db89486ac..8109d37fcc91 100644 --- a/packages/dds/tree/src/shared-tree/sharedTree.ts +++ b/packages/dds/tree/src/shared-tree/sharedTree.ts @@ -312,19 +312,19 @@ export class SharedTree }, ); - this.checkout.events.on("transactionStarted", () => { + this.checkout.transaction.events.on("started", () => { if (this.isAttached()) { // It is currently forbidden to attach during a transaction, so transaction state changes can be ignored until after attaching. this.commitEnricher.startTransaction(); } }); - this.checkout.events.on("transactionAborted", () => { + this.checkout.transaction.events.on("aborting", () => { if (this.isAttached()) { // It is currently forbidden to attach during a transaction, so transaction state changes can be ignored until after attaching. this.commitEnricher.abortTransaction(); } }); - this.checkout.events.on("transactionCommitted", () => { + this.checkout.transaction.events.on("committing", () => { if (this.isAttached()) { // It is currently forbidden to attach during a transaction, so transaction state changes can be ignored until after attaching. this.commitEnricher.commitTransaction(); @@ -376,13 +376,13 @@ export class SharedTree > ): void { // We do not submit ops for changes that are part of a transaction. - if (!this.checkout.isTransacting()) { + if (!this.checkout.transaction.isInProgress()) { super.submitCommit(...args); } } protected override didAttach(): void { - if (this.checkout.isTransacting()) { + if (this.checkout.transaction.isInProgress()) { // Attaching during a transaction is not currently supported. // At least part of of the system is known to not handle this case correctly - commit enrichment - and there may be others. throw new UsageError( @@ -398,7 +398,7 @@ export class SharedTree > ): void { assert( - !this.checkout.isTransacting(), + !this.checkout.transaction.isInProgress(), 0x674 /* Unexpected transaction is open while applying stashed ops */, ); super.applyStashedOp(...args); diff --git a/packages/dds/tree/src/shared-tree/treeCheckout.ts b/packages/dds/tree/src/shared-tree/treeCheckout.ts index 483c7f48594d..ab08a104ee6e 100644 --- a/packages/dds/tree/src/shared-tree/treeCheckout.ts +++ b/packages/dds/tree/src/shared-tree/treeCheckout.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { assert } from "@fluidframework/core-utils/internal"; +import { assert, unreachableCase } from "@fluidframework/core-utils/internal"; import type { HasListeners, IEmitter, @@ -44,8 +44,6 @@ import { visitDelta, type RevertibleAlphaFactory, type RevertibleAlpha, - type GraphCommit, - findAncestor, } from "../core/index.js"; import { type FieldBatchCodec, @@ -58,20 +56,14 @@ import { } from "../feature-libraries/index.js"; import { SharedTreeBranch, + TransactionResult, TransactionStack, getChangeReplaceType, onForkTransitive, type SharedTreeBranchChange, + type Transactor, } from "../shared-tree-core/index.js"; -import { - Breakable, - TransactionResult, - disposeSymbol, - fail, - getLast, - hasSingle, - hasSome, -} from "../util/index.js"; +import { Breakable, disposeSymbol, fail, getLast, hasSingle, hasSome } from "../util/index.js"; import { SharedTreeChangeFamily, hasSchemaChange } from "./sharedTreeChangeFamily.js"; import type { SharedTreeChange } from "./sharedTreeChangeTypes.js"; @@ -111,21 +103,6 @@ export interface CheckoutEvents { */ changed(data: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void; - /** - * Fired after a new transaction is started. - */ - transactionStarted(): void; - - /** - * Fired after the current transaction is aborted. - */ - transactionAborted(): void; - - /** - * Fired after the current transaction is committed. - */ - transactionCommitted(): void; - /** * Fired when a new branch is created from this checkout. */ @@ -230,7 +207,7 @@ export interface ITreeCheckout extends AnchorLocator, ViewableTree { /** * A collection of functions for managing transactions. */ - readonly transaction: ITransaction; + readonly transaction: Transactor; branch(): ITreeCheckoutFork; @@ -334,69 +311,6 @@ export function createTreeCheckout( ); } -/** - * A collection of functions for managing transactions. - * Transactions allow edits to be batched into atomic units. - * Edits made during a transaction will update the local state of the tree immediately, but will be squashed into a single edit when the transaction is committed. - * If the transaction is aborted, the local state will be reset to what it was before the transaction began. - * Transactions may nest, meaning that a transaction may be started while a transaction is already ongoing. - * - * To avoid updating observers of the view state with intermediate results during a transaction, - * use {@link ITreeCheckout#branch} and {@link ISharedTreeFork#merge}. - */ -export interface ITransaction { - /** - * Start a new transaction. - * If a transaction is already in progress when this new transaction starts, then this transaction will be "nested" inside of it, - * i.e. the outer transaction will still be in progress after this new transaction is committed or aborted. - * - * @remarks - Asynchronous transactions are not supported on the root checkout, - * since it is always kept up-to-date with the latest remote edits and the results of this rebasing (which might invalidate - * the transaction) is not visible to the application author. - * Instead, - * - * 1. fork the root checkout - * 2. run the transaction on the fork - * 3. merge the fork back into the root checkout - * - * @privateRemarks - There is currently no enforcement that asynchronous transactions don't happen on the root checkout. - * AB#6488 tracks adding some enforcement to make it more clear to application authors that this is not supported. - */ - start(): void; - /** - * Close this transaction by squashing its edits and committing them as a single edit. - * If this is the root checkout and there are no ongoing transactions remaining, the squashed edit will be submitted to Fluid. - */ - commit(): TransactionResult.Commit; - /** - * Close this transaction and revert the state of the tree to what it was before this transaction began. - */ - abort(): TransactionResult.Abort; - /** - * True if there is at least one transaction currently in progress on this view, otherwise false. - */ - inProgress(): boolean; -} - -class Transaction implements ITransaction { - public constructor(private readonly checkout: TreeCheckout) {} - - public start(): void { - this.checkout.startTransaction(); - } - public commit(): TransactionResult.Commit { - this.checkout.commitTransaction(); - return TransactionResult.Commit; - } - public abort(): TransactionResult.Abort { - this.checkout.abortTransaction(); - return TransactionResult.Abort; - } - public inProgress(): boolean { - return this.checkout.isTransacting(); - } -} - /** * Branch (like in a version control system) of SharedTree. * @@ -428,32 +342,6 @@ export class TreeCheckout implements ITreeCheckoutFork { private readonly views = new Set>(); - public readonly transaction: ITransaction; - private readonly transactions = new TransactionStack(); - /** - * After pushing a starting revision to the transaction stack, this branch might be rebased - * over commits which are children of that starting revision. When the transaction is committed, - * those rebased-over commits should not be included in the transaction's squash commit, even though - * they exist between the starting revision and the final commit within the transaction. - * - * Whenever `rebaseOnto` is called during a transaction, this map is augmented with an entry from the - * original merge-base to the new merge-base. - * - * This state need only be retained for the lifetime of the transaction. - * - * TODO: This strategy might need to be revisited when adding better support for async transactions. - * Since: - * - * 1. Transactionality is guaranteed primarily by squashing at commit time - * 2. Branches may be rebased with an ongoing transaction - * - * a rebase operation might invalidate only a portion of a transaction's commits, thus defeating the - * purpose of transactionality. - * - * AB#6483 and children items track this work. - */ - private readonly initialTransactionRevToRebasedRev = new Map(); - /** * Set of revertibles maintained for automatic disposal */ @@ -469,11 +357,6 @@ export class TreeCheckout implements ITreeCheckoutFork { SharedTreeBranch >(); - /** - * copies of the removed roots used as snapshots for reverting to previous state when transactions are aborted - */ - private readonly removedRootsSnapshots: DetachedFieldIndex[] = []; - /** * The name of the telemetry event logged for calls to {@link TreeCheckout.revertRevertible}. * @privateRemarks Exposed for testing purposes. @@ -502,7 +385,6 @@ export class TreeCheckout implements ITreeCheckoutFork { private readonly logger?: ITelemetryLoggerExt, private readonly breaker: Breakable = new Breakable("TreeCheckout"), ) { - this.transaction = new Transaction(this); // We subscribe to `beforeChange` rather than `afterChange` here because it's possible that the change is invalid WRT our forest. // For example, a bug in the editor might produce a malformed change object and thus applying the change to the forest will throw an error. // In such a case we will crash here, preventing the change from being added to the commit graph, and preventing `afterChange` from firing. @@ -562,7 +444,7 @@ export class TreeCheckout implements ITreeCheckoutFork { _branch.events.on("afterChange", (event) => { // The following logic allows revertibles to be generated for the change. // Currently only appends (including merges) and transaction commits are supported. - if (!this.isTransacting()) { + if (!this.transaction.isInProgress()) { if ( event.type === "append" || (event.type === "replace" && getChangeReplaceType(event) === "transactionCommit") @@ -756,87 +638,54 @@ export class TreeCheckout implements ITreeCheckoutFork { return this.forest.anchors.locate(anchor); } - // #region Transactions - - public isTransacting(): boolean { - return this.transactions.size !== 0; + public get transaction(): Transactor { + return this.#transaction; } - - public startTransaction(): void { - this.checkNotDisposed(); - + /** + * The {@link Transactor} for this checkout. + * @remarks In the context of a checkout, transactions allow edits to be batched into atomic units. + * Edits made during a transaction will update the local state of the tree immediately, but will be squashed into a single edit when the transaction is committed. + * If the transaction is aborted, the local state will be reset to what it was before the transaction began. + * Transactions may nest, meaning that a transaction may be started while a transaction is already ongoing. + * + * To avoid updating observers of the view state with intermediate results during a transaction, + * use {@link ITreeCheckout#branch} and {@link ISharedTreeFork#merge}. + */ + readonly #transaction = new TransactionStack(() => { + // Keep track of the commit that each transaction was on when it started + // TODO:#8603: This may need to be computed differently if we allow rebasing during a transaction. + const startCommit = this._branch.getHead(); + // Keep track of all the forks created during the transaction so that we can dispose them when the transaction ends. + // This is a policy decision that we think is useful for the user, but it is not necessary for correctness. const forks = new Set(); const onDisposeUnSubscribes: (() => void)[] = []; const onForkUnSubscribe = onForkTransitive(this, (fork) => { forks.add(fork); onDisposeUnSubscribes.push(fork.events.on("dispose", () => forks.delete(fork))); }); - this.transactions.push(this._branch.getHead().revision, () => { + // When each transaction is started, take a snapshot of the current state of removed roots + const removedRoots = this.removedRoots.clone(); + this._branch.editor.enterTransaction(); + return (result) => { + this._branch.editor.exitTransaction(); + switch (result) { + case TransactionResult.Abort: + this._branch.removeAfter(startCommit); + // If a transaction is rolled back, revert removed roots back to the latest snapshot + this.removedRoots = removedRoots; + break; + case TransactionResult.Commit: + this._branch.squashAfter(startCommit); + break; + default: + unreachableCase(result); + } + forks.forEach((fork) => fork.dispose()); onDisposeUnSubscribes.forEach((unsubscribe) => unsubscribe()); onForkUnSubscribe(); - }); - this._branch.editor.enterTransaction(); - // When a transaction is started, take a snapshot of the current state of removed roots - this.events.emit("transactionStarted"); - this.removedRootsSnapshots.push(this.removedRoots.clone()); - } - - public abortTransaction(): void { - this.checkNotDisposed(); - const [startCommit] = this.popTransaction(); - this._branch.editor.exitTransaction(); - this.events.emit("transactionAborted"); - this._branch.removeAfter(startCommit); - // After a transaction is rolled back, revert removed roots back to the latest snapshot - const snapshot = this.removedRootsSnapshots.pop(); - assert(snapshot !== undefined, 0x9ae /* a snapshot for removed roots does not exist */); - this.removedRoots = snapshot; - } - - public commitTransaction(): void { - this.checkNotDisposed(); - const [startCommit, commits] = this.popTransaction(); - this._branch.editor.exitTransaction(); - this.events.emit("transactionCommitted"); - // When a transaction is committed, the latest snapshot of removed roots can be discarded - this.removedRootsSnapshots.pop(); - if (!hasSome(commits)) { - return undefined; - } - - this._branch.squashAfter(startCommit); - } - - private popTransaction(): [GraphCommit, GraphCommit[]] { - const { startRevision: startRevisionOriginal } = this.transactions.pop(); - let startRevision = startRevisionOriginal; - - for ( - let r: RevisionTag | undefined = startRevision; - r !== undefined; - r = this.initialTransactionRevToRebasedRev.get(startRevision) - ) { - startRevision = r; - } - - if (!this.isTransacting()) { - this.initialTransactionRevToRebasedRev.clear(); - } - - const commits: GraphCommit[] = []; - const startCommit = findAncestor( - [this._branch.getHead(), commits], - (c) => c.revision === startRevision, - ); - assert( - startCommit !== undefined, - 0x593 /* Expected branch to be ahead of transaction start revision */, - ); - return [startCommit, commits]; - } - - // #endregion Transactions + }; + }); public branch(): TreeCheckout { this.checkNotDisposed( @@ -872,7 +721,7 @@ export class TreeCheckout implements ITreeCheckoutFork { "The source of the branch rebase has been disposed and cannot be rebased.", ); assert( - !checkout.isTransacting(), + !checkout.transaction.isInProgress(), 0x9af /* A view cannot be rebased while it has a pending transaction */, ); assert( @@ -880,16 +729,7 @@ export class TreeCheckout implements ITreeCheckoutFork { 0xa5d /* The main branch cannot be rebased onto another branch. */, ); - const result = checkout._branch.rebaseOnto(this._branch); - if (result !== undefined && this.isTransacting()) { - const { targetCommits } = result.commits; - // If `targetCommits` were empty, then `result` would be undefined and we couldn't reach here - assert(hasSome(targetCommits), "Expected target commits to be non-empty"); - const src = targetCommits[0].parent?.revision; - assert(src !== undefined, "Expected parent to be defined"); - const dst = getLast(targetCommits).revision; - this.initialTransactionRevToRebasedRev.set(src, dst); - } + checkout._branch.rebaseOnto(this._branch); } public rebaseOnto(checkout: ITreeCheckout): void { @@ -909,10 +749,10 @@ export class TreeCheckout implements ITreeCheckoutFork { "The source of the branch merge has been disposed and cannot be merged.", ); assert( - !this.isTransacting(), + !this.transaction.isInProgress(), 0x9b0 /* Views cannot be merged into a view while it has a pending transaction */, ); - while (checkout.transaction.inProgress()) { + while (checkout.transaction.isInProgress()) { checkout.transaction.commit(); } this._branch.merge(checkout._branch); @@ -936,9 +776,7 @@ export class TreeCheckout implements ITreeCheckoutFork { "The branch has already been disposed and cannot be disposed again.", ); this.disposed = true; - while (this.isTransacting()) { - this.abortTransaction(); - } + this.#transaction.dispose(); this.purgeRevertibles(); this._branch.dispose(); for (const view of this.views) { @@ -985,7 +823,7 @@ export class TreeCheckout implements ITreeCheckoutFork { } private revertRevertible(revision: RevisionTag, kind: CommitKind): RevertMetrics { - if (this.isTransacting()) { + if (this.transaction.isInProgress()) { throw new UsageError("Undo is not yet supported during transactions."); } @@ -1072,22 +910,3 @@ export class TreeCheckout implements ITreeCheckoutFork { ); } } - -/** - * Run a synchronous transaction on the given shared tree view. - * This is a convenience helper around the {@link SharedTreeFork#transaction} APIs. - * @param view - the view on which to run the transaction - * @param transaction - the transaction function. This will be executed immediately. It is passed `view` as an argument for convenience. - * If this function returns an `Abort` result then the transaction will be aborted. Otherwise, it will be committed. - * @returns whether or not the transaction was committed or aborted - */ -export function runSynchronous( - view: ITreeCheckout, - transaction: (view: ITreeCheckout) => TransactionResult | void, -): TransactionResult { - view.transaction.start(); - const result = transaction(view); - return result === TransactionResult.Abort - ? view.transaction.abort() - : view.transaction.commit(); -} diff --git a/packages/dds/tree/src/test/shared-tree-core/sharedTreeCore.spec.ts b/packages/dds/tree/src/test/shared-tree-core/sharedTreeCore.spec.ts index cd78248fdb18..02655638fd14 100644 --- a/packages/dds/tree/src/test/shared-tree-core/sharedTreeCore.spec.ts +++ b/packages/dds/tree/src/test/shared-tree-core/sharedTreeCore.spec.ts @@ -526,7 +526,7 @@ describe("SharedTreeCore", () => { deltaConnection: dataStoreRuntime1.createDeltaConnection(), objectStorage: new MockStorage(), }); - tree.startTransaction(); + tree.transaction.start(); assert.equal(enricher.enrichmentLog.length, 0); changeTree(tree); assert.equal(enricher.enrichmentLog.length, 1); @@ -534,7 +534,7 @@ describe("SharedTreeCore", () => { changeTree(tree); assert.equal(enricher.enrichmentLog.length, 2); assert.equal(enricher.enrichmentLog[1].input, tree.getLocalBranch().getHead().change); - tree.commitTransaction(); + tree.transaction.commit(); assert.equal(enricher.enrichmentLog.length, 2); assert.equal(machine.submissionLog.length, 1); assert.notEqual(machine.submissionLog[0], tree.getLocalBranch().getHead().change); @@ -553,12 +553,12 @@ describe("SharedTreeCore", () => { deltaConnection: dataStoreRuntime1.createDeltaConnection(), objectStorage: new MockStorage(), }); - tree.startTransaction(); + tree.transaction.start(); assert.equal(enricher.enrichmentLog.length, 0); changeTree(tree); assert.equal(enricher.enrichmentLog.length, 1); assert.equal(enricher.enrichmentLog[0].input, tree.getLocalBranch().getHead().change); - tree.abortTransaction(); + tree.transaction.abort(); assert.equal(enricher.enrichmentLog.length, 1); assert.equal(machine.submissionLog.length, 0); }); diff --git a/packages/dds/tree/src/test/shared-tree-core/transaction.spec.ts b/packages/dds/tree/src/test/shared-tree-core/transaction.spec.ts new file mode 100644 index 000000000000..8bfee1e8c96e --- /dev/null +++ b/packages/dds/tree/src/test/shared-tree-core/transaction.spec.ts @@ -0,0 +1,152 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; +import { TransactionStack, type OnPop } from "../../shared-tree-core/index.js"; +import { validateAssertionError } from "@fluidframework/test-runtime-utils/internal"; + +describe("TransactionStacks", () => { + it("emit an event after starting a transaction", () => { + const transaction = new TransactionStack(); + let started = false; + transaction.events.on("started", () => { + assert.equal(transaction.isInProgress(), true); + started = true; + }); + transaction.start(); + assert.equal(started, true); + }); + + it("emit an event just before aborting a transaction", () => { + const transaction = new TransactionStack(); + let aborting = false; + transaction.events.on("aborting", () => { + assert.equal(transaction.isInProgress(), true); + aborting = true; + }); + transaction.start(); + transaction.abort(); + assert.equal(aborting, true); + }); + + it("emit an event just before committing a transaction", () => { + const transaction = new TransactionStack(); + let committing = false; + transaction.events.on("committing", () => { + assert.equal(transaction.isInProgress(), true); + committing = true; + }); + transaction.start(); + transaction.commit(); + assert.equal(committing, true); + }); + + it("report whether or not a transaction is in progress", () => { + const transaction = new TransactionStack(); + assert.equal(transaction.isInProgress(), false); + transaction.start(); + assert.equal(transaction.isInProgress(), true); + transaction.start(); + assert.equal(transaction.isInProgress(), true); + transaction.commit(); + assert.equal(transaction.isInProgress(), true); + transaction.abort(); + assert.equal(transaction.isInProgress(), false); + }); + + it("run a function when a transaction begins", () => { + let invoked = false; + const transaction = new TransactionStack((): void => { + invoked = true; + assert.equal(transaction.isInProgress(), false); + }); + transaction.start(); + assert.equal(invoked, true); + }); + + it("run a function when a transaction aborts", () => { + let invoked = false; + const transaction = new TransactionStack((): OnPop => { + return () => { + invoked = true; + assert.equal(transaction.isInProgress(), false); + }; + }); + transaction.start(); + assert.equal(invoked, false); + transaction.abort(); + assert.equal(invoked, true); + }); + + it("run a function when a transaction commits", () => { + let invoked = false; + const transaction = new TransactionStack((): OnPop => { + return () => { + invoked = true; + assert.equal(transaction.isInProgress(), false); + }; + }); + transaction.start(); + assert.equal(invoked, false); + transaction.commit(); + assert.equal(invoked, true); + }); + + it("throw an error if committing without starting a transaction", () => { + const transaction = new TransactionStack(); + assert.throws( + () => transaction.commit(), + (e: Error) => validateAssertionError(e, "No transaction to commit"), + ); + }); + + it("throw an error if aborting without starting a transaction", () => { + const transaction = new TransactionStack(); + assert.throws( + () => transaction.abort(), + (e: Error) => validateAssertionError(e, "No transaction to abort"), + ); + }); + + it("can't be used after disposal", () => { + const transaction = new TransactionStack(); + assert.equal(transaction.disposed, false); + transaction.dispose(); + assert.equal(transaction.disposed, true); + assert.throws( + () => transaction.isInProgress(), + (e: Error) => validateAssertionError(e, "Transactor is disposed"), + ); + assert.throws( + () => transaction.start(), + (e: Error) => validateAssertionError(e, "Transactor is disposed"), + ); + assert.throws( + () => transaction.commit(), + (e: Error) => validateAssertionError(e, "Transactor is disposed"), + ); + assert.throws( + () => transaction.abort(), + (e: Error) => validateAssertionError(e, "Transactor is disposed"), + ); + assert.throws( + () => transaction.dispose(), + (e: Error) => validateAssertionError(e, "Transactor is disposed"), + ); + }); + + it("abort all transactions when disposed", () => { + let aborted = 0; + const transaction = new TransactionStack(() => { + return () => { + aborted += 1; + }; + }); + transaction.start(); + transaction.start(); + transaction.dispose(); + assert.equal(aborted, 2); + }); +}); diff --git a/packages/dds/tree/src/test/shared-tree-core/utils.ts b/packages/dds/tree/src/test/shared-tree-core/utils.ts index de85955fbbfb..8c3037c3cd44 100644 --- a/packages/dds/tree/src/test/shared-tree-core/utils.ts +++ b/packages/dds/tree/src/test/shared-tree-core/utils.ts @@ -10,11 +10,7 @@ import type { import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils/internal"; import type { ICodecOptions } from "../../codec/index.js"; -import { - RevisionTagCodec, - TreeStoredSchemaRepository, - type GraphCommit, -} from "../../core/index.js"; +import { RevisionTagCodec, TreeStoredSchemaRepository } from "../../core/index.js"; import { typeboxValidator } from "../../external-utilities/index.js"; import { DefaultChangeFamily, @@ -32,9 +28,13 @@ import { type SharedTreeBranch, SharedTreeCore, type Summarizable, + TransactionResult, + TransactionStack, + type Transactor, } from "../../shared-tree-core/index.js"; import { testIdCompressor } from "../utils.js"; import { strict as assert } from "node:assert"; +import { unreachableCase } from "@fluidframework/core-utils/internal"; /** * A `SharedTreeCore` with @@ -48,8 +48,6 @@ export class TestSharedTreeCore extends SharedTreeCore; - public constructor( runtime: IFluidDataStoreRuntime = new MockFluidDataStoreRuntime({ idCompressor: testIdCompressor, @@ -97,33 +95,26 @@ export class TestSharedTreeCore extends SharedTreeCore["submitCommit"]> ): void { // We do not submit ops for changes that are part of a transaction. - if (this.transactionStart === undefined) { + if (!this.transaction.isInProgress()) { super.submitCommit(...args); } } - public startTransaction(): void { - assert( - this.transactionStart === undefined, - "Transaction already started. TestSharedTreeCore does not support nested transactions.", - ); - this.transactionStart = this.getLocalBranch().getHead(); + public transaction: Transactor = new TransactionStack(() => { + const startCommit = this.getLocalBranch().getHead(); this.commitEnricher.startTransaction(); - } - - public abortTransaction(): void { - assert(this.transactionStart !== undefined, "No transaction to abort."); - const start = this.transactionStart; - this.transactionStart = undefined; - this.commitEnricher.abortTransaction(); - this.getLocalBranch().removeAfter(start); - } - - public commitTransaction(): void { - assert(this.transactionStart !== undefined, "No transaction to commit."); - const start = this.transactionStart; - this.transactionStart = undefined; - this.commitEnricher.commitTransaction(); - this.getLocalBranch().squashAfter(start); - } + return (result) => { + this.commitEnricher.commitTransaction(); + switch (result) { + case TransactionResult.Commit: + this.getLocalBranch().squashAfter(startCommit); + break; + case TransactionResult.Abort: + this.getLocalBranch().removeAfter(startCommit); + break; + default: + unreachableCase(result); + } + }; + }); } diff --git a/packages/dds/tree/src/test/shared-tree/fuzz/fuzzEditGenerators.ts b/packages/dds/tree/src/test/shared-tree/fuzz/fuzzEditGenerators.ts index 54473b2e5618..b373599f17c9 100644 --- a/packages/dds/tree/src/test/shared-tree/fuzz/fuzzEditGenerators.ts +++ b/packages/dds/tree/src/test/shared-tree/fuzz/fuzzEditGenerators.ts @@ -528,7 +528,7 @@ export const makeTransactionEditGenerator = ( boundary: "commit", }, opWeights.commit, - (state) => viewFromState(state).checkout.transaction.inProgress(), + (state) => viewFromState(state).checkout.transaction.isInProgress(), ], [ { @@ -536,7 +536,7 @@ export const makeTransactionEditGenerator = ( boundary: "abort", }, opWeights.abort, - (state) => viewFromState(state).checkout.transaction.inProgress(), + (state) => viewFromState(state).checkout.transaction.isInProgress(), ], ]); }; @@ -642,7 +642,7 @@ export function makeOpGenerator( [ () => makeConstraintEditGenerator(weights), constraintWeight, - (state: FuzzTestState) => viewFromState(state).checkout.transaction.inProgress(), + (state: FuzzTestState) => viewFromState(state).checkout.transaction.isInProgress(), ], ] as const ) diff --git a/packages/dds/tree/src/test/shared-tree/fuzz/fuzzEditReducers.ts b/packages/dds/tree/src/test/shared-tree/fuzz/fuzzEditReducers.ts index cad473aaffde..a4c11632dba0 100644 --- a/packages/dds/tree/src/test/shared-tree/fuzz/fuzzEditReducers.ts +++ b/packages/dds/tree/src/test/shared-tree/fuzz/fuzzEditReducers.ts @@ -340,7 +340,7 @@ export function applyTransactionBoundary( unreachableCase(boundary); } - if (!checkout.transaction.inProgress()) { + if (!checkout.transaction.isInProgress()) { // Transaction is complete, so merge the changes into the root view and clean up the fork from the state. state.transactionViews.delete(state.client.channel); const rootView = viewFromState(state); diff --git a/packages/dds/tree/src/test/shared-tree/schematizeTree.spec.ts b/packages/dds/tree/src/test/shared-tree/schematizeTree.spec.ts index 7e0758018954..b5d415101257 100644 --- a/packages/dds/tree/src/test/shared-tree/schematizeTree.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/schematizeTree.spec.ts @@ -26,7 +26,6 @@ import type { ITreeCheckoutFork, CheckoutEvents, ISharedTreeEditor, - ITransaction, } from "../../shared-tree/index.js"; import { type TreeStoredContent, @@ -49,6 +48,7 @@ import { // eslint-disable-next-line import/no-internal-modules import { toStoredSchema } from "../../simple-tree/toStoredSchema.js"; import { jsonSequenceRootSchema } from "../sequenceRootUtils.js"; +import type { Transactor } from "../../shared-tree-core/index.js"; const builder = new SchemaFactory("test"); const root = builder.number; @@ -153,7 +153,7 @@ describe("schematizeTree", () => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions forest: { isEmpty } as IForestSubscription, editor: undefined as unknown as ISharedTreeEditor, - transaction: undefined as unknown as ITransaction, + transaction: undefined as unknown as Transactor, branch(): ITreeCheckoutFork { throw new Error("Function not implemented."); }, diff --git a/packages/dds/tree/src/test/shared-tree/treeCheckout.spec.ts b/packages/dds/tree/src/test/shared-tree/treeCheckout.spec.ts index 86ec5fba5081..b0aa970a08a3 100644 --- a/packages/dds/tree/src/test/shared-tree/treeCheckout.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/treeCheckout.spec.ts @@ -693,7 +693,7 @@ describe("sharedTreeView", () => { viewBranch.root.insertAtEnd("43"); tree.merge(treeBranch, false); assert.deepEqual(viewBranch.root, ["42", "43"]); - assert.equal(viewBranch.checkout.transaction.inProgress(), false); + assert.equal(viewBranch.checkout.transaction.isInProgress(), false); }); itView("do not close across forks", ({ view, tree }) => { @@ -704,7 +704,7 @@ describe("sharedTreeView", () => { view.root.insertAtEnd("A"); assert.throws( () => viewBranch.checkout.transaction.commit(), - (e: Error) => validateAssertionError(e, "No transaction is currently in progress"), + (e: Error) => validateAssertionError(e, "No transaction to commit"), ); }); @@ -822,15 +822,15 @@ describe("sharedTreeView", () => { }); itView("statuses are reported correctly", ({ view }) => { - assert.equal(view.checkout.isTransacting(), false); + assert.equal(view.checkout.transaction.isInProgress(), false); view.checkout.transaction.start(); - assert.equal(view.checkout.isTransacting(), true); + assert.equal(view.checkout.transaction.isInProgress(), true); view.checkout.transaction.start(); - assert.equal(view.checkout.isTransacting(), true); + assert.equal(view.checkout.transaction.isInProgress(), true); view.checkout.transaction.commit(); - assert.equal(view.checkout.isTransacting(), true); + assert.equal(view.checkout.transaction.isInProgress(), true); view.checkout.transaction.abort(); - assert.equal(view.checkout.isTransacting(), false); + assert.equal(view.checkout.transaction.isInProgress(), false); }); }); diff --git a/packages/dds/tree/src/test/utils.ts b/packages/dds/tree/src/test/utils.ts index 76a7bb911f88..ab98b4405993 100644 --- a/packages/dds/tree/src/test/utils.ts +++ b/packages/dds/tree/src/test/utils.ts @@ -120,7 +120,6 @@ import { type TreeCheckout, createTreeCheckout, type ISharedTreeEditor, - type ITransaction, type ITreeCheckoutFork, } from "../shared-tree/index.js"; // eslint-disable-next-line import/no-internal-modules @@ -152,6 +151,7 @@ import type { Client } from "@fluid-private/test-dds-utils"; import { JsonUnion, cursorToJsonObject, singleJsonCursor } from "./json/index.js"; // eslint-disable-next-line import/no-internal-modules import type { TreeSimpleContent } from "./feature-libraries/flex-tree/utils.js"; +import type { Transactor } from "../shared-tree-core/index.js"; // Testing utilities @@ -1219,7 +1219,7 @@ export class MockTreeCheckout implements ITreeCheckout { } return this.options.editor; } - public get transaction(): ITransaction { + public get transaction(): Transactor { throw new Error("'transaction' property not implemented in MockTreeCheckout."); } public get events(): Listenable { diff --git a/packages/dds/tree/src/util/index.ts b/packages/dds/tree/src/util/index.ts index 897bc1841f1a..2c1c14a9dae1 100644 --- a/packages/dds/tree/src/util/index.ts +++ b/packages/dds/tree/src/util/index.ts @@ -37,7 +37,6 @@ export { } from "./nestedMap.js"; export { addToNestedSet, type NestedSet, nestedSetContains } from "./nestedSet.js"; export { type OffsetList, OffsetListFactory } from "./offsetList.js"; -export { TransactionResult } from "./transactionResult.js"; export type { areSafelyAssignable, Contravariant, diff --git a/packages/dds/tree/src/util/transactionResult.ts b/packages/dds/tree/src/util/transactionResult.ts deleted file mode 100644 index a8d8c8b7e209..000000000000 --- a/packages/dds/tree/src/util/transactionResult.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * Describes the result of a transaction. - * Transactions may either succeed and commit, or fail and abort. - */ -export enum TransactionResult { - /** - * Indicates the transaction failed. - */ - Abort, - /** - * Indicates the transaction succeeded. - */ - Commit, -} From 2c0a072b5b6157e3c623f6a4c741463e1b6c107f Mon Sep 17 00:00:00 2001 From: Rishhi Balakrishnan <107130183+RishhiB@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:12:37 -0800 Subject: [PATCH 35/40] Pull eslint config changes from #23050 (#23184) pulling eslint config changes out of https://github.com/microsoft/FluidFramework/pull/23050 Also corrects the eslint config in experimental/PropertyDDS/packages/property-properties/.eslintrc.cjs, which had an entry for a non-existent rule. Tyler corrected the rule name. --- examples/benchmarks/bubblebench/common/.eslintrc.cjs | 4 +++- .../PropertyDDS/packages/property-changeset/.eslintrc.cjs | 1 + .../PropertyDDS/packages/property-common/.eslintrc.cjs | 1 + .../PropertyDDS/packages/property-properties/.eslintrc.cjs | 4 +++- .../end-to-end-tests/azure-client/.eslintrc.cjs | 1 + packages/test/stochastic-test-utils/.eslintrc.cjs | 1 + packages/test/test-pairwise-generator/.eslintrc.cjs | 3 +++ .../tools/devtools/devtools-browser-extension/.eslintrc.cjs | 2 ++ 8 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/benchmarks/bubblebench/common/.eslintrc.cjs b/examples/benchmarks/bubblebench/common/.eslintrc.cjs index 484c63b7e874..bd863a6a893a 100644 --- a/examples/benchmarks/bubblebench/common/.eslintrc.cjs +++ b/examples/benchmarks/bubblebench/common/.eslintrc.cjs @@ -5,5 +5,7 @@ module.exports = { extends: [require.resolve("@fluidframework/eslint-config-fluid"), "prettier"], - rules: {}, + rules: { + "@fluid-internal/fluid/no-unchecked-record-access": "warn", + }, }; diff --git a/experimental/PropertyDDS/packages/property-changeset/.eslintrc.cjs b/experimental/PropertyDDS/packages/property-changeset/.eslintrc.cjs index b28b0ecdd060..263d0dbfca0c 100644 --- a/experimental/PropertyDDS/packages/property-changeset/.eslintrc.cjs +++ b/experimental/PropertyDDS/packages/property-changeset/.eslintrc.cjs @@ -54,5 +54,6 @@ module.exports = { "tsdoc/syntax": "off", "unicorn/better-regex": "off", "unicorn/filename-case": "off", + "@fluid-internal/fluid/no-unchecked-record-access": "warn", }, }; diff --git a/experimental/PropertyDDS/packages/property-common/.eslintrc.cjs b/experimental/PropertyDDS/packages/property-common/.eslintrc.cjs index b87d46b057cc..6439561c2c80 100644 --- a/experimental/PropertyDDS/packages/property-common/.eslintrc.cjs +++ b/experimental/PropertyDDS/packages/property-common/.eslintrc.cjs @@ -12,6 +12,7 @@ module.exports = { project: ["./tsconfig.json", "./src/test/tsconfig.json"], }, rules: { + "@fluid-internal/fluid/no-unchecked-record-access": "warn", "prefer-arrow-callback": "off", "tsdoc/syntax": "off", }, diff --git a/experimental/PropertyDDS/packages/property-properties/.eslintrc.cjs b/experimental/PropertyDDS/packages/property-properties/.eslintrc.cjs index 5f1337f4b6fd..c0cc797a0f39 100644 --- a/experimental/PropertyDDS/packages/property-properties/.eslintrc.cjs +++ b/experimental/PropertyDDS/packages/property-properties/.eslintrc.cjs @@ -18,7 +18,6 @@ module.exports = { "@typescript-eslint/dot-notation": "off", "@typescript-eslint/no-dynamic-delete": "off", "@typescript-eslint/no-extraneous-class": "off", - "@typescript-eslint/no-extraneous-dependencies": "off", "@typescript-eslint/no-implied-eval": "off", "@typescript-eslint/no-invalid-this": "off", "@typescript-eslint/no-require-imports": "off", @@ -42,6 +41,7 @@ module.exports = { "@typescript-eslint/unbound-method": "off", "guard-for-in": "off", "import/no-duplicates": "off", + "import/no-extraneous-dependencies": "off", "import/no-internal-modules": "off", "max-len": "off", "no-bitwise": "off", @@ -60,5 +60,7 @@ module.exports = { "quote-props": "off", "tsdoc/syntax": "off", "unicorn/better-regex": "off", + + "@fluid-internal/fluid/no-unchecked-record-access": "warn", }, }; diff --git a/packages/service-clients/end-to-end-tests/azure-client/.eslintrc.cjs b/packages/service-clients/end-to-end-tests/azure-client/.eslintrc.cjs index 5210e990439d..f3531ca7789e 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/.eslintrc.cjs +++ b/packages/service-clients/end-to-end-tests/azure-client/.eslintrc.cjs @@ -8,6 +8,7 @@ module.exports = { rules: { "prefer-arrow-callback": "off", "@typescript-eslint/strict-boolean-expressions": "off", // requires strictNullChecks=true in tsconfig + "@fluid-internal/fluid/no-unchecked-record-access": "warn", }, parserOptions: { project: ["./src/test/tsconfig.json"], diff --git a/packages/test/stochastic-test-utils/.eslintrc.cjs b/packages/test/stochastic-test-utils/.eslintrc.cjs index b8ea1b89b791..ff3f1b4e757c 100644 --- a/packages/test/stochastic-test-utils/.eslintrc.cjs +++ b/packages/test/stochastic-test-utils/.eslintrc.cjs @@ -12,6 +12,7 @@ module.exports = { project: ["./tsconfig.json", "./src/test/tsconfig.json"], }, rules: { + "@fluid-internal/fluid/no-unchecked-record-access": "warn", "import/no-nodejs-modules": "off", }, }; diff --git a/packages/test/test-pairwise-generator/.eslintrc.cjs b/packages/test/test-pairwise-generator/.eslintrc.cjs index fd46f852c0e4..6dba3d131b9f 100644 --- a/packages/test/test-pairwise-generator/.eslintrc.cjs +++ b/packages/test/test-pairwise-generator/.eslintrc.cjs @@ -5,4 +5,7 @@ module.exports = { extends: ["@fluidframework/eslint-config-fluid/minimal-deprecated", "prettier"], + rules: { + "@fluid-internal/fluid/no-unchecked-record-access": "warn", + }, }; diff --git a/packages/tools/devtools/devtools-browser-extension/.eslintrc.cjs b/packages/tools/devtools/devtools-browser-extension/.eslintrc.cjs index c96b98fbf340..1eb931840396 100644 --- a/packages/tools/devtools/devtools-browser-extension/.eslintrc.cjs +++ b/packages/tools/devtools/devtools-browser-extension/.eslintrc.cjs @@ -26,6 +26,8 @@ module.exports = { devDependencies: ["src/**/test/**"], }, ], + + "@fluid-internal/fluid/no-unchecked-record-access": "warn", }, overrides: [ { From 4b8443519883ba6ea6346a623f93eda9a57b5750 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Tue, 26 Nov 2024 19:11:04 -0800 Subject: [PATCH 36/40] build(client): Fix configs preventing incremental build (#22897) Fixes several problems causing builds to not be fully incremental. 1. data-object-base had no tests but included test-related tasks. Since no test output was generated, the task was never incremental. Those superfluous tasks have been removed. They can be added back if/when the package is no longer experimental. 2. Added new declarative tasks for `flub check buildversion` and `markdown-magic`. 3. The ai-collab project has `"allowJs": true` in its tsconfig. That setting causes problems with incremental builds (see #23197 for more details). It's removed in this change but if it is truly we can add it back. --------- Co-authored-by: Alex Villarreal <716334+alexvy86@users.noreply.github.com> --- examples/apps/ai-collab/tsconfig.json | 1 - fluidBuild.config.cjs | 37 ++++++++++++++++++- .../framework/data-object-base/.eslintrc.cjs | 2 +- .../framework/data-object-base/package.json | 7 +--- .../src/test/tsconfig.cjs.json | 12 ------ .../data-object-base/src/test/tsconfig.json | 15 -------- 6 files changed, 37 insertions(+), 37 deletions(-) delete mode 100644 packages/framework/data-object-base/src/test/tsconfig.cjs.json delete mode 100644 packages/framework/data-object-base/src/test/tsconfig.json diff --git a/examples/apps/ai-collab/tsconfig.json b/examples/apps/ai-collab/tsconfig.json index f1fc91ac3ed1..4e0506976f6e 100644 --- a/examples/apps/ai-collab/tsconfig.json +++ b/examples/apps/ai-collab/tsconfig.json @@ -2,7 +2,6 @@ "extends": "../../../common/build/build-common/tsconfig.node16.json", "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, diff --git a/fluidBuild.config.cjs b/fluidBuild.config.cjs index c4e9afd2e513..7841ae6f9f82 100644 --- a/fluidBuild.config.cjs +++ b/fluidBuild.config.cjs @@ -160,10 +160,39 @@ module.exports = { multiCommandExecutables: ["oclif", "syncpack"], declarativeTasks: { + // fluid-build lowercases the executable name, so we need to use buildversion instead of buildVersion. + "flub check buildversion": { + inputGlobs: [ + "package.json", + + // release group packages; while ** is supported, it is very slow, so these entries capture all the levels we + // have packages at today. Once we can upgrade to a later version of + // globby things might be faster. + "{azure,examples,experimental,packages}/*/*/package.json", + "{azure,examples,experimental,packages}/*/*/*/package.json", + "{azure,examples,experimental,packages}/*/*/*/*/package.json", + "tools/markdown-magic/package.json", + ], + outputGlobs: ["package.json"], + gitignore: ["input", "output"], + }, "jssm-viz": { inputGlobs: ["src/**/*.fsl"], outputGlobs: ["src/**/*.fsl.svg"], }, + "markdown-magic": { + inputGlobs: [], + outputGlobs: [ + // release group packages; while ** is supported, it is very slow, so these entries capture all the levels we + // have generated markdown files at today. Once we can upgrade to a later version of + // globby things might be faster. + "{azure,examples,experimental,packages}/*/*/*.md", + "{azure,examples,experimental,packages}/*/*/*/*.md", + "{azure,examples,experimental,packages}/*/*/*/*/*.md", + "tools/markdown-magic/**/*.md", + ], + gitignore: ["input", "output"], + }, "oclif manifest": { inputGlobs: ["package.json", "src/**"], outputGlobs: ["oclif.manifest.json"], @@ -188,7 +217,9 @@ module.exports = { outputGlobs: [ "package.json", - // release group packages + // release group packages; while ** is supported, it is very slow, so these entries capture all the levels we + // have packages at today. Once we can upgrade to a later version of + // globby things might be faster. "{azure,examples,experimental,packages}/*/*/package.json", "{azure,examples,experimental,packages}/*/*/*/package.json", "{azure,examples,experimental,packages}/*/*/*/*/package.json", @@ -212,7 +243,9 @@ module.exports = { outputGlobs: [ "package.json", - // release group packages + // release group packages; while ** is supported, it is very slow, so these entries capture all the levels we + // have packages at today. Once we can upgrade to a later version of + // globby things might be faster. "{azure,examples,experimental,packages}/*/*/package.json", "{azure,examples,experimental,packages}/*/*/*/package.json", "{azure,examples,experimental,packages}/*/*/*/*/package.json", diff --git a/packages/framework/data-object-base/.eslintrc.cjs b/packages/framework/data-object-base/.eslintrc.cjs index 831eb9f65973..c3bad505f132 100644 --- a/packages/framework/data-object-base/.eslintrc.cjs +++ b/packages/framework/data-object-base/.eslintrc.cjs @@ -6,7 +6,7 @@ module.exports = { extends: [require.resolve("@fluidframework/eslint-config-fluid/strict"), "prettier"], parserOptions: { - project: ["./tsconfig.json", "./src/test/tsconfig.json"], + project: ["./tsconfig.json"], }, rules: { "@typescript-eslint/strict-boolean-expressions": "off", diff --git a/packages/framework/data-object-base/package.json b/packages/framework/data-object-base/package.json index c587b7821b46..a11c7f69d93c 100644 --- a/packages/framework/data-object-base/package.json +++ b/packages/framework/data-object-base/package.json @@ -37,9 +37,6 @@ "build:compile": "fluid-build . --task compile", "build:docs": "api-extractor run --local", "build:esnext": "tsc --project ./tsconfig.json", - "build:test": "npm run build:test:esm && npm run build:test:cjs", - "build:test:cjs": "fluid-tsc commonjs --project ./src/test/tsconfig.cjs.json", - "build:test:esm": "tsc --project ./src/test/tsconfig.json", "check:are-the-types-wrong": "attw --pack .", "check:biome": "biome check .", "check:exports": "concurrently \"npm:check:exports:*\"", @@ -57,9 +54,7 @@ "format:prettier": "prettier --write . --cache --ignore-path ../../../.prettierignore", "lint": "fluid-build . --task lint", "lint:fix": "fluid-build . --task eslint:fix --task format", - "tsc": "fluid-tsc commonjs --project ./tsconfig.cjs.json && copyfiles -f ../../../common/build/build-common/src/cjs/package.json ./dist", - "typetests:gen": "flub generate typetests --dir . -v", - "typetests:prepare": "flub typetests --dir . --reset --previous --normalize" + "tsc": "fluid-tsc commonjs --project ./tsconfig.cjs.json && copyfiles -f ../../../common/build/build-common/src/cjs/package.json ./dist" }, "dependencies": { "@fluid-internal/client-utils": "workspace:~", diff --git a/packages/framework/data-object-base/src/test/tsconfig.cjs.json b/packages/framework/data-object-base/src/test/tsconfig.cjs.json deleted file mode 100644 index f966e4a2484f..000000000000 --- a/packages/framework/data-object-base/src/test/tsconfig.cjs.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - // This config must be used in a "type": "commonjs" environment. (Use fluid-tsc commonjs.) - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/test", - }, - "references": [ - { - "path": "../../tsconfig.cjs.json", - }, - ], -} diff --git a/packages/framework/data-object-base/src/test/tsconfig.json b/packages/framework/data-object-base/src/test/tsconfig.json deleted file mode 100644 index cd9607691d4a..000000000000 --- a/packages/framework/data-object-base/src/test/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../../../common/build/build-common/tsconfig.test.node16.json", - "compilerOptions": { - "rootDir": "./", - "outDir": "../../lib/test", - "types": [], - "noEmit": true, // Contains 'type tests' only -- no emmission required. - }, - "include": ["./**/*"], - "references": [ - { - "path": "../..", - }, - ], -} From a7bef9a820d64239d2e5abd711d6792e76e1b8dd Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Tue, 26 Nov 2024 19:58:27 -0800 Subject: [PATCH 37/40] build(client): Update eslint config to latest version (5.6.0) (#23050) Updates the client release group to eslint config 5.6.0. --- .../packages/azure-local-service/package.json | 2 +- .../packages/azure-service-utils/package.json | 2 +- .../test/scenario-runner/package.json | 2 +- examples/apps/ai-collab/package.json | 3 +- examples/apps/attributable-map/package.json | 2 +- .../apps/collaborative-textarea/package.json | 2 +- examples/apps/contact-collection/package.json | 2 +- examples/apps/data-object-grid/package.json | 2 +- examples/apps/presence-tracker/package.json | 2 +- examples/apps/task-selection/package.json | 2 +- examples/apps/tree-cli-app/package.json | 2 +- examples/apps/tree-comparison/package.json | 2 +- .../bubblebench/baseline/package.json | 2 +- .../bubblebench/common/package.json | 2 +- .../experimental-tree/package.json | 2 +- .../benchmarks/bubblebench/ot/package.json | 2 +- .../bubblebench/shared-tree/package.json | 2 +- .../package.json | 2 +- examples/benchmarks/tablebench/package.json | 2 +- .../app-insights-logger/package.json | 2 +- examples/data-objects/canvas/package.json | 2 +- examples/data-objects/clicker/package.json | 2 +- examples/data-objects/codemirror/package.json | 2 +- examples/data-objects/diceroller/package.json | 2 +- .../data-objects/inventory-app/package.json | 2 +- examples/data-objects/monaco/package.json | 2 +- .../constellation-model/package.json | 2 +- .../multiview/constellation-view/package.json | 2 +- .../multiview/container/package.json | 2 +- .../multiview/coordinate-model/package.json | 2 +- .../multiview/interface/package.json | 2 +- .../plot-coordinate-view/package.json | 2 +- .../slider-coordinate-view/package.json | 2 +- .../multiview/triangle-view/package.json | 2 +- .../data-objects/prosemirror/package.json | 2 +- examples/data-objects/smde/package.json | 3 +- .../data-objects/table-document/package.json | 2 +- examples/data-objects/todo/package.json | 2 +- examples/data-objects/webflow/package.json | 2 +- examples/external-data/package.json | 2 +- .../external-controller/package.json | 2 +- .../odsp-client/shared-tree-demo/package.json | 2 +- examples/utils/bundle-size-tests/package.json | 2 +- examples/utils/example-utils/package.json | 2 +- examples/utils/migration-tools/package.json | 2 +- .../utils/webpack-fluid-loader/package.json | 2 +- .../live-schema-upgrade/package.json | 2 +- .../same-container/package.json | 2 +- .../separate-container/package.json | 2 +- .../version-migration/tree-shim/package.json | 2 +- .../container-views/package.json | 2 +- .../external-views/package.json | 2 +- .../view-framework-sampler/package.json | 2 +- .../packages/property-common/package.json | 2 +- .../packages/property-dds/package.json | 2 +- .../dds/attributable-map/package.json | 2 +- experimental/dds/ot/ot/package.json | 2 +- .../dds/ot/sharejs/json1/package.json | 2 +- .../dds/sequence-deprecated/package.json | 2 +- experimental/dds/tree/package.json | 2 +- .../framework/data-objects/package.json | 2 +- .../framework/last-edited/package.json | 2 +- .../framework/tree-react-api/package.json | 2 +- package.json | 2 +- packages/common/client-utils/package.json | 2 +- .../common/container-definitions/package.json | 2 +- packages/common/core-interfaces/package.json | 2 +- packages/common/core-utils/package.json | 2 +- .../common/driver-definitions/package.json | 2 +- packages/dds/cell/package.json | 2 +- packages/dds/counter/package.json | 2 +- packages/dds/ink/package.json | 2 +- packages/dds/map/package.json | 2 +- packages/dds/matrix/package.json | 2 +- packages/dds/merge-tree/package.json | 2 +- packages/dds/ordered-collection/package.json | 2 +- packages/dds/pact-map/package.json | 2 +- packages/dds/register-collection/package.json | 2 +- packages/dds/sequence/package.json | 2 +- packages/dds/shared-object-base/package.json | 2 +- .../dds/shared-summary-block/package.json | 2 +- packages/dds/task-manager/package.json | 2 +- packages/dds/test-dds-utils/package.json | 2 +- packages/dds/tree/package.json | 2 +- packages/drivers/debugger/package.json | 2 +- packages/drivers/driver-base/package.json | 2 +- .../drivers/driver-web-cache/package.json | 2 +- packages/drivers/file-driver/package.json | 2 +- packages/drivers/local-driver/package.json | 2 +- .../odsp-driver-definitions/package.json | 2 +- packages/drivers/odsp-driver/package.json | 2 +- .../drivers/odsp-urlResolver/package.json | 2 +- packages/drivers/replay-driver/package.json | 2 +- .../drivers/routerlicious-driver/package.json | 2 +- .../routerlicious-urlResolver/package.json | 2 +- .../drivers/tinylicious-driver/package.json | 2 +- .../framework/agent-scheduler/package.json | 2 +- packages/framework/ai-collab/package.json | 2 +- packages/framework/aqueduct/package.json | 2 +- packages/framework/attributor/package.json | 2 +- .../framework/data-object-base/package.json | 2 +- .../framework/dds-interceptions/package.json | 2 +- .../framework/fluid-framework/package.json | 2 +- packages/framework/fluid-static/package.json | 2 +- .../oldest-client-observer/package.json | 2 +- packages/framework/presence/package.json | 2 +- .../framework/request-handler/package.json | 2 +- packages/framework/synthesize/package.json | 2 +- packages/framework/undo-redo/package.json | 2 +- packages/loader/container-loader/package.json | 2 +- packages/loader/driver-utils/package.json | 2 +- .../loader/test-loader-utils/package.json | 2 +- .../package.json | 2 +- .../runtime/container-runtime/package.json | 2 +- .../datastore-definitions/package.json | 2 +- packages/runtime/datastore/package.json | 2 +- packages/runtime/id-compressor/package.json | 2 +- .../runtime/runtime-definitions/package.json | 2 +- packages/runtime/runtime-utils/package.json | 2 +- .../runtime/test-runtime-utils/package.json | 2 +- .../service-clients/azure-client/package.json | 2 +- .../azure-client/package.json | 2 +- .../end-to-end-tests/odsp-client/package.json | 2 +- .../service-clients/odsp-client/package.json | 2 +- .../tinylicious-client/package.json | 2 +- packages/test/functional-tests/package.json | 2 +- packages/test/local-server-tests/package.json | 3 +- packages/test/mocha-test-setup/package.json | 2 +- packages/test/snapshots/package.json | 2 +- .../test/stochastic-test-utils/package.json | 2 +- .../test/test-driver-definitions/package.json | 2 +- packages/test/test-drivers/package.json | 2 +- .../test/test-end-to-end-tests/package.json | 2 +- .../test/test-pairwise-generator/package.json | 2 +- packages/test/test-service-load/package.json | 2 +- packages/test/test-utils/package.json | 2 +- packages/test/test-version-utils/package.json | 2 +- .../changelog-generator-wrapper/package.json | 2 +- .../devtools-browser-extension/package.json | 2 +- .../tools/devtools/devtools-core/package.json | 2 +- .../devtools/devtools-example/package.json | 2 +- .../tools/devtools/devtools-view/package.json | 2 +- packages/tools/devtools/devtools/package.json | 2 +- packages/tools/fetch-tool/package.json | 2 +- packages/tools/fluid-runner/package.json | 2 +- packages/tools/replay-tool/package.json | 2 +- packages/utils/odsp-doclib-utils/package.json | 2 +- packages/utils/telemetry-utils/package.json | 2 +- packages/utils/tool-utils/package.json | 2 +- pnpm-lock.yaml | 611 +++++++++--------- 150 files changed, 457 insertions(+), 455 deletions(-) diff --git a/azure/packages/azure-local-service/package.json b/azure/packages/azure-local-service/package.json index ffacb110d215..f43ec5540ff9 100644 --- a/azure/packages/azure-local-service/package.json +++ b/azure/packages/azure-local-service/package.json @@ -39,7 +39,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "eslint": "~8.55.0", "eslint-config-prettier": "~9.0.0", "pm2": "^5.4.2", diff --git a/azure/packages/azure-service-utils/package.json b/azure/packages/azure-service-utils/package.json index 8d985aab6ef1..8c0f5d91bd06 100644 --- a/azure/packages/azure-service-utils/package.json +++ b/azure/packages/azure-service-utils/package.json @@ -100,7 +100,7 @@ "@fluidframework/azure-service-utils-previous": "npm:@fluidframework/azure-service-utils@2.10.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/jsrsasign": "^10.5.12", "@types/uuid": "^9.0.2", diff --git a/azure/packages/test/scenario-runner/package.json b/azure/packages/test/scenario-runner/package.json index 7094d754eaa0..5d263851fd50 100644 --- a/azure/packages/test/scenario-runner/package.json +++ b/azure/packages/test/scenario-runner/package.json @@ -96,7 +96,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/js-yaml": "^4.0.5", "@types/mocha": "^9.1.1", "@types/nock": "^9.3.0", diff --git a/examples/apps/ai-collab/package.json b/examples/apps/ai-collab/package.json index a4efcc400fd3..9c603d88cc98 100644 --- a/examples/apps/ai-collab/package.json +++ b/examples/apps/ai-collab/package.json @@ -29,7 +29,6 @@ "start": "next dev", "start:server": "tinylicious" }, - "dependencies": {}, "devDependencies": { "@azure/identity": "^4.4.1", "@azure/msal-browser": "^3.25.0", @@ -42,7 +41,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/devtools": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/odsp-client": "workspace:~", "@fluidframework/tinylicious-client": "workspace:~", "@fluidframework/tree": "workspace:~", diff --git a/examples/apps/attributable-map/package.json b/examples/apps/attributable-map/package.json index 52762c8ff183..bd37a176d9e2 100644 --- a/examples/apps/attributable-map/package.json +++ b/examples/apps/attributable-map/package.json @@ -49,7 +49,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/node": "^18.19.0", "eslint": "~8.55.0", "html-webpack-plugin": "^5.6.0", diff --git a/examples/apps/collaborative-textarea/package.json b/examples/apps/collaborative-textarea/package.json index a3311364f643..ad5940ad1790 100644 --- a/examples/apps/collaborative-textarea/package.json +++ b/examples/apps/collaborative-textarea/package.json @@ -62,7 +62,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@fluidframework/test-utils": "workspace:~", "@types/jest": "29.5.3", diff --git a/examples/apps/contact-collection/package.json b/examples/apps/contact-collection/package.json index f5441c72276b..eae4e36566ee 100644 --- a/examples/apps/contact-collection/package.json +++ b/examples/apps/contact-collection/package.json @@ -54,7 +54,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/apps/data-object-grid/package.json b/examples/apps/data-object-grid/package.json index f4a2c5e93f1c..fb9aead1b24e 100644 --- a/examples/apps/data-object-grid/package.json +++ b/examples/apps/data-object-grid/package.json @@ -68,7 +68,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/apps/presence-tracker/package.json b/examples/apps/presence-tracker/package.json index 161f7964c39f..f7da0401772e 100644 --- a/examples/apps/presence-tracker/package.json +++ b/examples/apps/presence-tracker/package.json @@ -56,7 +56,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/apps/task-selection/package.json b/examples/apps/task-selection/package.json index 8fce5321d16a..b240ce353693 100644 --- a/examples/apps/task-selection/package.json +++ b/examples/apps/task-selection/package.json @@ -57,7 +57,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/apps/tree-cli-app/package.json b/examples/apps/tree-cli-app/package.json index ba1e0ae25549..2294878b7276 100644 --- a/examples/apps/tree-cli-app/package.json +++ b/examples/apps/tree-cli-app/package.json @@ -44,7 +44,7 @@ "@biomejs/biome": "~1.9.3", "@fluid-internal/mocha-test-setup": "workspace:~", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", "cross-env": "^7.0.3", diff --git a/examples/apps/tree-comparison/package.json b/examples/apps/tree-comparison/package.json index 5e074e0ed64c..c7c5977b43f5 100644 --- a/examples/apps/tree-comparison/package.json +++ b/examples/apps/tree-comparison/package.json @@ -63,7 +63,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/benchmarks/bubblebench/baseline/package.json b/examples/benchmarks/bubblebench/baseline/package.json index b09ce5e88b69..4cbdf3e966a7 100644 --- a/examples/benchmarks/bubblebench/baseline/package.json +++ b/examples/benchmarks/bubblebench/baseline/package.json @@ -55,7 +55,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/benchmarks/bubblebench/common/package.json b/examples/benchmarks/bubblebench/common/package.json index 3424d9d50f56..70ad62959218 100644 --- a/examples/benchmarks/bubblebench/common/package.json +++ b/examples/benchmarks/bubblebench/common/package.json @@ -55,7 +55,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "eslint": "~8.55.0", diff --git a/examples/benchmarks/bubblebench/experimental-tree/package.json b/examples/benchmarks/bubblebench/experimental-tree/package.json index 6017afef5207..a19a590e4532 100644 --- a/examples/benchmarks/bubblebench/experimental-tree/package.json +++ b/examples/benchmarks/bubblebench/experimental-tree/package.json @@ -56,7 +56,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/benchmarks/bubblebench/ot/package.json b/examples/benchmarks/bubblebench/ot/package.json index e69aa85b2702..db69a497e1bb 100644 --- a/examples/benchmarks/bubblebench/ot/package.json +++ b/examples/benchmarks/bubblebench/ot/package.json @@ -57,7 +57,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/benchmarks/bubblebench/shared-tree/package.json b/examples/benchmarks/bubblebench/shared-tree/package.json index 4403605813b0..c5717fafef3f 100644 --- a/examples/benchmarks/bubblebench/shared-tree/package.json +++ b/examples/benchmarks/bubblebench/shared-tree/package.json @@ -64,7 +64,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/benchmarks/odspsnapshotfetch-perftestapp/package.json b/examples/benchmarks/odspsnapshotfetch-perftestapp/package.json index 6503d3680ac6..17f2c934a8ed 100644 --- a/examples/benchmarks/odspsnapshotfetch-perftestapp/package.json +++ b/examples/benchmarks/odspsnapshotfetch-perftestapp/package.json @@ -51,7 +51,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/express": "^4.17.21", "@types/fs-extra": "^9.0.11", "@types/node": "^18.19.0", diff --git a/examples/benchmarks/tablebench/package.json b/examples/benchmarks/tablebench/package.json index bf0237c02cc4..f4852fe16815 100644 --- a/examples/benchmarks/tablebench/package.json +++ b/examples/benchmarks/tablebench/package.json @@ -66,7 +66,7 @@ "@fluid-tools/benchmark": "^0.50.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/id-compressor": "workspace:~", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/examples/client-logger/app-insights-logger/package.json b/examples/client-logger/app-insights-logger/package.json index 769010e8a8e2..a11ff47df115 100644 --- a/examples/client-logger/app-insights-logger/package.json +++ b/examples/client-logger/app-insights-logger/package.json @@ -58,7 +58,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^16.0.1", diff --git a/examples/data-objects/canvas/package.json b/examples/data-objects/canvas/package.json index 11a66b6d0dad..533bb485446f 100644 --- a/examples/data-objects/canvas/package.json +++ b/examples/data-objects/canvas/package.json @@ -52,7 +52,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/data-objects/clicker/package.json b/examples/data-objects/clicker/package.json index 4be7b5a88af0..6d9c14925000 100644 --- a/examples/data-objects/clicker/package.json +++ b/examples/data-objects/clicker/package.json @@ -64,7 +64,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@fluidframework/test-utils": "workspace:~", "@types/jest": "29.5.3", diff --git a/examples/data-objects/codemirror/package.json b/examples/data-objects/codemirror/package.json index 03146d635bce..1216161cce66 100644 --- a/examples/data-objects/codemirror/package.json +++ b/examples/data-objects/codemirror/package.json @@ -71,7 +71,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/codemirror": "5.60.7", "@types/node": "^18.19.0", "@types/react": "^18.3.11", diff --git a/examples/data-objects/diceroller/package.json b/examples/data-objects/diceroller/package.json index 0754a2292801..0fe93d7d137d 100644 --- a/examples/data-objects/diceroller/package.json +++ b/examples/data-objects/diceroller/package.json @@ -51,7 +51,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/data-objects/inventory-app/package.json b/examples/data-objects/inventory-app/package.json index 03ff3c0e6fd9..06ac1b829b66 100644 --- a/examples/data-objects/inventory-app/package.json +++ b/examples/data-objects/inventory-app/package.json @@ -55,7 +55,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/data-objects/monaco/package.json b/examples/data-objects/monaco/package.json index ffd026425e10..accaac1ec891 100644 --- a/examples/data-objects/monaco/package.json +++ b/examples/data-objects/monaco/package.json @@ -53,7 +53,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/react": "^18.3.11", "css-loader": "^7.1.2", "eslint": "~8.55.0", diff --git a/examples/data-objects/multiview/constellation-model/package.json b/examples/data-objects/multiview/constellation-model/package.json index 5a44ff5820dd..58511c646dc5 100644 --- a/examples/data-objects/multiview/constellation-model/package.json +++ b/examples/data-objects/multiview/constellation-model/package.json @@ -49,7 +49,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "eslint": "~8.55.0", "prettier": "~3.0.3", "rimraf": "^4.4.0", diff --git a/examples/data-objects/multiview/constellation-view/package.json b/examples/data-objects/multiview/constellation-view/package.json index 88f844caa1ad..8ca6e35fd1c8 100644 --- a/examples/data-objects/multiview/constellation-view/package.json +++ b/examples/data-objects/multiview/constellation-view/package.json @@ -48,7 +48,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/react": "^18.3.11", "copyfiles": "^2.4.1", "eslint": "~8.55.0", diff --git a/examples/data-objects/multiview/container/package.json b/examples/data-objects/multiview/container/package.json index 15e159f6cc50..bd7db764b6c6 100644 --- a/examples/data-objects/multiview/container/package.json +++ b/examples/data-objects/multiview/container/package.json @@ -63,7 +63,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/data-objects/multiview/coordinate-model/package.json b/examples/data-objects/multiview/coordinate-model/package.json index add91b55dbf8..a992b56092c4 100644 --- a/examples/data-objects/multiview/coordinate-model/package.json +++ b/examples/data-objects/multiview/coordinate-model/package.json @@ -47,7 +47,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "eslint": "~8.55.0", "prettier": "~3.0.3", "rimraf": "^4.4.0", diff --git a/examples/data-objects/multiview/interface/package.json b/examples/data-objects/multiview/interface/package.json index d9d5fb7afbd7..1afc2dddb61d 100644 --- a/examples/data-objects/multiview/interface/package.json +++ b/examples/data-objects/multiview/interface/package.json @@ -46,7 +46,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "eslint": "~8.55.0", "prettier": "~3.0.3", "rimraf": "^4.4.0", diff --git a/examples/data-objects/multiview/plot-coordinate-view/package.json b/examples/data-objects/multiview/plot-coordinate-view/package.json index 8a7f5572f18a..10cdce43ced5 100644 --- a/examples/data-objects/multiview/plot-coordinate-view/package.json +++ b/examples/data-objects/multiview/plot-coordinate-view/package.json @@ -47,7 +47,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/react": "^18.3.11", "copyfiles": "^2.4.1", "eslint": "~8.55.0", diff --git a/examples/data-objects/multiview/slider-coordinate-view/package.json b/examples/data-objects/multiview/slider-coordinate-view/package.json index 3bb6547e9c18..e172678e4757 100644 --- a/examples/data-objects/multiview/slider-coordinate-view/package.json +++ b/examples/data-objects/multiview/slider-coordinate-view/package.json @@ -47,7 +47,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/react": "^18.3.11", "copyfiles": "^2.4.1", "eslint": "~8.55.0", diff --git a/examples/data-objects/multiview/triangle-view/package.json b/examples/data-objects/multiview/triangle-view/package.json index 377e21f49410..df487853f4a4 100644 --- a/examples/data-objects/multiview/triangle-view/package.json +++ b/examples/data-objects/multiview/triangle-view/package.json @@ -47,7 +47,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/react": "^18.3.11", "copyfiles": "^2.4.1", "eslint": "~8.55.0", diff --git a/examples/data-objects/prosemirror/package.json b/examples/data-objects/prosemirror/package.json index da720bcfd9bf..9669b56cc825 100644 --- a/examples/data-objects/prosemirror/package.json +++ b/examples/data-objects/prosemirror/package.json @@ -82,7 +82,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/node": "^18.19.0", "@types/orderedmap": "^1.0.0", "@types/prosemirror-model": "^1.17.0", diff --git a/examples/data-objects/smde/package.json b/examples/data-objects/smde/package.json index 42aa89e6760e..ea3a7a07a6bb 100644 --- a/examples/data-objects/smde/package.json +++ b/examples/data-objects/smde/package.json @@ -38,7 +38,6 @@ "webpack": "webpack --env production", "webpack:dev": "webpack --env development" }, - "dependencies": {}, "devDependencies": { "@biomejs/biome": "~1.9.3", "@fluid-example/example-utils": "workspace:~", @@ -53,7 +52,7 @@ "@fluidframework/core-utils": "workspace:~", "@fluidframework/datastore": "workspace:~", "@fluidframework/datastore-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/map": "workspace:~", "@fluidframework/request-handler": "workspace:~", "@fluidframework/runtime-definitions": "workspace:~", diff --git a/examples/data-objects/table-document/package.json b/examples/data-objects/table-document/package.json index 95aa52be6993..ea83a46337a3 100644 --- a/examples/data-objects/table-document/package.json +++ b/examples/data-objects/table-document/package.json @@ -96,7 +96,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/test-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/examples/data-objects/todo/package.json b/examples/data-objects/todo/package.json index 8503acd3a74e..757ed2abe10b 100644 --- a/examples/data-objects/todo/package.json +++ b/examples/data-objects/todo/package.json @@ -57,7 +57,7 @@ "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@fluidframework/test-utils": "workspace:~", "@types/jest": "29.5.3", diff --git a/examples/data-objects/webflow/package.json b/examples/data-objects/webflow/package.json index 255104f92676..02bc559e2399 100644 --- a/examples/data-objects/webflow/package.json +++ b/examples/data-objects/webflow/package.json @@ -94,7 +94,7 @@ "@fluid-private/test-version-utils": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/test-utils": "workspace:~", "@types/debug": "^4.1.5", diff --git a/examples/external-data/package.json b/examples/external-data/package.json index 4327614870a9..238235740e72 100644 --- a/examples/external-data/package.json +++ b/examples/external-data/package.json @@ -84,7 +84,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/cors": "^2.8.4", "@types/express": "^4.17.21", diff --git a/examples/service-clients/azure-client/external-controller/package.json b/examples/service-clients/azure-client/external-controller/package.json index 539c4bd80b5a..bdee0a710653 100644 --- a/examples/service-clients/azure-client/external-controller/package.json +++ b/examples/service-clients/azure-client/external-controller/package.json @@ -60,7 +60,7 @@ "@fluidframework/container-definitions": "workspace:~", "@fluidframework/container-loader": "workspace:~", "@fluidframework/devtools": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/fluid-static": "workspace:~", "@fluidframework/local-driver": "workspace:~", "@fluidframework/server-local-server": "^5.0.0", diff --git a/examples/service-clients/odsp-client/shared-tree-demo/package.json b/examples/service-clients/odsp-client/shared-tree-demo/package.json index c157bf327908..d519754929d6 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/package.json +++ b/examples/service-clients/odsp-client/shared-tree-demo/package.json @@ -48,7 +48,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/node": "^18.19.0", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", diff --git a/examples/utils/bundle-size-tests/package.json b/examples/utils/bundle-size-tests/package.json index 8d8ab9db4593..7a7a474a53b4 100644 --- a/examples/utils/bundle-size-tests/package.json +++ b/examples/utils/bundle-size-tests/package.json @@ -52,7 +52,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/bundle-size-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@mixer/webpack-bundle-compare": "^0.1.0", "@types/node": "^18.19.0", "eslint": "~8.55.0", diff --git a/examples/utils/example-utils/package.json b/examples/utils/example-utils/package.json index 40becb4ff954..b497b6df1dfe 100644 --- a/examples/utils/example-utils/package.json +++ b/examples/utils/example-utils/package.json @@ -83,7 +83,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", diff --git a/examples/utils/migration-tools/package.json b/examples/utils/migration-tools/package.json index 79b6566eb8bc..00622aee5220 100644 --- a/examples/utils/migration-tools/package.json +++ b/examples/utils/migration-tools/package.json @@ -88,7 +88,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/uuid": "^9.0.2", "concurrently": "^8.2.1", diff --git a/examples/utils/webpack-fluid-loader/package.json b/examples/utils/webpack-fluid-loader/package.json index 16cfb3aff6b7..f98086880598 100644 --- a/examples/utils/webpack-fluid-loader/package.json +++ b/examples/utils/webpack-fluid-loader/package.json @@ -119,7 +119,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/express": "^4.17.21", "@types/fs-extra": "^9.0.11", "@types/mocha": "^9.1.1", diff --git a/examples/version-migration/live-schema-upgrade/package.json b/examples/version-migration/live-schema-upgrade/package.json index d10c85660f5b..9c1d6bb6a37a 100644 --- a/examples/version-migration/live-schema-upgrade/package.json +++ b/examples/version-migration/live-schema-upgrade/package.json @@ -56,7 +56,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/version-migration/same-container/package.json b/examples/version-migration/same-container/package.json index e568fe9c3fd6..110615a67418 100644 --- a/examples/version-migration/same-container/package.json +++ b/examples/version-migration/same-container/package.json @@ -65,7 +65,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/version-migration/separate-container/package.json b/examples/version-migration/separate-container/package.json index c1ab78f19b93..81f8f2b912bc 100644 --- a/examples/version-migration/separate-container/package.json +++ b/examples/version-migration/separate-container/package.json @@ -67,7 +67,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/version-migration/tree-shim/package.json b/examples/version-migration/tree-shim/package.json index 36af7a6a4f6b..40a25c181660 100644 --- a/examples/version-migration/tree-shim/package.json +++ b/examples/version-migration/tree-shim/package.json @@ -64,7 +64,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/view-integration/container-views/package.json b/examples/view-integration/container-views/package.json index 79a88d780686..88823dc063a9 100644 --- a/examples/view-integration/container-views/package.json +++ b/examples/view-integration/container-views/package.json @@ -52,7 +52,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/view-integration/external-views/package.json b/examples/view-integration/external-views/package.json index 73fc35e46097..d65b1d50cb0b 100644 --- a/examples/view-integration/external-views/package.json +++ b/examples/view-integration/external-views/package.json @@ -52,7 +52,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/examples/view-integration/view-framework-sampler/package.json b/examples/view-integration/view-framework-sampler/package.json index 74c500dfc332..dcc98dce7e56 100644 --- a/examples/view-integration/view-framework-sampler/package.json +++ b/examples/view-integration/view-framework-sampler/package.json @@ -53,7 +53,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", "@types/jest-environment-puppeteer": "workspace:~", diff --git a/experimental/PropertyDDS/packages/property-common/package.json b/experimental/PropertyDDS/packages/property-common/package.json index 5b055f159619..916dcb31ede0 100644 --- a/experimental/PropertyDDS/packages/property-common/package.json +++ b/experimental/PropertyDDS/packages/property-common/package.json @@ -74,7 +74,7 @@ "@fluid-internal/mocha-test-setup": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/chai": "^4.0.0", "@types/debug": "^4.1.5", diff --git a/experimental/PropertyDDS/packages/property-dds/package.json b/experimental/PropertyDDS/packages/property-dds/package.json index c82550652f6a..aad8612f06f0 100644 --- a/experimental/PropertyDDS/packages/property-dds/package.json +++ b/experimental/PropertyDDS/packages/property-dds/package.json @@ -85,7 +85,7 @@ "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-loader": "workspace:~", "@fluidframework/container-runtime": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/local-driver": "workspace:~", "@fluidframework/sequence": "workspace:~", "@fluidframework/server-local-server": "^5.0.0", diff --git a/experimental/dds/attributable-map/package.json b/experimental/dds/attributable-map/package.json index c59e9b6579f0..ccceb0edba04 100644 --- a/experimental/dds/attributable-map/package.json +++ b/experimental/dds/attributable-map/package.json @@ -110,7 +110,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/experimental/dds/ot/ot/package.json b/experimental/dds/ot/ot/package.json index 5afb8d8de5a5..09c3599a58d6 100644 --- a/experimental/dds/ot/ot/package.json +++ b/experimental/dds/ot/ot/package.json @@ -100,7 +100,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/experimental/dds/ot/sharejs/json1/package.json b/experimental/dds/ot/sharejs/json1/package.json index f7c4210edebc..b88ca8ce8972 100644 --- a/experimental/dds/ot/sharejs/json1/package.json +++ b/experimental/dds/ot/sharejs/json1/package.json @@ -100,7 +100,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/experimental/dds/sequence-deprecated/package.json b/experimental/dds/sequence-deprecated/package.json index 97996c65689b..b9820054d614 100644 --- a/experimental/dds/sequence-deprecated/package.json +++ b/experimental/dds/sequence-deprecated/package.json @@ -101,7 +101,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/diff": "^3.5.1", diff --git a/experimental/dds/tree/package.json b/experimental/dds/tree/package.json index 0d217625e51d..5d1e4b384996 100644 --- a/experimental/dds/tree/package.json +++ b/experimental/dds/tree/package.json @@ -102,7 +102,7 @@ "@fluidframework/container-definitions": "workspace:~", "@fluidframework/container-loader": "workspace:~", "@fluidframework/container-runtime": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/test-runtime-utils": "workspace:~", "@fluidframework/test-utils": "workspace:~", diff --git a/experimental/framework/data-objects/package.json b/experimental/framework/data-objects/package.json index ea0163ff0148..06db4c869b4a 100644 --- a/experimental/framework/data-objects/package.json +++ b/experimental/framework/data-objects/package.json @@ -66,7 +66,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", "concurrently": "^8.2.1", diff --git a/experimental/framework/last-edited/package.json b/experimental/framework/last-edited/package.json index 6daba75c5a86..7fa8cef4223a 100644 --- a/experimental/framework/last-edited/package.json +++ b/experimental/framework/last-edited/package.json @@ -66,7 +66,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", "concurrently": "^8.2.1", diff --git a/experimental/framework/tree-react-api/package.json b/experimental/framework/tree-react-api/package.json index 22a15dc0015e..612ea7d692f0 100644 --- a/experimental/framework/tree-react-api/package.json +++ b/experimental/framework/tree-react-api/package.json @@ -113,7 +113,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/tinylicious-client": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/package.json b/package.json index 50b942a2092b..cdd99b60412b 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "@fluid-tools/markdown-magic": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-tools": "^1.0.195075", "@microsoft/api-documenter": "^7.21.6", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/common/client-utils/package.json b/packages/common/client-utils/package.json index cf8f3b65b8e4..137e1f6abc89 100644 --- a/packages/common/client-utils/package.json +++ b/packages/common/client-utils/package.json @@ -140,7 +140,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/base64-js": "^1.3.0", "@types/jest": "29.5.3", diff --git a/packages/common/container-definitions/package.json b/packages/common/container-definitions/package.json index 9dcdccb46929..f9727624a109 100644 --- a/packages/common/container-definitions/package.json +++ b/packages/common/container-definitions/package.json @@ -103,7 +103,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-definitions-previous": "npm:@fluidframework/container-definitions@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "concurrently": "^8.2.1", "copyfiles": "^2.4.1", diff --git a/packages/common/core-interfaces/package.json b/packages/common/core-interfaces/package.json index 904ffae8751c..685c1b0f505d 100644 --- a/packages/common/core-interfaces/package.json +++ b/packages/common/core-interfaces/package.json @@ -99,7 +99,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/core-interfaces-previous": "npm:@fluidframework/core-interfaces@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", "concurrently": "^8.2.1", diff --git a/packages/common/core-utils/package.json b/packages/common/core-utils/package.json index f88132dbd8c8..e9af1054a83c 100644 --- a/packages/common/core-utils/package.json +++ b/packages/common/core-utils/package.json @@ -125,7 +125,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/core-utils-previous": "npm:@fluidframework/core-utils@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/packages/common/driver-definitions/package.json b/packages/common/driver-definitions/package.json index c84e7fae2827..cec8156d7b0b 100644 --- a/packages/common/driver-definitions/package.json +++ b/packages/common/driver-definitions/package.json @@ -98,7 +98,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/driver-definitions-previous": "npm:@fluidframework/driver-definitions@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "concurrently": "^8.2.1", "copyfiles": "^2.4.1", diff --git a/packages/dds/cell/package.json b/packages/dds/cell/package.json index 34459090f9a8..b946461a4cb0 100644 --- a/packages/dds/cell/package.json +++ b/packages/dds/cell/package.json @@ -117,7 +117,7 @@ "@fluidframework/build-tools": "^0.51.0", "@fluidframework/cell-previous": "npm:@fluidframework/cell@2.10.0", "@fluidframework/container-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/dds/counter/package.json b/packages/dds/counter/package.json index 2223d56d5dcf..bb60d7708c93 100644 --- a/packages/dds/counter/package.json +++ b/packages/dds/counter/package.json @@ -134,7 +134,7 @@ "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-definitions": "workspace:~", "@fluidframework/counter-previous": "npm:@fluidframework/counter@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/dds/ink/package.json b/packages/dds/ink/package.json index f8733126d71e..7f6cbfd780dc 100644 --- a/packages/dds/ink/package.json +++ b/packages/dds/ink/package.json @@ -102,7 +102,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/dds/map/package.json b/packages/dds/map/package.json index c3944fc0b88f..db877bd34ac5 100644 --- a/packages/dds/map/package.json +++ b/packages/dds/map/package.json @@ -146,7 +146,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/map-previous": "npm:@fluidframework/map@2.10.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/dds/matrix/package.json b/packages/dds/matrix/package.json index 766d00f3997c..853d6578d1a5 100644 --- a/packages/dds/matrix/package.json +++ b/packages/dds/matrix/package.json @@ -149,7 +149,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/matrix-previous": "npm:@fluidframework/matrix@2.10.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/dds/merge-tree/package.json b/packages/dds/merge-tree/package.json index a5a63bacbc23..e8257d9eab36 100644 --- a/packages/dds/merge-tree/package.json +++ b/packages/dds/merge-tree/package.json @@ -155,7 +155,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/merge-tree-previous": "npm:@fluidframework/merge-tree@2.10.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/dds/ordered-collection/package.json b/packages/dds/ordered-collection/package.json index fde74d941966..b33dc2041b5c 100644 --- a/packages/dds/ordered-collection/package.json +++ b/packages/dds/ordered-collection/package.json @@ -136,7 +136,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/ordered-collection-previous": "npm:@fluidframework/ordered-collection@2.10.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/dds/pact-map/package.json b/packages/dds/pact-map/package.json index aeee4658b3ad..26bbffd714e7 100644 --- a/packages/dds/pact-map/package.json +++ b/packages/dds/pact-map/package.json @@ -101,7 +101,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/dds/register-collection/package.json b/packages/dds/register-collection/package.json index 778eb1e232f7..9790c6dc7c9e 100644 --- a/packages/dds/register-collection/package.json +++ b/packages/dds/register-collection/package.json @@ -134,7 +134,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/register-collection-previous": "npm:@fluidframework/register-collection@2.10.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/dds/sequence/package.json b/packages/dds/sequence/package.json index a2a87a256d65..139defc08f8b 100644 --- a/packages/dds/sequence/package.json +++ b/packages/dds/sequence/package.json @@ -159,7 +159,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/sequence-previous": "npm:@fluidframework/sequence@2.10.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/dds/shared-object-base/package.json b/packages/dds/shared-object-base/package.json index 0c7ca581f358..06af73bb478b 100644 --- a/packages/dds/shared-object-base/package.json +++ b/packages/dds/shared-object-base/package.json @@ -139,7 +139,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/shared-object-base-previous": "npm:@fluidframework/shared-object-base@2.10.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/dds/shared-summary-block/package.json b/packages/dds/shared-summary-block/package.json index 9274c095c959..33631eada5ef 100644 --- a/packages/dds/shared-summary-block/package.json +++ b/packages/dds/shared-summary-block/package.json @@ -133,7 +133,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/shared-summary-block-previous": "npm:@fluidframework/shared-summary-block@2.10.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/dds/task-manager/package.json b/packages/dds/task-manager/package.json index 6b0eebf184c6..abd69797e518 100644 --- a/packages/dds/task-manager/package.json +++ b/packages/dds/task-manager/package.json @@ -137,7 +137,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/task-manager-previous": "npm:@fluidframework/task-manager@2.10.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/dds/test-dds-utils/package.json b/packages/dds/test-dds-utils/package.json index c89649d18ed2..9bf65c5ee1cc 100644 --- a/packages/dds/test-dds-utils/package.json +++ b/packages/dds/test-dds-utils/package.json @@ -105,7 +105,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/packages/dds/tree/package.json b/packages/dds/tree/package.json index 0d37649ebd29..0cfeff901a12 100644 --- a/packages/dds/tree/package.json +++ b/packages/dds/tree/package.json @@ -183,7 +183,7 @@ "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-definitions": "workspace:~", "@fluidframework/container-loader": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@fluidframework/test-utils": "workspace:~", "@fluidframework/tree-previous": "npm:@fluidframework/tree@2.10.0", diff --git a/packages/drivers/debugger/package.json b/packages/drivers/debugger/package.json index 8725f9267fa2..52baa59d1db0 100644 --- a/packages/drivers/debugger/package.json +++ b/packages/drivers/debugger/package.json @@ -97,7 +97,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/debugger-previous": "npm:@fluidframework/debugger@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", "concurrently": "^8.2.1", diff --git a/packages/drivers/driver-base/package.json b/packages/drivers/driver-base/package.json index e7154325a14e..3a74d6d1a459 100644 --- a/packages/drivers/driver-base/package.json +++ b/packages/drivers/driver-base/package.json @@ -114,7 +114,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/driver-base-previous": "npm:@fluidframework/driver-base@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/packages/drivers/driver-web-cache/package.json b/packages/drivers/driver-web-cache/package.json index b4371b2c9167..219e22e6e291 100644 --- a/packages/drivers/driver-web-cache/package.json +++ b/packages/drivers/driver-web-cache/package.json @@ -104,7 +104,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/driver-web-cache-previous": "npm:@fluidframework/driver-web-cache@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/jest": "29.5.3", "@types/node": "^18.19.0", diff --git a/packages/drivers/file-driver/package.json b/packages/drivers/file-driver/package.json index 183dc63538d0..918e677fd2c0 100644 --- a/packages/drivers/file-driver/package.json +++ b/packages/drivers/file-driver/package.json @@ -79,7 +79,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/file-driver-previous": "npm:@fluidframework/file-driver@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", diff --git a/packages/drivers/local-driver/package.json b/packages/drivers/local-driver/package.json index e3756c6c8dd5..6e821de2c10f 100644 --- a/packages/drivers/local-driver/package.json +++ b/packages/drivers/local-driver/package.json @@ -141,7 +141,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/local-driver-previous": "npm:@fluidframework/local-driver@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/jsrsasign": "^10.5.12", diff --git a/packages/drivers/odsp-driver-definitions/package.json b/packages/drivers/odsp-driver-definitions/package.json index 1023a01382e3..0921e31bfef7 100644 --- a/packages/drivers/odsp-driver-definitions/package.json +++ b/packages/drivers/odsp-driver-definitions/package.json @@ -96,7 +96,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/odsp-driver-definitions-previous": "npm:@fluidframework/odsp-driver-definitions@2.10.0", "@microsoft/api-extractor": "7.47.8", "concurrently": "^8.2.1", diff --git a/packages/drivers/odsp-driver/package.json b/packages/drivers/odsp-driver/package.json index 86081211cb6d..e19f0113072d 100644 --- a/packages/drivers/odsp-driver/package.json +++ b/packages/drivers/odsp-driver/package.json @@ -137,7 +137,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/odsp-driver-previous": "npm:@fluidframework/odsp-driver@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/drivers/odsp-urlResolver/package.json b/packages/drivers/odsp-urlResolver/package.json index 9520800a6f9d..dd487c87cb80 100644 --- a/packages/drivers/odsp-urlResolver/package.json +++ b/packages/drivers/odsp-urlResolver/package.json @@ -89,7 +89,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/odsp-urlresolver-previous": "npm:@fluidframework/odsp-urlresolver@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/drivers/replay-driver/package.json b/packages/drivers/replay-driver/package.json index 465b130b4806..8f87c3c5bb80 100644 --- a/packages/drivers/replay-driver/package.json +++ b/packages/drivers/replay-driver/package.json @@ -79,7 +79,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/replay-driver-previous": "npm:@fluidframework/replay-driver@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/nock": "^9.3.0", diff --git a/packages/drivers/routerlicious-driver/package.json b/packages/drivers/routerlicious-driver/package.json index 4123d9e86aa1..4bfdc83958ca 100644 --- a/packages/drivers/routerlicious-driver/package.json +++ b/packages/drivers/routerlicious-driver/package.json @@ -137,7 +137,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/routerlicious-driver-previous": "npm:@fluidframework/routerlicious-driver@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/drivers/routerlicious-urlResolver/package.json b/packages/drivers/routerlicious-urlResolver/package.json index f5e8f3b08837..850ce5581e21 100644 --- a/packages/drivers/routerlicious-urlResolver/package.json +++ b/packages/drivers/routerlicious-urlResolver/package.json @@ -87,7 +87,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/routerlicious-urlresolver-previous": "npm:@fluidframework/routerlicious-urlresolver@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/drivers/tinylicious-driver/package.json b/packages/drivers/tinylicious-driver/package.json index fe55ea129a02..6d5bc7562bd8 100644 --- a/packages/drivers/tinylicious-driver/package.json +++ b/packages/drivers/tinylicious-driver/package.json @@ -88,7 +88,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/tinylicious-driver-previous": "npm:@fluidframework/tinylicious-driver@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/jsrsasign": "^10.5.12", diff --git a/packages/framework/agent-scheduler/package.json b/packages/framework/agent-scheduler/package.json index fe8198d4dd15..b9f8e8e7c261 100644 --- a/packages/framework/agent-scheduler/package.json +++ b/packages/framework/agent-scheduler/package.json @@ -108,7 +108,7 @@ "@fluidframework/agent-scheduler-previous": "npm:@fluidframework/agent-scheduler@2.10.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", "@types/uuid": "^9.0.2", diff --git a/packages/framework/ai-collab/package.json b/packages/framework/ai-collab/package.json index 91fe5aa7661c..4f2c89cb5012 100644 --- a/packages/framework/ai-collab/package.json +++ b/packages/framework/ai-collab/package.json @@ -125,7 +125,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/id-compressor": "workspace:~", "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/test-runtime-utils": "workspace:~", diff --git a/packages/framework/aqueduct/package.json b/packages/framework/aqueduct/package.json index a504c3294d68..001c88460b5c 100644 --- a/packages/framework/aqueduct/package.json +++ b/packages/framework/aqueduct/package.json @@ -139,7 +139,7 @@ "@fluidframework/aqueduct-previous": "npm:@fluidframework/aqueduct@2.10.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/packages/framework/attributor/package.json b/packages/framework/attributor/package.json index 757a572d5cf9..4596e2e9c438 100644 --- a/packages/framework/attributor/package.json +++ b/packages/framework/attributor/package.json @@ -107,7 +107,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/merge-tree": "workspace:~", "@fluidframework/sequence": "workspace:~", "@fluidframework/test-runtime-utils": "workspace:~", diff --git a/packages/framework/data-object-base/package.json b/packages/framework/data-object-base/package.json index a11c7f69d93c..ea35831b966e 100644 --- a/packages/framework/data-object-base/package.json +++ b/packages/framework/data-object-base/package.json @@ -76,7 +76,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", "concurrently": "^8.2.1", diff --git a/packages/framework/dds-interceptions/package.json b/packages/framework/dds-interceptions/package.json index d8294397d796..d22246b3320c 100644 --- a/packages/framework/dds-interceptions/package.json +++ b/packages/framework/dds-interceptions/package.json @@ -99,7 +99,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/diff": "^3.5.1", diff --git a/packages/framework/fluid-framework/package.json b/packages/framework/fluid-framework/package.json index 80262e631fea..22c9a5fec2c3 100644 --- a/packages/framework/fluid-framework/package.json +++ b/packages/framework/fluid-framework/package.json @@ -116,7 +116,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", "concurrently": "^8.2.1", diff --git a/packages/framework/fluid-static/package.json b/packages/framework/fluid-static/package.json index 421c26a1c1fa..72aae249e997 100644 --- a/packages/framework/fluid-static/package.json +++ b/packages/framework/fluid-static/package.json @@ -120,7 +120,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/fluid-static-previous": "npm:@fluidframework/fluid-static@2.10.0", "@fluidframework/map": "workspace:~", "@fluidframework/sequence": "workspace:~", diff --git a/packages/framework/oldest-client-observer/package.json b/packages/framework/oldest-client-observer/package.json index 93a4739b2f8b..b3c51e33c585 100644 --- a/packages/framework/oldest-client-observer/package.json +++ b/packages/framework/oldest-client-observer/package.json @@ -120,7 +120,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", "concurrently": "^8.2.1", diff --git a/packages/framework/presence/package.json b/packages/framework/presence/package.json index 9a73ecdaaf4d..371e6fe69aa2 100644 --- a/packages/framework/presence/package.json +++ b/packages/framework/presence/package.json @@ -130,7 +130,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/driver-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@fluidframework/test-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/framework/request-handler/package.json b/packages/framework/request-handler/package.json index dbc25c77a9dc..579779f1a37c 100644 --- a/packages/framework/request-handler/package.json +++ b/packages/framework/request-handler/package.json @@ -129,7 +129,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/request-handler-previous": "npm:@fluidframework/request-handler@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/diff": "^3.5.1", diff --git a/packages/framework/synthesize/package.json b/packages/framework/synthesize/package.json index 2de42e71deb7..f1e5f746fc44 100644 --- a/packages/framework/synthesize/package.json +++ b/packages/framework/synthesize/package.json @@ -127,7 +127,7 @@ "@fluidframework/build-tools": "^0.51.0", "@fluidframework/core-interfaces": "workspace:~", "@fluidframework/datastore": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/synthesize-previous": "npm:@fluidframework/synthesize@2.10.0", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/framework/undo-redo/package.json b/packages/framework/undo-redo/package.json index 5e1868d0bac1..a39f09a754cb 100644 --- a/packages/framework/undo-redo/package.json +++ b/packages/framework/undo-redo/package.json @@ -111,7 +111,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@fluidframework/undo-redo-previous": "npm:@fluidframework/undo-redo@2.10.0", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/loader/container-loader/package.json b/packages/loader/container-loader/package.json index 809997236433..c195d9f8905e 100644 --- a/packages/loader/container-loader/package.json +++ b/packages/loader/container-loader/package.json @@ -193,7 +193,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-loader-previous": "npm:@fluidframework/container-loader@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/debug": "^4.1.5", "@types/double-ended-queue": "^2.1.0", diff --git a/packages/loader/driver-utils/package.json b/packages/loader/driver-utils/package.json index 76bf876c3afd..34bac828374a 100644 --- a/packages/loader/driver-utils/package.json +++ b/packages/loader/driver-utils/package.json @@ -134,7 +134,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/driver-utils-previous": "npm:@fluidframework/driver-utils@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/packages/loader/test-loader-utils/package.json b/packages/loader/test-loader-utils/package.json index be0999259f80..b36f43f22fe7 100644 --- a/packages/loader/test-loader-utils/package.json +++ b/packages/loader/test-loader-utils/package.json @@ -62,7 +62,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "concurrently": "^8.2.1", "copyfiles": "^2.4.1", diff --git a/packages/runtime/container-runtime-definitions/package.json b/packages/runtime/container-runtime-definitions/package.json index d71af51664b2..15da5cbdbaa4 100644 --- a/packages/runtime/container-runtime-definitions/package.json +++ b/packages/runtime/container-runtime-definitions/package.json @@ -93,7 +93,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-runtime-definitions-previous": "npm:@fluidframework/container-runtime-definitions@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "concurrently": "^8.2.1", "copyfiles": "^2.4.1", diff --git a/packages/runtime/container-runtime/package.json b/packages/runtime/container-runtime/package.json index cf1ef9060659..4793c376eaf8 100644 --- a/packages/runtime/container-runtime/package.json +++ b/packages/runtime/container-runtime/package.json @@ -209,7 +209,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-runtime-previous": "npm:@fluidframework/container-runtime@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/double-ended-queue": "^2.1.0", diff --git a/packages/runtime/datastore-definitions/package.json b/packages/runtime/datastore-definitions/package.json index 3fc3c37acc3c..0c9ef81f0418 100644 --- a/packages/runtime/datastore-definitions/package.json +++ b/packages/runtime/datastore-definitions/package.json @@ -94,7 +94,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/datastore-definitions-previous": "npm:@fluidframework/datastore-definitions@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "concurrently": "^8.2.1", "copyfiles": "^2.4.1", diff --git a/packages/runtime/datastore/package.json b/packages/runtime/datastore/package.json index e1746150e570..cad0d89d3fad 100644 --- a/packages/runtime/datastore/package.json +++ b/packages/runtime/datastore/package.json @@ -140,7 +140,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/datastore-previous": "npm:@fluidframework/datastore@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/lodash": "^4.14.118", diff --git a/packages/runtime/id-compressor/package.json b/packages/runtime/id-compressor/package.json index d93bcfe04c7d..0df76fb0be09 100644 --- a/packages/runtime/id-compressor/package.json +++ b/packages/runtime/id-compressor/package.json @@ -146,7 +146,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/id-compressor-previous": "npm:@fluidframework/id-compressor@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/runtime/runtime-definitions/package.json b/packages/runtime/runtime-definitions/package.json index d7135f17cc69..e403b528de66 100644 --- a/packages/runtime/runtime-definitions/package.json +++ b/packages/runtime/runtime-definitions/package.json @@ -99,7 +99,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/runtime-definitions-previous": "npm:@fluidframework/runtime-definitions@2.10.0", "@microsoft/api-extractor": "7.47.8", "concurrently": "^8.2.1", diff --git a/packages/runtime/runtime-utils/package.json b/packages/runtime/runtime-utils/package.json index 3005e43a7418..657e0bebe946 100644 --- a/packages/runtime/runtime-utils/package.json +++ b/packages/runtime/runtime-utils/package.json @@ -134,7 +134,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/runtime-utils-previous": "npm:@fluidframework/runtime-utils@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/runtime/test-runtime-utils/package.json b/packages/runtime/test-runtime-utils/package.json index 36eaeac84bb2..b29341071363 100644 --- a/packages/runtime/test-runtime-utils/package.json +++ b/packages/runtime/test-runtime-utils/package.json @@ -137,7 +137,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils-previous": "npm:@fluidframework/test-runtime-utils@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/jsrsasign": "^10.5.12", diff --git a/packages/service-clients/azure-client/package.json b/packages/service-clients/azure-client/package.json index b4410d14cef7..604e6c09403f 100644 --- a/packages/service-clients/azure-client/package.json +++ b/packages/service-clients/azure-client/package.json @@ -115,7 +115,7 @@ "@fluidframework/azure-local-service": "workspace:~", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-runtime-utils": "workspace:~", "@fluidframework/test-utils": "workspace:~", "@fluidframework/tree": "workspace:~", diff --git a/packages/service-clients/end-to-end-tests/azure-client/package.json b/packages/service-clients/end-to-end-tests/azure-client/package.json index 44a6cb856384..d4bbcc341058 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/package.json +++ b/packages/service-clients/end-to-end-tests/azure-client/package.json @@ -97,7 +97,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/driver-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/mocha": "^9.1.1", "@types/nock": "^9.3.0", "@types/node": "^18.19.0", diff --git a/packages/service-clients/end-to-end-tests/odsp-client/package.json b/packages/service-clients/end-to-end-tests/odsp-client/package.json index 3db1b908b8e4..59b8a335191a 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/package.json +++ b/packages/service-clients/end-to-end-tests/odsp-client/package.json @@ -82,7 +82,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/mocha": "^9.1.1", "@types/nock": "^9.3.0", "@types/node": "^18.19.0", diff --git a/packages/service-clients/odsp-client/package.json b/packages/service-clients/odsp-client/package.json index 2205c020bbdd..f228ffa46f1c 100644 --- a/packages/service-clients/odsp-client/package.json +++ b/packages/service-clients/odsp-client/package.json @@ -126,7 +126,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/service-clients/tinylicious-client/package.json b/packages/service-clients/tinylicious-client/package.json index 523c65ce7b59..8b3fbe17ff6a 100644 --- a/packages/service-clients/tinylicious-client/package.json +++ b/packages/service-clients/tinylicious-client/package.json @@ -109,7 +109,7 @@ "@fluidframework/build-tools": "^0.51.0", "@fluidframework/container-runtime": "workspace:~", "@fluidframework/container-runtime-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-utils": "workspace:~", "@fluidframework/tinylicious-client-previous": "npm:@fluidframework/tinylicious-client@2.10.0", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/test/functional-tests/package.json b/packages/test/functional-tests/package.json index 0d135909a6de..04fb15e81e60 100644 --- a/packages/test/functional-tests/package.json +++ b/packages/test/functional-tests/package.json @@ -72,7 +72,7 @@ "@fluidframework/container-runtime": "workspace:~", "@fluidframework/datastore-definitions": "workspace:~", "@fluidframework/driver-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/id-compressor": "workspace:~", "@fluidframework/map": "workspace:~", "@fluidframework/matrix": "workspace:~", diff --git a/packages/test/local-server-tests/package.json b/packages/test/local-server-tests/package.json index 6dfa5d66e1b1..d891c3e431ac 100644 --- a/packages/test/local-server-tests/package.json +++ b/packages/test/local-server-tests/package.json @@ -55,7 +55,6 @@ ], "temp-directory": "nyc/.nyc_output" }, - "dependencies": {}, "devDependencies": { "@biomejs/biome": "~1.9.3", "@fluid-experimental/tree": "workspace:~", @@ -74,7 +73,7 @@ "@fluidframework/datastore-definitions": "workspace:~", "@fluidframework/driver-definitions": "workspace:~", "@fluidframework/driver-utils": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/local-driver": "workspace:~", "@fluidframework/map": "workspace:~", "@fluidframework/runtime-definitions": "workspace:~", diff --git a/packages/test/mocha-test-setup/package.json b/packages/test/mocha-test-setup/package.json index 1c8066b6e8fd..f70d2e7a00cc 100644 --- a/packages/test/mocha-test-setup/package.json +++ b/packages/test/mocha-test-setup/package.json @@ -68,7 +68,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/packages/test/snapshots/package.json b/packages/test/snapshots/package.json index e8c9fe889727..93472a64554d 100644 --- a/packages/test/snapshots/package.json +++ b/packages/test/snapshots/package.json @@ -92,7 +92,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", "c8": "^8.0.1", diff --git a/packages/test/stochastic-test-utils/package.json b/packages/test/stochastic-test-utils/package.json index de3151841c48..34ca89719801 100644 --- a/packages/test/stochastic-test-utils/package.json +++ b/packages/test/stochastic-test-utils/package.json @@ -104,7 +104,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/packages/test/test-driver-definitions/package.json b/packages/test/test-driver-definitions/package.json index ac81bdba3798..d241ce446c2e 100644 --- a/packages/test/test-driver-definitions/package.json +++ b/packages/test/test-driver-definitions/package.json @@ -60,7 +60,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "concurrently": "^8.2.1", "copyfiles": "^2.4.1", diff --git a/packages/test/test-drivers/package.json b/packages/test/test-drivers/package.json index 0249c8e60c87..fa397fb53b03 100644 --- a/packages/test/test-drivers/package.json +++ b/packages/test/test-drivers/package.json @@ -80,7 +80,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/node": "^18.19.0", "@types/uuid": "^9.0.2", diff --git a/packages/test/test-end-to-end-tests/package.json b/packages/test/test-end-to-end-tests/package.json index d2b0a1596106..5cfd8cc45d2e 100644 --- a/packages/test/test-end-to-end-tests/package.json +++ b/packages/test/test-end-to-end-tests/package.json @@ -135,7 +135,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/protocol-definitions": "^3.2.0", "@types/mocha": "^9.1.1", "@types/nock": "^9.3.0", diff --git a/packages/test/test-pairwise-generator/package.json b/packages/test/test-pairwise-generator/package.json index 36e6d7defc99..c1b3df101edd 100644 --- a/packages/test/test-pairwise-generator/package.json +++ b/packages/test/test-pairwise-generator/package.json @@ -89,7 +89,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/packages/test/test-service-load/package.json b/packages/test/test-service-load/package.json index b14457a892e9..e78f2fe1b32e 100644 --- a/packages/test/test-service-load/package.json +++ b/packages/test/test-service-load/package.json @@ -113,7 +113,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", "c8": "^8.0.1", diff --git a/packages/test/test-utils/package.json b/packages/test/test-utils/package.json index 1e1e46d6a2d9..8133971cf189 100644 --- a/packages/test/test-utils/package.json +++ b/packages/test/test-utils/package.json @@ -146,7 +146,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-utils-previous": "npm:@fluidframework/test-utils@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/debug": "^4.1.5", diff --git a/packages/test/test-version-utils/package.json b/packages/test/test-version-utils/package.json index ef0cc89d4e99..6f3208a3d9a9 100644 --- a/packages/test/test-version-utils/package.json +++ b/packages/test/test-version-utils/package.json @@ -112,7 +112,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", "@types/nock": "^9.3.0", diff --git a/packages/tools/changelog-generator-wrapper/package.json b/packages/tools/changelog-generator-wrapper/package.json index a2b4ee079ae1..c842d63f4492 100644 --- a/packages/tools/changelog-generator-wrapper/package.json +++ b/packages/tools/changelog-generator-wrapper/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@fluidframework/build-common": "^2.0.3", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "concurrently": "^8.2.1", "eslint": "~8.55.0", "prettier": "~3.0.3", diff --git a/packages/tools/devtools/devtools-browser-extension/package.json b/packages/tools/devtools/devtools-browser-extension/package.json index 52e616616eef..166d36499448 100644 --- a/packages/tools/devtools/devtools-browser-extension/package.json +++ b/packages/tools/devtools/devtools-browser-extension/package.json @@ -98,7 +98,7 @@ "@fluidframework/container-definitions": "workspace:~", "@fluidframework/container-loader": "workspace:~", "@fluidframework/container-runtime-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/sequence": "workspace:~", "@fluidframework/test-utils": "workspace:~", diff --git a/packages/tools/devtools/devtools-core/package.json b/packages/tools/devtools/devtools-core/package.json index 7bbb7c914018..baf381552a16 100644 --- a/packages/tools/devtools/devtools-core/package.json +++ b/packages/tools/devtools/devtools-core/package.json @@ -134,7 +134,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/devtools-core-previous": "npm:@fluidframework/devtools-core@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/id-compressor": "workspace:~", "@fluidframework/test-runtime-utils": "workspace:~", "@microsoft/api-extractor": "7.47.8", diff --git a/packages/tools/devtools/devtools-example/package.json b/packages/tools/devtools/devtools-example/package.json index 23a3ba4ad994..188013777d94 100644 --- a/packages/tools/devtools/devtools-example/package.json +++ b/packages/tools/devtools/devtools-example/package.json @@ -76,7 +76,7 @@ "@biomejs/biome": "~1.9.3", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/test-utils": "workspace:~", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^5.16.5", diff --git a/packages/tools/devtools/devtools-view/package.json b/packages/tools/devtools/devtools-view/package.json index 37d299806554..5a898029fa22 100644 --- a/packages/tools/devtools/devtools-view/package.json +++ b/packages/tools/devtools/devtools-view/package.json @@ -90,7 +90,7 @@ "@fluidframework/build-tools": "^0.51.0", "@fluidframework/core-interfaces": "workspace:~", "@fluidframework/datastore-definitions": "workspace:~", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/shared-object-base": "workspace:~", "@microsoft/api-extractor": "7.47.8", "@previewjs/api": "^13.0.0", diff --git a/packages/tools/devtools/devtools/package.json b/packages/tools/devtools/devtools/package.json index cb5fc2f16554..5d1d2106dfa5 100644 --- a/packages/tools/devtools/devtools/package.json +++ b/packages/tools/devtools/devtools/package.json @@ -130,7 +130,7 @@ "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", "@fluidframework/devtools-previous": "npm:@fluidframework/devtools@2.10.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@microsoft/api-extractor": "7.47.8", "@types/chai": "^4.0.0", "@types/mocha": "^9.1.1", diff --git a/packages/tools/fetch-tool/package.json b/packages/tools/fetch-tool/package.json index f8b097722ec7..e5c7b20670c1 100644 --- a/packages/tools/fetch-tool/package.json +++ b/packages/tools/fetch-tool/package.json @@ -55,7 +55,7 @@ "@fluid-tools/fetch-tool-previous": "npm:@fluid-tools/fetch-tool@2.10.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/node": "^18.19.0", "copyfiles": "^2.4.1", "eslint": "~8.55.0", diff --git a/packages/tools/fluid-runner/package.json b/packages/tools/fluid-runner/package.json index 9f3b523c5bb8..0578677367c0 100644 --- a/packages/tools/fluid-runner/package.json +++ b/packages/tools/fluid-runner/package.json @@ -136,7 +136,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/fluid-runner-previous": "npm:@fluidframework/fluid-runner@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/tools/replay-tool/package.json b/packages/tools/replay-tool/package.json index 192f80c7dbe1..ae6b8c0f953a 100644 --- a/packages/tools/replay-tool/package.json +++ b/packages/tools/replay-tool/package.json @@ -83,7 +83,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@types/json-stable-stringify": "^1.0.32", "@types/node": "^18.19.0", "copyfiles": "^2.4.1", diff --git a/packages/utils/odsp-doclib-utils/package.json b/packages/utils/odsp-doclib-utils/package.json index c45674200624..18d679d08b2b 100644 --- a/packages/utils/odsp-doclib-utils/package.json +++ b/packages/utils/odsp-doclib-utils/package.json @@ -133,7 +133,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/odsp-doclib-utils-previous": "npm:@fluidframework/odsp-doclib-utils@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/mocha": "^9.1.1", diff --git a/packages/utils/telemetry-utils/package.json b/packages/utils/telemetry-utils/package.json index d896d421b288..be4358527aa9 100644 --- a/packages/utils/telemetry-utils/package.json +++ b/packages/utils/telemetry-utils/package.json @@ -131,7 +131,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/telemetry-utils-previous": "npm:@fluidframework/telemetry-utils@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/debug": "^4.1.5", diff --git a/packages/utils/tool-utils/package.json b/packages/utils/tool-utils/package.json index d8e91bf8a85a..89e6deb26eeb 100644 --- a/packages/utils/tool-utils/package.json +++ b/packages/utils/tool-utils/package.json @@ -115,7 +115,7 @@ "@fluid-tools/build-cli": "^0.51.0", "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.51.0", - "@fluidframework/eslint-config-fluid": "^5.4.0", + "@fluidframework/eslint-config-fluid": "^5.6.0", "@fluidframework/tool-utils-previous": "npm:@fluidframework/tool-utils@2.10.0", "@microsoft/api-extractor": "7.47.8", "@types/debug": "^4.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12fe1b7c343c..0a92b2988216 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,8 +49,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -131,8 +131,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) eslint: specifier: ~8.55.0 version: 8.55.0 @@ -186,8 +186,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -313,8 +313,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/js-yaml': specifier: ^4.0.5 version: 4.0.9 @@ -391,8 +391,8 @@ importers: specifier: workspace:~ version: link:../../../packages/tools/devtools/devtools '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/odsp-client': specifier: workspace:~ version: link:../../../packages/service-clients/odsp-client @@ -509,8 +509,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/node': specifier: ^18.19.0 version: 18.19.54 @@ -597,8 +597,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -727,8 +727,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -893,8 +893,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -1044,8 +1044,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -1171,8 +1171,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -1271,8 +1271,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/mocha': specifier: ^9.1.1 version: 9.1.1 @@ -1377,8 +1377,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -1504,8 +1504,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -1616,8 +1616,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/react': specifier: ^18.3.11 version: 18.3.11 @@ -1683,8 +1683,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -1798,8 +1798,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -1904,8 +1904,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -2019,8 +2019,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/express': specifier: ^4.17.21 version: 4.17.21 @@ -2125,8 +2125,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/id-compressor': specifier: workspace:~ version: link:../../../packages/runtime/id-compressor @@ -2231,8 +2231,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -2355,8 +2355,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -2473,8 +2473,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -2606,8 +2606,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/codemirror': specifier: 5.60.7 version: 5.60.7 @@ -2682,8 +2682,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -2791,8 +2791,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -2900,8 +2900,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/react': specifier: ^18.3.11 version: 18.3.11 @@ -2988,8 +2988,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) eslint: specifier: ~8.55.0 version: 8.55.0 @@ -3025,8 +3025,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/react': specifier: ^18.3.11 version: 18.3.11 @@ -3107,8 +3107,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -3207,8 +3207,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) eslint: specifier: ~8.55.0 version: 8.55.0 @@ -3241,8 +3241,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) eslint: specifier: ~8.55.0 version: 8.55.0 @@ -3275,8 +3275,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/react': specifier: ^18.3.11 version: 18.3.11 @@ -3315,8 +3315,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/react': specifier: ^18.3.11 version: 18.3.11 @@ -3355,8 +3355,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/react': specifier: ^18.3.11 version: 18.3.11 @@ -3470,8 +3470,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/node': specifier: ^18.19.0 version: 18.19.54 @@ -3569,8 +3569,8 @@ importers: specifier: workspace:~ version: link:../../../packages/runtime/datastore-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/map': specifier: workspace:~ version: link:../../../packages/dds/map @@ -3687,8 +3687,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/runtime-utils': specifier: workspace:~ version: link:../../../packages/runtime/runtime-utils @@ -3787,8 +3787,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -3929,8 +3929,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/runtime-utils': specifier: workspace:~ version: link:../../../packages/runtime/runtime-utils @@ -4128,8 +4128,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -4288,8 +4288,8 @@ importers: specifier: workspace:~ version: link:../../../../packages/tools/devtools/devtools '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/fluid-static': specifier: workspace:~ version: link:../../../../packages/framework/fluid-static @@ -4427,8 +4427,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/node': specifier: ^18.19.0 version: 18.19.54 @@ -4533,8 +4533,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(webpack-cli@5.1.4) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@mixer/webpack-bundle-compare': specifier: ^0.1.0 version: 0.1.1 @@ -4675,8 +4675,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -4781,8 +4781,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -4908,8 +4908,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/express': specifier: ^4.17.21 version: 4.17.21 @@ -5026,8 +5026,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -5180,8 +5180,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -5358,8 +5358,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -5524,8 +5524,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -5660,8 +5660,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -5772,8 +5772,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -5890,8 +5890,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-tools': specifier: ^1.0.195075 version: 1.0.195075 @@ -6105,8 +6105,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -6255,8 +6255,8 @@ importers: specifier: workspace:~ version: link:../../../../packages/runtime/container-runtime '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/local-driver': specifier: workspace:~ version: link:../../../../packages/drivers/local-driver @@ -6476,8 +6476,8 @@ importers: specifier: workspace:~ version: link:../../../packages/common/container-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../../packages/runtime/test-runtime-utils @@ -6573,8 +6573,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../../../packages/runtime/test-runtime-utils @@ -6670,8 +6670,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../../../../packages/runtime/test-runtime-utils @@ -6764,8 +6764,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../../packages/runtime/test-runtime-utils @@ -6903,8 +6903,8 @@ importers: specifier: workspace:~ version: link:../../../packages/runtime/container-runtime '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../../packages/runtime/test-runtime-utils @@ -7018,8 +7018,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -7088,8 +7088,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -7161,8 +7161,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/tinylicious-client': specifier: workspace:~ version: link:../../../packages/service-clients/tinylicious-client @@ -7261,8 +7261,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -7382,8 +7382,8 @@ importers: specifier: npm:@fluidframework/container-definitions@2.10.0 version: /@fluidframework/container-definitions@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -7430,8 +7430,8 @@ importers: specifier: npm:@fluidframework/core-interfaces@2.10.0 version: /@fluidframework/core-interfaces@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -7484,8 +7484,8 @@ importers: specifier: npm:@fluidframework/core-utils@2.10.0 version: /@fluidframework/core-utils@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -7563,8 +7563,8 @@ importers: specifier: npm:@fluidframework/driver-definitions@2.10.0 version: /@fluidframework/driver-definitions@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -7639,8 +7639,8 @@ importers: specifier: workspace:~ version: link:../../common/container-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../runtime/test-runtime-utils @@ -7739,8 +7739,8 @@ importers: specifier: npm:@fluidframework/counter@2.10.0 version: /@fluidframework/counter@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../runtime/test-runtime-utils @@ -7836,8 +7836,8 @@ importers: specifier: workspace:~ version: link:../../common/container-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../runtime/test-runtime-utils @@ -7960,8 +7960,8 @@ importers: specifier: workspace:~ version: link:../../common/container-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/map-previous': specifier: npm:@fluidframework/map@2.10.0 version: /@fluidframework/map@2.10.0(debug@4.3.7) @@ -8093,8 +8093,8 @@ importers: specifier: workspace:~ version: link:../../common/container-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/matrix-previous': specifier: npm:@fluidframework/matrix@2.10.0 version: /@fluidframework/matrix@2.10.0 @@ -8223,8 +8223,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/merge-tree-previous': specifier: npm:@fluidframework/merge-tree@2.10.0 version: /@fluidframework/merge-tree@2.10.0(debug@4.3.7) @@ -8338,8 +8338,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/ordered-collection-previous': specifier: npm:@fluidframework/ordered-collection@2.10.0 version: /@fluidframework/ordered-collection@2.10.0 @@ -8444,8 +8444,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../runtime/test-runtime-utils @@ -8547,8 +8547,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/register-collection-previous': specifier: npm:@fluidframework/register-collection@2.10.0 version: /@fluidframework/register-collection@2.10.0 @@ -8671,8 +8671,8 @@ importers: specifier: workspace:~ version: link:../../common/container-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/sequence-previous': specifier: npm:@fluidframework/sequence@2.10.0 version: /@fluidframework/sequence@2.10.0 @@ -8801,8 +8801,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/shared-object-base-previous': specifier: npm:@fluidframework/shared-object-base@2.10.0 version: /@fluidframework/shared-object-base@2.10.0(debug@4.3.7) @@ -8910,8 +8910,8 @@ importers: specifier: workspace:~ version: link:../../common/container-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/shared-summary-block-previous': specifier: npm:@fluidframework/shared-summary-block@2.10.0 version: /@fluidframework/shared-summary-block@2.10.0 @@ -9028,8 +9028,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/task-manager-previous': specifier: npm:@fluidframework/task-manager@2.10.0 version: /@fluidframework/task-manager@2.10.0 @@ -9146,8 +9146,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -9282,8 +9282,8 @@ importers: specifier: workspace:~ version: link:../../loader/container-loader '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../runtime/test-runtime-utils @@ -9400,8 +9400,8 @@ importers: specifier: npm:@fluidframework/debugger@2.10.0 version: /@fluidframework/debugger@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -9470,8 +9470,8 @@ importers: specifier: npm:@fluidframework/driver-base@2.10.0 version: /@fluidframework/driver-base@2.10.0(debug@4.3.7) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -9555,8 +9555,8 @@ importers: specifier: npm:@fluidframework/driver-web-cache@2.10.0 version: /@fluidframework/driver-web-cache@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -9628,8 +9628,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/file-driver-previous': specifier: npm:@fluidframework/file-driver@2.10.0 version: /@fluidframework/file-driver@2.10.0 @@ -9725,8 +9725,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/local-driver-previous': specifier: npm:@fluidframework/local-driver@2.10.0 version: /@fluidframework/local-driver@2.10.0(debug@4.3.7) @@ -9840,8 +9840,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/odsp-driver-previous': specifier: npm:@fluidframework/odsp-driver@2.10.0 version: /@fluidframework/odsp-driver@2.10.0(debug@4.3.7) @@ -9922,8 +9922,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/odsp-driver-definitions-previous': specifier: npm:@fluidframework/odsp-driver-definitions@2.10.0 version: /@fluidframework/odsp-driver-definitions@2.10.0 @@ -9992,8 +9992,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/odsp-urlresolver-previous': specifier: npm:@fluidframework/odsp-urlresolver@2.10.0 version: /@fluidframework/odsp-urlresolver@2.10.0 @@ -10074,8 +10074,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/replay-driver-previous': specifier: npm:@fluidframework/replay-driver@2.10.0 version: /@fluidframework/replay-driver@2.10.0 @@ -10168,8 +10168,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/routerlicious-driver-previous': specifier: npm:@fluidframework/routerlicious-driver@2.10.0 version: /@fluidframework/routerlicious-driver@2.10.0(debug@4.3.7) @@ -10268,8 +10268,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/routerlicious-urlresolver-previous': specifier: npm:@fluidframework/routerlicious-urlresolver@2.10.0 version: /@fluidframework/routerlicious-urlresolver@2.10.0 @@ -10359,8 +10359,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/tinylicious-driver-previous': specifier: npm:@fluidframework/tinylicious-driver@2.10.0 version: /@fluidframework/tinylicious-driver@2.10.0 @@ -10462,8 +10462,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -10535,8 +10535,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/id-compressor': specifier: workspace:~ version: link:../../runtime/id-compressor @@ -10656,8 +10656,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -10765,8 +10765,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/merge-tree': specifier: workspace:~ version: link:../../dds/merge-tree @@ -11059,8 +11059,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -11123,8 +11123,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../runtime/test-runtime-utils @@ -11226,8 +11226,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -11317,8 +11317,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/fluid-static-previous': specifier: npm:@fluidframework/fluid-static@2.10.0 version: /@fluidframework/fluid-static@2.10.0 @@ -11408,8 +11408,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -11502,8 +11502,8 @@ importers: specifier: workspace:~ version: link:../../common/driver-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../runtime/test-runtime-utils @@ -11593,8 +11593,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/request-handler-previous': specifier: npm:@fluidframework/request-handler@2.10.0 version: /@fluidframework/request-handler@2.10.0(debug@4.3.7) @@ -11678,8 +11678,8 @@ importers: specifier: workspace:~ version: link:../../runtime/datastore '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/runtime-utils': specifier: workspace:~ version: link:../../runtime/runtime-utils @@ -11766,8 +11766,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../runtime/test-runtime-utils @@ -11890,8 +11890,8 @@ importers: specifier: npm:@fluidframework/container-loader@2.10.0 version: /@fluidframework/container-loader@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -12002,8 +12002,8 @@ importers: specifier: npm:@fluidframework/driver-utils@2.10.0 version: /@fluidframework/driver-utils@2.10.0(debug@4.3.7) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -12087,8 +12087,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -12193,8 +12193,8 @@ importers: specifier: npm:@fluidframework/container-runtime@2.10.0 version: /@fluidframework/container-runtime@2.10.0(debug@4.3.7) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../test-runtime-utils @@ -12287,8 +12287,8 @@ importers: specifier: npm:@fluidframework/container-runtime-definitions@2.10.0 version: /@fluidframework/container-runtime-definitions@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -12372,8 +12372,8 @@ importers: specifier: npm:@fluidframework/datastore@2.10.0 version: /@fluidframework/datastore@2.10.0(debug@4.3.7) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../test-runtime-utils @@ -12463,8 +12463,8 @@ importers: specifier: npm:@fluidframework/datastore-definitions@2.10.0 version: /@fluidframework/datastore-definitions@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -12533,8 +12533,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/id-compressor-previous': specifier: npm:@fluidframework/id-compressor@2.10.0 version: /@fluidframework/id-compressor@2.10.0 @@ -12618,8 +12618,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/runtime-definitions-previous': specifier: npm:@fluidframework/runtime-definitions@2.10.0 version: /@fluidframework/runtime-definitions@2.10.0 @@ -12700,8 +12700,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/runtime-utils-previous': specifier: npm:@fluidframework/runtime-utils@2.10.0 version: /@fluidframework/runtime-utils@2.10.0(debug@4.3.7) @@ -12821,8 +12821,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils-previous': specifier: npm:@fluidframework/test-runtime-utils@2.10.0 version: /@fluidframework/test-runtime-utils@2.10.0 @@ -12936,8 +12936,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-runtime-utils': specifier: workspace:~ version: link:../../runtime/test-runtime-utils @@ -13102,8 +13102,8 @@ importers: specifier: workspace:~ version: link:../../../common/driver-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/mocha': specifier: ^9.1.1 version: 9.1.1 @@ -13217,8 +13217,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/mocha': specifier: ^9.1.1 version: 9.1.1 @@ -13311,8 +13311,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-utils': specifier: workspace:~ version: link:../../test/test-utils @@ -13420,8 +13420,8 @@ importers: specifier: workspace:~ version: link:../../runtime/container-runtime-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-utils': specifier: workspace:~ version: link:../../test/test-utils @@ -13507,8 +13507,8 @@ importers: specifier: workspace:~ version: link:../../common/driver-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/id-compressor': specifier: workspace:~ version: link:../../runtime/id-compressor @@ -13645,8 +13645,8 @@ importers: specifier: workspace:~ version: link:../../loader/driver-utils '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/local-driver': specifier: workspace:~ version: link:../../drivers/local-driver @@ -13739,8 +13739,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -13854,8 +13854,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/mocha': specifier: ^9.1.1 version: 9.1.1 @@ -13921,8 +13921,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -13994,8 +13994,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -14097,8 +14097,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -14296,8 +14296,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/protocol-definitions': specifier: ^3.2.0 version: 3.2.0 @@ -14363,8 +14363,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -14499,8 +14499,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/mocha': specifier: ^9.1.1 version: 9.1.1 @@ -14626,8 +14626,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-utils-previous': specifier: npm:@fluidframework/test-utils@2.10.0 version: /@fluidframework/test-utils@2.10.0 @@ -14798,8 +14798,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -14904,8 +14904,8 @@ importers: specifier: ^2.0.3 version: 2.0.3 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) concurrently: specifier: ^8.2.1 version: 8.2.2 @@ -14956,8 +14956,8 @@ importers: specifier: npm:@fluidframework/devtools@2.10.0 version: /@fluidframework/devtools@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@microsoft/api-extractor': specifier: 7.47.8 version: 7.47.8(patch_hash=ldzfpsbo3oeejrejk775zxplmi)(@types/node@18.19.54) @@ -15077,8 +15077,8 @@ importers: specifier: workspace:~ version: link:../../../runtime/container-runtime-definitions '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/runtime-utils': specifier: workspace:~ version: link:../../../runtime/runtime-utils @@ -15288,8 +15288,8 @@ importers: specifier: npm:@fluidframework/devtools-core@2.10.0 version: /@fluidframework/devtools-core@2.10.0 '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/id-compressor': specifier: workspace:~ version: link:../../../runtime/id-compressor @@ -15442,8 +15442,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/test-utils': specifier: workspace:~ version: link:../../../test/test-utils @@ -15611,8 +15611,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/shared-object-base': specifier: workspace:~ version: link:../../../dds/shared-object-base @@ -15810,8 +15810,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/node': specifier: ^18.19.0 version: 18.19.54 @@ -15883,8 +15883,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/fluid-runner-previous': specifier: npm:@fluidframework/fluid-runner@2.10.0 version: /@fluidframework/fluid-runner@2.10.0 @@ -16034,8 +16034,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@types/json-stable-stringify': specifier: ^1.0.32 version: 1.0.36 @@ -16104,8 +16104,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/odsp-doclib-utils-previous': specifier: npm:@fluidframework/odsp-doclib-utils@2.10.0 version: /@fluidframework/odsp-doclib-utils@2.10.0(debug@4.3.7) @@ -16192,8 +16192,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/telemetry-utils-previous': specifier: npm:@fluidframework/telemetry-utils@2.10.0 version: /@fluidframework/telemetry-utils@2.10.0 @@ -16298,8 +16298,8 @@ importers: specifier: ^0.51.0 version: 0.51.0(@types/node@18.19.54) '@fluidframework/eslint-config-fluid': - specifier: ^5.4.0 - version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + specifier: ^5.6.0 + version: 5.6.0(eslint@8.55.0)(typescript@5.4.5) '@fluidframework/tool-utils-previous': specifier: npm:@fluidframework/tool-utils@2.10.0 version: /@fluidframework/tool-utils@2.10.0 @@ -19299,8 +19299,8 @@ packages: sha.js: 2.4.11 dev: true - /@fluid-internal/eslint-plugin-fluid@0.1.2(eslint@8.55.0)(typescript@5.4.5): - resolution: {integrity: sha512-E7LF4ukpCoyZcxpDUQz0edXsKllbh4m8NAdiug6sSI1KIIQFwtq5vvW3kQ0Op5xA9w10T6crfcvmuAzdP84UGg==} + /@fluid-internal/eslint-plugin-fluid@0.1.3(eslint@8.55.0)(typescript@5.4.5): + resolution: {integrity: sha512-l3MPEQ34lYP9QOIQ9vx9ncyvkYqR7ci7ZixxD4RdOmGBnAFOqipBjn5Je9AJfztQ3jWgcT3jiV+AVT3rBjc2Yw==} dependencies: '@microsoft/tsdoc': 0.14.2 '@typescript-eslint/parser': 6.21.0(eslint@8.55.0)(typescript@5.4.5) @@ -20158,16 +20158,17 @@ packages: - supports-color dev: true - /@fluidframework/eslint-config-fluid@5.4.0(eslint@8.55.0)(typescript@5.4.5): - resolution: {integrity: sha512-V9lKsH1oFq3pX8UjSv8AyZ9BswPEcozGi3Ic/KuMdsYHj8Ibm3EgTtYSyNgVOAFivDW474qvXc5PDhKD8T/mfw==} + /@fluidframework/eslint-config-fluid@5.6.0(eslint@8.55.0)(typescript@5.4.5): + resolution: {integrity: sha512-XMQDPFpHbuSPJyRJvSg1mcjBOxCocqpGkIWNmNc/P1bdC6H01Ghi0Q4rFzb2icbJ5SyQFEOr0zAxYd882O3YdQ==} dependencies: - '@fluid-internal/eslint-plugin-fluid': 0.1.2(eslint@8.55.0)(typescript@5.4.5) + '@fluid-internal/eslint-plugin-fluid': 0.1.3(eslint@8.55.0)(typescript@5.4.5) '@microsoft/tsdoc': 0.14.2 '@rushstack/eslint-patch': 1.4.0 '@rushstack/eslint-plugin': 0.13.1(eslint@8.55.0)(typescript@5.4.5) '@rushstack/eslint-plugin-security': 0.7.1(eslint@8.55.0)(typescript@5.4.5) '@typescript-eslint/eslint-plugin': 6.7.5(@typescript-eslint/parser@6.7.5)(eslint@8.55.0)(typescript@5.4.5) '@typescript-eslint/parser': 6.7.5(eslint@8.55.0)(typescript@5.4.5) + eslint-config-biome: 1.9.3 eslint-config-prettier: 9.0.0(eslint@8.55.0) eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.7.5)(eslint-plugin-i@2.29.1)(eslint@8.55.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.55.0) @@ -28540,6 +28541,10 @@ packages: source-map: 0.6.1 dev: true + /eslint-config-biome@1.9.3: + resolution: {integrity: sha512-Zrz6Z+Gtv1jqnfHsqvpwCSqclktvQF1OnyDAfDOvjzzck2c5Nw3crEHI2KLuH+LnNBttiPAb7Y7e8sF158sOgQ==} + dev: true + /eslint-config-prettier@9.0.0(eslint@8.55.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true From 7706cd94a010647b5b6780ff8cc149933f03a2d2 Mon Sep 17 00:00:00 2001 From: Matt Rakow Date: Wed, 27 Nov 2024 08:50:12 -0800 Subject: [PATCH 38/40] fix(migration-tools): Fix id for create new request in SessionStorageSimpleLoader (#23213) --- .../src/simpleLoader/sessionStorageSimpleLoader.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/utils/migration-tools/src/simpleLoader/sessionStorageSimpleLoader.ts b/examples/utils/migration-tools/src/simpleLoader/sessionStorageSimpleLoader.ts index 25e88866fe93..3556c16c4786 100644 --- a/examples/utils/migration-tools/src/simpleLoader/sessionStorageSimpleLoader.ts +++ b/examples/utils/migration-tools/src/simpleLoader/sessionStorageSimpleLoader.ts @@ -40,24 +40,22 @@ export class SessionStorageSimpleLoader implements ISimpleLoader { public async createDetached( version: string, ): Promise<{ container: IContainer; attach: () => Promise }> { - const documentId = uuid(); const loader = new SimpleLoader({ urlResolver, documentServiceFactory: new LocalDocumentServiceFactory(localServer), codeLoader: this.codeLoader, logger: this.logger, - generateCreateNewRequest: () => createLocalResolverCreateNewRequest(documentId), + generateCreateNewRequest: () => createLocalResolverCreateNewRequest(uuid()), }); return loader.createDetached(version); } public async loadExisting(id: string): Promise { - const documentId = id; const loader = new SimpleLoader({ urlResolver, documentServiceFactory: new LocalDocumentServiceFactory(localServer), codeLoader: this.codeLoader, logger: this.logger, - generateCreateNewRequest: () => createLocalResolverCreateNewRequest(documentId), + generateCreateNewRequest: () => createLocalResolverCreateNewRequest(uuid()), }); return loader.loadExisting(`${window.location.origin}/${id}`); } From b50cf25ba14e484e40de4dca4f0e63913ea85a3f Mon Sep 17 00:00:00 2001 From: Noah Encke <78610362+noencke@users.noreply.github.com> Date: Wed, 27 Nov 2024 08:57:10 -0800 Subject: [PATCH 39/40] Move transaction event (#23218) ## Description Move a segment of code in TreeCheckout that updates the detached field index. Rather than doing it in response to a branch change event, it can be done directly after the checkout commits a transaction. This allows the code to be simpler and more direct. --- .../dds/tree/src/shared-tree/treeCheckout.ts | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/dds/tree/src/shared-tree/treeCheckout.ts b/packages/dds/tree/src/shared-tree/treeCheckout.ts index ab08a104ee6e..7a26edf947a4 100644 --- a/packages/dds/tree/src/shared-tree/treeCheckout.ts +++ b/packages/dds/tree/src/shared-tree/treeCheckout.ts @@ -63,7 +63,7 @@ import { type SharedTreeBranchChange, type Transactor, } from "../shared-tree-core/index.js"; -import { Breakable, disposeSymbol, fail, getLast, hasSingle, hasSome } from "../util/index.js"; +import { Breakable, disposeSymbol, fail, getLast, hasSome } from "../util/index.js"; import { SharedTreeChangeFamily, hasSchemaChange } from "./sharedTreeChangeFamily.js"; import type { SharedTreeChange } from "./sharedTreeChangeTypes.js"; @@ -429,17 +429,6 @@ export class TreeCheckout implements ITreeCheckoutFork { } this.events.emit("afterBatch"); } - if (event.type === "replace" && getChangeReplaceType(event) === "transactionCommit") { - assert( - hasSingle(event.newCommits), - "Expected exactly one new commit for transaction commit event", - ); - const firstCommit = event.newCommits[0]; - const transactionRevision = firstCommit.revision; - for (const transactionStep of event.removedCommits) { - this.removedRoots.updateMajor(transactionStep.revision, transactionRevision); - } - } }); _branch.events.on("afterChange", (event) => { // The following logic allows revertibles to be generated for the change. @@ -674,9 +663,14 @@ export class TreeCheckout implements ITreeCheckoutFork { // If a transaction is rolled back, revert removed roots back to the latest snapshot this.removedRoots = removedRoots; break; - case TransactionResult.Commit: - this._branch.squashAfter(startCommit); + case TransactionResult.Commit: { + const removedCommits = this._branch.squashAfter(startCommit); + const transactionRevision = this._branch.getHead().revision; + for (const transactionStep of removedCommits) { + this.removedRoots.updateMajor(transactionStep.revision, transactionRevision); + } break; + } default: unreachableCase(result); } @@ -900,13 +894,12 @@ export class TreeCheckout implements ITreeCheckoutFork { */ private isRemoteChangeEvent(event: SharedTreeBranchChange): boolean { return ( - // remote changes are only ever applied to the main branch + // Remote changes are only ever applied to the main branch !this.isBranch && - // remote changes are applied to the main branch by rebasing it onto the trunk, - // no other rebases are allowed on the main branch so this means any replaces that are not - // transaction commits are remote changes + // Remote changes are applied to the main branch by rebasing it onto the trunk. + // No other rebases are allowed on the main branch, so we can use this to detect remote changes. event.type === "replace" && - getChangeReplaceType(event) !== "transactionCommit" + getChangeReplaceType(event) === "rebase" ); } } From 5d813e5b68bf8633cd4b43ec366cb9754e3f9d2e Mon Sep 17 00:00:00 2001 From: Jason Hartman Date: Wed, 27 Nov 2024 12:35:31 -0800 Subject: [PATCH 40/40] test(client-presence): enable API lint policy check (#23221) --- fluidBuild.config.cjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/fluidBuild.config.cjs b/fluidBuild.config.cjs index 7841ae6f9f82..ff1df8a82488 100644 --- a/fluidBuild.config.cjs +++ b/fluidBuild.config.cjs @@ -423,8 +423,6 @@ module.exports = { "^examples/data-objects/table-document/", // AB#8147: ./test/EditLog export should be ./internal/... or tagged for support "^experimental/dds/tree/", - // comments in api-extractor JSON files fail parsing - PR #22498 to fix - "^packages/framework/presence/", // Packages with APIs that don't need strict API linting "^build-tools/",