@fluidframework/map 1.1.0 → 1.2.0-77818
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.d.ts +5 -0
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +288 -79
- package/dist/directory.js.map +1 -1
- package/dist/interfaces.d.ts +29 -14
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.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.d.ts +5 -0
- package/lib/directory.d.ts.map +1 -1
- package/lib/directory.js +288 -79
- package/lib/directory.js.map +1 -1
- package/lib/interfaces.d.ts +29 -14
- package/lib/interfaces.d.ts.map +1 -1
- package/lib/interfaces.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 +13 -13
- package/src/directory.ts +348 -85
- package/src/interfaces.ts +29 -14
- package/src/packageVersion.ts +1 -1
package/lib/directory.js
CHANGED
|
@@ -364,6 +364,17 @@ export class SharedDirectory extends SharedObject {
|
|
|
364
364
|
handler.process(op, local, localOpMetadata);
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
|
|
369
|
+
* @internal
|
|
370
|
+
*/
|
|
371
|
+
rollback(content, localOpMetadata) {
|
|
372
|
+
const op = content;
|
|
373
|
+
const subdir = this.getWorkingDirectory(op.path);
|
|
374
|
+
if (subdir) {
|
|
375
|
+
subdir.rollback(op, localOpMetadata);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
367
378
|
/**
|
|
368
379
|
* Converts the given relative path to absolute against the root.
|
|
369
380
|
* @param relativePath - The path to convert
|
|
@@ -399,8 +410,7 @@ export class SharedDirectory extends SharedObject {
|
|
|
399
410
|
submit: (op, localOpMetadata) => {
|
|
400
411
|
const subdir = this.getWorkingDirectory(op.path);
|
|
401
412
|
if (subdir) {
|
|
402
|
-
|
|
403
|
-
subdir.submitClearMessage(op);
|
|
413
|
+
subdir.resubmitClearMessage(op, localOpMetadata);
|
|
404
414
|
}
|
|
405
415
|
},
|
|
406
416
|
});
|
|
@@ -414,8 +424,7 @@ export class SharedDirectory extends SharedObject {
|
|
|
414
424
|
submit: (op, localOpMetadata) => {
|
|
415
425
|
const subdir = this.getWorkingDirectory(op.path);
|
|
416
426
|
if (subdir) {
|
|
417
|
-
|
|
418
|
-
subdir.submitKeyMessage(op);
|
|
427
|
+
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
419
428
|
}
|
|
420
429
|
},
|
|
421
430
|
});
|
|
@@ -430,8 +439,7 @@ export class SharedDirectory extends SharedObject {
|
|
|
430
439
|
submit: (op, localOpMetadata) => {
|
|
431
440
|
const subdir = this.getWorkingDirectory(op.path);
|
|
432
441
|
if (subdir) {
|
|
433
|
-
|
|
434
|
-
subdir.submitKeyMessage(op);
|
|
442
|
+
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
435
443
|
}
|
|
436
444
|
},
|
|
437
445
|
});
|
|
@@ -446,7 +454,7 @@ export class SharedDirectory extends SharedObject {
|
|
|
446
454
|
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
447
455
|
if (parentSubdir) {
|
|
448
456
|
// We don't reuse the metadata but send a new one on each submit.
|
|
449
|
-
parentSubdir.
|
|
457
|
+
parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
450
458
|
}
|
|
451
459
|
},
|
|
452
460
|
});
|
|
@@ -461,7 +469,7 @@ export class SharedDirectory extends SharedObject {
|
|
|
461
469
|
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
462
470
|
if (parentSubdir) {
|
|
463
471
|
// We don't reuse the metadata but send a new one on each submit.
|
|
464
|
-
parentSubdir.
|
|
472
|
+
parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
465
473
|
}
|
|
466
474
|
},
|
|
467
475
|
});
|
|
@@ -529,6 +537,24 @@ export class SharedDirectory extends SharedObject {
|
|
|
529
537
|
return builder.getSummaryTree();
|
|
530
538
|
}
|
|
531
539
|
}
|
|
540
|
+
function isKeyEditLocalOpMetadata(metadata) {
|
|
541
|
+
return metadata !== undefined && typeof metadata.pendingMessageId === "number" && metadata.type === "edit";
|
|
542
|
+
}
|
|
543
|
+
function isClearLocalOpMetadata(metadata) {
|
|
544
|
+
return metadata !== undefined && metadata.type === "clear" && typeof metadata.pendingMessageId === "number" &&
|
|
545
|
+
typeof metadata.previousStorage === "object";
|
|
546
|
+
}
|
|
547
|
+
function isSubDirLocalOpMetadata(metadata) {
|
|
548
|
+
return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
|
|
549
|
+
((metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean") ||
|
|
550
|
+
metadata.type === "deleteSubDir");
|
|
551
|
+
}
|
|
552
|
+
function isDirectoryLocalOpMetadata(metadata) {
|
|
553
|
+
return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
|
|
554
|
+
(metadata.type === "edit" || metadata.type === "deleteSubDir" ||
|
|
555
|
+
(metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
|
|
556
|
+
(metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean"));
|
|
557
|
+
}
|
|
532
558
|
/**
|
|
533
559
|
* Node of the directory tree.
|
|
534
560
|
* @sealed
|
|
@@ -548,9 +574,9 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
548
574
|
this.serializer = serializer;
|
|
549
575
|
this.absolutePath = absolutePath;
|
|
550
576
|
/**
|
|
551
|
-
* Tells if the sub directory is
|
|
577
|
+
* Tells if the sub directory is deleted or not.
|
|
552
578
|
*/
|
|
553
|
-
this.
|
|
579
|
+
this._deleted = false;
|
|
554
580
|
/**
|
|
555
581
|
* String representation for the class.
|
|
556
582
|
*/
|
|
@@ -576,20 +602,25 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
576
602
|
*/
|
|
577
603
|
this.pendingMessageId = -1;
|
|
578
604
|
/**
|
|
579
|
-
*
|
|
580
|
-
* of that clear operation. Otherwise, is -1.
|
|
605
|
+
* The pending ids of any clears that have been performed locally but not yet ack'd from the server
|
|
581
606
|
*/
|
|
582
|
-
this.
|
|
607
|
+
this.pendingClearMessageIds = [];
|
|
583
608
|
}
|
|
584
609
|
dispose(error) {
|
|
585
|
-
this.
|
|
610
|
+
this._deleted = true;
|
|
586
611
|
this.emit("disposed", this);
|
|
587
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* Unmark the deleted property when rolling back delete.
|
|
615
|
+
*/
|
|
616
|
+
undispose() {
|
|
617
|
+
this._deleted = false;
|
|
618
|
+
}
|
|
588
619
|
get disposed() {
|
|
589
|
-
return this.
|
|
620
|
+
return this._deleted;
|
|
590
621
|
}
|
|
591
622
|
throwIfDisposed() {
|
|
592
|
-
if (this.
|
|
623
|
+
if (this._deleted) {
|
|
593
624
|
throw new UsageError("Cannot access Disposed subDirectory");
|
|
594
625
|
}
|
|
595
626
|
}
|
|
@@ -623,7 +654,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
623
654
|
const localValue = this.directory.localValueMaker.fromInMemory(value);
|
|
624
655
|
const serializableValue = makeSerializable(localValue, this.serializer, this.directory.handle);
|
|
625
656
|
// Set the value locally.
|
|
626
|
-
this.setCore(key, localValue, true);
|
|
657
|
+
const previousValue = this.setCore(key, localValue, true);
|
|
627
658
|
// If we are not attached, don't submit the op.
|
|
628
659
|
if (!this.directory.isAttached()) {
|
|
629
660
|
return this;
|
|
@@ -634,7 +665,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
634
665
|
type: "set",
|
|
635
666
|
value: serializableValue,
|
|
636
667
|
};
|
|
637
|
-
this.submitKeyMessage(op);
|
|
668
|
+
this.submitKeyMessage(op, previousValue);
|
|
638
669
|
return this;
|
|
639
670
|
}
|
|
640
671
|
/**
|
|
@@ -656,7 +687,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
656
687
|
throw new Error(`SubDirectory name may not contain ${posix.sep}`);
|
|
657
688
|
}
|
|
658
689
|
// Create the sub directory locally first.
|
|
659
|
-
this.createSubDirectoryCore(subdirName, true);
|
|
690
|
+
const isNew = this.createSubDirectoryCore(subdirName, true);
|
|
660
691
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
661
692
|
const subDir = this._subdirectories.get(subdirName);
|
|
662
693
|
// If we are not attached, don't submit the op.
|
|
@@ -668,7 +699,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
668
699
|
subdirName,
|
|
669
700
|
type: "createSubDirectory",
|
|
670
701
|
};
|
|
671
|
-
this.
|
|
702
|
+
this.submitCreateSubDirectoryMessage(op, !isNew);
|
|
672
703
|
return subDir;
|
|
673
704
|
}
|
|
674
705
|
/**
|
|
@@ -691,18 +722,18 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
691
722
|
deleteSubDirectory(subdirName) {
|
|
692
723
|
this.throwIfDisposed();
|
|
693
724
|
// Delete the sub directory locally first.
|
|
694
|
-
const
|
|
725
|
+
const subDir = this.deleteSubDirectoryCore(subdirName, true);
|
|
695
726
|
// If we are not attached, don't submit the op.
|
|
696
727
|
if (!this.directory.isAttached()) {
|
|
697
|
-
return
|
|
728
|
+
return subDir !== undefined;
|
|
698
729
|
}
|
|
699
730
|
const op = {
|
|
700
731
|
path: this.absolutePath,
|
|
701
732
|
subdirName,
|
|
702
733
|
type: "deleteSubDirectory",
|
|
703
734
|
};
|
|
704
|
-
this.
|
|
705
|
-
return
|
|
735
|
+
this.submitDeleteSubDirectoryMessage(op, subDir);
|
|
736
|
+
return subDir !== undefined;
|
|
706
737
|
}
|
|
707
738
|
/**
|
|
708
739
|
* {@inheritDoc IDirectory.subdirectories}
|
|
@@ -726,35 +757,36 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
726
757
|
delete(key) {
|
|
727
758
|
this.throwIfDisposed();
|
|
728
759
|
// Delete the key locally first.
|
|
729
|
-
const
|
|
760
|
+
const previousValue = this.deleteCore(key, true);
|
|
730
761
|
// If we are not attached, don't submit the op.
|
|
731
762
|
if (!this.directory.isAttached()) {
|
|
732
|
-
return
|
|
763
|
+
return previousValue !== undefined;
|
|
733
764
|
}
|
|
734
765
|
const op = {
|
|
735
766
|
key,
|
|
736
767
|
path: this.absolutePath,
|
|
737
768
|
type: "delete",
|
|
738
769
|
};
|
|
739
|
-
this.submitKeyMessage(op);
|
|
740
|
-
return
|
|
770
|
+
this.submitKeyMessage(op, previousValue);
|
|
771
|
+
return previousValue !== undefined;
|
|
741
772
|
}
|
|
742
773
|
/**
|
|
743
774
|
* Deletes all keys from within this IDirectory.
|
|
744
775
|
*/
|
|
745
776
|
clear() {
|
|
746
777
|
this.throwIfDisposed();
|
|
747
|
-
// Clear the data locally first.
|
|
748
|
-
this.clearCore(true);
|
|
749
778
|
// If we are not attached, don't submit the op.
|
|
750
779
|
if (!this.directory.isAttached()) {
|
|
780
|
+
this.clearCore(true);
|
|
751
781
|
return;
|
|
752
782
|
}
|
|
783
|
+
const copy = new Map(this._storage);
|
|
784
|
+
this.clearCore(true);
|
|
753
785
|
const op = {
|
|
754
786
|
path: this.absolutePath,
|
|
755
787
|
type: "clear",
|
|
756
788
|
};
|
|
757
|
-
this.submitClearMessage(op);
|
|
789
|
+
this.submitClearMessage(op, copy);
|
|
758
790
|
}
|
|
759
791
|
/**
|
|
760
792
|
* Issue a callback on each entry under this IDirectory.
|
|
@@ -849,15 +881,12 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
849
881
|
processClearMessage(op, local, localOpMetadata) {
|
|
850
882
|
this.throwIfDisposed();
|
|
851
883
|
if (local) {
|
|
852
|
-
assert(localOpMetadata
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
this.pendingClearMessageId = -1;
|
|
856
|
-
}
|
|
884
|
+
assert(isClearLocalOpMetadata(localOpMetadata), 0x00f /* `pendingMessageId is missing from the local client's ${op.type} operation` */);
|
|
885
|
+
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
886
|
+
assert(pendingClearMessageId === localOpMetadata.pendingMessageId, "pendingMessageId does not match");
|
|
857
887
|
return;
|
|
858
888
|
}
|
|
859
889
|
this.clearExceptPendingKeys();
|
|
860
|
-
this.directory.emit("clear", local, this.directory);
|
|
861
890
|
}
|
|
862
891
|
/**
|
|
863
892
|
* Process a delete operation.
|
|
@@ -905,7 +934,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
905
934
|
*/
|
|
906
935
|
processCreateSubDirectoryMessage(op, local, localOpMetadata) {
|
|
907
936
|
this.throwIfDisposed();
|
|
908
|
-
if (!this.
|
|
937
|
+
if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
|
|
909
938
|
return;
|
|
910
939
|
}
|
|
911
940
|
this.createSubDirectoryCore(op.subdirName, local);
|
|
@@ -921,7 +950,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
921
950
|
*/
|
|
922
951
|
processDeleteSubDirectoryMessage(op, local, localOpMetadata) {
|
|
923
952
|
this.throwIfDisposed();
|
|
924
|
-
if (!this.
|
|
953
|
+
if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
|
|
925
954
|
return;
|
|
926
955
|
}
|
|
927
956
|
this.deleteSubDirectoryCore(op.subdirName, local);
|
|
@@ -929,35 +958,139 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
929
958
|
/**
|
|
930
959
|
* Submit a clear operation.
|
|
931
960
|
* @param op - The operation
|
|
932
|
-
* @internal
|
|
933
961
|
*/
|
|
934
|
-
submitClearMessage(op) {
|
|
962
|
+
submitClearMessage(op, previousValue) {
|
|
935
963
|
this.throwIfDisposed();
|
|
964
|
+
const pendingMsgId = ++this.pendingMessageId;
|
|
965
|
+
this.pendingClearMessageIds.push(pendingMsgId);
|
|
966
|
+
const metadata = {
|
|
967
|
+
type: "clear",
|
|
968
|
+
pendingMessageId: pendingMsgId,
|
|
969
|
+
previousStorage: previousValue,
|
|
970
|
+
};
|
|
971
|
+
this.directory.submitDirectoryMessage(op, metadata);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Resubmit a clear operation.
|
|
975
|
+
* @param op - The operation
|
|
976
|
+
* @internal
|
|
977
|
+
*/
|
|
978
|
+
resubmitClearMessage(op, localOpMetadata) {
|
|
979
|
+
assert(isClearLocalOpMetadata(localOpMetadata), "Invalid localOpMetadata for clear");
|
|
980
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
981
|
+
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
982
|
+
assert(pendingClearMessageId === localOpMetadata.pendingMessageId, "pendingMessageId does not match");
|
|
983
|
+
this.submitClearMessage(op, localOpMetadata.previousStorage);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Get a new pending message id for the op and cache it to track the pending op
|
|
987
|
+
*/
|
|
988
|
+
getKeyMessageId(op) {
|
|
989
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
936
990
|
const pendingMessageId = ++this.pendingMessageId;
|
|
937
|
-
this.
|
|
938
|
-
|
|
991
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
992
|
+
if (pendingMessageIds !== undefined) {
|
|
993
|
+
pendingMessageIds.push(pendingMessageId);
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
this.pendingKeys.set(op.key, [pendingMessageId]);
|
|
997
|
+
}
|
|
998
|
+
return pendingMessageId;
|
|
939
999
|
}
|
|
940
1000
|
/**
|
|
941
1001
|
* Submit a key operation.
|
|
942
1002
|
* @param op - The operation
|
|
1003
|
+
* @param previousValue - The value of the key before this op
|
|
1004
|
+
*/
|
|
1005
|
+
submitKeyMessage(op, previousValue) {
|
|
1006
|
+
this.throwIfDisposed();
|
|
1007
|
+
const pendingMessageId = this.getKeyMessageId(op);
|
|
1008
|
+
const localMetadata = { type: "edit", pendingMessageId, previousValue };
|
|
1009
|
+
this.directory.submitDirectoryMessage(op, localMetadata);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Submit a key message to remote clients based on a previous submit.
|
|
1013
|
+
* @param op - The map key message
|
|
1014
|
+
* @param localOpMetadata - Metadata from the previous submit
|
|
943
1015
|
* @internal
|
|
944
1016
|
*/
|
|
945
|
-
|
|
1017
|
+
resubmitKeyMessage(op, localOpMetadata) {
|
|
1018
|
+
assert(isKeyEditLocalOpMetadata(localOpMetadata), "Invalid localOpMetadata in submit");
|
|
1019
|
+
// clear the old pending message id
|
|
1020
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1021
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, "Unexpected pending message received");
|
|
1022
|
+
pendingMessageIds.shift();
|
|
1023
|
+
if (pendingMessageIds.length === 0) {
|
|
1024
|
+
this.pendingKeys.delete(op.key);
|
|
1025
|
+
}
|
|
1026
|
+
this.submitKeyMessage(op, localOpMetadata.previousValue);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Get a new pending message id for the op and cache it to track the pending op
|
|
1030
|
+
*/
|
|
1031
|
+
getSubDirMessageId(op) {
|
|
1032
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
1033
|
+
const newMessageId = ++this.pendingMessageId;
|
|
1034
|
+
const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
|
|
1035
|
+
if (pendingMessageIds !== undefined) {
|
|
1036
|
+
pendingMessageIds.push(newMessageId);
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
|
|
1040
|
+
}
|
|
1041
|
+
return newMessageId;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Submit a create subdirectory operation.
|
|
1045
|
+
* @param op - The operation
|
|
1046
|
+
* @param prevExisted - Whether the subdirectory existed before the op
|
|
1047
|
+
*/
|
|
1048
|
+
submitCreateSubDirectoryMessage(op, prevExisted) {
|
|
946
1049
|
this.throwIfDisposed();
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
1050
|
+
const newMessageId = this.getSubDirMessageId(op);
|
|
1051
|
+
const localOpMetadata = {
|
|
1052
|
+
type: "createSubDir",
|
|
1053
|
+
pendingMessageId: newMessageId,
|
|
1054
|
+
previouslyExisted: prevExisted,
|
|
1055
|
+
};
|
|
1056
|
+
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
950
1057
|
}
|
|
951
1058
|
/**
|
|
952
|
-
* Submit a subdirectory operation.
|
|
1059
|
+
* Submit a delete subdirectory operation.
|
|
953
1060
|
* @param op - The operation
|
|
954
|
-
* @
|
|
1061
|
+
* @param subDir - Any subdirectory deleted by the op
|
|
955
1062
|
*/
|
|
956
|
-
|
|
1063
|
+
submitDeleteSubDirectoryMessage(op, subDir) {
|
|
957
1064
|
this.throwIfDisposed();
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
1065
|
+
const newMessageId = this.getSubDirMessageId(op);
|
|
1066
|
+
const localOpMetadata = {
|
|
1067
|
+
type: "deleteSubDir",
|
|
1068
|
+
pendingMessageId: newMessageId,
|
|
1069
|
+
subDirectory: subDir,
|
|
1070
|
+
};
|
|
1071
|
+
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Submit a subdirectory operation again
|
|
1075
|
+
* @param op - The operation
|
|
1076
|
+
* @param localOpMetadata - metadata submitted with the op originally
|
|
1077
|
+
* @internal
|
|
1078
|
+
*/
|
|
1079
|
+
resubmitSubDirectoryMessage(op, localOpMetadata) {
|
|
1080
|
+
assert(isSubDirLocalOpMetadata(localOpMetadata), "Invalid localOpMetadata for sub directory op");
|
|
1081
|
+
// clear the old pending message id
|
|
1082
|
+
const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
|
|
1083
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, "Unexpected pending message received");
|
|
1084
|
+
pendingMessageIds.shift();
|
|
1085
|
+
if (pendingMessageIds.length === 0) {
|
|
1086
|
+
this.pendingSubDirectories.delete(op.subdirName);
|
|
1087
|
+
}
|
|
1088
|
+
if (localOpMetadata.type === "createSubDir") {
|
|
1089
|
+
this.submitCreateSubDirectoryMessage(op, localOpMetadata.previouslyExisted);
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
|
|
1093
|
+
}
|
|
961
1094
|
}
|
|
962
1095
|
/**
|
|
963
1096
|
* Get the storage of this subdirectory in a serializable format, to be used in snapshotting.
|
|
@@ -1004,6 +1137,67 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1004
1137
|
this.throwIfDisposed();
|
|
1005
1138
|
return this._storage.get(key);
|
|
1006
1139
|
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Remove the pendingMessageId from the map tracking it on rollback
|
|
1142
|
+
* @param map - map tracking the pending messages
|
|
1143
|
+
* @param key - key of the edit in the op
|
|
1144
|
+
*/
|
|
1145
|
+
rollbackPendingMessageId(map, key, pendingMessageId) {
|
|
1146
|
+
const pendingMessageIds = map.get(key);
|
|
1147
|
+
const lastPendingMessageId = pendingMessageIds === null || pendingMessageIds === void 0 ? void 0 : pendingMessageIds.pop();
|
|
1148
|
+
if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
|
|
1149
|
+
throw new Error("Rollback op does not match last pending");
|
|
1150
|
+
}
|
|
1151
|
+
if (pendingMessageIds.length === 0) {
|
|
1152
|
+
map.delete(key);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Rollback a local op
|
|
1157
|
+
* @param op - The operation to rollback
|
|
1158
|
+
* @param localOpMetadata - The local metadata associated with the op.
|
|
1159
|
+
*/
|
|
1160
|
+
rollback(op, localOpMetadata) {
|
|
1161
|
+
if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
|
|
1162
|
+
throw new Error("Invalid localOpMetadata");
|
|
1163
|
+
}
|
|
1164
|
+
if (op.type === "clear" && localOpMetadata.type === "clear") {
|
|
1165
|
+
localOpMetadata.previousStorage.forEach((localValue, key) => {
|
|
1166
|
+
this.setCore(key, localValue, true);
|
|
1167
|
+
});
|
|
1168
|
+
const lastPendingClearId = this.pendingClearMessageIds.pop();
|
|
1169
|
+
if (lastPendingClearId === undefined || lastPendingClearId !== localOpMetadata.pendingMessageId) {
|
|
1170
|
+
throw new Error("Rollback op does match last clear");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
else if ((op.type === "delete" || op.type === "set") && localOpMetadata.type === "edit") {
|
|
1174
|
+
if (localOpMetadata.previousValue === undefined) {
|
|
1175
|
+
this.deleteCore(op.key, true);
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
this.setCore(op.key, localOpMetadata.previousValue, true);
|
|
1179
|
+
}
|
|
1180
|
+
this.rollbackPendingMessageId(this.pendingKeys, op.key, localOpMetadata.pendingMessageId);
|
|
1181
|
+
}
|
|
1182
|
+
else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
|
|
1183
|
+
if (!localOpMetadata.previouslyExisted) {
|
|
1184
|
+
this.deleteSubDirectoryCore(op.subdirName, true);
|
|
1185
|
+
}
|
|
1186
|
+
this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
|
|
1187
|
+
}
|
|
1188
|
+
else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
|
|
1189
|
+
if (localOpMetadata.subDirectory !== undefined) {
|
|
1190
|
+
this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
|
|
1191
|
+
// don't need to register events because deleting never unregistered
|
|
1192
|
+
this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
|
|
1193
|
+
this.emit("subDirectoryCreated", op.subdirName, true, this);
|
|
1194
|
+
}
|
|
1195
|
+
this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
throw new Error("Unsupported op for rollback");
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1007
1201
|
/**
|
|
1008
1202
|
* Converts the given relative path into an absolute path.
|
|
1009
1203
|
* @param path - Relative path to convert
|
|
@@ -1016,16 +1210,16 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1016
1210
|
* If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
|
|
1017
1211
|
* not process the incoming operation.
|
|
1018
1212
|
* @param op - Operation to check
|
|
1019
|
-
* @param local - Whether the
|
|
1020
|
-
* @param
|
|
1021
|
-
*
|
|
1022
|
-
* For messages from a remote client, this will be undefined.
|
|
1213
|
+
* @param local - Whether the operation originated from the local client
|
|
1214
|
+
* @param localOpMetadata - For local client ops, this is the metadata that was submitted with the op.
|
|
1215
|
+
* For ops from a remote client, this will be undefined.
|
|
1023
1216
|
* @returns True if the operation should be processed, false otherwise
|
|
1024
1217
|
*/
|
|
1025
1218
|
needProcessStorageOperation(op, local, localOpMetadata) {
|
|
1026
|
-
if (this.
|
|
1219
|
+
if (this.pendingClearMessageIds.length > 0) {
|
|
1027
1220
|
if (local) {
|
|
1028
|
-
assert(localOpMetadata !== undefined && localOpMetadata
|
|
1221
|
+
assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata) &&
|
|
1222
|
+
localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0], 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
|
|
1029
1223
|
}
|
|
1030
1224
|
// If I have a NACK clear, we can ignore all ops.
|
|
1031
1225
|
return false;
|
|
@@ -1035,9 +1229,11 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1035
1229
|
// Found an NACK op, clear it from the directory if the latest sequence number in the directory
|
|
1036
1230
|
// match the message's and don't process the op.
|
|
1037
1231
|
if (local) {
|
|
1038
|
-
assert(localOpMetadata !== undefined, 0x011 /* pendingMessageId is missing from the local client's operation */);
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1232
|
+
assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata), 0x011 /* pendingMessageId is missing from the local client's operation */);
|
|
1233
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1234
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, "Unexpected pending message received");
|
|
1235
|
+
pendingMessageIds.shift();
|
|
1236
|
+
if (pendingMessageIds.length === 0) {
|
|
1041
1237
|
this.pendingKeys.delete(op.key);
|
|
1042
1238
|
}
|
|
1043
1239
|
}
|
|
@@ -1056,13 +1252,15 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1056
1252
|
* For messages from a remote client, this will be undefined.
|
|
1057
1253
|
* @returns True if the operation should be processed, false otherwise
|
|
1058
1254
|
*/
|
|
1059
|
-
|
|
1255
|
+
needProcessSubDirectoryOperation(op, local, localOpMetadata) {
|
|
1060
1256
|
const pendingSubDirectoryMessageId = this.pendingSubDirectories.get(op.subdirName);
|
|
1061
1257
|
if (pendingSubDirectoryMessageId !== undefined) {
|
|
1062
1258
|
if (local) {
|
|
1063
|
-
assert(localOpMetadata
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1259
|
+
assert(isSubDirLocalOpMetadata(localOpMetadata), 0x012 /* pendingMessageId is missing from the local client's operation */);
|
|
1260
|
+
const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
|
|
1261
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, "Unexpected pending message received");
|
|
1262
|
+
pendingMessageIds.shift();
|
|
1263
|
+
if (pendingMessageIds.length === 0) {
|
|
1066
1264
|
this.pendingSubDirectories.delete(op.subdirName);
|
|
1067
1265
|
}
|
|
1068
1266
|
}
|
|
@@ -1081,9 +1279,9 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1081
1279
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1082
1280
|
temp.set(key, this._storage.get(key));
|
|
1083
1281
|
});
|
|
1084
|
-
this.
|
|
1282
|
+
this.clearCore(false);
|
|
1085
1283
|
temp.forEach((value, key, map) => {
|
|
1086
|
-
this.
|
|
1284
|
+
this.setCore(key, value, true);
|
|
1087
1285
|
});
|
|
1088
1286
|
}
|
|
1089
1287
|
/**
|
|
@@ -1099,11 +1297,11 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1099
1297
|
* Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
|
|
1100
1298
|
* @param key - The key being deleted
|
|
1101
1299
|
* @param local - Whether the message originated from the local client
|
|
1102
|
-
* @
|
|
1103
|
-
* @returns True if the key existed and was deleted, false if it did not exist
|
|
1300
|
+
* @returns Previous local value of the key if it existed, undefined if it did not exist
|
|
1104
1301
|
*/
|
|
1105
1302
|
deleteCore(key, local) {
|
|
1106
|
-
const
|
|
1303
|
+
const previousLocalValue = this._storage.get(key);
|
|
1304
|
+
const previousValue = previousLocalValue === null || previousLocalValue === void 0 ? void 0 : previousLocalValue.value;
|
|
1107
1305
|
const successfullyRemoved = this._storage.delete(key);
|
|
1108
1306
|
if (successfullyRemoved) {
|
|
1109
1307
|
const event = { key, path: this.absolutePath, previousValue };
|
|
@@ -1111,27 +1309,30 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1111
1309
|
const containedEvent = { key, previousValue };
|
|
1112
1310
|
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1113
1311
|
}
|
|
1114
|
-
return
|
|
1312
|
+
return previousLocalValue;
|
|
1115
1313
|
}
|
|
1116
1314
|
/**
|
|
1117
1315
|
* Set implementation used for both locally sourced sets as well as incoming remote sets.
|
|
1118
1316
|
* @param key - The key being set
|
|
1119
1317
|
* @param value - The value being set
|
|
1120
1318
|
* @param local - Whether the message originated from the local client
|
|
1121
|
-
* @
|
|
1319
|
+
* @returns Previous local value of the key, if any
|
|
1122
1320
|
*/
|
|
1123
1321
|
setCore(key, value, local) {
|
|
1124
|
-
const
|
|
1322
|
+
const previousLocalValue = this._storage.get(key);
|
|
1323
|
+
const previousValue = previousLocalValue === null || previousLocalValue === void 0 ? void 0 : previousLocalValue.value;
|
|
1125
1324
|
this._storage.set(key, value);
|
|
1126
1325
|
const event = { key, path: this.absolutePath, previousValue };
|
|
1127
1326
|
this.directory.emit("valueChanged", event, local, this.directory);
|
|
1128
1327
|
const containedEvent = { key, previousValue };
|
|
1129
1328
|
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1329
|
+
return previousLocalValue;
|
|
1130
1330
|
}
|
|
1131
1331
|
/**
|
|
1132
1332
|
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
1133
1333
|
* @param subdirName - The name of the subdirectory being created
|
|
1134
1334
|
* @param local - Whether the message originated from the local client
|
|
1335
|
+
* @returns - True if is newly created, false if it already existed.
|
|
1135
1336
|
*/
|
|
1136
1337
|
createSubDirectoryCore(subdirName, local) {
|
|
1137
1338
|
if (!this._subdirectories.has(subdirName)) {
|
|
@@ -1140,7 +1341,9 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1140
1341
|
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
1141
1342
|
this._subdirectories.set(subdirName, subDir);
|
|
1142
1343
|
this.emit("subDirectoryCreated", subdirName, local, this);
|
|
1344
|
+
return true;
|
|
1143
1345
|
}
|
|
1346
|
+
return false;
|
|
1144
1347
|
}
|
|
1145
1348
|
registerEventsOnSubDirectory(subDirectory, subDirName) {
|
|
1146
1349
|
subDirectory.on("subDirectoryCreated", (relativePath, local) => {
|
|
@@ -1154,18 +1357,17 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1154
1357
|
* Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
1155
1358
|
* @param subdirName - The name of the subdirectory being deleted
|
|
1156
1359
|
* @param local - Whether the message originated from the local client
|
|
1157
|
-
* @param op - The message if from a remote delete, or null if from a local delete
|
|
1158
1360
|
*/
|
|
1159
1361
|
deleteSubDirectoryCore(subdirName, local) {
|
|
1160
|
-
const previousValue = this.
|
|
1362
|
+
const previousValue = this._subdirectories.get(subdirName);
|
|
1161
1363
|
// This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
|
|
1162
|
-
// Might want to consider cleaning out the structure more exhaustively though?
|
|
1163
|
-
const successfullyRemoved = this._subdirectories.delete(subdirName);
|
|
1364
|
+
// Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
|
|
1164
1365
|
if (previousValue !== undefined) {
|
|
1366
|
+
this._subdirectories.delete(subdirName);
|
|
1165
1367
|
this.disposeSubDirectoryTree(previousValue);
|
|
1166
1368
|
this.emit("subDirectoryDeleted", subdirName, local, this);
|
|
1167
1369
|
}
|
|
1168
|
-
return
|
|
1370
|
+
return previousValue;
|
|
1169
1371
|
}
|
|
1170
1372
|
disposeSubDirectoryTree(directory) {
|
|
1171
1373
|
if (!directory) {
|
|
@@ -1180,5 +1382,12 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1180
1382
|
directory.dispose();
|
|
1181
1383
|
}
|
|
1182
1384
|
}
|
|
1385
|
+
undeleteSubDirectoryTree(directory) {
|
|
1386
|
+
// Restore deleted subdirectory tree. This will unmark "deleted" from the subdirectories from bottom to top.
|
|
1387
|
+
for (const [_, subDirectory] of this._subdirectories.entries()) {
|
|
1388
|
+
this.undeleteSubDirectoryTree(subDirectory);
|
|
1389
|
+
}
|
|
1390
|
+
directory.undispose();
|
|
1391
|
+
}
|
|
1183
1392
|
}
|
|
1184
1393
|
//# sourceMappingURL=directory.js.map
|