@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/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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
61
|
+
this.sequencedData = new Map();
|
|
74
62
|
/**
|
|
75
|
-
*
|
|
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.
|
|
69
|
+
this.pendingData = [];
|
|
78
70
|
/**
|
|
79
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
164
|
+
* Get an iterator over the entries in this map.
|
|
90
165
|
* @returns The iterator
|
|
91
166
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
187
|
+
* Get an iterator over the keys in this map.
|
|
97
188
|
* @returns The iterator
|
|
98
189
|
*/
|
|
99
|
-
|
|
100
|
-
const
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
176
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
205
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
457
|
-
(0, internal_1.assert)(
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
|
483
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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
|
|
500
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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
|