@fluidframework/map 2.0.0-internal.3.0.2 → 2.0.0-internal.3.2.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 (57) hide show
  1. package/.eslintrc.js +11 -16
  2. package/.mocharc.js +2 -2
  3. package/README.md +3 -3
  4. package/api-extractor.json +2 -2
  5. package/dist/directory.d.ts +1 -1
  6. package/dist/directory.d.ts.map +1 -1
  7. package/dist/directory.js +40 -18
  8. package/dist/directory.js.map +1 -1
  9. package/dist/interfaces.d.ts +12 -12
  10. package/dist/interfaces.d.ts.map +1 -1
  11. package/dist/interfaces.js.map +1 -1
  12. package/dist/internalInterfaces.d.ts.map +1 -1
  13. package/dist/internalInterfaces.js.map +1 -1
  14. package/dist/localValues.d.ts.map +1 -1
  15. package/dist/localValues.js.map +1 -1
  16. package/dist/map.d.ts +1 -1
  17. package/dist/map.d.ts.map +1 -1
  18. package/dist/map.js +4 -2
  19. package/dist/map.js.map +1 -1
  20. package/dist/mapKernel.d.ts.map +1 -1
  21. package/dist/mapKernel.js +33 -22
  22. package/dist/mapKernel.js.map +1 -1
  23. package/dist/packageVersion.d.ts +1 -1
  24. package/dist/packageVersion.js +1 -1
  25. package/dist/packageVersion.js.map +1 -1
  26. package/lib/directory.d.ts +1 -1
  27. package/lib/directory.d.ts.map +1 -1
  28. package/lib/directory.js +42 -20
  29. package/lib/directory.js.map +1 -1
  30. package/lib/interfaces.d.ts +12 -12
  31. package/lib/interfaces.d.ts.map +1 -1
  32. package/lib/interfaces.js.map +1 -1
  33. package/lib/internalInterfaces.d.ts.map +1 -1
  34. package/lib/internalInterfaces.js.map +1 -1
  35. package/lib/localValues.d.ts.map +1 -1
  36. package/lib/localValues.js.map +1 -1
  37. package/lib/map.d.ts +1 -1
  38. package/lib/map.d.ts.map +1 -1
  39. package/lib/map.js +5 -3
  40. package/lib/map.js.map +1 -1
  41. package/lib/mapKernel.d.ts.map +1 -1
  42. package/lib/mapKernel.js +34 -23
  43. package/lib/mapKernel.js.map +1 -1
  44. package/lib/packageVersion.d.ts +1 -1
  45. package/lib/packageVersion.js +1 -1
  46. package/lib/packageVersion.js.map +1 -1
  47. package/package.json +50 -50
  48. package/prettier.config.cjs +1 -1
  49. package/src/directory.ts +1952 -1875
  50. package/src/interfaces.ts +303 -306
  51. package/src/internalInterfaces.ts +67 -67
  52. package/src/localValues.ts +85 -94
  53. package/src/map.ts +363 -355
  54. package/src/mapKernel.ts +725 -690
  55. package/src/packageVersion.ts +1 -1
  56. package/tsconfig.esnext.json +5 -5
  57. package/tsconfig.json +9 -15
package/src/mapKernel.ts CHANGED
@@ -6,50 +6,38 @@
6
6
  import { IFluidHandle } from "@fluidframework/core-interfaces";
7
7
  import { IFluidSerializer, ValueType } from "@fluidframework/shared-object-base";
8
8
  import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
9
+ import { ISerializableValue, ISerializedValue, ISharedMapEvents } from "./interfaces";
9
10
  import {
10
- ISerializableValue,
11
- ISerializedValue,
12
- ISharedMapEvents,
13
- } from "./interfaces";
14
- import {
15
- IMapSetOperation,
16
- IMapDeleteOperation,
17
- IMapClearOperation,
18
- IMapKeyEditLocalOpMetadata,
19
- IMapKeyAddLocalOpMetadata,
20
- IMapClearLocalOpMetadata,
11
+ IMapSetOperation,
12
+ IMapDeleteOperation,
13
+ IMapClearOperation,
14
+ IMapKeyEditLocalOpMetadata,
15
+ IMapKeyAddLocalOpMetadata,
16
+ IMapClearLocalOpMetadata,
21
17
  } from "./internalInterfaces";
22
- import {
23
- ILocalValue,
24
- LocalValueMaker,
25
- makeSerializable,
26
- } from "./localValues";
18
+ import { ILocalValue, LocalValueMaker, makeSerializable } from "./localValues";
27
19
 
28
20
  /**
29
21
  * Defines the means to process and submit a given op on a map.
30
22
  */
31
23
  interface IMapMessageHandler {
32
- /**
33
- * Apply the given operation.
34
- * @param op - The map operation to apply
35
- * @param local - Whether the message originated from the local client
36
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
37
- * For messages from a remote client, this will be undefined.
38
- */
39
- process(
40
- op: IMapOperation,
41
- local: boolean,
42
- localOpMetadata: MapLocalOpMetadata,
43
- ): void;
44
-
45
- /**
46
- * Communicate the operation to remote clients.
47
- * @param op - The map operation to submit
48
- * @param localOpMetadata - The metadata to be submitted with the message.
49
- */
50
- submit(op: IMapOperation, localOpMetadata: MapLocalOpMetadata): void;
51
-
52
- applyStashedOp(op: IMapOperation): MapLocalOpMetadata;
24
+ /**
25
+ * Apply the given operation.
26
+ * @param op - The map operation to apply
27
+ * @param local - Whether the message originated from the local client
28
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
29
+ * For messages from a remote client, this will be undefined.
30
+ */
31
+ process(op: IMapOperation, local: boolean, localOpMetadata: MapLocalOpMetadata): void;
32
+
33
+ /**
34
+ * Communicate the operation to remote clients.
35
+ * @param op - The map operation to submit
36
+ * @param localOpMetadata - The metadata to be submitted with the message.
37
+ */
38
+ submit(op: IMapOperation, localOpMetadata: MapLocalOpMetadata): void;
39
+
40
+ applyStashedOp(op: IMapOperation): MapLocalOpMetadata;
53
41
  }
54
42
 
55
43
  /**
@@ -71,14 +59,14 @@ export type IMapOperation = IMapKeyOperation | IMapClearOperation;
71
59
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse | JSON.parse}.
72
60
  */
73
61
  export interface IMapDataObjectSerializable {
74
- [key: string]: ISerializableValue;
62
+ [key: string]: ISerializableValue;
75
63
  }
76
64
 
77
65
  /**
78
66
  * Serialized key/value data.
79
67
  */
80
68
  export interface IMapDataObjectSerialized {
81
- [key: string]: ISerializedValue;
69
+ [key: string]: ISerializedValue;
82
70
  }
83
71
 
84
72
  type MapKeyLocalOpMetadata = IMapKeyEditLocalOpMetadata | IMapKeyAddLocalOpMetadata;
@@ -87,673 +75,720 @@ type MapLocalOpMetadata = IMapClearLocalOpMetadata | MapKeyLocalOpMetadata;
87
75
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
88
76
 
89
77
  function isMapKeyLocalOpMetadata(metadata: any): metadata is MapKeyLocalOpMetadata {
90
- return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
91
- (metadata.type === "add" || metadata.type === "edit");
78
+ return (
79
+ metadata !== undefined &&
80
+ typeof metadata.pendingMessageId === "number" &&
81
+ (metadata.type === "add" || metadata.type === "edit")
82
+ );
92
83
  }
93
84
 
