@fluidframework/container-loader 2.0.0-rc.4.0.6 → 2.0.0-rc.5.0.0

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 (232) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/api-extractor/api-extractor-lint-bundle.json +5 -0
  3. package/api-extractor/api-extractor-lint-legacy.cjs.json +5 -0
  4. package/api-extractor/api-extractor-lint-legacy.esm.json +5 -0
  5. package/api-extractor/api-extractor-lint-public.cjs.json +5 -0
  6. package/api-extractor/api-extractor-lint-public.esm.json +5 -0
  7. package/api-extractor.json +1 -1
  8. package/api-report/{container-loader.api.md → container-loader.alpha.api.md} +60 -17
  9. package/api-report/container-loader.beta.api.md +44 -0
  10. package/api-report/container-loader.public.api.md +44 -0
  11. package/biome.jsonc +4 -0
  12. package/dist/attachment.d.ts +1 -1
  13. package/dist/attachment.d.ts.map +1 -1
  14. package/dist/attachment.js.map +1 -1
  15. package/dist/audience.d.ts +3 -2
  16. package/dist/audience.d.ts.map +1 -1
  17. package/dist/audience.js.map +1 -1
  18. package/dist/catchUpMonitor.d.ts.map +1 -1
  19. package/dist/catchUpMonitor.js.map +1 -1
  20. package/dist/connectionManager.d.ts +4 -3
  21. package/dist/connectionManager.d.ts.map +1 -1
  22. package/dist/connectionManager.js +7 -8
  23. package/dist/connectionManager.js.map +1 -1
  24. package/dist/connectionStateHandler.d.ts +1 -1
  25. package/dist/connectionStateHandler.d.ts.map +1 -1
  26. package/dist/connectionStateHandler.js +8 -4
  27. package/dist/connectionStateHandler.js.map +1 -1
  28. package/dist/container.d.ts +3 -2
  29. package/dist/container.d.ts.map +1 -1
  30. package/dist/container.js +78 -80
  31. package/dist/container.js.map +1 -1
  32. package/dist/containerContext.d.ts +2 -2
  33. package/dist/containerContext.d.ts.map +1 -1
  34. package/dist/containerContext.js.map +1 -1
  35. package/dist/containerStorageAdapter.d.ts +5 -4
  36. package/dist/containerStorageAdapter.d.ts.map +1 -1
  37. package/dist/containerStorageAdapter.js +24 -8
  38. package/dist/containerStorageAdapter.js.map +1 -1
  39. package/dist/contracts.d.ts +2 -2
  40. package/dist/contracts.d.ts.map +1 -1
  41. package/dist/contracts.js.map +1 -1
  42. package/dist/debugLogger.d.ts.map +1 -1
  43. package/dist/debugLogger.js.map +1 -1
  44. package/dist/deltaManager.d.ts +6 -5
  45. package/dist/deltaManager.d.ts.map +1 -1
  46. package/dist/deltaManager.js +15 -16
  47. package/dist/deltaManager.js.map +1 -1
  48. package/dist/deltaQueue.d.ts.map +1 -1
  49. package/dist/deltaQueue.js.map +1 -1
  50. package/dist/disposal.js.map +1 -1
  51. package/dist/error.d.ts.map +1 -1
  52. package/dist/error.js.map +1 -1
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/legacy.d.ts +5 -0
  57. package/dist/loadPaused.js.map +1 -1
  58. package/dist/loader.d.ts +1 -1
  59. package/dist/loader.d.ts.map +1 -1
  60. package/dist/loader.js.map +1 -1
  61. package/dist/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  62. package/dist/memoryBlobStorage.d.ts.map +1 -1
  63. package/dist/memoryBlobStorage.js +4 -3
  64. package/dist/memoryBlobStorage.js.map +1 -1
  65. package/dist/noopHeuristic.d.ts +1 -1
  66. package/dist/noopHeuristic.d.ts.map +1 -1
  67. package/dist/noopHeuristic.js.map +1 -1
  68. package/dist/packageVersion.d.ts +1 -1
  69. package/dist/packageVersion.js +1 -1
  70. package/dist/packageVersion.js.map +1 -1
  71. package/dist/protocol/index.d.ts +7 -0
  72. package/dist/protocol/index.d.ts.map +1 -0
  73. package/dist/protocol/index.js +12 -0
  74. package/dist/protocol/index.js.map +1 -0
  75. package/dist/protocol/protocol.d.ts +52 -0
  76. package/dist/protocol/protocol.d.ts.map +1 -0
  77. package/dist/protocol/protocol.js +112 -0
  78. package/dist/protocol/protocol.js.map +1 -0
  79. package/dist/protocol/quorum.d.ts +185 -0
  80. package/dist/protocol/quorum.d.ts.map +1 -0
  81. package/dist/protocol/quorum.js +419 -0
  82. package/dist/protocol/quorum.js.map +1 -0
  83. package/dist/protocol.d.ts +4 -4
  84. package/dist/protocol.d.ts.map +1 -1
  85. package/dist/protocol.js +6 -6
  86. package/dist/protocol.js.map +1 -1
  87. package/dist/protocolTreeDocumentStorageService.d.ts +16 -9
  88. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  89. package/dist/protocolTreeDocumentStorageService.js +18 -9
  90. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  91. package/dist/quorum.d.ts +1 -1
  92. package/dist/quorum.d.ts.map +1 -1
  93. package/dist/quorum.js.map +1 -1
  94. package/dist/retriableDocumentStorageService.d.ts +2 -2
  95. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  96. package/dist/retriableDocumentStorageService.js +3 -1
  97. package/dist/retriableDocumentStorageService.js.map +1 -1
  98. package/dist/serializedStateManager.d.ts +5 -5
  99. package/dist/serializedStateManager.d.ts.map +1 -1
  100. package/dist/serializedStateManager.js +16 -12
  101. package/dist/serializedStateManager.js.map +1 -1
  102. package/dist/utils.d.ts +2 -1
  103. package/dist/utils.d.ts.map +1 -1
  104. package/dist/utils.js +7 -7
  105. package/dist/utils.js.map +1 -1
  106. package/lib/attachment.d.ts +1 -1
  107. package/lib/attachment.d.ts.map +1 -1
  108. package/lib/attachment.js.map +1 -1
  109. package/lib/audience.d.ts +3 -2
  110. package/lib/audience.d.ts.map +1 -1
  111. package/lib/audience.js.map +1 -1
  112. package/lib/catchUpMonitor.d.ts.map +1 -1
  113. package/lib/catchUpMonitor.js.map +1 -1
  114. package/lib/connectionManager.d.ts +4 -3
  115. package/lib/connectionManager.d.ts.map +1 -1
  116. package/lib/connectionManager.js +6 -7
  117. package/lib/connectionManager.js.map +1 -1
  118. package/lib/connectionStateHandler.d.ts +1 -1
  119. package/lib/connectionStateHandler.d.ts.map +1 -1
  120. package/lib/connectionStateHandler.js +8 -4
  121. package/lib/connectionStateHandler.js.map +1 -1
  122. package/lib/container.d.ts +3 -2
  123. package/lib/container.d.ts.map +1 -1
  124. package/lib/container.js +20 -22
  125. package/lib/container.js.map +1 -1
  126. package/lib/containerContext.d.ts +2 -2
  127. package/lib/containerContext.d.ts.map +1 -1
  128. package/lib/containerContext.js.map +1 -1
  129. package/lib/containerStorageAdapter.d.ts +5 -4
  130. package/lib/containerStorageAdapter.d.ts.map +1 -1
  131. package/lib/containerStorageAdapter.js +24 -8
  132. package/lib/containerStorageAdapter.js.map +1 -1
  133. package/lib/contracts.d.ts +2 -2
  134. package/lib/contracts.d.ts.map +1 -1
  135. package/lib/contracts.js.map +1 -1
  136. package/lib/debugLogger.d.ts.map +1 -1
  137. package/lib/debugLogger.js.map +1 -1
  138. package/lib/deltaManager.d.ts +6 -5
  139. package/lib/deltaManager.d.ts.map +1 -1
  140. package/lib/deltaManager.js +8 -9
  141. package/lib/deltaManager.js.map +1 -1
  142. package/lib/deltaQueue.d.ts.map +1 -1
  143. package/lib/deltaQueue.js.map +1 -1
  144. package/lib/disposal.js.map +1 -1
  145. package/lib/error.d.ts.map +1 -1
  146. package/lib/error.js.map +1 -1
  147. package/lib/index.d.ts +1 -0
  148. package/lib/index.d.ts.map +1 -1
  149. package/lib/index.js.map +1 -1
  150. package/lib/legacy.d.ts +5 -0
  151. package/lib/loadPaused.js.map +1 -1
  152. package/lib/loader.d.ts +1 -1
  153. package/lib/loader.d.ts.map +1 -1
  154. package/lib/loader.js.map +1 -1
  155. package/lib/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  156. package/lib/memoryBlobStorage.d.ts.map +1 -1
  157. package/lib/memoryBlobStorage.js +4 -3
  158. package/lib/memoryBlobStorage.js.map +1 -1
  159. package/lib/noopHeuristic.d.ts +1 -1
  160. package/lib/noopHeuristic.d.ts.map +1 -1
  161. package/lib/noopHeuristic.js.map +1 -1
  162. package/lib/packageVersion.d.ts +1 -1
  163. package/lib/packageVersion.js +1 -1
  164. package/lib/packageVersion.js.map +1 -1
  165. package/lib/protocol/index.d.ts +7 -0
  166. package/lib/protocol/index.d.ts.map +1 -0
  167. package/lib/protocol/index.js +7 -0
  168. package/lib/protocol/index.js.map +1 -0
  169. package/lib/protocol/protocol.d.ts +52 -0
  170. package/lib/protocol/protocol.d.ts.map +1 -0
  171. package/lib/protocol/protocol.js +108 -0
  172. package/lib/protocol/protocol.js.map +1 -0
  173. package/lib/protocol/quorum.d.ts +185 -0
  174. package/lib/protocol/quorum.d.ts.map +1 -0
  175. package/lib/protocol/quorum.js +410 -0
  176. package/lib/protocol/quorum.js.map +1 -0
  177. package/lib/protocol.d.ts +4 -4
  178. package/lib/protocol.d.ts.map +1 -1
  179. package/lib/protocol.js +2 -2
  180. package/lib/protocol.js.map +1 -1
  181. package/lib/protocolTreeDocumentStorageService.d.ts +16 -9
  182. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  183. package/lib/protocolTreeDocumentStorageService.js +18 -9
  184. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  185. package/lib/quorum.d.ts +1 -1
  186. package/lib/quorum.d.ts.map +1 -1
  187. package/lib/quorum.js.map +1 -1
  188. package/lib/retriableDocumentStorageService.d.ts +2 -2
  189. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  190. package/lib/retriableDocumentStorageService.js +3 -1
  191. package/lib/retriableDocumentStorageService.js.map +1 -1
  192. package/lib/serializedStateManager.d.ts +5 -5
  193. package/lib/serializedStateManager.d.ts.map +1 -1
  194. package/lib/serializedStateManager.js +17 -13
  195. package/lib/serializedStateManager.js.map +1 -1
  196. package/lib/tsdoc-metadata.json +1 -1
  197. package/lib/utils.d.ts +2 -1
  198. package/lib/utils.d.ts.map +1 -1
  199. package/lib/utils.js +3 -3
  200. package/lib/utils.js.map +1 -1
  201. package/package.json +32 -31
  202. package/src/attachment.ts +8 -5
  203. package/src/audience.ts +4 -7
  204. package/src/catchUpMonitor.ts +4 -2
  205. package/src/connectionManager.ts +27 -24
  206. package/src/connectionStateHandler.ts +11 -10
  207. package/src/container.ts +64 -72
  208. package/src/containerContext.ts +5 -5
  209. package/src/containerStorageAdapter.ts +37 -22
  210. package/src/contracts.ts +4 -5
  211. package/src/debugLogger.ts +2 -3
  212. package/src/deltaManager.ts +13 -18
  213. package/src/deltaQueue.ts +4 -1
  214. package/src/error.ts +4 -1
  215. package/src/index.ts +7 -0
  216. package/src/loadPaused.ts +1 -1
  217. package/src/loader.ts +1 -1
  218. package/src/memoryBlobStorage.ts +6 -4
  219. package/src/noopHeuristic.ts +1 -1
  220. package/src/packageVersion.ts +1 -1
  221. package/src/protocol/README.md +10 -0
  222. package/src/protocol/index.ts +16 -0
  223. package/src/protocol/protocol.ts +185 -0
  224. package/src/protocol/quorum.ts +577 -0
  225. package/src/protocol.ts +6 -9
  226. package/src/protocolTreeDocumentStorageService.ts +30 -13
  227. package/src/quorum.ts +1 -1
  228. package/src/retriableDocumentStorageService.ts +6 -7
  229. package/src/serializedStateManager.ts +33 -34
  230. package/src/utils.ts +16 -10
  231. package/tsconfig.json +2 -0
  232. package/tsdoc.json +4 -0
