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