@fluidframework/map 2.52.0 → 2.53.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/directory.d.ts +60 -95
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +420 -319
- package/dist/directory.js.map +1 -1
- package/dist/mapKernel.d.ts.map +1 -1
- package/dist/mapKernel.js +5 -20
- 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/dist/utils.d.ts +13 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +26 -0
- package/dist/utils.js.map +1 -0
- package/lib/directory.d.ts +60 -95
- package/lib/directory.d.ts.map +1 -1
- package/lib/directory.js +420 -319
- package/lib/directory.js.map +1 -1
- package/lib/mapKernel.d.ts.map +1 -1
- package/lib/mapKernel.js +1 -16
- 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/lib/utils.d.ts +13 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/utils.js +21 -0
- package/lib/utils.js.map +1 -0
- package/package.json +18 -18
- package/src/directory.ts +608 -450
- package/src/mapKernel.ts +1 -19
- package/src/packageVersion.ts +1 -1
- package/src/utils.ts +23 -0
package/dist/directory.js
CHANGED
|
@@ -19,6 +19,7 @@ const internal_6 = require("@fluidframework/shared-object-base/internal");
|
|
|
19
19
|
const internal_7 = require("@fluidframework/telemetry-utils/internal");
|
|
20
20
|
const path_browserify_1 = __importDefault(require("path-browserify"));
|
|
21
21
|
const localValues_js_1 = require("./localValues.js");
|
|
22
|
+
const utils_js_1 = require("./utils.js");
|
|
22
23
|
// We use path-browserify since this code can run safely on the server or the browser.
|
|
23
24
|
// We standardize on using posix slashes everywhere.
|
|
24
25
|
const posix = path_browserify_1.default.posix;
|
|
@@ -346,7 +347,7 @@ class SharedDirectory extends internal_6.SharedObject {
|
|
|
346
347
|
const message = content;
|
|
347
348
|
const handler = this.messageHandlers.get(message.type);
|
|
348
349
|
(0, internal_1.assert)(handler !== undefined, 0x00d /* Missing message handler for message type */);
|
|
349
|
-
handler.
|
|
350
|
+
handler.resubmit(message, localOpMetadata);
|
|
350
351
|
}
|
|
351
352
|
/**
|
|
352
353
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
|
|
@@ -494,7 +495,7 @@ class SharedDirectory extends internal_6.SharedObject {
|
|
|
494
495
|
subdir.processClearMessage(msg, op, local, localOpMetadata);
|
|
495
496
|
}
|
|
496
497
|
},
|
|
497
|
-
|
|
498
|
+
resubmit: (op, localOpMetadata) => {
|
|
498
499
|
const subdir = this.getWorkingDirectory(op.path);
|
|
499
500
|
if (subdir) {
|
|
500
501
|
subdir.resubmitClearMessage(op, localOpMetadata);
|
|
@@ -510,7 +511,7 @@ class SharedDirectory extends internal_6.SharedObject {
|
|
|
510
511
|
subdir.processDeleteMessage(msg, op, local, localOpMetadata);
|
|
511
512
|
}
|
|
512
513
|
},
|
|
513
|
-
|
|
514
|
+
resubmit: (op, localOpMetadata) => {
|
|
514
515
|
const subdir = this.getWorkingDirectory(op.path);
|
|
515
516
|
if (subdir) {
|
|
516
517
|
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
@@ -528,7 +529,7 @@ class SharedDirectory extends internal_6.SharedObject {
|
|
|
528
529
|
subdir.processSetMessage(msg, op, localValue, local, localOpMetadata);
|
|
529
530
|
}
|
|
530
531
|
},
|
|
531
|
-
|
|
532
|
+
resubmit: (op, localOpMetadata) => {
|
|
532
533
|
const subdir = this.getWorkingDirectory(op.path);
|
|
533
534
|
if (subdir) {
|
|
534
535
|
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
@@ -544,7 +545,7 @@ class SharedDirectory extends internal_6.SharedObject {
|
|
|
544
545
|
parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
545
546
|
}
|
|
546
547
|
},
|
|
547
|
-
|
|
548
|
+
resubmit: (op, localOpMetadata) => {
|
|
548
549
|
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
549
550
|
if (parentSubdir) {
|
|
550
551
|
// We don't reuse the metadata but send a new one on each submit.
|
|
@@ -561,7 +562,7 @@ class SharedDirectory extends internal_6.SharedObject {
|
|
|
561
562
|
parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
562
563
|
}
|
|
563
564
|
},
|
|
564
|
-
|
|
565
|
+
resubmit: (op, localOpMetadata) => {
|
|
565
566
|
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
566
567
|
if (parentSubdir) {
|
|
567
568
|
// We don't reuse the metadata but send a new one on each submit.
|
|
@@ -662,28 +663,6 @@ class SharedDirectory extends internal_6.SharedObject {
|
|
|
662
663
|
}
|
|
663
664
|
}
|
|
664
665
|
exports.SharedDirectory = SharedDirectory;
|
|
665
|
-
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
666
|
-
function isKeyEditLocalOpMetadata(metadata) {
|
|
667
|
-
return (metadata !== undefined &&
|
|
668
|
-
typeof metadata.pendingMessageId === "number" &&
|
|
669
|
-
metadata.type === "edit");
|
|
670
|
-
}
|
|
671
|
-
function isClearLocalOpMetadata(metadata) {
|
|
672
|
-
return (metadata !== undefined &&
|
|
673
|
-
metadata.type === "clear" &&
|
|
674
|
-
typeof metadata.pendingMessageId === "number" &&
|
|
675
|
-
typeof metadata.previousStorage === "object");
|
|
676
|
-
}
|
|
677
|
-
function isSubDirLocalOpMetadata(metadata) {
|
|
678
|
-
return (metadata !== undefined &&
|
|
679
|
-
(metadata.type === "createSubDir" || metadata.type === "deleteSubDir"));
|
|
680
|
-
}
|
|
681
|
-
function isDirectoryLocalOpMetadata(metadata) {
|
|
682
|
-
return (isKeyEditLocalOpMetadata(metadata) ||
|
|
683
|
-
isClearLocalOpMetadata(metadata) ||
|
|
684
|
-
isSubDirLocalOpMetadata(metadata));
|
|
685
|
-
}
|
|
686
|
-
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
687
666
|
// eslint-disable-next-line @rushstack/no-new-null
|
|
688
667
|
function assertNonNullClientId(clientId) {
|
|
689
668
|
(0, internal_1.assert)(clientId !== null, 0x6af /* client id should never be null */);
|
|
@@ -720,21 +699,10 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
720
699
|
* String representation for the class.
|
|
721
700
|
*/
|
|
722
701
|
this[_b] = "SubDirectory";
|
|
723
|
-
/**
|
|
724
|
-
* The in-memory data the directory is storing.
|
|
725
|
-
*/
|
|
726
|
-
this._storage = new Map();
|
|
727
702
|
/**
|
|
728
703
|
* The subdirectories the directory is holding.
|
|
729
704
|
*/
|
|
730
705
|
this._subdirectories = new Map();
|
|
731
|
-
/**
|
|
732
|
-
* Keys that have been modified locally but not yet ack'd from the server. This is for operations on keys like
|
|
733
|
-
* set/delete operations on keys. The value of this map is list of pendingMessageIds at which that key
|
|
734
|
-
* was modified. We don't store the type of ops, and behaviour of key ops are different from behaviour of sub
|
|
735
|
-
* directory ops, so we have separate map from subDirectories tracker.
|
|
736
|
-
*/
|
|
737
|
-
this.pendingKeys = new Map();
|
|
738
706
|
/**
|
|
739
707
|
* Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
|
|
740
708
|
* of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
@@ -748,18 +716,111 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
748
716
|
*/
|
|
749
717
|
this.pendingCreateSubDirectoriesTracker = new Map();
|
|
750
718
|
/**
|
|
751
|
-
*
|
|
719
|
+
* Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
|
|
720
|
+
* of the creation order.
|
|
752
721
|
*/
|
|
753
|
-
this.
|
|
722
|
+
this.localCreationSeq = 0;
|
|
754
723
|
/**
|
|
755
|
-
* The
|
|
724
|
+
* The data this SubDirectory instance is storing, but only including sequenced values (no local pending
|
|
725
|
+
* modifications are included).
|
|
756
726
|
*/
|
|
757
|
-
this.
|
|
727
|
+
this.sequencedStorageData = new Map();
|
|
758
728
|
/**
|
|
759
|
-
*
|
|
760
|
-
*
|
|
729
|
+
* A data structure containing all local pending storage modifications, which is used in combination
|
|
730
|
+
* with the sequencedStorageData to compute optimistic values.
|
|
731
|
+
*
|
|
732
|
+
* Pending sets are aggregated into "lifetimes", which permit correct relative iteration order
|
|
733
|
+
* even across remote operations and rollbacks.
|
|
761
734
|
*/
|
|
762
|
-
this.
|
|
735
|
+
this.pendingStorageData = [];
|
|
736
|
+
/**
|
|
737
|
+
* An internal iterator that iterates over the entries in the directory.
|
|
738
|
+
*/
|
|
739
|
+
this.internalIterator = () => {
|
|
740
|
+
// We perform iteration in two steps - first by iterating over members of the sequenced storage data that are not
|
|
741
|
+
// optimistically deleted or cleared, and then over the pending data lifetimes that have not subsequently
|
|
742
|
+
// been deleted or cleared. In total, this give an ordering of members based on when they were initially
|
|
743
|
+
// added to the sub directory (even if they were later modified), similar to the native Map.
|
|
744
|
+
const sequencedStorageDataIterator = this.sequencedStorageData.keys();
|
|
745
|
+
const pendingStorageDataIterator = this.pendingStorageData.values();
|
|
746
|
+
const next = () => {
|
|
747
|
+
let nextSequencedKey = sequencedStorageDataIterator.next();
|
|
748
|
+
while (!nextSequencedKey.done) {
|
|
749
|
+
const key = nextSequencedKey.value;
|
|
750
|
+
// If we have any pending deletes or clears, then we won't iterate to this key yet (if at all).
|
|
751
|
+
// Either it is optimistically deleted and will not be part of the iteration, or it was
|
|
752
|
+
// re-added later and we'll iterate to it when we get to the pending data.
|
|
753
|
+
if (!this.pendingStorageData.some((entry) => entry.type === "clear" || (entry.type === "delete" && entry.key === key))) {
|
|
754
|
+
(0, internal_1.assert)(this.has(key), 0xc03 /* key should exist in sequenced or pending data */);
|
|
755
|
+
const optimisticValue = this.getOptimisticValue(key);
|
|
756
|
+
return { value: [key, optimisticValue], done: false };
|
|
757
|
+
}
|
|
758
|
+
nextSequencedKey = sequencedStorageDataIterator.next();
|
|
759
|
+
}
|
|
760
|
+
let nextPending = pendingStorageDataIterator.next();
|
|
761
|
+
while (!nextPending.done) {
|
|
762
|
+
const nextPendingEntry = nextPending.value;
|
|
763
|
+
// A lifetime entry may need to be iterated.
|
|
764
|
+
if (nextPendingEntry.type === "lifetime") {
|
|
765
|
+
const nextPendingEntryIndex = this.pendingStorageData.indexOf(nextPendingEntry);
|
|
766
|
+
const mostRecentDeleteOrClearIndex = (0, utils_js_1.findLastIndex)(this.pendingStorageData, (entry) => entry.type === "clear" ||
|
|
767
|
+
(entry.type === "delete" && entry.key === nextPendingEntry.key));
|
|
768
|
+
// Only iterate the pending entry now if it hasn't been deleted or cleared.
|
|
769
|
+
if (nextPendingEntryIndex > mostRecentDeleteOrClearIndex) {
|
|
770
|
+
const latestPendingValue =
|
|
771
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
772
|
+
nextPendingEntry.keySets[nextPendingEntry.keySets.length - 1];
|
|
773
|
+
// Skip iterating if we would have would have already iterated it as part of the sequenced data.
|
|
774
|
+
// This is not a perfect check in the case the map has changed since the iterator was created
|
|
775
|
+
// (e.g. if a remote client added the same key in the meantime).
|
|
776
|
+
if (!this.sequencedStorageData.has(nextPendingEntry.key) ||
|
|
777
|
+
mostRecentDeleteOrClearIndex !== -1) {
|
|
778
|
+
return { value: [nextPendingEntry.key, latestPendingValue.value], done: false };
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
nextPending = pendingStorageDataIterator.next();
|
|
783
|
+
}
|
|
784
|
+
return { value: undefined, done: true };
|
|
785
|
+
};
|
|
786
|
+
const iterator = {
|
|
787
|
+
next,
|
|
788
|
+
[Symbol.iterator]() {
|
|
789
|
+
return this;
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
return iterator;
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* Compute the optimistic local value for a given key. This combines the sequenced data with
|
|
796
|
+
* any pending changes that have not yet been sequenced.
|
|
797
|
+
*/
|
|
798
|
+
this.getOptimisticValue = (key) => {
|
|
799
|
+
const latestPendingEntry = (0, utils_js_1.findLast)(this.pendingStorageData, (entry) => entry.type === "clear" || entry.key === key);
|
|
800
|
+
if (latestPendingEntry === undefined) {
|
|
801
|
+
return this.sequencedStorageData.get(key);
|
|
802
|
+
}
|
|
803
|
+
else if (latestPendingEntry.type === "lifetime") {
|
|
804
|
+
const latestPendingSet =
|
|
805
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
806
|
+
latestPendingEntry.keySets[latestPendingEntry.keySets.length - 1];
|
|
807
|
+
return latestPendingSet.value;
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
// Delete or clear
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
/**
|
|
815
|
+
* Determine if the directory optimistically has the key.
|
|
816
|
+
* This will return true even if the value is undefined.
|
|
817
|
+
*/
|
|
818
|
+
this.optimisticallyHas = (key) => {
|
|
819
|
+
const latestPendingEntry = (0, utils_js_1.findLast)(this.pendingStorageData, (entry) => entry.type === "clear" || entry.key === key);
|
|
820
|
+
return latestPendingEntry === undefined
|
|
821
|
+
? this.sequencedStorageData.has(key)
|
|
822
|
+
: latestPendingEntry.type === "lifetime";
|
|
823
|
+
};
|
|
763
824
|
this.localCreationSeqTracker = new DirectoryCreationTracker();
|
|
764
825
|
this.ackedCreationSeqTracker = new DirectoryCreationTracker();
|
|
765
826
|
}
|
|
@@ -789,14 +850,13 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
789
850
|
*/
|
|
790
851
|
has(key) {
|
|
791
852
|
this.throwIfDisposed();
|
|
792
|
-
return this.
|
|
853
|
+
return this.optimisticallyHas(key);
|
|
793
854
|
}
|
|
794
855
|
/**
|
|
795
856
|
* {@inheritDoc IDirectory.get}
|
|
796
857
|
*/
|
|
797
858
|
get(key) {
|
|
798
|
-
this.
|
|
799
|
-
return this._storage.get(key);
|
|
859
|
+
return this.getOptimisticValue(key);
|
|
800
860
|
}
|
|
801
861
|
/**
|
|
802
862
|
* {@inheritDoc IDirectory.set}
|
|
@@ -807,21 +867,60 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
807
867
|
if (key === undefined || key === null) {
|
|
808
868
|
throw new Error("Undefined and null keys are not supported");
|
|
809
869
|
}
|
|
870
|
+
const previousOptimisticLocalValue = this.getOptimisticValue(key);
|
|
810
871
|
// Create a local value and serialize it.
|
|
811
872
|
(0, internal_6.bindHandles)(value, this.serializer, this.directory.handle);
|
|
812
|
-
// Set the value locally.
|
|
813
|
-
const previousValue = this.setCore(key, value, true);
|
|
814
873
|
// If we are not attached, don't submit the op.
|
|
815
874
|
if (!this.directory.isAttached()) {
|
|
875
|
+
this.sequencedStorageData.set(key, value);
|
|
876
|
+
const event = {
|
|
877
|
+
key,
|
|
878
|
+
path: this.absolutePath,
|
|
879
|
+
previousValue: previousOptimisticLocalValue,
|
|
880
|
+
};
|
|
881
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
882
|
+
const containedEvent = {
|
|
883
|
+
key,
|
|
884
|
+
previousValue: previousOptimisticLocalValue,
|
|
885
|
+
};
|
|
886
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
816
887
|
return this;
|
|
817
888
|
}
|
|
889
|
+
// A new pending key lifetime is created if:
|
|
890
|
+
// 1. There isn't any pending entry for the key yet
|
|
891
|
+
// 2. The most recent pending entry for the key was a deletion (as this terminates the prior lifetime)
|
|
892
|
+
// 3. A clear was sent after the last pending entry for the key (which also terminates the prior lifetime)
|
|
893
|
+
let latestPendingEntry = (0, utils_js_1.findLast)(this.pendingStorageData, (entry) => entry.type === "clear" || entry.key === key);
|
|
894
|
+
if (latestPendingEntry === undefined ||
|
|
895
|
+
latestPendingEntry.type === "delete" ||
|
|
896
|
+
latestPendingEntry.type === "clear") {
|
|
897
|
+
latestPendingEntry = { type: "lifetime", path: this.absolutePath, key, keySets: [] };
|
|
898
|
+
this.pendingStorageData.push(latestPendingEntry);
|
|
899
|
+
}
|
|
900
|
+
const pendingKeySet = {
|
|
901
|
+
type: "set",
|
|
902
|
+
path: this.absolutePath,
|
|
903
|
+
value,
|
|
904
|
+
};
|
|
905
|
+
latestPendingEntry.keySets.push(pendingKeySet);
|
|
818
906
|
const op = {
|
|
819
907
|
key,
|
|
820
908
|
path: this.absolutePath,
|
|
821
909
|
type: "set",
|
|
822
910
|
value: { type: internal_6.ValueType[internal_6.ValueType.Plain], value },
|
|
823
911
|
};
|
|
824
|
-
this.submitKeyMessage(op,
|
|
912
|
+
this.submitKeyMessage(op, pendingKeySet);
|
|
913
|
+
const directoryValueChanged = {
|
|
914
|
+
key,
|
|
915
|
+
path: this.absolutePath,
|
|
916
|
+
previousValue: previousOptimisticLocalValue,
|
|
917
|
+
};
|
|
918
|
+
this.directory.emit("valueChanged", directoryValueChanged, true, this.directory);
|
|
919
|
+
const valueChanged = {
|
|
920
|
+
key,
|
|
921
|
+
previousValue: previousOptimisticLocalValue,
|
|
922
|
+
};
|
|
923
|
+
this.emit("containedValueChanged", valueChanged, true, this);
|
|
825
924
|
return this;
|
|
826
925
|
}
|
|
827
926
|
/**
|
|
@@ -983,37 +1082,76 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
983
1082
|
*/
|
|
984
1083
|
delete(key) {
|
|
985
1084
|
this.throwIfDisposed();
|
|
986
|
-
|
|
987
|
-
const previousValue = this.deleteCore(key, true);
|
|
988
|
-
// If we are not attached, don't submit the op.
|
|
1085
|
+
const previousOptimisticLocalValue = this.getOptimisticValue(key);
|
|
989
1086
|
if (!this.directory.isAttached()) {
|
|
990
|
-
|
|
1087
|
+
const successfullyRemoved = this.sequencedStorageData.delete(key);
|
|
1088
|
+
// Only emit if we actually deleted something.
|
|
1089
|
+
if (previousOptimisticLocalValue !== undefined && successfullyRemoved) {
|
|
1090
|
+
const event = {
|
|
1091
|
+
key,
|
|
1092
|
+
path: this.absolutePath,
|
|
1093
|
+
previousValue: previousOptimisticLocalValue,
|
|
1094
|
+
};
|
|
1095
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
1096
|
+
const containedEvent = {
|
|
1097
|
+
key,
|
|
1098
|
+
previousValue: previousOptimisticLocalValue,
|
|
1099
|
+
};
|
|
1100
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
1101
|
+
}
|
|
1102
|
+
return successfullyRemoved;
|
|
991
1103
|
}
|
|
1104
|
+
const pendingKeyDelete = {
|
|
1105
|
+
type: "delete",
|
|
1106
|
+
path: this.absolutePath,
|
|
1107
|
+
key,
|
|
1108
|
+
};
|
|
1109
|
+
this.pendingStorageData.push(pendingKeyDelete);
|
|
992
1110
|
const op = {
|
|
993
1111
|
key,
|
|
994
|
-
path: this.absolutePath,
|
|
995
1112
|
type: "delete",
|
|
1113
|
+
path: this.absolutePath,
|
|
996
1114
|
};
|
|
997
|
-
this.submitKeyMessage(op,
|
|
998
|
-
|
|
1115
|
+
this.submitKeyMessage(op, pendingKeyDelete);
|
|
1116
|
+
// Only emit if we locally believe we deleted something. Otherwise we still send the op
|
|
1117
|
+
// (permitting speculative deletion even if we don't see anything locally) but don't emit
|
|
1118
|
+
// a valueChanged since we in fact did not locally observe a value change.
|
|
1119
|
+
if (previousOptimisticLocalValue !== undefined) {
|
|
1120
|
+
const event = {
|
|
1121
|
+
key,
|
|
1122
|
+
path: this.absolutePath,
|
|
1123
|
+
previousValue: previousOptimisticLocalValue,
|
|
1124
|
+
};
|
|
1125
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
1126
|
+
const containedEvent = {
|
|
1127
|
+
key,
|
|
1128
|
+
previousValue: previousOptimisticLocalValue,
|
|
1129
|
+
};
|
|
1130
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
1131
|
+
}
|
|
1132
|
+
return true;
|
|
999
1133
|
}
|
|
1000
1134
|
/**
|
|
1001
1135
|
* Deletes all keys from within this IDirectory.
|
|
1002
1136
|
*/
|
|
1003
1137
|
clear() {
|
|
1004
1138
|
this.throwIfDisposed();
|
|
1005
|
-
// If we are not attached, don't submit the op.
|
|
1006
1139
|
if (!this.directory.isAttached()) {
|
|
1007
|
-
this.
|
|
1140
|
+
this.sequencedStorageData.clear();
|
|
1141
|
+
this.directory.emit("clear", true, this.directory);
|
|
1008
1142
|
return;
|
|
1009
1143
|
}
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
const op = {
|
|
1144
|
+
const pendingClear = {
|
|
1145
|
+
type: "clear",
|
|
1013
1146
|
path: this.absolutePath,
|
|
1147
|
+
};
|
|
1148
|
+
this.pendingStorageData.push(pendingClear);
|
|
1149
|
+
this.directory.emit("clear", true, this.directory);
|
|
1150
|
+
const op = {
|
|
1014
1151
|
type: "clear",
|
|
1152
|
+
path: this.absolutePath,
|
|
1015
1153
|
};
|
|
1016
|
-
this.submitClearMessage(op,
|
|
1154
|
+
this.submitClearMessage(op, pendingClear);
|
|
1017
1155
|
}
|
|
1018
1156
|
/**
|
|
1019
1157
|
* Issue a callback on each entry under this IDirectory.
|
|
@@ -1021,17 +1159,16 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1021
1159
|
*/
|
|
1022
1160
|
forEach(callback) {
|
|
1023
1161
|
this.throwIfDisposed();
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
});
|
|
1162
|
+
for (const [key, localValue] of this.internalIterator()) {
|
|
1163
|
+
callback(localValue.value, key, this);
|
|
1164
|
+
}
|
|
1028
1165
|
}
|
|
1029
1166
|
/**
|
|
1030
1167
|
* The number of entries under this IDirectory.
|
|
1031
1168
|
*/
|
|
1032
1169
|
get size() {
|
|
1033
1170
|
this.throwIfDisposed();
|
|
1034
|
-
return this.
|
|
1171
|
+
return [...this.internalIterator()].length;
|
|
1035
1172
|
}
|
|
1036
1173
|
/**
|
|
1037
1174
|
* Get an iterator over the entries under this IDirectory.
|
|
@@ -1039,7 +1176,23 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1039
1176
|
*/
|
|
1040
1177
|
entries() {
|
|
1041
1178
|
this.throwIfDisposed();
|
|
1042
|
-
|
|
1179
|
+
const internalIterator = this.internalIterator();
|
|
1180
|
+
const next = () => {
|
|
1181
|
+
const nextResult = internalIterator.next();
|
|
1182
|
+
if (nextResult.done) {
|
|
1183
|
+
return { value: undefined, done: true };
|
|
1184
|
+
}
|
|
1185
|
+
// Unpack the stored value
|
|
1186
|
+
const [key, localValue] = nextResult.value;
|
|
1187
|
+
return { value: [key, localValue], done: false };
|
|
1188
|
+
};
|
|
1189
|
+
const iterator = {
|
|
1190
|
+
next,
|
|
1191
|
+
[Symbol.iterator]() {
|
|
1192
|
+
return this;
|
|
1193
|
+
},
|
|
1194
|
+
};
|
|
1195
|
+
return iterator;
|
|
1043
1196
|
}
|
|
1044
1197
|
/**
|
|
1045
1198
|
* Get an iterator over the keys under this IDirectory.
|
|
@@ -1047,7 +1200,22 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1047
1200
|
*/
|
|
1048
1201
|
keys() {
|
|
1049
1202
|
this.throwIfDisposed();
|
|
1050
|
-
|
|
1203
|
+
const internalIterator = this.internalIterator();
|
|
1204
|
+
const next = () => {
|
|
1205
|
+
const nextResult = internalIterator.next();
|
|
1206
|
+
if (nextResult.done) {
|
|
1207
|
+
return { value: undefined, done: true };
|
|
1208
|
+
}
|
|
1209
|
+
const [key] = nextResult.value;
|
|
1210
|
+
return { value: key, done: false };
|
|
1211
|
+
};
|
|
1212
|
+
const iterator = {
|
|
1213
|
+
next,
|
|
1214
|
+
[Symbol.iterator]() {
|
|
1215
|
+
return this;
|
|
1216
|
+
},
|
|
1217
|
+
};
|
|
1218
|
+
return iterator;
|
|
1051
1219
|
}
|
|
1052
1220
|
/**
|
|
1053
1221
|
* Get an iterator over the values under this IDirectory.
|
|
@@ -1055,7 +1223,22 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1055
1223
|
*/
|
|
1056
1224
|
values() {
|
|
1057
1225
|
this.throwIfDisposed();
|
|
1058
|
-
|
|
1226
|
+
const internalIterator = this.internalIterator();
|
|
1227
|
+
const next = () => {
|
|
1228
|
+
const nextResult = internalIterator.next();
|
|
1229
|
+
if (nextResult.done) {
|
|
1230
|
+
return { value: undefined, done: true };
|
|
1231
|
+
}
|
|
1232
|
+
const [, localValue] = nextResult.value;
|
|
1233
|
+
return { value: localValue, done: false };
|
|
1234
|
+
};
|
|
1235
|
+
const iterator = {
|
|
1236
|
+
next,
|
|
1237
|
+
[Symbol.iterator]() {
|
|
1238
|
+
return this;
|
|
1239
|
+
},
|
|
1240
|
+
};
|
|
1241
|
+
return iterator;
|
|
1059
1242
|
}
|
|
1060
1243
|
/**
|
|
1061
1244
|
* Get an iterator over the entries under this IDirectory.
|
|
@@ -1063,7 +1246,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1063
1246
|
*/
|
|
1064
1247
|
[(_b = Symbol.toStringTag, Symbol.iterator)]() {
|
|
1065
1248
|
this.throwIfDisposed();
|
|
1066
|
-
return this.
|
|
1249
|
+
return this.internalIterator();
|
|
1067
1250
|
}
|
|
1068
1251
|
/**
|
|
1069
1252
|
* Process a clear operation.
|
|
@@ -1079,12 +1262,35 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1079
1262
|
return;
|
|
1080
1263
|
}
|
|
1081
1264
|
if (local) {
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1084
|
-
(0, internal_1.assert)(
|
|
1085
|
-
|
|
1265
|
+
this.sequencedStorageData.clear();
|
|
1266
|
+
const pendingClear = this.pendingStorageData.shift();
|
|
1267
|
+
(0, internal_1.assert)(pendingClear !== undefined &&
|
|
1268
|
+
pendingClear.type === "clear" &&
|
|
1269
|
+
pendingClear === localOpMetadata, 0xc04 /* Got a local clear message we weren't expecting */);
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
// For pending set operations, collect the previous values before clearing sequenced data
|
|
1273
|
+
const pendingSets = [];
|
|
1274
|
+
for (const entry of this.pendingStorageData) {
|
|
1275
|
+
if (entry.type === "lifetime") {
|
|
1276
|
+
const previousValue = this.sequencedStorageData.get(entry.key);
|
|
1277
|
+
pendingSets.push({ key: entry.key, previousValue });
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
this.sequencedStorageData.clear();
|
|
1281
|
+
// Only emit for remote ops, we would have already emitted for local ops. Only emit if there
|
|
1282
|
+
// is no optimistically-applied local pending clear that would supersede this remote clear.
|
|
1283
|
+
if (!this.pendingStorageData.some((entry) => entry.type === "clear")) {
|
|
1284
|
+
this.directory.emit("clear", local, this.directory);
|
|
1285
|
+
}
|
|
1286
|
+
// For pending set operations, emit valueChanged events
|
|
1287
|
+
for (const { key, previousValue } of pendingSets) {
|
|
1288
|
+
this.directory.emit("valueChanged", {
|
|
1289
|
+
key,
|
|
1290
|
+
previousValue,
|
|
1291
|
+
}, local, this.directory);
|
|
1292
|
+
}
|
|
1086
1293
|
}
|
|
1087
|
-
this.clearExceptPendingKeys(false);
|
|
1088
1294
|
}
|
|
1089
1295
|
/**
|
|
1090
1296
|
* Process a delete operation.
|
|
@@ -1096,11 +1302,33 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1096
1302
|
*/
|
|
1097
1303
|
processDeleteMessage(msg, op, local, localOpMetadata) {
|
|
1098
1304
|
this.throwIfDisposed();
|
|
1099
|
-
if (!
|
|
1100
|
-
this.needProcessStorageOperation(op, local, localOpMetadata))) {
|
|
1305
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1101
1306
|
return;
|
|
1102
1307
|
}
|
|
1103
|
-
|
|
1308
|
+
if (local) {
|
|
1309
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type !== "clear" && entry.key === op.key);
|
|
1310
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
1311
|
+
(0, internal_1.assert)(pendingEntry !== undefined &&
|
|
1312
|
+
pendingEntry.type === "delete" &&
|
|
1313
|
+
pendingEntry.key === op.key, 0xc05 /* Got a local delete message we weren't expecting */);
|
|
1314
|
+
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
1315
|
+
this.sequencedStorageData.delete(op.key);
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
const previousValue = this.sequencedStorageData.get(op.key);
|
|
1319
|
+
this.sequencedStorageData.delete(op.key);
|
|
1320
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
1321
|
+
if (!this.pendingStorageData.some((entry) => entry.type === "clear" || entry.key === op.key)) {
|
|
1322
|
+
const event = {
|
|
1323
|
+
key: op.key,
|
|
1324
|
+
path: this.absolutePath,
|
|
1325
|
+
previousValue,
|
|
1326
|
+
};
|
|
1327
|
+
this.directory.emit("valueChanged", event, local, this.directory);
|
|
1328
|
+
const containedEvent = { key: op.key, previousValue };
|
|
1329
|
+
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1104
1332
|
}
|
|
1105
1333
|
/**
|
|
1106
1334
|
* Process a set operation.
|
|
@@ -1112,13 +1340,33 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1112
1340
|
*/
|
|
1113
1341
|
processSetMessage(msg, op, value, local, localOpMetadata) {
|
|
1114
1342
|
this.throwIfDisposed();
|
|
1115
|
-
if (!
|
|
1116
|
-
this.needProcessStorageOperation(op, local, localOpMetadata))) {
|
|
1343
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1117
1344
|
return;
|
|
1118
1345
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1346
|
+
const { key } = op;
|
|
1347
|
+
if (local) {
|
|
1348
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type !== "clear" && entry.key === key);
|
|
1349
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
1350
|
+
(0, internal_1.assert)(pendingEntry !== undefined && pendingEntry.type === "lifetime", 0xc06 /* Couldn't match local set message to pending lifetime */);
|
|
1351
|
+
const pendingKeySet = pendingEntry.keySets.shift();
|
|
1352
|
+
(0, internal_1.assert)(pendingKeySet !== undefined && pendingKeySet === localOpMetadata, 0xc07 /* Got a local set message we weren't expecting */);
|
|
1353
|
+
if (pendingEntry.keySets.length === 0) {
|
|
1354
|
+
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
1355
|
+
}
|
|
1356
|
+
this.sequencedStorageData.set(key, pendingKeySet.value);
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
// Get the previous value before setting the new value
|
|
1360
|
+
const previousValue = this.sequencedStorageData.get(key);
|
|
1361
|
+
this.sequencedStorageData.set(key, value);
|
|
1362
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
1363
|
+
if (!this.pendingStorageData.some((entry) => entry.type === "clear" || entry.key === key)) {
|
|
1364
|
+
const event = { key, path: this.absolutePath, previousValue };
|
|
1365
|
+
this.directory.emit("valueChanged", event, local, this.directory);
|
|
1366
|
+
const containedEvent = { key, previousValue };
|
|
1367
|
+
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1122
1370
|
}
|
|
1123
1371
|
/**
|
|
1124
1372
|
* Process a create subdirectory operation.
|
|
@@ -1156,57 +1404,33 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1156
1404
|
/**
|
|
1157
1405
|
* Submit a clear operation.
|
|
1158
1406
|
* @param op - The operation
|
|
1407
|
+
* @param localOpMetadata - The pending operation metadata
|
|
1159
1408
|
*/
|
|
1160
|
-
submitClearMessage(op,
|
|
1409
|
+
submitClearMessage(op, localOpMetadata) {
|
|
1161
1410
|
this.throwIfDisposed();
|
|
1162
|
-
|
|
1163
|
-
this.pendingClearMessageIds.push(pendingMsgId);
|
|
1164
|
-
const metadata = {
|
|
1165
|
-
type: "clear",
|
|
1166
|
-
pendingMessageId: pendingMsgId,
|
|
1167
|
-
previousStorage: previousValue,
|
|
1168
|
-
};
|
|
1169
|
-
this.directory.submitDirectoryMessage(op, metadata);
|
|
1411
|
+
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1170
1412
|
}
|
|
1171
1413
|
/**
|
|
1172
1414
|
* Resubmit a clear operation.
|
|
1173
1415
|
* @param op - The operation
|
|
1174
1416
|
*/
|
|
1175
1417
|
resubmitClearMessage(op, localOpMetadata) {
|
|
1176
|
-
(0, internal_1.assert)(isClearLocalOpMetadata(localOpMetadata), 0x32b /* Invalid localOpMetadata for clear */);
|
|
1177
|
-
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
1178
|
-
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
1179
1418
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1180
1419
|
// is already deleted, in which case we don't need to submit the op.
|
|
1181
|
-
|
|
1182
|
-
|
|
1420
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type === "clear");
|
|
1421
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
1422
|
+
if (pendingEntry !== undefined) {
|
|
1423
|
+
this.submitClearMessage(op, localOpMetadata);
|
|
1183
1424
|
}
|
|
1184
1425
|
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Get a new pending message id for the op and cache it to track the pending op
|
|
1187
|
-
*/
|
|
1188
|
-
getKeyMessageId(op) {
|
|
1189
|
-
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
1190
|
-
const pendingMessageId = ++this.pendingMessageId;
|
|
1191
|
-
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1192
|
-
if (pendingMessageIds === undefined) {
|
|
1193
|
-
this.pendingKeys.set(op.key, [pendingMessageId]);
|
|
1194
|
-
}
|
|
1195
|
-
else {
|
|
1196
|
-
pendingMessageIds.push(pendingMessageId);
|
|
1197
|
-
}
|
|
1198
|
-
return pendingMessageId;
|
|
1199
|
-
}
|
|
1200
1426
|
/**
|
|
1201
1427
|
* Submit a key operation.
|
|
1202
1428
|
* @param op - The operation
|
|
1203
|
-
* @param
|
|
1429
|
+
* @param localOpMetadata - The pending operation metadata
|
|
1204
1430
|
*/
|
|
1205
|
-
submitKeyMessage(op,
|
|
1431
|
+
submitKeyMessage(op, localOpMetadata) {
|
|
1206
1432
|
this.throwIfDisposed();
|
|
1207
|
-
|
|
1208
|
-
const localMetadata = { type: "edit", pendingMessageId, previousValue };
|
|
1209
|
-
this.directory.submitDirectoryMessage(op, localMetadata);
|
|
1433
|
+
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1210
1434
|
}
|
|
1211
1435
|
/**
|
|
1212
1436
|
* Submit a key message to remote clients based on a previous submit.
|
|
@@ -1214,21 +1438,12 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1214
1438
|
* @param localOpMetadata - Metadata from the previous submit
|
|
1215
1439
|
*/
|
|
1216
1440
|
resubmitKeyMessage(op, localOpMetadata) {
|
|
1217
|
-
(0, internal_1.assert)(isKeyEditLocalOpMetadata(localOpMetadata), 0x32d /* Invalid localOpMetadata in submit */);
|
|
1218
|
-
// clear the old pending message id
|
|
1219
|
-
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1220
1441
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1221
1442
|
// is already deleted, in which case we don't need to submit the op.
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
pendingMessageIds.splice(index, 1);
|
|
1228
|
-
if (pendingMessageIds.length === 0) {
|
|
1229
|
-
this.pendingKeys.delete(op.key);
|
|
1230
|
-
}
|
|
1231
|
-
this.submitKeyMessage(op, localOpMetadata.previousValue);
|
|
1443
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type !== "clear" && entry.key === op.key);
|
|
1444
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
1445
|
+
if (pendingEntry !== undefined) {
|
|
1446
|
+
this.submitKeyMessage(op, localOpMetadata);
|
|
1232
1447
|
}
|
|
1233
1448
|
}
|
|
1234
1449
|
incrementPendingSubDirCount(map, subDirName) {
|
|
@@ -1286,7 +1501,6 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1286
1501
|
* @param localOpMetadata - metadata submitted with the op originally
|
|
1287
1502
|
*/
|
|
1288
1503
|
resubmitSubDirectoryMessage(op, localOpMetadata) {
|
|
1289
|
-
(0, internal_1.assert)(isSubDirLocalOpMetadata(localOpMetadata), 0x32f /* Invalid localOpMetadata for sub directory op */);
|
|
1290
1504
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1291
1505
|
// is already deleted, in which case we don't need to submit the op.
|
|
1292
1506
|
if (localOpMetadata.type === "createSubDir" &&
|
|
@@ -1301,8 +1515,9 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1301
1515
|
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1302
1516
|
this.submitCreateSubDirectoryMessage(op);
|
|
1303
1517
|
}
|
|
1304
|
-
else {
|
|
1518
|
+
else if (localOpMetadata.type === "deleteSubDir") {
|
|
1305
1519
|
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
1520
|
+
(0, internal_1.assert)(localOpMetadata.subDirectory !== undefined, 0xc08 /* localOpMetadata.subDirectory should be defined */);
|
|
1306
1521
|
this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
|
|
1307
1522
|
}
|
|
1308
1523
|
}
|
|
@@ -1313,7 +1528,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1313
1528
|
*/
|
|
1314
1529
|
*getSerializedStorage(serializer) {
|
|
1315
1530
|
this.throwIfDisposed();
|
|
1316
|
-
for (const [key, value] of this.
|
|
1531
|
+
for (const [key, value] of this.sequencedStorageData.entries()) {
|
|
1317
1532
|
const serializedValue = (0, localValues_js_1.serializeValue)(value, serializer, this.directory.handle);
|
|
1318
1533
|
const res = [key, serializedValue];
|
|
1319
1534
|
yield res;
|
|
@@ -1334,7 +1549,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1334
1549
|
*/
|
|
1335
1550
|
populateStorage(key, value) {
|
|
1336
1551
|
this.throwIfDisposed();
|
|
1337
|
-
this.
|
|
1552
|
+
this.sequencedStorageData.set(key, value);
|
|
1338
1553
|
}
|
|
1339
1554
|
/**
|
|
1340
1555
|
* Populate a subdirectory into this subdirectory, to be used when loading from snapshot.
|
|
@@ -1346,32 +1561,6 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1346
1561
|
this.registerEventsOnSubDirectory(newSubDir, subdirName);
|
|
1347
1562
|
this._subdirectories.set(subdirName, newSubDir);
|
|
1348
1563
|
}
|
|
1349
|
-
/**
|
|
1350
|
-
* Retrieve the local value at the given key. This is used to get value type information stashed on the local
|
|
1351
|
-
* value so op handlers can be retrieved
|
|
1352
|
-
* @param key - The key to retrieve from
|
|
1353
|
-
* @returns The local value
|
|
1354
|
-
*/
|
|
1355
|
-
getLocalValue(key) {
|
|
1356
|
-
this.throwIfDisposed();
|
|
1357
|
-
return this._storage.get(key);
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* Remove the pendingMessageId from the map tracking it on rollback
|
|
1361
|
-
* @param map - map tracking the pending messages
|
|
1362
|
-
* @param key - key of the edit in the op
|
|
1363
|
-
*/
|
|
1364
|
-
rollbackPendingMessageId(map, key, pendingMessageId) {
|
|
1365
|
-
const pendingMessageIds = map.get(key);
|
|
1366
|
-
const lastPendingMessageId = pendingMessageIds?.pop();
|
|
1367
|
-
if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
|
|
1368
|
-
throw new Error("Rollback op does not match last pending");
|
|
1369
|
-
}
|
|
1370
|
-
if (pendingMessageIds.length === 0) {
|
|
1371
|
-
map.delete(key);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
1375
1564
|
/**
|
|
1376
1565
|
* Rollback a local op
|
|
1377
1566
|
* @param op - The operation to rollback
|
|
@@ -1379,41 +1568,80 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1379
1568
|
*/
|
|
1380
1569
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1381
1570
|
rollback(op, localOpMetadata) {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1571
|
+
const directoryOp = op;
|
|
1572
|
+
if (directoryOp.type === "clear") {
|
|
1573
|
+
// A pending clear will be last in the list, since it terminates all prior lifetimes.
|
|
1574
|
+
const pendingClear = this.pendingStorageData.pop();
|
|
1575
|
+
(0, internal_1.assert)(pendingClear !== undefined &&
|
|
1576
|
+
pendingClear.type === "clear" &&
|
|
1577
|
+
localOpMetadata.type === "clear", 0xc09 /* Unexpected clear rollback */);
|
|
1578
|
+
for (const [key] of this.internalIterator()) {
|
|
1579
|
+
const event = {
|
|
1580
|
+
key,
|
|
1581
|
+
path: this.absolutePath,
|
|
1582
|
+
previousValue: undefined,
|
|
1583
|
+
};
|
|
1584
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
1585
|
+
const containedEvent = { key, previousValue: undefined };
|
|
1586
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
1393
1587
|
}
|
|
1394
1588
|
}
|
|
1395
|
-
else if ((
|
|
1396
|
-
localOpMetadata.type === "
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
(0,
|
|
1400
|
-
|
|
1401
|
-
|
|
1589
|
+
else if ((directoryOp.type === "delete" || directoryOp.type === "set") &&
|
|
1590
|
+
(localOpMetadata.type === "set" || localOpMetadata.type === "delete")) {
|
|
1591
|
+
// A pending set/delete may not be last in the list, as the lifetimes' order is based on when
|
|
1592
|
+
// they were created, not when they were last modified.
|
|
1593
|
+
const pendingEntryIndex = (0, utils_js_1.findLastIndex)(this.pendingStorageData, (entry) => entry.type !== "clear" && entry.key === directoryOp.key);
|
|
1594
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
1595
|
+
(0, internal_1.assert)(pendingEntry !== undefined &&
|
|
1596
|
+
(pendingEntry.type === "delete" || pendingEntry.type === "lifetime"), 0xc0a /* Unexpected pending data for set/delete op */);
|
|
1597
|
+
if (pendingEntry.type === "delete") {
|
|
1598
|
+
(0, internal_1.assert)(pendingEntry === localOpMetadata, 0xc0b /* Unexpected delete rollback */);
|
|
1599
|
+
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
1600
|
+
// Only emit if rolling back the delete actually results in a value becoming visible.
|
|
1601
|
+
if (this.getOptimisticValue(directoryOp.key) !== undefined) {
|
|
1602
|
+
const event = {
|
|
1603
|
+
key: directoryOp.key,
|
|
1604
|
+
path: this.absolutePath,
|
|
1605
|
+
previousValue: undefined,
|
|
1606
|
+
};
|
|
1607
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
1608
|
+
const containedEvent = {
|
|
1609
|
+
key: directoryOp.key,
|
|
1610
|
+
previousValue: undefined,
|
|
1611
|
+
};
|
|
1612
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
1613
|
+
}
|
|
1402
1614
|
}
|
|
1403
|
-
else {
|
|
1404
|
-
|
|
1615
|
+
else if (pendingEntry.type === "lifetime") {
|
|
1616
|
+
const pendingKeySet = pendingEntry.keySets.pop();
|
|
1617
|
+
(0, internal_1.assert)(pendingKeySet !== undefined && pendingKeySet === localOpMetadata, 0xc0c /* Unexpected set rollback */);
|
|
1618
|
+
if (pendingEntry.keySets.length === 0) {
|
|
1619
|
+
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
1620
|
+
}
|
|
1621
|
+
const event = {
|
|
1622
|
+
key: directoryOp.key,
|
|
1623
|
+
path: this.absolutePath,
|
|
1624
|
+
previousValue: pendingKeySet.value,
|
|
1625
|
+
};
|
|
1626
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
1627
|
+
const containedEvent = {
|
|
1628
|
+
key: directoryOp.key,
|
|
1629
|
+
previousValue: pendingKeySet.value,
|
|
1630
|
+
};
|
|
1631
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
1405
1632
|
}
|
|
1406
|
-
this.rollbackPendingMessageId(this.pendingKeys, key, localOpMetadata.pendingMessageId);
|
|
1407
1633
|
}
|
|
1408
|
-
else if (
|
|
1409
|
-
|
|
1634
|
+
else if (directoryOp.type === "createSubDirectory" &&
|
|
1635
|
+
localOpMetadata.type === "createSubDir") {
|
|
1636
|
+
const subdirName = directoryOp.subdirName;
|
|
1410
1637
|
(0, internal_1.assert)(subdirName !== undefined, 0x8af /* "subdirName" property is missing from "createSubDirectory" operation. */);
|
|
1411
1638
|
(0, internal_1.assert)(typeof subdirName === "string", 0x8b0 /* "subdirName" property in "createSubDirectory" operation is misconfigured. Expected a string. */);
|
|
1412
1639
|
this.deleteSubDirectoryCore(subdirName, true);
|
|
1413
1640
|
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, subdirName);
|
|
1414
1641
|
}
|
|
1415
|
-
else if (
|
|
1416
|
-
|
|
1642
|
+
else if (directoryOp.type === "deleteSubDirectory" &&
|
|
1643
|
+
localOpMetadata.type === "deleteSubDir") {
|
|
1644
|
+
const subdirName = directoryOp.subdirName;
|
|
1417
1645
|
(0, internal_1.assert)(subdirName !== undefined, 0x8b1 /* "subdirName" property is missing from "deleteSubDirectory" operation. */);
|
|
1418
1646
|
(0, internal_1.assert)(typeof subdirName === "string", 0x8b2 /* "subdirName" property in "deleteSubDirectory" operation is misconfigured. Expected a string. */);
|
|
1419
1647
|
if (localOpMetadata.subDirectory !== undefined) {
|
|
@@ -1439,7 +1667,6 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1439
1667
|
throw new Error("Unsupported op for rollback");
|
|
1440
1668
|
}
|
|
1441
1669
|
}
|
|
1442
|
-
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
|
1443
1670
|
/**
|
|
1444
1671
|
* Converts the given relative path into an absolute path.
|
|
1445
1672
|
* @param path - Relative path to convert
|
|
@@ -1448,71 +1675,6 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1448
1675
|
makeAbsolute(relativePath) {
|
|
1449
1676
|
return posix.resolve(this.absolutePath, relativePath);
|
|
1450
1677
|
}
|
|
1451
|
-
/**
|
|
1452
|
-
* If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
|
|
1453
|
-
* not process the incoming operation.
|
|
1454
|
-
* @param op - Operation to check
|
|
1455
|
-
* @param local - Whether the operation originated from the local client
|
|
1456
|
-
* @param localOpMetadata - For local client ops, this is the metadata that was submitted with the op.
|
|
1457
|
-
* For ops from a remote client, this will be undefined.
|
|
1458
|
-
* @returns True if the operation should be processed, false otherwise
|
|
1459
|
-
*/
|
|
1460
|
-
needProcessStorageOperation(op, local, localOpMetadata) {
|
|
1461
|
-
const firstPendingClearMessageId = this.pendingClearMessageIds[0];
|
|
1462
|
-
if (firstPendingClearMessageId !== undefined) {
|
|
1463
|
-
if (local) {
|
|
1464
|
-
(0, internal_1.assert)(localOpMetadata !== undefined &&
|
|
1465
|
-
isKeyEditLocalOpMetadata(localOpMetadata) &&
|
|
1466
|
-
localOpMetadata.pendingMessageId < firstPendingClearMessageId, 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
|
|
1467
|
-
// Remove all pendingMessageIds lower than first pendingClearMessageId.
|
|
1468
|
-
const lowestPendingClearMessageId = firstPendingClearMessageId;
|
|
1469
|
-
const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
|
|
1470
|
-
if (pendingKeyMessageIdArray !== undefined) {
|
|
1471
|
-
let index = 0;
|
|
1472
|
-
let pendingKeyMessageId = pendingKeyMessageIdArray[index];
|
|
1473
|
-
while (pendingKeyMessageId !== undefined &&
|
|
1474
|
-
pendingKeyMessageId < lowestPendingClearMessageId) {
|
|
1475
|
-
index += 1;
|
|
1476
|
-
pendingKeyMessageId = pendingKeyMessageIdArray[index];
|
|
1477
|
-
}
|
|
1478
|
-
const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
|
|
1479
|
-
if (newPendingKeyMessageId.length === 0) {
|
|
1480
|
-
this.pendingKeys.delete(op.key);
|
|
1481
|
-
}
|
|
1482
|
-
else {
|
|
1483
|
-
this.pendingKeys.set(op.key, newPendingKeyMessageId);
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
// If I have a NACK clear, we can ignore all ops.
|
|
1488
|
-
return false;
|
|
1489
|
-
}
|
|
1490
|
-
const pendingKeyMessageIds = this.pendingKeys.get(op.key);
|
|
1491
|
-
if (pendingKeyMessageIds !== undefined) {
|
|
1492
|
-
// Found an NACK op, clear it from the directory if the latest sequence number in the directory
|
|
1493
|
-
// match the message's and don't process the op.
|
|
1494
|
-
if (local) {
|
|
1495
|
-
(0, internal_1.assert)(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata), 0x011 /* pendingMessageId is missing from the local client's operation */);
|
|
1496
|
-
if (pendingKeyMessageIds[0] !== localOpMetadata.pendingMessageId) {
|
|
1497
|
-
// TODO: AB#7742: Hitting this block indicates that the pending message Id received
|
|
1498
|
-
// is not consistent with the "next" local op
|
|
1499
|
-
this.logger.sendTelemetryEvent({
|
|
1500
|
-
eventName: "unexpectedPendingMessage",
|
|
1501
|
-
expectedPendingMessage: pendingKeyMessageIds[0],
|
|
1502
|
-
actualPendingMessage: localOpMetadata.pendingMessageId,
|
|
1503
|
-
expectedPendingMessagesLength: pendingKeyMessageIds.length,
|
|
1504
|
-
});
|
|
1505
|
-
}
|
|
1506
|
-
pendingKeyMessageIds.shift();
|
|
1507
|
-
if (pendingKeyMessageIds.length === 0) {
|
|
1508
|
-
this.pendingKeys.delete(op.key);
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
return false;
|
|
1512
|
-
}
|
|
1513
|
-
// If we don't have a NACK op on the key, we need to process the remote ops.
|
|
1514
|
-
return !local;
|
|
1515
|
-
}
|
|
1516
1678
|
/**
|
|
1517
1679
|
* This return true if the message is for the current instance of this sub directory. As the sub directory
|
|
1518
1680
|
* can be deleted and created again, then this finds if the message is for current instance of directory or not.
|
|
@@ -1543,7 +1705,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1543
1705
|
if ((pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
|
|
1544
1706
|
(pendingCreateCount !== undefined && pendingCreateCount > 0)) {
|
|
1545
1707
|
if (local) {
|
|
1546
|
-
(0, internal_1.assert)(
|
|
1708
|
+
(0, internal_1.assert)(localOpMetadata !== undefined, 0xc0d /* localOpMetadata should be defined */);
|
|
1547
1709
|
if (localOpMetadata.type === "deleteSubDir") {
|
|
1548
1710
|
(0, internal_1.assert)(pendingDeleteCount !== undefined && pendingDeleteCount > 0, 0x6c2 /* pendingDeleteCount should exist */);
|
|
1549
1711
|
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
@@ -1560,7 +1722,8 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1560
1722
|
}
|
|
1561
1723
|
// If this is delete op and we have keys in this subDirectory, then we need to delete these
|
|
1562
1724
|
// keys except the pending ones as they will be sequenced after this delete.
|
|
1563
|
-
directory.
|
|
1725
|
+
directory.sequencedStorageData.clear();
|
|
1726
|
+
directory.emit("clear", true, directory);
|
|
1564
1727
|
// In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
|
|
1565
1728
|
// creators as the previous directory is getting deleted and we will initialize again when
|
|
1566
1729
|
// we will receive op for the create again.
|
|
@@ -1616,68 +1779,6 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
|
1616
1779
|
}
|
|
1617
1780
|
return !local;
|
|
1618
1781
|
}
|
|
1619
|
-
/**
|
|
1620
|
-
* Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
|
|
1621
|
-
*/
|
|
1622
|
-
clearExceptPendingKeys(local) {
|
|
1623
|
-
// Assuming the pendingKeys is small and the map is large
|
|
1624
|
-
// we will get the value for the pendingKeys and clear the map
|
|
1625
|
-
const temp = new Map();
|
|
1626
|
-
for (const [key] of this.pendingKeys) {
|
|
1627
|
-
const value = this._storage.get(key);
|
|
1628
|
-
// If this key is already deleted, then we don't need to add it again.
|
|
1629
|
-
if (value !== undefined) {
|
|
1630
|
-
temp.set(key, value);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
this.clearCore(local);
|
|
1634
|
-
for (const [key, value] of temp.entries()) {
|
|
1635
|
-
this.setCore(key, value, true);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
/**
|
|
1639
|
-
* Clear implementation used for both locally sourced clears as well as incoming remote clears.
|
|
1640
|
-
* @param local - Whether the message originated from the local client
|
|
1641
|
-
*/
|
|
1642
|
-
clearCore(local) {
|
|
1643
|
-
this._storage.clear();
|
|
1644
|
-
this.directory.emit("clear", local, this.directory);
|
|
1645
|
-
}
|
|
1646
|
-
/**
|
|
1647
|
-
* Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
|
|
1648
|
-
* @param key - The key being deleted
|
|
1649
|
-
* @param local - Whether the message originated from the local client
|
|
1650
|
-
* @returns Previous local value of the key if it existed, undefined if it did not exist
|
|
1651
|
-
*/
|
|
1652
|
-
deleteCore(key, local) {
|
|
1653
|
-
const previousLocalValue = this._storage.get(key);
|
|
1654
|
-
const previousValue = previousLocalValue;
|
|
1655
|
-
const successfullyRemoved = this._storage.delete(key);
|
|
1656
|
-
if (successfullyRemoved) {
|
|
1657
|
-
const event = { key, path: this.absolutePath, previousValue };
|
|
1658
|
-
this.directory.emit("valueChanged", event, local, this.directory);
|
|
1659
|
-
const containedEvent = { key, previousValue };
|
|
1660
|
-
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1661
|
-
}
|
|
1662
|
-
return previousLocalValue;
|
|
1663
|
-
}
|
|
1664
|
-
/**
|
|
1665
|
-
* Set implementation used for both locally sourced sets as well as incoming remote sets.
|
|
1666
|
-
* @param key - The key being set
|
|
1667
|
-
* @param value - The value being set
|
|
1668
|
-
* @param local - Whether the message originated from the local client
|
|
1669
|
-
* @returns Previous local value of the key, if any
|
|
1670
|
-
*/
|
|
1671
|
-
setCore(key, value, local) {
|
|
1672
|
-
const previousLocalValue = this._storage.get(key);
|
|
1673
|
-
const previousValue = previousLocalValue;
|
|
1674
|
-
this._storage.set(key, value);
|
|
1675
|
-
const event = { key, path: this.absolutePath, previousValue };
|
|
1676
|
-
this.directory.emit("valueChanged", event, local, this.directory);
|
|
1677
|
-
const containedEvent = { key, previousValue };
|
|
1678
|
-
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1679
|
-
return previousLocalValue;
|
|
1680
|
-
}
|
|
1681
1782
|
/**
|
|
1682
1783
|
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
1683
1784
|
* @param subdirName - The name of the subdirectory being created
|