@@ -0,0 +1,577 @@
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/internal";
8
+ import { IQuorumClients, ISequencedClient } from "@fluidframework/driver-definitions";
9
+ import {
10
+ ISequencedDocumentMessage,
11
+ ICommittedProposal,
12
+ IQuorum,
13
+ IQuorumProposals,
14
+ ISequencedProposal,
15
+ } from "@fluidframework/driver-definitions/internal";
16
+ import events_pkg from "events_pkg";
17
+ const { EventEmitter } = events_pkg;
18
+
19
+ /**
20
+ * Structure for tracking proposals that have been sequenced but not approved yet.
21
+ */
22
+ class PendingProposal implements ISequencedProposal {
23
+ constructor(
24
+ public readonly sequenceNumber: number,
25
+ public readonly key: string,
26
+ public readonly value: any,
27
+ public readonly local: boolean,
28
+ ) {}
29
+ }
30
+
31
+ /**
32
+ * Snapshot format for a QuorumClients
33
+ * @alpha
34
+ */
35
+ export type QuorumClientsSnapshot = [string, ISequencedClient][];
36
+
37
+ /**
38
+ * Snapshot format for a QuorumProposals
39
+ * @alpha
40
+ */
41
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
42
+ export type QuorumProposalsSnapshot = {
43
+ proposals: [number, ISequencedProposal, string[]][];
44
+ values: [string, ICommittedProposal][];
45
+ };
46
+
47
+ /**
48
+ * Snapshot format for a Quorum
49
+ * @alpha
50
+ */
51
+ export interface IQuorumSnapshot {
52
+ members: QuorumClientsSnapshot;
53
+ proposals: QuorumProposalsSnapshot["proposals"];
54
+ values: QuorumProposalsSnapshot["values"];
55
+ }
56
+
57
+ /**
58
+ * The QuorumClients is used to track members joining and leaving the collaboration session.
59
+ * @internal
60
+ */
61
+ export class QuorumClients
62
+ extends TypedEventEmitter<IQuorumClients["on"]>
63
+ implements IQuorumClients
64
+ {
65
+ private readonly members: Map<string, ISequencedClient>;
66
+ private isDisposed: boolean = false;
67
+ public get disposed() {
68
+ return this.isDisposed;
69
+ }
70
+
71
+ /**
72
+ * Cached snapshot state, to avoid unnecessary deep clones on repeated snapshot calls.
73
+ * Cleared immediately (set to undefined) when the cache becomes invalid.
74
+ */
75
+ private snapshotCache: QuorumClientsSnapshot | undefined;
76
+
77
+ constructor(snapshot: QuorumClientsSnapshot) {
78
+ super();
79
+
80
+ this.members = new Map(snapshot);
81
+ this.snapshotCache = snapshot;
82
+ }
83
+
84
+ /**
85
+ * Snapshots the current state of the QuorumClients
86
+ * @returns a snapshot of the clients in the quorum
87
+ */
88
+ public snapshot(): QuorumClientsSnapshot {
89
+ this.snapshotCache ??= Array.from(this.members);
90
+
91
+ return this.snapshotCache;
92
+ }
93
+
94
+ /**
95
+ * Adds a new client to the quorum
96
+ */
97
+ public addMember(clientId: string, details: ISequencedClient) {
98
+ assert(!!clientId, 0x9a0 /* clientId has to be non-empty string */);
99
+ assert(!this.members.has(clientId), 0x9a1 /* clientId not found */);
100
+ this.members.set(clientId, details);
101
+ this.emit("addMember", clientId, details);
102
+
103
+ // clear the cache
104
+ this.snapshotCache = undefined;
105
+ }
106
+
107
+ /**
108
+ * Removes a client from the quorum
109
+ */
110
+ public removeMember(clientId: string) {
111
+ assert(!!clientId, 0x9a2 /* clientId has to be non-empty string */);
112
+ assert(this.members.has(clientId), 0x9a3 /* clientId not found */);
113
+ this.members.delete(clientId);
114
+ this.emit("removeMember", clientId);
115
+
116
+ // clear the cache
117
+ this.snapshotCache = undefined;
118
+ }
119
+
120
+ /**
121
+ * Retrieves all the members in the quorum
122
+ */
123
+ public getMembers(): Map<string, ISequencedClient> {
124
+ return new Map(this.members);
125
+ }
126
+
127
+ /**
128
+ * Retrieves a specific member of the quorum
129
+ */
130
+ public getMember(clientId: string): ISequencedClient | undefined {
131
+ return this.members.get(clientId);
132
+ }
133
+
134
+ public dispose(): void {
135
+ this.isDisposed = true;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * The QuorumProposals holds a key/value store. Proposed values become finalized in the store once all connected
141
+ * clients have seen the proposal.
142
+ * @internal
143
+ */
144
+ export class QuorumProposals
145
+ extends TypedEventEmitter<IQuorumProposals["on"]>
146
+ implements IQuorumProposals
147
+ {
148
+ private readonly proposals: Map<number, PendingProposal>;
149
+ private readonly values: Map<string, ICommittedProposal>;
150
+ private isDisposed: boolean = false;
151
+ public get disposed() {
152
+ return this.isDisposed;
153
+ }
154
+
155
+ // Event emitter for changes to the environment that affect pending proposal promises.
156
+ private readonly stateEvents = new EventEmitter();
157
+
158
+ /**
159
+ * Cached snapshot state, to avoid unnecessary deep clones on repeated snapshot calls.
160
+ * Cleared immediately (set to undefined) when the cache becomes invalid.
161
+ */
162
+ private proposalsSnapshotCache: QuorumProposalsSnapshot["proposals"] | undefined;
163
+ private valuesSnapshotCache: QuorumProposalsSnapshot["values"] | undefined;
164
+
165
+ constructor(
166
+ snapshot: QuorumProposalsSnapshot,
167
+ private readonly sendProposal: (key: string, value: any) => number,
168
+ ) {
169
+ super();
170
+
171
+ this.proposals = new Map(
172
+ snapshot.proposals.map(([, proposal]) => {
173
+ return [
174
+ proposal.sequenceNumber,
175
+ new PendingProposal(
176
+ proposal.sequenceNumber,
177
+ proposal.key,
178
+ proposal.value,
179
+ false, // local
180
+ ),
181
+ ] as [number, PendingProposal];
182
+ }),
183
+ );
184
+ this.values = new Map(snapshot.values);
185
+ this.proposalsSnapshotCache = snapshot.proposals;
186
+ this.valuesSnapshotCache = snapshot.values;
187
+ }
188
+
189
+ /**
190
+ * Snapshots the current state of the QuorumProposals
191
+ * @returns arrays of proposals and values
192
+ */
193
+ public snapshot(): QuorumProposalsSnapshot {
194
+ this.proposalsSnapshotCache ??= Array.from(this.proposals).map(
195
+ ([sequenceNumber, proposal]) => [
196
+ sequenceNumber,
197
+ { sequenceNumber, key: proposal.key, value: proposal.value },
198
+ [], // rejections, which has been removed
199
+ ],
200
+ );
201
+ this.valuesSnapshotCache ??= Array.from(this.values);
202
+
203
+ return {
204
+ proposals: this.proposalsSnapshotCache,
205
+ values: this.valuesSnapshotCache,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Returns whether the quorum has achieved a consensus for the given key.
211
+ */
212
+ public has(key: string): boolean {
213
+ return this.values.has(key);
214
+ }
215
+
216
+ /**
217
+ * Returns the consensus value for the given key
218
+ */
219
+ public get(key: string): any {
220
+ return this.values.get(key)?.value;
221
+ }
222
+
223
+ /**
224
+ * Returns additional data about the approved consensus value
225
+ * @deprecated Removed in recent protocol-definitions. Use get() instead.
226
+ */
227
+ public getApprovalData(key: string): ICommittedProposal | undefined {
228
+ return this.values.get(key);
229
+ }
230
+
231
+ /**
232
+ * Proposes a new value. Returns a promise that will either:
233
+ * - Resolve when the proposal is accepted
234
+ * - Reject if the proposal fails to send or if the QuorumProposals is disposed
235
+ */
236
+ public async propose(key: string, value: any): Promise<void> {
237
+ const clientSequenceNumber = this.sendProposal(key, value);
238
+ if (clientSequenceNumber < 0) {
239
+ this.emit("error", { eventName: "ProposalInDisconnectedState", key });
240
+ throw new Error("Can't propose in disconnected state");
241
+ }
242
+
243
+ return new Promise<void>((resolve, reject) => {
244
+ // The sequence number that our proposal was assigned and went pending.
245
+ // If undefined, then it's not sequenced yet.
246
+ let thisProposalSequenceNumber: number | undefined;
247
+
248
+ // A proposal goes through two phases before this promise resolves:
249
+ // 1. Sequencing - waiting for the proposal to be ack'd by the server.
250
+ // 2. Approval - waiting for the proposal to be approved by connected clients.
251
+ const localProposalSequencedHandler = (sequencedCSN: number, sequenceNumber: number) => {
252
+ if (sequencedCSN === clientSequenceNumber) {
253
+ thisProposalSequenceNumber = sequenceNumber;
254
+ this.stateEvents.off("localProposalSequenced", localProposalSequencedHandler);
255
+ this.stateEvents.off("disconnected", disconnectedHandler);
256
+ this.stateEvents.on("localProposalApproved", localProposalApprovedHandler);
257
+ }
258
+ };
259
+ const localProposalApprovedHandler = (sequenceNumber: number) => {
260
+ // Proposals can be uniquely identified by the sequenceNumber they were assigned.
261
+ if (sequenceNumber === thisProposalSequenceNumber) {
262
+ resolve();
263
+ removeListeners();
264
+ }
265
+ };
266
+
267
+ // There are two error flows we consider: disconnect and disposal.
268
+ // If we get disconnected before the proposal is sequenced, it has one of two possible futures:
269
+ // 1. We reconnect and see the proposal was sequenced in the meantime.
270
+ // -> The promise can still resolve, once it is approved.
271
+ // 2. We reconnect and see the proposal was not sequenced in the meantime, so it will never sequence.
272
+ // -> The promise rejects.
273
+ const disconnectedHandler = () => {
274
+ // If we haven't seen the ack by the time we disconnect, we hope to see it by the time we reconnect.
275
+ if (thisProposalSequenceNumber === undefined) {
276
+ this.stateEvents.once("connected", () => {
277
+ // If we don't see the ack by the time reconnection finishes, it failed to send.
278
+ if (thisProposalSequenceNumber === undefined) {
279
+ reject(new Error("Client disconnected without successfully sending proposal"));
280
+ removeListeners();
281
+ }
282
+ });
283
+ }
284
+ };
285
+ // If the QuorumProposals is disposed of, we assume something catastrophic has happened
286
+ // All outstanding proposals are considered rejected.
287
+ const disposedHandler = () => {
288
+ reject(new Error("QuorumProposals was disposed"));
289
+ removeListeners();
290
+ };
291
+ // Convenience function to clean up our listeners.
292
+ const removeListeners = () => {
293
+ this.stateEvents.off("localProposalSequenced", localProposalSequencedHandler);
294
+ this.stateEvents.off("localProposalApproved", localProposalApprovedHandler);
295
+ this.stateEvents.off("disconnected", disconnectedHandler);
296
+ this.stateEvents.off("disposed", disposedHandler);
297
+ };
298
+ this.stateEvents.on("localProposalSequenced", localProposalSequencedHandler);
299
+ this.stateEvents.on("disconnected", disconnectedHandler);
300
+ this.stateEvents.on("disposed", disposedHandler);
301
+ });
302
+ }
303
+
304
+ /**
305
+ * Begins tracking a new proposal
306
+ */
307
+ public addProposal(
308
+ key: string,
309
+ value: any,
310
+ sequenceNumber: number,
311
+ local: boolean,
312
+ clientSequenceNumber: number,
313
+ ) {
314
+ assert(!this.proposals.has(sequenceNumber), 0x9a4 /* sequenceNumber not found */);
315
+
316
+ const proposal = new PendingProposal(sequenceNumber, key, value, local);
317
+ this.proposals.set(sequenceNumber, proposal);
318
+
319
+ // Legacy event, from rejection support. May still have some use for clients to learn that a proposal is
320
+ // likely to be approved soon.
321
+ this.emit("addProposal", proposal);
322
+
323
+ if (local) {
324
+ this.stateEvents.emit("localProposalSequenced", clientSequenceNumber, sequenceNumber);
325
+ }
326
+
327
+ // clear the proposal cache
328
+ this.proposalsSnapshotCache = undefined;
329
+ }
330
+
331
+ /**
332
+ * Updates the minimum sequence number. If the MSN advances past the sequence number for any proposal then it
333
+ * becomes an approved value.
334
+ */
335
+ public updateMinimumSequenceNumber(message: ISequencedDocumentMessage): void {
336
+ const msn = message.minimumSequenceNumber;
337
+
338
+ // Accept proposals proposals whose sequenceNumber is <= the minimumSequenceNumber
339
+
340
+ // Return a sorted list of approved proposals. We sort so that we apply them in their sequence number order
341
+ // TODO this can be optimized if necessary to avoid the linear search+sort
342
+ const completed: PendingProposal[] = [];
343
+ for (const [sequenceNumber, proposal] of this.proposals) {
344
+ if (sequenceNumber <= msn) {
345
+ completed.push(proposal);
346
+ }
347
+ }
348
+ completed.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
349
+
350
+ for (const proposal of completed) {
351
+ const committedProposal: ICommittedProposal = {
352
+ approvalSequenceNumber: message.sequenceNumber,
353
+ // No longer used. We still stamp a -1 for compat with older versions of the quorum.
354
+ // Can be removed after 0.1035 and higher is ubiquitous.
355
+ commitSequenceNumber: -1,
356
+ key: proposal.key,
357
+ sequenceNumber: proposal.sequenceNumber,
358
+ value: proposal.value,
359
+ };
360
+
361
+ this.values.set(committedProposal.key, committedProposal);
362
+
363
+ // clear the values cache
364
+ this.valuesSnapshotCache = undefined;
365
+
366
+ // check if there are multiple proposals with matching keys
367
+ let proposalSettled = false;
368
+ let proposalKeySeen = false;
369
+ for (const [, p] of this.proposals) {
370
+ if (p.key === committedProposal.key) {
371
+ if (!proposalKeySeen) {
372
+ // set proposalSettled to true if the proposal key match is unique thus far
373
+ proposalSettled = true;
374
+ } else {
375
+ // set proposalSettled to false if matching proposal key is not unique
376
+ proposalSettled = false;
377
+ break;
378
+ }
379
+ proposalKeySeen = true;
380
+ }
381
+ }
382
+
383
+ this.emit(
384
+ "approveProposal",
385
+ committedProposal.sequenceNumber,
386
+ committedProposal.key,
387
+ committedProposal.value,
388
+ committedProposal.approvalSequenceNumber,
389
+ );
390
+
391
+ // emit approveProposalComplete when all pending proposals are processed
392
+ if (proposalSettled) {
393
+ this.emit(
394
+ "approveProposalComplete",
395
+ committedProposal.sequenceNumber,
396
+ committedProposal.key,
397
+ committedProposal.value,
398
+ committedProposal.approvalSequenceNumber,
399
+ );
400
+ }
401
+
402
+ this.proposals.delete(proposal.sequenceNumber);
403
+
404
+ // clear the proposals cache
405
+ this.proposalsSnapshotCache = undefined;
406
+ if (proposal.local) {
407
+ this.stateEvents.emit("localProposalApproved", proposal.sequenceNumber);
408
+ }
409
+ }
410
+ }
411
+
412
+ public setConnectionState(connected: boolean) {
413
+ if (connected) {
414
+ this.stateEvents.emit("connected");
415
+ } else {
416
+ this.stateEvents.emit("disconnected");
417
+ }
418
+ }
419
+
420
+ public dispose(): void {
421
+ this.isDisposed = true;
422
+ this.stateEvents.emit("disposed");
423
+ }
424
+ }
425
+
426
+ /**
427
+ * A quorum represents all clients currently within the collaboration window. As well as the values
428
+ * they have agreed upon and any pending proposals.
429
+ * @internal
430
+ */
431
+ export class Quorum extends TypedEventEmitter<IQuorum["on"]> implements IQuorum {
432
+ private readonly quorumClients: QuorumClients;
433
+ private readonly quorumProposals: QuorumProposals;
434
+ private readonly isDisposed: boolean = false;
435
+ public get disposed() {
436
+ return this.isDisposed;
437
+ }
438
+
439
+ constructor(
440
+ members: QuorumClientsSnapshot,
441
+ proposals: QuorumProposalsSnapshot["proposals"],
442
+ values: QuorumProposalsSnapshot["values"],
443
+ sendProposal: (key: string, value: any) => number,
444
+ ) {
445
+ super();
446
+
447
+ this.quorumClients = new QuorumClients(members);
448
+ this.quorumClients.on("addMember", (clientId: string, details: ISequencedClient) => {
449
+ this.emit("addMember", clientId, details);
450
+ });
451
+ this.quorumClients.on("removeMember", (clientId: string) => {
452
+ this.emit("removeMember", clientId);
453
+ });
454
+
455
+ this.quorumProposals = new QuorumProposals({ proposals, values }, sendProposal);
456
+ this.quorumProposals.on("addProposal", (proposal: ISequencedProposal) => {
457
+ this.emit("addProposal", proposal);
458
+ });
459
+ this.quorumProposals.on(
460
+ "approveProposal",
461
+ (sequenceNumber: number, key: string, value: any, approvalSequenceNumber: number) => {
462
+ this.emit("approveProposal", sequenceNumber, key, value, approvalSequenceNumber);
463
+ },
464
+ );
465
+ }
466
+
467
+ public close() {
468
+ this.removeAllListeners();
469
+ }
470
+
471
+ /**
472
+ * Snapshots the entire quorum
473
+ * @returns a quorum snapshot
474
+ */
475
+ public snapshot(): IQuorumSnapshot {
476
+ const members = this.quorumClients.snapshot();
477
+ const { proposals, values } = this.quorumProposals.snapshot();
478
+ return {
479
+ members,
480
+ proposals,
481
+ values,
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Returns whether the quorum has achieved a consensus for the given key.
487
+ */
488
+ public has(key: string): boolean {
489
+ return this.quorumProposals.has(key);
490
+ }
491
+
492
+ /**
493
+ * Returns the consensus value for the given key
494
+ */
495
+ public get(key: string): any {
496
+ return this.quorumProposals.get(key);
497
+ }
498
+
499
+ /**
500
+ * Returns additional data about the approved consensus value
501
+ * @deprecated Removed in recent protocol-definitions. Use get() instead.
502
+ */
503
+ public getApprovalData(key: string): ICommittedProposal | undefined {
504
+ return this.quorumProposals.getApprovalData(key);
505
+ }
506
+
507
+ /**
508
+ * Adds a new client to the quorum
509
+ */
510
+ public addMember(clientId: string, details: ISequencedClient) {
511
+ this.quorumClients.addMember(clientId, details);
512
+ }
513
+
514
+ /**
515
+ * Removes a client from the quorum
516
+ */
517
+ public removeMember(clientId: string) {
518
+ this.quorumClients.removeMember(clientId);
519
+ }
520
+
521
+ /**
522
+ * Retrieves all the members in the quorum
523
+ */
524
+ public getMembers(): Map<string, ISequencedClient> {
525
+ return this.quorumClients.getMembers();
526
+ }
527
+
528
+ /**
529
+ * Retrieves a specific member of the quorum
530
+ */
531
+ public getMember(clientId: string): ISequencedClient | undefined {
532
+ return this.quorumClients.getMember(clientId);
533
+ }
534
+
535
+ /**
536
+ * Proposes a new value. Returns a promise that will resolve when the proposal is either accepted, or reject if
537
+ * the proposal fails to send.
538
+ */
539
+ public async propose(key: string, value: any): Promise<void> {
540
+ return this.quorumProposals.propose(key, value);
541
+ }
542
+
543
+ /**
544
+ * Begins tracking a new proposal
545
+ */
546
+ public addProposal(
547
+ key: string,
548
+ value: any,
549
+ sequenceNumber: number,
550
+ local: boolean,
551
+ clientSequenceNumber: number,
552
+ ) {
553
+ return this.quorumProposals.addProposal(
554
+ key,
555
+ value,
556
+ sequenceNumber,
557
+ local,
558
+ clientSequenceNumber,
559
+ );
560
+ }
561
+
562
+ /**
563
+ * Updates the minimum sequence number. If the MSN advances past the sequence number for any proposal then it
564
+ * becomes an approved value.
565
+ */
566
+ public updateMinimumSequenceNumber(message: ISequencedDocumentMessage): void {
567
+ this.quorumProposals.updateMinimumSequenceNumber(message);
568
+ }
569
+
570
+ public setConnectionState(connected: boolean, clientId?: string) {
571
+ this.quorumProposals.setConnectionState(connected);
572
+ }
573
+
574
+ public dispose(): void {
575
+ throw new Error("Not implemented.");
576
+ }
577
+ }
package/src/protocol.ts CHANGED
@@ -4,20 +4,17 @@
4
4
  */
5
5
 
6
6
  import { IAudienceOwner } from "@fluidframework/container-definitions/internal";
7
- import { canBeCoalescedByService } from "@fluidframework/driver-utils/internal";
8
- import {
9
- IProtocolHandler as IBaseProtocolHandler,
10
- IQuorumSnapshot,
11
- ProtocolOpHandler,
12
- } from "@fluidframework/protocol-base";
13
7
  import {
14
8
  IDocumentAttributes,
15
9
  IProcessMessageResult,
16
- ISequencedDocumentMessage,
17
10
  ISignalClient,
18
- ISignalMessage,
19
11
  MessageType,
20
- } from "@fluidframework/protocol-definitions";
12
+ ISequencedDocumentMessage,
13
+ ISignalMessage,
14
+ } from "@fluidframework/driver-definitions/internal";
15
+ import { canBeCoalescedByService } from "@fluidframework/driver-utils/internal";
16
+
17
+ import { IBaseProtocolHandler, IQuorumSnapshot, ProtocolOpHandler } from "./protocol/index.js";
21
18
 
22
19
  // ADO: #1986: Start using enum from protocol-base.
23
20
  export enum SignalType {
@@ -4,21 +4,36 @@
4
4
  */
5
5
 
6
6
  import { IDisposable } from "@fluidframework/core-interfaces";
7
+ import { ISummaryTree } from "@fluidframework/driver-definitions";
7
8
  import {
8
9
  IDocumentStorageService,
9
10
  ISummaryContext,
10
11
  } from "@fluidframework/driver-definitions/internal";
11
- import { ISummaryTree } from "@fluidframework/protocol-definitions";
12
12
 
13
13
  /**
14
14
  * A storage service wrapper whose sole job is to intercept calls to uploadSummaryWithContext and ensure they include
15
15
  * the protocol summary, using the provided callback to add it if necessary.
16
16
  */
17
17
  export class ProtocolTreeStorageService implements IDocumentStorageService, IDisposable {
18
+ /**
19
+ *
20
+ * @param internalStorageService - Document storage service responsible to make api calls to the storage.
21
+ * @param addProtocolSummaryIfMissing - Function to add protocol summary tree to the summary. Used in scenarios where single-commit summaries are used.
22
+ * @param shouldSummarizeProtocolTree - Callback function to learn about the service preference on whether single-commit summaries are enabled.
23
+ */
18
24
  constructor(
19
25
  private readonly internalStorageService: IDocumentStorageService & IDisposable,
20
26
  private readonly addProtocolSummaryIfMissing: (summaryTree: ISummaryTree) => ISummaryTree,
21
- ) {}
27
+ private readonly shouldSummarizeProtocolTree: () => boolean,
28
+ ) {
29
+ this.getSnapshotTree = internalStorageService.getSnapshotTree.bind(internalStorageService);
30
+ this.getSnapshot = internalStorageService.getSnapshot?.bind(internalStorageService);
31
+ this.getVersions = internalStorageService.getVersions.bind(internalStorageService);
32
+ this.createBlob = internalStorageService.createBlob.bind(internalStorageService);
33
+ this.readBlob = internalStorageService.readBlob.bind(internalStorageService);
34
+ this.downloadSummary = internalStorageService.downloadSummary.bind(internalStorageService);
35
+ this.dispose = internalStorageService.dispose.bind(internalStorageService);
36
+ }
22
37
  public get policies() {
23
38
  return this.internalStorageService.policies;
24
39
  }
@@ -26,21 +41,23 @@ export class ProtocolTreeStorageService implements IDocumentStorageService, IDis
26
41
  return this.internalStorageService.disposed;
27
42
  }
28
43
 
29
- getSnapshotTree = this.internalStorageService.getSnapshotTree.bind(this.internalStorageService);
30
- getSnapshot = this.internalStorageService.getSnapshot?.bind(this.internalStorageService);
31
- getVersions = this.internalStorageService.getVersions.bind(this.internalStorageService);
32
- createBlob = this.internalStorageService.createBlob.bind(this.internalStorageService);
33
- readBlob = this.internalStorageService.readBlob.bind(this.internalStorageService);
34
- downloadSummary = this.internalStorageService.downloadSummary.bind(this.internalStorageService);
35
- dispose = this.internalStorageService.dispose.bind(this.internalStorageService);
44
+ getSnapshotTree: IDocumentStorageService["getSnapshotTree"];
45
+ getSnapshot: IDocumentStorageService["getSnapshot"];
46
+ getVersions: IDocumentStorageService["getVersions"];
47
+ createBlob: IDocumentStorageService["createBlob"];
48
+ readBlob: IDocumentStorageService["readBlob"];
49
+ downloadSummary: IDocumentStorageService["downloadSummary"];
50
+ dispose: IDisposable["dispose"];
36
51
 
37
52
  async uploadSummaryWithContext(
38
53
  summary: ISummaryTree,
39
54
  context: ISummaryContext,
40
55
  ): Promise<string> {
41
- return this.internalStorageService.uploadSummaryWithContext(
42
- this.addProtocolSummaryIfMissing(summary),
43
- context,
44
- );
56
+ return this.shouldSummarizeProtocolTree()
57
+ ? this.internalStorageService.uploadSummaryWithContext(
58
+ this.addProtocolSummaryIfMissing(summary),
59
+ context,
60
+ )
61
+ : this.internalStorageService.uploadSummaryWithContext(summary, context);
45
62
  }
46
63
  }
package/src/quorum.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IFluidCodeDetails } from "@fluidframework/container-definitions/internal";
7
- import { ICommittedProposal } from "@fluidframework/protocol-definitions";
7
+ import { ICommittedProposal } from "@fluidframework/driver-definitions/internal";
8
8
 
9
9
  export function initQuorumValuesFromCodeDetails(
10
10
  source: IFluidCodeDetails,