@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/src/directory.ts
CHANGED
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
ISerializedValue,
|
|
49
49
|
} from "./internalInterfaces.js";
|
|
50
50
|
import { serializeValue, migrateIfSharedSerializable } from "./localValues.js";
|
|
51
|
+
import { findLast, findLastIndex } from "./utils.js";
|
|
51
52
|
|
|
52
53
|
// We use path-browserify since this code can run safely on the server or the browser.
|
|
53
54
|
// We standardize on using posix slashes everywhere.
|
|
@@ -71,15 +72,15 @@ interface IDirectoryMessageHandler {
|
|
|
71
72
|
msg: ISequencedDocumentMessage,
|
|
72
73
|
op: IDirectoryOperation,
|
|
73
74
|
local: boolean,
|
|
74
|
-
localOpMetadata:
|
|
75
|
+
localOpMetadata: DirectoryLocalOpMetadata | undefined,
|
|
75
76
|
): void;
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
|
-
*
|
|
79
|
-
* @param op - The directory operation to
|
|
80
|
-
* @param localOpMetadata - The metadata
|
|
79
|
+
* Resubmit a previously submitted operation that was not delivered.
|
|
80
|
+
* @param op - The directory operation to resubmit
|
|
81
|
+
* @param localOpMetadata - The metadata that was originally submitted with the message.
|
|
81
82
|
*/
|
|
82
|
-
|
|
83
|
+
resubmit(op: IDirectoryOperation, localOpMetadata: DirectoryLocalOpMetadata): void;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -205,6 +206,45 @@ export type IDirectorySubDirectoryOperation =
|
|
|
205
206
|
*/
|
|
206
207
|
export type IDirectoryOperation = IDirectoryStorageOperation | IDirectorySubDirectoryOperation;
|
|
207
208
|
|
|
209
|
+
interface PendingKeySet {
|
|
210
|
+
type: "set";
|
|
211
|
+
path: string;
|
|
212
|
+
value: unknown;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
interface PendingKeyDelete {
|
|
216
|
+
type: "delete";
|
|
217
|
+
path: string;
|
|
218
|
+
key: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface PendingClear {
|
|
222
|
+
type: "clear";
|
|
223
|
+
path: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Represents the "lifetime" of a series of pending set operations before the pending
|
|
228
|
+
* set operations are ack'd.
|
|
229
|
+
*/
|
|
230
|
+
interface PendingKeyLifetime {
|
|
231
|
+
type: "lifetime";
|
|
232
|
+
key: string;
|
|
233
|
+
path: string;
|
|
234
|
+
/**
|
|
235
|
+
* A non-empty array of pending key sets that occurred during this lifetime. If the list
|
|
236
|
+
* becomes empty (e.g. during processing or rollback), the lifetime no longer exists and
|
|
237
|
+
* must be removed from the pending data.
|
|
238
|
+
*/
|
|
239
|
+
keySets: PendingKeySet[];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A member of the pendingStorageData array, which tracks outstanding changes and can be used to
|
|
244
|
+
* compute optimistic values. Local sets are aggregated into lifetimes.
|
|
245
|
+
*/
|
|
246
|
+
type PendingStorageEntry = PendingKeyLifetime | PendingKeyDelete | PendingClear;
|
|
247
|
+
|
|
208
248
|
/**
|
|
209
249
|
* Create info for the subdirectory.
|
|
210
250
|
*
|
|
@@ -650,7 +690,10 @@ export class SharedDirectory
|
|
|
650
690
|
* @param localOpMetadata - The local metadata associated with the op. We send a unique id that is used to track
|
|
651
691
|
* this op while it has not been ack'd. This will be sent when we receive this op back from the server.
|
|
652
692
|
*/
|
|
653
|
-
public submitDirectoryMessage(
|
|
693
|
+
public submitDirectoryMessage(
|
|
694
|
+
op: IDirectoryOperation,
|
|
695
|
+
localOpMetadata: DirectoryLocalOpMetadata,
|
|
696
|
+
): void {
|
|
654
697
|
this.submitLocalMessage(op, localOpMetadata);
|
|
655
698
|
}
|
|
656
699
|
|
|
@@ -662,11 +705,14 @@ export class SharedDirectory
|
|
|
662
705
|
/**
|
|
663
706
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
|
|
664
707
|
*/
|
|
665
|
-
protected override reSubmitCore(
|
|
708
|
+
protected override reSubmitCore(
|
|
709
|
+
content: unknown,
|
|
710
|
+
localOpMetadata: DirectoryLocalOpMetadata,
|
|
711
|
+
): void {
|
|
666
712
|
const message = content as IDirectoryOperation;
|
|
667
713
|
const handler = this.messageHandlers.get(message.type);
|
|
668
714
|
assert(handler !== undefined, 0x00d /* Missing message handler for message type */);
|
|
669
|
-
handler.
|
|
715
|
+
handler.resubmit(message, localOpMetadata);
|
|
670
716
|
}
|
|
671
717
|
|
|
672
718
|
/**
|
|
@@ -774,7 +820,7 @@ export class SharedDirectory
|
|
|
774
820
|
protected processCore(
|
|
775
821
|
message: ISequencedDocumentMessage,
|
|
776
822
|
local: boolean,
|
|
777
|
-
localOpMetadata:
|
|
823
|
+
localOpMetadata: DirectoryLocalOpMetadata | undefined,
|
|
778
824
|
): void {
|
|
779
825
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
780
826
|
if (message.type === MessageType.Operation) {
|
|
@@ -791,7 +837,10 @@ export class SharedDirectory
|
|
|
791
837
|
/**
|
|
792
838
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
|
|
793
839
|
*/
|
|
794
|
-
protected override rollback(
|
|
840
|
+
protected override rollback(
|
|
841
|
+
content: unknown,
|
|
842
|
+
localOpMetadata: DirectoryLocalOpMetadata,
|
|
843
|
+
): void {
|
|
795
844
|
const op: IDirectoryOperation = content as IDirectoryOperation;
|
|
796
845
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
797
846
|
if (subdir) {
|
|
@@ -839,8 +888,8 @@ export class SharedDirectory
|
|
|
839
888
|
process: (
|
|
840
889
|
msg: ISequencedDocumentMessage,
|
|
841
890
|
op: IDirectoryClearOperation,
|
|
842
|
-
local,
|
|
843
|
-
localOpMetadata,
|
|
891
|
+
local: boolean,
|
|
892
|
+
localOpMetadata: ClearLocalOpMetadata | undefined,
|
|
844
893
|
) => {
|
|
845
894
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
846
895
|
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
@@ -849,7 +898,7 @@ export class SharedDirectory
|
|
|
849
898
|
subdir.processClearMessage(msg, op, local, localOpMetadata);
|
|
850
899
|
}
|
|
851
900
|
},
|
|
852
|
-
|
|
901
|
+
resubmit: (op: IDirectoryClearOperation, localOpMetadata: ClearLocalOpMetadata) => {
|
|
853
902
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
854
903
|
if (subdir) {
|
|
855
904
|
subdir.resubmitClearMessage(op, localOpMetadata);
|
|
@@ -860,8 +909,8 @@ export class SharedDirectory
|
|
|
860
909
|
process: (
|
|
861
910
|
msg: ISequencedDocumentMessage,
|
|
862
911
|
op: IDirectoryDeleteOperation,
|
|
863
|
-
local,
|
|
864
|
-
localOpMetadata,
|
|
912
|
+
local: boolean,
|
|
913
|
+
localOpMetadata: EditLocalOpMetadata | undefined,
|
|
865
914
|
) => {
|
|
866
915
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
867
916
|
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
@@ -870,7 +919,7 @@ export class SharedDirectory
|
|
|
870
919
|
subdir.processDeleteMessage(msg, op, local, localOpMetadata);
|
|
871
920
|
}
|
|
872
921
|
},
|
|
873
|
-
|
|
922
|
+
resubmit: (op: IDirectoryDeleteOperation, localOpMetadata: EditLocalOpMetadata) => {
|
|
874
923
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
875
924
|
if (subdir) {
|
|
876
925
|
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
@@ -881,8 +930,8 @@ export class SharedDirectory
|
|
|
881
930
|
process: (
|
|
882
931
|
msg: ISequencedDocumentMessage,
|
|
883
932
|
op: IDirectorySetOperation,
|
|
884
|
-
local,
|
|
885
|
-
localOpMetadata,
|
|
933
|
+
local: boolean,
|
|
934
|
+
localOpMetadata: EditLocalOpMetadata | undefined,
|
|
886
935
|
) => {
|
|
887
936
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
888
937
|
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
@@ -893,7 +942,7 @@ export class SharedDirectory
|
|
|
893
942
|
subdir.processSetMessage(msg, op, localValue, local, localOpMetadata);
|
|
894
943
|
}
|
|
895
944
|
},
|
|
896
|
-
|
|
945
|
+
resubmit: (op: IDirectorySetOperation, localOpMetadata: EditLocalOpMetadata) => {
|
|
897
946
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
898
947
|
if (subdir) {
|
|
899
948
|
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
@@ -905,8 +954,8 @@ export class SharedDirectory
|
|
|
905
954
|
process: (
|
|
906
955
|
msg: ISequencedDocumentMessage,
|
|
907
956
|
op: IDirectoryCreateSubDirectoryOperation,
|
|
908
|
-
local,
|
|
909
|
-
localOpMetadata,
|
|
957
|
+
local: boolean,
|
|
958
|
+
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
910
959
|
) => {
|
|
911
960
|
const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
912
961
|
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
@@ -915,7 +964,10 @@ export class SharedDirectory
|
|
|
915
964
|
parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
916
965
|
}
|
|
917
966
|
},
|
|
918
|
-
|
|
967
|
+
resubmit: (
|
|
968
|
+
op: IDirectoryCreateSubDirectoryOperation,
|
|
969
|
+
localOpMetadata: SubDirLocalOpMetadata,
|
|
970
|
+
) => {
|
|
919
971
|
const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
920
972
|
if (parentSubdir) {
|
|
921
973
|
// We don't reuse the metadata but send a new one on each submit.
|
|
@@ -928,8 +980,8 @@ export class SharedDirectory
|
|
|
928
980
|
process: (
|
|
929
981
|
msg: ISequencedDocumentMessage,
|
|
930
982
|
op: IDirectoryDeleteSubDirectoryOperation,
|
|
931
|
-
local,
|
|
932
|
-
localOpMetadata,
|
|
983
|
+
local: boolean,
|
|
984
|
+
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
933
985
|
) => {
|
|
934
986
|
const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
935
987
|
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
@@ -938,7 +990,10 @@ export class SharedDirectory
|
|
|
938
990
|
parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
939
991
|
}
|
|
940
992
|
},
|
|
941
|
-
|
|
993
|
+
resubmit: (
|
|
994
|
+
op: IDirectoryDeleteSubDirectoryOperation,
|
|
995
|
+
localOpMetadata: SubDirLocalOpMetadata,
|
|
996
|
+
) => {
|
|
942
997
|
const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
943
998
|
if (parentSubdir) {
|
|
944
999
|
// We don't reuse the metadata but send a new one on each submit.
|
|
@@ -1050,18 +1105,6 @@ export class SharedDirectory
|
|
|
1050
1105
|
}
|
|
1051
1106
|
}
|
|
1052
1107
|
|
|
1053
|
-
interface IKeyEditLocalOpMetadata {
|
|
1054
|
-
type: "edit";
|
|
1055
|
-
pendingMessageId: number;
|
|
1056
|
-
previousValue: unknown;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
interface IClearLocalOpMetadata {
|
|
1060
|
-
type: "clear";
|
|
1061
|
-
pendingMessageId: number;
|
|
1062
|
-
previousStorage: Map<string, unknown>;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
1108
|
interface ICreateSubDirLocalOpMetadata {
|
|
1066
1109
|
type: "createSubDir";
|
|
1067
1110
|
}
|
|
@@ -1073,49 +1116,16 @@ interface IDeleteSubDirLocalOpMetadata {
|
|
|
1073
1116
|
|
|
1074
1117
|
type SubDirLocalOpMetadata = ICreateSubDirLocalOpMetadata | IDeleteSubDirLocalOpMetadata;
|
|
1075
1118
|
|
|
1076
|
-
|
|
1077
|
-
* Types of local op metadata.
|
|
1078
|
-
*/
|
|
1079
|
-
export type DirectoryLocalOpMetadata =
|
|
1080
|
-
| IClearLocalOpMetadata
|
|
1081
|
-
| IKeyEditLocalOpMetadata
|
|
1082
|
-
| SubDirLocalOpMetadata;
|
|
1083
|
-
|
|
1084
|
-
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
1085
|
-
|
|
1086
|
-
function isKeyEditLocalOpMetadata(metadata: any): metadata is IKeyEditLocalOpMetadata {
|
|
1087
|
-
return (
|
|
1088
|
-
metadata !== undefined &&
|
|
1089
|
-
typeof metadata.pendingMessageId === "number" &&
|
|
1090
|
-
metadata.type === "edit"
|
|
1091
|
-
);
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
function isClearLocalOpMetadata(metadata: any): metadata is IClearLocalOpMetadata {
|
|
1095
|
-
return (
|
|
1096
|
-
metadata !== undefined &&
|
|
1097
|
-
metadata.type === "clear" &&
|
|
1098
|
-
typeof metadata.pendingMessageId === "number" &&
|
|
1099
|
-
typeof metadata.previousStorage === "object"
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1119
|
+
type EditLocalOpMetadata = PendingKeySet | PendingKeyDelete;
|
|
1102
1120
|
|
|
1103
|
-
|
|
1104
|
-
return (
|
|
1105
|
-
metadata !== undefined &&
|
|
1106
|
-
(metadata.type === "createSubDir" || metadata.type === "deleteSubDir")
|
|
1107
|
-
);
|
|
1108
|
-
}
|
|
1121
|
+
type ClearLocalOpMetadata = PendingClear;
|
|
1109
1122
|
|
|
1110
|
-
|
|
1111
|
-
return (
|
|
1112
|
-
isKeyEditLocalOpMetadata(metadata) ||
|
|
1113
|
-
isClearLocalOpMetadata(metadata) ||
|
|
1114
|
-
isSubDirLocalOpMetadata(metadata)
|
|
1115
|
-
);
|
|
1116
|
-
}
|
|
1123
|
+
type StorageLocalOpMetadata = EditLocalOpMetadata | ClearLocalOpMetadata;
|
|
1117
1124
|
|
|
1118
|
-
|
|
1125
|
+
/**
|
|
1126
|
+
* Types of local op metadata.
|
|
1127
|
+
*/
|
|
1128
|
+
export type DirectoryLocalOpMetadata = StorageLocalOpMetadata | SubDirLocalOpMetadata;
|
|
1119
1129
|
|
|
1120
1130
|
// eslint-disable-next-line @rushstack/no-new-null
|
|
1121
1131
|
function assertNonNullClientId(clientId: string | null): asserts clientId is string {
|
|
@@ -1139,24 +1149,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1139
1149
|
*/
|
|
1140
1150
|
public [Symbol.toStringTag]: string = "SubDirectory";
|
|
1141
1151
|
|
|
1142
|
-
/**
|
|
1143
|
-
* The in-memory data the directory is storing.
|
|
1144
|
-
*/
|
|
1145
|
-
private readonly _storage = new Map<string, unknown>();
|
|
1146
|
-
|
|
1147
1152
|
/**
|
|
1148
1153
|
* The subdirectories the directory is holding.
|
|
1149
1154
|
*/
|
|
1150
1155
|
private readonly _subdirectories = new Map<string, SubDirectory>();
|
|
1151
1156
|
|
|
1152
|
-
/**
|
|
1153
|
-
* Keys that have been modified locally but not yet ack'd from the server. This is for operations on keys like
|
|
1154
|
-
* set/delete operations on keys. The value of this map is list of pendingMessageIds at which that key
|
|
1155
|
-
* was modified. We don't store the type of ops, and behaviour of key ops are different from behaviour of sub
|
|
1156
|
-
* directory ops, so we have separate map from subDirectories tracker.
|
|
1157
|
-
*/
|
|
1158
|
-
private readonly pendingKeys = new Map<string, number[]>();
|
|
1159
|
-
|
|
1160
1157
|
/**
|
|
1161
1158
|
* Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
|
|
1162
1159
|
* of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
@@ -1171,16 +1168,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1171
1168
|
*/
|
|
1172
1169
|
private readonly pendingCreateSubDirectoriesTracker = new Map<string, number>();
|
|
1173
1170
|
|
|
1174
|
-
/**
|
|
1175
|
-
* This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
|
|
1176
|
-
*/
|
|
1177
|
-
private pendingMessageId: number = -1;
|
|
1178
|
-
|
|
1179
|
-
/**
|
|
1180
|
-
* The pending ids of any clears that have been performed locally but not yet ack'd from the server
|
|
1181
|
-
*/
|
|
1182
|
-
private readonly pendingClearMessageIds: number[] = [];
|
|
1183
|
-
|
|
1184
1171
|
/**
|
|
1185
1172
|
* Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
|
|
1186
1173
|
* of the creation order.
|
|
@@ -1251,15 +1238,14 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1251
1238
|
*/
|
|
1252
1239
|
public has(key: string): boolean {
|
|
1253
1240
|
this.throwIfDisposed();
|
|
1254
|
-
return this.
|
|
1241
|
+
return this.optimisticallyHas(key);
|
|
1255
1242
|
}
|
|
1256
1243
|
|
|
1257
1244
|
/**
|
|
1258
1245
|
* {@inheritDoc IDirectory.get}
|
|
1259
1246
|
*/
|
|
1260
1247
|
public get<T = unknown>(key: string): T | undefined {
|
|
1261
|
-
this.
|
|
1262
|
-
return this._storage.get(key) as T | undefined;
|
|
1248
|
+
return this.getOptimisticValue(key) as T | undefined;
|
|
1263
1249
|
}
|
|
1264
1250
|
|
|
1265
1251
|
/**
|
|
@@ -1271,25 +1257,70 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1271
1257
|
if (key === undefined || key === null) {
|
|
1272
1258
|
throw new Error("Undefined and null keys are not supported");
|
|
1273
1259
|
}
|
|
1260
|
+
const previousOptimisticLocalValue = this.getOptimisticValue(key);
|
|
1274
1261
|
|
|
1275
1262
|
// Create a local value and serialize it.
|
|
1276
1263
|
bindHandles(value, this.serializer, this.directory.handle);
|
|
1277
1264
|
|
|
1278
|
-
// Set the value locally.
|
|
1279
|
-
const previousValue = this.setCore(key, value, true);
|
|
1280
|
-
|
|
1281
1265
|
// If we are not attached, don't submit the op.
|
|
1282
1266
|
if (!this.directory.isAttached()) {
|
|
1267
|
+
this.sequencedStorageData.set(key, value);
|
|
1268
|
+
const event: IDirectoryValueChanged = {
|
|
1269
|
+
key,
|
|
1270
|
+
path: this.absolutePath,
|
|
1271
|
+
previousValue: previousOptimisticLocalValue,
|
|
1272
|
+
};
|
|
1273
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
1274
|
+
const containedEvent: IValueChanged = {
|
|
1275
|
+
key,
|
|
1276
|
+
previousValue: previousOptimisticLocalValue,
|
|
1277
|
+
};
|
|
1278
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
1283
1279
|
return this;
|
|
1284
1280
|
}
|
|
1285
1281
|
|
|
1286
|
-
|
|
1282
|
+
// A new pending key lifetime is created if:
|
|
1283
|
+
// 1. There isn't any pending entry for the key yet
|
|
1284
|
+
// 2. The most recent pending entry for the key was a deletion (as this terminates the prior lifetime)
|
|
1285
|
+
// 3. A clear was sent after the last pending entry for the key (which also terminates the prior lifetime)
|
|
1286
|
+
let latestPendingEntry = findLast(
|
|
1287
|
+
this.pendingStorageData,
|
|
1288
|
+
(entry) => entry.type === "clear" || entry.key === key,
|
|
1289
|
+
);
|
|
1290
|
+
if (
|
|
1291
|
+
latestPendingEntry === undefined ||
|
|
1292
|
+
latestPendingEntry.type === "delete" ||
|
|
1293
|
+
latestPendingEntry.type === "clear"
|
|
1294
|
+
) {
|
|
1295
|
+
latestPendingEntry = { type: "lifetime", path: this.absolutePath, key, keySets: [] };
|
|
1296
|
+
this.pendingStorageData.push(latestPendingEntry);
|
|
1297
|
+
}
|
|
1298
|
+
const pendingKeySet: PendingKeySet = {
|
|
1299
|
+
type: "set",
|
|
1300
|
+
path: this.absolutePath,
|
|
1301
|
+
value,
|
|
1302
|
+
};
|
|
1303
|
+
latestPendingEntry.keySets.push(pendingKeySet);
|
|
1304
|
+
|
|
1305
|
+
const op: IDirectoryOperation = {
|
|
1287
1306
|
key,
|
|
1288
1307
|
path: this.absolutePath,
|
|
1289
1308
|
type: "set",
|
|
1290
1309
|
value: { type: ValueType[ValueType.Plain], value },
|
|
1291
1310
|
};
|
|
1292
|
-
this.submitKeyMessage(op,
|
|
1311
|
+
this.submitKeyMessage(op, pendingKeySet);
|
|
1312
|
+
|
|
1313
|
+
const directoryValueChanged: IDirectoryValueChanged = {
|
|
1314
|
+
key,
|
|
1315
|
+
path: this.absolutePath,
|
|
1316
|
+
previousValue: previousOptimisticLocalValue,
|
|
1317
|
+
};
|
|
1318
|
+
this.directory.emit("valueChanged", directoryValueChanged, true, this.directory);
|
|
1319
|
+
const valueChanged: IValueChanged = {
|
|
1320
|
+
key,
|
|
1321
|
+
previousValue: previousOptimisticLocalValue,
|
|
1322
|
+
};
|
|
1323
|
+
this.emit("containedValueChanged", valueChanged, true, this);
|
|
1293
1324
|
return this;
|
|
1294
1325
|
}
|
|
1295
1326
|
|
|
@@ -1481,22 +1512,57 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1481
1512
|
*/
|
|
1482
1513
|
public delete(key: string): boolean {
|
|
1483
1514
|
this.throwIfDisposed();
|
|
1484
|
-
|
|
1485
|
-
const previousValue = this.deleteCore(key, true);
|
|
1515
|
+
const previousOptimisticLocalValue = this.getOptimisticValue(key);
|
|
1486
1516
|
|
|
1487
|
-
// If we are not attached, don't submit the op.
|
|
1488
1517
|
if (!this.directory.isAttached()) {
|
|
1489
|
-
|
|
1518
|
+
const successfullyRemoved = this.sequencedStorageData.delete(key);
|
|
1519
|
+
// Only emit if we actually deleted something.
|
|
1520
|
+
if (previousOptimisticLocalValue !== undefined && successfullyRemoved) {
|
|
1521
|
+
const event: IDirectoryValueChanged = {
|
|
1522
|
+
key,
|
|
1523
|
+
path: this.absolutePath,
|
|
1524
|
+
previousValue: previousOptimisticLocalValue,
|
|
1525
|
+
};
|
|
1526
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
1527
|
+
const containedEvent: IValueChanged = {
|
|
1528
|
+
key,
|
|
1529
|
+
previousValue: previousOptimisticLocalValue,
|
|
1530
|
+
};
|
|
1531
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
1532
|
+
}
|
|
1533
|
+
return successfullyRemoved;
|
|
1490
1534
|
}
|
|
1491
1535
|
|
|
1492
|
-
const
|
|
1493
|
-
key,
|
|
1494
|
-
path: this.absolutePath,
|
|
1536
|
+
const pendingKeyDelete: PendingKeyDelete = {
|
|
1495
1537
|
type: "delete",
|
|
1538
|
+
path: this.absolutePath,
|
|
1539
|
+
key,
|
|
1496
1540
|
};
|
|
1541
|
+
this.pendingStorageData.push(pendingKeyDelete);
|
|
1497
1542
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1543
|
+
const op: IDirectoryOperation = {
|
|
1544
|
+
key,
|
|
1545
|
+
type: "delete",
|
|
1546
|
+
path: this.absolutePath,
|
|
1547
|
+
};
|
|
1548
|
+
this.submitKeyMessage(op, pendingKeyDelete);
|
|
1549
|
+
// Only emit if we locally believe we deleted something. Otherwise we still send the op
|
|
1550
|
+
// (permitting speculative deletion even if we don't see anything locally) but don't emit
|
|
1551
|
+
// a valueChanged since we in fact did not locally observe a value change.
|
|
1552
|
+
if (previousOptimisticLocalValue !== undefined) {
|
|
1553
|
+
const event: IDirectoryValueChanged = {
|
|
1554
|
+
key,
|
|
1555
|
+
path: this.absolutePath,
|
|
1556
|
+
previousValue: previousOptimisticLocalValue,
|
|
1557
|
+
};
|
|
1558
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
1559
|
+
const containedEvent: IValueChanged = {
|
|
1560
|
+
key,
|
|
1561
|
+
previousValue: previousOptimisticLocalValue,
|
|
1562
|
+
};
|
|
1563
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
1564
|
+
}
|
|
1565
|
+
return true;
|
|
1500
1566
|
}
|
|
1501
1567
|
|
|
1502
1568
|
/**
|
|
@@ -1505,19 +1571,24 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1505
1571
|
public clear(): void {
|
|
1506
1572
|
this.throwIfDisposed();
|
|
1507
1573
|
|
|
1508
|
-
// If we are not attached, don't submit the op.
|
|
1509
1574
|
if (!this.directory.isAttached()) {
|
|
1510
|
-
this.
|
|
1575
|
+
this.sequencedStorageData.clear();
|
|
1576
|
+
this.directory.emit("clear", true, this.directory);
|
|
1511
1577
|
return;
|
|
1512
1578
|
}
|
|
1513
1579
|
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1516
|
-
const op: IDirectoryClearOperation = {
|
|
1580
|
+
const pendingClear: PendingClear = {
|
|
1581
|
+
type: "clear",
|
|
1517
1582
|
path: this.absolutePath,
|
|
1583
|
+
};
|
|
1584
|
+
this.pendingStorageData.push(pendingClear);
|
|
1585
|
+
|
|
1586
|
+
this.directory.emit("clear", true, this.directory);
|
|
1587
|
+
const op: IDirectoryOperation = {
|
|
1518
1588
|
type: "clear",
|
|
1589
|
+
path: this.absolutePath,
|
|
1519
1590
|
};
|
|
1520
|
-
this.submitClearMessage(op,
|
|
1591
|
+
this.submitClearMessage(op, pendingClear);
|
|
1521
1592
|
}
|
|
1522
1593
|
|
|
1523
1594
|
/**
|
|
@@ -1528,10 +1599,9 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1528
1599
|
callback: (value: unknown, key: string, map: Map<string, unknown>) => void,
|
|
1529
1600
|
): void {
|
|
1530
1601
|
this.throwIfDisposed();
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
});
|
|
1602
|
+
for (const [key, localValue] of this.internalIterator()) {
|
|
1603
|
+
callback((localValue as { value: unknown }).value, key, this);
|
|
1604
|
+
}
|
|
1535
1605
|
}
|
|
1536
1606
|
|
|
1537
1607
|
/**
|
|
@@ -1539,7 +1609,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1539
1609
|
*/
|
|
1540
1610
|
public get size(): number {
|
|
1541
1611
|
this.throwIfDisposed();
|
|
1542
|
-
return this.
|
|
1612
|
+
return [...this.internalIterator()].length;
|
|
1543
1613
|
}
|
|
1544
1614
|
|
|
1545
1615
|
/**
|
|
@@ -1548,7 +1618,24 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1548
1618
|
*/
|
|
1549
1619
|
public entries(): IterableIterator<[string, unknown]> {
|
|
1550
1620
|
this.throwIfDisposed();
|
|
1551
|
-
|
|
1621
|
+
const internalIterator = this.internalIterator();
|
|
1622
|
+
const next = (): IteratorResult<[string, unknown]> => {
|
|
1623
|
+
const nextResult = internalIterator.next();
|
|
1624
|
+
if (nextResult.done) {
|
|
1625
|
+
return { value: undefined, done: true };
|
|
1626
|
+
}
|
|
1627
|
+
// Unpack the stored value
|
|
1628
|
+
const [key, localValue] = nextResult.value;
|
|
1629
|
+
return { value: [key, localValue], done: false };
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
const iterator = {
|
|
1633
|
+
next,
|
|
1634
|
+
[Symbol.iterator](): IterableIterator<[string, unknown]> {
|
|
1635
|
+
return this;
|
|
1636
|
+
},
|
|
1637
|
+
};
|
|
1638
|
+
return iterator;
|
|
1552
1639
|
}
|
|
1553
1640
|
|
|
1554
1641
|
/**
|
|
@@ -1557,7 +1644,22 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1557
1644
|
*/
|
|
1558
1645
|
public keys(): IterableIterator<string> {
|
|
1559
1646
|
this.throwIfDisposed();
|
|
1560
|
-
|
|
1647
|
+
const internalIterator = this.internalIterator();
|
|
1648
|
+
const next = (): IteratorResult<string> => {
|
|
1649
|
+
const nextResult = internalIterator.next();
|
|
1650
|
+
if (nextResult.done) {
|
|
1651
|
+
return { value: undefined, done: true };
|
|
1652
|
+
}
|
|
1653
|
+
const [key] = nextResult.value;
|
|
1654
|
+
return { value: key, done: false };
|
|
1655
|
+
};
|
|
1656
|
+
const iterator = {
|
|
1657
|
+
next,
|
|
1658
|
+
[Symbol.iterator](): IterableIterator<string> {
|
|
1659
|
+
return this;
|
|
1660
|
+
},
|
|
1661
|
+
};
|
|
1662
|
+
return iterator;
|
|
1561
1663
|
}
|
|
1562
1664
|
|
|
1563
1665
|
/**
|
|
@@ -1566,7 +1668,22 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1566
1668
|
*/
|
|
1567
1669
|
public values(): IterableIterator<unknown> {
|
|
1568
1670
|
this.throwIfDisposed();
|
|
1569
|
-
|
|
1671
|
+
const internalIterator = this.internalIterator();
|
|
1672
|
+
const next = (): IteratorResult<unknown> => {
|
|
1673
|
+
const nextResult = internalIterator.next();
|
|
1674
|
+
if (nextResult.done) {
|
|
1675
|
+
return { value: undefined, done: true };
|
|
1676
|
+
}
|
|
1677
|
+
const [, localValue] = nextResult.value;
|
|
1678
|
+
return { value: localValue, done: false };
|
|
1679
|
+
};
|
|
1680
|
+
const iterator = {
|
|
1681
|
+
next,
|
|
1682
|
+
[Symbol.iterator](): IterableIterator<unknown> {
|
|
1683
|
+
return this;
|
|
1684
|
+
},
|
|
1685
|
+
};
|
|
1686
|
+
return iterator;
|
|
1570
1687
|
}
|
|
1571
1688
|
|
|
1572
1689
|
/**
|
|
@@ -1575,9 +1692,135 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1575
1692
|
*/
|
|
1576
1693
|
public [Symbol.iterator](): IterableIterator<[string, unknown]> {
|
|
1577
1694
|
this.throwIfDisposed();
|
|
1578
|
-
return this.
|
|
1695
|
+
return this.internalIterator();
|
|
1579
1696
|
}
|
|
1580
1697
|
|
|
1698
|
+
/**
|
|
1699
|
+
* The data this SubDirectory instance is storing, but only including sequenced values (no local pending
|
|
1700
|
+
* modifications are included).
|
|
1701
|
+
*/
|
|
1702
|
+
private readonly sequencedStorageData = new Map<string, unknown>();
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* A data structure containing all local pending storage modifications, which is used in combination
|
|
1706
|
+
* with the sequencedStorageData to compute optimistic values.
|
|
1707
|
+
*
|
|
1708
|
+
* Pending sets are aggregated into "lifetimes", which permit correct relative iteration order
|
|
1709
|
+
* even across remote operations and rollbacks.
|
|
1710
|
+
*/
|
|
1711
|
+
private readonly pendingStorageData: PendingStorageEntry[] = [];
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* An internal iterator that iterates over the entries in the directory.
|
|
1715
|
+
*/
|
|
1716
|
+
private readonly internalIterator = (): IterableIterator<[string, unknown]> => {
|
|
1717
|
+
// We perform iteration in two steps - first by iterating over members of the sequenced storage data that are not
|
|
1718
|
+
// optimistically deleted or cleared, and then over the pending data lifetimes that have not subsequently
|
|
1719
|
+
// been deleted or cleared. In total, this give an ordering of members based on when they were initially
|
|
1720
|
+
// added to the sub directory (even if they were later modified), similar to the native Map.
|
|
1721
|
+
const sequencedStorageDataIterator = this.sequencedStorageData.keys();
|
|
1722
|
+
const pendingStorageDataIterator = this.pendingStorageData.values();
|
|
1723
|
+
const next = (): IteratorResult<[string, unknown]> => {
|
|
1724
|
+
let nextSequencedKey = sequencedStorageDataIterator.next();
|
|
1725
|
+
while (!nextSequencedKey.done) {
|
|
1726
|
+
const key = nextSequencedKey.value;
|
|
1727
|
+
// If we have any pending deletes or clears, then we won't iterate to this key yet (if at all).
|
|
1728
|
+
// Either it is optimistically deleted and will not be part of the iteration, or it was
|
|
1729
|
+
// re-added later and we'll iterate to it when we get to the pending data.
|
|
1730
|
+
if (
|
|
1731
|
+
!this.pendingStorageData.some(
|
|
1732
|
+
(entry) =>
|
|
1733
|
+
entry.type === "clear" || (entry.type === "delete" && entry.key === key),
|
|
1734
|
+
)
|
|
1735
|
+
) {
|
|
1736
|
+
assert(this.has(key), 0xc03 /* key should exist in sequenced or pending data */);
|
|
1737
|
+
const optimisticValue = this.getOptimisticValue(key);
|
|
1738
|
+
return { value: [key, optimisticValue], done: false };
|
|
1739
|
+
}
|
|
1740
|
+
nextSequencedKey = sequencedStorageDataIterator.next();
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
let nextPending = pendingStorageDataIterator.next();
|
|
1744
|
+
while (!nextPending.done) {
|
|
1745
|
+
const nextPendingEntry = nextPending.value;
|
|
1746
|
+
// A lifetime entry may need to be iterated.
|
|
1747
|
+
if (nextPendingEntry.type === "lifetime") {
|
|
1748
|
+
const nextPendingEntryIndex = this.pendingStorageData.indexOf(nextPendingEntry);
|
|
1749
|
+
const mostRecentDeleteOrClearIndex = findLastIndex(
|
|
1750
|
+
this.pendingStorageData,
|
|
1751
|
+
(entry) =>
|
|
1752
|
+
entry.type === "clear" ||
|
|
1753
|
+
(entry.type === "delete" && entry.key === nextPendingEntry.key),
|
|
1754
|
+
);
|
|
1755
|
+
// Only iterate the pending entry now if it hasn't been deleted or cleared.
|
|
1756
|
+
if (nextPendingEntryIndex > mostRecentDeleteOrClearIndex) {
|
|
1757
|
+
const latestPendingValue =
|
|
1758
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1759
|
+
nextPendingEntry.keySets[nextPendingEntry.keySets.length - 1]!;
|
|
1760
|
+
// Skip iterating if we would have would have already iterated it as part of the sequenced data.
|
|
1761
|
+
// This is not a perfect check in the case the map has changed since the iterator was created
|
|
1762
|
+
// (e.g. if a remote client added the same key in the meantime).
|
|
1763
|
+
if (
|
|
1764
|
+
!this.sequencedStorageData.has(nextPendingEntry.key) ||
|
|
1765
|
+
mostRecentDeleteOrClearIndex !== -1
|
|
1766
|
+
) {
|
|
1767
|
+
return { value: [nextPendingEntry.key, latestPendingValue.value], done: false };
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
nextPending = pendingStorageDataIterator.next();
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
return { value: undefined, done: true };
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
const iterator = {
|
|
1778
|
+
next,
|
|
1779
|
+
[Symbol.iterator](): IterableIterator<[string, unknown]> {
|
|
1780
|
+
return this;
|
|
1781
|
+
},
|
|
1782
|
+
};
|
|
1783
|
+
return iterator;
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Compute the optimistic local value for a given key. This combines the sequenced data with
|
|
1788
|
+
* any pending changes that have not yet been sequenced.
|
|
1789
|
+
*/
|
|
1790
|
+
private readonly getOptimisticValue = (key: string): unknown => {
|
|
1791
|
+
const latestPendingEntry = findLast(
|
|
1792
|
+
this.pendingStorageData,
|
|
1793
|
+
(entry) => entry.type === "clear" || entry.key === key,
|
|
1794
|
+
);
|
|
1795
|
+
|
|
1796
|
+
if (latestPendingEntry === undefined) {
|
|
1797
|
+
return this.sequencedStorageData.get(key);
|
|
1798
|
+
} else if (latestPendingEntry.type === "lifetime") {
|
|
1799
|
+
const latestPendingSet =
|
|
1800
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1801
|
+
latestPendingEntry.keySets[latestPendingEntry.keySets.length - 1]!;
|
|
1802
|
+
return latestPendingSet.value;
|
|
1803
|
+
} else {
|
|
1804
|
+
// Delete or clear
|
|
1805
|
+
return undefined;
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Determine if the directory optimistically has the key.
|
|
1811
|
+
* This will return true even if the value is undefined.
|
|
1812
|
+
*/
|
|
1813
|
+
private readonly optimisticallyHas = (key: string): boolean => {
|
|
1814
|
+
const latestPendingEntry = findLast(
|
|
1815
|
+
this.pendingStorageData,
|
|
1816
|
+
(entry) => entry.type === "clear" || entry.key === key,
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
return latestPendingEntry === undefined
|
|
1820
|
+
? this.sequencedStorageData.has(key)
|
|
1821
|
+
: latestPendingEntry.type === "lifetime";
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1581
1824
|
/**
|
|
1582
1825
|
* Process a clear operation.
|
|
1583
1826
|
* @param msg - The message from the server to apply.
|
|
@@ -1590,25 +1833,52 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1590
1833
|
msg: ISequencedDocumentMessage,
|
|
1591
1834
|
op: IDirectoryClearOperation,
|
|
1592
1835
|
local: boolean,
|
|
1593
|
-
localOpMetadata:
|
|
1836
|
+
localOpMetadata: ClearLocalOpMetadata | undefined,
|
|
1594
1837
|
): void {
|
|
1595
1838
|
this.throwIfDisposed();
|
|
1596
1839
|
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1597
1840
|
return;
|
|
1598
1841
|
}
|
|
1842
|
+
|
|
1599
1843
|
if (local) {
|
|
1844
|
+
this.sequencedStorageData.clear();
|
|
1845
|
+
const pendingClear = this.pendingStorageData.shift();
|
|
1600
1846
|
assert(
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
assert(
|
|
1606
|
-
pendingClearMessageId === localOpMetadata.pendingMessageId,
|
|
1607
|
-
0x32a /* pendingMessageId does not match */,
|
|
1847
|
+
pendingClear !== undefined &&
|
|
1848
|
+
pendingClear.type === "clear" &&
|
|
1849
|
+
pendingClear === localOpMetadata,
|
|
1850
|
+
0xc04 /* Got a local clear message we weren't expecting */,
|
|
1608
1851
|
);
|
|
1609
|
-
|
|
1852
|
+
} else {
|
|
1853
|
+
// For pending set operations, collect the previous values before clearing sequenced data
|
|
1854
|
+
const pendingSets: { key: string; previousValue: unknown }[] = [];
|
|
1855
|
+
for (const entry of this.pendingStorageData) {
|
|
1856
|
+
if (entry.type === "lifetime") {
|
|
1857
|
+
const previousValue = this.sequencedStorageData.get(entry.key);
|
|
1858
|
+
pendingSets.push({ key: entry.key, previousValue });
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
this.sequencedStorageData.clear();
|
|
1862
|
+
|
|
1863
|
+
// Only emit for remote ops, we would have already emitted for local ops. Only emit if there
|
|
1864
|
+
// is no optimistically-applied local pending clear that would supersede this remote clear.
|
|
1865
|
+
if (!this.pendingStorageData.some((entry) => entry.type === "clear")) {
|
|
1866
|
+
this.directory.emit("clear", local, this.directory);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// For pending set operations, emit valueChanged events
|
|
1870
|
+
for (const { key, previousValue } of pendingSets) {
|
|
1871
|
+
this.directory.emit(
|
|
1872
|
+
"valueChanged",
|
|
1873
|
+
{
|
|
1874
|
+
key,
|
|
1875
|
+
previousValue,
|
|
1876
|
+
},
|
|
1877
|
+
local,
|
|
1878
|
+
this.directory,
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1610
1881
|
}
|
|
1611
|
-
this.clearExceptPendingKeys(false);
|
|
1612
1882
|
}
|
|
1613
1883
|
|
|
1614
1884
|
/**
|
|
@@ -1623,18 +1893,44 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1623
1893
|
msg: ISequencedDocumentMessage,
|
|
1624
1894
|
op: IDirectoryDeleteOperation,
|
|
1625
1895
|
local: boolean,
|
|
1626
|
-
localOpMetadata:
|
|
1896
|
+
localOpMetadata: EditLocalOpMetadata | undefined,
|
|
1627
1897
|
): void {
|
|
1628
1898
|
this.throwIfDisposed();
|
|
1629
|
-
if (
|
|
1630
|
-
!(
|
|
1631
|
-
this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1632
|
-
this.needProcessStorageOperation(op, local, localOpMetadata)
|
|
1633
|
-
)
|
|
1634
|
-
) {
|
|
1899
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1635
1900
|
return;
|
|
1636
1901
|
}
|
|
1637
|
-
|
|
1902
|
+
if (local) {
|
|
1903
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex(
|
|
1904
|
+
(entry) => entry.type !== "clear" && entry.key === op.key,
|
|
1905
|
+
);
|
|
1906
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
1907
|
+
assert(
|
|
1908
|
+
pendingEntry !== undefined &&
|
|
1909
|
+
pendingEntry.type === "delete" &&
|
|
1910
|
+
pendingEntry.key === op.key,
|
|
1911
|
+
0xc05 /* Got a local delete message we weren't expecting */,
|
|
1912
|
+
);
|
|
1913
|
+
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
1914
|
+
this.sequencedStorageData.delete(op.key);
|
|
1915
|
+
} else {
|
|
1916
|
+
const previousValue: unknown = this.sequencedStorageData.get(op.key);
|
|
1917
|
+
this.sequencedStorageData.delete(op.key);
|
|
1918
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
1919
|
+
if (
|
|
1920
|
+
!this.pendingStorageData.some(
|
|
1921
|
+
(entry) => entry.type === "clear" || entry.key === op.key,
|
|
1922
|
+
)
|
|
1923
|
+
) {
|
|
1924
|
+
const event: IDirectoryValueChanged = {
|
|
1925
|
+
key: op.key,
|
|
1926
|
+
path: this.absolutePath,
|
|
1927
|
+
previousValue,
|
|
1928
|
+
};
|
|
1929
|
+
this.directory.emit("valueChanged", event, local, this.directory);
|
|
1930
|
+
const containedEvent: IValueChanged = { key: op.key, previousValue };
|
|
1931
|
+
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1638
1934
|
}
|
|
1639
1935
|
|
|
1640
1936
|
/**
|
|
@@ -1650,21 +1946,49 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1650
1946
|
op: IDirectorySetOperation,
|
|
1651
1947
|
value: unknown,
|
|
1652
1948
|
local: boolean,
|
|
1653
|
-
localOpMetadata:
|
|
1949
|
+
localOpMetadata: EditLocalOpMetadata | undefined,
|
|
1654
1950
|
): void {
|
|
1655
1951
|
this.throwIfDisposed();
|
|
1656
|
-
if (
|
|
1657
|
-
!(
|
|
1658
|
-
this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1659
|
-
this.needProcessStorageOperation(op, local, localOpMetadata)
|
|
1660
|
-
)
|
|
1661
|
-
) {
|
|
1952
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1662
1953
|
return;
|
|
1663
1954
|
}
|
|
1664
1955
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1956
|
+
const { key } = op;
|
|
1957
|
+
|
|
1958
|
+
if (local) {
|
|
1959
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex(
|
|
1960
|
+
(entry) => entry.type !== "clear" && entry.key === key,
|
|
1961
|
+
);
|
|
1962
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
1963
|
+
assert(
|
|
1964
|
+
pendingEntry !== undefined && pendingEntry.type === "lifetime",
|
|
1965
|
+
0xc06 /* Couldn't match local set message to pending lifetime */,
|
|
1966
|
+
);
|
|
1967
|
+
const pendingKeySet = pendingEntry.keySets.shift();
|
|
1968
|
+
assert(
|
|
1969
|
+
pendingKeySet !== undefined && pendingKeySet === localOpMetadata,
|
|
1970
|
+
0xc07 /* Got a local set message we weren't expecting */,
|
|
1971
|
+
);
|
|
1972
|
+
if (pendingEntry.keySets.length === 0) {
|
|
1973
|
+
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
this.sequencedStorageData.set(key, pendingKeySet.value);
|
|
1977
|
+
} else {
|
|
1978
|
+
// Get the previous value before setting the new value
|
|
1979
|
+
const previousValue: unknown = this.sequencedStorageData.get(key);
|
|
1980
|
+
this.sequencedStorageData.set(key, value);
|
|
1981
|
+
|
|
1982
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
1983
|
+
if (
|
|
1984
|
+
!this.pendingStorageData.some((entry) => entry.type === "clear" || entry.key === key)
|
|
1985
|
+
) {
|
|
1986
|
+
const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
|
|
1987
|
+
this.directory.emit("valueChanged", event, local, this.directory);
|
|
1988
|
+
const containedEvent: IValueChanged = { key, previousValue };
|
|
1989
|
+
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1668
1992
|
}
|
|
1669
1993
|
|
|
1670
1994
|
/**
|
|
@@ -1679,7 +2003,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1679
2003
|
msg: ISequencedDocumentMessage,
|
|
1680
2004
|
op: IDirectoryCreateSubDirectoryOperation,
|
|
1681
2005
|
local: boolean,
|
|
1682
|
-
localOpMetadata:
|
|
2006
|
+
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
1683
2007
|
): void {
|
|
1684
2008
|
this.throwIfDisposed();
|
|
1685
2009
|
if (
|
|
@@ -1711,7 +2035,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1711
2035
|
msg: ISequencedDocumentMessage,
|
|
1712
2036
|
op: IDirectoryDeleteSubDirectoryOperation,
|
|
1713
2037
|
local: boolean,
|
|
1714
|
-
localOpMetadata:
|
|
2038
|
+
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
1715
2039
|
): void {
|
|
1716
2040
|
this.throwIfDisposed();
|
|
1717
2041
|
if (
|
|
@@ -1728,65 +2052,46 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1728
2052
|
/**
|
|
1729
2053
|
* Submit a clear operation.
|
|
1730
2054
|
* @param op - The operation
|
|
2055
|
+
* @param localOpMetadata - The pending operation metadata
|
|
1731
2056
|
*/
|
|
1732
2057
|
private submitClearMessage(
|
|
1733
2058
|
op: IDirectoryClearOperation,
|
|
1734
|
-
|
|
2059
|
+
localOpMetadata: ClearLocalOpMetadata,
|
|
1735
2060
|
): void {
|
|
1736
2061
|
this.throwIfDisposed();
|
|
1737
|
-
|
|
1738
|
-
this.pendingClearMessageIds.push(pendingMsgId);
|
|
1739
|
-
const metadata: IClearLocalOpMetadata = {
|
|
1740
|
-
type: "clear",
|
|
1741
|
-
pendingMessageId: pendingMsgId,
|
|
1742
|
-
previousStorage: previousValue,
|
|
1743
|
-
};
|
|
1744
|
-
this.directory.submitDirectoryMessage(op, metadata);
|
|
2062
|
+
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1745
2063
|
}
|
|
1746
2064
|
|
|
1747
2065
|
/**
|
|
1748
2066
|
* Resubmit a clear operation.
|
|
1749
2067
|
* @param op - The operation
|
|
1750
2068
|
*/
|
|
1751
|
-
public resubmitClearMessage(
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
);
|
|
1756
|
-
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
1757
|
-
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
2069
|
+
public resubmitClearMessage(
|
|
2070
|
+
op: IDirectoryClearOperation,
|
|
2071
|
+
localOpMetadata: ClearLocalOpMetadata,
|
|
2072
|
+
): void {
|
|
1758
2073
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1759
2074
|
// is already deleted, in which case we don't need to submit the op.
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
* Get a new pending message id for the op and cache it to track the pending op
|
|
1767
|
-
*/
|
|
1768
|
-
private getKeyMessageId(op: IDirectoryKeyOperation): number {
|
|
1769
|
-
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
1770
|
-
const pendingMessageId = ++this.pendingMessageId;
|
|
1771
|
-
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1772
|
-
if (pendingMessageIds === undefined) {
|
|
1773
|
-
this.pendingKeys.set(op.key, [pendingMessageId]);
|
|
1774
|
-
} else {
|
|
1775
|
-
pendingMessageIds.push(pendingMessageId);
|
|
2075
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex(
|
|
2076
|
+
(entry) => entry.type === "clear",
|
|
2077
|
+
);
|
|
2078
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
2079
|
+
if (pendingEntry !== undefined) {
|
|
2080
|
+
this.submitClearMessage(op, localOpMetadata);
|
|
1776
2081
|
}
|
|
1777
|
-
return pendingMessageId;
|
|
1778
2082
|
}
|
|
1779
2083
|
|
|
1780
2084
|
/**
|
|
1781
2085
|
* Submit a key operation.
|
|
1782
2086
|
* @param op - The operation
|
|
1783
|
-
* @param
|
|
2087
|
+
* @param localOpMetadata - The pending operation metadata
|
|
1784
2088
|
*/
|
|
1785
|
-
private submitKeyMessage(
|
|
2089
|
+
private submitKeyMessage(
|
|
2090
|
+
op: IDirectoryKeyOperation,
|
|
2091
|
+
localOpMetadata: PendingKeySet | PendingKeyDelete,
|
|
2092
|
+
): void {
|
|
1786
2093
|
this.throwIfDisposed();
|
|
1787
|
-
|
|
1788
|
-
const localMetadata = { type: "edit", pendingMessageId, previousValue };
|
|
1789
|
-
this.directory.submitDirectoryMessage(op, localMetadata);
|
|
2094
|
+
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1790
2095
|
}
|
|
1791
2096
|
|
|
1792
2097
|
/**
|
|
@@ -1794,26 +2099,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1794
2099
|
* @param op - The map key message
|
|
1795
2100
|
* @param localOpMetadata - Metadata from the previous submit
|
|
1796
2101
|
*/
|
|
1797
|
-
public resubmitKeyMessage(
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
);
|
|
1802
|
-
|
|
1803
|
-
// clear the old pending message id
|
|
1804
|
-
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
2102
|
+
public resubmitKeyMessage(
|
|
2103
|
+
op: IDirectoryKeyOperation,
|
|
2104
|
+
localOpMetadata: EditLocalOpMetadata,
|
|
2105
|
+
): void {
|
|
1805
2106
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1806
2107
|
// is already deleted, in which case we don't need to submit the op.
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
if (pendingMessageIds.length === 0) {
|
|
1814
|
-
this.pendingKeys.delete(op.key);
|
|
1815
|
-
}
|
|
1816
|
-
this.submitKeyMessage(op, localOpMetadata.previousValue);
|
|
2108
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex(
|
|
2109
|
+
(entry) => entry.type !== "clear" && entry.key === op.key,
|
|
2110
|
+
);
|
|
2111
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
2112
|
+
if (pendingEntry !== undefined) {
|
|
2113
|
+
this.submitKeyMessage(op, localOpMetadata as PendingKeySet | PendingKeyDelete);
|
|
1817
2114
|
}
|
|
1818
2115
|
}
|
|
1819
2116
|
|
|
@@ -1863,7 +2160,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1863
2160
|
*/
|
|
1864
2161
|
private submitDeleteSubDirectoryMessage(
|
|
1865
2162
|
op: IDirectorySubDirectoryOperation,
|
|
1866
|
-
subDir: SubDirectory
|
|
2163
|
+
subDir: SubDirectory,
|
|
1867
2164
|
): void {
|
|
1868
2165
|
this.throwIfDisposed();
|
|
1869
2166
|
this.updatePendingSubDirMessageCount(op);
|
|
@@ -1882,13 +2179,8 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1882
2179
|
*/
|
|
1883
2180
|
public resubmitSubDirectoryMessage(
|
|
1884
2181
|
op: IDirectorySubDirectoryOperation,
|
|
1885
|
-
localOpMetadata:
|
|
2182
|
+
localOpMetadata: SubDirLocalOpMetadata,
|
|
1886
2183
|
): void {
|
|
1887
|
-
assert(
|
|
1888
|
-
isSubDirLocalOpMetadata(localOpMetadata),
|
|
1889
|
-
0x32f /* Invalid localOpMetadata for sub directory op */,
|
|
1890
|
-
);
|
|
1891
|
-
|
|
1892
2184
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1893
2185
|
// is already deleted, in which case we don't need to submit the op.
|
|
1894
2186
|
if (
|
|
@@ -1906,8 +2198,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1906
2198
|
if (localOpMetadata.type === "createSubDir") {
|
|
1907
2199
|
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1908
2200
|
this.submitCreateSubDirectoryMessage(op);
|
|
1909
|
-
} else {
|
|
2201
|
+
} else if (localOpMetadata.type === "deleteSubDir") {
|
|
1910
2202
|
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
2203
|
+
assert(
|
|
2204
|
+
localOpMetadata.subDirectory !== undefined,
|
|
2205
|
+
0xc08 /* localOpMetadata.subDirectory should be defined */,
|
|
2206
|
+
);
|
|
1911
2207
|
this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
|
|
1912
2208
|
}
|
|
1913
2209
|
}
|
|
@@ -1921,7 +2217,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1921
2217
|
serializer: IFluidSerializer,
|
|
1922
2218
|
): Generator<[string, ISerializedValue], void> {
|
|
1923
2219
|
this.throwIfDisposed();
|
|
1924
|
-
for (const [key, value] of this.
|
|
2220
|
+
for (const [key, value] of this.sequencedStorageData.entries()) {
|
|
1925
2221
|
const serializedValue = serializeValue(value, serializer, this.directory.handle);
|
|
1926
2222
|
const res: [string, ISerializedValue] = [key, serializedValue];
|
|
1927
2223
|
yield res;
|
|
@@ -1944,7 +2240,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1944
2240
|
*/
|
|
1945
2241
|
public populateStorage(key: string, value: unknown): void {
|
|
1946
2242
|
this.throwIfDisposed();
|
|
1947
|
-
this.
|
|
2243
|
+
this.sequencedStorageData.set(key, value);
|
|
1948
2244
|
}
|
|
1949
2245
|
|
|
1950
2246
|
/**
|
|
@@ -1958,82 +2254,93 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1958
2254
|
this._subdirectories.set(subdirName, newSubDir);
|
|
1959
2255
|
}
|
|
1960
2256
|
|
|
1961
|
-
/**
|
|
1962
|
-
* Retrieve the local value at the given key. This is used to get value type information stashed on the local
|
|
1963
|
-
* value so op handlers can be retrieved
|
|
1964
|
-
* @param key - The key to retrieve from
|
|
1965
|
-
* @returns The local value
|
|
1966
|
-
*/
|
|
1967
|
-
public getLocalValue<T>(key: string): T {
|
|
1968
|
-
this.throwIfDisposed();
|
|
1969
|
-
return this._storage.get(key) as T;
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
/**
|
|
1973
|
-
* Remove the pendingMessageId from the map tracking it on rollback
|
|
1974
|
-
* @param map - map tracking the pending messages
|
|
1975
|
-
* @param key - key of the edit in the op
|
|
1976
|
-
*/
|
|
1977
|
-
private rollbackPendingMessageId(
|
|
1978
|
-
map: Map<string, number[]>,
|
|
1979
|
-
key: string,
|
|
1980
|
-
pendingMessageId,
|
|
1981
|
-
): void {
|
|
1982
|
-
const pendingMessageIds = map.get(key);
|
|
1983
|
-
const lastPendingMessageId = pendingMessageIds?.pop();
|
|
1984
|
-
if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
|
|
1985
|
-
throw new Error("Rollback op does not match last pending");
|
|
1986
|
-
}
|
|
1987
|
-
if (pendingMessageIds.length === 0) {
|
|
1988
|
-
map.delete(key);
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
1993
|
-
|
|
1994
2257
|
/**
|
|
1995
2258
|
* Rollback a local op
|
|
1996
2259
|
* @param op - The operation to rollback
|
|
1997
2260
|
* @param localOpMetadata - The local metadata associated with the op.
|
|
1998
2261
|
*/
|
|
1999
2262
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2000
|
-
public rollback(op: any, localOpMetadata:
|
|
2001
|
-
|
|
2002
|
-
throw new Error("Invalid localOpMetadata");
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
if (op.type === "clear" && localOpMetadata.type === "clear") {
|
|
2006
|
-
for (const [key, localValue] of localOpMetadata.previousStorage.entries()) {
|
|
2007
|
-
this.setCore(key, localValue, true);
|
|
2008
|
-
}
|
|
2263
|
+
public rollback(op: any, localOpMetadata: DirectoryLocalOpMetadata): void {
|
|
2264
|
+
const directoryOp = op as IDirectoryOperation;
|
|
2009
2265
|
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2266
|
+
if (directoryOp.type === "clear") {
|
|
2267
|
+
// A pending clear will be last in the list, since it terminates all prior lifetimes.
|
|
2268
|
+
const pendingClear = this.pendingStorageData.pop();
|
|
2269
|
+
assert(
|
|
2270
|
+
pendingClear !== undefined &&
|
|
2271
|
+
pendingClear.type === "clear" &&
|
|
2272
|
+
localOpMetadata.type === "clear",
|
|
2273
|
+
0xc09 /* Unexpected clear rollback */,
|
|
2274
|
+
);
|
|
2275
|
+
for (const [key] of this.internalIterator()) {
|
|
2276
|
+
const event: IDirectoryValueChanged = {
|
|
2277
|
+
key,
|
|
2278
|
+
path: this.absolutePath,
|
|
2279
|
+
previousValue: undefined,
|
|
2280
|
+
};
|
|
2281
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
2282
|
+
const containedEvent: IValueChanged = { key, previousValue: undefined };
|
|
2283
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
2016
2284
|
}
|
|
2017
2285
|
} else if (
|
|
2018
|
-
(
|
|
2019
|
-
localOpMetadata.type === "
|
|
2286
|
+
(directoryOp.type === "delete" || directoryOp.type === "set") &&
|
|
2287
|
+
(localOpMetadata.type === "set" || localOpMetadata.type === "delete")
|
|
2020
2288
|
) {
|
|
2021
|
-
|
|
2022
|
-
|
|
2289
|
+
// A pending set/delete may not be last in the list, as the lifetimes' order is based on when
|
|
2290
|
+
// they were created, not when they were last modified.
|
|
2291
|
+
const pendingEntryIndex = findLastIndex(
|
|
2292
|
+
this.pendingStorageData,
|
|
2293
|
+
(entry) => entry.type !== "clear" && entry.key === directoryOp.key,
|
|
2294
|
+
);
|
|
2295
|
+
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
2023
2296
|
assert(
|
|
2024
|
-
|
|
2025
|
-
|
|
2297
|
+
pendingEntry !== undefined &&
|
|
2298
|
+
(pendingEntry.type === "delete" || pendingEntry.type === "lifetime"),
|
|
2299
|
+
0xc0a /* Unexpected pending data for set/delete op */,
|
|
2026
2300
|
);
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
this.
|
|
2030
|
-
|
|
2031
|
-
this.
|
|
2301
|
+
if (pendingEntry.type === "delete") {
|
|
2302
|
+
assert(pendingEntry === localOpMetadata, 0xc0b /* Unexpected delete rollback */);
|
|
2303
|
+
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
2304
|
+
// Only emit if rolling back the delete actually results in a value becoming visible.
|
|
2305
|
+
if (this.getOptimisticValue(directoryOp.key) !== undefined) {
|
|
2306
|
+
const event: IDirectoryValueChanged = {
|
|
2307
|
+
key: directoryOp.key,
|
|
2308
|
+
path: this.absolutePath,
|
|
2309
|
+
previousValue: undefined,
|
|
2310
|
+
};
|
|
2311
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
2312
|
+
const containedEvent: IValueChanged = {
|
|
2313
|
+
key: directoryOp.key,
|
|
2314
|
+
previousValue: undefined,
|
|
2315
|
+
};
|
|
2316
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
2317
|
+
}
|
|
2318
|
+
} else if (pendingEntry.type === "lifetime") {
|
|
2319
|
+
const pendingKeySet = pendingEntry.keySets.pop();
|
|
2320
|
+
assert(
|
|
2321
|
+
pendingKeySet !== undefined && pendingKeySet === localOpMetadata,
|
|
2322
|
+
0xc0c /* Unexpected set rollback */,
|
|
2323
|
+
);
|
|
2324
|
+
if (pendingEntry.keySets.length === 0) {
|
|
2325
|
+
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
2326
|
+
}
|
|
2327
|
+
const event: IDirectoryValueChanged = {
|
|
2328
|
+
key: directoryOp.key,
|
|
2329
|
+
path: this.absolutePath,
|
|
2330
|
+
previousValue: pendingKeySet.value,
|
|
2331
|
+
};
|
|
2332
|
+
this.directory.emit("valueChanged", event, true, this.directory);
|
|
2333
|
+
const containedEvent: IValueChanged = {
|
|
2334
|
+
key: directoryOp.key,
|
|
2335
|
+
previousValue: pendingKeySet.value,
|
|
2336
|
+
};
|
|
2337
|
+
this.emit("containedValueChanged", containedEvent, true, this);
|
|
2032
2338
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2339
|
+
} else if (
|
|
2340
|
+
directoryOp.type === "createSubDirectory" &&
|
|
2341
|
+
localOpMetadata.type === "createSubDir"
|
|
2342
|
+
) {
|
|
2343
|
+
const subdirName: unknown = directoryOp.subdirName;
|
|
2037
2344
|
assert(
|
|
2038
2345
|
subdirName !== undefined,
|
|
2039
2346
|
0x8af /* "subdirName" property is missing from "createSubDirectory" operation. */,
|
|
@@ -2045,8 +2352,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2045
2352
|
|
|
2046
2353
|
this.deleteSubDirectoryCore(subdirName, true);
|
|
2047
2354
|
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, subdirName);
|
|
2048
|
-
} else if (
|
|
2049
|
-
|
|
2355
|
+
} else if (
|
|
2356
|
+
directoryOp.type === "deleteSubDirectory" &&
|
|
2357
|
+
localOpMetadata.type === "deleteSubDir"
|
|
2358
|
+
) {
|
|
2359
|
+
const subdirName: unknown = directoryOp.subdirName;
|
|
2050
2360
|
assert(
|
|
2051
2361
|
subdirName !== undefined,
|
|
2052
2362
|
0x8b1 /* "subdirName" property is missing from "deleteSubDirectory" operation. */,
|
|
@@ -2079,8 +2389,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2079
2389
|
}
|
|
2080
2390
|
}
|
|
2081
2391
|
|
|
2082
|
-
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
|
2083
|
-
|
|
2084
2392
|
/**
|
|
2085
2393
|
* Converts the given relative path into an absolute path.
|
|
2086
2394
|
* @param path - Relative path to convert
|
|
@@ -2090,86 +2398,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2090
2398
|
return posix.resolve(this.absolutePath, relativePath);
|
|
2091
2399
|
}
|
|
2092
2400
|
|
|
2093
|
-
/**
|
|
2094
|
-
* If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
|
|
2095
|
-
* not process the incoming operation.
|
|
2096
|
-
* @param op - Operation to check
|
|
2097
|
-
* @param local - Whether the operation originated from the local client
|
|
2098
|
-
* @param localOpMetadata - For local client ops, this is the metadata that was submitted with the op.
|
|
2099
|
-
* For ops from a remote client, this will be undefined.
|
|
2100
|
-
* @returns True if the operation should be processed, false otherwise
|
|
2101
|
-
*/
|
|
2102
|
-
private needProcessStorageOperation(
|
|
2103
|
-
op: IDirectoryKeyOperation,
|
|
2104
|
-
local: boolean,
|
|
2105
|
-
localOpMetadata: unknown,
|
|
2106
|
-
): boolean {
|
|
2107
|
-
const firstPendingClearMessageId = this.pendingClearMessageIds[0];
|
|
2108
|
-
if (firstPendingClearMessageId !== undefined) {
|
|
2109
|
-
if (local) {
|
|
2110
|
-
assert(
|
|
2111
|
-
localOpMetadata !== undefined &&
|
|
2112
|
-
isKeyEditLocalOpMetadata(localOpMetadata) &&
|
|
2113
|
-
localOpMetadata.pendingMessageId < firstPendingClearMessageId,
|
|
2114
|
-
0x010 /* "Received out of order storage op when there is an unackd clear message" */,
|
|
2115
|
-
);
|
|
2116
|
-
// Remove all pendingMessageIds lower than first pendingClearMessageId.
|
|
2117
|
-
const lowestPendingClearMessageId = firstPendingClearMessageId;
|
|
2118
|
-
const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
|
|
2119
|
-
if (pendingKeyMessageIdArray !== undefined) {
|
|
2120
|
-
let index = 0;
|
|
2121
|
-
let pendingKeyMessageId = pendingKeyMessageIdArray[index];
|
|
2122
|
-
while (
|
|
2123
|
-
pendingKeyMessageId !== undefined &&
|
|
2124
|
-
pendingKeyMessageId < lowestPendingClearMessageId
|
|
2125
|
-
) {
|
|
2126
|
-
index += 1;
|
|
2127
|
-
pendingKeyMessageId = pendingKeyMessageIdArray[index];
|
|
2128
|
-
}
|
|
2129
|
-
const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
|
|
2130
|
-
if (newPendingKeyMessageId.length === 0) {
|
|
2131
|
-
this.pendingKeys.delete(op.key);
|
|
2132
|
-
} else {
|
|
2133
|
-
this.pendingKeys.set(op.key, newPendingKeyMessageId);
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
// If I have a NACK clear, we can ignore all ops.
|
|
2139
|
-
return false;
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
const pendingKeyMessageIds = this.pendingKeys.get(op.key);
|
|
2143
|
-
if (pendingKeyMessageIds !== undefined) {
|
|
2144
|
-
// Found an NACK op, clear it from the directory if the latest sequence number in the directory
|
|
2145
|
-
// match the message's and don't process the op.
|
|
2146
|
-
if (local) {
|
|
2147
|
-
assert(
|
|
2148
|
-
localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata),
|
|
2149
|
-
0x011 /* pendingMessageId is missing from the local client's operation */,
|
|
2150
|
-
);
|
|
2151
|
-
if (pendingKeyMessageIds[0] !== localOpMetadata.pendingMessageId) {
|
|
2152
|
-
// TODO: AB#7742: Hitting this block indicates that the pending message Id received
|
|
2153
|
-
// is not consistent with the "next" local op
|
|
2154
|
-
this.logger.sendTelemetryEvent({
|
|
2155
|
-
eventName: "unexpectedPendingMessage",
|
|
2156
|
-
expectedPendingMessage: pendingKeyMessageIds[0],
|
|
2157
|
-
actualPendingMessage: localOpMetadata.pendingMessageId,
|
|
2158
|
-
expectedPendingMessagesLength: pendingKeyMessageIds.length,
|
|
2159
|
-
});
|
|
2160
|
-
}
|
|
2161
|
-
pendingKeyMessageIds.shift();
|
|
2162
|
-
if (pendingKeyMessageIds.length === 0) {
|
|
2163
|
-
this.pendingKeys.delete(op.key);
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
return false;
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
// If we don't have a NACK op on the key, we need to process the remote ops.
|
|
2170
|
-
return !local;
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
2401
|
/**
|
|
2174
2402
|
* This return true if the message is for the current instance of this sub directory. As the sub directory
|
|
2175
2403
|
* can be deleted and created again, then this finds if the message is for current instance of directory or not.
|
|
@@ -2200,7 +2428,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2200
2428
|
msg: ISequencedDocumentMessage,
|
|
2201
2429
|
op: IDirectorySubDirectoryOperation,
|
|
2202
2430
|
local: boolean,
|
|
2203
|
-
localOpMetadata:
|
|
2431
|
+
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
2204
2432
|
): boolean {
|
|
2205
2433
|
assertNonNullClientId(msg.clientId);
|
|
2206
2434
|
const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
|
|
@@ -2210,10 +2438,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2210
2438
|
(pendingCreateCount !== undefined && pendingCreateCount > 0)
|
|
2211
2439
|
) {
|
|
2212
2440
|
if (local) {
|
|
2213
|
-
assert(
|
|
2214
|
-
isSubDirLocalOpMetadata(localOpMetadata),
|
|
2215
|
-
0x012 /* pendingMessageId is missing from the local client's operation */,
|
|
2216
|
-
);
|
|
2441
|
+
assert(localOpMetadata !== undefined, 0xc0d /* localOpMetadata should be defined */);
|
|
2217
2442
|
if (localOpMetadata.type === "deleteSubDir") {
|
|
2218
2443
|
assert(
|
|
2219
2444
|
pendingDeleteCount !== undefined && pendingDeleteCount > 0,
|
|
@@ -2241,7 +2466,9 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2241
2466
|
}
|
|
2242
2467
|
// If this is delete op and we have keys in this subDirectory, then we need to delete these
|
|
2243
2468
|
// keys except the pending ones as they will be sequenced after this delete.
|
|
2244
|
-
directory.
|
|
2469
|
+
directory.sequencedStorageData.clear();
|
|
2470
|
+
directory.emit("clear", true, directory);
|
|
2471
|
+
|
|
2245
2472
|
// In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
|
|
2246
2473
|
// creators as the previous directory is getting deleted and we will initialize again when
|
|
2247
2474
|
// we will receive op for the create again.
|
|
@@ -2304,75 +2531,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2304
2531
|
return !local;
|
|
2305
2532
|
}
|
|
2306
2533
|
|
|
2307
|
-
/**
|
|
2308
|
-
* Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
|
|
2309
|
-
*/
|
|
2310
|
-
private clearExceptPendingKeys(local: boolean): void {
|
|
2311
|
-
// Assuming the pendingKeys is small and the map is large
|
|
2312
|
-
// we will get the value for the pendingKeys and clear the map
|
|
2313
|
-
const temp = new Map<string, unknown>();
|
|
2314
|
-
|
|
2315
|
-
for (const [key] of this.pendingKeys) {
|
|
2316
|
-
const value = this._storage.get(key);
|
|
2317
|
-
// If this key is already deleted, then we don't need to add it again.
|
|
2318
|
-
if (value !== undefined) {
|
|
2319
|
-
temp.set(key, value);
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
this.clearCore(local);
|
|
2324
|
-
|
|
2325
|
-
for (const [key, value] of temp.entries()) {
|
|
2326
|
-
this.setCore(key, value, true);
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
/**
|
|
2331
|
-
* Clear implementation used for both locally sourced clears as well as incoming remote clears.
|
|
2332
|
-
* @param local - Whether the message originated from the local client
|
|
2333
|
-
*/
|
|
2334
|
-
private clearCore(local: boolean): void {
|
|
2335
|
-
this._storage.clear();
|
|
2336
|
-
this.directory.emit("clear", local, this.directory);
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
/**
|
|
2340
|
-
* Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
|
|
2341
|
-
* @param key - The key being deleted
|
|
2342
|
-
* @param local - Whether the message originated from the local client
|
|
2343
|
-
* @returns Previous local value of the key if it existed, undefined if it did not exist
|
|
2344
|
-
*/
|
|
2345
|
-
private deleteCore(key: string, local: boolean): unknown {
|
|
2346
|
-
const previousLocalValue = this._storage.get(key);
|
|
2347
|
-
const previousValue: unknown = previousLocalValue;
|
|
2348
|
-
const successfullyRemoved = this._storage.delete(key);
|
|
2349
|
-
if (successfullyRemoved) {
|
|
2350
|
-
const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
|
|
2351
|
-
this.directory.emit("valueChanged", event, local, this.directory);
|
|
2352
|
-
const containedEvent: IValueChanged = { key, previousValue };
|
|
2353
|
-
this.emit("containedValueChanged", containedEvent, local, this);
|
|
2354
|
-
}
|
|
2355
|
-
return previousLocalValue;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
/**
|
|
2359
|
-
* Set implementation used for both locally sourced sets as well as incoming remote sets.
|
|
2360
|
-
* @param key - The key being set
|
|
2361
|
-
* @param value - The value being set
|
|
2362
|
-
* @param local - Whether the message originated from the local client
|
|
2363
|
-
* @returns Previous local value of the key, if any
|
|
2364
|
-
*/
|
|
2365
|
-
private setCore(key: string, value: unknown, local: boolean): unknown {
|
|
2366
|
-
const previousLocalValue = this._storage.get(key);
|
|
2367
|
-
const previousValue: unknown = previousLocalValue;
|
|
2368
|
-
this._storage.set(key, value);
|
|
2369
|
-
const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
|
|
2370
|
-
this.directory.emit("valueChanged", event, local, this.directory);
|
|
2371
|
-
const containedEvent: IValueChanged = { key, previousValue };
|
|
2372
|
-
this.emit("containedValueChanged", containedEvent, local, this);
|
|
2373
|
-
return previousLocalValue;
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
2534
|
/**
|
|
2377
2535
|
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
2378
2536
|
* @param subdirName - The name of the subdirectory being created
|