@fluidframework/map 1.0.0 → 1.1.0-76254
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/directory.js +4 -4
- package/dist/directory.js.map +1 -1
- package/dist/interfaces.d.ts +13 -14
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/map.d.ts +5 -0
- package/dist/map.d.ts.map +1 -1
- package/dist/map.js +7 -0
- package/dist/map.js.map +1 -1
- package/dist/mapKernel.d.ts +20 -11
- package/dist/mapKernel.d.ts.map +1 -1
- package/dist/mapKernel.js +160 -82
- package/dist/mapKernel.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/lib/directory.js +4 -4
- package/lib/directory.js.map +1 -1
- package/lib/interfaces.d.ts +13 -14
- package/lib/interfaces.d.ts.map +1 -1
- package/lib/interfaces.js.map +1 -1
- package/lib/map.d.ts +5 -0
- package/lib/map.d.ts.map +1 -1
- package/lib/map.js +7 -0
- package/lib/map.js.map +1 -1
- package/lib/mapKernel.d.ts +20 -11
- package/lib/mapKernel.d.ts.map +1 -1
- package/lib/mapKernel.js +160 -82
- package/lib/mapKernel.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/package.json +15 -15
- package/src/directory.ts +4 -4
- package/src/interfaces.ts +14 -16
- package/src/map.ts +8 -0
- package/src/mapKernel.ts +195 -90
- package/src/packageVersion.ts +1 -1
package/src/mapKernel.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
|
|
|
9
9
|
import {
|
|
10
10
|
ISerializableValue,
|
|
11
11
|
ISerializedValue,
|
|
12
|
-
IValueChanged,
|
|
13
12
|
ISharedMapEvents,
|
|
14
13
|
} from "./interfaces";
|
|
15
14
|
import {
|
|
@@ -112,6 +111,40 @@ export interface IMapDataObjectSerialized {
|
|
|
112
111
|
[key: string]: ISerializedValue;
|
|
113
112
|
}
|
|
114
113
|
|
|
114
|
+
interface IMapKeyEditLocalOpMetadata {
|
|
115
|
+
type: "edit";
|
|
116
|
+
pendingMessageId: number;
|
|
117
|
+
previousValue?: ILocalValue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface IMapKeyAddLocalOpMetadata {
|
|
121
|
+
type: "add";
|
|
122
|
+
pendingMessageId: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface IMapClearLocalOpMetadata {
|
|
126
|
+
type: "clear";
|
|
127
|
+
pendingMessageId: number;
|
|
128
|
+
previousMap?: Map<string, ILocalValue>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type MapKeyLocalOpMetadata = IMapKeyEditLocalOpMetadata | IMapKeyAddLocalOpMetadata;
|
|
132
|
+
type MapLocalOpMetadata = IMapClearLocalOpMetadata | MapKeyLocalOpMetadata;
|
|
133
|
+
|
|
134
|
+
function isMapKeyLocalOpMetadata(metadata: any): metadata is MapKeyLocalOpMetadata {
|
|
135
|
+
return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
|
|
136
|
+
(metadata.type === "add" || metadata.type === "edit");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isClearLocalOpMetadata(metadata: any): metadata is IMapClearLocalOpMetadata {
|
|
140
|
+
return metadata !== undefined && metadata.type === "clear" && typeof metadata.pendingMessageId === "number";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isMapLocalOpMetadata(metadata: any): metadata is MapLocalOpMetadata {
|
|
144
|
+
return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
|
|
145
|
+
(metadata.type === "add" || metadata.type === "edit" || metadata.type === "clear");
|
|
146
|
+
}
|
|
147
|
+
|
|
115
148
|
/**
|
|
116
149
|
* A SharedMap is a map-like distributed data structure.
|
|
117
150
|
*/
|
|
@@ -136,7 +169,7 @@ export class MapKernel {
|
|
|
136
169
|
/**
|
|
137
170
|
* Keys that have been modified locally but not yet ack'd from the server.
|
|
138
171
|
*/
|
|
139
|
-
private readonly pendingKeys: Map<string, number> = new Map();
|
|
172
|
+
private readonly pendingKeys: Map<string, number[]> = new Map();
|
|
140
173
|
|
|
141
174
|
/**
|
|
142
175
|
* This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
|
|
@@ -144,10 +177,9 @@ export class MapKernel {
|
|
|
144
177
|
private pendingMessageId: number = -1;
|
|
145
178
|
|
|
146
179
|
/**
|
|
147
|
-
*
|
|
148
|
-
* of that clear operation. Otherwise, is -1.
|
|
180
|
+
* The pending ids of any clears that have been performed locally but not yet ack'd from the server
|
|
149
181
|
*/
|
|
150
|
-
private
|
|
182
|
+
private readonly pendingClearMessageIds: number[] = [];
|
|
151
183
|
|
|
152
184
|
/**
|
|
153
185
|
* Object to create encapsulations of the values stored in the map.
|
|
@@ -250,14 +282,8 @@ export class MapKernel {
|
|
|
250
282
|
* {@inheritDoc ISharedMap.get}
|
|
251
283
|
*/
|
|
252
284
|
public get<T = any>(key: string): T | undefined {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
258
|
-
const localValue = this.data.get(key)!;
|
|
259
|
-
|
|
260
|
-
return localValue.value as T;
|
|
285
|
+
const localValue = this.data.get(key);
|
|
286
|
+
return localValue === undefined ? undefined : localValue.value as T;
|
|
261
287
|
}
|
|
262
288
|
|
|
263
289
|
/**
|
|
@@ -286,7 +312,7 @@ export class MapKernel {
|
|
|
286
312
|
this.handle);
|
|
287
313
|
|
|
288
314
|
// Set the value locally.
|
|
289
|
-
this.setCore(
|
|
315
|
+
const previousValue = this.setCore(
|
|
290
316
|
key,
|
|
291
317
|
localValue,
|
|
292
318
|
true,
|
|
@@ -302,7 +328,7 @@ export class MapKernel {
|
|
|
302
328
|
type: "set",
|
|
303
329
|
value: serializableValue,
|
|
304
330
|
};
|
|
305
|
-
this.submitMapKeyMessage(op);
|
|
331
|
+
this.submitMapKeyMessage(op, previousValue);
|
|
306
332
|
}
|
|
307
333
|
|
|
308
334
|
/**
|
|
@@ -312,26 +338,28 @@ export class MapKernel {
|
|
|
312
338
|
*/
|
|
313
339
|
public delete(key: string): boolean {
|
|
314
340
|
// Delete the key locally first.
|
|
315
|
-
const
|
|
341
|
+
const previousValue = this.deleteCore(key, true);
|
|
316
342
|
|
|
317
343
|
// If we are not attached, don't submit the op.
|
|
318
344
|
if (!this.isAttached()) {
|
|
319
|
-
return
|
|
345
|
+
return previousValue !== undefined;
|
|
320
346
|
}
|
|
321
347
|
|
|
322
348
|
const op: IMapDeleteOperation = {
|
|
323
349
|
key,
|
|
324
350
|
type: "delete",
|
|
325
351
|
};
|
|
326
|
-
this.submitMapKeyMessage(op);
|
|
352
|
+
this.submitMapKeyMessage(op, previousValue);
|
|
327
353
|
|
|
328
|
-
return
|
|
354
|
+
return previousValue !== undefined;
|
|
329
355
|
}
|
|
330
356
|
|
|
331
357
|
/**
|
|
332
358
|
* Clear all data from the map.
|
|
333
359
|
*/
|
|
334
360
|
public clear(): void {
|
|
361
|
+
const copy = this.isAttached() ? new Map<string, ILocalValue>(this.data) : undefined;
|
|
362
|
+
|
|
335
363
|
// Clear the data locally first.
|
|
336
364
|
this.clearCore(true);
|
|
337
365
|
|
|
@@ -343,7 +371,7 @@ export class MapKernel {
|
|
|
343
371
|
const op: IMapClearOperation = {
|
|
344
372
|
type: "clear",
|
|
345
373
|
};
|
|
346
|
-
this.submitMapClearMessage(op);
|
|
374
|
+
this.submitMapClearMessage(op, copy);
|
|
347
375
|
}
|
|
348
376
|
|
|
349
377
|
/**
|
|
@@ -399,27 +427,25 @@ export class MapKernel {
|
|
|
399
427
|
* @returns True if the operation was submitted, false otherwise.
|
|
400
428
|
*/
|
|
401
429
|
public trySubmitMessage(op: any, localOpMetadata: unknown): boolean {
|
|
402
|
-
const
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
this.messageHandlers.get(type)!.submit(op as IMapOperation, localOpMetadata);
|
|
406
|
-
return true;
|
|
430
|
+
const handler = this.messageHandlers.get(op.type);
|
|
431
|
+
if (handler === undefined) {
|
|
432
|
+
return false;
|
|
407
433
|
}
|
|
408
|
-
|
|
434
|
+
handler.submit(op as IMapOperation, localOpMetadata);
|
|
435
|
+
return true;
|
|
409
436
|
}
|
|
410
437
|
|
|
411
438
|
public tryGetStashedOpLocalMetadata(op: any): unknown {
|
|
412
|
-
const
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
return this.messageHandlers.get(type)!.getStashedOpLocalMetadata(op as IMapOperation);
|
|
439
|
+
const handler = this.messageHandlers.get(op.type);
|
|
440
|
+
if (handler === undefined) {
|
|
441
|
+
throw new Error("no apply stashed op handler");
|
|
416
442
|
}
|
|
417
|
-
|
|
443
|
+
return handler.getStashedOpLocalMetadata(op as IMapOperation);
|
|
418
444
|
}
|
|
419
445
|
|
|
420
446
|
/**
|
|
421
447
|
* Process the given op if a handler is registered.
|
|
422
|
-
* @param
|
|
448
|
+
* @param op - The message to process
|
|
423
449
|
* @param local - Whether the message originated from the local client
|
|
424
450
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
425
451
|
* For messages from a remote client, this will be undefined.
|
|
@@ -430,14 +456,56 @@ export class MapKernel {
|
|
|
430
456
|
local: boolean,
|
|
431
457
|
localOpMetadata: unknown,
|
|
432
458
|
): boolean {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
459
|
+
const handler = this.messageHandlers.get(op.type);
|
|
460
|
+
if (handler === undefined) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
handler.process(op, local, localOpMetadata);
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Rollback a local op
|
|
469
|
+
* @param op - The operation to rollback
|
|
470
|
+
* @param localOpMetadata - The local metadata associated with the op.
|
|
471
|
+
*/
|
|
472
|
+
public rollback(op: any, localOpMetadata: unknown) {
|
|
473
|
+
if (!isMapLocalOpMetadata(localOpMetadata)) {
|
|
474
|
+
throw new Error("Invalid localOpMetadata");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (op.type === "clear" && localOpMetadata.type === "clear") {
|
|
478
|
+
if (localOpMetadata.previousMap === undefined) {
|
|
479
|
+
throw new Error("Cannot rollback without previous map");
|
|
480
|
+
}
|
|
481
|
+
localOpMetadata.previousMap.forEach((localValue, key) => {
|
|
482
|
+
this.setCore(key, localValue, true);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const lastPendingClearId = this.pendingClearMessageIds.pop();
|
|
486
|
+
if (lastPendingClearId === undefined || lastPendingClearId !== localOpMetadata.pendingMessageId) {
|
|
487
|
+
throw new Error("Rollback op does match last clear");
|
|
488
|
+
}
|
|
489
|
+
} else if (op.type === "delete" || op.type === "set") {
|
|
490
|
+
if (localOpMetadata.type === "add") {
|
|
491
|
+
this.deleteCore(op.key, true);
|
|
492
|
+
} else if (localOpMetadata.type === "edit" && localOpMetadata.previousValue !== undefined) {
|
|
493
|
+
this.setCore(op.key, localOpMetadata.previousValue, true);
|
|
494
|
+
} else {
|
|
495
|
+
throw new Error("Cannot rollback without previous value");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
499
|
+
const lastPendingMessageId = pendingMessageIds?.pop();
|
|
500
|
+
if (!pendingMessageIds || lastPendingMessageId !== localOpMetadata.pendingMessageId) {
|
|
501
|
+
throw new Error("Rollback op does not match last pending");
|
|
502
|
+
}
|
|
503
|
+
if (pendingMessageIds.length === 0) {
|
|
504
|
+
this.pendingKeys.delete(op.key);
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
throw new Error("Unsupported op for rollback");
|
|
439
508
|
}
|
|
440
|
-
return false;
|
|
441
509
|
}
|
|
442
510
|
|
|
443
511
|
/**
|
|
@@ -445,19 +513,19 @@ export class MapKernel {
|
|
|
445
513
|
* @param key - The key being set
|
|
446
514
|
* @param value - The value being set
|
|
447
515
|
* @param local - Whether the message originated from the local client
|
|
448
|
-
* @
|
|
516
|
+
* @returns Previous local value of the key, if any
|
|
449
517
|
*/
|
|
450
|
-
private setCore(key: string, value: ILocalValue, local: boolean):
|
|
451
|
-
const
|
|
518
|
+
private setCore(key: string, value: ILocalValue, local: boolean): ILocalValue | undefined {
|
|
519
|
+
const previousLocalValue = this.data.get(key);
|
|
520
|
+
const previousValue = previousLocalValue?.value;
|
|
452
521
|
this.data.set(key, value);
|
|
453
|
-
|
|
454
|
-
|
|
522
|
+
this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
|
|
523
|
+
return previousLocalValue;
|
|
455
524
|
}
|
|
456
525
|
|
|
457
526
|
/**
|
|
458
527
|
* Clear implementation used for both locally sourced clears as well as incoming remote clears.
|
|
459
528
|
* @param local - Whether the message originated from the local client
|
|
460
|
-
* @param op - The message if from a remote clear, or null if from a local clear
|
|
461
529
|
*/
|
|
462
530
|
private clearCore(local: boolean): void {
|
|
463
531
|
this.data.clear();
|
|
@@ -468,17 +536,16 @@ export class MapKernel {
|
|
|
468
536
|
* Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
|
|
469
537
|
* @param key - The key being deleted
|
|
470
538
|
* @param local - Whether the message originated from the local client
|
|
471
|
-
* @
|
|
472
|
-
* @returns True if the key existed and was deleted, false if it did not exist
|
|
539
|
+
* @returns Previous local value of the key if it existed, undefined if it did not exist
|
|
473
540
|
*/
|
|
474
|
-
private deleteCore(key: string, local: boolean):
|
|
475
|
-
const
|
|
541
|
+
private deleteCore(key: string, local: boolean): ILocalValue | undefined {
|
|
542
|
+
const previousLocalValue = this.data.get(key);
|
|
543
|
+
const previousValue = previousLocalValue?.value;
|
|
476
544
|
const successfullyRemoved = this.data.delete(key);
|
|
477
545
|
if (successfullyRemoved) {
|
|
478
|
-
|
|
479
|
-
this.eventEmitter.emit("valueChanged", event, local, this.eventEmitter);
|
|
546
|
+
this.eventEmitter.emit("valueChanged", { key, previousValue }, local, this.eventEmitter);
|
|
480
547
|
}
|
|
481
|
-
return
|
|
548
|
+
return previousLocalValue;
|
|
482
549
|
}
|
|
483
550
|
|
|
484
551
|
/**
|
|
@@ -492,9 +559,9 @@ export class MapKernel {
|
|
|
492
559
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
493
560
|
temp.set(key, this.data.get(key)!);
|
|
494
561
|
});
|
|
495
|
-
this.
|
|
562
|
+
this.clearCore(false);
|
|
496
563
|
temp.forEach((value, key) => {
|
|
497
|
-
this.
|
|
564
|
+
this.setCore(key, value, true);
|
|
498
565
|
});
|
|
499
566
|
}
|
|
500
567
|
|
|
@@ -521,7 +588,6 @@ export class MapKernel {
|
|
|
521
588
|
* not process the incoming operation.
|
|
522
589
|
* @param op - Operation to check
|
|
523
590
|
* @param local - Whether the message originated from the local client
|
|
524
|
-
* @param message - The message
|
|
525
591
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
526
592
|
* For messages from a remote client, this will be undefined.
|
|
527
593
|
* @returns True if the operation should be processed, false otherwise
|
|
@@ -531,24 +597,28 @@ export class MapKernel {
|
|
|
531
597
|
local: boolean,
|
|
532
598
|
localOpMetadata: unknown,
|
|
533
599
|
): boolean {
|
|
534
|
-
if (this.
|
|
600
|
+
if (this.pendingClearMessageIds.length > 0) {
|
|
535
601
|
if (local) {
|
|
536
|
-
assert(localOpMetadata !== undefined && localOpMetadata
|
|
602
|
+
assert(localOpMetadata !== undefined && isMapKeyLocalOpMetadata(localOpMetadata) &&
|
|
603
|
+
localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
|
|
537
604
|
0x013 /* "Received out of order op when there is an unackd clear message" */);
|
|
538
605
|
}
|
|
539
606
|
// If we have an unack'd clear, we can ignore all ops.
|
|
540
607
|
return false;
|
|
541
608
|
}
|
|
542
609
|
|
|
543
|
-
|
|
610
|
+
const pendingKeyMessageId = this.pendingKeys.get(op.key);
|
|
611
|
+
if (pendingKeyMessageId !== undefined) {
|
|
544
612
|
// Found an unack'd op. Clear it from the map if the pendingMessageId in the map matches this message's
|
|
545
613
|
// and don't process the op.
|
|
546
614
|
if (local) {
|
|
547
|
-
assert(localOpMetadata !== undefined,
|
|
615
|
+
assert(localOpMetadata !== undefined && isMapKeyLocalOpMetadata(localOpMetadata),
|
|
548
616
|
0x014 /* pendingMessageId is missing from the local client's operation */);
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
617
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
618
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
|
|
619
|
+
0x2fa /* Unexpected pending message received */);
|
|
620
|
+
pendingMessageIds.shift();
|
|
621
|
+
if (pendingMessageIds.length === 0) {
|
|
552
622
|
this.pendingKeys.delete(op.key);
|
|
553
623
|
}
|
|
554
624
|
}
|
|
@@ -570,12 +640,11 @@ export class MapKernel {
|
|
|
570
640
|
{
|
|
571
641
|
process: (op: IMapClearOperation, local, localOpMetadata) => {
|
|
572
642
|
if (local) {
|
|
573
|
-
assert(localOpMetadata
|
|
643
|
+
assert(isClearLocalOpMetadata(localOpMetadata),
|
|
574
644
|
0x015 /* "pendingMessageId is missing from the local client's clear operation" */);
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
645
|
+
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
646
|
+
assert(pendingClearMessageId === localOpMetadata.pendingMessageId,
|
|
647
|
+
0x2fb /* pendingMessageId does not match */);
|
|
579
648
|
return;
|
|
580
649
|
}
|
|
581
650
|
if (this.pendingKeys.size !== 0) {
|
|
@@ -585,12 +654,16 @@ export class MapKernel {
|
|
|
585
654
|
this.clearCore(local);
|
|
586
655
|
},
|
|
587
656
|
submit: (op: IMapClearOperation, localOpMetadata: unknown) => {
|
|
588
|
-
|
|
589
|
-
|
|
657
|
+
assert(isClearLocalOpMetadata(localOpMetadata), 0x2fc /* Invalid localOpMetadata for clear */);
|
|
658
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
659
|
+
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
660
|
+
assert(pendingClearMessageId === localOpMetadata.pendingMessageId,
|
|
661
|
+
0x2fd /* pendingMessageId does not match */);
|
|
662
|
+
this.submitMapClearMessage(op, localOpMetadata.previousMap);
|
|
590
663
|
},
|
|
591
664
|
getStashedOpLocalMetadata: (op: IMapClearOperation) => {
|
|
592
|
-
// We don't reuse the metadata but send a new one on each submit.
|
|
593
|
-
return this.
|
|
665
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
666
|
+
return { type: "clear", pendingMessageId: this.getMapClearMessageId() };
|
|
594
667
|
},
|
|
595
668
|
});
|
|
596
669
|
messageHandlers.set(
|
|
@@ -603,12 +676,11 @@ export class MapKernel {
|
|
|
603
676
|
this.deleteCore(op.key, local);
|
|
604
677
|
},
|
|
605
678
|
submit: (op: IMapDeleteOperation, localOpMetadata: unknown) => {
|
|
606
|
-
|
|
607
|
-
this.submitMapKeyMessage(op);
|
|
679
|
+
this.resubmitMapKeyMessage(op, localOpMetadata);
|
|
608
680
|
},
|
|
609
681
|
getStashedOpLocalMetadata: (op: IMapDeleteOperation) => {
|
|
610
|
-
// We don't reuse the metadata but send a new one on each submit.
|
|
611
|
-
return this.
|
|
682
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
683
|
+
return { type: "edit", pendingMessageId: this.getMapKeyMessageId(op) };
|
|
612
684
|
},
|
|
613
685
|
});
|
|
614
686
|
messageHandlers.set(
|
|
@@ -624,21 +696,20 @@ export class MapKernel {
|
|
|
624
696
|
this.setCore(op.key, context, local);
|
|
625
697
|
},
|
|
626
698
|
submit: (op: IMapSetOperation, localOpMetadata: unknown) => {
|
|
627
|
-
|
|
628
|
-
this.submitMapKeyMessage(op);
|
|
699
|
+
this.resubmitMapKeyMessage(op, localOpMetadata);
|
|
629
700
|
},
|
|
630
701
|
getStashedOpLocalMetadata: (op: IMapSetOperation) => {
|
|
631
|
-
// We don't reuse the metadata but send a new one on each submit.
|
|
632
|
-
return this.
|
|
702
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
703
|
+
return { type: "edit", pendingMessageId: this.getMapKeyMessageId(op) };
|
|
633
704
|
},
|
|
634
705
|
});
|
|
635
706
|
|
|
636
707
|
return messageHandlers;
|
|
637
708
|
}
|
|
638
709
|
|
|
639
|
-
private
|
|
710
|
+
private getMapClearMessageId(): number {
|
|
640
711
|
const pendingMessageId = ++this.pendingMessageId;
|
|
641
|
-
this.
|
|
712
|
+
this.pendingClearMessageIds.push(pendingMessageId);
|
|
642
713
|
return pendingMessageId;
|
|
643
714
|
}
|
|
644
715
|
|
|
@@ -646,23 +717,57 @@ export class MapKernel {
|
|
|
646
717
|
* Submit a clear message to remote clients.
|
|
647
718
|
* @param op - The clear message
|
|
648
719
|
*/
|
|
649
|
-
private submitMapClearMessage(op: IMapClearOperation): void {
|
|
650
|
-
const
|
|
651
|
-
this.submitMessage(op,
|
|
720
|
+
private submitMapClearMessage(op: IMapClearOperation, previousMap?: Map<string, ILocalValue>): void {
|
|
721
|
+
const metadata = { type: "clear", pendingMessageId: this.getMapClearMessageId(), previousMap };
|
|
722
|
+
this.submitMessage(op, metadata);
|
|
652
723
|
}
|
|
653
724
|
|
|
654
|
-
private
|
|
725
|
+
private getMapKeyMessageId(op: IMapKeyOperation): number {
|
|
655
726
|
const pendingMessageId = ++this.pendingMessageId;
|
|
656
|
-
this.pendingKeys.
|
|
727
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
728
|
+
if (pendingMessageIds !== undefined) {
|
|
729
|
+
pendingMessageIds.push(pendingMessageId);
|
|
730
|
+
} else {
|
|
731
|
+
this.pendingKeys.set(op.key, [pendingMessageId]);
|
|
732
|
+
}
|
|
657
733
|
return pendingMessageId;
|
|
658
734
|
}
|
|
659
735
|
|
|
660
736
|
/**
|
|
661
737
|
* Submit a map key message to remote clients.
|
|
662
738
|
* @param op - The map key message
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
this.
|
|
739
|
+
* @param previousValue - The value of the key before this op
|
|
740
|
+
*/
|
|
741
|
+
private submitMapKeyMessage(op: IMapKeyOperation, previousValue?: ILocalValue): void {
|
|
742
|
+
const pendingMessageId = this.getMapKeyMessageId(op);
|
|
743
|
+
const localMetadata = previousValue ?
|
|
744
|
+
{ type: "edit", pendingMessageId, previousValue } :
|
|
745
|
+
{ type: "add", pendingMessageId };
|
|
746
|
+
this.submitMessage(op, localMetadata);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Submit a map key message to remote clients based on a previous submit.
|
|
751
|
+
* @param op - The map key message
|
|
752
|
+
* @param localOpMetadata - Metadata from the previous submit
|
|
753
|
+
*/
|
|
754
|
+
private resubmitMapKeyMessage(op: IMapKeyOperation, localOpMetadata: unknown): void {
|
|
755
|
+
assert(isMapKeyLocalOpMetadata(localOpMetadata), 0x2fe /* Invalid localOpMetadata in submit */);
|
|
756
|
+
|
|
757
|
+
// clear the old pending message id
|
|
758
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
759
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
|
|
760
|
+
0x2ff /* Unexpected pending message received */);
|
|
761
|
+
pendingMessageIds.shift();
|
|
762
|
+
if (pendingMessageIds.length === 0) {
|
|
763
|
+
this.pendingKeys.delete(op.key);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
767
|
+
const pendingMessageId = this.getMapKeyMessageId(op);
|
|
768
|
+
const localMetadata = localOpMetadata.type === "edit" ?
|
|
769
|
+
{ type: "edit", pendingMessageId, previousValue: localOpMetadata.previousValue } :
|
|
770
|
+
{ type: "add", pendingMessageId };
|
|
771
|
+
this.submitMessage(op, localMetadata);
|
|
667
772
|
}
|
|
668
773
|
}
|
package/src/packageVersion.ts
CHANGED