@fluidframework/map 1.4.0-121020 → 2.0.0-dev-rc.1.0.0.225277

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