@fluidframework/map 1.4.0-121020 → 2.0.0-dev-rc.1.0.0.225277
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/.eslintrc.js +12 -11
- package/.mocharc.js +12 -0
- package/CHANGELOG.md +162 -0
- package/README.md +24 -8
- package/api-extractor-esm.json +4 -0
- package/api-extractor-lint.json +4 -0
- package/api-extractor.json +2 -2
- package/api-report/map.api.md +297 -0
- package/dist/{directory.js → directory.cjs} +749 -228
- package/dist/directory.cjs.map +1 -0
- package/dist/directory.d.ts +567 -34
- package/dist/directory.d.ts.map +1 -1
- package/dist/index.cjs +27 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/{interfaces.js → interfaces.cjs} +1 -1
- package/dist/interfaces.cjs.map +1 -0
- package/dist/interfaces.d.ts +167 -184
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/internalInterfaces.cjs +7 -0
- package/dist/internalInterfaces.cjs.map +1 -0
- package/dist/internalInterfaces.d.ts +101 -0
- package/dist/internalInterfaces.d.ts.map +1 -0
- package/dist/{localValues.js → localValues.cjs} +15 -3
- package/dist/localValues.cjs.map +1 -0
- package/dist/localValues.d.ts +17 -6
- package/dist/localValues.d.ts.map +1 -1
- package/dist/map-alpha.d.ts +982 -0
- package/dist/map-beta.d.ts +275 -0
- package/dist/map-public.d.ts +275 -0
- package/dist/map-untrimmed.d.ts +996 -0
- package/dist/{map.js → map.cjs} +39 -34
- package/dist/map.cjs.map +1 -0
- package/dist/map.d.ts +10 -17
- package/dist/map.d.ts.map +1 -1
- package/dist/{mapKernel.js → mapKernel.cjs} +122 -79
- package/dist/mapKernel.cjs.map +1 -0
- package/dist/mapKernel.d.ts +17 -48
- package/dist/mapKernel.d.ts.map +1 -1
- package/dist/{packageVersion.js → packageVersion.cjs} +2 -2
- package/dist/packageVersion.cjs.map +1 -0
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/tsdoc-metadata.json +11 -0
- package/lib/directory.d.mts +902 -0
- package/lib/directory.d.mts.map +1 -0
- package/lib/{directory.js → directory.mjs} +736 -199
- package/lib/directory.mjs.map +1 -0
- package/lib/index.d.mts +9 -0
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +8 -0
- package/lib/index.mjs.map +1 -0
- package/lib/{interfaces.d.ts → interfaces.d.mts} +168 -185
- package/lib/interfaces.d.mts.map +1 -0
- package/lib/{interfaces.js → interfaces.mjs} +1 -1
- package/lib/interfaces.mjs.map +1 -0
- package/lib/internalInterfaces.d.mts +101 -0
- package/lib/internalInterfaces.d.mts.map +1 -0
- package/lib/internalInterfaces.mjs +6 -0
- package/lib/internalInterfaces.mjs.map +1 -0
- package/lib/{localValues.d.ts → localValues.d.mts} +19 -8
- package/lib/localValues.d.mts.map +1 -0
- package/lib/{localValues.js → localValues.mjs} +15 -3
- package/lib/localValues.mjs.map +1 -0
- package/lib/map-alpha.d.mts +970 -0
- package/lib/map-beta.d.mts +263 -0
- package/lib/map-public.d.mts +263 -0
- package/lib/map-untrimmed.d.mts +984 -0
- package/lib/{map.d.ts → map.d.mts} +12 -19
- package/lib/map.d.mts.map +1 -0
- package/lib/{map.js → map.mjs} +40 -35
- package/lib/map.mjs.map +1 -0
- package/lib/{mapKernel.d.ts → mapKernel.d.mts} +19 -50
- package/lib/mapKernel.d.mts.map +1 -0
- package/lib/{mapKernel.js → mapKernel.mjs} +116 -73
- package/lib/mapKernel.mjs.map +1 -0
- package/lib/{packageVersion.d.ts → packageVersion.d.mts} +2 -2
- package/lib/packageVersion.d.mts.map +1 -0
- package/lib/{packageVersion.js → packageVersion.mjs} +2 -2
- package/lib/packageVersion.mjs.map +1 -0
- package/package.json +143 -65
- package/prettier.config.cjs +8 -0
- package/src/directory.ts +2544 -1727
- package/src/index.ts +31 -5
- package/src/interfaces.ts +346 -345
- package/src/internalInterfaces.ts +119 -0
- package/src/localValues.ts +103 -96
- package/src/map.ts +362 -351
- package/src/mapKernel.ts +755 -722
- package/src/packageVersion.ts +1 -1
- package/tsc-multi.test.json +4 -0
- package/tsconfig.json +10 -15
- package/dist/directory.js.map +0 -1
- package/dist/index.js +0 -34
- package/dist/index.js.map +0 -1
- package/dist/interfaces.js.map +0 -1
- package/dist/localValues.js.map +0 -1
- package/dist/map.js.map +0 -1
- package/dist/mapKernel.js.map +0 -1
- package/dist/packageVersion.js.map +0 -1
- package/lib/directory.d.ts +0 -369
- package/lib/directory.d.ts.map +0 -1
- package/lib/directory.js.map +0 -1
- package/lib/index.d.ts +0 -20
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -20
- package/lib/index.js.map +0 -1
- package/lib/interfaces.d.ts.map +0 -1
- package/lib/interfaces.js.map +0 -1
- package/lib/localValues.d.ts.map +0 -1
- package/lib/localValues.js.map +0 -1
- package/lib/map.d.ts.map +0 -1
- package/lib/map.js.map +0 -1
- package/lib/mapKernel.d.ts.map +0 -1
- package/lib/mapKernel.js.map +0 -1
- package/lib/packageVersion.d.ts.map +0 -1
- package/lib/packageVersion.js.map +0 -1
- package/tsconfig.esnext.json +0 -7
|
@@ -3,44 +3,32 @@
|
|
|
3
3
|
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
4
4
|
* Licensed under the MIT License.
|
|
5
5
|
*/
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
8
|
};
|
|
25
9
|
var _a, _b;
|
|
26
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
11
|
exports.SharedDirectory = exports.DirectoryFactory = void 0;
|
|
28
|
-
const
|
|
29
|
-
const
|
|
12
|
+
const core_utils_1 = require("@fluidframework/core-utils");
|
|
13
|
+
const client_utils_1 = require("@fluid-internal/client-utils");
|
|
14
|
+
const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
|
|
30
15
|
const driver_utils_1 = require("@fluidframework/driver-utils");
|
|
31
16
|
const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
|
|
32
17
|
const shared_object_base_1 = require("@fluidframework/shared-object-base");
|
|
33
18
|
const runtime_utils_1 = require("@fluidframework/runtime-utils");
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
19
|
+
const path_browserify_1 = __importDefault(require("path-browserify"));
|
|
20
|
+
const merge_tree_1 = require("@fluidframework/merge-tree");
|
|
21
|
+
const localValues_1 = require("./localValues.cjs");
|
|
22
|
+
const packageVersion_1 = require("./packageVersion.cjs");
|
|
37
23
|
// We use path-browserify since this code can run safely on the server or the browser.
|
|
38
24
|
// We standardize on using posix slashes everywhere.
|
|
39
|
-
const posix =
|
|
25
|
+
const posix = path_browserify_1.default.posix;
|
|
40
26
|
const snapshotFileName = "header";
|
|
41
27
|
/**
|
|
42
|
-
*
|
|
28
|
+
* {@link @fluidframework/datastore-definitions#IChannelFactory} for {@link SharedDirectory}.
|
|
29
|
+
*
|
|
43
30
|
* @sealed
|
|
31
|
+
* @alpha
|
|
44
32
|
*/
|
|
45
33
|
class DirectoryFactory {
|
|
46
34
|
/**
|
|
@@ -86,11 +74,104 @@ DirectoryFactory.Attributes = {
|
|
|
86
74
|
packageVersion: packageVersion_1.pkgVersion,
|
|
87
75
|
};
|
|
88
76
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
77
|
+
* The comparator essentially performs the following procedure to determine the order of subdirectory creation:
|
|
78
|
+
* 1. If subdirectory A has a non-negative 'seq' and subdirectory B has a negative 'seq', subdirectory A is always placed first due to
|
|
79
|
+
* the policy that acknowledged subdirectories precede locally created ones that have not been committed yet.
|
|
80
|
+
*
|
|
81
|
+
* 2. When both subdirectories A and B have a non-negative 'seq', they are compared as follows:
|
|
82
|
+
* - If A and B have different 'seq', they are ordered based on 'seq', and the one with the lower 'seq' will be positioned ahead. Notably this rule
|
|
83
|
+
* should not be applied in the directory ordering, since the lowest 'seq' is -1, when the directory is created locally but not acknowledged yet.
|
|
84
|
+
* - In the case where A and B have equal 'seq', the one with the lower 'clientSeq' will be positioned ahead. This scenario occurs when grouped
|
|
85
|
+
* batching is enabled, and a lower 'clientSeq' indicates that it was processed earlier after the batch was ungrouped.
|
|
86
|
+
*
|
|
87
|
+
* 3. When both subdirectories A and B have a negative 'seq', they are compared as follows:
|
|
88
|
+
* - If A and B have different 'seq', the one with lower 'seq' will be positioned ahead, which indicates the corresponding creation message was
|
|
89
|
+
* acknowledged by the server earlier.
|
|
90
|
+
* - If A and B have equal 'seq', the one with lower 'clientSeq' will be placed at the front. This scenario suggests that both subdirectories A
|
|
91
|
+
* and B were created locally and not acknowledged yet, with the one possessing the lower 'clientSeq' being created earlier.
|
|
92
|
+
*
|
|
93
|
+
* 4. A 'seq' value of zero indicates that the subdirectory was created in detached state, and it is considered acknowledged for the
|
|
94
|
+
* purpose of ordering.
|
|
95
|
+
*/
|
|
96
|
+
const seqDataComparator = (a, b) => {
|
|
97
|
+
if (isAcknowledgedOrDetached(a)) {
|
|
98
|
+
if (isAcknowledgedOrDetached(b)) {
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
100
|
+
return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq - b.clientSeq;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
return -1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (!isAcknowledgedOrDetached(b)) {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
109
|
+
return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq - b.clientSeq;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
function isAcknowledgedOrDetached(seqData) {
|
|
117
|
+
return seqData.seq >= 0;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A utility class for tracking associations between keys and their creation indices.
|
|
121
|
+
* This is relevant to support map iteration in insertion order, see
|
|
122
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
|
|
123
|
+
*
|
|
124
|
+
* TODO: It can be combined with the creation tracker utilized in SharedMap
|
|
125
|
+
*/
|
|
126
|
+
class DirectoryCreationTracker {
|
|
127
|
+
constructor() {
|
|
128
|
+
this.indexToKey = new merge_tree_1.RedBlackTree(seqDataComparator);
|
|
129
|
+
this.keyToIndex = new Map();
|
|
130
|
+
}
|
|
131
|
+
set(key, seqData) {
|
|
132
|
+
this.indexToKey.put(seqData, key);
|
|
133
|
+
this.keyToIndex.set(key, seqData);
|
|
134
|
+
}
|
|
135
|
+
has(keyOrSeqData) {
|
|
136
|
+
return typeof keyOrSeqData === "string"
|
|
137
|
+
? this.keyToIndex.has(keyOrSeqData)
|
|
138
|
+
: this.indexToKey.get(keyOrSeqData) !== undefined;
|
|
139
|
+
}
|
|
140
|
+
delete(keyOrSeqData) {
|
|
141
|
+
if (this.has(keyOrSeqData)) {
|
|
142
|
+
if (typeof keyOrSeqData === "string") {
|
|
143
|
+
const seqData = this.keyToIndex.get(keyOrSeqData);
|
|
144
|
+
this.keyToIndex.delete(keyOrSeqData);
|
|
145
|
+
this.indexToKey.remove(seqData);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const key = this.indexToKey.get(keyOrSeqData)?.data;
|
|
149
|
+
this.indexToKey.remove(keyOrSeqData);
|
|
150
|
+
this.keyToIndex.delete(key);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Retrieves all subdirectories with creation order that satisfy an optional constraint function.
|
|
156
|
+
* @param constraint - An optional constraint function that filters keys.
|
|
157
|
+
* @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
|
|
158
|
+
*/
|
|
159
|
+
keys(constraint) {
|
|
160
|
+
const keys = [];
|
|
161
|
+
this.indexToKey.mapRange((node) => {
|
|
162
|
+
if (!constraint || constraint(node.data)) {
|
|
163
|
+
keys.push(node.data);
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}, keys);
|
|
167
|
+
return keys;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* {@inheritDoc ISharedDirectory}
|
|
92
172
|
*
|
|
93
173
|
* @example
|
|
174
|
+
*
|
|
94
175
|
* ```typescript
|
|
95
176
|
* mySharedDirectory.createSubDirectory("a").createSubDirectory("b").createSubDirectory("c").set("foo", val1);
|
|
96
177
|
* const mySubDir = mySharedDirectory.getWorkingDirectory("/a/b/c");
|
|
@@ -98,8 +179,33 @@ DirectoryFactory.Attributes = {
|
|
|
98
179
|
* ```
|
|
99
180
|
*
|
|
100
181
|
* @sealed
|
|
182
|
+
* @alpha
|
|
101
183
|
*/
|
|
102
184
|
class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
185
|
+
/**
|
|
186
|
+
* Create a new shared directory
|
|
187
|
+
*
|
|
188
|
+
* @param runtime - Data store runtime the new shared directory belongs to
|
|
189
|
+
* @param id - Optional name of the shared directory
|
|
190
|
+
* @returns Newly create shared directory (but not attached yet)
|
|
191
|
+
*/
|
|
192
|
+
static create(runtime, id) {
|
|
193
|
+
return runtime.createChannel(id, DirectoryFactory.Type);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get a factory for SharedDirectory to register with the data store.
|
|
197
|
+
*
|
|
198
|
+
* @returns A factory that creates and load SharedDirectory
|
|
199
|
+
*/
|
|
200
|
+
static getFactory() {
|
|
201
|
+
return new DirectoryFactory();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* {@inheritDoc IDirectory.absolutePath}
|
|
205
|
+
*/
|
|
206
|
+
get absolutePath() {
|
|
207
|
+
return this.root.absolutePath;
|
|
208
|
+
}
|
|
103
209
|
/**
|
|
104
210
|
* Constructs a new shared directory. If the object is non-local an id and service interfaces will
|
|
105
211
|
* be provided.
|
|
@@ -116,7 +222,7 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
116
222
|
/**
|
|
117
223
|
* Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
|
|
118
224
|
*/
|
|
119
|
-
this.root = new SubDirectory(this, this.runtime, this.serializer, posix.sep);
|
|
225
|
+
this.root = new SubDirectory({ seq: 0, clientSeq: 0 }, new Set(), this, this.runtime, this.serializer, posix.sep);
|
|
120
226
|
/**
|
|
121
227
|
* Mapping of op types to message handlers.
|
|
122
228
|
*/
|
|
@@ -134,33 +240,11 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
134
240
|
this.emit("subDirectoryDeleted", relativePath, local, this);
|
|
135
241
|
});
|
|
136
242
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Create a new shared directory
|
|
139
|
-
*
|
|
140
|
-
* @param runtime - Data store runtime the new shared directory belongs to
|
|
141
|
-
* @param id - Optional name of the shared directory
|
|
142
|
-
* @returns Newly create shared directory (but not attached yet)
|
|
143
|
-
*/
|
|
144
|
-
static create(runtime, id) {
|
|
145
|
-
return runtime.createChannel(id, DirectoryFactory.Type);
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Get a factory for SharedDirectory to register with the data store.
|
|
149
|
-
*
|
|
150
|
-
* @returns A factory that creates and load SharedDirectory
|
|
151
|
-
*/
|
|
152
|
-
static getFactory() {
|
|
153
|
-
return new DirectoryFactory();
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* {@inheritDoc IDirectory.absolutePath}
|
|
157
|
-
*/
|
|
158
|
-
get absolutePath() {
|
|
159
|
-
return this.root.absolutePath;
|
|
160
|
-
}
|
|
161
243
|
/**
|
|
162
244
|
* {@inheritDoc IDirectory.get}
|
|
163
245
|
*/
|
|
246
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
164
248
|
get(key) {
|
|
165
249
|
return this.root.get(key);
|
|
166
250
|
}
|
|
@@ -209,13 +293,18 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
209
293
|
* Issue a callback on each entry under this IDirectory.
|
|
210
294
|
* @param callback - Callback to issue
|
|
211
295
|
*/
|
|
296
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
297
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
298
|
forEach(callback) {
|
|
299
|
+
// eslint-disable-next-line unicorn/no-array-for-each, unicorn/no-array-callback-reference
|
|
213
300
|
this.root.forEach(callback);
|
|
214
301
|
}
|
|
215
302
|
/**
|
|
216
303
|
* Get an iterator over the entries under this IDirectory.
|
|
217
304
|
* @returns The iterator
|
|
218
305
|
*/
|
|
306
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
307
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
219
308
|
[(_a = Symbol.toStringTag, Symbol.iterator)]() {
|
|
220
309
|
return this.root[Symbol.iterator]();
|
|
221
310
|
}
|
|
@@ -223,6 +312,8 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
223
312
|
* Get an iterator over the entries under this IDirectory.
|
|
224
313
|
* @returns The iterator
|
|
225
314
|
*/
|
|
315
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
316
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
226
317
|
entries() {
|
|
227
318
|
return this.root.entries();
|
|
228
319
|
}
|
|
@@ -243,6 +334,8 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
243
334
|
* Get an iterator over the values under this IDirectory.
|
|
244
335
|
* @returns The iterator
|
|
245
336
|
*/
|
|
337
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
338
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
246
339
|
values() {
|
|
247
340
|
return this.root.values();
|
|
248
341
|
}
|
|
@@ -285,7 +378,7 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
285
378
|
return this.root;
|
|
286
379
|
}
|
|
287
380
|
let currentSubDir = this.root;
|
|
288
|
-
const subdirs = absolutePath.
|
|
381
|
+
const subdirs = absolutePath.slice(1).split(posix.sep);
|
|
289
382
|
for (const subdir of subdirs) {
|
|
290
383
|
currentSubDir = currentSubDir.getSubDirectory(subdir);
|
|
291
384
|
if (!currentSubDir) {
|
|
@@ -296,7 +389,6 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
296
389
|
}
|
|
297
390
|
/**
|
|
298
391
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
|
|
299
|
-
* @internal
|
|
300
392
|
*/
|
|
301
393
|
summarizeCore(serializer, telemetryContext) {
|
|
302
394
|
return this.serializeDirectory(this.root, serializer);
|
|
@@ -306,29 +398,25 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
306
398
|
* @param op - Op to submit
|
|
307
399
|
* @param localOpMetadata - The local metadata associated with the op. We send a unique id that is used to track
|
|
308
400
|
* this op while it has not been ack'd. This will be sent when we receive this op back from the server.
|
|
309
|
-
* @internal
|
|
310
401
|
*/
|
|
311
402
|
submitDirectoryMessage(op, localOpMetadata) {
|
|
312
403
|
this.submitLocalMessage(op, localOpMetadata);
|
|
313
404
|
}
|
|
314
405
|
/**
|
|
315
406
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
|
|
316
|
-
* @internal
|
|
317
407
|
*/
|
|
318
408
|
onDisconnect() { }
|
|
319
409
|
/**
|
|
320
410
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
|
|
321
|
-
* @internal
|
|
322
411
|
*/
|
|
323
412
|
reSubmitCore(content, localOpMetadata) {
|
|
324
413
|
const message = content;
|
|
325
414
|
const handler = this.messageHandlers.get(message.type);
|
|
326
|
-
(0,
|
|
415
|
+
(0, core_utils_1.assert)(handler !== undefined, 0x00d /* Missing message handler for message type */);
|
|
327
416
|
handler.submit(message, localOpMetadata);
|
|
328
417
|
}
|
|
329
418
|
/**
|
|
330
419
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
|
|
331
|
-
* @internal
|
|
332
420
|
*/
|
|
333
421
|
async loadCore(storage) {
|
|
334
422
|
const data = await (0, driver_utils_1.readAndParse)(storage, snapshotFileName);
|
|
@@ -349,7 +437,6 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
349
437
|
/**
|
|
350
438
|
* Populate the directory with the given directory data.
|
|
351
439
|
* @param data - A JSON string containing serialized directory data
|
|
352
|
-
* @internal
|
|
353
440
|
*/
|
|
354
441
|
populate(data) {
|
|
355
442
|
const stack = [];
|
|
@@ -358,11 +445,42 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
358
445
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
359
446
|
const [currentSubDir, currentSubDirObject] = stack.pop();
|
|
360
447
|
if (currentSubDirObject.subdirectories) {
|
|
448
|
+
// Utilize a map to store the seq -> clientSeq for the newly created subdirectory
|
|
449
|
+
const tempSeqNums = new Map();
|
|
361
450
|
for (const [subdirName, subdirObject] of Object.entries(currentSubDirObject.subdirectories)) {
|
|
362
451
|
let newSubDir = currentSubDir.getSubDirectory(subdirName);
|
|
452
|
+
let seqData;
|
|
363
453
|
if (!newSubDir) {
|
|
364
|
-
|
|
454
|
+
const createInfo = subdirObject.ci;
|
|
455
|
+
// We do not store the client sequence number in the storage because the order has already been
|
|
456
|
+
// guaranteed during the serialization process. As a result, it is only essential to utilize the
|
|
457
|
+
// "fake" client sequence number to signify the loading order, and there is no need to retain
|
|
458
|
+
// the actual client sequence number at this point.
|
|
459
|
+
if (createInfo !== undefined && createInfo.csn > -1) {
|
|
460
|
+
// If csn is -1, then initialize it with 0, otherwise we will never process ops for this
|
|
461
|
+
// sub directory. This could be done at serialization time too, but we need to maintain
|
|
462
|
+
// back compat too and also we will actually know the state when it was serialized.
|
|
463
|
+
if (!tempSeqNums.has(createInfo.csn)) {
|
|
464
|
+
tempSeqNums.set(createInfo.csn, 0);
|
|
465
|
+
}
|
|
466
|
+
let fakeClientSeq = tempSeqNums.get(createInfo.csn);
|
|
467
|
+
seqData = { seq: createInfo.csn, clientSeq: fakeClientSeq };
|
|
468
|
+
tempSeqNums.set(createInfo.csn, ++fakeClientSeq);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
seqData = {
|
|
472
|
+
seq: 0,
|
|
473
|
+
clientSeq: ++currentSubDir.localCreationSeq,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
newSubDir = new SubDirectory(seqData, createInfo !== undefined
|
|
477
|
+
? new Set(createInfo.ccIds)
|
|
478
|
+
: new Set(), this, this.runtime, this.serializer, posix.join(currentSubDir.absolutePath, subdirName));
|
|
365
479
|
currentSubDir.populateSubDirectory(subdirName, newSubDir);
|
|
480
|
+
// Record the newly inserted subdirectory to the creation tracker
|
|
481
|
+
currentSubDir.ackedCreationSeqTracker.set(subdirName, {
|
|
482
|
+
...seqData,
|
|
483
|
+
});
|
|
366
484
|
}
|
|
367
485
|
stack.push([newSubDir, subdirObject]);
|
|
368
486
|
}
|
|
@@ -377,20 +495,18 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
377
495
|
}
|
|
378
496
|
/**
|
|
379
497
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
|
|
380
|
-
* @internal
|
|
381
498
|
*/
|
|
382
499
|
processCore(message, local, localOpMetadata) {
|
|
383
500
|
if (message.type === protocol_definitions_1.MessageType.Operation) {
|
|
384
501
|
const op = message.contents;
|
|
385
502
|
const handler = this.messageHandlers.get(op.type);
|
|
386
|
-
(0,
|
|
387
|
-
handler.process(op, local, localOpMetadata);
|
|
503
|
+
(0, core_utils_1.assert)(handler !== undefined, 0x00e /* Missing message handler for message type */);
|
|
504
|
+
handler.process(message, op, local, localOpMetadata);
|
|
388
505
|
}
|
|
389
506
|
}
|
|
390
507
|
/**
|
|
391
508
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
|
|
392
|
-
|
|
393
|
-
*/
|
|
509
|
+
*/
|
|
394
510
|
rollback(content, localOpMetadata) {
|
|
395
511
|
const op = content;
|
|
396
512
|
const subdir = this.getWorkingDirectory(op.path);
|
|
@@ -415,19 +531,50 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
415
531
|
* @param serializable - The remote information that we can convert into a real object
|
|
416
532
|
* @returns The local value that was produced
|
|
417
533
|
*/
|
|
418
|
-
makeLocal(key, absolutePath,
|
|
419
|
-
|
|
534
|
+
makeLocal(key, absolutePath,
|
|
535
|
+
// eslint-disable-next-line import/no-deprecated
|
|
536
|
+
serializable) {
|
|
537
|
+
(0, core_utils_1.assert)(serializable.type === shared_object_base_1.ValueType[shared_object_base_1.ValueType.Plain] ||
|
|
538
|
+
serializable.type === shared_object_base_1.ValueType[shared_object_base_1.ValueType.Shared], 0x1e4 /* "Unexpected serializable type" */);
|
|
420
539
|
return this.localValueMaker.fromSerializable(serializable);
|
|
421
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* This checks if there is pending delete op for local delete for a any subdir in the relative path.
|
|
543
|
+
* @param relativePath - path of sub directory.
|
|
544
|
+
* @returns `true` if there is pending delete, `false` otherwise.
|
|
545
|
+
*/
|
|
546
|
+
isSubDirectoryDeletePending(relativePath) {
|
|
547
|
+
const absolutePath = this.makeAbsolute(relativePath);
|
|
548
|
+
if (absolutePath === posix.sep) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
let currentParent = this.root;
|
|
552
|
+
const nodeList = absolutePath.split(posix.sep);
|
|
553
|
+
let start = 1;
|
|
554
|
+
while (start < nodeList.length) {
|
|
555
|
+
const subDirName = nodeList[start];
|
|
556
|
+
if (currentParent.isSubDirectoryDeletePending(subDirName)) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
currentParent = currentParent.getSubDirectory(subDirName);
|
|
560
|
+
if (currentParent === undefined) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
start += 1;
|
|
564
|
+
}
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
422
567
|
/**
|
|
423
568
|
* Set the message handlers for the directory.
|
|
424
569
|
*/
|
|
425
570
|
setMessageHandlers() {
|
|
426
571
|
this.messageHandlers.set("clear", {
|
|
427
|
-
process: (op, local, localOpMetadata) => {
|
|
572
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
428
573
|
const subdir = this.getWorkingDirectory(op.path);
|
|
429
|
-
|
|
430
|
-
|
|
574
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
575
|
+
// as we are going to delete this subDirectory.
|
|
576
|
+
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
577
|
+
subdir.processClearMessage(msg, op, local, localOpMetadata);
|
|
431
578
|
}
|
|
432
579
|
},
|
|
433
580
|
submit: (op, localOpMetadata) => {
|
|
@@ -436,12 +583,20 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
436
583
|
subdir.resubmitClearMessage(op, localOpMetadata);
|
|
437
584
|
}
|
|
438
585
|
},
|
|
586
|
+
applyStashedOp: (op) => {
|
|
587
|
+
const subdir = this.getWorkingDirectory(op.path);
|
|
588
|
+
if (subdir) {
|
|
589
|
+
return subdir.applyStashedClearMessage(op);
|
|
590
|
+
}
|
|
591
|
+
},
|
|
439
592
|
});
|
|
440
593
|
this.messageHandlers.set("delete", {
|
|
441
|
-
process: (op, local, localOpMetadata) => {
|
|
594
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
442
595
|
const subdir = this.getWorkingDirectory(op.path);
|
|
443
|
-
|
|
444
|
-
|
|
596
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
597
|
+
// as we are going to delete this subDirectory.
|
|
598
|
+
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
599
|
+
subdir.processDeleteMessage(msg, op, local, localOpMetadata);
|
|
445
600
|
}
|
|
446
601
|
},
|
|
447
602
|
submit: (op, localOpMetadata) => {
|
|
@@ -450,13 +605,21 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
450
605
|
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
451
606
|
}
|
|
452
607
|
},
|
|
608
|
+
applyStashedOp: (op) => {
|
|
609
|
+
const subdir = this.getWorkingDirectory(op.path);
|
|
610
|
+
if (subdir) {
|
|
611
|
+
return subdir.applyStashedDeleteMessage(op);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
453
614
|
});
|
|
454
615
|
this.messageHandlers.set("set", {
|
|
455
|
-
process: (op, local, localOpMetadata) => {
|
|
616
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
456
617
|
const subdir = this.getWorkingDirectory(op.path);
|
|
457
|
-
|
|
618
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
619
|
+
// as we are going to delete this subDirectory.
|
|
620
|
+
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
458
621
|
const context = local ? undefined : this.makeLocal(op.key, op.path, op.value);
|
|
459
|
-
subdir.processSetMessage(op, context, local, localOpMetadata);
|
|
622
|
+
subdir.processSetMessage(msg, op, context, local, localOpMetadata);
|
|
460
623
|
}
|
|
461
624
|
},
|
|
462
625
|
submit: (op, localOpMetadata) => {
|
|
@@ -465,12 +628,21 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
465
628
|
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
466
629
|
}
|
|
467
630
|
},
|
|
631
|
+
applyStashedOp: (op) => {
|
|
632
|
+
const subdir = this.getWorkingDirectory(op.path);
|
|
633
|
+
if (subdir) {
|
|
634
|
+
const context = this.makeLocal(op.key, op.path, op.value);
|
|
635
|
+
return subdir.applyStashedSetMessage(op, context);
|
|
636
|
+
}
|
|
637
|
+
},
|
|
468
638
|
});
|
|
469
639
|
this.messageHandlers.set("createSubDirectory", {
|
|
470
|
-
process: (op, local, localOpMetadata) => {
|
|
640
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
471
641
|
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
472
|
-
|
|
473
|
-
|
|
642
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
643
|
+
// as we are going to delete this subDirectory.
|
|
644
|
+
if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
645
|
+
parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
474
646
|
}
|
|
475
647
|
},
|
|
476
648
|
submit: (op, localOpMetadata) => {
|
|
@@ -480,12 +652,20 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
480
652
|
parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
481
653
|
}
|
|
482
654
|
},
|
|
655
|
+
applyStashedOp: (op) => {
|
|
656
|
+
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
657
|
+
if (parentSubdir) {
|
|
658
|
+
return parentSubdir.applyStashedCreateSubDirMessage(op);
|
|
659
|
+
}
|
|
660
|
+
},
|
|
483
661
|
});
|
|
484
662
|
this.messageHandlers.set("deleteSubDirectory", {
|
|
485
|
-
process: (op, local, localOpMetadata) => {
|
|
663
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
486
664
|
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
487
|
-
|
|
488
|
-
|
|
665
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
666
|
+
// as we are going to delete this subDirectory.
|
|
667
|
+
if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
668
|
+
parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
489
669
|
}
|
|
490
670
|
},
|
|
491
671
|
submit: (op, localOpMetadata) => {
|
|
@@ -495,13 +675,23 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
495
675
|
parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
496
676
|
}
|
|
497
677
|
},
|
|
678
|
+
applyStashedOp: (op) => {
|
|
679
|
+
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
680
|
+
if (parentSubdir) {
|
|
681
|
+
return parentSubdir.applyStashedDeleteSubDirMessage(op);
|
|
682
|
+
}
|
|
683
|
+
},
|
|
498
684
|
});
|
|
499
685
|
}
|
|
500
686
|
/**
|
|
501
|
-
* @
|
|
687
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
|
|
502
688
|
*/
|
|
503
|
-
applyStashedOp() {
|
|
504
|
-
|
|
689
|
+
applyStashedOp(op) {
|
|
690
|
+
const handler = this.messageHandlers.get(op.type);
|
|
691
|
+
if (handler === undefined) {
|
|
692
|
+
throw new Error("no apply stashed op handler");
|
|
693
|
+
}
|
|
694
|
+
return handler.applyStashedOp(op);
|
|
505
695
|
}
|
|
506
696
|
serializeDirectory(root, serializer, telemetryContext) {
|
|
507
697
|
const MinValueSizeSeparateSnapshotBlob = 8 * 1024;
|
|
@@ -514,20 +704,21 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
514
704
|
while (stack.length > 0) {
|
|
515
705
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
516
706
|
const [currentSubDir, currentSubDirObject] = stack.pop();
|
|
707
|
+
currentSubDirObject.ci = currentSubDir.getSerializableCreateInfo();
|
|
517
708
|
for (const [key, value] of currentSubDir.getSerializedStorage(serializer)) {
|
|
518
709
|
if (!currentSubDirObject.storage) {
|
|
519
710
|
currentSubDirObject.storage = {};
|
|
520
711
|
}
|
|
712
|
+
// eslint-disable-next-line import/no-deprecated
|
|
521
713
|
const result = {
|
|
522
714
|
type: value.type,
|
|
523
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
524
715
|
value: value.value && JSON.parse(value.value),
|
|
525
716
|
};
|
|
526
717
|
if (value.value && value.value.length >= MinValueSizeSeparateSnapshotBlob) {
|
|
527
718
|
const extraContent = {};
|
|
528
719
|
let largeContent = extraContent;
|
|
529
720
|
if (currentSubDir.absolutePath !== posix.sep) {
|
|
530
|
-
for (const dir of currentSubDir.absolutePath.
|
|
721
|
+
for (const dir of currentSubDir.absolutePath.slice(1).split(posix.sep)) {
|
|
531
722
|
const subDataObject = {};
|
|
532
723
|
largeContent.subdirectories = { [dir]: subDataObject };
|
|
533
724
|
largeContent = subDataObject;
|
|
@@ -561,38 +752,49 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
|
|
|
561
752
|
}
|
|
562
753
|
}
|
|
563
754
|
exports.SharedDirectory = SharedDirectory;
|
|
755
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
564
756
|
function isKeyEditLocalOpMetadata(metadata) {
|
|
565
|
-
return metadata !== undefined &&
|
|
757
|
+
return (metadata !== undefined &&
|
|
758
|
+
typeof metadata.pendingMessageId === "number" &&
|
|
759
|
+
metadata.type === "edit");
|
|
566
760
|
}
|
|
567
761
|
function isClearLocalOpMetadata(metadata) {
|
|
568
|
-
return metadata !== undefined &&
|
|
569
|
-
|
|
762
|
+
return (metadata !== undefined &&
|
|
763
|
+
metadata.type === "clear" &&
|
|
764
|
+
typeof metadata.pendingMessageId === "number" &&
|
|
765
|
+
typeof metadata.previousStorage === "object");
|
|
570
766
|
}
|
|
571
767
|
function isSubDirLocalOpMetadata(metadata) {
|
|
572
|
-
return metadata !== undefined &&
|
|
573
|
-
(
|
|
574
|
-
metadata.type === "deleteSubDir");
|
|
768
|
+
return (metadata !== undefined &&
|
|
769
|
+
(metadata.type === "createSubDir" || metadata.type === "deleteSubDir"));
|
|
575
770
|
}
|
|
576
771
|
function isDirectoryLocalOpMetadata(metadata) {
|
|
577
|
-
return metadata
|
|
578
|
-
(metadata
|
|
579
|
-
|
|
580
|
-
|
|
772
|
+
return (isKeyEditLocalOpMetadata(metadata) ||
|
|
773
|
+
isClearLocalOpMetadata(metadata) ||
|
|
774
|
+
isSubDirLocalOpMetadata(metadata));
|
|
775
|
+
}
|
|
776
|
+
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
777
|
+
function assertNonNullClientId(clientId) {
|
|
778
|
+
(0, core_utils_1.assert)(clientId !== null, 0x6af /* client id should never be null */);
|
|
581
779
|
}
|
|
582
780
|
/**
|
|
583
781
|
* Node of the directory tree.
|
|
584
782
|
* @sealed
|
|
585
783
|
*/
|
|
586
|
-
class SubDirectory extends
|
|
784
|
+
class SubDirectory extends client_utils_1.TypedEventEmitter {
|
|
587
785
|
/**
|
|
588
786
|
* Constructor.
|
|
787
|
+
* @param sequenceNumber - Message seq number at which this was created.
|
|
788
|
+
* @param clientIds - Ids of client which created this directory.
|
|
589
789
|
* @param directory - Reference back to the SharedDirectory to perform operations
|
|
590
790
|
* @param runtime - The data store runtime this directory is associated with
|
|
591
791
|
* @param serializer - The serializer to serialize / parse handles
|
|
592
792
|
* @param absolutePath - The absolute path of this IDirectory
|
|
593
793
|
*/
|
|
594
|
-
constructor(directory, runtime, serializer, absolutePath) {
|
|
794
|
+
constructor(seqData, clientIds, directory, runtime, serializer, absolutePath) {
|
|
595
795
|
super();
|
|
796
|
+
this.seqData = seqData;
|
|
797
|
+
this.clientIds = clientIds;
|
|
596
798
|
this.directory = directory;
|
|
597
799
|
this.runtime = runtime;
|
|
598
800
|
this.serializer = serializer;
|
|
@@ -614,13 +816,24 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
614
816
|
*/
|
|
615
817
|
this._subdirectories = new Map();
|
|
616
818
|
/**
|
|
617
|
-
* Keys that have been modified locally but not yet ack'd from the server.
|
|
819
|
+
* Keys that have been modified locally but not yet ack'd from the server. This is for operations on keys like
|
|
820
|
+
* set/delete operations on keys. The value of this map is list of pendingMessageIds at which that key
|
|
821
|
+
* was modified. We don't store the type of ops, and behaviour of key ops are different from behaviour of sub
|
|
822
|
+
* directory ops, so we have separate map from subDirectories tracker.
|
|
618
823
|
*/
|
|
619
824
|
this.pendingKeys = new Map();
|
|
620
825
|
/**
|
|
621
|
-
* Subdirectories that have been
|
|
826
|
+
* Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
|
|
827
|
+
* of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
828
|
+
* deleted sub directory.
|
|
829
|
+
*/
|
|
830
|
+
this.pendingDeleteSubDirectoriesTracker = new Map();
|
|
831
|
+
/**
|
|
832
|
+
* Subdirectories that have been created locally but not yet ack'd from the server. This maintains the record
|
|
833
|
+
* of create op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
834
|
+
* created sub directory.
|
|
622
835
|
*/
|
|
623
|
-
this.
|
|
836
|
+
this.pendingCreateSubDirectoriesTracker = new Map();
|
|
624
837
|
/**
|
|
625
838
|
* This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
|
|
626
839
|
*/
|
|
@@ -629,23 +842,31 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
629
842
|
* The pending ids of any clears that have been performed locally but not yet ack'd from the server
|
|
630
843
|
*/
|
|
631
844
|
this.pendingClearMessageIds = [];
|
|
845
|
+
/**
|
|
846
|
+
* Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
|
|
847
|
+
* of the creation order.
|
|
848
|
+
*/
|
|
849
|
+
this.localCreationSeq = 0;
|
|
850
|
+
this.localCreationSeqTracker = new DirectoryCreationTracker();
|
|
851
|
+
this.ackedCreationSeqTracker = new DirectoryCreationTracker();
|
|
632
852
|
}
|
|
633
853
|
dispose(error) {
|
|
634
854
|
this._deleted = true;
|
|
635
855
|
this.emit("disposed", this);
|
|
636
856
|
}
|
|
637
857
|
/**
|
|
638
|
-
* Unmark the deleted property when rolling back delete.
|
|
858
|
+
* Unmark the deleted property only when rolling back delete.
|
|
639
859
|
*/
|
|
640
860
|
undispose() {
|
|
641
861
|
this._deleted = false;
|
|
862
|
+
this.emit("undisposed", this);
|
|
642
863
|
}
|
|
643
864
|
get disposed() {
|
|
644
865
|
return this._deleted;
|
|
645
866
|
}
|
|
646
867
|
throwIfDisposed() {
|
|
647
868
|
if (this._deleted) {
|
|
648
|
-
throw new
|
|
869
|
+
throw new telemetry_utils_1.UsageError("Cannot access Disposed subDirectory");
|
|
649
870
|
}
|
|
650
871
|
}
|
|
651
872
|
/**
|
|
@@ -661,9 +882,8 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
661
882
|
* {@inheritDoc IDirectory.get}
|
|
662
883
|
*/
|
|
663
884
|
get(key) {
|
|
664
|
-
var _c;
|
|
665
885
|
this.throwIfDisposed();
|
|
666
|
-
return
|
|
886
|
+
return this._storage.get(key)?.value;
|
|
667
887
|
}
|
|
668
888
|
/**
|
|
669
889
|
* {@inheritDoc IDirectory.set}
|
|
@@ -711,21 +931,38 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
711
931
|
throw new Error(`SubDirectory name may not contain ${posix.sep}`);
|
|
712
932
|
}
|
|
713
933
|
// Create the sub directory locally first.
|
|
714
|
-
const isNew = this.createSubDirectoryCore(subdirName, true);
|
|
715
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
934
|
+
const isNew = this.createSubDirectoryCore(subdirName, true, this.getLocalSeq(), this.runtime.clientId ?? "detached");
|
|
716
935
|
const subDir = this._subdirectories.get(subdirName);
|
|
936
|
+
(0, core_utils_1.assert)(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
|
|
717
937
|
// If we are not attached, don't submit the op.
|
|
718
938
|
if (!this.directory.isAttached()) {
|
|
719
939
|
return subDir;
|
|
720
940
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
941
|
+
// Only submit the op, if it is newly created.
|
|
942
|
+
if (isNew) {
|
|
943
|
+
const op = {
|
|
944
|
+
path: this.absolutePath,
|
|
945
|
+
subdirName,
|
|
946
|
+
type: "createSubDirectory",
|
|
947
|
+
};
|
|
948
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
949
|
+
}
|
|
727
950
|
return subDir;
|
|
728
951
|
}
|
|
952
|
+
/**
|
|
953
|
+
* @returns The Sequence Data which should be used for local changes.
|
|
954
|
+
* @remarks While detached, 0 is used rather than -1 to represent a change which should be universally known (as opposed to known
|
|
955
|
+
* only by the local client). This ensures that if the directory is later attached, none of its data needs to be updated (the values
|
|
956
|
+
* last set while detached will now be known to any new client, until they are changed).
|
|
957
|
+
*
|
|
958
|
+
* The client sequence number is incremented by 1 for maintaining the internal order of locally created subdirectories
|
|
959
|
+
* TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
|
|
960
|
+
*/
|
|
961
|
+
getLocalSeq() {
|
|
962
|
+
return this.directory.isAttached()
|
|
963
|
+
? { seq: -1, clientSeq: ++this.localCreationSeq }
|
|
964
|
+
: { seq: 0, clientSeq: ++this.localCreationSeq };
|
|
965
|
+
}
|
|
729
966
|
/**
|
|
730
967
|
* {@inheritDoc IDirectory.getSubDirectory}
|
|
731
968
|
*/
|
|
@@ -751,12 +988,15 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
751
988
|
if (!this.directory.isAttached()) {
|
|
752
989
|
return subDir !== undefined;
|
|
753
990
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
991
|
+
// Only submit the op, if the directory existed and we deleted it.
|
|
992
|
+
if (subDir !== undefined) {
|
|
993
|
+
const op = {
|
|
994
|
+
path: this.absolutePath,
|
|
995
|
+
subdirName,
|
|
996
|
+
type: "deleteSubDirectory",
|
|
997
|
+
};
|
|
998
|
+
this.submitDeleteSubDirectoryMessage(op, subDir);
|
|
999
|
+
}
|
|
760
1000
|
return subDir !== undefined;
|
|
761
1001
|
}
|
|
762
1002
|
/**
|
|
@@ -764,7 +1004,26 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
764
1004
|
*/
|
|
765
1005
|
subdirectories() {
|
|
766
1006
|
this.throwIfDisposed();
|
|
767
|
-
|
|
1007
|
+
const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
|
|
1008
|
+
const localSubdirsInOrder = this.localCreationSeqTracker.keys((key) => !this.ackedCreationSeqTracker.has(key));
|
|
1009
|
+
const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
|
|
1010
|
+
(0, core_utils_1.assert)(subdirNames.length === this._subdirectories.size, 0x85c /* The count of keys for iteration should be consistent with the size of actual data */);
|
|
1011
|
+
const entriesIterator = {
|
|
1012
|
+
index: 0,
|
|
1013
|
+
dirs: this._subdirectories,
|
|
1014
|
+
next() {
|
|
1015
|
+
if (this.index < subdirNames.length) {
|
|
1016
|
+
const subdirName = subdirNames[this.index++];
|
|
1017
|
+
const subdir = this.dirs.get(subdirName);
|
|
1018
|
+
return { value: [subdirName, subdir], done: false };
|
|
1019
|
+
}
|
|
1020
|
+
return { value: undefined, done: true };
|
|
1021
|
+
},
|
|
1022
|
+
[Symbol.iterator]() {
|
|
1023
|
+
return this;
|
|
1024
|
+
},
|
|
1025
|
+
};
|
|
1026
|
+
return entriesIterator;
|
|
768
1027
|
}
|
|
769
1028
|
/**
|
|
770
1029
|
* {@inheritDoc IDirectory.getWorkingDirectory}
|
|
@@ -773,6 +1032,17 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
773
1032
|
this.throwIfDisposed();
|
|
774
1033
|
return this.directory.getWorkingDirectory(this.makeAbsolute(relativePath));
|
|
775
1034
|
}
|
|
1035
|
+
/**
|
|
1036
|
+
* This checks if there is pending delete op for local delete for a given child subdirectory.
|
|
1037
|
+
* @param subDirName - directory name.
|
|
1038
|
+
* @returns true if there is pending delete.
|
|
1039
|
+
*/
|
|
1040
|
+
isSubDirectoryDeletePending(subDirName) {
|
|
1041
|
+
if (this.pendingDeleteSubDirectoriesTracker.has(subDirName)) {
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
776
1046
|
/**
|
|
777
1047
|
* Deletes the given key from within this IDirectory.
|
|
778
1048
|
* @param key - The key to delete
|
|
@@ -818,6 +1088,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
818
1088
|
*/
|
|
819
1089
|
forEach(callback) {
|
|
820
1090
|
this.throwIfDisposed();
|
|
1091
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
821
1092
|
this._storage.forEach((localValue, key, map) => {
|
|
822
1093
|
callback(localValue.value, key, map);
|
|
823
1094
|
});
|
|
@@ -839,13 +1110,9 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
839
1110
|
const iterator = {
|
|
840
1111
|
next() {
|
|
841
1112
|
const nextVal = localEntriesIterator.next();
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
else {
|
|
846
|
-
// Unpack the stored value
|
|
847
|
-
return { value: [nextVal.value[0], nextVal.value[1].value], done: false };
|
|
848
|
-
}
|
|
1113
|
+
return nextVal.done
|
|
1114
|
+
? { value: undefined, done: true }
|
|
1115
|
+
: { value: [nextVal.value[0], nextVal.value[1].value], done: false };
|
|
849
1116
|
},
|
|
850
1117
|
[Symbol.iterator]() {
|
|
851
1118
|
return this;
|
|
@@ -871,13 +1138,9 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
871
1138
|
const iterator = {
|
|
872
1139
|
next() {
|
|
873
1140
|
const nextVal = localValuesIterator.next();
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
else {
|
|
878
|
-
// Unpack the stored value
|
|
879
|
-
return { value: nextVal.value.value, done: false };
|
|
880
|
-
}
|
|
1141
|
+
return nextVal.done
|
|
1142
|
+
? { value: undefined, done: true }
|
|
1143
|
+
: { value: nextVal.value.value, done: false };
|
|
881
1144
|
},
|
|
882
1145
|
[Symbol.iterator]() {
|
|
883
1146
|
return this;
|
|
@@ -895,48 +1158,90 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
895
1158
|
}
|
|
896
1159
|
/**
|
|
897
1160
|
* Process a clear operation.
|
|
1161
|
+
* @param msg - The message from the server to apply.
|
|
898
1162
|
* @param op - The op to process
|
|
899
1163
|
* @param local - Whether the message originated from the local client
|
|
900
1164
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
901
1165
|
* For messages from a remote client, this will be undefined.
|
|
902
1166
|
* @internal
|
|
903
1167
|
*/
|
|
904
|
-
processClearMessage(op, local, localOpMetadata) {
|
|
1168
|
+
processClearMessage(msg, op, local, localOpMetadata) {
|
|
905
1169
|
this.throwIfDisposed();
|
|
1170
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
906
1173
|
if (local) {
|
|
907
|
-
(0,
|
|
1174
|
+
(0, core_utils_1.assert)(isClearLocalOpMetadata(localOpMetadata), 0x00f /* pendingMessageId is missing from the local client's operation */);
|
|
908
1175
|
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
909
|
-
(0,
|
|
1176
|
+
(0, core_utils_1.assert)(pendingClearMessageId === localOpMetadata.pendingMessageId, 0x32a /* pendingMessageId does not match */);
|
|
910
1177
|
return;
|
|
911
1178
|
}
|
|
912
|
-
this.clearExceptPendingKeys();
|
|
1179
|
+
this.clearExceptPendingKeys(false);
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Apply clear operation locally and generate metadata
|
|
1183
|
+
* @param op - Op to apply
|
|
1184
|
+
* @returns metadata generated for stahed op
|
|
1185
|
+
*/
|
|
1186
|
+
applyStashedClearMessage(op) {
|
|
1187
|
+
this.throwIfDisposed();
|
|
1188
|
+
const previousValue = new Map(this._storage);
|
|
1189
|
+
this.clearExceptPendingKeys(true);
|
|
1190
|
+
const pendingMsgId = ++this.pendingMessageId;
|
|
1191
|
+
this.pendingClearMessageIds.push(pendingMsgId);
|
|
1192
|
+
const metadata = {
|
|
1193
|
+
type: "clear",
|
|
1194
|
+
pendingMessageId: pendingMsgId,
|
|
1195
|
+
previousStorage: previousValue,
|
|
1196
|
+
};
|
|
1197
|
+
return metadata;
|
|
913
1198
|
}
|
|
914
1199
|
/**
|
|
915
1200
|
* Process a delete operation.
|
|
1201
|
+
* @param msg - The message from the server to apply.
|
|
916
1202
|
* @param op - The op to process
|
|
917
1203
|
* @param local - Whether the message originated from the local client
|
|
918
1204
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
919
1205
|
* For messages from a remote client, this will be undefined.
|
|
920
1206
|
* @internal
|
|
921
1207
|
*/
|
|
922
|
-
processDeleteMessage(op, local, localOpMetadata) {
|
|
1208
|
+
processDeleteMessage(msg, op, local, localOpMetadata) {
|
|
923
1209
|
this.throwIfDisposed();
|
|
924
|
-
if (!this.
|
|
1210
|
+
if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1211
|
+
this.needProcessStorageOperation(op, local, localOpMetadata))) {
|
|
925
1212
|
return;
|
|
926
1213
|
}
|
|
927
1214
|
this.deleteCore(op.key, local);
|
|
928
1215
|
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Apply delete operation locally and generate metadata
|
|
1218
|
+
* @param op - Op to apply
|
|
1219
|
+
* @returns metadata generated for stahed op
|
|
1220
|
+
*/
|
|
1221
|
+
applyStashedDeleteMessage(op) {
|
|
1222
|
+
this.throwIfDisposed();
|
|
1223
|
+
const previousValue = this.deleteCore(op.key, true);
|
|
1224
|
+
const pendingMessageId = this.getKeyMessageId(op);
|
|
1225
|
+
const localMetadata = {
|
|
1226
|
+
type: "edit",
|
|
1227
|
+
pendingMessageId,
|
|
1228
|
+
previousValue,
|
|
1229
|
+
};
|
|
1230
|
+
return localMetadata;
|
|
1231
|
+
}
|
|
929
1232
|
/**
|
|
930
1233
|
* Process a set operation.
|
|
1234
|
+
* @param msg - The message from the server to apply.
|
|
931
1235
|
* @param op - The op to process
|
|
932
1236
|
* @param local - Whether the message originated from the local client
|
|
933
1237
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
934
1238
|
* For messages from a remote client, this will be undefined.
|
|
935
1239
|
* @internal
|
|
936
1240
|
*/
|
|
937
|
-
processSetMessage(op, context, local, localOpMetadata) {
|
|
1241
|
+
processSetMessage(msg, op, context, local, localOpMetadata) {
|
|
938
1242
|
this.throwIfDisposed();
|
|
939
|
-
if (!this.
|
|
1243
|
+
if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1244
|
+
this.needProcessStorageOperation(op, local, localOpMetadata))) {
|
|
940
1245
|
return;
|
|
941
1246
|
}
|
|
942
1247
|
// needProcessStorageOperation should have returned false if local is true
|
|
@@ -944,36 +1249,89 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
944
1249
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
945
1250
|
this.setCore(op.key, context, local);
|
|
946
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Apply set operation locally and generate metadata
|
|
1254
|
+
* @param op - Op to apply
|
|
1255
|
+
* @returns metadata generated for stahed op
|
|
1256
|
+
*/
|
|
1257
|
+
applyStashedSetMessage(op, context) {
|
|
1258
|
+
this.throwIfDisposed();
|
|
1259
|
+
// Set the value locally.
|
|
1260
|
+
const previousValue = this.setCore(op.key, context, true);
|
|
1261
|
+
// Create metadata
|
|
1262
|
+
const pendingMessageId = this.getKeyMessageId(op);
|
|
1263
|
+
const localMetadata = {
|
|
1264
|
+
type: "edit",
|
|
1265
|
+
pendingMessageId,
|
|
1266
|
+
previousValue,
|
|
1267
|
+
};
|
|
1268
|
+
return localMetadata;
|
|
1269
|
+
}
|
|
947
1270
|
/**
|
|
948
1271
|
* Process a create subdirectory operation.
|
|
1272
|
+
* @param msg - The message from the server to apply.
|
|
949
1273
|
* @param op - The op to process
|
|
950
1274
|
* @param local - Whether the message originated from the local client
|
|
951
1275
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
952
1276
|
* For messages from a remote client, this will be undefined.
|
|
953
1277
|
* @internal
|
|
954
1278
|
*/
|
|
955
|
-
processCreateSubDirectoryMessage(op, local, localOpMetadata) {
|
|
1279
|
+
processCreateSubDirectoryMessage(msg, op, local, localOpMetadata) {
|
|
956
1280
|
this.throwIfDisposed();
|
|
957
|
-
if (!this.
|
|
1281
|
+
if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1282
|
+
this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
|
|
958
1283
|
return;
|
|
959
1284
|
}
|
|
960
|
-
|
|
1285
|
+
assertNonNullClientId(msg.clientId);
|
|
1286
|
+
this.createSubDirectoryCore(op.subdirName, local, { seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber }, msg.clientId);
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Apply createSubDirectory operation locally and generate metadata
|
|
1290
|
+
* @param op - Op to apply
|
|
1291
|
+
* @returns metadata generated for stahed op
|
|
1292
|
+
*/
|
|
1293
|
+
applyStashedCreateSubDirMessage(op) {
|
|
1294
|
+
this.throwIfDisposed();
|
|
1295
|
+
// Create the sub directory locally first.
|
|
1296
|
+
this.createSubDirectoryCore(op.subdirName, true, this.getLocalSeq(), this.runtime.clientId ?? "detached");
|
|
1297
|
+
this.updatePendingSubDirMessageCount(op);
|
|
1298
|
+
const localOpMetadata = {
|
|
1299
|
+
type: "createSubDir",
|
|
1300
|
+
};
|
|
1301
|
+
return localOpMetadata;
|
|
961
1302
|
}
|
|
962
1303
|
/**
|
|
963
1304
|
* Process a delete subdirectory operation.
|
|
1305
|
+
* @param msg - The message from the server to apply.
|
|
964
1306
|
* @param op - The op to process
|
|
965
1307
|
* @param local - Whether the message originated from the local client
|
|
966
1308
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
967
1309
|
* For messages from a remote client, this will be undefined.
|
|
968
1310
|
* @internal
|
|
969
1311
|
*/
|
|
970
|
-
processDeleteSubDirectoryMessage(op, local, localOpMetadata) {
|
|
1312
|
+
processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata) {
|
|
971
1313
|
this.throwIfDisposed();
|
|
972
|
-
if (!this.
|
|
1314
|
+
if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1315
|
+
this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
|
|
973
1316
|
return;
|
|
974
1317
|
}
|
|
975
1318
|
this.deleteSubDirectoryCore(op.subdirName, local);
|
|
976
1319
|
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Apply deleteSubDirectory operation locally and generate metadata
|
|
1322
|
+
* @param op - Op to apply
|
|
1323
|
+
* @returns metadata generated for stahed op
|
|
1324
|
+
*/
|
|
1325
|
+
applyStashedDeleteSubDirMessage(op) {
|
|
1326
|
+
this.throwIfDisposed();
|
|
1327
|
+
const subDir = this.deleteSubDirectoryCore(op.subdirName, true);
|
|
1328
|
+
this.updatePendingSubDirMessageCount(op);
|
|
1329
|
+
const metadata = {
|
|
1330
|
+
type: "deleteSubDir",
|
|
1331
|
+
subDirectory: subDir,
|
|
1332
|
+
};
|
|
1333
|
+
return metadata;
|
|
1334
|
+
}
|
|
977
1335
|
/**
|
|
978
1336
|
* Submit a clear operation.
|
|
979
1337
|
* @param op - The operation
|
|
@@ -995,11 +1353,14 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
995
1353
|
* @internal
|
|
996
1354
|
*/
|
|
997
1355
|
resubmitClearMessage(op, localOpMetadata) {
|
|
998
|
-
(0,
|
|
1356
|
+
(0, core_utils_1.assert)(isClearLocalOpMetadata(localOpMetadata), 0x32b /* Invalid localOpMetadata for clear */);
|
|
999
1357
|
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
1000
1358
|
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
1001
|
-
|
|
1002
|
-
|
|
1359
|
+
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1360
|
+
// is already deleted, in which case we don't need to submit the op.
|
|
1361
|
+
if (pendingClearMessageId === localOpMetadata.pendingMessageId) {
|
|
1362
|
+
this.submitClearMessage(op, localOpMetadata.previousStorage);
|
|
1363
|
+
}
|
|
1003
1364
|
}
|
|
1004
1365
|
/**
|
|
1005
1366
|
* Get a new pending message id for the op and cache it to track the pending op
|
|
@@ -1034,43 +1395,55 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1034
1395
|
* @internal
|
|
1035
1396
|
*/
|
|
1036
1397
|
resubmitKeyMessage(op, localOpMetadata) {
|
|
1037
|
-
(0,
|
|
1398
|
+
(0, core_utils_1.assert)(isKeyEditLocalOpMetadata(localOpMetadata), 0x32d /* Invalid localOpMetadata in submit */);
|
|
1038
1399
|
// clear the old pending message id
|
|
1039
1400
|
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (pendingMessageIds
|
|
1043
|
-
|
|
1401
|
+
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1402
|
+
// is already deleted, in which case we don't need to submit the op.
|
|
1403
|
+
if (pendingMessageIds !== undefined) {
|
|
1404
|
+
const index = pendingMessageIds.findIndex((id) => id === localOpMetadata.pendingMessageId);
|
|
1405
|
+
if (index === -1) {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
pendingMessageIds.splice(index, 1);
|
|
1409
|
+
if (pendingMessageIds.length === 0) {
|
|
1410
|
+
this.pendingKeys.delete(op.key);
|
|
1411
|
+
}
|
|
1412
|
+
this.submitKeyMessage(op, localOpMetadata.previousValue);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
incrementPendingSubDirCount(map, subDirName) {
|
|
1416
|
+
const count = map.get(subDirName) ?? 0;
|
|
1417
|
+
map.set(subDirName, count + 1);
|
|
1418
|
+
}
|
|
1419
|
+
decrementPendingSubDirCount(map, subDirName) {
|
|
1420
|
+
const count = map.get(subDirName) ?? 0;
|
|
1421
|
+
map.set(subDirName, count - 1);
|
|
1422
|
+
if (count <= 1) {
|
|
1423
|
+
map.delete(subDirName);
|
|
1044
1424
|
}
|
|
1045
|
-
this.submitKeyMessage(op, localOpMetadata.previousValue);
|
|
1046
1425
|
}
|
|
1047
1426
|
/**
|
|
1048
|
-
*
|
|
1427
|
+
* Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
|
|
1428
|
+
* or while resubmitting the op.
|
|
1049
1429
|
*/
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
|
|
1054
|
-
if (pendingMessageIds !== undefined) {
|
|
1055
|
-
pendingMessageIds.push(newMessageId);
|
|
1430
|
+
updatePendingSubDirMessageCount(op) {
|
|
1431
|
+
if (op.type === "deleteSubDirectory") {
|
|
1432
|
+
this.incrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
1056
1433
|
}
|
|
1057
|
-
else {
|
|
1058
|
-
this.
|
|
1434
|
+
else if (op.type === "createSubDirectory") {
|
|
1435
|
+
this.incrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1059
1436
|
}
|
|
1060
|
-
return newMessageId;
|
|
1061
1437
|
}
|
|
1062
1438
|
/**
|
|
1063
1439
|
* Submit a create subdirectory operation.
|
|
1064
1440
|
* @param op - The operation
|
|
1065
|
-
* @param prevExisted - Whether the subdirectory existed before the op
|
|
1066
1441
|
*/
|
|
1067
|
-
submitCreateSubDirectoryMessage(op
|
|
1442
|
+
submitCreateSubDirectoryMessage(op) {
|
|
1068
1443
|
this.throwIfDisposed();
|
|
1069
|
-
|
|
1444
|
+
this.updatePendingSubDirMessageCount(op);
|
|
1070
1445
|
const localOpMetadata = {
|
|
1071
1446
|
type: "createSubDir",
|
|
1072
|
-
pendingMessageId: newMessageId,
|
|
1073
|
-
previouslyExisted: prevExisted,
|
|
1074
1447
|
};
|
|
1075
1448
|
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1076
1449
|
}
|
|
@@ -1081,10 +1454,9 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1081
1454
|
*/
|
|
1082
1455
|
submitDeleteSubDirectoryMessage(op, subDir) {
|
|
1083
1456
|
this.throwIfDisposed();
|
|
1084
|
-
|
|
1457
|
+
this.updatePendingSubDirMessageCount(op);
|
|
1085
1458
|
const localOpMetadata = {
|
|
1086
1459
|
type: "deleteSubDir",
|
|
1087
|
-
pendingMessageId: newMessageId,
|
|
1088
1460
|
subDirectory: subDir,
|
|
1089
1461
|
};
|
|
1090
1462
|
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
@@ -1096,18 +1468,23 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1096
1468
|
* @internal
|
|
1097
1469
|
*/
|
|
1098
1470
|
resubmitSubDirectoryMessage(op, localOpMetadata) {
|
|
1099
|
-
(0,
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1471
|
+
(0, core_utils_1.assert)(isSubDirLocalOpMetadata(localOpMetadata), 0x32f /* Invalid localOpMetadata for sub directory op */);
|
|
1472
|
+
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1473
|
+
// is already deleted, in which case we don't need to submit the op.
|
|
1474
|
+
if (localOpMetadata.type === "createSubDir" &&
|
|
1475
|
+
!this.pendingCreateSubDirectoriesTracker.has(op.subdirName)) {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
else if (localOpMetadata.type === "deleteSubDir" &&
|
|
1479
|
+
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
|
|
1480
|
+
return;
|
|
1106
1481
|
}
|
|
1107
1482
|
if (localOpMetadata.type === "createSubDir") {
|
|
1108
|
-
this.
|
|
1483
|
+
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1484
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
1109
1485
|
}
|
|
1110
1486
|
else {
|
|
1487
|
+
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
1111
1488
|
this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
|
|
1112
1489
|
}
|
|
1113
1490
|
}
|
|
@@ -1125,6 +1502,14 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1125
1502
|
yield res;
|
|
1126
1503
|
}
|
|
1127
1504
|
}
|
|
1505
|
+
getSerializableCreateInfo() {
|
|
1506
|
+
this.throwIfDisposed();
|
|
1507
|
+
const createInfo = {
|
|
1508
|
+
csn: this.seqData.seq,
|
|
1509
|
+
ccIds: Array.from(this.clientIds),
|
|
1510
|
+
};
|
|
1511
|
+
return createInfo;
|
|
1512
|
+
}
|
|
1128
1513
|
/**
|
|
1129
1514
|
* Populate a key value in this subdirectory's storage, to be used when loading from snapshot.
|
|
1130
1515
|
* @param key - The key to populate
|
|
@@ -1163,7 +1548,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1163
1548
|
*/
|
|
1164
1549
|
rollbackPendingMessageId(map, key, pendingMessageId) {
|
|
1165
1550
|
const pendingMessageIds = map.get(key);
|
|
1166
|
-
const lastPendingMessageId = pendingMessageIds
|
|
1551
|
+
const lastPendingMessageId = pendingMessageIds?.pop();
|
|
1167
1552
|
if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
|
|
1168
1553
|
throw new Error("Rollback op does not match last pending");
|
|
1169
1554
|
}
|
|
@@ -1171,21 +1556,24 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1171
1556
|
map.delete(key);
|
|
1172
1557
|
}
|
|
1173
1558
|
}
|
|
1559
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
1174
1560
|
/**
|
|
1175
1561
|
* Rollback a local op
|
|
1176
1562
|
* @param op - The operation to rollback
|
|
1177
1563
|
* @param localOpMetadata - The local metadata associated with the op.
|
|
1178
1564
|
*/
|
|
1565
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1179
1566
|
rollback(op, localOpMetadata) {
|
|
1180
1567
|
if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
|
|
1181
1568
|
throw new Error("Invalid localOpMetadata");
|
|
1182
1569
|
}
|
|
1183
1570
|
if (op.type === "clear" && localOpMetadata.type === "clear") {
|
|
1184
|
-
localOpMetadata.previousStorage.
|
|
1571
|
+
for (const [key, localValue] of localOpMetadata.previousStorage.entries()) {
|
|
1185
1572
|
this.setCore(key, localValue, true);
|
|
1186
|
-
}
|
|
1573
|
+
}
|
|
1187
1574
|
const lastPendingClearId = this.pendingClearMessageIds.pop();
|
|
1188
|
-
if (lastPendingClearId === undefined ||
|
|
1575
|
+
if (lastPendingClearId === undefined ||
|
|
1576
|
+
lastPendingClearId !== localOpMetadata.pendingMessageId) {
|
|
1189
1577
|
throw new Error("Rollback op does match last clear");
|
|
1190
1578
|
}
|
|
1191
1579
|
}
|
|
@@ -1199,24 +1587,34 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1199
1587
|
this.rollbackPendingMessageId(this.pendingKeys, op.key, localOpMetadata.pendingMessageId);
|
|
1200
1588
|
}
|
|
1201
1589
|
else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
|
|
1590
|
+
this.deleteSubDirectoryCore(op.subdirName, true);
|
|
1591
|
+
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1206
1592
|
}
|
|
1207
1593
|
else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
|
|
1208
1594
|
if (localOpMetadata.subDirectory !== undefined) {
|
|
1209
1595
|
this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
|
|
1210
1596
|
// don't need to register events because deleting never unregistered
|
|
1211
1597
|
this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
|
|
1598
|
+
// Restore the record in creation tracker
|
|
1599
|
+
if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
|
|
1600
|
+
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
1601
|
+
...localOpMetadata.subDirectory.seqData,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
else {
|
|
1605
|
+
this.localCreationSeqTracker.set(op.subdirName, {
|
|
1606
|
+
...localOpMetadata.subDirectory.seqData,
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1212
1609
|
this.emit("subDirectoryCreated", op.subdirName, true, this);
|
|
1213
1610
|
}
|
|
1214
|
-
this.
|
|
1611
|
+
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subDirName);
|
|
1215
1612
|
}
|
|
1216
1613
|
else {
|
|
1217
1614
|
throw new Error("Unsupported op for rollback");
|
|
1218
1615
|
}
|
|
1219
1616
|
}
|
|
1617
|
+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
|
1220
1618
|
/**
|
|
1221
1619
|
* Converts the given relative path into an absolute path.
|
|
1222
1620
|
* @param path - Relative path to convert
|
|
@@ -1237,22 +1635,38 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1237
1635
|
needProcessStorageOperation(op, local, localOpMetadata) {
|
|
1238
1636
|
if (this.pendingClearMessageIds.length > 0) {
|
|
1239
1637
|
if (local) {
|
|
1240
|
-
(0,
|
|
1638
|
+
(0, core_utils_1.assert)(localOpMetadata !== undefined &&
|
|
1639
|
+
isKeyEditLocalOpMetadata(localOpMetadata) &&
|
|
1241
1640
|
localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0], 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
|
|
1641
|
+
// Remove all pendingMessageIds lower than first pendingClearMessageId.
|
|
1642
|
+
const lowestPendingClearMessageId = this.pendingClearMessageIds[0];
|
|
1643
|
+
const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
|
|
1644
|
+
if (pendingKeyMessageIdArray !== undefined) {
|
|
1645
|
+
let index = 0;
|
|
1646
|
+
while (pendingKeyMessageIdArray[index] < lowestPendingClearMessageId) {
|
|
1647
|
+
index += 1;
|
|
1648
|
+
}
|
|
1649
|
+
const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
|
|
1650
|
+
if (newPendingKeyMessageId.length === 0) {
|
|
1651
|
+
this.pendingKeys.delete(op.key);
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
this.pendingKeys.set(op.key, newPendingKeyMessageId);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1242
1657
|
}
|
|
1243
1658
|
// If I have a NACK clear, we can ignore all ops.
|
|
1244
1659
|
return false;
|
|
1245
1660
|
}
|
|
1246
|
-
const
|
|
1247
|
-
if (
|
|
1661
|
+
const pendingKeyMessageIds = this.pendingKeys.get(op.key);
|
|
1662
|
+
if (pendingKeyMessageIds !== undefined) {
|
|
1248
1663
|
// Found an NACK op, clear it from the directory if the latest sequence number in the directory
|
|
1249
1664
|
// match the message's and don't process the op.
|
|
1250
1665
|
if (local) {
|
|
1251
|
-
(0,
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
if (pendingMessageIds.length === 0) {
|
|
1666
|
+
(0, core_utils_1.assert)(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata), 0x011 /* pendingMessageId is missing from the local client's operation */);
|
|
1667
|
+
(0, core_utils_1.assert)(pendingKeyMessageIds[0] === localOpMetadata.pendingMessageId, 0x331 /* Unexpected pending message received */);
|
|
1668
|
+
pendingKeyMessageIds.shift();
|
|
1669
|
+
if (pendingKeyMessageIds.length === 0) {
|
|
1256
1670
|
this.pendingKeys.delete(op.key);
|
|
1257
1671
|
}
|
|
1258
1672
|
}
|
|
@@ -1261,6 +1675,19 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1261
1675
|
// If we don't have a NACK op on the key, we need to process the remote ops.
|
|
1262
1676
|
return !local;
|
|
1263
1677
|
}
|
|
1678
|
+
/**
|
|
1679
|
+
* This return true if the message is for the current instance of this sub directory. As the sub directory
|
|
1680
|
+
* can be deleted and created again, then this finds if the message is for current instance of directory or not.
|
|
1681
|
+
* @param msg - message for the directory
|
|
1682
|
+
*/
|
|
1683
|
+
isMessageForCurrentInstanceOfSubDirectory(msg) {
|
|
1684
|
+
// If the message is either from the creator of directory or this directory was created when
|
|
1685
|
+
// container was detached or in case this directory is already live(known to other clients)
|
|
1686
|
+
// and the op was created after the directory was created then apply this op.
|
|
1687
|
+
return ((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
|
|
1688
|
+
this.clientIds.has("detached") ||
|
|
1689
|
+
(this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber));
|
|
1690
|
+
}
|
|
1264
1691
|
/**
|
|
1265
1692
|
* If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
|
|
1266
1693
|
* not process the incoming operation.
|
|
@@ -1271,16 +1698,80 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1271
1698
|
* For messages from a remote client, this will be undefined.
|
|
1272
1699
|
* @returns True if the operation should be processed, false otherwise
|
|
1273
1700
|
*/
|
|
1274
|
-
needProcessSubDirectoryOperation(op, local, localOpMetadata) {
|
|
1275
|
-
|
|
1276
|
-
|
|
1701
|
+
needProcessSubDirectoryOperation(msg, op, local, localOpMetadata) {
|
|
1702
|
+
assertNonNullClientId(msg.clientId);
|
|
1703
|
+
const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
|
|
1704
|
+
const pendingCreateCount = this.pendingCreateSubDirectoriesTracker.get(op.subdirName);
|
|
1705
|
+
if ((pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
|
|
1706
|
+
(pendingCreateCount !== undefined && pendingCreateCount > 0)) {
|
|
1277
1707
|
if (local) {
|
|
1278
|
-
(0,
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1708
|
+
(0, core_utils_1.assert)(isSubDirLocalOpMetadata(localOpMetadata), 0x012 /* pendingMessageId is missing from the local client's operation */);
|
|
1709
|
+
if (localOpMetadata.type === "deleteSubDir") {
|
|
1710
|
+
(0, core_utils_1.assert)(pendingDeleteCount !== undefined && pendingDeleteCount > 0, 0x6c2 /* pendingDeleteCount should exist */);
|
|
1711
|
+
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
1712
|
+
}
|
|
1713
|
+
else if (localOpMetadata.type === "createSubDir") {
|
|
1714
|
+
(0, core_utils_1.assert)(pendingCreateCount !== undefined && pendingCreateCount > 0, 0x6c3 /* pendingCreateCount should exist */);
|
|
1715
|
+
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (op.type === "deleteSubDirectory") {
|
|
1719
|
+
const resetSubDirectoryTree = (directory) => {
|
|
1720
|
+
if (!directory) {
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
// If this is delete op and we have keys in this subDirectory, then we need to delete these
|
|
1724
|
+
// keys except the pending ones as they will be sequenced after this delete.
|
|
1725
|
+
directory.clearExceptPendingKeys(local);
|
|
1726
|
+
// In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
|
|
1727
|
+
// creators as the previous directory is getting deleted and we will initialize again when
|
|
1728
|
+
// we will receive op for the create again.
|
|
1729
|
+
directory.seqData.seq = -1;
|
|
1730
|
+
directory.seqData.clientSeq = -1;
|
|
1731
|
+
directory.clientIds.clear();
|
|
1732
|
+
// Do the same thing for the subtree of the directory. If create is not pending for a child, then just
|
|
1733
|
+
// delete it.
|
|
1734
|
+
const subDirectories = directory.subdirectories();
|
|
1735
|
+
for (const [subDirName, subDir] of subDirectories) {
|
|
1736
|
+
if (directory.pendingCreateSubDirectoriesTracker.has(subDirName)) {
|
|
1737
|
+
resetSubDirectoryTree(subDir);
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
directory.deleteSubDirectoryCore(subDirName, false);
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
const subDirectory = this._subdirectories.get(op.subdirName);
|
|
1744
|
+
// Clear the creation tracker record
|
|
1745
|
+
this.ackedCreationSeqTracker.delete(op.subdirName);
|
|
1746
|
+
resetSubDirectoryTree(subDirectory);
|
|
1747
|
+
}
|
|
1748
|
+
if (op.type === "createSubDirectory") {
|
|
1749
|
+
const dir = this._subdirectories.get(op.subdirName);
|
|
1750
|
+
// Child sub directory create seq number can't be lower than the parent subdirectory.
|
|
1751
|
+
// The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
|
|
1752
|
+
if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
|
|
1753
|
+
if (dir?.seqData.seq === -1) {
|
|
1754
|
+
// Only set the sequence data based on the first message
|
|
1755
|
+
dir.seqData.seq = msg.sequenceNumber;
|
|
1756
|
+
dir.seqData.clientSeq = msg.clientSequenceNumber;
|
|
1757
|
+
// set the creation seq in tracker
|
|
1758
|
+
if (!this.ackedCreationSeqTracker.has(op.subdirName) &&
|
|
1759
|
+
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
|
|
1760
|
+
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
1761
|
+
seq: msg.sequenceNumber,
|
|
1762
|
+
clientSeq: msg.clientSequenceNumber,
|
|
1763
|
+
});
|
|
1764
|
+
if (local) {
|
|
1765
|
+
this.localCreationSeqTracker.delete(op.subdirName);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
// The client created the dir at or after the dirs seq, so list its client id as a creator.
|
|
1770
|
+
if (dir !== undefined &&
|
|
1771
|
+
!dir.clientIds.has(msg.clientId) &&
|
|
1772
|
+
dir.seqData.seq <= msg.sequenceNumber) {
|
|
1773
|
+
dir.clientIds.add(msg.clientId);
|
|
1774
|
+
}
|
|
1284
1775
|
}
|
|
1285
1776
|
}
|
|
1286
1777
|
return false;
|
|
@@ -1290,18 +1781,21 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1290
1781
|
/**
|
|
1291
1782
|
* Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
|
|
1292
1783
|
*/
|
|
1293
|
-
clearExceptPendingKeys() {
|
|
1784
|
+
clearExceptPendingKeys(local) {
|
|
1294
1785
|
// Assuming the pendingKeys is small and the map is large
|
|
1295
1786
|
// we will get the value for the pendingKeys and clear the map
|
|
1296
1787
|
const temp = new Map();
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1788
|
+
for (const [key] of this.pendingKeys) {
|
|
1789
|
+
const value = this._storage.get(key);
|
|
1790
|
+
// If this key is already deleted, then we don't need to add it again.
|
|
1791
|
+
if (value !== undefined) {
|
|
1792
|
+
temp.set(key, value);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
this.clearCore(local);
|
|
1796
|
+
for (const [key, value] of temp.entries()) {
|
|
1303
1797
|
this.setCore(key, value, true);
|
|
1304
|
-
}
|
|
1798
|
+
}
|
|
1305
1799
|
}
|
|
1306
1800
|
/**
|
|
1307
1801
|
* Clear implementation used for both locally sourced clears as well as incoming remote clears.
|
|
@@ -1319,7 +1813,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1319
1813
|
*/
|
|
1320
1814
|
deleteCore(key, local) {
|
|
1321
1815
|
const previousLocalValue = this._storage.get(key);
|
|
1322
|
-
const previousValue = previousLocalValue
|
|
1816
|
+
const previousValue = previousLocalValue?.value;
|
|
1323
1817
|
const successfullyRemoved = this._storage.delete(key);
|
|
1324
1818
|
if (successfullyRemoved) {
|
|
1325
1819
|
const event = { key, path: this.absolutePath, previousValue };
|
|
@@ -1338,7 +1832,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1338
1832
|
*/
|
|
1339
1833
|
setCore(key, value, local) {
|
|
1340
1834
|
const previousLocalValue = this._storage.get(key);
|
|
1341
|
-
const previousValue = previousLocalValue
|
|
1835
|
+
const previousValue = previousLocalValue?.value;
|
|
1342
1836
|
this._storage.set(key, value);
|
|
1343
1837
|
const event = { key, path: this.absolutePath, previousValue };
|
|
1344
1838
|
this.directory.emit("valueChanged", event, local, this.directory);
|
|
@@ -1350,17 +1844,33 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1350
1844
|
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
1351
1845
|
* @param subdirName - The name of the subdirectory being created
|
|
1352
1846
|
* @param local - Whether the message originated from the local client
|
|
1353
|
-
* @
|
|
1847
|
+
* @param seqData - Sequence number and client sequence number at which this directory is created
|
|
1848
|
+
* @param clientId - Id of client which created this directory.
|
|
1849
|
+
* @returns True if is newly created, false if it already existed.
|
|
1354
1850
|
*/
|
|
1355
|
-
createSubDirectoryCore(subdirName, local) {
|
|
1356
|
-
|
|
1851
|
+
createSubDirectoryCore(subdirName, local, seqData, clientId) {
|
|
1852
|
+
const subdir = this._subdirectories.get(subdirName);
|
|
1853
|
+
if (subdir === undefined) {
|
|
1357
1854
|
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
1358
|
-
const subDir = new SubDirectory(this.directory, this.runtime, this.serializer, absolutePath);
|
|
1855
|
+
const subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath);
|
|
1856
|
+
/**
|
|
1857
|
+
* Store the sequnce numbers of newly created subdirectory to the proper creation tracker, based
|
|
1858
|
+
* on whether the creation behavior has been ack'd or not
|
|
1859
|
+
*/
|
|
1860
|
+
if (!isAcknowledgedOrDetached(seqData)) {
|
|
1861
|
+
this.localCreationSeqTracker.set(subdirName, { ...seqData });
|
|
1862
|
+
}
|
|
1863
|
+
else {
|
|
1864
|
+
this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
|
|
1865
|
+
}
|
|
1359
1866
|
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
1360
1867
|
this._subdirectories.set(subdirName, subDir);
|
|
1361
1868
|
this.emit("subDirectoryCreated", subdirName, local, this);
|
|
1362
1869
|
return true;
|
|
1363
1870
|
}
|
|
1871
|
+
else {
|
|
1872
|
+
subdir.clientIds.add(clientId);
|
|
1873
|
+
}
|
|
1364
1874
|
return false;
|
|
1365
1875
|
}
|
|
1366
1876
|
registerEventsOnSubDirectory(subDirectory, subDirName) {
|
|
@@ -1382,6 +1892,16 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1382
1892
|
// Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
|
|
1383
1893
|
if (previousValue !== undefined) {
|
|
1384
1894
|
this._subdirectories.delete(subdirName);
|
|
1895
|
+
/**
|
|
1896
|
+
* Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
|
|
1897
|
+
* ack'd already or still not committed yet (could be both).
|
|
1898
|
+
*/
|
|
1899
|
+
if (this.ackedCreationSeqTracker.has(subdirName)) {
|
|
1900
|
+
this.ackedCreationSeqTracker.delete(subdirName);
|
|
1901
|
+
}
|
|
1902
|
+
if (this.localCreationSeqTracker.has(subdirName)) {
|
|
1903
|
+
this.localCreationSeqTracker.delete(subdirName);
|
|
1904
|
+
}
|
|
1385
1905
|
this.disposeSubDirectoryTree(previousValue);
|
|
1386
1906
|
this.emit("subDirectoryDeleted", subdirName, local, this);
|
|
1387
1907
|
}
|
|
@@ -1401,11 +1921,12 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
|
|
|
1401
1921
|
}
|
|
1402
1922
|
}
|
|
1403
1923
|
undeleteSubDirectoryTree(directory) {
|
|
1404
|
-
// Restore deleted subdirectory tree.
|
|
1405
|
-
|
|
1924
|
+
// Restore deleted subdirectory tree. Need to undispose the current directory first, then get access to the iterator.
|
|
1925
|
+
// This will unmark "deleted" from the subdirectories from top to bottom.
|
|
1926
|
+
directory.undispose();
|
|
1927
|
+
for (const [_, subDirectory] of directory.subdirectories()) {
|
|
1406
1928
|
this.undeleteSubDirectoryTree(subDirectory);
|
|
1407
1929
|
}
|
|
1408
|
-
directory.undispose();
|
|
1409
1930
|
}
|
|
1410
1931
|
}
|
|
1411
|
-
//# sourceMappingURL=directory.
|
|
1932
|
+
//# sourceMappingURL=directory.cjs.map
|