diff --git a/PACKAGES.md b/PACKAGES.md index 7ac9b783c777..65c7168a7037 100644 --- a/PACKAGES.md +++ b/PACKAGES.md @@ -189,7 +189,7 @@ The dependencies between layers are enforced by the layer-check command._ | Packages | Layer Dependencies | | --- | --- | -| - [@fluid-example/blobs](/examples/apps/blobs) (private)
- [@fluid-example/collaborative-textarea](/examples/apps/collaborative-textarea) (private)
- [@fluid-example/contact-collection](/examples/apps/contact-collection) (private)
- [@fluid-example/data-object-grid](/examples/apps/data-object-grid) (private)
- [@fluid-example/diceroller](/examples/apps/diceroller) (private)
- [@fluid-example/presence-tracker](/examples/apps/presence-tracker) (private)
- [@fluid-example/staging](/examples/apps/staging) (private)
- [@fluid-example/task-selection](/examples/apps/task-selection) (private)
- [@fluid-example/tree-cli-app](/examples/apps/tree-cli-app) (private)
- [@fluid-example/tree-comparison](/examples/apps/tree-comparison) (private)
- [@fluid-example/bubblebench-baseline](/examples/benchmarks/bubblebench/baseline) (private)
- [@fluid-example/bubblebench-common](/examples/benchmarks/bubblebench/common) (private)
- [@fluid-example/bubblebench-experimental-tree](/examples/benchmarks/bubblebench/experimental-tree) (private)
- [@fluid-example/bubblebench-ot](/examples/benchmarks/bubblebench/ot) (private)
- [@fluid-example/bubblebench-shared-tree](/examples/benchmarks/bubblebench/shared-tree) (private)
- [@fluid-internal/tablebench](/examples/benchmarks/tablebench) (private)
- [@fluid-example/app-insights-logger](/examples/client-logger/app-insights-logger) (private)
- [@fluid-example/canvas](/examples/data-objects/canvas) (private)
- [@fluid-example/clicker](/examples/data-objects/clicker) (private)
- [@fluid-example/codemirror](/examples/data-objects/codemirror) (private)
- [@fluid-example/inventory-app](/examples/data-objects/inventory-app) (private)
- [@fluid-example/monaco](/examples/data-objects/monaco) (private)
- [@fluid-example/multiview-constellation-model](/examples/data-objects/multiview/constellation-model) (private)
- [@fluid-example/multiview-constellation-view](/examples/data-objects/multiview/constellation-view) (private)
- [@fluid-example/multiview-container](/examples/data-objects/multiview/container) (private)
- [@fluid-example/multiview-coordinate-model](/examples/data-objects/multiview/coordinate-model) (private)
- [@fluid-example/multiview-coordinate-interface](/examples/data-objects/multiview/interface) (private)
- [@fluid-example/multiview-plot-coordinate-view](/examples/data-objects/multiview/plot-coordinate-view) (private)
- [@fluid-example/multiview-slider-coordinate-view](/examples/data-objects/multiview/slider-coordinate-view) (private)
- [@fluid-example/multiview-triangle-view](/examples/data-objects/multiview/triangle-view) (private)
- [@fluid-example/prosemirror](/examples/data-objects/prosemirror) (private)
- [@fluid-example/smde](/examples/data-objects/smde) (private)
- [@fluid-example/table-document](/examples/data-objects/table-document)
- [@fluid-example/table-tree](/examples/data-objects/table-tree) (private)
- [@fluid-example/text-editor](/examples/data-objects/text-editor) (private)
- [@fluid-example/todo](/examples/data-objects/todo) (private)
- [@fluid-example/webflow](/examples/data-objects/webflow) (private)
- [@fluid-example/app-integration-external-data](/examples/external-data) (private)
- [@fluid-example/azure-client-todo-list](/examples/service-clients/azure-client/todo-list) (private)
- [@fluid-example/shared-tree-demo](/examples/service-clients/odsp-client/shared-tree-demo) (private)
- [@fluid-example/bundle-size-tests](/examples/utils/bundle-size-tests) (private)
- [@fluid-example/example-driver](/examples/utils/example-driver) (private)
- [@fluid-example/example-utils](/examples/utils/example-utils) (private)
- [@fluid-example/example-webpack-integration](/examples/utils/example-webpack-integration) (private)
- [@fluid-example/import-testing](/examples/utils/import-testing) (private)
- [@fluid-example/migration-tools](/examples/utils/migration-tools) (private)
- [@fluid-example/typescript-versions-host](/examples/utils/typescript-versions-host) (private)
- [@fluid-example/webpack-fluid-loader](/examples/utils/webpack-fluid-loader) (private)
- [@fluid-example/app-integration-live-schema-upgrade](/examples/version-migration/live-schema-upgrade) (private)
- [@fluid-example/version-migration-same-container](/examples/version-migration/same-container) (private)
- [@fluid-example/version-migration-separate-container](/examples/version-migration/separate-container) (private)
- [@fluid-example/tree-shim](/examples/version-migration/tree-shim) (private)
- [@fluid-example/app-integration-container-views](/examples/view-integration/container-views) (private)
- [@fluid-example/app-integration-external-views](/examples/view-integration/external-views) (private)
- [@fluid-example/view-framework-sampler](/examples/view-integration/view-framework-sampler) (private) | - [Core-Interfaces](#Core-Interfaces)
- [Driver-Definitions](#Driver-Definitions)
- [Container-Definitions](#Container-Definitions)
- [Core-Utils](#Core-Utils)
- [Client-Utils](#Client-Utils)
- [Telemetry-Utils](#Telemetry-Utils)
- [Driver-Utils](#Driver-Utils)
- [Other-Utils](#Other-Utils)
- [Tool-Utils](#Tool-Utils)
- [Driver](#Driver)
- [Loader](#Loader)
- [Runtime](#Runtime)
- [Framework](#Framework)
- [UberPackage](#UberPackage)
- [Server-Libs](#Server-Libs)
- [Routerlicious-Driver](#Routerlicious-Driver)
- [Test-Utils](#Test-Utils)
- [ServiceClients](#ServiceClients)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  | +| - [@fluid-example/blobs](/examples/apps/blobs) (private)
- [@fluid-example/claims-example](/examples/apps/claims-example) (private)
- [@fluid-example/collaborative-textarea](/examples/apps/collaborative-textarea) (private)
- [@fluid-example/contact-collection](/examples/apps/contact-collection) (private)
- [@fluid-example/data-object-grid](/examples/apps/data-object-grid) (private)
- [@fluid-example/diceroller](/examples/apps/diceroller) (private)
- [@fluid-example/presence-tracker](/examples/apps/presence-tracker) (private)
- [@fluid-example/staging](/examples/apps/staging) (private)
- [@fluid-example/task-selection](/examples/apps/task-selection) (private)
- [@fluid-example/tree-cli-app](/examples/apps/tree-cli-app) (private)
- [@fluid-example/tree-comparison](/examples/apps/tree-comparison) (private)
- [@fluid-example/bubblebench-baseline](/examples/benchmarks/bubblebench/baseline) (private)
- [@fluid-example/bubblebench-common](/examples/benchmarks/bubblebench/common) (private)
- [@fluid-example/bubblebench-experimental-tree](/examples/benchmarks/bubblebench/experimental-tree) (private)
- [@fluid-example/bubblebench-ot](/examples/benchmarks/bubblebench/ot) (private)
- [@fluid-example/bubblebench-shared-tree](/examples/benchmarks/bubblebench/shared-tree) (private)
- [@fluid-internal/tablebench](/examples/benchmarks/tablebench) (private)
- [@fluid-example/app-insights-logger](/examples/client-logger/app-insights-logger) (private)
- [@fluid-example/canvas](/examples/data-objects/canvas) (private)
- [@fluid-example/clicker](/examples/data-objects/clicker) (private)
- [@fluid-example/codemirror](/examples/data-objects/codemirror) (private)
- [@fluid-example/inventory-app](/examples/data-objects/inventory-app) (private)
- [@fluid-example/monaco](/examples/data-objects/monaco) (private)
- [@fluid-example/multiview-constellation-model](/examples/data-objects/multiview/constellation-model) (private)
- [@fluid-example/multiview-constellation-view](/examples/data-objects/multiview/constellation-view) (private)
- [@fluid-example/multiview-container](/examples/data-objects/multiview/container) (private)
- [@fluid-example/multiview-coordinate-model](/examples/data-objects/multiview/coordinate-model) (private)
- [@fluid-example/multiview-coordinate-interface](/examples/data-objects/multiview/interface) (private)
- [@fluid-example/multiview-plot-coordinate-view](/examples/data-objects/multiview/plot-coordinate-view) (private)
- [@fluid-example/multiview-slider-coordinate-view](/examples/data-objects/multiview/slider-coordinate-view) (private)
- [@fluid-example/multiview-triangle-view](/examples/data-objects/multiview/triangle-view) (private)
- [@fluid-example/prosemirror](/examples/data-objects/prosemirror) (private)
- [@fluid-example/smde](/examples/data-objects/smde) (private)
- [@fluid-example/table-document](/examples/data-objects/table-document)
- [@fluid-example/table-tree](/examples/data-objects/table-tree) (private)
- [@fluid-example/text-editor](/examples/data-objects/text-editor) (private)
- [@fluid-example/todo](/examples/data-objects/todo) (private)
- [@fluid-example/webflow](/examples/data-objects/webflow) (private)
- [@fluid-example/app-integration-external-data](/examples/external-data) (private)
- [@fluid-example/azure-client-todo-list](/examples/service-clients/azure-client/todo-list) (private)
- [@fluid-example/shared-tree-demo](/examples/service-clients/odsp-client/shared-tree-demo) (private)
- [@fluid-example/bundle-size-tests](/examples/utils/bundle-size-tests) (private)
- [@fluid-example/example-driver](/examples/utils/example-driver) (private)
- [@fluid-example/example-utils](/examples/utils/example-utils) (private)
- [@fluid-example/example-webpack-integration](/examples/utils/example-webpack-integration) (private)
- [@fluid-example/import-testing](/examples/utils/import-testing) (private)
- [@fluid-example/migration-tools](/examples/utils/migration-tools) (private)
- [@fluid-example/typescript-versions-host](/examples/utils/typescript-versions-host) (private)
- [@fluid-example/webpack-fluid-loader](/examples/utils/webpack-fluid-loader) (private)
- [@fluid-example/app-integration-live-schema-upgrade](/examples/version-migration/live-schema-upgrade) (private)
- [@fluid-example/version-migration-same-container](/examples/version-migration/same-container) (private)
- [@fluid-example/version-migration-separate-container](/examples/version-migration/separate-container) (private)
- [@fluid-example/tree-shim](/examples/version-migration/tree-shim) (private)
- [@fluid-example/app-integration-container-views](/examples/view-integration/container-views) (private)
- [@fluid-example/app-integration-external-views](/examples/view-integration/external-views) (private)
- [@fluid-example/view-framework-sampler](/examples/view-integration/view-framework-sampler) (private) | - [Core-Interfaces](#Core-Interfaces)
- [Driver-Definitions](#Driver-Definitions)
- [Container-Definitions](#Container-Definitions)
- [Core-Utils](#Core-Utils)
- [Client-Utils](#Client-Utils)
- [Telemetry-Utils](#Telemetry-Utils)
- [Driver-Utils](#Driver-Utils)
- [Other-Utils](#Other-Utils)
- [Tool-Utils](#Tool-Utils)
- [Driver](#Driver)
- [Loader](#Loader)
- [Runtime](#Runtime)
- [Framework](#Framework)
- [UberPackage](#UberPackage)
- [Server-Libs](#Server-Libs)
- [Routerlicious-Driver](#Routerlicious-Driver)
- [Test-Utils](#Test-Utils)
- [ServiceClients](#ServiceClients)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  | ### Tools diff --git a/examples/apps/claims-example/.gitignore b/examples/apps/claims-example/.gitignore new file mode 100644 index 000000000000..abe1eb97c9b2 --- /dev/null +++ b/examples/apps/claims-example/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +lib diff --git a/examples/apps/claims-example/.npmignore b/examples/apps/claims-example/.npmignore new file mode 100644 index 000000000000..f518002fc4dd --- /dev/null +++ b/examples/apps/claims-example/.npmignore @@ -0,0 +1,7 @@ +nyc +*.log +**/*.tsbuildinfo +src/test +dist/test +lib/test +**/_api-extractor-temp/** diff --git a/examples/apps/claims-example/LICENSE b/examples/apps/claims-example/LICENSE new file mode 100644 index 000000000000..60af0a6a40e9 --- /dev/null +++ b/examples/apps/claims-example/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation and contributors. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/apps/claims-example/README.md b/examples/apps/claims-example/README.md new file mode 100644 index 000000000000..1bb7b9421167 --- /dev/null +++ b/examples/apps/claims-example/README.md @@ -0,0 +1,115 @@ +# @fluid-example/claims-example + +This example demonstrates the **Claims DDS** with a simple "claim a key" UI. Type any string key and claim it. Under the hood each key is paired with the **`IFluidHandle`** of a freshly created `SharedDirectory` (a real DDS) that records its owner, and the key is bound to that handle using first-writer-wins semantics — so the first client to claim a key wins, competing claims from other clients are rejected, and every client resolves the winning handle to the same shared object. + +> **Why this shape?** The Claims DDS is currently an internal building block. The intent is for it to eventually live inside every `PureDataObject`, reachable through an API on the data object itself. That API does not exist yet, so this example wires the Claims DDS up by hand inside a root data object (built on `@fluidframework/aqueduct`) that abstracts the Claims API behind a single `trySetClaim(key)` method. It therefore depends on the internal-only `@fluid-internal/claims` package and consumes the container/runtime plumbing through its `/legacy` entry points. + +This example follows the external-views pattern: the container code establishes the model (`ClaimsDataObject`) but leaves the view code (`view.tsx`) and its binding to the model up to the consumer of the container (`app.ts`). + +## What it shows + +- **A data object that abstracts the DDS** — `ClaimsDataObject` (an aqueduct `DataObject`) owns a single Claims DDS and exposes just `claimant`, `claimedKeys`, `getOwner`, and `trySetClaim(key)`. The view never touches the Claims DDS directly. +- **Handle-based claiming** — the claim value is the `IFluidHandle` of a backing `SharedDirectory` (a real DDS), not a primitive. Claiming a key creates that `SharedDirectory`, records the owner on it, and claims the key with its handle. +- **First-writer-wins** — `trySetClaim` only succeeds if the key is currently unclaimed. Open the app in two browser tabs and race to claim the same key; only the first claim wins. +- **Switching to the winner on a lost race** — when a claim loses, `ClaimResult`'s `currentValue` (the winner's handle) lets the loser switch to the winning `SharedDirectory` and report the actual owner. +- **Cross-client handle resolution** — every client resolves the winning handle to the *same* `SharedDirectory`, so all clients agree on the owner of each claimed key. + +## ClaimResult overview + +`ClaimResult` is a discriminated union returned by `trySetClaim`: + +| Status | Meaning | Available fields | +|--------|---------|-----------------| +| `"Accepted"` | Claim accepted synchronously (only in detached mode) | `currentValue: T` | +| `"AlreadyClaimed"` | Another client already claimed this key | `currentValue: T \| undefined` | +| `"Pending"` | Op is in-flight awaiting server confirmation | `promise: Promise>` | + +In a connected container, `trySetClaim` returns `"Pending"`; awaiting the promise yields a `ClaimConfirmation` whose status is `"Accepted"`, `"AlreadyClaimed"`, or `"Aborted"`. `ClaimsDataObject.trySetClaim(key)` collapses that lifecycle into a simple `boolean` (whether this client won) for the view to consume. On `"AlreadyClaimed"` it uses the reported `currentValue` (the winner's handle) to switch the loser to the winner's directory. + + + + + + +## Getting Started + +You can run this example using the following steps: + +1. Enable [corepack](https://nodejs.org/docs/latest-v16.x/api/corepack.html) by running `corepack enable`. +1. Run `pnpm install` and `pnpm run build:fast --nolint` from the `FluidFramework` root directory. + - For an even faster build, you can add the package name to the build command, like this: + `pnpm run build:fast --nolint @fluid-example/claims-example` +1. In a separate terminal, start a Tinylicious server by running `pnpm tinylicious` in this directory. +1. If using codespaces in a browser, set tinylicious (port 7070) visibility to "public". "Private to Organization" will not work. See [sharing a port](https://docs.github.com/en/codespaces/developing-in-a-codespace/forwarding-ports-in-your-codespace#sharing-a-port) for how to do this. +1. Run `pnpm start` from this directory and open in a web browser to see the app running. +1. If you want to run the app against SharePoint, follow the instructions in [webpack-fluid-loader](https://github.com/microsoft/FluidFramework/blob/main/examples/utils/webpack-fluid-loader/README.md#sharepoint) to get auth credentials. Then run `pnpm start:spo` or `pnpm start:spo-df` and open like above. + + + + + +## Testing + +```bash + npm run test:jest +``` + +For in browser testing update `./jest-puppeteer.config.js` to: + +```javascript + launch: { + dumpio: true, // output browser console to cmd line + slowMo: 500, + headless: false, + }, +``` + +## Data model + +The claims example uses the following distributed data structures: + +- Claims - exclusive key ownership, keyed by the claimed string, valued by a `SharedDirectory` handle +- SharedDirectory - one per claimed key, holding its owner (plus the data object's own root directory) + + + + + + +## Contribution Guidelines + +There are many ways to [contribute](https://github.com/microsoft/FluidFramework/blob/main/CONTRIBUTING.md) to Fluid. + +- Participate in Q&A in our [GitHub Discussions](https://github.com/microsoft/FluidFramework/discussions). +- [Submit bugs](https://github.com/microsoft/FluidFramework/issues) and help us verify fixes as they are checked in. +- Review the [source code changes](https://github.com/microsoft/FluidFramework/pulls). +- [Contribute bug fixes](https://github.com/microsoft/FluidFramework/blob/main/CONTRIBUTING.md). + +Detailed instructions for working in the repo can be found in the [Wiki](https://github.com/microsoft/FluidFramework/wiki). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +This project may contain Microsoft trademarks or logos for Microsoft projects, products, or services. +Use of these trademarks or logos must follow Microsoft’s [Trademark & Brand Guidelines](https://www.microsoft.com/trademarks). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. + +## Help + +Not finding what you're looking for in this README? Check out [fluidframework.com](https://fluidframework.com/docs/). + +Still not finding what you're looking for? Please [file an issue](https://github.com/microsoft/FluidFramework/wiki/Submitting-Bugs-and-Feature-Requests). + +Thank you! + +## Trademark + +This project may contain Microsoft trademarks or logos for Microsoft projects, products, or services. + +Use of these trademarks or logos must follow Microsoft's [Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). + +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. + + + + diff --git a/examples/apps/claims-example/eslint.config.mts b/examples/apps/claims-example/eslint.config.mts new file mode 100644 index 000000000000..08ec44e32694 --- /dev/null +++ b/examples/apps/claims-example/eslint.config.mts @@ -0,0 +1,12 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { Linter } from "eslint"; +import { recommended } from "@fluidframework/eslint-config-fluid/flat.mts"; +import sharedConfig from "../../eslint.config.data.mts"; + +const config: Linter.Config[] = [...recommended, ...sharedConfig]; + +export default config; diff --git a/examples/apps/claims-example/jest-puppeteer.config.cjs b/examples/apps/claims-example/jest-puppeteer.config.cjs new file mode 100644 index 000000000000..0ff67c6709bc --- /dev/null +++ b/examples/apps/claims-example/jest-puppeteer.config.cjs @@ -0,0 +1,22 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +module.exports = { + server: { + command: `npm run start:test -- --no-hot --no-live-reload --port ${process.env["PORT"]}`, + port: process.env["PORT"], + launchTimeout: 10000, + usedPortAction: "error", + }, + launch: { + args: ["--no-sandbox", "--disable-setuid-sandbox"], // https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#setting-up-chrome-linux-sandbox + dumpio: process.env.FLUID_TEST_VERBOSE !== undefined, // output browser console to cmd line + // slowMo: 500, // slows down process for easier viewing + + // Use chrome-headless-shell since some tests don't work as-is with the new headless mode. + // AB#7150: Remove this once we have fixed the tests. + headless: "shell", + }, +}; diff --git a/examples/apps/claims-example/jest.config.cjs b/examples/apps/claims-example/jest.config.cjs new file mode 100644 index 000000000000..c21a584e093d --- /dev/null +++ b/examples/apps/claims-example/jest.config.cjs @@ -0,0 +1,33 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +// Get the test port from the global map and set it in env for this test +const testTools = require("@fluidframework/test-tools"); +const { name } = require("./package.json"); + +const mappedPort = testTools.getTestPort(name); +process.env["PORT"] = mappedPort; + +module.exports = { + preset: "jest-puppeteer", + globals: { + PATH: `http://localhost:${mappedPort}`, + }, + testMatch: ["**/?(*.)+(spec|test).[t]s"], + testPathIgnorePatterns: ["/node_modules/", "dist"], + transform: { + "^.+\\.ts?$": "ts-jest", + }, + reporters: [ + "default", + [ + "jest-junit", + { + outputDirectory: "nyc", + outputName: "jest-junit-report.xml", + }, + ], + ], +}; diff --git a/examples/apps/claims-example/package.json b/examples/apps/claims-example/package.json new file mode 100644 index 000000000000..a52692b58ea9 --- /dev/null +++ b/examples/apps/claims-example/package.json @@ -0,0 +1,87 @@ +{ + "name": "@fluid-example/claims-example", + "version": "2.111.0", + "private": true, + "description": "Example app demonstrating exclusive resource ownership with the Claims DDS", + "homepage": "https://fluidframework.com", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/FluidFramework.git", + "directory": "examples/apps/claims-example" + }, + "license": "MIT", + "author": "Microsoft and contributors", + "type": "module", + "scripts": { + "build": "fluid-build . --task build", + "build:compile": "fluid-build . --task compile", + "build:esnext": "tsc --project ./tsconfig.json", + "check:biome": "biome check .", + "check:format": "npm run check:biome", + "clean": "rimraf --glob dist lib \"**/*.tsbuildinfo\" \"**/*.build.log\" nyc", + "eslint": "eslint --quiet --format stylish src", + "eslint:fix": "eslint --quiet --format stylish src --fix --fix-type problem,suggestion,layout", + "format": "npm run format:biome", + "format:biome": "biome check . --write", + "lint": "fluid-build . --task lint", + "lint:fix": "fluid-build . --task eslint:fix --task format", + "prepack": "npm run webpack", + "start": "npm run start:t9s", + "start:local": "webpack serve --env service=local", + "start:odsp": "webpack serve --env service=odsp", + "start:t9s": "webpack serve --env service=t9s", + "start:test": "webpack serve --env service=local", + "test": "npm run test:jest", + "test:jest": "jest --ci", + "test:jest:verbose": "cross-env FLUID_TEST_VERBOSE=1 jest --ci", + "tinylicious": "tinylicious", + "webpack": "webpack --env production", + "webpack:dev": "webpack --env development" + }, + "dependencies": { + "@fluid-example/example-driver": "workspace:~", + "@fluid-internal/claims": "workspace:~", + "@fluidframework/aqueduct": "workspace:~", + "@fluidframework/container-definitions": "workspace:~", + "@fluidframework/container-loader": "workspace:~", + "@fluidframework/core-interfaces": "workspace:~", + "@fluidframework/map": "workspace:~", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@biomejs/biome": "~2.4.5", + "@fluid-example/example-webpack-integration": "workspace:~", + "@fluid-tools/build-cli": "catalog:buildTools", + "@fluidframework/build-common": "^2.0.3", + "@fluidframework/build-tools": "catalog:buildTools", + "@fluidframework/eslint-config-fluid": "catalog:eslint", + "@fluidframework/test-tools": "^2.0.0", + "@types/jest": "29.5.3", + "@types/jest-environment-puppeteer": "workspace:~", + "@types/node": "catalog:types", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.0", + "cross-env": "^10.1.0", + "eslint": "catalog:eslint", + "expect-puppeteer": "^9.0.2", + "html-webpack-plugin": "^5.6.0", + "jest": "^29.6.2", + "jest-environment-puppeteer": "^10.1.3", + "jest-junit": "^16.0.0", + "jest-puppeteer": "^10.1.3", + "jiti": "^2.6.1", + "puppeteer": "^23.6.0", + "rimraf": "^6.1.3", + "tinylicious": "^7.0.1", + "ts-jest": "^29.1.1", + "ts-loader": "^9.5.1", + "typescript": "~5.4.5", + "webpack": "^5.94.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "~4.15.2" + }, + "typeValidation": { + "disabled": true + } +} diff --git a/examples/apps/claims-example/src/app.ts b/examples/apps/claims-example/src/app.ts new file mode 100644 index 000000000000..969a903432af --- /dev/null +++ b/examples/apps/claims-example/src/app.ts @@ -0,0 +1,102 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { + createExampleDriver, + getSpecifiedServiceFromWebpack, +} from "@fluid-example/example-driver"; +import type { + ICodeDetailsLoader, + IContainer, + IFluidCodeDetails, + IFluidModuleWithDetails, +} from "@fluidframework/container-definitions/legacy"; +import { + createDetachedContainer, + loadExistingContainer, +} from "@fluidframework/container-loader/legacy"; +import { createElement } from "react"; +// eslint-disable-next-line import-x/no-internal-modules +import { createRoot } from "react-dom/client"; + +import { + ClaimsExampleContainerRuntimeFactory, + type IClaimsDataObject, +} from "./container/index.js"; +import { ClaimsView } from "./view.js"; + +const service = getSpecifiedServiceFromWebpack(); +const { + urlResolver, + documentServiceFactory, + createCreateNewRequest, + createLoadExistingRequest, +} = await createExampleDriver(service); + +const codeLoader: ICodeDetailsLoader = { + load: async (details: IFluidCodeDetails): Promise => { + return { + module: { fluidExport: new ClaimsExampleContainerRuntimeFactory() }, + details, + }; + }, +}; + +let id: string; +let container: IContainer; + +if (location.hash.length === 0) { + // Some services support or require specifying the container id at attach time (local, odsp). For + // services that do not (t9s), the passed id will be ignored. + id = Date.now().toString(); + const createNewRequest = createCreateNewRequest(id); + container = await createDetachedContainer({ + codeDetails: { package: "1.0" }, + urlResolver, + documentServiceFactory, + codeLoader, + }); + await container.attach(createNewRequest); + // For most services, the id on the resolvedUrl is the authoritative source for the container id + // (regardless of whether the id passed in createCreateNewRequest is respected or not). However, + // for odsp the id is a hashed combination of drive and container ID which we can't use. Instead, + // we retain the id we generated above. + if (service !== "odsp") { + if (container.resolvedUrl === undefined) { + throw new Error("Resolved Url unexpectedly missing!"); + } + id = container.resolvedUrl.id; + } +} else { + id = location.hash.slice(1); + container = await loadExistingContainer({ + request: await createLoadExistingRequest(id), + urlResolver, + documentServiceFactory, + codeLoader, + }); +} + +const claimsDataObject = (await container.getEntryPoint()) as IClaimsDataObject; + +// Render view +const appDiv = document.createElement("div"); +document.body.append(appDiv); +const appRoot = createRoot(appDiv); +appRoot.render(createElement(ClaimsView, { claimsDataObject })); + +// Update url and tab title +location.hash = id; +document.title = id; + +// For testing purposes, we expose a way to load an additional instance of the container in the same page +globalThis.loadAdditionalContainer = async () => { + return loadExistingContainer({ + request: await createLoadExistingRequest(id), + urlResolver, + documentServiceFactory, + codeLoader, + }); +}; diff --git a/examples/apps/claims-example/src/container/claimsDataObject/claimsDataObject.ts b/examples/apps/claims-example/src/container/claimsDataObject/claimsDataObject.ts new file mode 100644 index 000000000000..c48fc11cdf9f --- /dev/null +++ b/examples/apps/claims-example/src/container/claimsDataObject/claimsDataObject.ts @@ -0,0 +1,224 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { ClaimsKind, type IClaims } from "@fluid-internal/claims"; +import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct/legacy"; +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { + type IDirectory, + type ISharedDirectory, + SharedDirectory, +} from "@fluidframework/map/legacy"; + +import type { IClaimsDataObject, IClaimsDataObjectEvents } from "./interface.js"; + +/** + * Key under which the Claims DDS handle is stored on the root directory. + */ +const claimsHandleKey = "claims"; + +/** + * Name of the root subdirectory used to mirror the set of claimed keys. + * + * @remarks + * The Claims DDS does not expose a way to enumerate its keys, so we mirror them into a + * subdirectory of the data object's root. This lets a client that loads an existing document + * (or joins late) discover which keys have already been claimed and resolve their winners. + */ +const claimedKeysDirName = "claimedKeys"; + +/** + * Entry recorded on each resource's backing SharedDirectory. + */ +const ownerKey = "owner"; + +type DirectoryHandle = IFluidHandle; + +interface IClaimsDataObjectTypes { + Events: IClaimsDataObjectEvents; +} + +/** + * The root data object for the claims example. + * + * @remarks + * It owns a single Claims DDS, created lazily the first time the document is created, and + * exposes a small {@link IClaimsDataObject.trySetClaim} surface so the view never touches the + * Claims DDS directly. Each claim's value is the handle of a freshly created `SharedDirectory` + * recording its owner, so every client resolves the winning handle to the same shared object. + */ +export class ClaimsDataObject + extends DataObject + implements IClaimsDataObject +{ + /** + * Each client (browser tab) gets its own identity, so claims made from different tabs + * compete with one another under first-writer-wins. + */ + public readonly claimant = `Client-${Math.random().toString(36).slice(2, 6)}`; + + /** + * The Claims DDS this data object abstracts. Set during initialization. + */ + private claims: IClaims | undefined; + + /** + * Subdirectory mirroring the claimed keys (see {@link claimedKeysDirName}). Set during + * initialization. + */ + private claimedKeysDir: IDirectory | undefined; + + /** + * Backing SharedDirectories for resolved winning claims, keyed by claim key. + */ + private readonly resolvedDirectories = new Map(); + + private get claimsOrThrow(): IClaims { + if (this.claims === undefined) { + throw new Error("ClaimsDataObject not initialized"); + } + return this.claims; + } + + private get claimedKeysDirOrThrow(): IDirectory { + if (this.claimedKeysDir === undefined) { + throw new Error("ClaimsDataObject not initialized"); + } + return this.claimedKeysDir; + } + + public get claimedKeys(): readonly string[] { + // Only keys whose winning directory has resolved are listed, so getOwner is always + // defined for a listed key. The mirror subdirectory ({@link claimedKeysDirName}) is + // used solely to rediscover and resolve keys when loading an existing document. + return [...this.resolvedDirectories.keys()]; + } + + public getOwner(key: string): string | undefined { + return this.resolvedDirectories.get(key)?.get(ownerKey); + } + + /** + * Called once when the document is first created. Creates the Claims DDS and the mirror + * subdirectory and stores them on the root. + */ + protected override async initializingFirstTime(): Promise { + const claims = ClaimsKind.create(this.runtime) as IClaims; + this.root.set(claimsHandleKey, claims.handle); + this.root.createSubDirectory(claimedKeysDirName); + } + + /** + * Called every time the data object is initialized. Resolves the Claims DDS and mirror + * subdirectory and wires up listeners so the local view stays in sync as claims are made. + */ + protected override async hasInitialized(): Promise { + const claimsHandle = + this.root.get>>(claimsHandleKey); + if (claimsHandle === undefined) { + throw new Error("Claims DDS handle missing from root"); + } + this.claims = await claimsHandle.get(); + this.claimedKeysDir = this.root.getSubDirectory(claimedKeysDirName); + + // Switch to (resolve) a key's winning directory whenever it is claimed, locally or + // remotely. This is also how a losing client ends up pointing at the winner. + this.claimsOrThrow.events.on("claimed", (key) => this.onClaimed(key)); + + // A late-joining client (or a reload of an existing document) discovers + // already-claimed keys from the mirror and resolves each of their winners. + for (const key of this.claimedKeysDirOrThrow.keys()) { + this.resolve(key); + } + } + + public readonly trySetClaim = async (key: string): Promise => { + // Each attempt creates a fresh backing directory recording this client as the owner. + // If the claim wins, this directory becomes the shared winner for the key. + const directory = SharedDirectory.create(this.runtime); + directory.set(ownerKey, this.claimant); + + const result = this.claimsOrThrow.trySetClaim(key, directory.handle as DirectoryHandle); + + // Connected claims come back "Pending"; await the op roundtrip for the real outcome. + const outcome = result.status === "Pending" ? await result.promise : result; + + if (outcome.status === "Accepted") { + // The "claimed" event resolves the winner, but resolve eagerly so the caller sees an + // accurate result even in detached mode (where no op — and so no event — is produced). + this.recordClaimedKey(key); + this.resolve(key); + return true; + } + + if (outcome.status === "AlreadyClaimed") { + // We lost the race: switch to the winner's directory instead of the one we created. + const winner = await outcome.currentValue?.get(); + if (winner !== undefined) { + this.adoptDirectory(key, winner); + } + } + + // Lost the race or aborted (e.g. disposed during the op). + return false; + }; + + /** + * Handles the Claims DDS "claimed" event (fired on every client when a claim is accepted) + * by switching to the winning directory for the key. + */ + private onClaimed(key: string): void { + this.resolve(key); + } + + /** + * Records a claimed key in the mirror subdirectory so it is discoverable on reload. Only + * the winning client calls this. Idempotent — a no-op if the key is already recorded. + */ + private recordClaimedKey(key: string): void { + const dir = this.claimedKeysDirOrThrow; + if (!dir.has(key)) { + dir.set(key, true); + } + } + + /** + * Resolves a key's winning handle to its backing directory and adopts it. + */ + private resolve(key: string): void { + const handle = this.claimsOrThrow.get(key); + if (handle === undefined) { + return; + } + handle + .get() + .then((directory) => this.adoptDirectory(key, directory)) + .catch((error: unknown) => console.error(`Failed to resolve claim "${key}"`, error)); + } + + /** + * Adopts a resolved directory as the local winner for a key (once) and watches it for + * changes so the view reflects owner updates. + */ + private adoptDirectory(key: string, directory: ISharedDirectory): void { + if (this.resolvedDirectories.get(key) === directory) { + return; // Already adopted this exact directory. + } + this.resolvedDirectories.set(key, directory); + directory.on("valueChanged", () => this.emit("claimsChanged")); + this.emit("claimsChanged"); + } +} + +/** + * The data object factory for {@link ClaimsDataObject}. Registers the Claims DDS factory; the + * SharedDirectory factory (used for the root and each claim's backing directory) is registered + * automatically by {@link DataObjectFactory}. + */ +export const ClaimsDataObjectFactory = new DataObjectFactory({ + type: "claims-data-object", + ctor: ClaimsDataObject, + sharedObjects: [ClaimsKind.getFactory()], +}); diff --git a/examples/apps/claims-example/src/container/claimsDataObject/index.ts b/examples/apps/claims-example/src/container/claimsDataObject/index.ts new file mode 100644 index 000000000000..8aab00732176 --- /dev/null +++ b/examples/apps/claims-example/src/container/claimsDataObject/index.ts @@ -0,0 +1,7 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export { ClaimsDataObject, ClaimsDataObjectFactory } from "./claimsDataObject.js"; +export type { IClaimsDataObject, IClaimsDataObjectEvents } from "./interface.js"; diff --git a/examples/apps/claims-example/src/container/claimsDataObject/interface.ts b/examples/apps/claims-example/src/container/claimsDataObject/interface.ts new file mode 100644 index 000000000000..52585c09dc01 --- /dev/null +++ b/examples/apps/claims-example/src/container/claimsDataObject/interface.ts @@ -0,0 +1,55 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { IEvent, IEventProvider } from "@fluidframework/core-interfaces"; + +/** + * Events emitted by an {@link IClaimsDataObject}. + */ +export interface IClaimsDataObjectEvents extends IEvent { + /** + * Emitted whenever the set of claimed keys (or an owner) changes, locally or remotely. + */ + (event: "claimsChanged", listener: () => void); +} + +/** + * IClaimsDataObject is the root data object for the example. It owns a single Claims DDS and + * abstracts that DDS's API behind a simple {@link IClaimsDataObject.trySetClaim} method, so the + * view never has to touch the Claims DDS directly. + * + * @remarks + * Each claimed key is paired with the `IFluidHandle` of a freshly created `SharedDirectory` + * (a real DDS) that records its owner. Because the claim value is a handle, every client + * resolves the *winning* directory to the same shared object — so even the client that loses + * a race ends up pointing at the winner's directory. + */ +export interface IClaimsDataObject extends IEventProvider { + /** + * The identity this client uses when it claims a key. Each client (browser tab) gets its + * own identity so first-writer-wins races between clients are easy to observe. + */ + readonly claimant: string; + + /** + * The keys that currently have an accepted claim, in claim order. + */ + readonly claimedKeys: readonly string[]; + + /** + * The identity of the client that owns a claimed key, or `undefined` if the key is + * unclaimed (or its winning directory hasn't finished resolving yet). + */ + getOwner(key: string): string | undefined; + + /** + * Attempts to claim a key using first-writer-wins semantics. + * + * @returns `true` if this client won the claim. If another client already owns the key the + * claim is rejected and `false` is returned; either way the data object switches to using + * the winning client's directory handle for the key. + */ + trySetClaim(key: string): Promise; +} diff --git a/examples/apps/claims-example/src/container/index.ts b/examples/apps/claims-example/src/container/index.ts new file mode 100644 index 000000000000..4c1ece112db3 --- /dev/null +++ b/examples/apps/claims-example/src/container/index.ts @@ -0,0 +1,7 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export type { IClaimsDataObject, IClaimsDataObjectEvents } from "./claimsDataObject/index.js"; +export { ClaimsExampleContainerRuntimeFactory } from "./runtimeFactory.js"; diff --git a/examples/apps/claims-example/src/container/runtimeFactory.ts b/examples/apps/claims-example/src/container/runtimeFactory.ts new file mode 100644 index 000000000000..355323885206 --- /dev/null +++ b/examples/apps/claims-example/src/container/runtimeFactory.ts @@ -0,0 +1,24 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { ContainerRuntimeFactoryWithDefaultDataStore } from "@fluidframework/aqueduct/legacy"; + +import { ClaimsDataObjectFactory } from "./claimsDataObject/index.js"; + +/** + * The container runtime factory for the claims example. It instantiates a single default data + * store ({@link ClaimsDataObject}) hosting the Claims DDS and exposes it as the container's + * entry point. + */ +export class ClaimsExampleContainerRuntimeFactory extends ContainerRuntimeFactoryWithDefaultDataStore { + public constructor() { + super({ + defaultFactory: ClaimsDataObjectFactory, + registryEntries: [ + [ClaimsDataObjectFactory.type, Promise.resolve(ClaimsDataObjectFactory)], + ], + }); + } +} diff --git a/examples/apps/claims-example/src/view.tsx b/examples/apps/claims-example/src/view.tsx new file mode 100644 index 000000000000..7882decc83d8 --- /dev/null +++ b/examples/apps/claims-example/src/view.tsx @@ -0,0 +1,106 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { type FC, useEffect, useState } from "react"; + +import type { IClaimsDataObject } from "./container/index.js"; + +export interface IClaimsViewProps { + claimsDataObject: IClaimsDataObject; +} + +export const ClaimsView: FC = ({ claimsDataObject }: IClaimsViewProps) => { + // Re-render whenever a claim is made or an owner changes, locally or remotely. + const [, forceUpdate] = useState({}); + const [keyToClaim, setKeyToClaim] = useState(""); + const [status, setStatus] = useState(""); + + useEffect(() => { + const onClaimsChanged = (): void => forceUpdate({}); + claimsDataObject.on("claimsChanged", onClaimsChanged); + return (): void => { + claimsDataObject.off("claimsChanged", onClaimsChanged); + }; + }, [claimsDataObject]); + + const handleClaim = (): void => { + const key = keyToClaim.trim(); + if (key.length === 0) { + return; + } + setKeyToClaim(""); + claimsDataObject + .trySetClaim(key) + .then((accepted) => { + // On a lost race the data object has already switched to the winner's handle; + // we just note that the key was already taken. + setStatus( + accepted + ? `You claimed "${key}".` + : `"${key}" was already claimed by another client.`, + ); + }) + .catch((error: unknown) => { + setStatus(`Claim failed: ${String(error)}`); + }); + }; + + return ( +
+

Claims

+

+ You are {claimsDataObject.claimant}. Type a key and claim it. Open + this page in another tab to watch two clients compete for the same key — the first to + claim it wins, and the loser is switched to the winner. +

+
+ setKeyToClaim(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + handleClaim(); + } + }} + style={{ width: "220px", marginRight: "8px" }} + /> + +
+ + + + + + + + + {claimsDataObject.claimedKeys.map((key) => { + const owner = claimsDataObject.getOwner(key); + const ownedByMe = owner === claimsDataObject.claimant; + return ( + + + + + ); + })} + +
Claimed keyOwner
{key} + {owner ?? "— resolving… —"} + {ownedByMe ? " (you)" : ""} +
+

{status}

+
+ ); +}; diff --git a/examples/apps/claims-example/tests/claimsExample.test.ts b/examples/apps/claims-example/tests/claimsExample.test.ts new file mode 100644 index 000000000000..b334058198f2 --- /dev/null +++ b/examples/apps/claims-example/tests/claimsExample.test.ts @@ -0,0 +1,69 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import puppeteer, { Browser, Page } from "puppeteer"; +import { globals } from "../jest.config.cjs"; +import type { IClaimsDataObject } from "../src/container/index.js"; + +describe("claims-example", () => { + let browser: Browser; + let page: Page; + + beforeAll(async () => { + // Launch the browser once for all tests + // Use chrome-headless-shell since some tests don't work as-is with the new headless mode. + // AB#7150: Remove this once we have fixed the tests. + browser = await puppeteer.launch({ headless: "shell" }); + // Load the page once to avoid a cold load on first test - otherwise the first test takes + // significantly longer to load. This way we can extend just the timeout for the cold load. + page = await browser.newPage(); + await page.goto(globals.PATH); + await page.waitForFunction(() => typeof globalThis.loadAdditionalContainer === "function"); + await page.close(); + }, 20_000); + + beforeEach(async () => { + page = await browser.newPage(); + await page.goto(globals.PATH); + await page.waitForFunction(() => typeof globalThis.loadAdditionalContainer === "function"); + }); + + it("The page loads and there is a Claim button", async () => { + // Validate there is a button that can be clicked + await expect(page).toClick("button", { text: "Claim" }); + }); + + it("propagates a claim and resolves the handle in other connected containers", async () => { + const key = "shared-key"; + const ownerP = page.evaluate(async (claimKey: string) => { + // Load an additional container, and use it to watch for the expected claim + const container = await globalThis.loadAdditionalContainer(); + const claimsDataObject = (await container.getEntryPoint()) as IClaimsDataObject; + return new Promise((resolve) => { + claimsDataObject.on("claimsChanged", () => { + // The other client resolves the claimed handle to the same backing + // directory and reads the owner recorded on it. + if (claimsDataObject.claimedKeys.includes(claimKey)) { + resolve(claimsDataObject.getOwner(claimKey)); + } + }); + }); + }, key); + + // Type a key and claim it from the main container. + await expect(page).toFill("input", key); + await expect(page).toClick("button", { text: "Claim" }); + const owner = await ownerP; + expect(typeof owner).toBe("string"); + }); + + afterEach(async () => { + await page.close(); + }); + + afterAll(async () => { + await browser.close(); + }); +}); diff --git a/examples/apps/claims-example/tsconfig.json b/examples/apps/claims-example/tsconfig.json new file mode 100644 index 000000000000..2752efca348a --- /dev/null +++ b/examples/apps/claims-example/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../common/build/build-common/tsconfig.node16.json", + "compilerOptions": { + "outDir": "./lib", + "types": ["react", "jest", "puppeteer", "jest-environment-puppeteer", "expect-puppeteer"], + "exactOptionalPropertyTypes": false, + }, + "include": ["src/**/*", "tests/**/*"], +} diff --git a/examples/apps/claims-example/webpack.config.cjs b/examples/apps/claims-example/webpack.config.cjs new file mode 100644 index 000000000000..e2bff6d013a3 --- /dev/null +++ b/examples/apps/claims-example/webpack.config.cjs @@ -0,0 +1,64 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +const { + createExampleDriverServiceWebpackPlugin, + createOdspMiddlewares, +} = require("@fluid-example/example-webpack-integration"); +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const webpack = require("webpack"); + +module.exports = (env) => { + const { production, service } = env; + + return { + entry: { + app: "./src/app.ts", + }, + resolve: { + extensionAlias: { + ".js": [".ts", ".tsx", ".js", ".cjs", ".mjs"], + }, + 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/claims-example", + libraryTarget: "umd", + }, + plugins: [ + new webpack.ProvidePlugin({ + process: "process/browser.js", + }), + new HtmlWebpackPlugin({ + title: "claims-example", + }), + createExampleDriverServiceWebpackPlugin(service), + ], + devServer: { + setupMiddlewares: (middlewares) => { + if (service === "odsp") { + middlewares.push(...createOdspMiddlewares()); + } + return middlewares; + }, + }, + mode: production ? "production" : "development", + devtool: production ? "source-map" : "inline-source-map", + }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ab7064f86ff..7a3efbdbd685 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -393,6 +393,127 @@ importers: specifier: ~4.15.2 version: 4.15.2(webpack-cli@5.1.4)(webpack@5.103.0) + examples/apps/claims-example: + dependencies: + '@fluid-example/example-driver': + specifier: workspace:~ + version: link:../../utils/example-driver + '@fluid-internal/claims': + specifier: workspace:~ + version: link:../../../packages/dds/claims + '@fluidframework/aqueduct': + specifier: workspace:~ + version: link:../../../packages/framework/aqueduct + '@fluidframework/container-definitions': + specifier: workspace:~ + version: link:../../../packages/common/container-definitions + '@fluidframework/container-loader': + specifier: workspace:~ + version: link:../../../packages/loader/container-loader + '@fluidframework/core-interfaces': + specifier: workspace:~ + version: link:../../../packages/common/core-interfaces + '@fluidframework/map': + specifier: workspace:~ + version: link:../../../packages/dds/map + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@biomejs/biome': + specifier: ~2.4.5 + version: 2.4.5 + '@fluid-example/example-webpack-integration': + specifier: workspace:~ + version: link:../../utils/example-webpack-integration + '@fluid-tools/build-cli': + specifier: catalog:buildTools + version: 0.65.0(@types/node@22.19.17)(webpack-cli@5.1.4) + '@fluidframework/build-common': + specifier: ^2.0.3 + version: 2.0.3 + '@fluidframework/build-tools': + specifier: catalog:buildTools + version: 0.65.0(@types/node@22.19.17) + '@fluidframework/eslint-config-fluid': + specifier: catalog:eslint + version: 13.0.0(@typescript-eslint/utils@8.61.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.4.5))(eslint@9.39.1(jiti@2.6.1))(typescript@5.4.5) + '@fluidframework/test-tools': + specifier: ^2.0.0 + version: 2.0.1 + '@types/jest': + specifier: 29.5.3 + version: 29.5.3 + '@types/jest-environment-puppeteer': + specifier: workspace:~ + version: link:../../../packages/test/types_jest-environment-puppeteer + '@types/node': + specifier: ~22.19.17 + version: 22.19.17 + '@types/react': + specifier: ^18.3.11 + version: 18.3.15 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.3(@types/react@18.3.15) + cross-env: + specifier: ^10.1.0 + version: 10.1.0 + eslint: + specifier: catalog:eslint + version: 9.39.1(jiti@2.6.1) + expect-puppeteer: + specifier: ^9.0.2 + version: 9.0.2 + html-webpack-plugin: + specifier: ^5.6.0 + version: 5.6.3(webpack@5.103.0) + jest: + specifier: ^29.6.2 + version: 29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.4.5)) + jest-environment-puppeteer: + specifier: ^10.1.3 + version: 10.1.4(typescript@5.4.5) + jest-junit: + specifier: ^16.0.0 + version: 16.0.0 + jest-puppeteer: + specifier: ^10.1.3 + version: 10.1.4(puppeteer@23.10.3(typescript@5.4.5))(typescript@5.4.5) + jiti: + specifier: ^2.6.1 + version: 2.6.1 + puppeteer: + specifier: ^23.6.0 + version: 23.10.3(typescript@5.4.5) + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + tinylicious: + specifier: ^7.0.1 + version: 7.0.1 + ts-jest: + specifier: ^29.1.1 + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.4.5)))(typescript@5.4.5) + ts-loader: + specifier: ^9.5.1 + version: 9.5.1(typescript@5.4.5)(webpack@5.103.0) + typescript: + specifier: ~5.4.5 + version: 5.4.5 + webpack: + specifier: ^5.94.0 + version: 5.103.0(webpack-cli@5.1.4) + webpack-cli: + specifier: ^5.1.4 + version: 5.1.4(webpack-dev-server@4.15.2)(webpack@5.103.0) + webpack-dev-server: + specifier: ~4.15.2 + version: 4.15.2(webpack-cli@5.1.4)(webpack@5.103.0) + examples/apps/collaborative-textarea: dependencies: '@fluid-example/example-utils': @@ -4408,7 +4529,7 @@ importers: version: 2.1.0 axios: specifier: 1.15.1 - version: 1.15.1(debug@4.4.3) + version: 1.15.1(debug@4.3.7) events_pkg: specifier: npm:events@^3.1.0 version: events@3.3.0 @@ -4559,7 +4680,7 @@ importers: version: 2.1.0 axios: specifier: 1.15.1 - version: 1.15.1(debug@4.4.3) + version: 1.15.1(debug@4.3.7) events_pkg: specifier: npm:events@^3.1.0 version: events@3.3.0 @@ -5374,7 +5495,7 @@ importers: version: link:../../../packages/utils/tool-utils axios: specifier: 1.15.1 - version: 1.15.1(debug@4.4.3) + version: 1.15.1(debug@4.3.7) buffer: specifier: ^6.0.3 version: 6.0.3 @@ -6725,7 +6846,7 @@ importers: version: link:../../../../packages/dds/shared-object-base axios: specifier: 1.15.1 - version: 1.15.1(debug@4.4.3) + version: 1.15.1(debug@4.3.7) buffer: specifier: ^6.0.3 version: 6.0.3 @@ -30411,7 +30532,7 @@ snapshots: '@fluidframework/routerlicious-driver': 1.4.0 '@fluidframework/runtime-utils': 1.4.0 '@fluidframework/server-services-client': 0.1036.5002 - axios: 0.30.3(debug@4.4.3) + axios: 0.30.3 uuid: 8.3.2 transitivePeerDependencies: - bufferutil @@ -30826,7 +30947,7 @@ snapshots: '@fluidframework/protocol-base': 0.1036.5002 '@fluidframework/protocol-definitions': 0.1028.2000 '@fluidframework/telemetry-utils': 1.4.0 - axios: 0.30.3(debug@4.4.3) + axios: 0.30.3 uuid: 8.3.2 transitivePeerDependencies: - debug @@ -31328,6 +31449,35 @@ snapshots: transitivePeerDependencies: - supports-color + '@fluidframework/server-lambdas@7.0.1': + dependencies: + '@fluidframework/common-utils': 3.1.0 + '@fluidframework/gitresources': 7.0.1 + '@fluidframework/protocol-base': 7.0.1 + '@fluidframework/protocol-definitions': 3.2.0 + '@fluidframework/server-lambdas-driver': 7.0.1 + '@fluidframework/server-services-client': 7.0.1 + '@fluidframework/server-services-core': 7.0.1 + '@fluidframework/server-services-telemetry': 7.0.1 + '@types/semver': 7.7.0 + assert: 2.1.0 + async: 3.2.6 + axios: 1.15.1(debug@4.3.7) + buffer: 6.0.3 + double-ended-queue: 2.1.0-0 + events: 3.3.0 + json-stringify-safe: 5.0.1 + lodash: 4.18.1 + nconf: 0.12.1 + opossum: 8.5.0 + semver: 7.7.4 + serialize-error: 8.1.0 + sha.js: 2.4.12 + uuid: 11.1.0 + transitivePeerDependencies: + - debug + - supports-color + '@fluidframework/server-lambdas@7.0.1(debug@4.4.3)': dependencies: '@fluidframework/common-utils': 3.1.0 @@ -34052,7 +34202,7 @@ snapshots: '@vvago/vale@3.12.0': dependencies: - axios: 1.15.1(debug@4.4.3) + axios: 1.15.1(debug@4.3.7) rimraf: 5.0.10 tar: 7.5.11 unzipper: 0.10.14 @@ -34464,6 +34614,14 @@ snapshots: axe-core@4.10.2: {} + axios@0.30.3: + dependencies: + follow-redirects: 1.16.0(debug@4.3.7) + form-data: 4.0.6 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@0.30.3(debug@4.4.3): dependencies: follow-redirects: 1.16.0(debug@4.4.3) @@ -37218,6 +37376,18 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy-middleware@2.0.9(@types/express@4.17.21): + dependencies: + '@types/http-proxy': 1.17.15 + http-proxy: 1.18.1 + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.8 + optionalDependencies: + '@types/express': 4.17.21 + transitivePeerDependencies: + - debug + http-proxy-middleware@2.0.9(@types/express@4.17.21)(debug@4.4.3): dependencies: '@types/http-proxy': 1.17.15 @@ -37230,6 +37400,14 @@ snapshots: transitivePeerDependencies: - debug + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.16.0(debug@4.3.7) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + http-proxy@1.18.1(debug@4.4.3): dependencies: eventemitter3: 4.0.7 @@ -37837,7 +38015,7 @@ snapshots: prompts: 2.4.2 spawnd: 10.1.4 tree-kill: 1.2.2 - wait-on: 8.0.1 + wait-on: 8.0.1(debug@4.3.7) transitivePeerDependencies: - debug - supports-color @@ -41803,7 +41981,7 @@ snapshots: '@fluidframework/gitresources': 7.0.1 '@fluidframework/protocol-base': 7.0.1 '@fluidframework/protocol-definitions': 3.2.0 - '@fluidframework/server-lambdas': 7.0.1(debug@4.4.3) + '@fluidframework/server-lambdas': 7.0.1 '@fluidframework/server-local-server': 7.0.1 '@fluidframework/server-memory-orderer': 7.0.1 '@fluidframework/server-services-client': 7.0.1 @@ -41813,7 +41991,7 @@ snapshots: '@fluidframework/server-services-utils': 7.0.1 '@fluidframework/server-test-utils': 7.0.1 agentkeepalive: 4.6.0 - axios: 1.15.1(debug@4.4.3) + axios: 1.15.1(debug@4.3.7) body-parser: 1.20.3 charwise: 3.0.1 compression: 1.7.5 @@ -41947,7 +42125,7 @@ snapshots: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.3 + semver: 7.7.4 typescript: 5.4.5 yargs-parser: 21.1.1 optionalDependencies: @@ -41961,7 +42139,7 @@ snapshots: chalk: 4.1.2 enhanced-resolve: 5.18.3 micromatch: 4.0.8 - semver: 7.7.3 + semver: 7.7.4 source-map: 0.7.4 typescript: 5.4.5 webpack: 5.103.0(webpack-cli@5.1.4) @@ -42457,16 +42635,6 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wait-on@8.0.1: - dependencies: - axios: 1.15.1(debug@4.4.3) - joi: 17.13.3 - lodash: 4.18.1 - minimist: 1.2.8 - rxjs: 7.8.2 - transitivePeerDependencies: - - debug - wait-on@8.0.1(debug@4.3.7): dependencies: axios: 1.15.1(debug@4.3.7) @@ -42652,7 +42820,7 @@ snapshots: express: 4.22.1 graceful-fs: 4.2.11 html-entities: 2.6.0 - http-proxy-middleware: 2.0.9(@types/express@4.17.21)(debug@4.4.3) + http-proxy-middleware: 2.0.9(@types/express@4.17.21) ipaddr.js: 2.2.0 launch-editor: 2.9.1 open: 8.4.2