@fluidframework/ordered-collection 2.0.0-internal.3.0.0 → 2.0.0-internal.3.1.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 (48) hide show
  1. package/.eslintrc.js +5 -7
  2. package/.mocharc.js +2 -2
  3. package/README.md +1 -1
  4. package/api-extractor.json +2 -2
  5. package/dist/consensusOrderedCollection.d.ts.map +1 -1
  6. package/dist/consensusOrderedCollection.js +4 -2
  7. package/dist/consensusOrderedCollection.js.map +1 -1
  8. package/dist/consensusOrderedCollectionFactory.d.ts.map +1 -1
  9. package/dist/consensusOrderedCollectionFactory.js.map +1 -1
  10. package/dist/consensusQueue.d.ts.map +1 -1
  11. package/dist/consensusQueue.js.map +1 -1
  12. package/dist/interfaces.d.ts.map +1 -1
  13. package/dist/interfaces.js.map +1 -1
  14. package/dist/packageVersion.d.ts +1 -1
  15. package/dist/packageVersion.js +1 -1
  16. package/dist/packageVersion.js.map +1 -1
  17. package/dist/snapshotableArray.d.ts.map +1 -1
  18. package/dist/snapshotableArray.js.map +1 -1
  19. package/dist/testUtils.d.ts.map +1 -1
  20. package/dist/testUtils.js.map +1 -1
  21. package/lib/consensusOrderedCollection.d.ts.map +1 -1
  22. package/lib/consensusOrderedCollection.js +4 -2
  23. package/lib/consensusOrderedCollection.js.map +1 -1
  24. package/lib/consensusOrderedCollectionFactory.d.ts.map +1 -1
  25. package/lib/consensusOrderedCollectionFactory.js.map +1 -1
  26. package/lib/consensusQueue.d.ts.map +1 -1
  27. package/lib/consensusQueue.js.map +1 -1
  28. package/lib/interfaces.d.ts.map +1 -1
  29. package/lib/interfaces.js.map +1 -1
  30. package/lib/packageVersion.d.ts +1 -1
  31. package/lib/packageVersion.js +1 -1
  32. package/lib/packageVersion.js.map +1 -1
  33. package/lib/snapshotableArray.d.ts.map +1 -1
  34. package/lib/snapshotableArray.js.map +1 -1
  35. package/lib/testUtils.d.ts.map +1 -1
  36. package/lib/testUtils.js +1 -1
  37. package/lib/testUtils.js.map +1 -1
  38. package/package.json +102 -101
  39. package/prettier.config.cjs +1 -1
  40. package/src/consensusOrderedCollection.ts +337 -318
  41. package/src/consensusOrderedCollectionFactory.ts +33 -32
  42. package/src/consensusQueue.ts +41 -37
  43. package/src/interfaces.ts +74 -74
  44. package/src/packageVersion.ts +1 -1
  45. package/src/snapshotableArray.ts +11 -11
  46. package/src/testUtils.ts +19 -18
  47. package/tsconfig.esnext.json +6 -6
  48. package/tsconfig.json +9 -13
@@ -6,69 +6,69 @@
6
6
  import { assert, bufferToString, unreachableCase } from "@fluidframework/common-utils";
7
7
  import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
8
8
  import {
9
- IChannelAttributes,
10
- IFluidDataStoreRuntime,
11
- IChannelStorageService,
9
+ IChannelAttributes,
10
+ IFluidDataStoreRuntime,
11
+ IChannelStorageService,
12
12
  } from "@fluidframework/datastore-definitions";
13
13
  import { IFluidSerializer, SharedObject } from "@fluidframework/shared-object-base";
14
14
  import { ISummaryTreeWithStats } from "@fluidframework/runtime-definitions";
15
15
  import { SummaryTreeBuilder } from "@fluidframework/runtime-utils";
16
16
  import { v4 as uuid } from "uuid";
17
17
  import {
18
- ConsensusCallback,
19
- ConsensusResult,
20
- IConsensusOrderedCollection,
21
- IOrderedCollection,
22
- IConsensusOrderedCollectionEvents,
18
+ ConsensusCallback,
19
+ ConsensusResult,
20
+ IConsensusOrderedCollection,
21
+ IOrderedCollection,
22
+ IConsensusOrderedCollectionEvents,
23
23
  } from "./interfaces";
24
24
 
