@fluid-experimental/oldest-client-observer 2.0.0-dev-rc.1.0.0.224419

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.eslintrc.js +11 -0
  2. package/CHANGELOG.md +144 -0
  3. package/LICENSE +21 -0
  4. package/README.md +39 -0
  5. package/api-extractor-lint.json +4 -0
  6. package/api-extractor.json +4 -0
  7. package/api-report/oldest-client-observer.api.md +54 -0
  8. package/dist/index.cjs +10 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/interfaces.cjs +7 -0
  13. package/dist/interfaces.cjs.map +1 -0
  14. package/dist/interfaces.d.ts +42 -0
  15. package/dist/interfaces.d.ts.map +1 -0
  16. package/dist/oldest-client-observer-alpha.d.ts +111 -0
  17. package/dist/oldest-client-observer-beta.d.ts +17 -0
  18. package/dist/oldest-client-observer-public.d.ts +17 -0
  19. package/dist/oldest-client-observer-untrimmed.d.ts +111 -0
  20. package/dist/oldestClientObserver.cjs +123 -0
  21. package/dist/oldestClientObserver.cjs.map +1 -0
  22. package/dist/oldestClientObserver.d.ts +72 -0
  23. package/dist/oldestClientObserver.d.ts.map +1 -0
  24. package/dist/tsdoc-metadata.json +11 -0
  25. package/lib/index.d.mts +7 -0
  26. package/lib/index.d.mts.map +1 -0
  27. package/lib/index.mjs +6 -0
  28. package/lib/index.mjs.map +1 -0
  29. package/lib/interfaces.d.mts +42 -0
  30. package/lib/interfaces.d.mts.map +1 -0
  31. package/lib/interfaces.mjs +6 -0
  32. package/lib/interfaces.mjs.map +1 -0
  33. package/lib/oldest-client-observer-alpha.d.mts +111 -0
  34. package/lib/oldest-client-observer-beta.d.mts +17 -0
  35. package/lib/oldest-client-observer-public.d.mts +17 -0
  36. package/lib/oldest-client-observer-untrimmed.d.mts +111 -0
  37. package/lib/oldestClientObserver.d.mts +72 -0
  38. package/lib/oldestClientObserver.d.mts.map +1 -0
  39. package/lib/oldestClientObserver.mjs +119 -0
  40. package/lib/oldestClientObserver.mjs.map +1 -0
  41. package/package.json +120 -0
  42. package/prettier.config.cjs +8 -0
  43. package/src/index.ts +12 -0
  44. package/src/interfaces.ts +51 -0
  45. package/src/oldestClientObserver.ts +138 -0
  46. package/tsconfig.json +12 -0
