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