94
85
  function isClearLocalOpMetadata(metadata: any): metadata is IMapClearLocalOpMetadata {
95
- return metadata !== undefined && metadata.type === "clear" && typeof metadata.pendingMessageId === "number";
86
+ return (
87
+ metadata !== undefined &&
88
+ metadata.type === "clear" &&
89
+ typeof metadata.pendingMessageId === "number"
90
+ );
96
91
  }
97
92
 
98
93
  function isMapLocalOpMetadata(metadata: any): metadata is MapLocalOpMetadata {
99
- return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
100
- (metadata.type === "add" || metadata.type === "edit" || metadata.type === "clear");
94
+ return (
95
+ metadata !== undefined &&
96
+ typeof metadata.pendingMessageId === "number" &&
97
+ (metadata.type === "add" || metadata.type === "edit" || metadata.type === "clear")
98
+ );
101
99
  }
102
100
 
103
101
  /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
104
102
 
105
- function createClearLocalOpMetadata(op: IMapClearOperation,
106
- pendingClearMessageId: number, previousMap?: Map<string, ILocalValue>): IMapClearLocalOpMetadata {
107
- const localMetadata: IMapClearLocalOpMetadata = {
108
- type: "clear",
109
- pendingMessageId: pendingClearMessageId, previousMap,
110
- };
111
- return localMetadata;
103
+ function createClearLocalOpMetadata(
104
+ op: IMapClearOperation,
105
+ pendingClearMessageId: number,
106
+ previousMap?: Map<string, ILocalValue>,
107
+ ): IMapClearLocalOpMetadata {
108
+ const localMetadata: IMapClearLocalOpMetadata = {
109
+ type: "clear",
110
+ pendingMessageId: pendingClearMessageId,
111
+ previousMap,
112
+ };
113
+ return localMetadata;
112
114
  }
113
115
 
114
- function createKeyLocalOpMetadata(op: IMapKeyOperation,
115
- pendingMessageId: number, previousValue?: ILocalValue): MapKeyLocalOpMetadata {
116
- const localMetadata: MapKeyLocalOpMetadata = previousValue ?
117
- { type: "edit", pendingMessageId, previousValue } :
118
- { type: "add", pendingMessageId };
119
- return localMetadata;
116
+ function createKeyLocalOpMetadata(
117
+ op: IMapKeyOperation,
118
+ pendingMessageId: number,
119
+ previousValue?: ILocalValue,
120
+ ): MapKeyLocalOpMetadata {
121
+ const localMetadata: MapKeyLocalOpMetadata = previousValue
122
+ ? { type: "edit", pendingMessageId, previousValue }
123
+ : { type: "add", pendingMessageId };
124
+ return localMetadata;
120
125
  }
121
126
 
122
127
  /**
123
128
  * A SharedMap is a map-like distributed data structure.
124
129
  */
125
130
  export class MapKernel {
126
- /**
127
- * The number of key/value pairs stored in the map.
128
- */
129
- public get size(): number {
130
- return this.data.size;
131
- }
132
-
133
- /**
134
- * Mapping of op types to message handlers.
135
- */
136
- private readonly messageHandlers: ReadonlyMap<string, IMapMessageHandler> = new Map();
137
-
138
- /**
139
- * The in-memory data the map is storing.
140
- */
141
- private readonly data = new Map<string, ILocalValue>();
142
-
143
- /**
144
- * Keys that have been modified locally but not yet ack'd from the server.
145
- */
146
- private readonly pendingKeys: Map<string, number[]> = new Map();
147
-
148
- /**
149
- * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
150
- */
151
- private pendingMessageId: number = -1;
152
-
153
- /**
154
- * The pending ids of any clears that have been performed locally but not yet ack'd from the server
155
- */
156
- private readonly pendingClearMessageIds: number[] = [];
157
-
158
- /**
159
- * Object to create encapsulations of the values stored in the map.
160
- */
161
- private readonly localValueMaker: LocalValueMaker;
162
-
163
- /**
164
- * Create a new shared map kernel.
165
- * @param serializer - The serializer to serialize / parse handles
166
- * @param handle - The handle of the shared object using the kernel
167
- * @param submitMessage - A callback to submit a message through the shared object
168
- * @param isAttached - To query whether the shared object should generate ops
169
- * @param valueTypes - The value types to register
170
- * @param eventEmitter - The object that will emit map events
171
- */
172
- public constructor(
173
- private readonly serializer: IFluidSerializer,
174
- private readonly handle: IFluidHandle,
175
- private readonly submitMessage: (op: unknown, localOpMetadata: unknown) => void,
176
- private readonly isAttached: () => boolean,
177
- private readonly eventEmitter: TypedEventEmitter<ISharedMapEvents>,
178
- ) {
179
- this.localValueMaker = new LocalValueMaker(serializer);
180
- this.messageHandlers = this.getMessageHandlers();
181
- }
182
-
183
- /**
184
- * Get an iterator over the keys in this map.
185
- * @returns The iterator
186
- */
187
- public keys(): IterableIterator<string> {
188
- return this.data.keys();
189
- }
190
-
191
- /**
192
- * Get an iterator over the entries in this map.
193
- * @returns The iterator
194
- */
195
- // TODO: Use `unknown` instead (breaking change).
196
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
197
- public entries(): IterableIterator<[string, any]> {
198
- const localEntriesIterator = this.data.entries();
199
- const iterator = {
200
- next(): IteratorResult<[string, unknown]> {
201
- const nextVal = localEntriesIterator.next();
202
- return nextVal.done
203
- ? { value: undefined, done: true }
204
- // Unpack the stored value
205
- : { value: [nextVal.value[0], nextVal.value[1].value], done: false };
206
- },
207
- [Symbol.iterator](): IterableIterator<[string, unknown]> {
208
- return this;
209
- },
210
- };
211
- return iterator;
212
- }
213
-
214
- /**
215
- * Get an iterator over the values in this map.
216
- * @returns The iterator
217
- */
218
- // TODO: Use `unknown` instead (breaking change).
219
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
- public values(): IterableIterator<any> {
221
- const localValuesIterator = this.data.values();
222
- const iterator = {
223
- next(): IteratorResult<unknown> {
224
- const nextVal = localValuesIterator.next();
225
- return nextVal.done
226
- ? { value: undefined, done: true }
227
- // Unpack the stored value
228
- : { value: nextVal.value.value as unknown, done: false };
229
- },
230
- [Symbol.iterator](): IterableIterator<unknown> {
231
- return this;
232
- },
233
- };
234
- return iterator;
235
- }
236
-
237
- /**
238
- * Get an iterator over the entries in this map.
239
- * @returns The iterator
240
- */
241
- // TODO: Use `unknown` instead (breaking change).
242
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
243
- public [Symbol.iterator](): IterableIterator<[string, any]> {
244
- return this.entries();
245
- }
246
-
247
- /**
248
- * Executes the given callback on each entry in the map.
249
- * @param callbackFn - Callback function
250
- */
251
- public forEach(callbackFn: (value: unknown, key: string, map: Map<string, unknown>) => void): void {
252
- // eslint-disable-next-line unicorn/no-array-for-each
253
- this.data.forEach((localValue, key, m) => {
254
- callbackFn(localValue.value, key, m);
255
- });
256
- }
257
-
258
- /**
259
- * {@inheritDoc ISharedMap.get}
260
- */
261
- // TODO: Use `unknown` instead (breaking change).
262
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
- public get<T = any>(key: string): T | undefined {
264
- const localValue = this.data.get(key);
265
- return localValue === undefined ? undefined : localValue.value as T;
266
- }
267
-
268
- /**
269
- * Check if a key exists in the map.
270
- * @param key - The key to check
271
- * @returns True if the key exists, false otherwise
272
- */
273
- public has(key: string): boolean {
274
- return this.data.has(key);
275
- }
276
-
277
- /**
278
- * {@inheritDoc ISharedMap.set}
279
- */
280
- public set(key: string, value: unknown): void {
281
- // Undefined/null keys can't be serialized to JSON in the manner we currently snapshot.
282
- if (key === undefined || key === null) {
283
- throw new Error("Undefined and null keys are not supported");
284
- }
285
-
286
- // Create a local value and serialize it.
287
- const localValue = this.localValueMaker.fromInMemory(value);
288
- const serializableValue = makeSerializable(
289
- localValue,
290
- this.serializer,
291
- this.handle);
292
-
293
- // Set the value locally.
294
- const previousValue = this.setCore(
295
- key,
296
- localValue,
297
- true,
298
- );
299
-
300
- // If we are not attached, don't submit the op.
301
- if (!this.isAttached()) {
302
- return;
303
- }
304
-
305
- const op: IMapSetOperation = {
306
- key,
307
- type: "set",
308
- value: serializableValue,
309
- };
310
- this.submitMapKeyMessage(op, previousValue);
311
- }
312
-
313
- /**
314
- * Delete a key from the map.
315
- * @param key - Key to delete
316
- * @returns True if the key existed and was deleted, false if it did not exist
317
- */
318
- public delete(key: string): boolean {
319
- // Delete the key locally first.
320
- const previousValue = this.deleteCore(key, true);
321
-
322
- // If we are not attached, don't submit the op.
323
- if (!this.isAttached()) {
324
- return previousValue !== undefined;
325
- }
326
-
327
- const op: IMapDeleteOperation = {
328
- key,
329
- type: "delete",
330
- };
331
- this.submitMapKeyMessage(op, previousValue);
332
-
333
- return previousValue !== undefined;
334
- }
335
-
336
- /**
337
- * Clear all data from the map.
338
- */
339
- public clear(): void {
340
- const copy = this.isAttached() ? new Map<string, ILocalValue>(this.data) : undefined;
341
-
342
- // Clear the data locally first.
343
- this.clearCore(true);
344
-
345
- // If we are not attached, don't submit the op.
346
- if (!this.isAttached()) {
347
- return;
348
- }
349
-
350
- const op: IMapClearOperation = {
351
- type: "clear",
352
- };
353
- this.submitMapClearMessage(op, copy);
354
- }
355
-
356
- /**
357
- * Serializes the data stored in the shared map to a JSON string
358
- * @param serializer - The serializer to use to serialize handles in its values.
359
- * @returns A JSON string containing serialized map data
360
- */
361
- public getSerializedStorage(serializer: IFluidSerializer): IMapDataObjectSerialized {
362
- const serializableMapData: IMapDataObjectSerialized = {};
363
- for (const [key, localValue] of this.data.entries()) {
364
- serializableMapData[key] = localValue.makeSerialized(serializer, this.handle);
365
- }
366
- return serializableMapData;
367
- }
368
-
369
- public getSerializableStorage(serializer: IFluidSerializer): IMapDataObjectSerializable {
370
- const serializableMapData: IMapDataObjectSerializable = {};
371
- for (const [key, localValue] of this.data.entries()) {
372
- serializableMapData[key] = makeSerializable(localValue, serializer, this.handle);
373
- }
374
- return serializableMapData;
375
- }
376
-
377
- public serialize(serializer: IFluidSerializer): string {
378
- return JSON.stringify(this.getSerializableStorage(serializer));
379
- }
380
-
381
- /**
382
- * Populate the kernel with the given map data.
383
- * @param data - A JSON string containing serialized map data
384
- */
385
- public populateFromSerializable(json: IMapDataObjectSerializable): void {
386
- for (const [key, serializable] of Object.entries(json)) {
387
- const localValue = {
388
- key,
389
- value: this.makeLocal(key, serializable),
390
- };
391
-
392
- this.data.set(localValue.key, localValue.value);
393
- }
394
- }
395
-
396
- public populate(json: string): void {
397
- this.populateFromSerializable(JSON.parse(json) as IMapDataObjectSerializable);
398
- }
399
-
400
- /**
401
- * Submit the given op if a handler is registered.
402
- * @param op - The operation to attempt to submit
403
- * @param localOpMetadata - The local metadata associated with the op. This is kept locally by the runtime
404
- * and not sent to the server. This will be sent back when this message is received back from the server. This is
405
- * also sent if we are asked to resubmit the message.
406
- * @returns True if the operation was submitted, false otherwise.
407
- */
408
- public trySubmitMessage(op: IMapOperation, localOpMetadata: unknown): boolean {
409
- const handler = this.messageHandlers.get(op.type);
410
- if (handler === undefined) {
411
- return false;
412
- }
413
- handler.submit(op, localOpMetadata as MapLocalOpMetadata);
414
- return true;
415
- }
416
-
417
- public tryApplyStashedOp(op: IMapOperation): unknown {
418
- const handler = this.messageHandlers.get(op.type);
419
- if (handler === undefined) {
420
- throw new Error("no apply stashed op handler");
421
- }
422
- return handler.applyStashedOp(op);
423
- }
424
-
425
- /**
426
- * Process the given op if a handler is registered.
427
- * @param op - The message to process
428
- * @param local - Whether the message originated from the local client
429
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
430
- * For messages from a remote client, this will be undefined.
431
- * @returns True if the operation was processed, false otherwise.
432
- */
433
- public tryProcessMessage(
434
- op: IMapOperation,
435
- local: boolean,
436
- localOpMetadata: unknown,
437
- ): boolean {
438
- const handler = this.messageHandlers.get(op.type);
439
- if (handler === undefined) {
440
- return false;
441
- }
442
- handler.process(op, local, localOpMetadata as MapLocalOpMetadata);
443
- return true;
444
- }
445
-
446
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
447
-
448
- /**
449
- * Rollback a local op
450
- * @param op - The operation to rollback
451
- * @param localOpMetadata - The local metadata associated with the op.
452
- */
453
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
454
- public rollback(op: any, localOpMetadata: unknown): void {
455
- if (!isMapLocalOpMetadata(localOpMetadata)) {
456
- throw new Error("Invalid localOpMetadata");
457
- }
458
-
459
- if (op.type === "clear" && localOpMetadata.type === "clear") {
460
- if (localOpMetadata.previousMap === undefined) {
461
- throw new Error("Cannot rollback without previous map");
462
- }
463
- for (const [key, localValue] of localOpMetadata.previousMap.entries()) {
464
- this.setCore(key, localValue, true);
465
- }
466
-
467
- const lastPendingClearId = this.pendingClearMessageIds.pop();
468
- if (lastPendingClearId === undefined || lastPendingClearId !== localOpMetadata.pendingMessageId) {
469
- throw new Error("Rollback op does match last clear");
470
- }
471
- } else if (op.type === "delete" || op.type === "set") {
472
- if (localOpMetadata.type === "add") {
473
- this.deleteCore(op.key as string, true);
474
- } else if (localOpMetadata.type === "edit" && localOpMetadata.previousValue !== undefined) {
475
- this.setCore(op.key as string, localOpMetadata.previousValue, true);
476
- } else {
477
- throw new Error("Cannot rollback without previous value");
478
- }
479
-
480
- const pendingMessageIds = this.pendingKeys.get(op.key as string);
481
- const lastPendingMessageId = pendingMessageIds?.pop();
482
- if (!pendingMessageIds || lastPendingMessageId !== localOpMetadata.pendingMessageId) {
483
- throw new Error("Rollback op does not match last pending");
484
- }
485
- if (pendingMessageIds.length === 0) {
486
- this.pendingKeys.delete(op.key as string);
487
- }
488
- } else {
489
- throw new Error("Unsupported op for rollback");
490
- }
491
- }
492
-
493
- /* eslint-enable @typescript-eslint/no-unsafe-member-access */
494
-
495
- /**
496
- * Set implementation used for both locally sourced sets as well as incoming remote sets.
497
- * @param key - The key being set
498
- * @param value - The value being set
499
- * @param local - Whether the message originated from the local client
500
- * @returns Previous local value of the key, if any
501
- */
502
- private setCore(key: string, value: ILocalValue, local: boolean): ILocalValue | undefined {
503
- const previousLocalValue = this.data.get(key);
504
- const previousValue: unknown = previousLocalValue?.value;
505
- this.data.set(key, value);
506
- this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
507
- return previousLocalValue;
508
- }
509
-
510
- /**
511
- * Clear implementation used for both locally sourced clears as well as incoming remote clears.
512
- * @param local - Whether the message originated from the local client
513
- */
514
- private clearCore(local: boolean): void {
515
- this.data.clear();
516
- this.eventEmitter.emit("clear", local, this.eventEmitter);
517
- }
518
-
519
- /**
520
- * Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
521
- * @param key - The key being deleted
522
- * @param local - Whether the message originated from the local client
523
- * @returns Previous local value of the key if it existed, undefined if it did not exist
524
- */
525
- private deleteCore(key: string, local: boolean): ILocalValue | undefined {
526
- const previousLocalValue = this.data.get(key);
527
- const previousValue: unknown = previousLocalValue?.value;
528
- const successfullyRemoved = this.data.delete(key);
529
- if (successfullyRemoved) {
530
- this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
531
- }
532
- return previousLocalValue;
533
- }
534
-
535
- /**
536
- * Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
537
- */
538
- private clearExceptPendingKeys(): void {
539
- // Assuming the pendingKeys is small and the map is large
540
- // we will get the value for the pendingKeys and clear the map
541
- const temp = new Map<string, ILocalValue>();
542
- for (const key of this.pendingKeys.keys()) {
543
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
544
- temp.set(key, this.data.get(key)!);
545
- }
546
- this.clearCore(false);
547
- for (const [key, value] of temp.entries()) {
548
- this.setCore(key, value, true);
549
- }
550
- }
551
-
552
- /**
553
- * The remote ISerializableValue we're receiving (either as a result of a load or an incoming set op) will
554
- * have the information we need to create a real object, but will not be the real object yet. For example,
555
- * we might know it's a map and the map's ID but not have the actual map or its data yet. makeLocal's
556
- * job is to convert that information into a real object for local usage.
557
- * @param key - The key that the caller intends to store the local value into (used for ops later). But
558
- * doesn't actually store the local value into that key. So better not lie!
559
- * @param serializable - The remote information that we can convert into a real object
560
- * @returns The local value that was produced
561
- */
562
- private makeLocal(key: string, serializable: ISerializableValue): ILocalValue {
563
- if (serializable.type === ValueType[ValueType.Plain] || serializable.type === ValueType[ValueType.Shared]) {
564
- return this.localValueMaker.fromSerializable(serializable);
565
- } else {
566
- throw new Error("Unknown local value type");
567
- }
568
- }
569
-
570
- /**
571
- * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
572
- * not process the incoming operation.
573
- * @param op - Operation to check
574
- * @param local - Whether the message originated from the local client
575
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
576
- * For messages from a remote client, this will be undefined.
577
- * @returns True if the operation should be processed, false otherwise
578
- */
579
- private needProcessKeyOperation(
580
- op: IMapKeyOperation,
581
- local: boolean,
582
- localOpMetadata: MapLocalOpMetadata,
583
- ): boolean {
584
- if (this.pendingClearMessageIds.length > 0) {
585
- if (local) {
586
- assert(localOpMetadata !== undefined && isMapKeyLocalOpMetadata(localOpMetadata) &&
587
- localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
588
- 0x013 /* "Received out of order op when there is an unackd clear message" */);
589
- }
590
- // If we have an unack'd clear, we can ignore all ops.
591
- return false;
592
- }
593
-
594
- const pendingKeyMessageId = this.pendingKeys.get(op.key);
595
- if (pendingKeyMessageId !== undefined) {
596
- // Found an unack'd op. Clear it from the map if the pendingMessageId in the map matches this message's
597
- // and don't process the op.
598
- if (local) {
599
- assert(localOpMetadata !== undefined && isMapKeyLocalOpMetadata(localOpMetadata),
600
- 0x014 /* pendingMessageId is missing from the local client's operation */);
601
- const pendingMessageIds = this.pendingKeys.get(op.key);
602
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
603
- 0x2fa /* Unexpected pending message received */);
604
- pendingMessageIds.shift();
605
- if (pendingMessageIds.length === 0) {
606
- this.pendingKeys.delete(op.key);
607
- }
608
- }
609
- return false;
610
- }
611
-
612
- // If we don't have a NACK op on the key, we need to process the remote ops.
613
- return !local;
614
- }
615
-
616
- /**
617
- * Get the message handlers for the map.
618
- * @returns A map of string op names to IMapMessageHandlers for those ops
619
- */
620
- private getMessageHandlers(): Map<string, IMapMessageHandler> {
621
- const messageHandlers = new Map<string, IMapMessageHandler>();
622
- messageHandlers.set(
623
- "clear",
624
- {
625
- process: (op: IMapClearOperation, local, localOpMetadata) => {
626
- if (local) {
627
- assert(isClearLocalOpMetadata(localOpMetadata),
628
- 0x015 /* "pendingMessageId is missing from the local client's clear operation" */);
629
- const pendingClearMessageId = this.pendingClearMessageIds.shift();
630
- assert(pendingClearMessageId === localOpMetadata.pendingMessageId,
631
- 0x2fb /* pendingMessageId does not match */);
632
- return;
633
- }
634
- if (this.pendingKeys.size > 0) {
635
- this.clearExceptPendingKeys();
636
- return;
637
- }
638
- this.clearCore(local);
639
- },
640
- submit: (op: IMapClearOperation, localOpMetadata: IMapClearLocalOpMetadata) => {
641
- assert(isClearLocalOpMetadata(localOpMetadata), 0x2fc /* Invalid localOpMetadata for clear */);
642
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
643
- const pendingClearMessageId = this.pendingClearMessageIds.shift();
644
- assert(pendingClearMessageId === localOpMetadata.pendingMessageId,
645
- 0x2fd /* pendingMessageId does not match */);
646
- this.submitMapClearMessage(op, localOpMetadata.previousMap);
647
- },
648
- applyStashedOp: (op: IMapClearOperation) => {
649
- const copy = new Map<string, ILocalValue>(this.data);
650
- this.clearCore(true);
651
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
652
- return createClearLocalOpMetadata(op, this.getMapClearMessageId(), copy);
653
- },
654
- });
655
- messageHandlers.set(
656
- "delete",
657
- {
658
- process: (op: IMapDeleteOperation, local, localOpMetadata) => {
659
- if (!this.needProcessKeyOperation(op, local, localOpMetadata)) {
660
- return;
661
- }
662
- this.deleteCore(op.key, local);
663
- },
664
- submit: (op: IMapDeleteOperation, localOpMetadata: MapKeyLocalOpMetadata) => {
665
- this.resubmitMapKeyMessage(op, localOpMetadata);
666
- },
667
- applyStashedOp: (op: IMapDeleteOperation) => {
668
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
669
- const previousValue = this.deleteCore(op.key, true);
670
- return createKeyLocalOpMetadata(op, this.getMapKeyMessageId(op), previousValue);
671
- },
672
- });
673
- messageHandlers.set(
674
- "set",
675
- {
676
- process: (op: IMapSetOperation, local, localOpMetadata) => {
677
- if (!this.needProcessKeyOperation(op, local, localOpMetadata)) {
678
- return;
679
- }
680
-
681
- // needProcessKeyOperation should have returned false if local is true
682
- const context = this.makeLocal(op.key, op.value);
683
- this.setCore(op.key, context, local);
684
- },
685
- submit: (op: IMapSetOperation, localOpMetadata: MapKeyLocalOpMetadata) => {
686
- this.resubmitMapKeyMessage(op, localOpMetadata);
687
- },
688
- applyStashedOp: (op: IMapSetOperation) => {
689
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
690
- const context = this.makeLocal(op.key, op.value);
691
- const previousValue = this.setCore(op.key, context, true);
692
- return createKeyLocalOpMetadata(op, this.getMapKeyMessageId(op), previousValue);
693
- },
694
- });
695
-
696
- return messageHandlers;
697
- }
698
-
699
- private getMapClearMessageId(): number {
700
- const pendingMessageId = ++this.pendingMessageId;
701
- this.pendingClearMessageIds.push(pendingMessageId);
702
- return pendingMessageId;
703
- }
704
-
705
- /**
706
- * Submit a clear message to remote clients.
707
- * @param op - The clear message
708
- */
709
- private submitMapClearMessage(op: IMapClearOperation, previousMap?: Map<string, ILocalValue>): void {
710
- const metadata = createClearLocalOpMetadata(op, this.getMapClearMessageId(), previousMap);
711
- this.submitMessage(op, metadata);
712
- }
713
-
714
- private getMapKeyMessageId(op: IMapKeyOperation): number {
715
- const pendingMessageId = ++this.pendingMessageId;
716
- const pendingMessageIds = this.pendingKeys.get(op.key);
717
- if (pendingMessageIds !== undefined) {
718
- pendingMessageIds.push(pendingMessageId);
719
- } else {
720
- this.pendingKeys.set(op.key, [pendingMessageId]);
721
- }
722
- return pendingMessageId;
723
- }
724
-
725
- /**
726
- * Submit a map key message to remote clients.
727
- * @param op - The map key message
728
- * @param previousValue - The value of the key before this op
729
- */
730
- private submitMapKeyMessage(op: IMapKeyOperation, previousValue?: ILocalValue): void {
731
- const localMetadata = createKeyLocalOpMetadata(op, this.getMapKeyMessageId(op), previousValue);
732
- this.submitMessage(op, localMetadata);
733
- }
734
-
735
- /**
736
- * Submit a map key message to remote clients based on a previous submit.
737
- * @param op - The map key message
738
- * @param localOpMetadata - Metadata from the previous submit
739
- */
740
- private resubmitMapKeyMessage(op: IMapKeyOperation, localOpMetadata: MapLocalOpMetadata): void {
741
- assert(isMapKeyLocalOpMetadata(localOpMetadata), 0x2fe /* Invalid localOpMetadata in submit */);
742
-
743
- // clear the old pending message id
744
- const pendingMessageIds = this.pendingKeys.get(op.key);
745
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
746
- 0x2ff /* Unexpected pending message received */);
747
- pendingMessageIds.shift();
748
- if (pendingMessageIds.length === 0) {
749
- this.pendingKeys.delete(op.key);
750
- }
751
-
752
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
753
- const pendingMessageId = this.getMapKeyMessageId(op);
754
- const localMetadata = localOpMetadata.type === "edit" ?
755
- { type: "edit", pendingMessageId, previousValue: localOpMetadata.previousValue } :
756
- { type: "add", pendingMessageId };
757
- this.submitMessage(op, localMetadata);
758
- }
131
+ /**
132
+ * The number of key/value pairs stored in the map.
133
+ */
134
+ public get size(): number {
135
+ return this.data.size;
136
+ }
137
+
138
+ /**
139
+ * Mapping of op types to message handlers.
140
+ */
141
+ private readonly messageHandlers: ReadonlyMap<string, IMapMessageHandler> = new Map();
142
+
143
+ /**
144
+ * The in-memory data the map is storing.
145
+ */
146
+ private readonly data = new Map<string, ILocalValue>();
147
+
148
+ /**
149
+ * Keys that have been modified locally but not yet ack'd from the server.
150
+ */
151
+ private readonly pendingKeys: Map<string, number[]> = new Map();
152
+
153
+ /**
154
+ * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
155
+ */
156
+ private pendingMessageId: number = -1;
157
+
158
+ /**
159
+ * The pending ids of any clears that have been performed locally but not yet ack'd from the server
160
+ */
161
+ private readonly pendingClearMessageIds: number[] = [];
162
+
163
+ /**
164
+ * Object to create encapsulations of the values stored in the map.
165
+ */
166
+ private readonly localValueMaker: LocalValueMaker;
167
+
168
+ /**
169
+ * Create a new shared map kernel.
170
+ * @param serializer - The serializer to serialize / parse handles
171
+ * @param handle - The handle of the shared object using the kernel
172
+ * @param submitMessage - A callback to submit a message through the shared object
173
+ * @param isAttached - To query whether the shared object should generate ops
174
+ * @param valueTypes - The value types to register
175
+ * @param eventEmitter - The object that will emit map events
176
+ */
177
+ public constructor(
178
+ private readonly serializer: IFluidSerializer,
179
+ private readonly handle: IFluidHandle,
180
+ private readonly submitMessage: (op: unknown, localOpMetadata: unknown) => void,
181
+ private readonly isAttached: () => boolean,
182
+ private readonly eventEmitter: TypedEventEmitter<ISharedMapEvents>,
183
+ ) {
184
+ this.localValueMaker = new LocalValueMaker(serializer);
185
+ this.messageHandlers = this.getMessageHandlers();
186
+ }
187
+
188
+ /**
189
+ * Get an iterator over the keys in this map.
190
+ * @returns The iterator
191
+ */
192
+ public keys(): IterableIterator<string> {
193
+ return this.data.keys();
194
+ }
195
+
196
+ /**
197
+ * Get an iterator over the entries in this map.
198
+ * @returns The iterator
199
+ */
200
+ // TODO: Use `unknown` instead (breaking change).
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ public entries(): IterableIterator<[string, any]> {
203
+ const localEntriesIterator = this.data.entries();
204
+ const iterator = {
205
+ next(): IteratorResult<[string, unknown]> {
206
+ const nextVal = localEntriesIterator.next();
207
+ return nextVal.done
208
+ ? { value: undefined, done: true }
209
+ : // Unpack the stored value
210
+ { value: [nextVal.value[0], nextVal.value[1].value], done: false };
211
+ },
212
+ [Symbol.iterator](): IterableIterator<[string, unknown]> {
213
+ return this;
214
+ },
215
+ };
216
+ return iterator;
217
+ }
218
+
219
+ /**
220
+ * Get an iterator over the values in this map.
221
+ * @returns The iterator
222
+ */
223
+ // TODO: Use `unknown` instead (breaking change).
224
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
+ public values(): IterableIterator<any> {
226
+ const localValuesIterator = this.data.values();
227
+ const iterator = {
228
+ next(): IteratorResult<unknown> {
229
+ const nextVal = localValuesIterator.next();
230
+ return nextVal.done
231
+ ? { value: undefined, done: true }
232
+ : // Unpack the stored value
233
+ { value: nextVal.value.value as unknown, done: false };
234
+ },
235
+ [Symbol.iterator](): IterableIterator<unknown> {
236
+ return this;
237
+ },
238
+ };
239
+ return iterator;
240
+ }
241
+
242
+ /**
243
+ * Get an iterator over the entries in this map.
244
+ * @returns The iterator
245
+ */
246
+ // TODO: Use `unknown` instead (breaking change).
247
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
248
+ public [Symbol.iterator](): IterableIterator<[string, any]> {
249
+ return this.entries();
250
+ }
251
+
252
+ /**
253
+ * Executes the given callback on each entry in the map.
254
+ * @param callbackFn - Callback function
255
+ */
256
+ public forEach(
257
+ callbackFn: (value: unknown, key: string, map: Map<string, unknown>) => void,
258
+ ): void {
259
+ // eslint-disable-next-line unicorn/no-array-for-each
260
+ this.data.forEach((localValue, key, m) => {
261
+ callbackFn(localValue.value, key, m);
262
+ });
263
+ }
264
+
265
+ /**
266
+ * {@inheritDoc ISharedMap.get}
267
+ */
268
+ // TODO: Use `unknown` instead (breaking change).
269
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
270
+ public get<T = any>(key: string): T | undefined {
271
+ const localValue = this.data.get(key);
272
+ return localValue === undefined ? undefined : (localValue.value as T);
273
+ }
274
+
275
+ /**
276
+ * Check if a key exists in the map.
277
+ * @param key - The key to check
278
+ * @returns True if the key exists, false otherwise
279
+ */
280
+ public has(key: string): boolean {
281
+ return this.data.has(key);
282
+ }
283
+
284
+ /**
285
+ * {@inheritDoc ISharedMap.set}
286
+ */
287
+ public set(key: string, value: unknown): void {
288
+ // Undefined/null keys can't be serialized to JSON in the manner we currently snapshot.
289
+ if (key === undefined || key === null) {
290
+ throw new Error("Undefined and null keys are not supported");
291
+ }
292
+
293
+ // Create a local value and serialize it.
294
+ const localValue = this.localValueMaker.fromInMemory(value);
295
+ const serializableValue = makeSerializable(localValue, this.serializer, this.handle);
296
+
297
+ // Set the value locally.
298
+ const previousValue = this.setCore(key, localValue, true);
299
+
300
+ // If we are not attached, don't submit the op.
301
+ if (!this.isAttached()) {
302
+ return;
303
+ }
304
+
305
+ const op: IMapSetOperation = {
306
+ key,
307
+ type: "set",
308
+ value: serializableValue,
309
+ };
310
+ this.submitMapKeyMessage(op, previousValue);
311
+ }
312
+
313
+ /**
314
+ * Delete a key from the map.
315
+ * @param key - Key to delete
316
+ * @returns True if the key existed and was deleted, false if it did not exist
317
+ */
318
+ public delete(key: string): boolean {
319
+ // Delete the key locally first.
320
+ const previousValue = this.deleteCore(key, true);
321
+
322
+ // If we are not attached, don't submit the op.
323
+ if (!this.isAttached()) {
324
+ return previousValue !== undefined;
325
+ }
326
+
327
+ const op: IMapDeleteOperation = {
328
+ key,
329
+ type: "delete",
330
+ };
331
+ this.submitMapKeyMessage(op, previousValue);
332
+
333
+ return previousValue !== undefined;
334
+ }
335
+
336
+ /**
337
+ * Clear all data from the map.
338
+ */
339
+ public clear(): void {
340
+ const copy = this.isAttached() ? new Map<string, ILocalValue>(this.data) : undefined;
341
+
342
+ // Clear the data locally first.
343
+ this.clearCore(true);
344
+
345
+ // If we are not attached, don't submit the op.
346
+ if (!this.isAttached()) {
347
+ return;
348
+ }
349
+
350
+ const op: IMapClearOperation = {
351
+ type: "clear",
352
+ };
353
+ this.submitMapClearMessage(op, copy);
354
+ }
355
+
356
+ /**
357
+ * Serializes the data stored in the shared map to a JSON string
358
+ * @param serializer - The serializer to use to serialize handles in its values.
359
+ * @returns A JSON string containing serialized map data
360
+ */
361
+ public getSerializedStorage(serializer: IFluidSerializer): IMapDataObjectSerialized {
362
+ const serializableMapData: IMapDataObjectSerialized = {};
363
+ for (const [key, localValue] of this.data.entries()) {
364
+ serializableMapData[key] = localValue.makeSerialized(serializer, this.handle);
365
+ }
366
+ return serializableMapData;
367
+ }
368
+
369
+ public getSerializableStorage(serializer: IFluidSerializer): IMapDataObjectSerializable {
370
+ const serializableMapData: IMapDataObjectSerializable = {};
371
+ for (const [key, localValue] of this.data.entries()) {
372
+ serializableMapData[key] = makeSerializable(localValue, serializer, this.handle);
373
+ }
374
+ return serializableMapData;
375
+ }
376
+
377
+ public serialize(serializer: IFluidSerializer): string {
378
+ return JSON.stringify(this.getSerializableStorage(serializer));
379
+ }
380
+
381
+ /**
382
+ * Populate the kernel with the given map data.
383
+ * @param data - A JSON string containing serialized map data
384
+ */
385
+ public populateFromSerializable(json: IMapDataObjectSerializable): void {
386
+ for (const [key, serializable] of Object.entries(json)) {
387
+ const localValue = {
388
+ key,
389
+ value: this.makeLocal(key, serializable),
390
+ };
391
+
392
+ this.data.set(localValue.key, localValue.value);
393
+ }
394
+ }
395
+
396
+ public populate(json: string): void {
397
+ this.populateFromSerializable(JSON.parse(json) as IMapDataObjectSerializable);
398
+ }
399
+
400
+ /**
401
+ * Submit the given op if a handler is registered.
402
+ * @param op - The operation to attempt to submit
403
+ * @param localOpMetadata - The local metadata associated with the op. This is kept locally by the runtime
404
+ * and not sent to the server. This will be sent back when this message is received back from the server. This is
405
+ * also sent if we are asked to resubmit the message.
406
+ * @returns True if the operation was submitted, false otherwise.
407
+ */
408
+ public trySubmitMessage(op: IMapOperation, localOpMetadata: unknown): boolean {
409
+ const handler = this.messageHandlers.get(op.type);
410
+ if (handler === undefined) {
411
+ return false;
412
+ }
413
+ handler.submit(op, localOpMetadata as MapLocalOpMetadata);
414
+ return true;
415
+ }
416
+
417
+ public tryApplyStashedOp(op: IMapOperation): unknown {
418
+ const handler = this.messageHandlers.get(op.type);
419
+ if (handler === undefined) {
420
+ throw new Error("no apply stashed op handler");
421
+ }
422
+ return handler.applyStashedOp(op);
423
+ }
424
+
425
+ /**
426
+ * Process the given op if a handler is registered.
427
+ * @param op - The message to process
428
+ * @param local - Whether the message originated from the local client
429
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
430
+ * For messages from a remote client, this will be undefined.
431
+ * @returns True if the operation was processed, false otherwise.
432
+ */
433
+ public tryProcessMessage(op: IMapOperation, local: boolean, localOpMetadata: unknown): boolean {
434
+ const handler = this.messageHandlers.get(op.type);
435
+ if (handler === undefined) {
436
+ return false;
437
+ }
438
+ handler.process(op, local, localOpMetadata as MapLocalOpMetadata);
439
+ return true;
440
+ }
441
+
442
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
443
+
444
+ /**
445
+ * Rollback a local op
446
+ * @param op - The operation to rollback
447
+ * @param localOpMetadata - The local metadata associated with the op.
448
+ */
449
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
450
+ public rollback(op: any, localOpMetadata: unknown): void {
451
+ if (!isMapLocalOpMetadata(localOpMetadata)) {
452
+ throw new Error("Invalid localOpMetadata");
453
+ }
454
+
455
+ if (op.type === "clear" && localOpMetadata.type === "clear") {
456
+ if (localOpMetadata.previousMap === undefined) {
457
+ throw new Error("Cannot rollback without previous map");
458
+ }
459
+ for (const [key, localValue] of localOpMetadata.previousMap.entries()) {
460
+ this.setCore(key, localValue, true);
461
+ }
462
+
463
+ const lastPendingClearId = this.pendingClearMessageIds.pop();
464
+ if (
465
+ lastPendingClearId === undefined ||
466
+ lastPendingClearId !== localOpMetadata.pendingMessageId
467
+ ) {
468
+ throw new Error("Rollback op does match last clear");
469
+ }
470
+ } else if (op.type === "delete" || op.type === "set") {
471
+ if (localOpMetadata.type === "add") {
472
+ this.deleteCore(op.key as string, true);
473
+ } else if (
474
+ localOpMetadata.type === "edit" &&
475
+ localOpMetadata.previousValue !== undefined
476
+ ) {
477
+ this.setCore(op.key as string, localOpMetadata.previousValue, true);
478
+ } else {
479
+ throw new Error("Cannot rollback without previous value");
480
+ }
481
+
482
+ const pendingMessageIds = this.pendingKeys.get(op.key as string);
483
+ const lastPendingMessageId = pendingMessageIds?.pop();
484
+ if (!pendingMessageIds || lastPendingMessageId !== localOpMetadata.pendingMessageId) {
485
+ throw new Error("Rollback op does not match last pending");
486
+ }
487
+ if (pendingMessageIds.length === 0) {
488
+ this.pendingKeys.delete(op.key as string);
489
+ }
490
+ } else {
491
+ throw new Error("Unsupported op for rollback");
492
+ }
493
+ }
494
+
495
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access */
496
+
497
+ /**
498
+ * Set implementation used for both locally sourced sets as well as incoming remote sets.
499
+ * @param key - The key being set
500
+ * @param value - The value being set
501
+ * @param local - Whether the message originated from the local client
502
+ * @returns Previous local value of the key, if any
503
+ */
504
+ private setCore(key: string, value: ILocalValue, local: boolean): ILocalValue | undefined {
505
+ const previousLocalValue = this.data.get(key);
506
+ const previousValue: unknown = previousLocalValue?.value;
507
+ this.data.set(key, value);
508
+ this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
509
+ return previousLocalValue;
510
+ }
511
+
512
+ /**
513
+ * Clear implementation used for both locally sourced clears as well as incoming remote clears.
514
+ * @param local - Whether the message originated from the local client
515
+ */
516
+ private clearCore(local: boolean): void {
517
+ this.data.clear();
518
+ this.eventEmitter.emit("clear", local, this.eventEmitter);
519
+ }
520
+
521
+ /**
522
+ * Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
523
+ * @param key - The key being deleted
524
+ * @param local - Whether the message originated from the local client
525
+ * @returns Previous local value of the key if it existed, undefined if it did not exist
526
+ */
527
+ private deleteCore(key: string, local: boolean): ILocalValue | undefined {
528
+ const previousLocalValue = this.data.get(key);
529
+ const previousValue: unknown = previousLocalValue?.value;
530
+ const successfullyRemoved = this.data.delete(key);
531
+ if (successfullyRemoved) {
532
+ this.eventEmitter.emit(
533
+ "valueChanged",
534
+ { key, previousValue },
535
+ local,
536
+ this.eventEmitter,
537
+ );
538
+ }
539
+ return previousLocalValue;
540
+ }
541
+
542
+ /**
543
+ * Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
544
+ */
545
+ private clearExceptPendingKeys(): void {
546
+ // Assuming the pendingKeys is small and the map is large
547
+ // we will get the value for the pendingKeys and clear the map
548
+ const temp = new Map<string, ILocalValue>();
549
+ for (const key of this.pendingKeys.keys()) {
550
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
551
+ temp.set(key, this.data.get(key)!);
552
+ }
553
+ this.clearCore(false);
554
+ for (const [key, value] of temp.entries()) {
555
+ this.setCore(key, value, true);
556
+ }
557
+ }
558
+
559
+ /**
560
+ * The remote ISerializableValue we're receiving (either as a result of a load or an incoming set op) will
561
+ * have the information we need to create a real object, but will not be the real object yet. For example,
562
+ * we might know it's a map and the map's ID but not have the actual map or its data yet. makeLocal's
563
+ * job is to convert that information into a real object for local usage.
564
+ * @param key - The key that the caller intends to store the local value into (used for ops later). But
565
+ * doesn't actually store the local value into that key. So better not lie!
566
+ * @param serializable - The remote information that we can convert into a real object
567
+ * @returns The local value that was produced
568
+ */
569
+ private makeLocal(key: string, serializable: ISerializableValue): ILocalValue {
570
+ if (
571
+ serializable.type === ValueType[ValueType.Plain] ||
572
+ serializable.type === ValueType[ValueType.Shared]
573
+ ) {
574
+ return this.localValueMaker.fromSerializable(serializable);
575
+ } else {
576
+ throw new Error("Unknown local value type");
577
+ }
578
+ }
579
+
580
+ /**
581
+ * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
582
+ * not process the incoming operation.
583
+ * @param op - Operation to check
584
+ * @param local - Whether the message originated from the local client
585
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
586
+ * For messages from a remote client, this will be undefined.
587
+ * @returns True if the operation should be processed, false otherwise
588
+ */
589
+ private needProcessKeyOperation(
590
+ op: IMapKeyOperation,
591
+ local: boolean,
592
+ localOpMetadata: MapLocalOpMetadata,
593
+ ): boolean {
594
+ if (this.pendingClearMessageIds.length > 0) {
595
+ if (local) {
596
+ assert(
597
+ localOpMetadata !== undefined &&
598
+ isMapKeyLocalOpMetadata(localOpMetadata) &&
599
+ localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
600
+ 0x013 /* "Received out of order op when there is an unackd clear message" */,
601
+ );
602
+ }
603
+ // If we have an unack'd clear, we can ignore all ops.
604
+ return false;
605
+ }
606
+
607
+ const pendingKeyMessageId = this.pendingKeys.get(op.key);
608
+ if (pendingKeyMessageId !== undefined) {
609
+ // Found an unack'd op. Clear it from the map if the pendingMessageId in the map matches this message's
610
+ // and don't process the op.
611
+ if (local) {
612
+ assert(
613
+ localOpMetadata !== undefined && isMapKeyLocalOpMetadata(localOpMetadata),
614
+ 0x014 /* pendingMessageId is missing from the local client's operation */,
615
+ );
616
+ const pendingMessageIds = this.pendingKeys.get(op.key);
617
+ assert(
618
+ pendingMessageIds !== undefined &&
619
+ pendingMessageIds[0] === localOpMetadata.pendingMessageId,
620
+ 0x2fa /* Unexpected pending message received */,
621
+ );
622
+ pendingMessageIds.shift();
623
+ if (pendingMessageIds.length === 0) {
624
+ this.pendingKeys.delete(op.key);
625
+ }
626
+ }
627
+ return false;
628
+ }
629
+
630
+ // If we don't have a NACK op on the key, we need to process the remote ops.
631
+ return !local;
632
+ }
633
+
634
+ /**
635
+ * Get the message handlers for the map.
636
+ * @returns A map of string op names to IMapMessageHandlers for those ops
637
+ */
638
+ private getMessageHandlers(): Map<string, IMapMessageHandler> {
639
+ const messageHandlers = new Map<string, IMapMessageHandler>();
640
+ messageHandlers.set("clear", {
641
+ process: (op: IMapClearOperation, local, localOpMetadata) => {
642
+ if (local) {
643
+ assert(
644
+ isClearLocalOpMetadata(localOpMetadata),
645
+ 0x015 /* "pendingMessageId is missing from the local client's clear operation" */,
646
+ );
647
+ const pendingClearMessageId = this.pendingClearMessageIds.shift();
648
+ assert(
649
+ pendingClearMessageId === localOpMetadata.pendingMessageId,
650
+ 0x2fb /* pendingMessageId does not match */,
651
+ );
652
+ return;
653
+ }
654
+ if (this.pendingKeys.size > 0) {
655
+ this.clearExceptPendingKeys();
656
+ return;
657
+ }
658
+ this.clearCore(local);
659
+ },
660
+ submit: (op: IMapClearOperation, localOpMetadata: IMapClearLocalOpMetadata) => {
661
+ assert(
662
+ isClearLocalOpMetadata(localOpMetadata),
663
+ 0x2fc /* Invalid localOpMetadata for clear */,
664
+ );
665
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
666
+ const pendingClearMessageId = this.pendingClearMessageIds.shift();
667
+ assert(
668
+ pendingClearMessageId === localOpMetadata.pendingMessageId,
669
+ 0x2fd /* pendingMessageId does not match */,
670
+ );
671
+ this.submitMapClearMessage(op, localOpMetadata.previousMap);
672
+ },
673
+ applyStashedOp: (op: IMapClearOperation) => {
674
+ const copy = new Map<string, ILocalValue>(this.data);
675
+ this.clearCore(true);
676
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
677
+ return createClearLocalOpMetadata(op, this.getMapClearMessageId(), copy);
678
+ },
679
+ });
680
+ messageHandlers.set("delete", {
681
+ process: (op: IMapDeleteOperation, local, localOpMetadata) => {
682
+ if (!this.needProcessKeyOperation(op, local, localOpMetadata)) {
683
+ return;
684
+ }
685
+ this.deleteCore(op.key, local);
686
+ },
687
+ submit: (op: IMapDeleteOperation, localOpMetadata: MapKeyLocalOpMetadata) => {
688
+ this.resubmitMapKeyMessage(op, localOpMetadata);
689
+ },
690
+ applyStashedOp: (op: IMapDeleteOperation) => {
691
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
692
+ const previousValue = this.deleteCore(op.key, true);
693
+ return createKeyLocalOpMetadata(op, this.getMapKeyMessageId(op), previousValue);
694
+ },
695
+ });
696
+ messageHandlers.set("set", {
697
+ process: (op: IMapSetOperation, local, localOpMetadata) => {
698
+ if (!this.needProcessKeyOperation(op, local, localOpMetadata)) {
699
+ return;
700
+ }
701
+
702
+ // needProcessKeyOperation should have returned false if local is true
703
+ const context = this.makeLocal(op.key, op.value);
704
+ this.setCore(op.key, context, local);
705
+ },
706
+ submit: (op: IMapSetOperation, localOpMetadata: MapKeyLocalOpMetadata) => {
707
+ this.resubmitMapKeyMessage(op, localOpMetadata);
708
+ },
709
+ applyStashedOp: (op: IMapSetOperation) => {
710
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
711
+ const context = this.makeLocal(op.key, op.value);
712
+ const previousValue = this.setCore(op.key, context, true);
713
+ return createKeyLocalOpMetadata(op, this.getMapKeyMessageId(op), previousValue);
714
+ },
715
+ });
716
+
717
+ return messageHandlers;
718
+ }
719
+
720
+ private getMapClearMessageId(): number {
721
+ const pendingMessageId = ++this.pendingMessageId;
722
+ this.pendingClearMessageIds.push(pendingMessageId);
723
+ return pendingMessageId;
724
+ }
725
+
726
+ /**
727
+ * Submit a clear message to remote clients.
728
+ * @param op - The clear message
729
+ */
730
+ private submitMapClearMessage(
731
+ op: IMapClearOperation,
732
+ previousMap?: Map<string, ILocalValue>,
733
+ ): void {
734
+ const metadata = createClearLocalOpMetadata(op, this.getMapClearMessageId(), previousMap);
735
+ this.submitMessage(op, metadata);
736
+ }
737
+
738
+ private getMapKeyMessageId(op: IMapKeyOperation): number {
739
+ const pendingMessageId = ++this.pendingMessageId;
740
+ const pendingMessageIds = this.pendingKeys.get(op.key);
741
+ if (pendingMessageIds !== undefined) {
742
+ pendingMessageIds.push(pendingMessageId);
743
+ } else {
744
+ this.pendingKeys.set(op.key, [pendingMessageId]);
745
+ }
746
+ return pendingMessageId;
747
+ }
748
+
749
+ /**
750
+ * Submit a map key message to remote clients.
751
+ * @param op - The map key message
752
+ * @param previousValue - The value of the key before this op
753
+ */
754
+ private submitMapKeyMessage(op: IMapKeyOperation, previousValue?: ILocalValue): void {
755
+ const localMetadata = createKeyLocalOpMetadata(
756
+ op,
757
+ this.getMapKeyMessageId(op),
758
+ previousValue,
759
+ );
760
+ this.submitMessage(op, localMetadata);
761
+ }
762
+
763
+ /**
764
+ * Submit a map key message to remote clients based on a previous submit.
765
+ * @param op - The map key message
766
+ * @param localOpMetadata - Metadata from the previous submit
767
+ */
768
+ private resubmitMapKeyMessage(op: IMapKeyOperation, localOpMetadata: MapLocalOpMetadata): void {
769
+ assert(
770
+ isMapKeyLocalOpMetadata(localOpMetadata),
771
+ 0x2fe /* Invalid localOpMetadata in submit */,
772
+ );
773
+
774
+ // clear the old pending message id
775
+ const pendingMessageIds = this.pendingKeys.get(op.key);
776
+ assert(
777
+ pendingMessageIds !== undefined &&
778
+ pendingMessageIds[0] === localOpMetadata.pendingMessageId,
779
+ 0x2ff /* Unexpected pending message received */,
780
+ );
781
+ pendingMessageIds.shift();
782
+ if (pendingMessageIds.length === 0) {
783
+ this.pendingKeys.delete(op.key);
784
+ }
785
+
786
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
787
+ const pendingMessageId = this.getMapKeyMessageId(op);
788
+ const localMetadata =
789
+ localOpMetadata.type === "edit"
790
+ ? { type: "edit", pendingMessageId, previousValue: localOpMetadata.previousValue }
791
+ : { type: "add", pendingMessageId };
792
+ this.submitMessage(op, localMetadata);
793
+ }
759
794
  }