@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/CHANGELOG.md +4 -0
- package/dist/internalInterfaces.d.ts +0 -56
- package/dist/internalInterfaces.d.ts.map +1 -1
- package/dist/internalInterfaces.js.map +1 -1
- package/dist/mapKernel.d.ts +36 -78
- package/dist/mapKernel.d.ts.map +1 -1
- package/dist/mapKernel.js +291 -308
- package/dist/mapKernel.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/lib/internalInterfaces.d.ts +0 -56
- package/lib/internalInterfaces.d.ts.map +1 -1
- package/lib/internalInterfaces.js.map +1 -1
- package/lib/mapKernel.d.ts +36 -78
- package/lib/mapKernel.d.ts.map +1 -1
- package/lib/mapKernel.js +292 -309
- package/lib/mapKernel.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/package.json +20 -20
- package/src/internalInterfaces.ts +0 -67
- package/src/mapKernel.ts +440 -434
- package/src/packageVersion.ts +1 -1
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,
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
58
|
+
this.sequencedData = new Map();
|
|
71
59
|
/**
|
|
72
|
-
*
|
|
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.
|
|
66
|
+
this.pendingData = [];
|
|
75
67
|
/**
|
|
76
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
161
|
+
* Get an iterator over the entries in this map.
|
|
87
162
|
* @returns The iterator
|
|
88
163
|
*/
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
184
|
+
* Get an iterator over the keys in this map.
|
|
94
185
|
* @returns The iterator
|
|
95
186
|
*/
|
|
96
|
-
|
|
97
|
-
const
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
173
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
202
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
454
|
-
assert(
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
|
480
|
-
|
|
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
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
|
497
|
-
|
|
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
|
-
|
|
500
|
-
|
|
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
|
-
|
|
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
|