@fluidframework/map 2.51.0 → 2.52.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.
package/dist/mapKernel.js CHANGED
@@ -8,32 +8,22 @@ exports.MapKernel = void 0;
8
8
  const internal_1 = require("@fluidframework/core-utils/internal");
9
9
  const internal_2 = require("@fluidframework/shared-object-base/internal");
10
10
  const localValues_js_1 = require("./localValues.js");
11
- /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
12
- function isMapKeyLocalOpMetadata(metadata) {
13
- return (metadata !== undefined &&
14
- typeof metadata.pendingMessageId === "number" &&
15
- (metadata.type === "add" || metadata.type === "edit"));
16
- }
17
- function isClearLocalOpMetadata(metadata) {
18
- return (metadata !== undefined &&
19
- metadata.type === "clear" &&
20
- typeof metadata.pendingMessageId === "number");
21
- }
22
- /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
23
- function createClearLocalOpMetadata(op, pendingClearMessageId, previousMap) {
24
- const localMetadata = {
25
- type: "clear",
26
- pendingMessageId: pendingClearMessageId,
27
- previousMap,
28
- };
29
- return localMetadata;
30
- }
31
- function createKeyLocalOpMetadata(op, pendingMessageId, previousValue) {
32
- const localMetadata = previousValue
33
- ? { type: "edit", pendingMessageId, previousValue }
34
- : { type: "add", pendingMessageId };
35
- return localMetadata;
36
- }
11
+ /**
12
+ * Rough polyfill for Array.findLastIndex until we target ES2023 or greater.
13
+ */
14
+ const findLastIndex = (array, callbackFn) => {
15
+ for (let i = array.length - 1; i >= 0; i--) {
16
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17
+ if (callbackFn(array[i])) {
18
+ return i;
19
+ }
20
+ }
21
+ return -1;
22
+ };
23
+ /**
24
+ * Rough polyfill for Array.findLast until we target ES2023 or greater.
25
+ */
26
+ const findLast = (array, callbackFn) => array[findLastIndex(array, callbackFn)];
37
27
  /**
38
28
  * A SharedMap is a map-like distributed data structure.
39
29
  */