25
25
  const snapshotFileNameData = "header";
26
26
  const snapshotFileNameTracking = "jobTracking";
27
27
 
28
28
  interface IConsensusOrderedCollectionValue<T> {
29
- // an ID used to indicate acquired item.
30
- // Used in acquire/release/complete ops.
31
- readonly acquireId: string;
29
+ // an ID used to indicate acquired item.
30
+ // Used in acquire/release/complete ops.
31
+ readonly acquireId: string;
32
32
 
33
- // The actual value
34
- readonly value: T;
33
+ // The actual value
34
+ readonly value: T;
35
35
  }
36
36
 
37
37
  /**
38
38
  * An operation for consensus ordered collection
39
39
  */
40
40
  interface IConsensusOrderedCollectionAddOperation {
41
- opName: "add";
42
- // serialized value
43
- value: string;
41
+ opName: "add";
42
+ // serialized value
43
+ value: string;
44
44
  }
45
45
 
46
46
  interface IConsensusOrderedCollectionAcquireOperation {
47
- opName: "acquire";
48
- // an ID used to indicate acquired item.
49
- // Used in acquire/release/complete ops.
50
- acquireId: string;
47
+ opName: "acquire";
48
+ // an ID used to indicate acquired item.
49
+ // Used in acquire/release/complete ops.
50
+ acquireId: string;
51
51
  }
52
52
 
53
53
  interface IConsensusOrderedCollectionCompleteOperation {
54
- opName: "complete";
55
- // an ID used to indicate acquired item.
56
- // Used in acquire/release/complete ops.
57
- acquireId: string;
54
+ opName: "complete";
55
+ // an ID used to indicate acquired item.
56
+ // Used in acquire/release/complete ops.
57
+ acquireId: string;
58
58
  }
59
59
 
60
60
  interface IConsensusOrderedCollectionReleaseOperation {
61
- opName: "release";
62
- // an ID used to indicate acquired item.
63
- // Used in acquire/release/complete ops.
64
- acquireId: string;
61
+ opName: "release";
62
+ // an ID used to indicate acquired item.
63
+ // Used in acquire/release/complete ops.
64
+ acquireId: string;
65
65
  }
66
66
 
67
67
  type IConsensusOrderedCollectionOperation =
68
- IConsensusOrderedCollectionAddOperation |
69
- IConsensusOrderedCollectionAcquireOperation |
70
- IConsensusOrderedCollectionCompleteOperation |
71
- IConsensusOrderedCollectionReleaseOperation;
68
+ | IConsensusOrderedCollectionAddOperation
69
+ | IConsensusOrderedCollectionAcquireOperation
70
+ | IConsensusOrderedCollectionCompleteOperation
71
+ | IConsensusOrderedCollectionReleaseOperation;
72
72
 
73
73
  /** The type of the resolve function to call after the local operation is ack'd */
74
74
  type PendingResolve<T> = (value: IConsensusOrderedCollectionValue<T> | undefined) => void;
