@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/src/mapKernel.ts
CHANGED
|
@@ -5,22 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import type { TypedEventEmitter } from "@fluid-internal/client-utils";
|
|
7
7
|
import type { IFluidHandle } from "@fluidframework/core-interfaces";
|
|
8
|
-
import {
|
|
9
|
-
assert,
|
|
10
|
-
DoublyLinkedList,
|
|
11
|
-
type ListNode,
|
|
12
|
-
unreachableCase,
|
|
13
|
-
} from "@fluidframework/core-utils/internal";
|
|
8
|
+
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
|
|
14
9
|
import type { IFluidSerializer } from "@fluidframework/shared-object-base/internal";
|
|
15
10
|
import { ValueType } from "@fluidframework/shared-object-base/internal";
|
|
16
11
|
|
|
17
12
|
import type { ISharedMapEvents } from "./interfaces.js";
|
|
18
13
|
import type {
|
|
19
|
-
IMapClearLocalOpMetadata,
|
|
20
14
|
IMapClearOperation,
|
|
21
15
|
IMapDeleteOperation,
|
|
22
|
-
IMapKeyAddLocalOpMetadata,
|
|
23
|
-
IMapKeyEditLocalOpMetadata,
|
|
24
16
|
IMapSetOperation,
|
|
25
17
|
// eslint-disable-next-line import/no-deprecated
|
|
26
18
|
ISerializableValue,
|
|
@@ -46,7 +38,7 @@ interface IMapMessageHandler {
|
|
|
46
38
|
process(
|
|
47
39
|
op: IMapOperation,
|
|
48
40
|
local: boolean,
|
|
49
|
-
localOpMetadata:
|
|
41
|
+
localOpMetadata: PendingLocalOpMetadata | undefined,
|
|
50
42
|
): void;
|
|
51
43
|
|
|
52
44
|
/**
|
|
@@ -54,18 +46,13 @@ interface IMapMessageHandler {
|
|
|
54
46
|
* @param op - The map operation to resubmit
|
|
55
47
|
* @param localOpMetadata - The metadata that was originally submitted with the message.
|
|
56
48
|
*/
|
|
57
|
-
resubmit(op: IMapOperation, localOpMetadata:
|
|
49
|
+
resubmit(op: IMapOperation, localOpMetadata: PendingLocalOpMetadata): void;
|
|
58
50
|
}
|
|
59
51
|
|
|
60
52
|
/**
|
|
61
|
-
*
|
|
62
|
-
*/
|
|
63
|
-
export type IMapKeyOperation = IMapSetOperation | IMapDeleteOperation;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Description of a map delta operation
|
|
53
|
+
* Union of all possible map operations.
|
|
67
54
|
*/
|
|
68
|
-
export type IMapOperation =
|
|
55
|
+
export type IMapOperation = IMapSetOperation | IMapDeleteOperation | IMapClearOperation;
|
|
69
56
|
|
|
70
57
|
/**
|
|
71
58
|
* Defines the in-memory object structure to be used for the conversion to/from serialized.
|
|
@@ -83,53 +70,61 @@ export type IMapDataObjectSerializable = Record<string, ISerializableValue>;
|
|
|
83
70
|
*/
|
|
84
71
|
export type IMapDataObjectSerialized = Record<string, ISerializedValue>;
|
|
85
72
|
|
|
86
|
-
|
|
87
|
-
type
|
|
88
|
-
|
|
89
|
-
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
90
|
-
|
|
91
|
-
function isMapKeyLocalOpMetadata(metadata: any): metadata is MapKeyLocalOpMetadata {
|
|
92
|
-
return (
|
|
93
|
-
metadata !== undefined &&
|
|
94
|
-
typeof metadata.pendingMessageId === "number" &&
|
|
95
|
-
(metadata.type === "add" || metadata.type === "edit")
|
|
96
|
-
);
|
|
73
|
+
interface PendingKeySet {
|
|
74
|
+
type: "set";
|
|
75
|
+
value: ILocalValue;
|
|
97
76
|
}
|
|
98
77
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
metadata.type === "clear" &&
|
|
103
|
-
typeof metadata.pendingMessageId === "number"
|
|
104
|
-
);
|
|
78
|
+
interface PendingKeyDelete {
|
|
79
|
+
type: "delete";
|
|
80
|
+
key: string;
|
|
105
81
|
}
|
|
106
82
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
function createClearLocalOpMetadata(
|
|
110
|
-
op: IMapClearOperation,
|
|
111
|
-
pendingClearMessageId: number,
|
|
112
|
-
previousMap?: Map<string, ILocalValue>,
|
|
113
|
-
): IMapClearLocalOpMetadata {
|
|
114
|
-
const localMetadata: IMapClearLocalOpMetadata = {
|
|
115
|
-
type: "clear",
|
|
116
|
-
pendingMessageId: pendingClearMessageId,
|
|
117
|
-
previousMap,
|
|
118
|
-
};
|
|
119
|
-
return localMetadata;
|
|
83
|
+
interface PendingClear {
|
|
84
|
+
type: "clear";
|
|
120
85
|
}
|
|
121
86
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
87
|
+
interface PendingKeyLifetime {
|
|
88
|
+
type: "lifetime";
|
|
89
|
+
key: string;
|
|
90
|
+
/**
|
|
91
|
+
* A non-empty array of pending key sets that occurred during this lifetime. If the list
|
|
92
|
+
* becomes empty (e.g. during processing or rollback), the lifetime no longer exists and
|
|
93
|
+
* must be removed from the pending data.
|
|
94
|
+
*/
|
|
95
|
+
keySets: PendingKeySet[];
|
|
131
96
|
}
|
|
132
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A member of the pendingData array, which tracks outstanding changes and can be used to
|
|
100
|
+
* compute optimistic values. Local sets are aggregated into lifetimes.
|
|
101
|
+
*/
|
|
102
|
+
type PendingDataEntry = PendingKeyLifetime | PendingKeyDelete | PendingClear;
|
|
103
|
+
/**
|
|
104
|
+
* An individual outstanding change, which will also be found in the pendingData array
|
|
105
|
+
* (though the PendingKeySets will be contained within a PendingKeyLifetime there).
|
|
106
|
+
*/
|
|
107
|
+
type PendingLocalOpMetadata = PendingKeySet | PendingKeyDelete | PendingClear;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Rough polyfill for Array.findLastIndex until we target ES2023 or greater.
|
|
111
|
+
*/
|
|
112
|
+
const findLastIndex = <T>(array: T[], callbackFn: (value: T) => boolean): number => {
|
|
113
|
+
for (let i = array.length - 1; i >= 0; i--) {
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
115
|
+
if (callbackFn(array[i]!)) {
|
|
116
|
+
return i;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return -1;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Rough polyfill for Array.findLast until we target ES2023 or greater.
|
|
124
|
+
*/
|
|
125
|
+
const findLast = <T>(array: T[], callbackFn: (value: T) => boolean): T | undefined =>
|
|
126
|
+
array[findLastIndex(array, callbackFn)];
|
|
127
|
+
|
|
133
128
|
/**
|
|
134
129
|
* A SharedMap is a map-like distributed data structure.
|
|
135
130
|
*/
|
|
@@ -138,7 +133,8 @@ export class MapKernel {
|
|
|
138
133
|
* The number of key/value pairs stored in the map.
|
|
139
134
|
*/
|
|
140
135
|
public get size(): number {
|
|
141
|
-
|
|
136
|
+
const iterableItems = [...this.internalIterator()];
|
|
137
|
+
return iterableItems.length;
|
|
142
138
|
}
|
|
143
139
|
|
|
144
140
|
/**
|
|
@@ -147,30 +143,18 @@ export class MapKernel {
|
|
|
147
143
|
private readonly messageHandlers: ReadonlyMap<string, IMapMessageHandler> = new Map();
|
|
148
144
|
|
|
149
145
|
/**
|
|
150
|
-
* The
|
|
146
|
+
* The data the map is storing, but only including sequenced values (no local pending
|
|
147
|
+
* modifications are included).
|
|
151
148
|
*/
|
|
152
|
-
private readonly
|
|
153
|
-
|
|
149
|
+
private readonly sequencedData = new Map<string, ILocalValue>();
|
|
154
150
|
/**
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
* This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
|
|
161
|
-
*/
|
|
162
|
-
private nextPendingMessageId: number = 0;
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* The pending metadata for any local operations that have not yet been ack'd from the server, in order.
|
|
166
|
-
*/
|
|
167
|
-
private readonly pendingMapLocalOpMetadata: DoublyLinkedList<MapLocalOpMetadata> =
|
|
168
|
-
new DoublyLinkedList<MapLocalOpMetadata>();
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* The pending ids of any clears that have been performed locally but not yet ack'd from the server
|
|
151
|
+
* A data structure containing all local pending modifications, which is used in combination
|
|
152
|
+
* with the sequencedData to compute optimistic values.
|
|
153
|
+
*
|
|
154
|
+
* Pending sets are aggregated into "lifetimes", which permit correct relative iteration order
|
|
155
|
+
* even across remote operations and rollbacks.
|
|
172
156
|
*/
|
|
173
|
-
private readonly
|
|
157
|
+
private readonly pendingData: PendingDataEntry[] = [];
|
|
174
158
|
|
|
175
159
|
/**
|
|
176
160
|
* Create a new shared map kernel.
|
|
@@ -192,27 +176,112 @@ export class MapKernel {
|
|
|
192
176
|
}
|
|
193
177
|
|
|
194
178
|
/**
|
|
195
|
-
* Get an iterator over the
|
|
196
|
-
*
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
179
|
+
* Get an iterator over the optimistically observable ILocalValue entries in the map. For example, excluding
|
|
180
|
+
* sequenced entries that have pending deletes/clears.
|
|
181
|
+
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* There is no perfect solution here, particularly when the iterator is retained over time and the map is
|
|
184
|
+
* modified or new ack's are received. The pendingData portion of the iteration is the most susceptible to
|
|
185
|
+
* this problem. The implementation prioritizes (in roughly this order):
|
|
186
|
+
* 1. Correct immediate iteration (i.e. when the map is not modified before iteration completes)
|
|
187
|
+
* 2. Consistent iteration order before/after sequencing of pending ops; acks don't change order
|
|
188
|
+
* 3. Consistent iteration order between synchronized clients, even if they each modified the map concurrently
|
|
189
|
+
* 4. Remaining as close as possible to the native Map iterator behavior, e.g. live-ish view rather than snapshot
|
|
190
|
+
*
|
|
191
|
+
* For this reason, it's important not to internally snapshot the output of the iterator for any purpose that
|
|
192
|
+
* does not immediately (synchronously) consume that output and dispose of it.
|
|
193
|
+
*/
|
|
194
|
+
private readonly internalIterator = (): IterableIterator<[string, ILocalValue]> => {
|
|
195
|
+
// We perform iteration in two steps - first by iterating over members of the sequenced data that are not
|
|
196
|
+
// optimistically deleted or cleared, and then over the pending data lifetimes that have not subsequently
|
|
197
|
+
// been deleted or cleared. In total, this give an ordering of members based on when they were initially
|
|
198
|
+
// added to the map (even if they were later modified), similar to the native Map.
|
|
199
|
+
const sequencedDataIterator = this.sequencedData.keys();
|
|
200
|
+
const pendingDataIterator = this.pendingData.values();
|
|
201
|
+
const next = (): IteratorResult<[string, ILocalValue]> => {
|
|
202
|
+
let nextSequencedKey = sequencedDataIterator.next();
|
|
203
|
+
while (!nextSequencedKey.done) {
|
|
204
|
+
const key = nextSequencedKey.value;
|
|
205
|
+
// If we have any pending deletes or clears, then we won't iterate to this key yet (if at all).
|
|
206
|
+
// Either it is optimistically deleted and will not be part of the iteration, or it was
|
|
207
|
+
// re-added later and we'll iterate to it when we get to the pending data.
|
|
208
|
+
if (
|
|
209
|
+
!this.pendingData.some(
|
|
210
|
+
(entry) =>
|
|
211
|
+
entry.type === "clear" || (entry.type === "delete" && entry.key === key),
|
|
212
|
+
)
|
|
213
|
+
) {
|
|
214
|
+
const optimisticValue = this.getOptimisticLocalValue(key);
|
|
215
|
+
assert(
|
|
216
|
+
optimisticValue !== undefined,
|
|
217
|
+
0xbf1 /* Should never iterate to a key with undefined optimisticValue */,
|
|
218
|
+
);
|
|
219
|
+
return { value: [key, optimisticValue], done: false };
|
|
220
|
+
}
|
|
221
|
+
nextSequencedKey = sequencedDataIterator.next();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let nextPending = pendingDataIterator.next();
|
|
225
|
+
while (!nextPending.done) {
|
|
226
|
+
const nextPendingEntry = nextPending.value;
|
|
227
|
+
// A lifetime entry may need to be iterated.
|
|
228
|
+
if (nextPendingEntry.type === "lifetime") {
|
|
229
|
+
const nextPendingEntryIndex = this.pendingData.indexOf(nextPendingEntry);
|
|
230
|
+
const mostRecentDeleteOrClearIndex = findLastIndex(
|
|
231
|
+
this.pendingData,
|
|
232
|
+
(entry) =>
|
|
233
|
+
entry.type === "clear" ||
|
|
234
|
+
(entry.type === "delete" && entry.key === nextPendingEntry.key),
|
|
235
|
+
);
|
|
236
|
+
// Only iterate the pending entry now if it hasn't been deleted or cleared.
|
|
237
|
+
if (nextPendingEntryIndex > mostRecentDeleteOrClearIndex) {
|
|
238
|
+
const latestPendingValue =
|
|
239
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
240
|
+
nextPendingEntry.keySets[nextPendingEntry.keySets.length - 1]!;
|
|
241
|
+
// Skip iterating if we would have would have already iterated it as part of the sequenced data.
|
|
242
|
+
// This is not a perfect check in the case the map has changed since the iterator was created
|
|
243
|
+
// (e.g. if a remote client added the same key in the meantime).
|
|
244
|
+
if (
|
|
245
|
+
!this.sequencedData.has(nextPendingEntry.key) ||
|
|
246
|
+
mostRecentDeleteOrClearIndex !== -1
|
|
247
|
+
) {
|
|
248
|
+
return { value: [nextPendingEntry.key, latestPendingValue.value], done: false };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
nextPending = pendingDataIterator.next();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { value: undefined, done: true };
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const iterator = {
|
|
259
|
+
next,
|
|
260
|
+
[Symbol.iterator](): IterableIterator<[string, ILocalValue]> {
|
|
261
|
+
return this;
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
return iterator;
|
|
265
|
+
};
|
|
201
266
|
|
|
202
267
|
/**
|
|
203
268
|
* Get an iterator over the entries in this map.
|
|
204
269
|
* @returns The iterator
|
|
205
270
|
*/
|
|
206
271
|
public entries(): IterableIterator<[string, unknown]> {
|
|
207
|
-
const
|
|
272
|
+
const internalIterator = this.internalIterator();
|
|
273
|
+
const next = (): IteratorResult<[string, unknown]> => {
|
|
274
|
+
const nextResult = internalIterator.next();
|
|
275
|
+
if (nextResult.done) {
|
|
276
|
+
return { value: undefined, done: true };
|
|
277
|
+
}
|
|
278
|
+
// Unpack the stored value
|
|
279
|
+
const [key, localValue] = nextResult.value;
|
|
280
|
+
return { value: [key, localValue.value], done: false };
|
|
281
|
+
};
|
|
282
|
+
|
|
208
283
|
const iterator = {
|
|
209
|
-
next
|
|
210
|
-
const nextVal = localEntriesIterator.next();
|
|
211
|
-
return nextVal.done
|
|
212
|
-
? { value: undefined, done: true }
|
|
213
|
-
: // Unpack the stored value
|
|
214
|
-
{ value: [nextVal.value[0], nextVal.value[1].value], done: false };
|
|
215
|
-
},
|
|
284
|
+
next,
|
|
216
285
|
[Symbol.iterator](): IterableIterator<[string, unknown]> {
|
|
217
286
|
return this;
|
|
218
287
|
},
|
|
@@ -220,20 +289,45 @@ export class MapKernel {
|
|
|
220
289
|
return iterator;
|
|
221
290
|
}
|
|
222
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Get an iterator over the keys in this map.
|
|
294
|
+
* @returns The iterator
|
|
295
|
+
*/
|
|
296
|
+
public keys(): IterableIterator<string> {
|
|
297
|
+
const internalIterator = this.internalIterator();
|
|
298
|
+
const next = (): IteratorResult<string> => {
|
|
299
|
+
const nextResult = internalIterator.next();
|
|
300
|
+
if (nextResult.done) {
|
|
301
|
+
return { value: undefined, done: true };
|
|
302
|
+
}
|
|
303
|
+
const [key] = nextResult.value;
|
|
304
|
+
return { value: key, done: false };
|
|
305
|
+
};
|
|
306
|
+
const iterator = {
|
|
307
|
+
next,
|
|
308
|
+
[Symbol.iterator](): IterableIterator<string> {
|
|
309
|
+
return this;
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
return iterator;
|
|
313
|
+
}
|
|
314
|
+
|
|
223
315
|
/**
|
|
224
316
|
* Get an iterator over the values in this map.
|
|
225
317
|
* @returns The iterator
|
|
226
318
|
*/
|
|
227
319
|
public values(): IterableIterator<unknown> {
|
|
228
|
-
const
|
|
320
|
+
const internalIterator = this.internalIterator();
|
|
321
|
+
const next = (): IteratorResult<unknown> => {
|
|
322
|
+
const nextResult = internalIterator.next();
|
|
323
|
+
if (nextResult.done) {
|
|
324
|
+
return { value: undefined, done: true };
|
|
325
|
+
}
|
|
326
|
+
const [, localValue] = nextResult.value;
|
|
327
|
+
return { value: localValue.value, done: false };
|
|
328
|
+
};
|
|
229
329
|
const iterator = {
|
|
230
|
-
next
|
|
231
|
-
const nextVal = localValuesIterator.next();
|
|
232
|
-
return nextVal.done
|
|
233
|
-
? { value: undefined, done: true }
|
|
234
|
-
: // Unpack the stored value
|
|
235
|
-
{ value: nextVal.value.value, done: false };
|
|
236
|
-
},
|
|
330
|
+
next,
|
|
237
331
|
[Symbol.iterator](): IterableIterator<unknown> {
|
|
238
332
|
return this;
|
|
239
333
|
},
|
|
@@ -256,17 +350,44 @@ export class MapKernel {
|
|
|
256
350
|
public forEach(
|
|
257
351
|
callbackFn: (value: unknown, key: string, map: Map<string, unknown>) => void,
|
|
258
352
|
): void {
|
|
353
|
+
// It would be better to iterate over the data without a temp map. However, we don't have a valid
|
|
354
|
+
// map to pass for the third argument here (really, it should probably should be a reference to the
|
|
355
|
+
// SharedMap and not the MapKernel).
|
|
356
|
+
const tempMap = new Map(this.internalIterator());
|
|
259
357
|
// eslint-disable-next-line unicorn/no-array-for-each
|
|
260
|
-
|
|
358
|
+
tempMap.forEach((localValue, key, m) => {
|
|
261
359
|
callbackFn(localValue.value, key, m);
|
|
262
360
|
});
|
|
263
361
|
}
|
|
264
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Compute the optimistic local value for a given key. This combines the sequenced data with
|
|
365
|
+
* any pending changes that have not yet been sequenced.
|
|
366
|
+
*/
|
|
367
|
+
private readonly getOptimisticLocalValue = (key: string): ILocalValue | undefined => {
|
|
368
|
+
const latestPendingEntry = findLast(
|
|
369
|
+
this.pendingData,
|
|
370
|
+
(entry) => entry.type === "clear" || entry.key === key,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (latestPendingEntry === undefined) {
|
|
374
|
+
return this.sequencedData.get(key);
|
|
375
|
+
} else if (latestPendingEntry.type === "lifetime") {
|
|
376
|
+
const latestPendingSet =
|
|
377
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
378
|
+
latestPendingEntry.keySets[latestPendingEntry.keySets.length - 1]!;
|
|
379
|
+
return latestPendingSet.value;
|
|
380
|
+
} else {
|
|
381
|
+
// Delete or clear
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
265
386
|
/**
|
|
266
387
|
* {@inheritDoc ISharedMap.get}
|
|
267
388
|
*/
|
|
268
389
|
public get<T = unknown>(key: string): T | undefined {
|
|
269
|
-
const localValue = this.
|
|
390
|
+
const localValue = this.getOptimisticLocalValue(key);
|
|
270
391
|
return localValue === undefined ? undefined : (localValue.value as T);
|
|
271
392
|
}
|
|
272
393
|
|
|
@@ -276,7 +397,7 @@ export class MapKernel {
|
|
|
276
397
|
* @returns True if the key exists, false otherwise
|
|
277
398
|
*/
|
|
278
399
|
public has(key: string): boolean {
|
|
279
|
-
return this.
|
|
400
|
+
return this.getOptimisticLocalValue(key) !== undefined;
|
|
280
401
|
}
|
|
281
402
|
|
|
282
403
|
/**
|
|
@@ -288,20 +409,55 @@ export class MapKernel {
|
|
|
288
409
|
throw new Error("Undefined and null keys are not supported");
|
|
289
410
|
}
|
|
290
411
|
|
|
291
|
-
|
|
292
|
-
const
|
|
412
|
+
const localValue: ILocalValue = { value };
|
|
413
|
+
const previousOptimisticLocalValue = this.getOptimisticLocalValue(key);
|
|
293
414
|
|
|
294
415
|
// If we are not attached, don't submit the op.
|
|
295
416
|
if (!this.isAttached()) {
|
|
417
|
+
this.sequencedData.set(key, localValue);
|
|
418
|
+
this.eventEmitter.emit(
|
|
419
|
+
"valueChanged",
|
|
420
|
+
{ key, previousValue: previousOptimisticLocalValue?.value },
|
|
421
|
+
true,
|
|
422
|
+
this.eventEmitter,
|
|
423
|
+
);
|
|
296
424
|
return;
|
|
297
425
|
}
|
|
298
426
|
|
|
427
|
+
// A new pending key lifetime is created if:
|
|
428
|
+
// 1. There isn't any pending entry for the key yet
|
|
429
|
+
// 2. The most recent pending entry for the key was a deletion (as this terminates the prior lifetime)
|
|
430
|
+
// 3. A clear was sent after the last pending entry for the key (which also terminates the prior lifetime)
|
|
431
|
+
let latestPendingEntry = findLast(
|
|
432
|
+
this.pendingData,
|
|
433
|
+
(entry) => entry.type === "clear" || entry.key === key,
|
|
434
|
+
);
|
|
435
|
+
if (
|
|
436
|
+
latestPendingEntry === undefined ||
|
|
437
|
+
latestPendingEntry.type === "delete" ||
|
|
438
|
+
latestPendingEntry.type === "clear"
|
|
439
|
+
) {
|
|
440
|
+
latestPendingEntry = { type: "lifetime", key, keySets: [] };
|
|
441
|
+
this.pendingData.push(latestPendingEntry);
|
|
442
|
+
}
|
|
443
|
+
const pendingKeySet: PendingKeySet = {
|
|
444
|
+
type: "set",
|
|
445
|
+
value: localValue,
|
|
446
|
+
};
|
|
447
|
+
latestPendingEntry.keySets.push(pendingKeySet);
|
|
448
|
+
|
|
299
449
|
const op: IMapSetOperation = {
|
|
300
450
|
key,
|
|
301
451
|
type: "set",
|
|
302
|
-
value: { type: ValueType[ValueType.Plain], value },
|
|
452
|
+
value: { type: ValueType[ValueType.Plain], value: localValue.value },
|
|
303
453
|
};
|
|
304
|
-
this.
|
|
454
|
+
this.submitMessage(op, pendingKeySet);
|
|
455
|
+
this.eventEmitter.emit(
|
|
456
|
+
"valueChanged",
|
|
457
|
+
{ key, previousValue: previousOptimisticLocalValue?.value },
|
|
458
|
+
true,
|
|
459
|
+
this.eventEmitter,
|
|
460
|
+
);
|
|
305
461
|
}
|
|
306
462
|
|
|
307
463
|
/**
|
|
@@ -310,44 +466,68 @@ export class MapKernel {
|
|
|
310
466
|
* @returns True if the key existed and was deleted, false if it did not exist
|
|
311
467
|
*/
|
|
312
468
|
public delete(key: string): boolean {
|
|
313
|
-
|
|
314
|
-
const previousValue = this.deleteCore(key, true);
|
|
469
|
+
const previousOptimisticLocalValue = this.getOptimisticLocalValue(key);
|
|
315
470
|
|
|
316
|
-
// If we are not attached, don't submit the op.
|
|
317
471
|
if (!this.isAttached()) {
|
|
318
|
-
|
|
472
|
+
const successfullyRemoved = this.sequencedData.delete(key);
|
|
473
|
+
// Only emit if we actually deleted something.
|
|
474
|
+
if (previousOptimisticLocalValue !== undefined) {
|
|
475
|
+
this.eventEmitter.emit(
|
|
476
|
+
"valueChanged",
|
|
477
|
+
{ key, previousValue: previousOptimisticLocalValue.value },
|
|
478
|
+
true,
|
|
479
|
+
this.eventEmitter,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
return successfullyRemoved;
|
|
319
483
|
}
|
|
320
484
|
|
|
485
|
+
const pendingKeyDelete: PendingKeyDelete = {
|
|
486
|
+
type: "delete",
|
|
487
|
+
key,
|
|
488
|
+
};
|
|
489
|
+
this.pendingData.push(pendingKeyDelete);
|
|
490
|
+
|
|
321
491
|
const op: IMapDeleteOperation = {
|
|
322
492
|
key,
|
|
323
493
|
type: "delete",
|
|
324
494
|
};
|
|
325
|
-
this.
|
|
495
|
+
this.submitMessage(op, pendingKeyDelete);
|
|
496
|
+
// Only emit if we locally believe we deleted something. Otherwise we still send the op
|
|
497
|
+
// (permitting speculative deletion even if we don't see anything locally) but don't emit
|
|
498
|
+
// a valueChanged since we in fact did not locally observe a value change.
|
|
499
|
+
if (previousOptimisticLocalValue !== undefined) {
|
|
500
|
+
this.eventEmitter.emit(
|
|
501
|
+
"valueChanged",
|
|
502
|
+
{ key, previousValue: previousOptimisticLocalValue.value },
|
|
503
|
+
true,
|
|
504
|
+
this.eventEmitter,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
326
507
|
|
|
327
|
-
return
|
|
508
|
+
return true;
|
|
328
509
|
}
|
|
329
510
|
|
|
330
511
|
/**
|
|
331
512
|
* Clear all data from the map.
|
|
332
513
|
*/
|
|
333
514
|
public clear(): void {
|
|
334
|
-
const copy = this.isAttached() ? new Map<string, ILocalValue>(this.data) : undefined;
|
|
335
|
-
|
|
336
|
-
// Clear the data locally first.
|
|
337
|
-
this.clearCore(true);
|
|
338
|
-
|
|
339
|
-
// Clear the pendingKeys immediately, the local unack'd operations are aborted
|
|
340
|
-
this.pendingKeys.clear();
|
|
341
|
-
|
|
342
|
-
// If we are not attached, don't submit the op.
|
|
343
515
|
if (!this.isAttached()) {
|
|
516
|
+
this.sequencedData.clear();
|
|
517
|
+
this.eventEmitter.emit("clear", true, this.eventEmitter);
|
|
344
518
|
return;
|
|
345
519
|
}
|
|
346
520
|
|
|
521
|
+
const pendingClear: PendingClear = {
|
|
522
|
+
type: "clear",
|
|
523
|
+
};
|
|
524
|
+
this.pendingData.push(pendingClear);
|
|
525
|
+
|
|
347
526
|
const op: IMapClearOperation = {
|
|
348
527
|
type: "clear",
|
|
349
528
|
};
|
|
350
|
-
this.
|
|
529
|
+
this.submitMessage(op, pendingClear);
|
|
530
|
+
this.eventEmitter.emit("clear", true, this.eventEmitter);
|
|
351
531
|
}
|
|
352
532
|
|
|
353
533
|
/**
|
|
@@ -357,7 +537,7 @@ export class MapKernel {
|
|
|
357
537
|
*/
|
|
358
538
|
public getSerializedStorage(serializer: IFluidSerializer): IMapDataObjectSerialized {
|
|
359
539
|
const serializedMapData: IMapDataObjectSerialized = {};
|
|
360
|
-
for (const [key, localValue] of this.
|
|
540
|
+
for (const [key, localValue] of this.sequencedData.entries()) {
|
|
361
541
|
serializedMapData[key] = serializeValue(localValue.value, serializer, this.handle);
|
|
362
542
|
}
|
|
363
543
|
return serializedMapData;
|
|
@@ -372,7 +552,7 @@ export class MapKernel {
|
|
|
372
552
|
this.serializer.decode(json) as IMapDataObjectSerializable,
|
|
373
553
|
)) {
|
|
374
554
|
migrateIfSharedSerializable(serializable, this.serializer, this.handle);
|
|
375
|
-
this.
|
|
555
|
+
this.sequencedData.set(key, { value: serializable.value });
|
|
376
556
|
}
|
|
377
557
|
}
|
|
378
558
|
|
|
@@ -389,7 +569,7 @@ export class MapKernel {
|
|
|
389
569
|
if (handler === undefined) {
|
|
390
570
|
return false;
|
|
391
571
|
}
|
|
392
|
-
handler.resubmit(op, localOpMetadata as
|
|
572
|
+
handler.resubmit(op, localOpMetadata as PendingLocalOpMetadata);
|
|
393
573
|
return true;
|
|
394
574
|
}
|
|
395
575
|
|
|
@@ -438,7 +618,7 @@ export class MapKernel {
|
|
|
438
618
|
if (handler === undefined) {
|
|
439
619
|
return false;
|
|
440
620
|
}
|
|
441
|
-
handler.process(op, local, localOpMetadata as
|
|
621
|
+
handler.process(op, local, localOpMetadata as PendingLocalOpMetadata | undefined);
|
|
442
622
|
return true;
|
|
443
623
|
}
|
|
444
624
|
|
|
@@ -449,167 +629,66 @@ export class MapKernel {
|
|
|
449
629
|
*/
|
|
450
630
|
public rollback(op: unknown, localOpMetadata: unknown): void {
|
|
451
631
|
const mapOp: IMapOperation = op as IMapOperation;
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const lastPendingClearId = this.pendingClearMessageIds.pop();
|
|
469
|
-
if (
|
|
470
|
-
lastPendingClearId === undefined ||
|
|
471
|
-
lastPendingClearId !== listNodeLocalOpMetadata.data.pendingMessageId
|
|
472
|
-
) {
|
|
473
|
-
throw new Error("Rollback op does match last clear");
|
|
474
|
-
}
|
|
475
|
-
} else if (mapOp.type === "delete" || mapOp.type === "set") {
|
|
476
|
-
if (listNodeLocalOpMetadata.data.type === "add") {
|
|
477
|
-
this.deleteCore(mapOp.key, true);
|
|
478
|
-
} else if (
|
|
479
|
-
listNodeLocalOpMetadata.data.type === "edit" &&
|
|
480
|
-
listNodeLocalOpMetadata.data.previousValue !== undefined
|
|
481
|
-
) {
|
|
482
|
-
this.setCore(mapOp.key, listNodeLocalOpMetadata.data.previousValue, true);
|
|
483
|
-
} else {
|
|
484
|
-
throw new Error("Cannot rollback without previous value");
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const pendingMessageIds = this.pendingKeys.get(mapOp.key);
|
|
488
|
-
const lastPendingMessageId = pendingMessageIds?.pop();
|
|
489
|
-
if (
|
|
490
|
-
!pendingMessageIds ||
|
|
491
|
-
lastPendingMessageId !== listNodeLocalOpMetadata.data.pendingMessageId
|
|
492
|
-
) {
|
|
493
|
-
throw new Error("Rollback op does not match last pending");
|
|
494
|
-
}
|
|
495
|
-
if (pendingMessageIds.length === 0) {
|
|
496
|
-
this.pendingKeys.delete(mapOp.key);
|
|
497
|
-
}
|
|
498
|
-
} else {
|
|
499
|
-
throw new Error("Unsupported op for rollback");
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Set implementation used for both locally sourced sets as well as incoming remote sets.
|
|
505
|
-
* @param key - The key being set
|
|
506
|
-
* @param value - The value being set
|
|
507
|
-
* @param local - Whether the message originated from the local client
|
|
508
|
-
* @returns Previous local value of the key, if any
|
|
509
|
-
*/
|
|
510
|
-
private setCore(key: string, value: ILocalValue, local: boolean): ILocalValue | undefined {
|
|
511
|
-
const previousLocalValue = this.data.get(key);
|
|
512
|
-
const previousValue: unknown = previousLocalValue?.value;
|
|
513
|
-
this.data.set(key, value);
|
|
514
|
-
this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
|
|
515
|
-
return previousLocalValue;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Clear implementation used for both locally sourced clears as well as incoming remote clears.
|
|
520
|
-
* @param local - Whether the message originated from the local client
|
|
521
|
-
*/
|
|
522
|
-
private clearCore(local: boolean): void {
|
|
523
|
-
this.data.clear();
|
|
524
|
-
this.eventEmitter.emit("clear", local, this.eventEmitter);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
|
|
529
|
-
* @param key - The key being deleted
|
|
530
|
-
* @param local - Whether the message originated from the local client
|
|
531
|
-
* @returns Previous local value of the key if it existed, undefined if it did not exist
|
|
532
|
-
*/
|
|
533
|
-
private deleteCore(key: string, local: boolean): ILocalValue | undefined {
|
|
534
|
-
const previousLocalValue = this.data.get(key);
|
|
535
|
-
const previousValue: unknown = previousLocalValue?.value;
|
|
536
|
-
const successfullyRemoved = this.data.delete(key);
|
|
537
|
-
if (successfullyRemoved) {
|
|
538
|
-
this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
|
|
539
|
-
}
|
|
540
|
-
return previousLocalValue;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
|
|
545
|
-
*/
|
|
546
|
-
private clearExceptPendingKeys(): void {
|
|
547
|
-
// Assuming the pendingKeys is small and the map is large
|
|
548
|
-
// we will get the value for the pendingKeys and clear the map
|
|
549
|
-
const temp = new Map<string, ILocalValue>();
|
|
550
|
-
for (const key of this.pendingKeys.keys()) {
|
|
551
|
-
// Verify if the most recent pending operation is a delete op, no need to retain it if so.
|
|
552
|
-
// This ensures the map size remains consistent.
|
|
553
|
-
if (this.data.has(key)) {
|
|
554
|
-
temp.set(key, this.data.get(key) as ILocalValue);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
this.clearCore(false);
|
|
558
|
-
for (const [key, value] of temp.entries()) {
|
|
559
|
-
this.setCore(key, value, true);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
|
|
565
|
-
* not process the incoming operation.
|
|
566
|
-
* @param op - Operation to check
|
|
567
|
-
* @param local - Whether the message originated from the local client
|
|
568
|
-
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
569
|
-
* For messages from a remote client, this will be undefined.
|
|
570
|
-
* @returns True if the operation should be processed, false otherwise
|
|
571
|
-
*/
|
|
572
|
-
private needProcessKeyOperation(
|
|
573
|
-
op: IMapKeyOperation,
|
|
574
|
-
local: boolean,
|
|
575
|
-
localOpMetadata: MapLocalOpMetadata | undefined,
|
|
576
|
-
): boolean {
|
|
577
|
-
if (this.pendingClearMessageIds[0] !== undefined) {
|
|
578
|
-
if (local) {
|
|
579
|
-
assert(
|
|
580
|
-
localOpMetadata !== undefined &&
|
|
581
|
-
isMapKeyLocalOpMetadata(localOpMetadata) &&
|
|
582
|
-
localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
|
|
583
|
-
0x013 /* "Received out of order op when there is an unackd clear message" */,
|
|
632
|
+
const typedLocalOpMetadata = localOpMetadata as PendingLocalOpMetadata;
|
|
633
|
+
if (mapOp.type === "clear") {
|
|
634
|
+
// A pending clear will be last in the list, since it terminates all prior lifetimes.
|
|
635
|
+
const pendingClear = this.pendingData.pop();
|
|
636
|
+
assert(
|
|
637
|
+
pendingClear !== undefined &&
|
|
638
|
+
pendingClear.type === "clear" &&
|
|
639
|
+
pendingClear === typedLocalOpMetadata,
|
|
640
|
+
0xbf2 /* Unexpected clear rollback */,
|
|
641
|
+
);
|
|
642
|
+
for (const [key] of this.internalIterator()) {
|
|
643
|
+
this.eventEmitter.emit(
|
|
644
|
+
"valueChanged",
|
|
645
|
+
{ key, previousValue: undefined },
|
|
646
|
+
true,
|
|
647
|
+
this.eventEmitter,
|
|
584
648
|
);
|
|
585
649
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
650
|
+
} else {
|
|
651
|
+
// A pending set/delete may not be last in the list, as the lifetimes' order is based on when
|
|
652
|
+
// they were created, not when they were last modified.
|
|
653
|
+
const pendingEntryIndex = findLastIndex(
|
|
654
|
+
this.pendingData,
|
|
655
|
+
(entry) => entry.type !== "clear" && entry.key === mapOp.key,
|
|
656
|
+
);
|
|
657
|
+
const pendingEntry = this.pendingData[pendingEntryIndex];
|
|
658
|
+
assert(
|
|
659
|
+
pendingEntry !== undefined &&
|
|
660
|
+
(pendingEntry.type === "delete" || pendingEntry.type === "lifetime"),
|
|
661
|
+
0xbf3 /* Unexpected pending data for set/delete op */,
|
|
662
|
+
);
|
|
663
|
+
if (pendingEntry.type === "delete") {
|
|
664
|
+
assert(pendingEntry === typedLocalOpMetadata, 0xbf4 /* Unexpected delete rollback */);
|
|
665
|
+
this.pendingData.splice(pendingEntryIndex, 1);
|
|
666
|
+
// Only emit if rolling back the delete actually results in a value becoming visible.
|
|
667
|
+
if (this.getOptimisticLocalValue(mapOp.key) !== undefined) {
|
|
668
|
+
this.eventEmitter.emit(
|
|
669
|
+
"valueChanged",
|
|
670
|
+
{ key: mapOp.key, previousValue: undefined },
|
|
671
|
+
true,
|
|
672
|
+
this.eventEmitter,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
} else if (pendingEntry.type === "lifetime") {
|
|
676
|
+
const pendingKeySet = pendingEntry.keySets.pop();
|
|
599
677
|
assert(
|
|
600
|
-
|
|
601
|
-
|
|
678
|
+
pendingKeySet !== undefined && pendingKeySet === typedLocalOpMetadata,
|
|
679
|
+
0xbf5 /* Unexpected set rollback */,
|
|
602
680
|
);
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
this.pendingKeys.delete(op.key);
|
|
681
|
+
if (pendingEntry.keySets.length === 0) {
|
|
682
|
+
this.pendingData.splice(pendingEntryIndex, 1);
|
|
606
683
|
}
|
|
684
|
+
this.eventEmitter.emit(
|
|
685
|
+
"valueChanged",
|
|
686
|
+
{ key: mapOp.key, previousValue: pendingKeySet.value.value },
|
|
687
|
+
true,
|
|
688
|
+
this.eventEmitter,
|
|
689
|
+
);
|
|
607
690
|
}
|
|
608
|
-
return false;
|
|
609
691
|
}
|
|
610
|
-
|
|
611
|
-
// If we don't have a NACK op on the key, we need to process the remote ops.
|
|
612
|
-
return !local;
|
|
613
692
|
}
|
|
614
693
|
|
|
615
694
|
/**
|
|
@@ -622,191 +701,118 @@ export class MapKernel {
|
|
|
622
701
|
process: (
|
|
623
702
|
op: IMapClearOperation,
|
|
624
703
|
local: boolean,
|
|
625
|
-
localOpMetadata:
|
|
704
|
+
localOpMetadata: PendingLocalOpMetadata | undefined,
|
|
626
705
|
) => {
|
|
706
|
+
this.sequencedData.clear();
|
|
627
707
|
if (local) {
|
|
628
|
-
const
|
|
708
|
+
const pendingClear = this.pendingData.shift();
|
|
629
709
|
assert(
|
|
630
|
-
|
|
631
|
-
|
|
710
|
+
pendingClear !== undefined &&
|
|
711
|
+
pendingClear.type === "clear" &&
|
|
712
|
+
pendingClear === localOpMetadata,
|
|
713
|
+
0xbf6 /* Got a local clear message we weren't expecting */,
|
|
632
714
|
);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
pendingClearMessageId === localOpMetadata.data.pendingMessageId,
|
|
640
|
-
0x2fb /* pendingMessageId does not match */,
|
|
641
|
-
);
|
|
642
|
-
return;
|
|
715
|
+
} else {
|
|
716
|
+
// Only emit for remote ops, we would have already emitted for local ops. Only emit if there
|
|
717
|
+
// is no optimistically-applied local pending clear that would supersede this remote clear.
|
|
718
|
+
if (!this.pendingData.some((entry) => entry.type === "clear")) {
|
|
719
|
+
this.eventEmitter.emit("clear", local, this.eventEmitter);
|
|
720
|
+
}
|
|
643
721
|
}
|
|
644
|
-
if (this.pendingKeys.size > 0) {
|
|
645
|
-
this.clearExceptPendingKeys();
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
this.clearCore(local);
|
|
649
722
|
},
|
|
650
|
-
resubmit: (op: IMapClearOperation, localOpMetadata:
|
|
651
|
-
|
|
652
|
-
assert(
|
|
653
|
-
removedLocalOpMetadata !== undefined,
|
|
654
|
-
0xbcd /* Resubmitting unexpected local clear op */,
|
|
655
|
-
);
|
|
656
|
-
assert(
|
|
657
|
-
isClearLocalOpMetadata(localOpMetadata.data),
|
|
658
|
-
0x2fc /* Invalid localOpMetadata for clear */,
|
|
659
|
-
);
|
|
660
|
-
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
661
|
-
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
662
|
-
assert(
|
|
663
|
-
pendingClearMessageId === localOpMetadata.data.pendingMessageId,
|
|
664
|
-
0x2fd /* pendingMessageId does not match */,
|
|
665
|
-
);
|
|
666
|
-
this.submitMapClearMessage(op, localOpMetadata.data.previousMap);
|
|
723
|
+
resubmit: (op: IMapClearOperation, localOpMetadata: PendingLocalOpMetadata) => {
|
|
724
|
+
this.submitMessage(op, localOpMetadata);
|
|
667
725
|
},
|
|
668
726
|
});
|
|
669
727
|
messageHandlers.set("delete", {
|
|
670
728
|
process: (
|
|
671
729
|
op: IMapDeleteOperation,
|
|
672
730
|
local: boolean,
|
|
673
|
-
localOpMetadata:
|
|
731
|
+
localOpMetadata: PendingLocalOpMetadata | undefined,
|
|
674
732
|
) => {
|
|
733
|
+
const { key } = op;
|
|
734
|
+
|
|
675
735
|
if (local) {
|
|
676
|
-
const
|
|
736
|
+
const pendingEntryIndex = this.pendingData.findIndex(
|
|
737
|
+
(entry) => entry.type !== "clear" && entry.key === key,
|
|
738
|
+
);
|
|
739
|
+
const pendingEntry = this.pendingData[pendingEntryIndex];
|
|
677
740
|
assert(
|
|
678
|
-
|
|
679
|
-
|
|
741
|
+
pendingEntry !== undefined &&
|
|
742
|
+
pendingEntry.type === "delete" &&
|
|
743
|
+
pendingEntry === localOpMetadata,
|
|
744
|
+
0xbf7 /* Got a local delete message we weren't expecting */,
|
|
680
745
|
);
|
|
746
|
+
this.pendingData.splice(pendingEntryIndex, 1);
|
|
747
|
+
|
|
748
|
+
this.sequencedData.delete(key);
|
|
749
|
+
} else {
|
|
750
|
+
const previousValue: unknown = this.sequencedData.get(key)?.value;
|
|
751
|
+
this.sequencedData.delete(key);
|
|
752
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
753
|
+
if (!this.pendingData.some((entry) => entry.type === "clear" || entry.key === key)) {
|
|
754
|
+
this.eventEmitter.emit(
|
|
755
|
+
"valueChanged",
|
|
756
|
+
{ key, previousValue },
|
|
757
|
+
local,
|
|
758
|
+
this.eventEmitter,
|
|
759
|
+
);
|
|
760
|
+
}
|
|
681
761
|
}
|
|
682
|
-
if (!this.needProcessKeyOperation(op, local, localOpMetadata?.data)) {
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
this.deleteCore(op.key, local);
|
|
686
762
|
},
|
|
687
|
-
resubmit: (op: IMapDeleteOperation, localOpMetadata:
|
|
688
|
-
|
|
689
|
-
assert(
|
|
690
|
-
removedLocalOpMetadata !== undefined,
|
|
691
|
-
0xbcf /* Resubmitting unexpected local delete op */,
|
|
692
|
-
);
|
|
693
|
-
this.resubmitMapKeyMessage(op, localOpMetadata.data);
|
|
763
|
+
resubmit: (op: IMapDeleteOperation, localOpMetadata: PendingLocalOpMetadata) => {
|
|
764
|
+
this.submitMessage(op, localOpMetadata);
|
|
694
765
|
},
|
|
695
766
|
});
|
|
696
767
|
messageHandlers.set("set", {
|
|
697
768
|
process: (
|
|
698
769
|
op: IMapSetOperation,
|
|
699
770
|
local: boolean,
|
|
700
|
-
localOpMetadata:
|
|
771
|
+
localOpMetadata: PendingLocalOpMetadata | undefined,
|
|
701
772
|
) => {
|
|
773
|
+
const { key, value } = op;
|
|
774
|
+
|
|
702
775
|
if (local) {
|
|
703
|
-
const
|
|
776
|
+
const pendingEntryIndex = this.pendingData.findIndex(
|
|
777
|
+
(entry) => entry.type !== "clear" && entry.key === key,
|
|
778
|
+
);
|
|
779
|
+
const pendingEntry = this.pendingData[pendingEntryIndex];
|
|
704
780
|
assert(
|
|
705
|
-
|
|
706
|
-
|
|
781
|
+
pendingEntry !== undefined && pendingEntry.type === "lifetime",
|
|
782
|
+
0xbf8 /* Couldn't match local set message to pending lifetime */,
|
|
707
783
|
);
|
|
784
|
+
const pendingKeySet = pendingEntry.keySets.shift();
|
|
785
|
+
assert(
|
|
786
|
+
pendingKeySet !== undefined && pendingKeySet === localOpMetadata,
|
|
787
|
+
0xbf9 /* Got a local set message we weren't expecting */,
|
|
788
|
+
);
|
|
789
|
+
if (pendingEntry.keySets.length === 0) {
|
|
790
|
+
this.pendingData.splice(pendingEntryIndex, 1);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
this.sequencedData.set(key, pendingKeySet.value);
|
|
794
|
+
} else {
|
|
795
|
+
migrateIfSharedSerializable(value, this.serializer, this.handle);
|
|
796
|
+
const localValue: ILocalValue = { value: value.value };
|
|
797
|
+
const previousValue: unknown = this.sequencedData.get(key)?.value;
|
|
798
|
+
this.sequencedData.set(key, localValue);
|
|
799
|
+
|
|
800
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
801
|
+
if (!this.pendingData.some((entry) => entry.type === "clear" || entry.key === key)) {
|
|
802
|
+
this.eventEmitter.emit(
|
|
803
|
+
"valueChanged",
|
|
804
|
+
{ key, previousValue },
|
|
805
|
+
local,
|
|
806
|
+
this.eventEmitter,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
708
809
|
}
|
|
709
|
-
if (!this.needProcessKeyOperation(op, local, localOpMetadata?.data)) {
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// needProcessKeyOperation should have returned false if local is true
|
|
714
|
-
migrateIfSharedSerializable(op.value, this.serializer, this.handle);
|
|
715
|
-
this.setCore(op.key, { value: op.value.value }, local);
|
|
716
810
|
},
|
|
717
|
-
resubmit: (op: IMapSetOperation, localOpMetadata:
|
|
718
|
-
|
|
719
|
-
assert(
|
|
720
|
-
removedLocalOpMetadata !== undefined,
|
|
721
|
-
0xbd1 /* Resubmitting unexpected local set op */,
|
|
722
|
-
);
|
|
723
|
-
this.resubmitMapKeyMessage(op, localOpMetadata.data);
|
|
811
|
+
resubmit: (op: IMapSetOperation, localOpMetadata: PendingLocalOpMetadata) => {
|
|
812
|
+
this.submitMessage(op, localOpMetadata);
|
|
724
813
|
},
|
|
725
814
|
});
|
|
726
815
|
|
|
727
816
|
return messageHandlers;
|
|
728
817
|
}
|
|
729
|
-
|
|
730
|
-
private getMapClearMessageId(): number {
|
|
731
|
-
const pendingMessageId = this.nextPendingMessageId++;
|
|
732
|
-
this.pendingClearMessageIds.push(pendingMessageId);
|
|
733
|
-
return pendingMessageId;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* Submit a clear message to remote clients.
|
|
738
|
-
* @param op - The clear message
|
|
739
|
-
*/
|
|
740
|
-
private submitMapClearMessage(
|
|
741
|
-
op: IMapClearOperation,
|
|
742
|
-
previousMap?: Map<string, ILocalValue>,
|
|
743
|
-
): void {
|
|
744
|
-
const pendingMessageId = this.getMapClearMessageId();
|
|
745
|
-
const localMetadata = createClearLocalOpMetadata(op, pendingMessageId, previousMap);
|
|
746
|
-
const listNode = this.pendingMapLocalOpMetadata.push(localMetadata).first;
|
|
747
|
-
this.submitMessage(op, listNode);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
private getMapKeyMessageId(op: IMapKeyOperation): number {
|
|
751
|
-
const pendingMessageId = this.nextPendingMessageId++;
|
|
752
|
-
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
753
|
-
if (pendingMessageIds === undefined) {
|
|
754
|
-
this.pendingKeys.set(op.key, [pendingMessageId]);
|
|
755
|
-
} else {
|
|
756
|
-
pendingMessageIds.push(pendingMessageId);
|
|
757
|
-
}
|
|
758
|
-
return pendingMessageId;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Submit a map key message to remote clients.
|
|
763
|
-
* @param op - The map key message
|
|
764
|
-
* @param previousValue - The value of the key before this op
|
|
765
|
-
*/
|
|
766
|
-
private submitMapKeyMessage(op: IMapKeyOperation, previousValue?: ILocalValue): void {
|
|
767
|
-
const pendingMessageId = this.getMapKeyMessageId(op);
|
|
768
|
-
const localMetadata = createKeyLocalOpMetadata(op, pendingMessageId, previousValue);
|
|
769
|
-
const listNode = this.pendingMapLocalOpMetadata.push(localMetadata).first;
|
|
770
|
-
this.submitMessage(op, listNode);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/**
|
|
774
|
-
* Submit a map key message to remote clients based on a previous submit.
|
|
775
|
-
* @param op - The map key message
|
|
776
|
-
* @param localOpMetadata - Metadata from the previous submit
|
|
777
|
-
*/
|
|
778
|
-
private resubmitMapKeyMessage(
|
|
779
|
-
op: IMapKeyOperation,
|
|
780
|
-
localOpMetadata: MapLocalOpMetadata,
|
|
781
|
-
): void {
|
|
782
|
-
assert(
|
|
783
|
-
isMapKeyLocalOpMetadata(localOpMetadata),
|
|
784
|
-
0x2fe /* Invalid localOpMetadata in submit */,
|
|
785
|
-
);
|
|
786
|
-
|
|
787
|
-
// no need to submit messages for op's that have been aborted
|
|
788
|
-
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
789
|
-
if (pendingMessageIds === undefined) {
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const index = pendingMessageIds.indexOf(localOpMetadata.pendingMessageId);
|
|
794
|
-
if (index === -1) {
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
pendingMessageIds.splice(index, 1);
|
|
799
|
-
if (pendingMessageIds.length === 0) {
|
|
800
|
-
this.pendingKeys.delete(op.key);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
804
|
-
const pendingMessageId = this.getMapKeyMessageId(op);
|
|
805
|
-
const localMetadata: MapKeyLocalOpMetadata =
|
|
806
|
-
localOpMetadata.type === "edit"
|
|
807
|
-
? { type: "edit", pendingMessageId, previousValue: localOpMetadata.previousValue }
|
|
808
|
-
: { type: "add", pendingMessageId };
|
|
809
|
-
const listNode = this.pendingMapLocalOpMetadata.push(localMetadata).first;
|
|
810
|
-
this.submitMessage(op, listNode);
|
|
811
|
-
}
|
|
812
818
|
}
|