@@ -42,7 +32,8 @@ class MapKernel {
42
32
  * The number of key/value pairs stored in the map.
43
33
  */
44
34
  get size() {
45
- return this.data.size;
35
+ const iterableItems = [...this.internalIterator()];
36
+ return iterableItems.length;
46
37
  }
47
38
  /**
48
39
  * Create a new shared map kernel.
@@ -64,48 +55,150 @@ class MapKernel {
64
55
  */
65
56
  this.messageHandlers = new Map();
66
57
  /**
67
- * The in-memory data the map is storing.
68
- */
69
- this.data = new Map();
70
- /**
71
- * Keys that have been modified locally but not yet ack'd from the server.
58
+ * The data the map is storing, but only including sequenced values (no local pending
59
+ * modifications are included).
72
60
  */
73
- this.pendingKeys = new Map();
61
+ this.sequencedData = new Map();
74
62
  /**
75
- * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
63
+ * A data structure containing all local pending modifications, which is used in combination
64
+ * with the sequencedData to compute optimistic values.
65
+ *
66
+ * Pending sets are aggregated into "lifetimes", which permit correct relative iteration order
67
+ * even across remote operations and rollbacks.
76
68
  */
77
- this.nextPendingMessageId = 0;
69
+ this.pendingData = [];
78
70
  /**
79
- * The pending metadata for any local operations that have not yet been ack'd from the server, in order.
71
+ * Get an iterator over the optimistically observable ILocalValue entries in the map. For example, excluding
72
+ * sequenced entries that have pending deletes/clears.
73
+ *
74
+ * @remarks
75
+ * There is no perfect solution here, particularly when the iterator is retained over time and the map is
76
+ * modified or new ack's are received. The pendingData portion of the iteration is the most susceptible to
77
+ * this problem. The implementation prioritizes (in roughly this order):
78
+ * 1. Correct immediate iteration (i.e. when the map is not modified before iteration completes)
79
+ * 2. Consistent iteration order before/after sequencing of pending ops; acks don't change order
80
+ * 3. Consistent iteration order between synchronized clients, even if they each modified the map concurrently
81
+ * 4. Remaining as close as possible to the native Map iterator behavior, e.g. live-ish view rather than snapshot
82
+ *
83
+ * For this reason, it's important not to internally snapshot the output of the iterator for any purpose that
84
+ * does not immediately (synchronously) consume that output and dispose of it.
80
85
  */
81
- this.pendingMapLocalOpMetadata = new internal_1.DoublyLinkedList();
86
+ this.internalIterator = () => {
87
+ // We perform iteration in two steps - first by iterating over members of the sequenced data that are not
88
+ // optimistically deleted or cleared, and then over the pending data lifetimes that have not subsequently
89
+ // been deleted or cleared. In total, this give an ordering of members based on when they were initially
90
+ // added to the map (even if they were later modified), similar to the native Map.
91
+ const sequencedDataIterator = this.sequencedData.keys();
92
+ const pendingDataIterator = this.pendingData.values();
93
+ const next = () => {
94
+ let nextSequencedKey = sequencedDataIterator.next();
95
+ while (!nextSequencedKey.done) {
96
+ const key = nextSequencedKey.value;
97
+ // If we have any pending deletes or clears, then we won't iterate to this key yet (if at all).
98
+ // Either it is optimistically deleted and will not be part of the iteration, or it was
99
+ // re-added later and we'll iterate to it when we get to the pending data.
100
+ if (!this.pendingData.some((entry) => entry.type === "clear" || (entry.type === "delete" && entry.key === key))) {
101
+ const optimisticValue = this.getOptimisticLocalValue(key);
102
+ (0, internal_1.assert)(optimisticValue !== undefined, 0xbf1 /* Should never iterate to a key with undefined optimisticValue */);
103
+ return { value: [key, optimisticValue], done: false };
104
+ }
105
+ nextSequencedKey = sequencedDataIterator.next();
106
+ }
107
+ let nextPending = pendingDataIterator.next();
108
+ while (!nextPending.done) {
109
+ const nextPendingEntry = nextPending.value;
110
+ // A lifetime entry may need to be iterated.
111
+ if (nextPendingEntry.type === "lifetime") {
112
+ const nextPendingEntryIndex = this.pendingData.indexOf(nextPendingEntry);
113
+ const mostRecentDeleteOrClearIndex = findLastIndex(this.pendingData, (entry) => entry.type === "clear" ||
114
+ (entry.type === "delete" && entry.key === nextPendingEntry.key));
115
+ // Only iterate the pending entry now if it hasn't been deleted or cleared.
116
+ if (nextPendingEntryIndex > mostRecentDeleteOrClearIndex) {
117
+ const latestPendingValue =
118
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
119
+ nextPendingEntry.keySets[nextPendingEntry.keySets.length - 1];
120
+ // Skip iterating if we would have would have already iterated it as part of the sequenced data.
121
+ // This is not a perfect check in the case the map has changed since the iterator was created
122
+ // (e.g. if a remote client added the same key in the meantime).
123
+ if (!this.sequencedData.has(nextPendingEntry.key) ||
124
+ mostRecentDeleteOrClearIndex !== -1) {
125
+ return { value: [nextPendingEntry.key, latestPendingValue.value], done: false };
126
+ }
127
+ }
128
+ }
129
+ nextPending = pendingDataIterator.next();
130
+ }
131
+ return { value: undefined, done: true };
132
+ };
133
+ const iterator = {
134
+ next,
135
+ [Symbol.iterator]() {
136
+ return this;
137
+ },
138
+ };
139
+ return iterator;
140
+ };
82
141
  /**
83
- * The pending ids of any clears that have been performed locally but not yet ack'd from the server
142
+ * Compute the optimistic local value for a given key. This combines the sequenced data with
143
+ * any pending changes that have not yet been sequenced.
84
144
  */
85
- this.pendingClearMessageIds = [];
145
+ this.getOptimisticLocalValue = (key) => {
146
+ const latestPendingEntry = findLast(this.pendingData, (entry) => entry.type === "clear" || entry.key === key);
147
+ if (latestPendingEntry === undefined) {
148
+ return this.sequencedData.get(key);
149
+ }
150
+ else if (latestPendingEntry.type === "lifetime") {
151
+ const latestPendingSet =
152
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
153
+ latestPendingEntry.keySets[latestPendingEntry.keySets.length - 1];
154
+ return latestPendingSet.value;
155
+ }
156
+ else {
157
+ // Delete or clear
158
+ return undefined;
159
+ }
160
+ };
86
161
  this.messageHandlers = this.getMessageHandlers();
87
162
  }
88
163
  /**
89
- * Get an iterator over the keys in this map.
164
+ * Get an iterator over the entries in this map.
90
165
  * @returns The iterator
91
166
  */