@@ -78,7 +78,7 @@ type PendingResolve<T> = (value: IConsensusOrderedCollectionValue<T> | undefined
78
78
  * Key is the acquireId from when it was acquired
79
79
  * Value is the acquired value, and the id of the client who acquired it, or undefined for unattached client
80
80
  */
81
- type JobTrackingInfo<T> = Map<string, { value: T; clientId: string | undefined; }>;
81
+ type JobTrackingInfo<T> = Map<string, { value: T; clientId: string | undefined }>;
82
82
  const idForLocalUnattachedClient = undefined;
83
83
 
84
84
  /**
@@ -91,289 +91,308 @@ const idForLocalUnattachedClient = undefined;
91
91
  * IOrderedCollection that will define the deterministic add/acquire order and snapshot ability.
92
92
  */
93
93
  export class ConsensusOrderedCollection<T = any>
94
- extends SharedObject<IConsensusOrderedCollectionEvents<T>> implements IConsensusOrderedCollection<T> {
95
- /**
96
- * The set of values that have been acquired but not yet completed or released
97
- */
98
- private jobTracking: JobTrackingInfo<T> = new Map();
99
-
100
- /**
101
- * Constructs a new consensus collection. If the object is non-local an id and service interfaces will
102
- * be provided
103
- */
104
- protected constructor(
105
- id: string,
106
- runtime: IFluidDataStoreRuntime,
107
- attributes: IChannelAttributes,
108
- private readonly data: IOrderedCollection<T>,
109
- ) {
110
- super(id, runtime, attributes, "fluid_consensusOrderedCollection_");
111
-
112
- // We can't simply call this.removeClient(this.runtime.clientId) in on runtime disconnected,
113
- // because other clients may disconnect concurrently.
114
- // Disconnect order matters because it defines the order items go back to the queue.
115
- // So we put items back to queue only when we process our own removeMember event.
116
- runtime.getQuorum().on("removeMember", (clientId: string) => {
117
- assert(!!clientId, 0x067 /* "Missing clientId for removal!" */);
118
- this.removeClient(clientId);
119
- });
120
- }
121
-
122
- /**
123
- * Add a value to the consensus collection.
124
- */
125
- public async add(value: T): Promise<void> {
126
- const valueSer = this.serializeValue(value, this.serializer);
127
-
128
- if (!this.isAttached()) {
129
- // For the case where this is not attached yet, explicitly JSON
130
- // clone the value to match the behavior of going thru the wire.
131
- const addValue = this.deserializeValue(valueSer, this.serializer) as T;
132
- this.addCore(addValue);
133
- return;
134
- }
135
-
136
- await this.submit<IConsensusOrderedCollectionAddOperation>({
137
- opName: "add",
138
- value: valueSer,
139
- });
140
- }
141
-
142
- /**
143
- * Remove a value from the consensus collection. If the collection is empty, returns false.
144
- * Otherwise calls callback with the value
145
- */
146
- public async acquire(callback: ConsensusCallback<T>): Promise<boolean> {
147
- const result = await this.acquireInternal();
148
- if (result === undefined) {
149
- return false;
150
- }
151
-
152
- const res = await callback(result.value);
153
-
154
- switch (res) {
155
- case ConsensusResult.Complete:
156
- await this.complete(result.acquireId);
157
- break;
158
- case ConsensusResult.Release:
159
- this.release(result.acquireId);
160
- this.emit("localRelease", result.value, true /* intentional */);
161
- break;
162
- default: unreachableCase(res);
163
- }
164
-
165
- return true;
166
- }
167
-
168
- /**
169
- * Wait for a value to be available and acquire it from the consensus collection
170
- */
171
- public async waitAndAcquire(callback: ConsensusCallback<T>): Promise<void> {
172
- do {
173
- if (this.data.size() === 0) {
174
- // Wait for new entry before trying to acquire again
175
- await this.newAckBasedPromise<T>((resolve) => {
176
- this.once("add", resolve);
177
- });
178
- }
179
- } while (!(await this.acquire(callback)));
180
- }
181
-
182
- protected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {
183
- // If we are transitioning from unattached to attached mode,
184
- // then we are losing all checked out work!
185
- this.removeClient(idForLocalUnattachedClient);
186
-
187
- const builder = new SummaryTreeBuilder();
188
- let blobContent = this.serializeValue(this.data.asArray(), serializer);
189
- builder.addBlob(snapshotFileNameData, blobContent);
190
- blobContent = this.serializeValue(Array.from(this.jobTracking.entries()), serializer);
191
- builder.addBlob(snapshotFileNameTracking, blobContent);
192
- return builder.getSummaryTree();
193
- }
194
-
195
- protected isActive() {
196
- return this.runtime.connected && this.runtime.deltaManager.active;
197
- }
198
-
199
- protected async complete(acquireId: string) {
200
- if (!this.isAttached()) {
201
- this.completeCore(acquireId);
202
- return;
203
- }
204
-
205
- // if not active, this item already was released to queue (as observed by other clients)
206
- if (this.isActive()) {
207
- await this.submit<IConsensusOrderedCollectionCompleteOperation>({
208
- opName: "complete",
209
- acquireId,
210
- });
211
- }
212
- }
213
-
214
- protected completeCore(acquireId: string) {
215
- // Note: item may be no longer in jobTracking and returned back to queue!
216
- const rec = this.jobTracking.get(acquireId);
217
- if (rec !== undefined) {
218
- this.jobTracking.delete(acquireId);
219
- this.emit("complete", rec.value);
220
- }
221
- }
222
-
223
- protected release(acquireId: string) {
224
- if (!this.isAttached()) {
225
- this.releaseCore(acquireId);
226
- return;
227
- }
228
-
229
- // if not active, this item already was released to queue (as observed by other clients)
230
- if (this.isActive()) {
231
- this.submit<IConsensusOrderedCollectionReleaseOperation>({
232
- opName: "release",
233
- acquireId,
234
- }).catch((error) => {
235
- this.runtime.logger.sendErrorEvent({ eventName: "ConsensusQueue_release" }, error);
236
- });
237
- }
238
- }
239
-
240
- protected releaseCore(acquireId: string) {
241
- // Note: item may be no longer in jobTracking and returned back to queue!
242
- const rec = this.jobTracking.get(acquireId);
243
- if (rec !== undefined) {
244
- this.jobTracking.delete(acquireId);
245
- this.data.add(rec.value);
246
- this.emit("add", rec.value, false /* newlyAdded */);
247
- }
248
- }
249
-
250
- /**
251
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
252
- */
253
- protected async loadCore(storage: IChannelStorageService): Promise<void> {
254
- assert(this.jobTracking.size === 0, 0x068 /* "On consensusOrderedCollection load, job tracking size > 0" */);
255
- const blob = await storage.readBlob(snapshotFileNameTracking);
256
- const rawContentTracking = bufferToString(blob, "utf8");
257
- const content = this.deserializeValue(rawContentTracking, this.serializer);
258
- this.jobTracking = new Map(content) as JobTrackingInfo<T>;
259
-
260
- assert(this.data.size() === 0, 0x069 /* "On consensusOrderedCollection load, data size > 0" */);
261
- const blob2 = await storage.readBlob(snapshotFileNameData);
262
- const rawContentData = bufferToString(blob2, "utf8");
263
- const content2 = this.deserializeValue(rawContentData, this.serializer) as T[];
264
- this.data.loadFrom(content2);
265
- }
266
-
267
- protected onDisconnect() {
268
- for (const [, { value, clientId }] of this.jobTracking) {
269
- if (clientId === this.runtime.clientId) {
270
- this.emit("localRelease", value, false /* intentional */);
271
- }
272
- }
273
- }
274
-
275
- protected processCore(message: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown) {
276
- if (message.type === MessageType.Operation) {
277
- const op: IConsensusOrderedCollectionOperation = message.contents;
278
- let value: IConsensusOrderedCollectionValue<T> | undefined;
279
- switch (op.opName) {
280
- case "add":
281
- this.addCore(this.deserializeValue(op.value, this.serializer) as T);
282
- break;
283
-
284
- case "acquire":
285
- value = this.acquireCore(op.acquireId, message.clientId);
286
- break;
287
-
288
- case "complete":
289
- this.completeCore(op.acquireId);
290
- break;
291
-
292
- case "release":
293
- this.releaseCore(op.acquireId);
294
- break;
295
-
296
- default: unreachableCase(op);
297
- }
298
- if (local) {
299
- // Resolve the pending promise for this operation now that we have received an ack for it.
300
- const resolve = localOpMetadata as PendingResolve<T>;
301
- resolve(value);
302
- }
303
- }
304
- }
305
-
306
- private async submit<TMessage extends IConsensusOrderedCollectionOperation>(
307
- message: TMessage,
308
- ): Promise<IConsensusOrderedCollectionValue<T> | undefined> {
309
- assert(this.isAttached(), 0x06a /* "Trying to submit message while detached!" */);
310
-
311
- return this.newAckBasedPromise<IConsensusOrderedCollectionValue<T> | undefined>((resolve) => {
312
- // Send the resolve function as the localOpMetadata. This will be provided back to us when the
313
- // op is ack'd.
314
- this.submitLocalMessage(message, resolve);
315
- // If we fail due to runtime being disposed, it's better to return undefined then unhandled exception.
316
- }).catch((error) => undefined);
317
- }
318
-
319
- private addCore(value: T) {
320
- this.data.add(value);
321
- this.emit("add", value, true /* newlyAdded */);
322
- }
323
-
324
- private acquireCore(acquireId: string, clientId?: string): IConsensusOrderedCollectionValue<T> | undefined {
325
- if (this.data.size() === 0) {
326
- return undefined;
327
- }
328
- const value = this.data.remove();
329
-
330
- const value2: IConsensusOrderedCollectionValue<T> = {
331
- acquireId,
332
- value,
333
- };
334
- this.jobTracking.set(value2.acquireId, { value, clientId });
335
-
336
- this.emit("acquire", value, clientId);
337
- return value2;
338
- }
339
-
340
- private async acquireInternal(): Promise<IConsensusOrderedCollectionValue<T> | undefined> {
341
- if (!this.isAttached()) {
342
- // can be undefined if queue is empty
343
- return this.acquireCore(uuid(), idForLocalUnattachedClient);
344
- }
345
-
346
- return this.submit<IConsensusOrderedCollectionAcquireOperation>({
347
- opName: "acquire",
348
- acquireId: uuid(),
349
- });
350
- }
351
-
352
- private removeClient(clientIdToRemove?: string) {
353
- const added: T[] = [];
354
- for (const [acquireId, { value, clientId }] of this.jobTracking) {
355
- if (clientId === clientIdToRemove) {
356
- this.jobTracking.delete(acquireId);
357
- this.data.add(value);
358
- added.push(value);
359
- }
360
- }
361
-
362
- // Raise all events only after all state changes are completed,
363
- // to guarantee same ordering of operations if collection is manipulated from events.
364
- added.map((value) => this.emit("add", value, false /* newlyAdded */));
365
- }
366
-
367
- private serializeValue(value, serializer: IFluidSerializer) {
368
- return serializer.stringify(value, this.handle);
369
- }
370
-
371
- private deserializeValue(content: string, serializer: IFluidSerializer) {
372
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
373
- return serializer.parse(content);
374
- }
375
-
376
- protected applyStashedOp() {
377
- throw new Error("not implemented");
378
- }
94
+ extends SharedObject<IConsensusOrderedCollectionEvents<T>>
95
+ implements IConsensusOrderedCollection<T>
96
+ {
97
+ /**
98
+ * The set of values that have been acquired but not yet completed or released
99
+ */
100
+ private jobTracking: JobTrackingInfo<T> = new Map();
101
+
102
+ /**
103
+ * Constructs a new consensus collection. If the object is non-local an id and service interfaces will
104
+ * be provided
105
+ */
106
+ protected constructor(
107
+ id: string,
108
+ runtime: IFluidDataStoreRuntime,
109
+ attributes: IChannelAttributes,
110
+ private readonly data: IOrderedCollection<T>,
111
+ ) {
112
+ super(id, runtime, attributes, "fluid_consensusOrderedCollection_");
113
+
114
+ // We can't simply call this.removeClient(this.runtime.clientId) in on runtime disconnected,
115
+ // because other clients may disconnect concurrently.
116
+ // Disconnect order matters because it defines the order items go back to the queue.
117
+ // So we put items back to queue only when we process our own removeMember event.
118
+ runtime.getQuorum().on("removeMember", (clientId: string) => {
119
+ assert(!!clientId, 0x067 /* "Missing clientId for removal!" */);
120
+ this.removeClient(clientId);
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Add a value to the consensus collection.
126
+ */
127
+ public async add(value: T): Promise<void> {
128
+ const valueSer = this.serializeValue(value, this.serializer);
129
+
130
+ if (!this.isAttached()) {
131
+ // For the case where this is not attached yet, explicitly JSON
132
+ // clone the value to match the behavior of going thru the wire.
133
+ const addValue = this.deserializeValue(valueSer, this.serializer) as T;
134
+ this.addCore(addValue);
135
+ return;
136
+ }
137
+
138
+ await this.submit<IConsensusOrderedCollectionAddOperation>({
139
+ opName: "add",
140
+ value: valueSer,
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Remove a value from the consensus collection. If the collection is empty, returns false.
146
+ * Otherwise calls callback with the value
147
+ */
148
+ public async acquire(callback: ConsensusCallback<T>): Promise<boolean> {
149
+ const result = await this.acquireInternal();
150
+ if (result === undefined) {
151
+ return false;
152
+ }
153
+
154
+ const res = await callback(result.value);
155
+
156
+ switch (res) {
157
+ case ConsensusResult.Complete:
158
+ await this.complete(result.acquireId);
159
+ break;
160
+ case ConsensusResult.Release:
161
+ this.release(result.acquireId);
162
+ this.emit("localRelease", result.value, true /* intentional */);
163
+ break;
164
+ default:
165
+ unreachableCase(res);
166
+ }
167
+
168
+ return true;
169
+ }
170
+
171
+ /**
172
+ * Wait for a value to be available and acquire it from the consensus collection
173
+ */
174
+ public async waitAndAcquire(callback: ConsensusCallback<T>): Promise<void> {
175
+ do {
176
+ if (this.data.size() === 0) {
177
+ // Wait for new entry before trying to acquire again
178
+ await this.newAckBasedPromise<T>((resolve) => {
179
+ this.once("add", resolve);
180
+ });
181
+ }
182
+ } while (!(await this.acquire(callback)));
183
+ }
184
+
185
+ protected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {
186
+ // If we are transitioning from unattached to attached mode,
187
+ // then we are losing all checked out work!
188
+ this.removeClient(idForLocalUnattachedClient);
189
+
190
+ const builder = new SummaryTreeBuilder();
191
+ let blobContent = this.serializeValue(this.data.asArray(), serializer);
192
+ builder.addBlob(snapshotFileNameData, blobContent);
193
+ blobContent = this.serializeValue(Array.from(this.jobTracking.entries()), serializer);
194
+ builder.addBlob(snapshotFileNameTracking, blobContent);
195
+ return builder.getSummaryTree();
196
+ }
197
+
198
+ protected isActive() {
199
+ return this.runtime.connected && this.runtime.deltaManager.active;
200
+ }
201
+
202
+ protected async complete(acquireId: string) {
203
+ if (!this.isAttached()) {
204
+ this.completeCore(acquireId);
205
+ return;
206
+ }
207
+
208
+ // if not active, this item already was released to queue (as observed by other clients)
209
+ if (this.isActive()) {
210
+ await this.submit<IConsensusOrderedCollectionCompleteOperation>({
211
+ opName: "complete",
212
+ acquireId,
213
+ });
214
+ }
215
+ }
216
+
217
+ protected completeCore(acquireId: string) {
218
+ // Note: item may be no longer in jobTracking and returned back to queue!
219
+ const rec = this.jobTracking.get(acquireId);
220
+ if (rec !== undefined) {
221
+ this.jobTracking.delete(acquireId);
222
+ this.emit("complete", rec.value);
223
+ }
224
+ }
225
+
226
+ protected release(acquireId: string) {
227
+ if (!this.isAttached()) {
228
+ this.releaseCore(acquireId);
229
+ return;
230
+ }
231
+
232
+ // if not active, this item already was released to queue (as observed by other clients)
233
+ if (this.isActive()) {
234
+ this.submit<IConsensusOrderedCollectionReleaseOperation>({
235
+ opName: "release",
236
+ acquireId,
237
+ }).catch((error) => {
238
+ this.runtime.logger.sendErrorEvent({ eventName: "ConsensusQueue_release" }, error);
239
+ });
240
+ }
241
+ }
242
+
243
+ protected releaseCore(acquireId: string) {
244
+ // Note: item may be no longer in jobTracking and returned back to queue!
245
+ const rec = this.jobTracking.get(acquireId);
246
+ if (rec !== undefined) {
247
+ this.jobTracking.delete(acquireId);
248
+ this.data.add(rec.value);
249
+ this.emit("add", rec.value, false /* newlyAdded */);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
255
+ */
256
+ protected async loadCore(storage: IChannelStorageService): Promise<void> {
257
+ assert(
258
+ this.jobTracking.size === 0,
259
+ 0x068 /* "On consensusOrderedCollection load, job tracking size > 0" */,
260
+ );
261
+ const blob = await storage.readBlob(snapshotFileNameTracking);
262
+ const rawContentTracking = bufferToString(blob, "utf8");
263
+ const content = this.deserializeValue(rawContentTracking, this.serializer);
264
+ this.jobTracking = new Map(content) as JobTrackingInfo<T>;
265
+
266
+ assert(
267
+ this.data.size() === 0,
268
+ 0x069 /* "On consensusOrderedCollection load, data size > 0" */,
269
+ );
270
+ const blob2 = await storage.readBlob(snapshotFileNameData);
271
+ const rawContentData = bufferToString(blob2, "utf8");
272
+ const content2 = this.deserializeValue(rawContentData, this.serializer) as T[];
273
+ this.data.loadFrom(content2);
274
+ }
275
+
276
+ protected onDisconnect() {
277
+ for (const [, { value, clientId }] of this.jobTracking) {
278
+ if (clientId === this.runtime.clientId) {
279
+ this.emit("localRelease", value, false /* intentional */);
280
+ }
281
+ }
282
+ }
283
+
284
+ protected processCore(
285
+ message: ISequencedDocumentMessage,
286
+ local: boolean,
287
+ localOpMetadata: unknown,
288
+ ) {
289
+ if (message.type === MessageType.Operation) {
290
+ const op: IConsensusOrderedCollectionOperation = message.contents;
291
+ let value: IConsensusOrderedCollectionValue<T> | undefined;
292
+ switch (op.opName) {
293
+ case "add":
294
+ this.addCore(this.deserializeValue(op.value, this.serializer) as T);
295
+ break;
296
+
297
+ case "acquire":
298
+ value = this.acquireCore(op.acquireId, message.clientId);
299
+ break;
300
+
301
+ case "complete":
302
+ this.completeCore(op.acquireId);
303
+ break;
304
+
305
+ case "release":
306
+ this.releaseCore(op.acquireId);
307
+ break;
308
+
309
+ default:
310
+ unreachableCase(op);
311
+ }
312
+ if (local) {
313
+ // Resolve the pending promise for this operation now that we have received an ack for it.
314
+ const resolve = localOpMetadata as PendingResolve<T>;
315
+ resolve(value);
316
+ }
317
+ }
318
+ }
319
+
320
+ private async submit<TMessage extends IConsensusOrderedCollectionOperation>(
321
+ message: TMessage,
322
+ ): Promise<IConsensusOrderedCollectionValue<T> | undefined> {
323
+ assert(this.isAttached(), 0x06a /* "Trying to submit message while detached!" */);
324
+
325
+ return this.newAckBasedPromise<IConsensusOrderedCollectionValue<T> | undefined>(
326
+ (resolve) => {
327
+ // Send the resolve function as the localOpMetadata. This will be provided back to us when the
328
+ // op is ack'd.
329
+ this.submitLocalMessage(message, resolve);
330
+ // If we fail due to runtime being disposed, it's better to return undefined then unhandled exception.
331
+ },
332
+ ).catch((error) => undefined);
333
+ }
334
+
335
+ private addCore(value: T) {
336
+ this.data.add(value);
337
+ this.emit("add", value, true /* newlyAdded */);
338
+ }
339
+
340
+ private acquireCore(
341
+ acquireId: string,
342
+ clientId?: string,
343
+ ): IConsensusOrderedCollectionValue<T> | undefined {
344
+ if (this.data.size() === 0) {
345
+ return undefined;
346
+ }
347
+ const value = this.data.remove();
348
+
349
+ const value2: IConsensusOrderedCollectionValue<T> = {
350
+ acquireId,
351
+ value,
352
+ };
353
+ this.jobTracking.set(value2.acquireId, { value, clientId });
354
+
355
+ this.emit("acquire", value, clientId);
356
+ return value2;
357
+ }
358
+
359
+ private async acquireInternal(): Promise<IConsensusOrderedCollectionValue<T> | undefined> {
360
+ if (!this.isAttached()) {
361
+ // can be undefined if queue is empty
362
+ return this.acquireCore(uuid(), idForLocalUnattachedClient);
363
+ }
364
+
365
+ return this.submit<IConsensusOrderedCollectionAcquireOperation>({
366
+ opName: "acquire",
367
+ acquireId: uuid(),
368
+ });
369
+ }
370
+
371
+ private removeClient(clientIdToRemove?: string) {
372
+ const added: T[] = [];
373
+ for (const [acquireId, { value, clientId }] of this.jobTracking) {
374
+ if (clientId === clientIdToRemove) {
375
+ this.jobTracking.delete(acquireId);
376
+ this.data.add(value);
377
+ added.push(value);
378
+ }
379
+ }
380
+
381
+ // Raise all events only after all state changes are completed,
382
+ // to guarantee same ordering of operations if collection is manipulated from events.
383
+ added.map((value) => this.emit("add", value, false /* newlyAdded */));
384
+ }
385
+
386
+ private serializeValue(value, serializer: IFluidSerializer) {
387
+ return serializer.stringify(value, this.handle);
388
+ }
389
+
390
+ private deserializeValue(content: string, serializer: IFluidSerializer) {
391
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
392
+ return serializer.parse(content);
393
+ }
394
+
395
+ protected applyStashedOp() {
396
+ throw new Error("not implemented");
397
+ }
379
398
  }