@@ -0,0 +1,119 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import { TypedEventEmitter } from "@fluid-internal/client-utils";
6
+ import { assert } from "@fluidframework/core-utils";
7
+ import { AttachState } from "@fluidframework/container-definitions";
8
+ /**
9
+ * The `OldestClientObserver` is a utility inspect if the local client is the oldest amongst connected clients (in
10
+ * terms of when they connected) and watch for changes.
11
+ *
12
+ * It is still experimental and under development. Please do try it out, but expect breaking changes in the future.
13
+ *
14
+ * @remarks
15
+ * ### Creation
16
+ *
17
+ * The `OldestClientObserver` constructor takes an `IOldestClientObservable`. This is most easily satisfied with
18
+ * either an `IContainerRuntime` or an `IFluidDataStoreRuntime`:
19
+ *
20
+ * ```typescript
21
+ * // E.g. from within a BaseContainerRuntimeFactory:
22
+ * protected async containerHasInitialized(runtime: IContainerRuntime) {
23
+ * const oldestClientObserver = new OldestClientObserver(runtime);
24
+ * // ...
25
+ * }
26
+ * ```
27
+ *
28
+ * ```typescript
29
+ * // From within a DataObject
30
+ * protected async hasInitialized() {
31
+ * const oldestClientObserver = new OldestClientObserver(this.runtime);
32
+ * // ...
33
+ * }
34
+ * ```
35
+ *
36
+ * ### Usage
37
+ *
38
+ * To check if the local client is the oldest, use the `isOldest()` method.
39
+ *
40
+ * ```typescript
41
+ * if (oldestClientObserver.isOldest()) {
42
+ * console.log("I'm the oldest");
43
+ * } else {
44
+ * console.log("Someone else is older");
45
+ * }
46
+ * ```
47
+ *
48
+ * ### Eventing
49
+ *
50
+ * `OldestClientObserver` is an `EventEmitter`, and will emit events when the local client becomes the oldest and when
51
+ * it is no longer the oldest.
52
+ *
53
+ * ```typescript
54
+ * oldestClientObserver.on("becameOldest", () => {
55
+ * console.log("I'm the oldest now");
56
+ * });
57
+ *
58
+ * oldestClientObserver.on("lostOldest", () => {
59
+ * console.log("I'm not the oldest anymore");
60
+ * });
61
+ * ```
62
+ * @alpha
63
+ */
64
+ export class OldestClientObserver extends TypedEventEmitter {
65
+ constructor(observable) {
66
+ super();
67
+ this.observable = observable;
68
+ this.currentIsOldest = false;
69
+ this.updateOldest = () => {
70
+ const oldest = this.computeIsOldest();
71
+ if (this.currentIsOldest !== oldest) {
72
+ this.currentIsOldest = oldest;
73
+ if (oldest) {
74
+ this.emit("becameOldest");
75
+ }
76
+ else {
77
+ this.emit("lostOldest");
78
+ }
79
+ }
80
+ };
81
+ this.quorum = this.observable.getQuorum();
82
+ this.currentIsOldest = this.computeIsOldest();
83
+ this.quorum.on("addMember", this.updateOldest);
84
+ this.quorum.on("removeMember", this.updateOldest);
85
+ observable.on("connected", this.updateOldest);
86
+ observable.on("disconnected", this.updateOldest);
87
+ }
88
+ isOldest() {
89
+ return this.currentIsOldest;
90
+ }
91
+ computeIsOldest() {
92
+ // If the container is detached, we are the only ones that know about it and are the oldest by default.
93
+ if (this.observable.attachState === AttachState.Detached) {
94
+ return true;
95
+ }
96
+ // If we're not connected we can't be the oldest connected client.
97
+ if (!this.observable.connected) {
98
+ return false;
99
+ }
100
+ // TODO: Clean up error code linter violations repo-wide.
101
+ assert(this.observable.clientId !== undefined,
102
+ // eslint-disable-next-line unicorn/numeric-separators-style
103
+ 0x1da /* "Client id should be set if connected" */);
104
+ const selfSequencedClient = this.quorum.getMember(this.observable.clientId);
105
+ // When in readonly mode our clientId will not be present in the quorum.
106
+ if (selfSequencedClient === undefined) {
107
+ return false;
108
+ }
109
+ const members = this.quorum.getMembers();
110
+ for (const sequencedClient of members.values()) {
111
+ if (sequencedClient.sequenceNumber < selfSequencedClient.sequenceNumber) {
112
+ return false;
113
+ }
114
+ }
115
+ // No member of the quorum was older
116
+ return true;
117
+ }
118
+ }
119
+ //# sourceMappingURL=oldestClientObserver.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oldestClientObserver.mjs","sourceRoot":"","sources":["../src/oldestClientObserver.ts"],"names":[],"mappings":"AAAA;;;GAGG;OAEI,EAAE,iBAAiB,EAAE,MAAM,8BAA8B;OACzD,EAAE,MAAM,EAAE,MAAM,4BAA4B;OAC5C,EAAE,WAAW,EAAE,MAAM,uCAAuC;AAQnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,MAAM,OAAO,oBACZ,SAAQ,iBAA8C;IAKtD,YAA6B,UAAmC;QAC/D,KAAK,EAAE,CAAC;QADoB,eAAU,GAAV,UAAU,CAAyB;QADxD,oBAAe,GAAY,KAAK,CAAC;QAexB,iBAAY,GAAG,GAAS,EAAE;YAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,eAAe,KAAK,MAAM,EAAE;gBACpC,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC;gBAC9B,IAAI,MAAM,EAAE;oBACX,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;iBAC1B;qBAAM;oBACN,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;iBACxB;aACD;QACF,CAAC,CAAC;QAtBD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC;QAC1C,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QAC9C,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,cAAc,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAClD,UAAU,CAAC,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC9C,UAAU,CAAC,EAAE,CAAC,cAAc,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IAClD,CAAC;IAEM,QAAQ;QACd,OAAO,IAAI,CAAC,eAAe,CAAC;IAC7B,CAAC;IAcO,eAAe;QACtB,uGAAuG;QACvG,IAAI,IAAI,CAAC,UAAU,CAAC,WAAW,KAAK,WAAW,CAAC,QAAQ,EAAE;YACzD,OAAO,IAAI,CAAC;SACZ;QAED,kEAAkE;QAClE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE;YAC/B,OAAO,KAAK,CAAC;SACb;QAED,yDAAyD;QACzD,MAAM,CACL,IAAI,CAAC,UAAU,CAAC,QAAQ,KAAK,SAAS;QACtC,4DAA4D;QAC5D,KAAK,CAAC,4CAA4C,CAClD,CAAC;QAEF,MAAM,mBAAmB,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC5E,wEAAwE;QACxE,IAAI,mBAAmB,KAAK,SAAS,EAAE;YACtC,OAAO,KAAK,CAAC;SACb;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACzC,KAAK,MAAM,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE;YAC/C,IAAI,eAAe,CAAC,cAAc,GAAG,mBAAmB,CAAC,cAAc,EAAE;gBACxE,OAAO,KAAK,CAAC;aACb;SACD;QAED,oCAAoC;QACpC,OAAO,IAAI,CAAC;IACb,CAAC;CACD","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { TypedEventEmitter } from \"@fluid-internal/client-utils\";\nimport { assert } from \"@fluidframework/core-utils\";\nimport { AttachState } from \"@fluidframework/container-definitions\";\nimport { IQuorumClients } from \"@fluidframework/protocol-definitions\";\nimport {\n\tIOldestClientObservable,\n\tIOldestClientObserverEvents,\n\tIOldestClientObserver,\n} from \"./interfaces\";\n\n/**\n * The `OldestClientObserver` is a utility inspect if the local client is the oldest amongst connected clients (in\n * terms of when they connected) and watch for changes.\n *\n * It is still experimental and under development. Please do try it out, but expect breaking changes in the future.\n *\n * @remarks\n * ### Creation\n *\n * The `OldestClientObserver` constructor takes an `IOldestClientObservable`. This is most easily satisfied with\n * either an `IContainerRuntime` or an `IFluidDataStoreRuntime`:\n *\n * ```typescript\n * // E.g. from within a BaseContainerRuntimeFactory:\n * protected async containerHasInitialized(runtime: IContainerRuntime) {\n * const oldestClientObserver = new OldestClientObserver(runtime);\n * // ...\n * }\n * ```\n *\n * ```typescript\n * // From within a DataObject\n * protected async hasInitialized() {\n * const oldestClientObserver = new OldestClientObserver(this.runtime);\n * // ...\n * }\n * ```\n *\n * ### Usage\n *\n * To check if the local client is the oldest, use the `isOldest()` method.\n *\n * ```typescript\n * if (oldestClientObserver.isOldest()) {\n * console.log(\"I'm the oldest\");\n * } else {\n * console.log(\"Someone else is older\");\n * }\n * ```\n *\n * ### Eventing\n *\n * `OldestClientObserver` is an `EventEmitter`, and will emit events when the local client becomes the oldest and when\n * it is no longer the oldest.\n *\n * ```typescript\n * oldestClientObserver.on(\"becameOldest\", () => {\n * console.log(\"I'm the oldest now\");\n * });\n *\n * oldestClientObserver.on(\"lostOldest\", () => {\n * console.log(\"I'm not the oldest anymore\");\n * });\n * ```\n * @alpha\n */\nexport class OldestClientObserver\n\textends TypedEventEmitter<IOldestClientObserverEvents>\n\timplements IOldestClientObserver\n{\n\tprivate readonly quorum: IQuorumClients;\n\tprivate currentIsOldest: boolean = false;\n\tconstructor(private readonly observable: IOldestClientObservable) {\n\t\tsuper();\n\t\tthis.quorum = this.observable.getQuorum();\n\t\tthis.currentIsOldest = this.computeIsOldest();\n\t\tthis.quorum.on(\"addMember\", this.updateOldest);\n\t\tthis.quorum.on(\"removeMember\", this.updateOldest);\n\t\tobservable.on(\"connected\", this.updateOldest);\n\t\tobservable.on(\"disconnected\", this.updateOldest);\n\t}\n\n\tpublic isOldest(): boolean {\n\t\treturn this.currentIsOldest;\n\t}\n\n\tprivate readonly updateOldest = (): void => {\n\t\tconst oldest = this.computeIsOldest();\n\t\tif (this.currentIsOldest !== oldest) {\n\t\t\tthis.currentIsOldest = oldest;\n\t\t\tif (oldest) {\n\t\t\t\tthis.emit(\"becameOldest\");\n\t\t\t} else {\n\t\t\t\tthis.emit(\"lostOldest\");\n\t\t\t}\n\t\t}\n\t};\n\n\tprivate computeIsOldest(): boolean {\n\t\t// If the container is detached, we are the only ones that know about it and are the oldest by default.\n\t\tif (this.observable.attachState === AttachState.Detached) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// If we're not connected we can't be the oldest connected client.\n\t\tif (!this.observable.connected) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// TODO: Clean up error code linter violations repo-wide.\n\t\tassert(\n\t\t\tthis.observable.clientId !== undefined,\n\t\t\t// eslint-disable-next-line unicorn/numeric-separators-style\n\t\t\t0x1da /* \"Client id should be set if connected\" */,\n\t\t);\n\n\t\tconst selfSequencedClient = this.quorum.getMember(this.observable.clientId);\n\t\t// When in readonly mode our clientId will not be present in the quorum.\n\t\tif (selfSequencedClient === undefined) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst members = this.quorum.getMembers();\n\t\tfor (const sequencedClient of members.values()) {\n\t\t\tif (sequencedClient.sequenceNumber < selfSequencedClient.sequenceNumber) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// No member of the quorum was older\n\t\treturn true;\n\t}\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@fluid-experimental/oldest-client-observer",
3
+ "version": "2.0.0-dev-rc.1.0.0.224419",
4
+ "description": "Data object to determine if the local client is the oldest amongst connected clients",
5
+ "homepage": "https://fluidframework.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/microsoft/FluidFramework.git",
9
+ "directory": "packages/framework/oldest-client-observer"
10
+ },
11
+ "license": "MIT",
12
+ "author": "Microsoft and contributors",
13
+ "sideEffects": false,
14
+ "exports": {
15
+ ".": {
16
+ "import": {
17
+ "types": "./lib/index.d.mts",
18
+ "default": "./lib/index.mjs"
19
+ },
20
+ "require": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.cjs"
23
+ }
24
+ }
25
+ },
26
+ "main": "dist/index.cjs",
27
+ "module": "lib/index.mjs",
28
+ "types": "dist/index.d.ts",
29
+ "c8": {
30
+ "all": true,
31
+ "cache-dir": "nyc/.cache",
32
+ "exclude": [
33
+ "src/test/**/*.*ts",
34
+ "dist/test/**/*.*js"
35
+ ],
36
+ "exclude-after-remap": false,
37
+ "include": [
38
+ "src/**/*.*ts",
39
+ "dist/**/*.*js"
40
+ ],
41
+ "report-dir": "nyc/report",
42
+ "reporter": [
43
+ "cobertura",
44
+ "html",
45
+ "text"
46
+ ],
47
+ "temp-directory": "nyc/.nyc_output"
48
+ },
49
+ "dependencies": {
50
+ "@fluid-internal/client-utils": "2.0.0-dev-rc.1.0.0.224419",
51
+ "@fluidframework/container-definitions": "2.0.0-dev-rc.1.0.0.224419",
52
+ "@fluidframework/core-interfaces": "2.0.0-dev-rc.1.0.0.224419",
53
+ "@fluidframework/core-utils": "2.0.0-dev-rc.1.0.0.224419",
54
+ "@fluidframework/protocol-definitions": "^3.1.0-223007"
55
+ },
56
+ "devDependencies": {
57
+ "@arethetypeswrong/cli": "^0.13.3",
58
+ "@fluid-private/test-dds-utils": "2.0.0-dev-rc.1.0.0.224419",
59
+ "@fluid-tools/build-cli": "0.29.0-222379",
60
+ "@fluidframework/build-common": "^2.0.3",
61
+ "@fluidframework/build-tools": "0.29.0-222379",
62
+ "@fluidframework/eslint-config-fluid": "^3.1.0",
63
+ "@fluidframework/test-runtime-utils": "2.0.0-dev-rc.1.0.0.224419",
64
+ "@microsoft/api-extractor": "^7.38.3",
65
+ "@types/node": "^18.19.0",
66
+ "copyfiles": "^2.4.1",
67
+ "cross-env": "^7.0.3",
68
+ "eslint": "~8.50.0",
69
+ "prettier": "~3.0.3",
70
+ "renamer": "^4.0.0",
71
+ "rimraf": "^4.4.0",
72
+ "tsc-multi": "^1.1.0",
73
+ "typescript": "~5.1.6"
74
+ },
75
+ "fluidBuild": {
76
+ "tasks": {
77
+ "build:docs": {
78
+ "dependsOn": [
79
+ "...",
80
+ "api-extractor:commonjs",
81
+ "api-extractor:esnext"
82
+ ],
83
+ "script": false
84
+ },
85
+ "tsc": [
86
+ "...",
87
+ "typetests:gen"
88
+ ]
89
+ }
90
+ },
91
+ "typeValidation": {
92
+ "disabled": true,
93
+ "broken": {}
94
+ },
95
+ "scripts": {
96
+ "api": "fluid-build . --task api",
97
+ "api-extractor:commonjs": "api-extractor run --local",
98
+ "api-extractor:esnext": "copyfiles -u 1 \"dist/**/*-@(alpha|beta|public|untrimmed).d.ts\" lib",
99
+ "build": "fluid-build . --task build",
100
+ "build:commonjs": "fluid-build . --task commonjs",
101
+ "build:compile": "fluid-build . --task compile",
102
+ "build:docs": "fluid-build . --task api",
103
+ "build:esnext": "tsc-multi --config ../../../common/build/build-common/tsc-multi.esm.json",
104
+ "build:rename-types": "renamer \"lib/**\" -f .d.ts -r .d.mts --force",
105
+ "check:are-the-types-wrong": "attw --pack",
106
+ "check:release-tags": "api-extractor run --local --config ./api-extractor-lint.json",
107
+ "ci:build:docs": "api-extractor run",
108
+ "clean": "rimraf --glob dist lib \"**/*.tsbuildinfo\" \"**/*.build.log\" _api-extractor-temp",
109
+ "eslint": "eslint --format stylish src",
110
+ "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout",
111
+ "format": "npm run prettier:fix",
112
+ "lint": "npm run prettier && npm run check:release-tags && npm run eslint",
113
+ "lint:fix": "npm run prettier:fix && npm run eslint:fix",
114
+ "prettier": "prettier --check . --cache --ignore-path ../../../.prettierignore",
115
+ "prettier:fix": "prettier --write . --cache --ignore-path ../../../.prettierignore",
116
+ "tsc": "tsc-multi --config ../../../common/build/build-common/tsc-multi.cjs.json",
117
+ "typetests:gen": "fluid-type-test-generator",
118
+ "typetests:prepare": "flub typetests --dir . --reset --previous --normalize"
119
+ }
120
+ }
@@ -0,0 +1,8 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ module.exports = {
7
+ ...require("@fluidframework/build-common/prettier.config.cjs"),
8
+ };
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ export {
7
+ IOldestClientObservable,
8
+ IOldestClientObservableEvents,
9
+ IOldestClientObserver,
10
+ IOldestClientObserverEvents,
11
+ } from "./interfaces";
12
+ export { OldestClientObserver } from "./oldestClientObserver";
@@ -0,0 +1,51 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { IEvent, IEventProvider } from "@fluidframework/core-interfaces";
7
+ import { AttachState } from "@fluidframework/container-definitions";
8
+ import { IQuorumClients } from "@fluidframework/protocol-definitions";
9
+
10
+ /**
11
+ * Events emitted by {@link IOldestClientObservable}.
12
+ * @alpha
13
+ */
14
+ export interface IOldestClientObservableEvents extends IEvent {
15
+ (event: "connected", listener: () => void);
16
+ (event: "disconnected", listener: () => void);
17
+ }
18
+
19
+ /**
20
+ * This is to make OldestClientObserver work with either a ContainerRuntime or an IFluidDataStoreRuntime
21
+ * (both expose the relevant API surface and eventing). However, really this info probably shouldn't live on either,
22
+ * since neither is really the source of truth (they are just the only currently-available plumbing options).
23
+ * It's information about the connection, so the real source of truth is lower (at the connection layer).
24
+ * @alpha
25
+ */
26
+ export interface IOldestClientObservable extends IEventProvider<IOldestClientObservableEvents> {
27
+ getQuorum(): IQuorumClients;
28
+ // Generic usage of attachState is a little unusual here. We will treat ourselves as "the oldest client that
29
+ // has information about this [container | data store]", which in the case of detached data store may disagree
30
+ // with whether we're the oldest client on the connected container. So in the data store case, it's only
31
+ // safe use this as an indicator about rights to tasks performed against this specific data store, and not
32
+ // more broadly.
33
+ attachState: AttachState;
34
+ connected: boolean;
35
+ clientId: string | undefined;
36
+ }
37
+
38
+ /**
39
+ * Events emitted by {@link IOldestClientObservable}.
40
+ * @alpha
41
+ */
42
+ export interface IOldestClientObserverEvents extends IEvent {
43
+ (event: "becameOldest" | "lostOldest", listener: () => void);
44
+ }
45
+
46
+ /**
47
+ * @alpha
48
+ */
49
+ export interface IOldestClientObserver extends IEventProvider<IOldestClientObserverEvents> {
50
+ isOldest(): boolean;
51
+ }
@@ -0,0 +1,138 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { TypedEventEmitter } from "@fluid-internal/client-utils";
7
+ import { assert } from "@fluidframework/core-utils";
8
+ import { AttachState } from "@fluidframework/container-definitions";
9
+ import { IQuorumClients } from "@fluidframework/protocol-definitions";
10
+ import {
11
+ IOldestClientObservable,
12
+ IOldestClientObserverEvents,
13
+ IOldestClientObserver,
14
+ } from "./interfaces";
15
+
16
+ /**
17
+ * The `OldestClientObserver` is a utility inspect if the local client is the oldest amongst connected clients (in
18
+ * terms of when they connected) and watch for changes.
19
+ *
20
+ * It is still experimental and under development. Please do try it out, but expect breaking changes in the future.
21
+ *
22
+ * @remarks
23
+ * ### Creation
24
+ *
25
+ * The `OldestClientObserver` constructor takes an `IOldestClientObservable`. This is most easily satisfied with
26
+ * either an `IContainerRuntime` or an `IFluidDataStoreRuntime`:
27
+ *
28
+ * ```typescript
29
+ * // E.g. from within a BaseContainerRuntimeFactory:
30
+ * protected async containerHasInitialized(runtime: IContainerRuntime) {
31
+ * const oldestClientObserver = new OldestClientObserver(runtime);
32
+ * // ...
33
+ * }
34
+ * ```
35
+ *
36
+ * ```typescript
37
+ * // From within a DataObject
38
+ * protected async hasInitialized() {
39
+ * const oldestClientObserver = new OldestClientObserver(this.runtime);
40
+ * // ...
41
+ * }
42
+ * ```
43
+ *
44
+ * ### Usage
45
+ *
46
+ * To check if the local client is the oldest, use the `isOldest()` method.
47
+ *
48
+ * ```typescript
49
+ * if (oldestClientObserver.isOldest()) {
50
+ * console.log("I'm the oldest");
51
+ * } else {
52
+ * console.log("Someone else is older");
53
+ * }
54
+ * ```
55
+ *
56
+ * ### Eventing
57
+ *
58
+ * `OldestClientObserver` is an `EventEmitter`, and will emit events when the local client becomes the oldest and when
59
+ * it is no longer the oldest.
60
+ *
61
+ * ```typescript
62
+ * oldestClientObserver.on("becameOldest", () => {
63
+ * console.log("I'm the oldest now");
64
+ * });
65
+ *
66
+ * oldestClientObserver.on("lostOldest", () => {
67
+ * console.log("I'm not the oldest anymore");
68
+ * });
69
+ * ```
70
+ * @alpha
71
+ */
72
+ export class OldestClientObserver
73
+ extends TypedEventEmitter<IOldestClientObserverEvents>
74
+ implements IOldestClientObserver
75
+ {
76
+ private readonly quorum: IQuorumClients;
77
+ private currentIsOldest: boolean = false;
78
+ constructor(private readonly observable: IOldestClientObservable) {
79
+ super();
80
+ this.quorum = this.observable.getQuorum();
81
+ this.currentIsOldest = this.computeIsOldest();
82
+ this.quorum.on("addMember", this.updateOldest);
83
+ this.quorum.on("removeMember", this.updateOldest);
84
+ observable.on("connected", this.updateOldest);
85
+ observable.on("disconnected", this.updateOldest);
86
+ }
87
+
88
+ public isOldest(): boolean {
89
+ return this.currentIsOldest;
90
+ }
91
+
92
+ private readonly updateOldest = (): void => {
93
+ const oldest = this.computeIsOldest();
94
+ if (this.currentIsOldest !== oldest) {
95
+ this.currentIsOldest = oldest;
96
+ if (oldest) {
97
+ this.emit("becameOldest");
98
+ } else {
99
+ this.emit("lostOldest");
100
+ }
101
+ }
102
+ };
103
+
104
+ private computeIsOldest(): boolean {
105
+ // If the container is detached, we are the only ones that know about it and are the oldest by default.
106
+ if (this.observable.attachState === AttachState.Detached) {
107
+ return true;
108
+ }
109
+
110
+ // If we're not connected we can't be the oldest connected client.
111
+ if (!this.observable.connected) {
112
+ return false;
113
+ }
114
+
115
+ // TODO: Clean up error code linter violations repo-wide.
116
+ assert(
117
+ this.observable.clientId !== undefined,
118
+ // eslint-disable-next-line unicorn/numeric-separators-style
119
+ 0x1da /* "Client id should be set if connected" */,
120
+ );
121
+
122
+ const selfSequencedClient = this.quorum.getMember(this.observable.clientId);
123
+ // When in readonly mode our clientId will not be present in the quorum.
124
+ if (selfSequencedClient === undefined) {
125
+ return false;
126
+ }
127
+
128
+ const members = this.quorum.getMembers();
129
+ for (const sequencedClient of members.values()) {
130
+ if (sequencedClient.sequenceNumber < selfSequencedClient.sequenceNumber) {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ // No member of the quorum was older
136
+ return true;
137
+ }
138
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": [
3
+ "../../../common/build/build-common/tsconfig.base.json",
4
+ "../../../common/build/build-common/tsconfig.cjs.json",
5
+ ],
6
+ "include": ["src/**/*"],
7
+ "exclude": ["src/test/**/*"],
8
+ "compilerOptions": {
9
+ "rootDir": "./src",
10
+ "outDir": "./dist",
11
+ },
12
+ }