92
- keys() {
93
- return this.data.keys();
167
+ entries() {
168
+ const internalIterator = this.internalIterator();
169
+ const next = () => {
170
+ const nextResult = internalIterator.next();
171
+ if (nextResult.done) {
172
+ return { value: undefined, done: true };
173
+ }
174
+ // Unpack the stored value
175
+ const [key, localValue] = nextResult.value;
176
+ return { value: [key, localValue.value], done: false };
177
+ };
178
+ const iterator = {
179
+ next,
180
+ [Symbol.iterator]() {
181
+ return this;
182
+ },
183
+ };
184
+ return iterator;
94
185
  }
95
186
  /**
96
- * Get an iterator over the entries in this map.
187
+ * Get an iterator over the keys in this map.
97
188
  * @returns The iterator
98
189
  */
99
- entries() {
100
- const localEntriesIterator = this.data.entries();
190
+ keys() {
191
+ const internalIterator = this.internalIterator();
192
+ const next = () => {
193
+ const nextResult = internalIterator.next();
194
+ if (nextResult.done) {
195
+ return { value: undefined, done: true };
196
+ }
197
+ const [key] = nextResult.value;
198
+ return { value: key, done: false };
199
+ };
101
200
  const iterator = {
102
- next() {
103
- const nextVal = localEntriesIterator.next();
104
- return nextVal.done
105
- ? { value: undefined, done: true }
106
- : // Unpack the stored value
107
- { value: [nextVal.value[0], nextVal.value[1].value], done: false };
108
- },
201
+ next,
109
202
  [Symbol.iterator]() {
110
203
  return this;
111
204
  },
@@ -117,15 +210,17 @@ class MapKernel {
117
210
  * @returns The iterator
118
211
  */
119
212
  values() {
120
- const localValuesIterator = this.data.values();
213
+ const internalIterator = this.internalIterator();
214
+ const next = () => {
215
+ const nextResult = internalIterator.next();
216
+ if (nextResult.done) {
217
+ return { value: undefined, done: true };
218
+ }
219
+ const [, localValue] = nextResult.value;
220
+ return { value: localValue.value, done: false };
221
+ };
121
222
  const iterator = {
122
- next() {
123
- const nextVal = localValuesIterator.next();
124
- return nextVal.done
125
- ? { value: undefined, done: true }
126
- : // Unpack the stored value
127
- { value: nextVal.value.value, done: false };
128
- },
223
+ next,
129
224
  [Symbol.iterator]() {
130
225
  return this;
131
226
  },
@@ -144,8 +239,12 @@ class MapKernel {
144
239
  * @param callbackFn - Callback function
145
240
  */
146
241
  forEach(callbackFn) {
242
+ // It would be better to iterate over the data without a temp map. However, we don't have a valid
243
+ // map to pass for the third argument here (really, it should probably should be a reference to the
244
+ // SharedMap and not the MapKernel).
245
+ const tempMap = new Map(this.internalIterator());
147
246
  // eslint-disable-next-line unicorn/no-array-for-each
148
- this.data.forEach((localValue, key, m) => {
247
+ tempMap.forEach((localValue, key, m) => {
149
248
  callbackFn(localValue.value, key, m);
150
249
  });
151
250
  }
@@ -153,7 +252,7 @@ class MapKernel {
153
252
  * {@inheritDoc ISharedMap.get}
154
253
  */
155
254
  get(key) {
156
- const localValue = this.data.get(key);
255
+ const localValue = this.getOptimisticLocalValue(key);
157
256
  return localValue === undefined ? undefined : localValue.value;
158
257
  }
159
258
  /**
@@ -162,7 +261,7 @@ class MapKernel {
162
261
  * @returns True if the key exists, false otherwise
163
262
  */
164
263
  has(key) {
165
- return this.data.has(key);
264
+ return this.getOptimisticLocalValue(key) !== undefined;
166
265
  }
167
266
  /**
168
267
  * {@inheritDoc ISharedMap.set}
@@ -172,18 +271,37 @@ class MapKernel {
172
271
  if (key === undefined || key === null) {
173
272
  throw new Error("Undefined and null keys are not supported");
174
273
  }
175
- // Set the value locally.
176
- const previousValue = this.setCore(key, { value }, true);
274
+ const localValue = { value };
275
+ const previousOptimisticLocalValue = this.getOptimisticLocalValue(key);
177
276
  // If we are not attached, don't submit the op.
178
277
  if (!this.isAttached()) {
278
+ this.sequencedData.set(key, localValue);
279
+ this.eventEmitter.emit("valueChanged", { key, previousValue: previousOptimisticLocalValue?.value }, true, this.eventEmitter);
179
280
  return;
180
281
  }
282
+ // A new pending key lifetime is created if:
283
+ // 1. There isn't any pending entry for the key yet
284
+ // 2. The most recent pending entry for the key was a deletion (as this terminates the prior lifetime)
285
+ // 3. A clear was sent after the last pending entry for the key (which also terminates the prior lifetime)
286
+ let latestPendingEntry = findLast(this.pendingData, (entry) => entry.type === "clear" || entry.key === key);
287
+ if (latestPendingEntry === undefined ||
288
+ latestPendingEntry.type === "delete" ||
289
+ latestPendingEntry.type === "clear") {
290
+ latestPendingEntry = { type: "lifetime", key, keySets: [] };
291
+ this.pendingData.push(latestPendingEntry);
292
+ }
293
+ const pendingKeySet = {
294
+ type: "set",
295
+ value: localValue,
296
+ };
297
+ latestPendingEntry.keySets.push(pendingKeySet);
181
298
  const op = {
182
299
  key,
183
300
  type: "set",
184
- value: { type: internal_2.ValueType[internal_2.ValueType.Plain], value },
301
+ value: { type: internal_2.ValueType[internal_2.ValueType.Plain], value: localValue.value },
185
302
  };
186
- this.submitMapKeyMessage(op, previousValue);
303
+ this.submitMessage(op, pendingKeySet);
304
+ this.eventEmitter.emit("valueChanged", { key, previousValue: previousOptimisticLocalValue?.value }, true, this.eventEmitter);
187
305
  }
188
306
  /**
189
307
  * Delete a key from the map.
@@ -191,36 +309,51 @@ class MapKernel {
191
309
  * @returns True if the key existed and was deleted, false if it did not exist
192
310
  */
193
311
  delete(key) {
194
- // Delete the key locally first.
195
- const previousValue = this.deleteCore(key, true);
196
- // If we are not attached, don't submit the op.
312
+ const previousOptimisticLocalValue = this.getOptimisticLocalValue(key);
197
313
  if (!this.isAttached()) {
198
- return previousValue !== undefined;
314
+ const successfullyRemoved = this.sequencedData.delete(key);
315
+ // Only emit if we actually deleted something.
316
+ if (previousOptimisticLocalValue !== undefined) {
317
+ this.eventEmitter.emit("valueChanged", { key, previousValue: previousOptimisticLocalValue.value }, true, this.eventEmitter);
318
+ }
319
+ return successfullyRemoved;
199
320
  }
321
+ const pendingKeyDelete = {
322
+ type: "delete",
323
+ key,
324
+ };
325
+ this.pendingData.push(pendingKeyDelete);
200
326
  const op = {
201
327
  key,
202
328
  type: "delete",
203
329
  };
204
- this.submitMapKeyMessage(op, previousValue);
205
- return previousValue !== undefined;
330
+ this.submitMessage(op, pendingKeyDelete);
331
+ // Only emit if we locally believe we deleted something. Otherwise we still send the op
332
+ // (permitting speculative deletion even if we don't see anything locally) but don't emit
333
+ // a valueChanged since we in fact did not locally observe a value change.
334
+ if (previousOptimisticLocalValue !== undefined) {
335
+ this.eventEmitter.emit("valueChanged", { key, previousValue: previousOptimisticLocalValue.value }, true, this.eventEmitter);
336
+ }
337
+ return true;
206
338
  }
207
339
  /**
208
340
  * Clear all data from the map.
209
341
  */
210
342
  clear() {
211
- const copy = this.isAttached() ? new Map(this.data) : undefined;
212
- // Clear the data locally first.
213
- this.clearCore(true);
214
- // Clear the pendingKeys immediately, the local unack'd operations are aborted
215
- this.pendingKeys.clear();
216
- // If we are not attached, don't submit the op.
217
343
  if (!this.isAttached()) {
344
+ this.sequencedData.clear();
345
+ this.eventEmitter.emit("clear", true, this.eventEmitter);
218
346
  return;
219
347
  }
348
+ const pendingClear = {
349
+ type: "clear",
350
+ };
351
+ this.pendingData.push(pendingClear);
220
352
  const op = {
221
353
  type: "clear",
222
354
  };
223
- this.submitMapClearMessage(op, copy);
355
+ this.submitMessage(op, pendingClear);
356
+ this.eventEmitter.emit("clear", true, this.eventEmitter);
224
357
  }
225
358
  /**
226
359
  * Serializes the data stored in the shared map to a JSON string
@@ -229,7 +362,7 @@ class MapKernel {
229
362
  */
230
363
  getSerializedStorage(serializer) {
231
364
  const serializedMapData = {};
232
- for (const [key, localValue] of this.data.entries()) {
365
+ for (const [key, localValue] of this.sequencedData.entries()) {
233
366
  serializedMapData[key] = (0, localValues_js_1.serializeValue)(localValue.value, serializer, this.handle);
234
367
  }
235
368
  return serializedMapData;
@@ -241,7 +374,7 @@ class MapKernel {
241
374
  populateFromSerializable(json) {
242
375
  for (const [key, serializable] of Object.entries(this.serializer.decode(json))) {
243
376
  (0, localValues_js_1.migrateIfSharedSerializable)(serializable, this.serializer, this.handle);
244
- this.data.set(key, { value: serializable.value });
377
+ this.sequencedData.set(key, { value: serializable.value });
245
378
  }
246
379
  }
247
380
  /**
@@ -310,139 +443,41 @@ class MapKernel {
310
443
  */
311
444
  rollback(op, localOpMetadata) {
312
445
  const mapOp = op;
313
- const listNodeLocalOpMetadata = localOpMetadata;
314
- const removedLocalOpMetadata = this.pendingMapLocalOpMetadata.pop();
315
- (0, internal_1.assert)(removedLocalOpMetadata !== undefined &&
316
- removedLocalOpMetadata === listNodeLocalOpMetadata, 0xbcb /* Rolling back unexpected op */);
317
- if (mapOp.type === "clear" && listNodeLocalOpMetadata.data.type === "clear") {
318
- if (listNodeLocalOpMetadata.data.previousMap === undefined) {
319
- throw new Error("Cannot rollback without previous map");
320
- }
321
- for (const [key, localValue] of listNodeLocalOpMetadata.data.previousMap.entries()) {
322
- this.setCore(key, localValue, true);
323
- }
324
- const lastPendingClearId = this.pendingClearMessageIds.pop();
325
- if (lastPendingClearId === undefined ||
326
- lastPendingClearId !== listNodeLocalOpMetadata.data.pendingMessageId) {
327
- throw new Error("Rollback op does match last clear");
328
- }
329
- }
330
- else if (mapOp.type === "delete" || mapOp.type === "set") {
331
- if (listNodeLocalOpMetadata.data.type === "add") {
332
- this.deleteCore(mapOp.key, true);
333
- }
334
- else if (listNodeLocalOpMetadata.data.type === "edit" &&
335
- listNodeLocalOpMetadata.data.previousValue !== undefined) {
336
- this.setCore(mapOp.key, listNodeLocalOpMetadata.data.previousValue, true);
337
- }
338
- else {
339
- throw new Error("Cannot rollback without previous value");
340
- }
341
- const pendingMessageIds = this.pendingKeys.get(mapOp.key);
342
- const lastPendingMessageId = pendingMessageIds?.pop();
343
- if (!pendingMessageIds ||
344
- lastPendingMessageId !== listNodeLocalOpMetadata.data.pendingMessageId) {
345
- throw new Error("Rollback op does not match last pending");
346
- }
347
- if (pendingMessageIds.length === 0) {
348
- this.pendingKeys.delete(mapOp.key);
446
+ const typedLocalOpMetadata = localOpMetadata;
447
+ if (mapOp.type === "clear") {
448
+ // A pending clear will be last in the list, since it terminates all prior lifetimes.
449
+ const pendingClear = this.pendingData.pop();
450
+ (0, internal_1.assert)(pendingClear !== undefined &&
451
+ pendingClear.type === "clear" &&
452
+ pendingClear === typedLocalOpMetadata, 0xbf2 /* Unexpected clear rollback */);
453
+ for (const [key] of this.internalIterator()) {
454
+ this.eventEmitter.emit("valueChanged", { key, previousValue: undefined }, true, this.eventEmitter);
349
455
  }
350
456
  }
351
457
  else {
352
- throw new Error("Unsupported op for rollback");
353
- }
354
- }
355
- /**
356
- * Set implementation used for both locally sourced sets as well as incoming remote sets.
357
- * @param key - The key being set
358
- * @param value - The value being set
359
- * @param local - Whether the message originated from the local client
360
- * @returns Previous local value of the key, if any
361
- */
362
- setCore(key, value, local) {
363
- const previousLocalValue = this.data.get(key);
364
- const previousValue = previousLocalValue?.value;
365
- this.data.set(key, value);
366
- this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
367
- return previousLocalValue;
368
- }
369
- /**
370
- * Clear implementation used for both locally sourced clears as well as incoming remote clears.
371
- * @param local - Whether the message originated from the local client
372
- */
373
- clearCore(local) {
374
- this.data.clear();
375
- this.eventEmitter.emit("clear", local, this.eventEmitter);
376
- }
377
- /**
378
- * Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
379
- * @param key - The key being deleted
380
- * @param local - Whether the message originated from the local client
381
- * @returns Previous local value of the key if it existed, undefined if it did not exist
382
- */
383
- deleteCore(key, local) {
384
- const previousLocalValue = this.data.get(key);
385
- const previousValue = previousLocalValue?.value;
386
- const successfullyRemoved = this.data.delete(key);
387
- if (successfullyRemoved) {
388
- this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
389
- }
390
- return previousLocalValue;
391
- }
392
- /**
393
- * Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
394
- */
395
- clearExceptPendingKeys() {
396
- // Assuming the pendingKeys is small and the map is large
397
- // we will get the value for the pendingKeys and clear the map
398
- const temp = new Map();
399
- for (const key of this.pendingKeys.keys()) {
400
- // Verify if the most recent pending operation is a delete op, no need to retain it if so.
401
- // This ensures the map size remains consistent.
402
- if (this.data.has(key)) {
403
- temp.set(key, this.data.get(key));
404
- }
405
- }
406
- this.clearCore(false);
407
- for (const [key, value] of temp.entries()) {
408
- this.setCore(key, value, true);
409
- }
410
- }
411
- /**
412
- * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
413
- * not process the incoming operation.
414
- * @param op - Operation to check
415
- * @param local - Whether the message originated from the local client
416
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
417
- * For messages from a remote client, this will be undefined.
418
- * @returns True if the operation should be processed, false otherwise
419
- */
420
- needProcessKeyOperation(op, local, localOpMetadata) {
421
- if (this.pendingClearMessageIds[0] !== undefined) {
422
- if (local) {
423
- (0, internal_1.assert)(localOpMetadata !== undefined &&
424
- isMapKeyLocalOpMetadata(localOpMetadata) &&
425
- localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0], 0x013 /* "Received out of order op when there is an unackd clear message" */);
458
+ // A pending set/delete may not be last in the list, as the lifetimes' order is based on when
459
+ // they were created, not when they were last modified.
460
+ const pendingEntryIndex = findLastIndex(this.pendingData, (entry) => entry.type !== "clear" && entry.key === mapOp.key);
461
+ const pendingEntry = this.pendingData[pendingEntryIndex];
462
+ (0, internal_1.assert)(pendingEntry !== undefined &&
463
+ (pendingEntry.type === "delete" || pendingEntry.type === "lifetime"), 0xbf3 /* Unexpected pending data for set/delete op */);
464
+ if (pendingEntry.type === "delete") {
465
+ (0, internal_1.assert)(pendingEntry === typedLocalOpMetadata, 0xbf4 /* Unexpected delete rollback */);
466
+ this.pendingData.splice(pendingEntryIndex, 1);
467
+ // Only emit if rolling back the delete actually results in a value becoming visible.
468
+ if (this.getOptimisticLocalValue(mapOp.key) !== undefined) {
469
+ this.eventEmitter.emit("valueChanged", { key: mapOp.key, previousValue: undefined }, true, this.eventEmitter);
470
+ }
426
471
  }
427
- // If we have an unack'd clear, we can ignore all ops.
428
- return false;
429
- }
430
- const pendingKeyMessageIds = this.pendingKeys.get(op.key);
431
- if (pendingKeyMessageIds !== undefined) {
432
- // Found an unack'd op. Clear it from the map if the pendingMessageId in the map matches this message's
433
- // and don't process the op.
434
- if (local) {
435
- (0, internal_1.assert)(localOpMetadata !== undefined && isMapKeyLocalOpMetadata(localOpMetadata), 0x014 /* pendingMessageId is missing from the local client's operation */);
436
- (0, internal_1.assert)(pendingKeyMessageIds[0] === localOpMetadata.pendingMessageId, 0x2fa /* Unexpected pending message received */);
437
- pendingKeyMessageIds.shift();
438
- if (pendingKeyMessageIds.length === 0) {
439
- this.pendingKeys.delete(op.key);
472
+ else if (pendingEntry.type === "lifetime") {
473
+ const pendingKeySet = pendingEntry.keySets.pop();
474
+ (0, internal_1.assert)(pendingKeySet !== undefined && pendingKeySet === typedLocalOpMetadata, 0xbf5 /* Unexpected set rollback */);
475
+ if (pendingEntry.keySets.length === 0) {
476
+ this.pendingData.splice(pendingEntryIndex, 1);
440
477
  }
478
+ this.eventEmitter.emit("valueChanged", { key: mapOp.key, previousValue: pendingKeySet.value.value }, true, this.eventEmitter);
441
479
  }
442
- return false;
443
480
  }
444
- // If we don't have a NACK op on the key, we need to process the remote ops.
445
- return !local;
446
481
  }
447
482
  /**
448
483
  * Get the message handlers for the map.
@@ -452,133 +487,81 @@ class MapKernel {
452
487
  const messageHandlers = new Map();
453
488
  messageHandlers.set("clear", {
454
489
  process: (op, local, localOpMetadata) => {
490
+ this.sequencedData.clear();
455
491
  if (local) {
456
- const removedLocalOpMetadata = this.pendingMapLocalOpMetadata.shift();
457
- (0, internal_1.assert)(removedLocalOpMetadata !== undefined && removedLocalOpMetadata === localOpMetadata, 0xbcc /* Processing unexpected local clear op */);
458
- (0, internal_1.assert)(isClearLocalOpMetadata(localOpMetadata.data), 0x015 /* "pendingMessageId is missing from the local client's clear operation" */);
459
- const pendingClearMessageId = this.pendingClearMessageIds.shift();
460
- (0, internal_1.assert)(pendingClearMessageId === localOpMetadata.data.pendingMessageId, 0x2fb /* pendingMessageId does not match */);
461
- return;
492
+ const pendingClear = this.pendingData.shift();
493
+ (0, internal_1.assert)(pendingClear !== undefined &&
494
+ pendingClear.type === "clear" &&
495
+ pendingClear === localOpMetadata, 0xbf6 /* Got a local clear message we weren't expecting */);
462
496
  }
463
- if (this.pendingKeys.size > 0) {
464
- this.clearExceptPendingKeys();
465
- return;
497
+ else {
498
+ // Only emit for remote ops, we would have already emitted for local ops. Only emit if there
499
+ // is no optimistically-applied local pending clear that would supersede this remote clear.
500
+ if (!this.pendingData.some((entry) => entry.type === "clear")) {
501
+ this.eventEmitter.emit("clear", local, this.eventEmitter);
502
+ }
466
503
  }
467
- this.clearCore(local);
468
504
  },
469
505
  resubmit: (op, localOpMetadata) => {
470
- const removedLocalOpMetadata = localOpMetadata.remove()?.data;
471
- (0, internal_1.assert)(removedLocalOpMetadata !== undefined, 0xbcd /* Resubmitting unexpected local clear op */);
472
- (0, internal_1.assert)(isClearLocalOpMetadata(localOpMetadata.data), 0x2fc /* Invalid localOpMetadata for clear */);
473
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
474
- const pendingClearMessageId = this.pendingClearMessageIds.shift();
475
- (0, internal_1.assert)(pendingClearMessageId === localOpMetadata.data.pendingMessageId, 0x2fd /* pendingMessageId does not match */);
476
- this.submitMapClearMessage(op, localOpMetadata.data.previousMap);
506
+ this.submitMessage(op, localOpMetadata);
477
507
  },
478
508
  });
479
509
  messageHandlers.set("delete", {
480
510
  process: (op, local, localOpMetadata) => {
511
+ const { key } = op;
481
512
  if (local) {
482
- const removedLocalOpMetadata = this.pendingMapLocalOpMetadata.shift();
483
- (0, internal_1.assert)(removedLocalOpMetadata !== undefined && removedLocalOpMetadata === localOpMetadata, 0xbce /* Processing unexpected local delete op */);
513
+ const pendingEntryIndex = this.pendingData.findIndex((entry) => entry.type !== "clear" && entry.key === key);
514
+ const pendingEntry = this.pendingData[pendingEntryIndex];
515
+ (0, internal_1.assert)(pendingEntry !== undefined &&
516
+ pendingEntry.type === "delete" &&
517
+ pendingEntry === localOpMetadata, 0xbf7 /* Got a local delete message we weren't expecting */);
518
+ this.pendingData.splice(pendingEntryIndex, 1);
519
+ this.sequencedData.delete(key);
484
520
  }
485
- if (!this.needProcessKeyOperation(op, local, localOpMetadata?.data)) {
486
- return;
521
+ else {
522
+ const previousValue = this.sequencedData.get(key)?.value;
523
+ this.sequencedData.delete(key);
524
+ // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
525
+ if (!this.pendingData.some((entry) => entry.type === "clear" || entry.key === key)) {
526
+ this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
527
+ }
487
528
  }
488
- this.deleteCore(op.key, local);
489
529
  },
490
530
  resubmit: (op, localOpMetadata) => {
491
- const removedLocalOpMetadata = localOpMetadata.remove()?.data;
492
- (0, internal_1.assert)(removedLocalOpMetadata !== undefined, 0xbcf /* Resubmitting unexpected local delete op */);
493
- this.resubmitMapKeyMessage(op, localOpMetadata.data);
531
+ this.submitMessage(op, localOpMetadata);
494
532
  },
495
533
  });
496
534
  messageHandlers.set("set", {
497
535
  process: (op, local, localOpMetadata) => {
536
+ const { key, value } = op;
498
537
  if (local) {
499
- const removedLocalOpMetadata = this.pendingMapLocalOpMetadata.shift();
500
- (0, internal_1.assert)(removedLocalOpMetadata !== undefined && removedLocalOpMetadata === localOpMetadata, 0xbd0 /* Processing unexpected local set op */);
538
+ const pendingEntryIndex = this.pendingData.findIndex((entry) => entry.type !== "clear" && entry.key === key);
539
+ const pendingEntry = this.pendingData[pendingEntryIndex];
540
+ (0, internal_1.assert)(pendingEntry !== undefined && pendingEntry.type === "lifetime", 0xbf8 /* Couldn't match local set message to pending lifetime */);
541
+ const pendingKeySet = pendingEntry.keySets.shift();
542
+ (0, internal_1.assert)(pendingKeySet !== undefined && pendingKeySet === localOpMetadata, 0xbf9 /* Got a local set message we weren't expecting */);
543
+ if (pendingEntry.keySets.length === 0) {
544
+ this.pendingData.splice(pendingEntryIndex, 1);
545
+ }
546
+ this.sequencedData.set(key, pendingKeySet.value);
501
547
  }
502
- if (!this.needProcessKeyOperation(op, local, localOpMetadata?.data)) {
503
- return;
548
+ else {
549
+ (0, localValues_js_1.migrateIfSharedSerializable)(value, this.serializer, this.handle);
550
+ const localValue = { value: value.value };
551
+ const previousValue = this.sequencedData.get(key)?.value;
552
+ this.sequencedData.set(key, localValue);
553
+ // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
554
+ if (!this.pendingData.some((entry) => entry.type === "clear" || entry.key === key)) {
555
+ this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
556
+ }
504
557
  }
505
- // needProcessKeyOperation should have returned false if local is true
506
- (0, localValues_js_1.migrateIfSharedSerializable)(op.value, this.serializer, this.handle);
507
- this.setCore(op.key, { value: op.value.value }, local);
508
558
  },
509
559
  resubmit: (op, localOpMetadata) => {
510
- const removedLocalOpMetadata = localOpMetadata.remove()?.data;
511
- (0, internal_1.assert)(removedLocalOpMetadata !== undefined, 0xbd1 /* Resubmitting unexpected local set op */);
512
- this.resubmitMapKeyMessage(op, localOpMetadata.data);
560
+ this.submitMessage(op, localOpMetadata);
513
561
  },
514
562
  });
515
563
  return messageHandlers;
516
564
  }
517
- getMapClearMessageId() {
518
- const pendingMessageId = this.nextPendingMessageId++;
519
- this.pendingClearMessageIds.push(pendingMessageId);
520
- return pendingMessageId;
521
- }
522
- /**
523
- * Submit a clear message to remote clients.
524
- * @param op - The clear message
525
- */
526
- submitMapClearMessage(op, previousMap) {
527
- const pendingMessageId = this.getMapClearMessageId();
528
- const localMetadata = createClearLocalOpMetadata(op, pendingMessageId, previousMap);
529
- const listNode = this.pendingMapLocalOpMetadata.push(localMetadata).first;
530
- this.submitMessage(op, listNode);
531
- }
532
- getMapKeyMessageId(op) {
533
- const pendingMessageId = this.nextPendingMessageId++;
534
- const pendingMessageIds = this.pendingKeys.get(op.key);
535
- if (pendingMessageIds === undefined) {
536
- this.pendingKeys.set(op.key, [pendingMessageId]);
537
- }
538
- else {
539
- pendingMessageIds.push(pendingMessageId);
540
- }
541
- return pendingMessageId;
542
- }
543
- /**
544
- * Submit a map key message to remote clients.
545
- * @param op - The map key message
546
- * @param previousValue - The value of the key before this op
547
- */
548
- submitMapKeyMessage(op, previousValue) {
549
- const pendingMessageId = this.getMapKeyMessageId(op);
550
- const localMetadata = createKeyLocalOpMetadata(op, pendingMessageId, previousValue);
551
- const listNode = this.pendingMapLocalOpMetadata.push(localMetadata).first;
552
- this.submitMessage(op, listNode);
553
- }
554
- /**
555
- * Submit a map key message to remote clients based on a previous submit.
556
- * @param op - The map key message
557
- * @param localOpMetadata - Metadata from the previous submit
558
- */
559
- resubmitMapKeyMessage(op, localOpMetadata) {
560
- (0, internal_1.assert)(isMapKeyLocalOpMetadata(localOpMetadata), 0x2fe /* Invalid localOpMetadata in submit */);
561
- // no need to submit messages for op's that have been aborted
562
- const pendingMessageIds = this.pendingKeys.get(op.key);
563
- if (pendingMessageIds === undefined) {
564
- return;
565
- }
566
- const index = pendingMessageIds.indexOf(localOpMetadata.pendingMessageId);
567
- if (index === -1) {
568
- return;
569
- }
570
- pendingMessageIds.splice(index, 1);
571
- if (pendingMessageIds.length === 0) {
572
- this.pendingKeys.delete(op.key);
573
- }
574
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
575
- const pendingMessageId = this.getMapKeyMessageId(op);
576
- const localMetadata = localOpMetadata.type === "edit"
577
- ? { type: "edit", pendingMessageId, previousValue: localOpMetadata.previousValue }
578
- : { type: "add", pendingMessageId };
579
- const listNode = this.pendingMapLocalOpMetadata.push(localMetadata).first;
580
- this.submitMessage(op, listNode);
581
- }
582
565
  }
583
566
  exports.MapKernel = MapKernel;
584
567
  //# sourceMappingURL=mapKernel.js.map