@fluidframework/map 1.4.0-121020 → 2.0.0-dev-rc.1.0.0.224419
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-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} +167 -184
- 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} +18 -7
- 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 +982 -0
- package/lib/map-beta.d.mts +275 -0
- package/lib/map-public.d.mts +275 -0
- package/lib/map-untrimmed.d.mts +996 -0
- package/lib/{map.d.ts → map.d.mts} +11 -18
- 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} +18 -49
- 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} +1 -1
- package/lib/{packageVersion.d.ts.map → packageVersion.d.mts.map} +1 -1
- package/lib/{packageVersion.js → packageVersion.mjs} +2 -2
- package/lib/packageVersion.mjs.map +1 -0
- package/map.test-files.tar +0 -0
- package/package.json +105 -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.js.map +0 -1
- package/tsconfig.esnext.json +0 -7
|
@@ -3,22 +3,26 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
var _a, _b;
|
|
6
|
-
import { assert
|
|
7
|
-
import {
|
|
6
|
+
import { assert } from "@fluidframework/core-utils";
|
|
7
|
+
import { TypedEventEmitter } from "@fluid-internal/client-utils";
|
|
8
|
+
import { UsageError } from "@fluidframework/telemetry-utils";
|
|
8
9
|
import { readAndParse } from "@fluidframework/driver-utils";
|
|
9
|
-
import { MessageType
|
|
10
|
+
import { MessageType } from "@fluidframework/protocol-definitions";
|
|
10
11
|
import { SharedObject, ValueType } from "@fluidframework/shared-object-base";
|
|
11
12
|
import { SummaryTreeBuilder } from "@fluidframework/runtime-utils";
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import path from "path-browserify";
|
|
14
|
+
import { RedBlackTree } from "@fluidframework/merge-tree";
|
|
15
|
+
import { LocalValueMaker, makeSerializable } from "./localValues.mjs";
|
|
16
|
+
import { pkgVersion } from "./packageVersion.mjs";
|
|
15
17
|
// We use path-browserify since this code can run safely on the server or the browser.
|
|
16
18
|
// We standardize on using posix slashes everywhere.
|
|
17
19
|
const posix = path.posix;
|
|
18
20
|
const snapshotFileName = "header";
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
22
|
+
* {@link @fluidframework/datastore-definitions#IChannelFactory} for {@link SharedDirectory}.
|
|
23
|
+
*
|
|
21
24
|
* @sealed
|
|
25
|
+
* @alpha
|
|
22
26
|
*/
|
|
23
27
|
export class DirectoryFactory {
|
|
24
28
|
/**
|
|
@@ -63,11 +67,104 @@ DirectoryFactory.Attributes = {
|
|
|
63
67
|
packageVersion: pkgVersion,
|
|
64
68
|
};
|
|
65
69
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
70
|
+
* The comparator essentially performs the following procedure to determine the order of subdirectory creation:
|
|
71
|
+
* 1. If subdirectory A has a non-negative 'seq' and subdirectory B has a negative 'seq', subdirectory A is always placed first due to
|
|
72
|
+
* the policy that acknowledged subdirectories precede locally created ones that have not been committed yet.
|
|
73
|
+
*
|
|
74
|
+
* 2. When both subdirectories A and B have a non-negative 'seq', they are compared as follows:
|
|
75
|
+
* - 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
|
|
76
|
+
* should not be applied in the directory ordering, since the lowest 'seq' is -1, when the directory is created locally but not acknowledged yet.
|
|
77
|
+
* - 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
|
|
78
|
+
* batching is enabled, and a lower 'clientSeq' indicates that it was processed earlier after the batch was ungrouped.
|
|
79
|
+
*
|
|
80
|
+
* 3. When both subdirectories A and B have a negative 'seq', they are compared as follows:
|
|
81
|
+
* - If A and B have different 'seq', the one with lower 'seq' will be positioned ahead, which indicates the corresponding creation message was
|
|
82
|
+
* acknowledged by the server earlier.
|
|
83
|
+
* - 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
|
|
84
|
+
* and B were created locally and not acknowledged yet, with the one possessing the lower 'clientSeq' being created earlier.
|
|
85
|
+
*
|
|
86
|
+
* 4. A 'seq' value of zero indicates that the subdirectory was created in detached state, and it is considered acknowledged for the
|
|
87
|
+
* purpose of ordering.
|
|
88
|
+
*/
|
|
89
|
+
const seqDataComparator = (a, b) => {
|
|
90
|
+
if (isAcknowledgedOrDetached(a)) {
|
|
91
|
+
if (isAcknowledgedOrDetached(b)) {
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
93
|
+
return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq - b.clientSeq;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
return -1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (!isAcknowledgedOrDetached(b)) {
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
102
|
+
return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq - b.clientSeq;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
function isAcknowledgedOrDetached(seqData) {
|
|
110
|
+
return seqData.seq >= 0;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A utility class for tracking associations between keys and their creation indices.
|
|
114
|
+
* This is relevant to support map iteration in insertion order, see
|
|
115
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
|
|
116
|
+
*
|
|
117
|
+
* TODO: It can be combined with the creation tracker utilized in SharedMap
|
|
118
|
+
*/
|
|
119
|
+
class DirectoryCreationTracker {
|
|
120
|
+
constructor() {
|
|
121
|
+
this.indexToKey = new RedBlackTree(seqDataComparator);
|
|
122
|
+
this.keyToIndex = new Map();
|
|
123
|
+
}
|
|
124
|
+
set(key, seqData) {
|
|
125
|
+
this.indexToKey.put(seqData, key);
|
|
126
|
+
this.keyToIndex.set(key, seqData);
|
|
127
|
+
}
|
|
128
|
+
has(keyOrSeqData) {
|
|
129
|
+
return typeof keyOrSeqData === "string"
|
|
130
|
+
? this.keyToIndex.has(keyOrSeqData)
|
|
131
|
+
: this.indexToKey.get(keyOrSeqData) !== undefined;
|
|
132
|
+
}
|
|
133
|
+
delete(keyOrSeqData) {
|
|
134
|
+
if (this.has(keyOrSeqData)) {
|
|
135
|
+
if (typeof keyOrSeqData === "string") {
|
|
136
|
+
const seqData = this.keyToIndex.get(keyOrSeqData);
|
|
137
|
+
this.keyToIndex.delete(keyOrSeqData);
|
|
138
|
+
this.indexToKey.remove(seqData);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const key = this.indexToKey.get(keyOrSeqData)?.data;
|
|
142
|
+
this.indexToKey.remove(keyOrSeqData);
|
|
143
|
+
this.keyToIndex.delete(key);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Retrieves all subdirectories with creation order that satisfy an optional constraint function.
|
|
149
|
+
* @param constraint - An optional constraint function that filters keys.
|
|
150
|
+
* @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
|
|
151
|
+
*/
|
|
152
|
+
keys(constraint) {
|
|
153
|
+
const keys = [];
|
|
154
|
+
this.indexToKey.mapRange((node) => {
|
|
155
|
+
if (!constraint || constraint(node.data)) {
|
|
156
|
+
keys.push(node.data);
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}, keys);
|
|
160
|
+
return keys;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* {@inheritDoc ISharedDirectory}
|
|
69
165
|
*
|
|
70
166
|
* @example
|
|
167
|
+
*
|
|
71
168
|
* ```typescript
|
|
72
169
|
* mySharedDirectory.createSubDirectory("a").createSubDirectory("b").createSubDirectory("c").set("foo", val1);
|
|
73
170
|
* const mySubDir = mySharedDirectory.getWorkingDirectory("/a/b/c");
|
|
@@ -75,8 +172,33 @@ DirectoryFactory.Attributes = {
|
|
|
75
172
|
* ```
|
|
76
173
|
*
|
|
77
174
|
* @sealed
|
|
175
|
+
* @alpha
|
|
78
176
|
*/
|
|
79
177
|
export class SharedDirectory extends SharedObject {
|
|
178
|
+
/**
|
|
179
|
+
* Create a new shared directory
|
|
180
|
+
*
|
|
181
|
+
* @param runtime - Data store runtime the new shared directory belongs to
|
|
182
|
+
* @param id - Optional name of the shared directory
|
|
183
|
+
* @returns Newly create shared directory (but not attached yet)
|
|
184
|
+
*/
|
|
185
|
+
static create(runtime, id) {
|
|
186
|
+
return runtime.createChannel(id, DirectoryFactory.Type);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get a factory for SharedDirectory to register with the data store.
|
|
190
|
+
*
|
|
191
|
+
* @returns A factory that creates and load SharedDirectory
|
|
192
|
+
*/
|
|
193
|
+
static getFactory() {
|
|
194
|
+
return new DirectoryFactory();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* {@inheritDoc IDirectory.absolutePath}
|
|
198
|
+
*/
|
|
199
|
+
get absolutePath() {
|
|
200
|
+
return this.root.absolutePath;
|
|
201
|
+
}
|
|
80
202
|
/**
|
|
81
203
|
* Constructs a new shared directory. If the object is non-local an id and service interfaces will
|
|
82
204
|
* be provided.
|
|
@@ -93,7 +215,7 @@ export class SharedDirectory extends SharedObject {
|
|
|
93
215
|
/**
|
|
94
216
|
* Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
|
|
95
217
|
*/
|
|
96
|
-
this.root = new SubDirectory(this, this.runtime, this.serializer, posix.sep);
|
|
218
|
+
this.root = new SubDirectory({ seq: 0, clientSeq: 0 }, new Set(), this, this.runtime, this.serializer, posix.sep);
|
|
97
219
|
/**
|
|
98
220
|
* Mapping of op types to message handlers.
|
|
99
221
|
*/
|
|
@@ -111,33 +233,11 @@ export class SharedDirectory extends SharedObject {
|
|
|
111
233
|
this.emit("subDirectoryDeleted", relativePath, local, this);
|
|
112
234
|
});
|
|
113
235
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Create a new shared directory
|
|
116
|
-
*
|
|
117
|
-
* @param runtime - Data store runtime the new shared directory belongs to
|
|
118
|
-
* @param id - Optional name of the shared directory
|
|
119
|
-
* @returns Newly create shared directory (but not attached yet)
|
|
120
|
-
*/
|
|
121
|
-
static create(runtime, id) {
|
|
122
|
-
return runtime.createChannel(id, DirectoryFactory.Type);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Get a factory for SharedDirectory to register with the data store.
|
|
126
|
-
*
|
|
127
|
-
* @returns A factory that creates and load SharedDirectory
|
|
128
|
-
*/
|
|
129
|
-
static getFactory() {
|
|
130
|
-
return new DirectoryFactory();
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* {@inheritDoc IDirectory.absolutePath}
|
|
134
|
-
*/
|
|
135
|
-
get absolutePath() {
|
|
136
|
-
return this.root.absolutePath;
|
|
137
|
-
}
|
|
138
236
|
/**
|
|
139
237
|
* {@inheritDoc IDirectory.get}
|
|
140
238
|
*/
|
|
239
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
240
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
141
241
|
get(key) {
|
|
142
242
|
return this.root.get(key);
|
|
143
243
|
}
|
|
@@ -186,13 +286,18 @@ export class SharedDirectory extends SharedObject {
|
|
|
186
286
|
* Issue a callback on each entry under this IDirectory.
|
|
187
287
|
* @param callback - Callback to issue
|
|
188
288
|
*/
|
|
289
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
290
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
189
291
|
forEach(callback) {
|
|
292
|
+
// eslint-disable-next-line unicorn/no-array-for-each, unicorn/no-array-callback-reference
|
|
190
293
|
this.root.forEach(callback);
|
|
191
294
|
}
|
|
192
295
|
/**
|
|
193
296
|
* Get an iterator over the entries under this IDirectory.
|
|
194
297
|
* @returns The iterator
|
|
195
298
|
*/
|
|
299
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
300
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
196
301
|
[(_a = Symbol.toStringTag, Symbol.iterator)]() {
|
|
197
302
|
return this.root[Symbol.iterator]();
|
|
198
303
|
}
|
|
@@ -200,6 +305,8 @@ export class SharedDirectory extends SharedObject {
|
|
|
200
305
|
* Get an iterator over the entries under this IDirectory.
|
|
201
306
|
* @returns The iterator
|
|
202
307
|
*/
|
|
308
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
309
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
203
310
|
entries() {
|
|
204
311
|
return this.root.entries();
|
|
205
312
|
}
|
|
@@ -220,6 +327,8 @@ export class SharedDirectory extends SharedObject {
|
|
|
220
327
|
* Get an iterator over the values under this IDirectory.
|
|
221
328
|
* @returns The iterator
|
|
222
329
|
*/
|
|
330
|
+
// TODO: Use `unknown` instead (breaking change).
|
|
331
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
223
332
|
values() {
|
|
224
333
|
return this.root.values();
|
|
225
334
|
}
|
|
@@ -262,7 +371,7 @@ export class SharedDirectory extends SharedObject {
|
|
|
262
371
|
return this.root;
|
|
263
372
|
}
|
|
264
373
|
let currentSubDir = this.root;
|
|
265
|
-
const subdirs = absolutePath.
|
|
374
|
+
const subdirs = absolutePath.slice(1).split(posix.sep);
|
|
266
375
|
for (const subdir of subdirs) {
|
|
267
376
|
currentSubDir = currentSubDir.getSubDirectory(subdir);
|
|
268
377
|
if (!currentSubDir) {
|
|
@@ -273,7 +382,6 @@ export class SharedDirectory extends SharedObject {
|
|
|
273
382
|
}
|
|
274
383
|
/**
|
|
275
384
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
|
|
276
|
-
* @internal
|
|
277
385
|
*/
|
|
278
386
|
summarizeCore(serializer, telemetryContext) {
|
|
279
387
|
return this.serializeDirectory(this.root, serializer);
|
|
@@ -283,19 +391,16 @@ export class SharedDirectory extends SharedObject {
|
|
|
283
391
|
* @param op - Op to submit
|
|
284
392
|
* @param localOpMetadata - The local metadata associated with the op. We send a unique id that is used to track
|
|
285
393
|
* this op while it has not been ack'd. This will be sent when we receive this op back from the server.
|
|
286
|
-
* @internal
|
|
287
394
|
*/
|
|
288
395
|
submitDirectoryMessage(op, localOpMetadata) {
|
|
289
396
|
this.submitLocalMessage(op, localOpMetadata);
|
|
290
397
|
}
|
|
291
398
|
/**
|
|
292
399
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
|
|
293
|
-
* @internal
|
|
294
400
|
*/
|
|
295
401
|
onDisconnect() { }
|
|
296
402
|
/**
|
|
297
403
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
|
|
298
|
-
* @internal
|
|
299
404
|
*/
|
|
300
405
|
reSubmitCore(content, localOpMetadata) {
|
|
301
406
|
const message = content;
|
|
@@ -305,7 +410,6 @@ export class SharedDirectory extends SharedObject {
|
|
|
305
410
|
}
|
|
306
411
|
/**
|
|
307
412
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
|
|
308
|
-
* @internal
|
|
309
413
|
*/
|
|
310
414
|
async loadCore(storage) {
|
|
311
415
|
const data = await readAndParse(storage, snapshotFileName);
|
|
@@ -326,7 +430,6 @@ export class SharedDirectory extends SharedObject {
|
|
|
326
430
|
/**
|
|
327
431
|
* Populate the directory with the given directory data.
|
|
328
432
|
* @param data - A JSON string containing serialized directory data
|
|
329
|
-
* @internal
|
|
330
433
|
*/
|
|
331
434
|
populate(data) {
|
|
332
435
|
const stack = [];
|
|
@@ -335,11 +438,42 @@ export class SharedDirectory extends SharedObject {
|
|
|
335
438
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
336
439
|
const [currentSubDir, currentSubDirObject] = stack.pop();
|
|
337
440
|
if (currentSubDirObject.subdirectories) {
|
|
441
|
+
// Utilize a map to store the seq -> clientSeq for the newly created subdirectory
|
|
442
|
+
const tempSeqNums = new Map();
|
|
338
443
|
for (const [subdirName, subdirObject] of Object.entries(currentSubDirObject.subdirectories)) {
|
|
339
444
|
let newSubDir = currentSubDir.getSubDirectory(subdirName);
|
|
445
|
+
let seqData;
|
|
340
446
|
if (!newSubDir) {
|
|
341
|
-
|
|
447
|
+
const createInfo = subdirObject.ci;
|
|
448
|
+
// We do not store the client sequence number in the storage because the order has already been
|
|
449
|
+
// guaranteed during the serialization process. As a result, it is only essential to utilize the
|
|
450
|
+
// "fake" client sequence number to signify the loading order, and there is no need to retain
|
|
451
|
+
// the actual client sequence number at this point.
|
|
452
|
+
if (createInfo !== undefined && createInfo.csn > -1) {
|
|
453
|
+
// If csn is -1, then initialize it with 0, otherwise we will never process ops for this
|
|
454
|
+
// sub directory. This could be done at serialization time too, but we need to maintain
|
|
455
|
+
// back compat too and also we will actually know the state when it was serialized.
|
|
456
|
+
if (!tempSeqNums.has(createInfo.csn)) {
|
|
457
|
+
tempSeqNums.set(createInfo.csn, 0);
|
|
458
|
+
}
|
|
459
|
+
let fakeClientSeq = tempSeqNums.get(createInfo.csn);
|
|
460
|
+
seqData = { seq: createInfo.csn, clientSeq: fakeClientSeq };
|
|
461
|
+
tempSeqNums.set(createInfo.csn, ++fakeClientSeq);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
seqData = {
|
|
465
|
+
seq: 0,
|
|
466
|
+
clientSeq: ++currentSubDir.localCreationSeq,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
newSubDir = new SubDirectory(seqData, createInfo !== undefined
|
|
470
|
+
? new Set(createInfo.ccIds)
|
|
471
|
+
: new Set(), this, this.runtime, this.serializer, posix.join(currentSubDir.absolutePath, subdirName));
|
|
342
472
|
currentSubDir.populateSubDirectory(subdirName, newSubDir);
|
|
473
|
+
// Record the newly inserted subdirectory to the creation tracker
|
|
474
|
+
currentSubDir.ackedCreationSeqTracker.set(subdirName, {
|
|
475
|
+
...seqData,
|
|
476
|
+
});
|
|
343
477
|
}
|
|
344
478
|
stack.push([newSubDir, subdirObject]);
|
|
345
479
|
}
|
|
@@ -354,20 +488,18 @@ export class SharedDirectory extends SharedObject {
|
|
|
354
488
|
}
|
|
355
489
|
/**
|
|
356
490
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
|
|
357
|
-
* @internal
|
|
358
491
|
*/
|
|
359
492
|
processCore(message, local, localOpMetadata) {
|
|
360
493
|
if (message.type === MessageType.Operation) {
|
|
361
494
|
const op = message.contents;
|
|
362
495
|
const handler = this.messageHandlers.get(op.type);
|
|
363
496
|
assert(handler !== undefined, 0x00e /* Missing message handler for message type */);
|
|
364
|
-
handler.process(op, local, localOpMetadata);
|
|
497
|
+
handler.process(message, op, local, localOpMetadata);
|
|
365
498
|
}
|
|
366
499
|
}
|
|
367
500
|
/**
|
|
368
501
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
|
|
369
|
-
|
|
370
|
-
*/
|
|
502
|
+
*/
|
|
371
503
|
rollback(content, localOpMetadata) {
|
|
372
504
|
const op = content;
|
|
373
505
|
const subdir = this.getWorkingDirectory(op.path);
|
|
@@ -392,19 +524,50 @@ export class SharedDirectory extends SharedObject {
|
|
|
392
524
|
* @param serializable - The remote information that we can convert into a real object
|
|
393
525
|
* @returns The local value that was produced
|
|
394
526
|
*/
|
|
395
|
-
makeLocal(key, absolutePath,
|
|
396
|
-
|
|
527
|
+
makeLocal(key, absolutePath,
|
|
528
|
+
// eslint-disable-next-line import/no-deprecated
|
|
529
|
+
serializable) {
|
|
530
|
+
assert(serializable.type === ValueType[ValueType.Plain] ||
|
|
531
|
+
serializable.type === ValueType[ValueType.Shared], 0x1e4 /* "Unexpected serializable type" */);
|
|
397
532
|
return this.localValueMaker.fromSerializable(serializable);
|
|
398
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* This checks if there is pending delete op for local delete for a any subdir in the relative path.
|
|
536
|
+
* @param relativePath - path of sub directory.
|
|
537
|
+
* @returns `true` if there is pending delete, `false` otherwise.
|
|
538
|
+
*/
|
|
539
|
+
isSubDirectoryDeletePending(relativePath) {
|
|
540
|
+
const absolutePath = this.makeAbsolute(relativePath);
|
|
541
|
+
if (absolutePath === posix.sep) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
let currentParent = this.root;
|
|
545
|
+
const nodeList = absolutePath.split(posix.sep);
|
|
546
|
+
let start = 1;
|
|
547
|
+
while (start < nodeList.length) {
|
|
548
|
+
const subDirName = nodeList[start];
|
|
549
|
+
if (currentParent.isSubDirectoryDeletePending(subDirName)) {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
currentParent = currentParent.getSubDirectory(subDirName);
|
|
553
|
+
if (currentParent === undefined) {
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
start += 1;
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
399
560
|
/**
|
|
400
561
|
* Set the message handlers for the directory.
|
|
401
562
|
*/
|
|
402
563
|
setMessageHandlers() {
|
|
403
564
|
this.messageHandlers.set("clear", {
|
|
404
|
-
process: (op, local, localOpMetadata) => {
|
|
565
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
405
566
|
const subdir = this.getWorkingDirectory(op.path);
|
|
406
|
-
|
|
407
|
-
|
|
567
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
568
|
+
// as we are going to delete this subDirectory.
|
|
569
|
+
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
570
|
+
subdir.processClearMessage(msg, op, local, localOpMetadata);
|
|
408
571
|
}
|
|
409
572
|
},
|
|
410
573
|
submit: (op, localOpMetadata) => {
|
|
@@ -413,12 +576,20 @@ export class SharedDirectory extends SharedObject {
|
|
|
413
576
|
subdir.resubmitClearMessage(op, localOpMetadata);
|
|
414
577
|
}
|
|
415
578
|
},
|
|
579
|
+
applyStashedOp: (op) => {
|
|
580
|
+
const subdir = this.getWorkingDirectory(op.path);
|
|
581
|
+
if (subdir) {
|
|
582
|
+
return subdir.applyStashedClearMessage(op);
|
|
583
|
+
}
|
|
584
|
+
},
|
|
416
585
|
});
|
|
417
586
|
this.messageHandlers.set("delete", {
|
|
418
|
-
process: (op, local, localOpMetadata) => {
|
|
587
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
419
588
|
const subdir = this.getWorkingDirectory(op.path);
|
|
420
|
-
|
|
421
|
-
|
|
589
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
590
|
+
// as we are going to delete this subDirectory.
|
|
591
|
+
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
592
|
+
subdir.processDeleteMessage(msg, op, local, localOpMetadata);
|
|
422
593
|
}
|
|
423
594
|
},
|
|
424
595
|
submit: (op, localOpMetadata) => {
|
|
@@ -427,13 +598,21 @@ export class SharedDirectory extends SharedObject {
|
|
|
427
598
|
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
428
599
|
}
|
|
429
600
|
},
|
|
601
|
+
applyStashedOp: (op) => {
|
|
602
|
+
const subdir = this.getWorkingDirectory(op.path);
|
|
603
|
+
if (subdir) {
|
|
604
|
+
return subdir.applyStashedDeleteMessage(op);
|
|
605
|
+
}
|
|
606
|
+
},
|
|
430
607
|
});
|
|
431
608
|
this.messageHandlers.set("set", {
|
|
432
|
-
process: (op, local, localOpMetadata) => {
|
|
609
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
433
610
|
const subdir = this.getWorkingDirectory(op.path);
|
|
434
|
-
|
|
611
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
612
|
+
// as we are going to delete this subDirectory.
|
|
613
|
+
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
435
614
|
const context = local ? undefined : this.makeLocal(op.key, op.path, op.value);
|
|
436
|
-
subdir.processSetMessage(op, context, local, localOpMetadata);
|
|
615
|
+
subdir.processSetMessage(msg, op, context, local, localOpMetadata);
|
|
437
616
|
}
|
|
438
617
|
},
|
|
439
618
|
submit: (op, localOpMetadata) => {
|
|
@@ -442,12 +621,21 @@ export class SharedDirectory extends SharedObject {
|
|
|
442
621
|
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
443
622
|
}
|
|
444
623
|
},
|
|
624
|
+
applyStashedOp: (op) => {
|
|
625
|
+
const subdir = this.getWorkingDirectory(op.path);
|
|
626
|
+
if (subdir) {
|
|
627
|
+
const context = this.makeLocal(op.key, op.path, op.value);
|
|
628
|
+
return subdir.applyStashedSetMessage(op, context);
|
|
629
|
+
}
|
|
630
|
+
},
|
|
445
631
|
});
|
|
446
632
|
this.messageHandlers.set("createSubDirectory", {
|
|
447
|
-
process: (op, local, localOpMetadata) => {
|
|
633
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
448
634
|
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
449
|
-
|
|
450
|
-
|
|
635
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
636
|
+
// as we are going to delete this subDirectory.
|
|
637
|
+
if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
638
|
+
parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
451
639
|
}
|
|
452
640
|
},
|
|
453
641
|
submit: (op, localOpMetadata) => {
|
|
@@ -457,12 +645,20 @@ export class SharedDirectory extends SharedObject {
|
|
|
457
645
|
parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
458
646
|
}
|
|
459
647
|
},
|
|
648
|
+
applyStashedOp: (op) => {
|
|
649
|
+
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
650
|
+
if (parentSubdir) {
|
|
651
|
+
return parentSubdir.applyStashedCreateSubDirMessage(op);
|
|
652
|
+
}
|
|
653
|
+
},
|
|
460
654
|
});
|
|
461
655
|
this.messageHandlers.set("deleteSubDirectory", {
|
|
462
|
-
process: (op, local, localOpMetadata) => {
|
|
656
|
+
process: (msg, op, local, localOpMetadata) => {
|
|
463
657
|
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
464
|
-
|
|
465
|
-
|
|
658
|
+
// If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
|
|
659
|
+
// as we are going to delete this subDirectory.
|
|
660
|
+
if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
661
|
+
parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
466
662
|
}
|
|
467
663
|
},
|
|
468
664
|
submit: (op, localOpMetadata) => {
|
|
@@ -472,13 +668,23 @@ export class SharedDirectory extends SharedObject {
|
|
|
472
668
|
parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
473
669
|
}
|
|
474
670
|
},
|
|
671
|
+
applyStashedOp: (op) => {
|
|
672
|
+
const parentSubdir = this.getWorkingDirectory(op.path);
|
|
673
|
+
if (parentSubdir) {
|
|
674
|
+
return parentSubdir.applyStashedDeleteSubDirMessage(op);
|
|
675
|
+
}
|
|
676
|
+
},
|
|
475
677
|
});
|
|
476
678
|
}
|
|
477
679
|
/**
|
|
478
|
-
* @
|
|
680
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
|
|
479
681
|
*/
|
|
480
|
-
applyStashedOp() {
|
|
481
|
-
|
|
682
|
+
applyStashedOp(op) {
|
|
683
|
+
const handler = this.messageHandlers.get(op.type);
|
|
684
|
+
if (handler === undefined) {
|
|
685
|
+
throw new Error("no apply stashed op handler");
|
|
686
|
+
}
|
|
687
|
+
return handler.applyStashedOp(op);
|
|
482
688
|
}
|
|
483
689
|
serializeDirectory(root, serializer, telemetryContext) {
|
|
484
690
|
const MinValueSizeSeparateSnapshotBlob = 8 * 1024;
|
|
@@ -491,20 +697,21 @@ export class SharedDirectory extends SharedObject {
|
|
|
491
697
|
while (stack.length > 0) {
|
|
492
698
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
493
699
|
const [currentSubDir, currentSubDirObject] = stack.pop();
|
|
700
|
+
currentSubDirObject.ci = currentSubDir.getSerializableCreateInfo();
|
|
494
701
|
for (const [key, value] of currentSubDir.getSerializedStorage(serializer)) {
|
|
495
702
|
if (!currentSubDirObject.storage) {
|
|
496
703
|
currentSubDirObject.storage = {};
|
|
497
704
|
}
|
|
705
|
+
// eslint-disable-next-line import/no-deprecated
|
|
498
706
|
const result = {
|
|
499
707
|
type: value.type,
|
|
500
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
501
708
|
value: value.value && JSON.parse(value.value),
|
|
502
709
|
};
|
|
503
710
|
if (value.value && value.value.length >= MinValueSizeSeparateSnapshotBlob) {
|
|
504
711
|
const extraContent = {};
|
|
505
712
|
let largeContent = extraContent;
|
|
506
713
|
if (currentSubDir.absolutePath !== posix.sep) {
|
|
507
|
-
for (const dir of currentSubDir.absolutePath.
|
|
714
|
+
for (const dir of currentSubDir.absolutePath.slice(1).split(posix.sep)) {
|
|
508
715
|
const subDataObject = {};
|
|
509
716
|
largeContent.subdirectories = { [dir]: subDataObject };
|
|
510
717
|
largeContent = subDataObject;
|
|
@@ -537,23 +744,30 @@ export class SharedDirectory extends SharedObject {
|
|
|
537
744
|
return builder.getSummaryTree();
|
|
538
745
|
}
|
|
539
746
|
}
|
|
747
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
540
748
|
function isKeyEditLocalOpMetadata(metadata) {
|
|
541
|
-
return metadata !== undefined &&
|
|
749
|
+
return (metadata !== undefined &&
|
|
750
|
+
typeof metadata.pendingMessageId === "number" &&
|
|
751
|
+
metadata.type === "edit");
|
|
542
752
|
}
|
|
543
753
|
function isClearLocalOpMetadata(metadata) {
|
|
544
|
-
return metadata !== undefined &&
|
|
545
|
-
|
|
754
|
+
return (metadata !== undefined &&
|
|
755
|
+
metadata.type === "clear" &&
|
|
756
|
+
typeof metadata.pendingMessageId === "number" &&
|
|
757
|
+
typeof metadata.previousStorage === "object");
|
|
546
758
|
}
|
|
547
759
|
function isSubDirLocalOpMetadata(metadata) {
|
|
548
|
-
return metadata !== undefined &&
|
|
549
|
-
(
|
|
550
|
-
metadata.type === "deleteSubDir");
|
|
760
|
+
return (metadata !== undefined &&
|
|
761
|
+
(metadata.type === "createSubDir" || metadata.type === "deleteSubDir"));
|
|
551
762
|
}
|
|
552
763
|
function isDirectoryLocalOpMetadata(metadata) {
|
|
553
|
-
return metadata
|
|
554
|
-
(metadata
|
|
555
|
-
|
|
556
|
-
|
|
764
|
+
return (isKeyEditLocalOpMetadata(metadata) ||
|
|
765
|
+
isClearLocalOpMetadata(metadata) ||
|
|
766
|
+
isSubDirLocalOpMetadata(metadata));
|
|
767
|
+
}
|
|
768
|
+
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
769
|
+
function assertNonNullClientId(clientId) {
|
|
770
|
+
assert(clientId !== null, 0x6af /* client id should never be null */);
|
|
557
771
|
}
|
|
558
772
|
/**
|
|
559
773
|
* Node of the directory tree.
|
|
@@ -562,13 +776,17 @@ function isDirectoryLocalOpMetadata(metadata) {
|
|
|
562
776
|
class SubDirectory extends TypedEventEmitter {
|
|
563
777
|
/**
|
|
564
778
|
* Constructor.
|
|
779
|
+
* @param sequenceNumber - Message seq number at which this was created.
|
|
780
|
+
* @param clientIds - Ids of client which created this directory.
|
|
565
781
|
* @param directory - Reference back to the SharedDirectory to perform operations
|
|
566
782
|
* @param runtime - The data store runtime this directory is associated with
|
|
567
783
|
* @param serializer - The serializer to serialize / parse handles
|
|
568
784
|
* @param absolutePath - The absolute path of this IDirectory
|
|
569
785
|
*/
|
|
570
|
-
constructor(directory, runtime, serializer, absolutePath) {
|
|
786
|
+
constructor(seqData, clientIds, directory, runtime, serializer, absolutePath) {
|
|
571
787
|
super();
|
|
788
|
+
this.seqData = seqData;
|
|
789
|
+
this.clientIds = clientIds;
|
|
572
790
|
this.directory = directory;
|
|
573
791
|
this.runtime = runtime;
|
|
574
792
|
this.serializer = serializer;
|
|
@@ -590,13 +808,24 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
590
808
|
*/
|
|
591
809
|
this._subdirectories = new Map();
|
|
592
810
|
/**
|
|
593
|
-
* Keys that have been modified locally but not yet ack'd from the server.
|
|
811
|
+
* Keys that have been modified locally but not yet ack'd from the server. This is for operations on keys like
|
|
812
|
+
* set/delete operations on keys. The value of this map is list of pendingMessageIds at which that key
|
|
813
|
+
* was modified. We don't store the type of ops, and behaviour of key ops are different from behaviour of sub
|
|
814
|
+
* directory ops, so we have separate map from subDirectories tracker.
|
|
594
815
|
*/
|
|
595
816
|
this.pendingKeys = new Map();
|
|
596
817
|
/**
|
|
597
|
-
* Subdirectories that have been
|
|
818
|
+
* Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
|
|
819
|
+
* of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
820
|
+
* deleted sub directory.
|
|
821
|
+
*/
|
|
822
|
+
this.pendingDeleteSubDirectoriesTracker = new Map();
|
|
823
|
+
/**
|
|
824
|
+
* Subdirectories that have been created locally but not yet ack'd from the server. This maintains the record
|
|
825
|
+
* of create op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
826
|
+
* created sub directory.
|
|
598
827
|
*/
|
|
599
|
-
this.
|
|
828
|
+
this.pendingCreateSubDirectoriesTracker = new Map();
|
|
600
829
|
/**
|
|
601
830
|
* This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
|
|
602
831
|
*/
|
|
@@ -605,16 +834,24 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
605
834
|
* The pending ids of any clears that have been performed locally but not yet ack'd from the server
|
|
606
835
|
*/
|
|
607
836
|
this.pendingClearMessageIds = [];
|
|
837
|
+
/**
|
|
838
|
+
* Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
|
|
839
|
+
* of the creation order.
|
|
840
|
+
*/
|
|
841
|
+
this.localCreationSeq = 0;
|
|
842
|
+
this.localCreationSeqTracker = new DirectoryCreationTracker();
|
|
843
|
+
this.ackedCreationSeqTracker = new DirectoryCreationTracker();
|
|
608
844
|
}
|
|
609
845
|
dispose(error) {
|
|
610
846
|
this._deleted = true;
|
|
611
847
|
this.emit("disposed", this);
|
|
612
848
|
}
|
|
613
849
|
/**
|
|
614
|
-
* Unmark the deleted property when rolling back delete.
|
|
850
|
+
* Unmark the deleted property only when rolling back delete.
|
|
615
851
|
*/
|
|
616
852
|
undispose() {
|
|
617
853
|
this._deleted = false;
|
|
854
|
+
this.emit("undisposed", this);
|
|
618
855
|
}
|
|
619
856
|
get disposed() {
|
|
620
857
|
return this._deleted;
|
|
@@ -637,9 +874,8 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
637
874
|
* {@inheritDoc IDirectory.get}
|
|
638
875
|
*/
|
|
639
876
|
get(key) {
|
|
640
|
-
var _c;
|
|
641
877
|
this.throwIfDisposed();
|
|
642
|
-
return
|
|
878
|
+
return this._storage.get(key)?.value;
|
|
643
879
|
}
|
|
644
880
|
/**
|
|
645
881
|
* {@inheritDoc IDirectory.set}
|
|
@@ -687,21 +923,38 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
687
923
|
throw new Error(`SubDirectory name may not contain ${posix.sep}`);
|
|
688
924
|
}
|
|
689
925
|
// Create the sub directory locally first.
|
|
690
|
-
const isNew = this.createSubDirectoryCore(subdirName, true);
|
|
691
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
926
|
+
const isNew = this.createSubDirectoryCore(subdirName, true, this.getLocalSeq(), this.runtime.clientId ?? "detached");
|
|
692
927
|
const subDir = this._subdirectories.get(subdirName);
|
|
928
|
+
assert(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
|
|
693
929
|
// If we are not attached, don't submit the op.
|
|
694
930
|
if (!this.directory.isAttached()) {
|
|
695
931
|
return subDir;
|
|
696
932
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
933
|
+
// Only submit the op, if it is newly created.
|
|
934
|
+
if (isNew) {
|
|
935
|
+
const op = {
|
|
936
|
+
path: this.absolutePath,
|
|
937
|
+
subdirName,
|
|
938
|
+
type: "createSubDirectory",
|
|
939
|
+
};
|
|
940
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
941
|
+
}
|
|
703
942
|
return subDir;
|
|
704
943
|
}
|
|
944
|
+
/**
|
|
945
|
+
* @returns The Sequence Data which should be used for local changes.
|
|
946
|
+
* @remarks While detached, 0 is used rather than -1 to represent a change which should be universally known (as opposed to known
|
|
947
|
+
* only by the local client). This ensures that if the directory is later attached, none of its data needs to be updated (the values
|
|
948
|
+
* last set while detached will now be known to any new client, until they are changed).
|
|
949
|
+
*
|
|
950
|
+
* The client sequence number is incremented by 1 for maintaining the internal order of locally created subdirectories
|
|
951
|
+
* TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
|
|
952
|
+
*/
|
|
953
|
+
getLocalSeq() {
|
|
954
|
+
return this.directory.isAttached()
|
|
955
|
+
? { seq: -1, clientSeq: ++this.localCreationSeq }
|
|
956
|
+
: { seq: 0, clientSeq: ++this.localCreationSeq };
|
|
957
|
+
}
|
|
705
958
|
/**
|
|
706
959
|
* {@inheritDoc IDirectory.getSubDirectory}
|
|
707
960
|
*/
|
|
@@ -727,12 +980,15 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
727
980
|
if (!this.directory.isAttached()) {
|
|
728
981
|
return subDir !== undefined;
|
|
729
982
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
983
|
+
// Only submit the op, if the directory existed and we deleted it.
|
|
984
|
+
if (subDir !== undefined) {
|
|
985
|
+
const op = {
|
|
986
|
+
path: this.absolutePath,
|
|
987
|
+
subdirName,
|
|
988
|
+
type: "deleteSubDirectory",
|
|
989
|
+
};
|
|
990
|
+
this.submitDeleteSubDirectoryMessage(op, subDir);
|
|
991
|
+
}
|
|
736
992
|
return subDir !== undefined;
|
|
737
993
|
}
|
|
738
994
|
/**
|
|
@@ -740,7 +996,26 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
740
996
|
*/
|
|
741
997
|
subdirectories() {
|
|
742
998
|
this.throwIfDisposed();
|
|
743
|
-
|
|
999
|
+
const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
|
|
1000
|
+
const localSubdirsInOrder = this.localCreationSeqTracker.keys((key) => !this.ackedCreationSeqTracker.has(key));
|
|
1001
|
+
const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
|
|
1002
|
+
assert(subdirNames.length === this._subdirectories.size, 0x85c /* The count of keys for iteration should be consistent with the size of actual data */);
|
|
1003
|
+
const entriesIterator = {
|
|
1004
|
+
index: 0,
|
|
1005
|
+
dirs: this._subdirectories,
|
|
1006
|
+
next() {
|
|
1007
|
+
if (this.index < subdirNames.length) {
|
|
1008
|
+
const subdirName = subdirNames[this.index++];
|
|
1009
|
+
const subdir = this.dirs.get(subdirName);
|
|
1010
|
+
return { value: [subdirName, subdir], done: false };
|
|
1011
|
+
}
|
|
1012
|
+
return { value: undefined, done: true };
|
|
1013
|
+
},
|
|
1014
|
+
[Symbol.iterator]() {
|
|
1015
|
+
return this;
|
|
1016
|
+
},
|
|
1017
|
+
};
|
|
1018
|
+
return entriesIterator;
|
|
744
1019
|
}
|
|
745
1020
|
/**
|
|
746
1021
|
* {@inheritDoc IDirectory.getWorkingDirectory}
|
|
@@ -749,6 +1024,17 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
749
1024
|
this.throwIfDisposed();
|
|
750
1025
|
return this.directory.getWorkingDirectory(this.makeAbsolute(relativePath));
|
|
751
1026
|
}
|
|
1027
|
+
/**
|
|
1028
|
+
* This checks if there is pending delete op for local delete for a given child subdirectory.
|
|
1029
|
+
* @param subDirName - directory name.
|
|
1030
|
+
* @returns true if there is pending delete.
|
|
1031
|
+
*/
|
|
1032
|
+
isSubDirectoryDeletePending(subDirName) {
|
|
1033
|
+
if (this.pendingDeleteSubDirectoriesTracker.has(subDirName)) {
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
752
1038
|
/**
|
|
753
1039
|
* Deletes the given key from within this IDirectory.
|
|
754
1040
|
* @param key - The key to delete
|
|
@@ -794,6 +1080,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
794
1080
|
*/
|
|
795
1081
|
forEach(callback) {
|
|
796
1082
|
this.throwIfDisposed();
|
|
1083
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
797
1084
|
this._storage.forEach((localValue, key, map) => {
|
|
798
1085
|
callback(localValue.value, key, map);
|
|
799
1086
|
});
|
|
@@ -815,13 +1102,9 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
815
1102
|
const iterator = {
|
|
816
1103
|
next() {
|
|
817
1104
|
const nextVal = localEntriesIterator.next();
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
else {
|
|
822
|
-
// Unpack the stored value
|
|
823
|
-
return { value: [nextVal.value[0], nextVal.value[1].value], done: false };
|
|
824
|
-
}
|
|
1105
|
+
return nextVal.done
|
|
1106
|
+
? { value: undefined, done: true }
|
|
1107
|
+
: { value: [nextVal.value[0], nextVal.value[1].value], done: false };
|
|
825
1108
|
},
|
|
826
1109
|
[Symbol.iterator]() {
|
|
827
1110
|
return this;
|
|
@@ -847,13 +1130,9 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
847
1130
|
const iterator = {
|
|
848
1131
|
next() {
|
|
849
1132
|
const nextVal = localValuesIterator.next();
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
else {
|
|
854
|
-
// Unpack the stored value
|
|
855
|
-
return { value: nextVal.value.value, done: false };
|
|
856
|
-
}
|
|
1133
|
+
return nextVal.done
|
|
1134
|
+
? { value: undefined, done: true }
|
|
1135
|
+
: { value: nextVal.value.value, done: false };
|
|
857
1136
|
},
|
|
858
1137
|
[Symbol.iterator]() {
|
|
859
1138
|
return this;
|
|
@@ -871,48 +1150,90 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
871
1150
|
}
|
|
872
1151
|
/**
|
|
873
1152
|
* Process a clear operation.
|
|
1153
|
+
* @param msg - The message from the server to apply.
|
|
874
1154
|
* @param op - The op to process
|
|
875
1155
|
* @param local - Whether the message originated from the local client
|
|
876
1156
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
877
1157
|
* For messages from a remote client, this will be undefined.
|
|
878
1158
|
* @internal
|
|
879
1159
|
*/
|
|
880
|
-
processClearMessage(op, local, localOpMetadata) {
|
|
1160
|
+
processClearMessage(msg, op, local, localOpMetadata) {
|
|
881
1161
|
this.throwIfDisposed();
|
|
1162
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
882
1165
|
if (local) {
|
|
883
1166
|
assert(isClearLocalOpMetadata(localOpMetadata), 0x00f /* pendingMessageId is missing from the local client's operation */);
|
|
884
1167
|
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
885
1168
|
assert(pendingClearMessageId === localOpMetadata.pendingMessageId, 0x32a /* pendingMessageId does not match */);
|
|
886
1169
|
return;
|
|
887
1170
|
}
|
|
888
|
-
this.clearExceptPendingKeys();
|
|
1171
|
+
this.clearExceptPendingKeys(false);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Apply clear operation locally and generate metadata
|
|
1175
|
+
* @param op - Op to apply
|
|
1176
|
+
* @returns metadata generated for stahed op
|
|
1177
|
+
*/
|
|
1178
|
+
applyStashedClearMessage(op) {
|
|
1179
|
+
this.throwIfDisposed();
|
|
1180
|
+
const previousValue = new Map(this._storage);
|
|
1181
|
+
this.clearExceptPendingKeys(true);
|
|
1182
|
+
const pendingMsgId = ++this.pendingMessageId;
|
|
1183
|
+
this.pendingClearMessageIds.push(pendingMsgId);
|
|
1184
|
+
const metadata = {
|
|
1185
|
+
type: "clear",
|
|
1186
|
+
pendingMessageId: pendingMsgId,
|
|
1187
|
+
previousStorage: previousValue,
|
|
1188
|
+
};
|
|
1189
|
+
return metadata;
|
|
889
1190
|
}
|
|
890
1191
|
/**
|
|
891
1192
|
* Process a delete operation.
|
|
1193
|
+
* @param msg - The message from the server to apply.
|
|
892
1194
|
* @param op - The op to process
|
|
893
1195
|
* @param local - Whether the message originated from the local client
|
|
894
1196
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
895
1197
|
* For messages from a remote client, this will be undefined.
|
|
896
1198
|
* @internal
|
|
897
1199
|
*/
|
|
898
|
-
processDeleteMessage(op, local, localOpMetadata) {
|
|
1200
|
+
processDeleteMessage(msg, op, local, localOpMetadata) {
|
|
899
1201
|
this.throwIfDisposed();
|
|
900
|
-
if (!this.
|
|
1202
|
+
if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1203
|
+
this.needProcessStorageOperation(op, local, localOpMetadata))) {
|
|
901
1204
|
return;
|
|
902
1205
|
}
|
|
903
1206
|
this.deleteCore(op.key, local);
|
|
904
1207
|
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Apply delete operation locally and generate metadata
|
|
1210
|
+
* @param op - Op to apply
|
|
1211
|
+
* @returns metadata generated for stahed op
|
|
1212
|
+
*/
|
|
1213
|
+
applyStashedDeleteMessage(op) {
|
|
1214
|
+
this.throwIfDisposed();
|
|
1215
|
+
const previousValue = this.deleteCore(op.key, true);
|
|
1216
|
+
const pendingMessageId = this.getKeyMessageId(op);
|
|
1217
|
+
const localMetadata = {
|
|
1218
|
+
type: "edit",
|
|
1219
|
+
pendingMessageId,
|
|
1220
|
+
previousValue,
|
|
1221
|
+
};
|
|
1222
|
+
return localMetadata;
|
|
1223
|
+
}
|
|
905
1224
|
/**
|
|
906
1225
|
* Process a set operation.
|
|
1226
|
+
* @param msg - The message from the server to apply.
|
|
907
1227
|
* @param op - The op to process
|
|
908
1228
|
* @param local - Whether the message originated from the local client
|
|
909
1229
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
910
1230
|
* For messages from a remote client, this will be undefined.
|
|
911
1231
|
* @internal
|
|
912
1232
|
*/
|
|
913
|
-
processSetMessage(op, context, local, localOpMetadata) {
|
|
1233
|
+
processSetMessage(msg, op, context, local, localOpMetadata) {
|
|
914
1234
|
this.throwIfDisposed();
|
|
915
|
-
if (!this.
|
|
1235
|
+
if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1236
|
+
this.needProcessStorageOperation(op, local, localOpMetadata))) {
|
|
916
1237
|
return;
|
|
917
1238
|
}
|
|
918
1239
|
// needProcessStorageOperation should have returned false if local is true
|
|
@@ -920,36 +1241,89 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
920
1241
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
921
1242
|
this.setCore(op.key, context, local);
|
|
922
1243
|
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Apply set operation locally and generate metadata
|
|
1246
|
+
* @param op - Op to apply
|
|
1247
|
+
* @returns metadata generated for stahed op
|
|
1248
|
+
*/
|
|
1249
|
+
applyStashedSetMessage(op, context) {
|
|
1250
|
+
this.throwIfDisposed();
|
|
1251
|
+
// Set the value locally.
|
|
1252
|
+
const previousValue = this.setCore(op.key, context, true);
|
|
1253
|
+
// Create metadata
|
|
1254
|
+
const pendingMessageId = this.getKeyMessageId(op);
|
|
1255
|
+
const localMetadata = {
|
|
1256
|
+
type: "edit",
|
|
1257
|
+
pendingMessageId,
|
|
1258
|
+
previousValue,
|
|
1259
|
+
};
|
|
1260
|
+
return localMetadata;
|
|
1261
|
+
}
|
|
923
1262
|
/**
|
|
924
1263
|
* Process a create subdirectory operation.
|
|
1264
|
+
* @param msg - The message from the server to apply.
|
|
925
1265
|
* @param op - The op to process
|
|
926
1266
|
* @param local - Whether the message originated from the local client
|
|
927
1267
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
928
1268
|
* For messages from a remote client, this will be undefined.
|
|
929
1269
|
* @internal
|
|
930
1270
|
*/
|
|
931
|
-
processCreateSubDirectoryMessage(op, local, localOpMetadata) {
|
|
1271
|
+
processCreateSubDirectoryMessage(msg, op, local, localOpMetadata) {
|
|
932
1272
|
this.throwIfDisposed();
|
|
933
|
-
if (!this.
|
|
1273
|
+
if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1274
|
+
this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
|
|
934
1275
|
return;
|
|
935
1276
|
}
|
|
936
|
-
|
|
1277
|
+
assertNonNullClientId(msg.clientId);
|
|
1278
|
+
this.createSubDirectoryCore(op.subdirName, local, { seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber }, msg.clientId);
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Apply createSubDirectory operation locally and generate metadata
|
|
1282
|
+
* @param op - Op to apply
|
|
1283
|
+
* @returns metadata generated for stahed op
|
|
1284
|
+
*/
|
|
1285
|
+
applyStashedCreateSubDirMessage(op) {
|
|
1286
|
+
this.throwIfDisposed();
|
|
1287
|
+
// Create the sub directory locally first.
|
|
1288
|
+
this.createSubDirectoryCore(op.subdirName, true, this.getLocalSeq(), this.runtime.clientId ?? "detached");
|
|
1289
|
+
this.updatePendingSubDirMessageCount(op);
|
|
1290
|
+
const localOpMetadata = {
|
|
1291
|
+
type: "createSubDir",
|
|
1292
|
+
};
|
|
1293
|
+
return localOpMetadata;
|
|
937
1294
|
}
|
|
938
1295
|
/**
|
|
939
1296
|
* Process a delete subdirectory operation.
|
|
1297
|
+
* @param msg - The message from the server to apply.
|
|
940
1298
|
* @param op - The op to process
|
|
941
1299
|
* @param local - Whether the message originated from the local client
|
|
942
1300
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
943
1301
|
* For messages from a remote client, this will be undefined.
|
|
944
1302
|
* @internal
|
|
945
1303
|
*/
|
|
946
|
-
processDeleteSubDirectoryMessage(op, local, localOpMetadata) {
|
|
1304
|
+
processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata) {
|
|
947
1305
|
this.throwIfDisposed();
|
|
948
|
-
if (!this.
|
|
1306
|
+
if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1307
|
+
this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
|
|
949
1308
|
return;
|
|
950
1309
|
}
|
|
951
1310
|
this.deleteSubDirectoryCore(op.subdirName, local);
|
|
952
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Apply deleteSubDirectory operation locally and generate metadata
|
|
1314
|
+
* @param op - Op to apply
|
|
1315
|
+
* @returns metadata generated for stahed op
|
|
1316
|
+
*/
|
|
1317
|
+
applyStashedDeleteSubDirMessage(op) {
|
|
1318
|
+
this.throwIfDisposed();
|
|
1319
|
+
const subDir = this.deleteSubDirectoryCore(op.subdirName, true);
|
|
1320
|
+
this.updatePendingSubDirMessageCount(op);
|
|
1321
|
+
const metadata = {
|
|
1322
|
+
type: "deleteSubDir",
|
|
1323
|
+
subDirectory: subDir,
|
|
1324
|
+
};
|
|
1325
|
+
return metadata;
|
|
1326
|
+
}
|
|
953
1327
|
/**
|
|
954
1328
|
* Submit a clear operation.
|
|
955
1329
|
* @param op - The operation
|
|
@@ -974,8 +1348,11 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
974
1348
|
assert(isClearLocalOpMetadata(localOpMetadata), 0x32b /* Invalid localOpMetadata for clear */);
|
|
975
1349
|
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
976
1350
|
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
977
|
-
|
|
978
|
-
|
|
1351
|
+
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1352
|
+
// is already deleted, in which case we don't need to submit the op.
|
|
1353
|
+
if (pendingClearMessageId === localOpMetadata.pendingMessageId) {
|
|
1354
|
+
this.submitClearMessage(op, localOpMetadata.previousStorage);
|
|
1355
|
+
}
|
|
979
1356
|
}
|
|
980
1357
|
/**
|
|
981
1358
|
* Get a new pending message id for the op and cache it to track the pending op
|
|
@@ -1013,40 +1390,52 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1013
1390
|
assert(isKeyEditLocalOpMetadata(localOpMetadata), 0x32d /* Invalid localOpMetadata in submit */);
|
|
1014
1391
|
// clear the old pending message id
|
|
1015
1392
|
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
if (pendingMessageIds
|
|
1019
|
-
|
|
1393
|
+
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1394
|
+
// is already deleted, in which case we don't need to submit the op.
|
|
1395
|
+
if (pendingMessageIds !== undefined) {
|
|
1396
|
+
const index = pendingMessageIds.findIndex((id) => id === localOpMetadata.pendingMessageId);
|
|
1397
|
+
if (index === -1) {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
pendingMessageIds.splice(index, 1);
|
|
1401
|
+
if (pendingMessageIds.length === 0) {
|
|
1402
|
+
this.pendingKeys.delete(op.key);
|
|
1403
|
+
}
|
|
1404
|
+
this.submitKeyMessage(op, localOpMetadata.previousValue);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
incrementPendingSubDirCount(map, subDirName) {
|
|
1408
|
+
const count = map.get(subDirName) ?? 0;
|
|
1409
|
+
map.set(subDirName, count + 1);
|
|
1410
|
+
}
|
|
1411
|
+
decrementPendingSubDirCount(map, subDirName) {
|
|
1412
|
+
const count = map.get(subDirName) ?? 0;
|
|
1413
|
+
map.set(subDirName, count - 1);
|
|
1414
|
+
if (count <= 1) {
|
|
1415
|
+
map.delete(subDirName);
|
|
1020
1416
|
}
|
|
1021
|
-
this.submitKeyMessage(op, localOpMetadata.previousValue);
|
|
1022
1417
|
}
|
|
1023
1418
|
/**
|
|
1024
|
-
*
|
|
1419
|
+
* Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
|
|
1420
|
+
* or while resubmitting the op.
|
|
1025
1421
|
*/
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
|
|
1030
|
-
if (pendingMessageIds !== undefined) {
|
|
1031
|
-
pendingMessageIds.push(newMessageId);
|
|
1422
|
+
updatePendingSubDirMessageCount(op) {
|
|
1423
|
+
if (op.type === "deleteSubDirectory") {
|
|
1424
|
+
this.incrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
1032
1425
|
}
|
|
1033
|
-
else {
|
|
1034
|
-
this.
|
|
1426
|
+
else if (op.type === "createSubDirectory") {
|
|
1427
|
+
this.incrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1035
1428
|
}
|
|
1036
|
-
return newMessageId;
|
|
1037
1429
|
}
|
|
1038
1430
|
/**
|
|
1039
1431
|
* Submit a create subdirectory operation.
|
|
1040
1432
|
* @param op - The operation
|
|
1041
|
-
* @param prevExisted - Whether the subdirectory existed before the op
|
|
1042
1433
|
*/
|
|
1043
|
-
submitCreateSubDirectoryMessage(op
|
|
1434
|
+
submitCreateSubDirectoryMessage(op) {
|
|
1044
1435
|
this.throwIfDisposed();
|
|
1045
|
-
|
|
1436
|
+
this.updatePendingSubDirMessageCount(op);
|
|
1046
1437
|
const localOpMetadata = {
|
|
1047
1438
|
type: "createSubDir",
|
|
1048
|
-
pendingMessageId: newMessageId,
|
|
1049
|
-
previouslyExisted: prevExisted,
|
|
1050
1439
|
};
|
|
1051
1440
|
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1052
1441
|
}
|
|
@@ -1057,10 +1446,9 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1057
1446
|
*/
|
|
1058
1447
|
submitDeleteSubDirectoryMessage(op, subDir) {
|
|
1059
1448
|
this.throwIfDisposed();
|
|
1060
|
-
|
|
1449
|
+
this.updatePendingSubDirMessageCount(op);
|
|
1061
1450
|
const localOpMetadata = {
|
|
1062
1451
|
type: "deleteSubDir",
|
|
1063
|
-
pendingMessageId: newMessageId,
|
|
1064
1452
|
subDirectory: subDir,
|
|
1065
1453
|
};
|
|
1066
1454
|
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
@@ -1073,17 +1461,22 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1073
1461
|
*/
|
|
1074
1462
|
resubmitSubDirectoryMessage(op, localOpMetadata) {
|
|
1075
1463
|
assert(isSubDirLocalOpMetadata(localOpMetadata), 0x32f /* Invalid localOpMetadata for sub directory op */);
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1464
|
+
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1465
|
+
// is already deleted, in which case we don't need to submit the op.
|
|
1466
|
+
if (localOpMetadata.type === "createSubDir" &&
|
|
1467
|
+
!this.pendingCreateSubDirectoriesTracker.has(op.subdirName)) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
else if (localOpMetadata.type === "deleteSubDir" &&
|
|
1471
|
+
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
|
|
1472
|
+
return;
|
|
1082
1473
|
}
|
|
1083
1474
|
if (localOpMetadata.type === "createSubDir") {
|
|
1084
|
-
this.
|
|
1475
|
+
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1476
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
1085
1477
|
}
|
|
1086
1478
|
else {
|
|
1479
|
+
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
1087
1480
|
this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
|
|
1088
1481
|
}
|
|
1089
1482
|
}
|
|
@@ -1101,6 +1494,14 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1101
1494
|
yield res;
|
|
1102
1495
|
}
|
|
1103
1496
|
}
|
|
1497
|
+
getSerializableCreateInfo() {
|
|
1498
|
+
this.throwIfDisposed();
|
|
1499
|
+
const createInfo = {
|
|
1500
|
+
csn: this.seqData.seq,
|
|
1501
|
+
ccIds: Array.from(this.clientIds),
|
|
1502
|
+
};
|
|
1503
|
+
return createInfo;
|
|
1504
|
+
}
|
|
1104
1505
|
/**
|
|
1105
1506
|
* Populate a key value in this subdirectory's storage, to be used when loading from snapshot.
|
|
1106
1507
|
* @param key - The key to populate
|
|
@@ -1139,7 +1540,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1139
1540
|
*/
|
|
1140
1541
|
rollbackPendingMessageId(map, key, pendingMessageId) {
|
|
1141
1542
|
const pendingMessageIds = map.get(key);
|
|
1142
|
-
const lastPendingMessageId = pendingMessageIds
|
|
1543
|
+
const lastPendingMessageId = pendingMessageIds?.pop();
|
|
1143
1544
|
if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
|
|
1144
1545
|
throw new Error("Rollback op does not match last pending");
|
|
1145
1546
|
}
|
|
@@ -1147,21 +1548,24 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1147
1548
|
map.delete(key);
|
|
1148
1549
|
}
|
|
1149
1550
|
}
|
|
1551
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
1150
1552
|
/**
|
|
1151
1553
|
* Rollback a local op
|
|
1152
1554
|
* @param op - The operation to rollback
|
|
1153
1555
|
* @param localOpMetadata - The local metadata associated with the op.
|
|
1154
1556
|
*/
|
|
1557
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1155
1558
|
rollback(op, localOpMetadata) {
|
|
1156
1559
|
if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
|
|
1157
1560
|
throw new Error("Invalid localOpMetadata");
|
|
1158
1561
|
}
|
|
1159
1562
|
if (op.type === "clear" && localOpMetadata.type === "clear") {
|
|
1160
|
-
localOpMetadata.previousStorage.
|
|
1563
|
+
for (const [key, localValue] of localOpMetadata.previousStorage.entries()) {
|
|
1161
1564
|
this.setCore(key, localValue, true);
|
|
1162
|
-
}
|
|
1565
|
+
}
|
|
1163
1566
|
const lastPendingClearId = this.pendingClearMessageIds.pop();
|
|
1164
|
-
if (lastPendingClearId === undefined ||
|
|
1567
|
+
if (lastPendingClearId === undefined ||
|
|
1568
|
+
lastPendingClearId !== localOpMetadata.pendingMessageId) {
|
|
1165
1569
|
throw new Error("Rollback op does match last clear");
|
|
1166
1570
|
}
|
|
1167
1571
|
}
|
|
@@ -1175,24 +1579,34 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1175
1579
|
this.rollbackPendingMessageId(this.pendingKeys, op.key, localOpMetadata.pendingMessageId);
|
|
1176
1580
|
}
|
|
1177
1581
|
else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
|
|
1582
|
+
this.deleteSubDirectoryCore(op.subdirName, true);
|
|
1583
|
+
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1182
1584
|
}
|
|
1183
1585
|
else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
|
|
1184
1586
|
if (localOpMetadata.subDirectory !== undefined) {
|
|
1185
1587
|
this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
|
|
1186
1588
|
// don't need to register events because deleting never unregistered
|
|
1187
1589
|
this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
|
|
1590
|
+
// Restore the record in creation tracker
|
|
1591
|
+
if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
|
|
1592
|
+
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
1593
|
+
...localOpMetadata.subDirectory.seqData,
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
else {
|
|
1597
|
+
this.localCreationSeqTracker.set(op.subdirName, {
|
|
1598
|
+
...localOpMetadata.subDirectory.seqData,
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1188
1601
|
this.emit("subDirectoryCreated", op.subdirName, true, this);
|
|
1189
1602
|
}
|
|
1190
|
-
this.
|
|
1603
|
+
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subDirName);
|
|
1191
1604
|
}
|
|
1192
1605
|
else {
|
|
1193
1606
|
throw new Error("Unsupported op for rollback");
|
|
1194
1607
|
}
|
|
1195
1608
|
}
|
|
1609
|
+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
|
1196
1610
|
/**
|
|
1197
1611
|
* Converts the given relative path into an absolute path.
|
|
1198
1612
|
* @param path - Relative path to convert
|
|
@@ -1213,22 +1627,38 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1213
1627
|
needProcessStorageOperation(op, local, localOpMetadata) {
|
|
1214
1628
|
if (this.pendingClearMessageIds.length > 0) {
|
|
1215
1629
|
if (local) {
|
|
1216
|
-
assert(localOpMetadata !== undefined &&
|
|
1630
|
+
assert(localOpMetadata !== undefined &&
|
|
1631
|
+
isKeyEditLocalOpMetadata(localOpMetadata) &&
|
|
1217
1632
|
localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0], 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
|
|
1633
|
+
// Remove all pendingMessageIds lower than first pendingClearMessageId.
|
|
1634
|
+
const lowestPendingClearMessageId = this.pendingClearMessageIds[0];
|
|
1635
|
+
const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
|
|
1636
|
+
if (pendingKeyMessageIdArray !== undefined) {
|
|
1637
|
+
let index = 0;
|
|
1638
|
+
while (pendingKeyMessageIdArray[index] < lowestPendingClearMessageId) {
|
|
1639
|
+
index += 1;
|
|
1640
|
+
}
|
|
1641
|
+
const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
|
|
1642
|
+
if (newPendingKeyMessageId.length === 0) {
|
|
1643
|
+
this.pendingKeys.delete(op.key);
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
this.pendingKeys.set(op.key, newPendingKeyMessageId);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1218
1649
|
}
|
|
1219
1650
|
// If I have a NACK clear, we can ignore all ops.
|
|
1220
1651
|
return false;
|
|
1221
1652
|
}
|
|
1222
|
-
const
|
|
1223
|
-
if (
|
|
1653
|
+
const pendingKeyMessageIds = this.pendingKeys.get(op.key);
|
|
1654
|
+
if (pendingKeyMessageIds !== undefined) {
|
|
1224
1655
|
// Found an NACK op, clear it from the directory if the latest sequence number in the directory
|
|
1225
1656
|
// match the message's and don't process the op.
|
|
1226
1657
|
if (local) {
|
|
1227
1658
|
assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata), 0x011 /* pendingMessageId is missing from the local client's operation */);
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
if (pendingMessageIds.length === 0) {
|
|
1659
|
+
assert(pendingKeyMessageIds[0] === localOpMetadata.pendingMessageId, 0x331 /* Unexpected pending message received */);
|
|
1660
|
+
pendingKeyMessageIds.shift();
|
|
1661
|
+
if (pendingKeyMessageIds.length === 0) {
|
|
1232
1662
|
this.pendingKeys.delete(op.key);
|
|
1233
1663
|
}
|
|
1234
1664
|
}
|
|
@@ -1237,6 +1667,19 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1237
1667
|
// If we don't have a NACK op on the key, we need to process the remote ops.
|
|
1238
1668
|
return !local;
|
|
1239
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* This return true if the message is for the current instance of this sub directory. As the sub directory
|
|
1672
|
+
* can be deleted and created again, then this finds if the message is for current instance of directory or not.
|
|
1673
|
+
* @param msg - message for the directory
|
|
1674
|
+
*/
|
|
1675
|
+
isMessageForCurrentInstanceOfSubDirectory(msg) {
|
|
1676
|
+
// If the message is either from the creator of directory or this directory was created when
|
|
1677
|
+
// container was detached or in case this directory is already live(known to other clients)
|
|
1678
|
+
// and the op was created after the directory was created then apply this op.
|
|
1679
|
+
return ((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
|
|
1680
|
+
this.clientIds.has("detached") ||
|
|
1681
|
+
(this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber));
|
|
1682
|
+
}
|
|
1240
1683
|
/**
|
|
1241
1684
|
* If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
|
|
1242
1685
|
* not process the incoming operation.
|
|
@@ -1247,16 +1690,80 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1247
1690
|
* For messages from a remote client, this will be undefined.
|
|
1248
1691
|
* @returns True if the operation should be processed, false otherwise
|
|
1249
1692
|
*/
|
|
1250
|
-
needProcessSubDirectoryOperation(op, local, localOpMetadata) {
|
|
1251
|
-
|
|
1252
|
-
|
|
1693
|
+
needProcessSubDirectoryOperation(msg, op, local, localOpMetadata) {
|
|
1694
|
+
assertNonNullClientId(msg.clientId);
|
|
1695
|
+
const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
|
|
1696
|
+
const pendingCreateCount = this.pendingCreateSubDirectoriesTracker.get(op.subdirName);
|
|
1697
|
+
if ((pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
|
|
1698
|
+
(pendingCreateCount !== undefined && pendingCreateCount > 0)) {
|
|
1253
1699
|
if (local) {
|
|
1254
1700
|
assert(isSubDirLocalOpMetadata(localOpMetadata), 0x012 /* pendingMessageId is missing from the local client's operation */);
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1701
|
+
if (localOpMetadata.type === "deleteSubDir") {
|
|
1702
|
+
assert(pendingDeleteCount !== undefined && pendingDeleteCount > 0, 0x6c2 /* pendingDeleteCount should exist */);
|
|
1703
|
+
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
1704
|
+
}
|
|
1705
|
+
else if (localOpMetadata.type === "createSubDir") {
|
|
1706
|
+
assert(pendingCreateCount !== undefined && pendingCreateCount > 0, 0x6c3 /* pendingCreateCount should exist */);
|
|
1707
|
+
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (op.type === "deleteSubDirectory") {
|
|
1711
|
+
const resetSubDirectoryTree = (directory) => {
|
|
1712
|
+
if (!directory) {
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
// If this is delete op and we have keys in this subDirectory, then we need to delete these
|
|
1716
|
+
// keys except the pending ones as they will be sequenced after this delete.
|
|
1717
|
+
directory.clearExceptPendingKeys(local);
|
|
1718
|
+
// In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
|
|
1719
|
+
// creators as the previous directory is getting deleted and we will initialize again when
|
|
1720
|
+
// we will receive op for the create again.
|
|
1721
|
+
directory.seqData.seq = -1;
|
|
1722
|
+
directory.seqData.clientSeq = -1;
|
|
1723
|
+
directory.clientIds.clear();
|
|
1724
|
+
// Do the same thing for the subtree of the directory. If create is not pending for a child, then just
|
|
1725
|
+
// delete it.
|
|
1726
|
+
const subDirectories = directory.subdirectories();
|
|
1727
|
+
for (const [subDirName, subDir] of subDirectories) {
|
|
1728
|
+
if (directory.pendingCreateSubDirectoriesTracker.has(subDirName)) {
|
|
1729
|
+
resetSubDirectoryTree(subDir);
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
directory.deleteSubDirectoryCore(subDirName, false);
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
const subDirectory = this._subdirectories.get(op.subdirName);
|
|
1736
|
+
// Clear the creation tracker record
|
|
1737
|
+
this.ackedCreationSeqTracker.delete(op.subdirName);
|
|
1738
|
+
resetSubDirectoryTree(subDirectory);
|
|
1739
|
+
}
|
|
1740
|
+
if (op.type === "createSubDirectory") {
|
|
1741
|
+
const dir = this._subdirectories.get(op.subdirName);
|
|
1742
|
+
// Child sub directory create seq number can't be lower than the parent subdirectory.
|
|
1743
|
+
// The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
|
|
1744
|
+
if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
|
|
1745
|
+
if (dir?.seqData.seq === -1) {
|
|
1746
|
+
// Only set the sequence data based on the first message
|
|
1747
|
+
dir.seqData.seq = msg.sequenceNumber;
|
|
1748
|
+
dir.seqData.clientSeq = msg.clientSequenceNumber;
|
|
1749
|
+
// set the creation seq in tracker
|
|
1750
|
+
if (!this.ackedCreationSeqTracker.has(op.subdirName) &&
|
|
1751
|
+
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
|
|
1752
|
+
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
1753
|
+
seq: msg.sequenceNumber,
|
|
1754
|
+
clientSeq: msg.clientSequenceNumber,
|
|
1755
|
+
});
|
|
1756
|
+
if (local) {
|
|
1757
|
+
this.localCreationSeqTracker.delete(op.subdirName);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
// The client created the dir at or after the dirs seq, so list its client id as a creator.
|
|
1762
|
+
if (dir !== undefined &&
|
|
1763
|
+
!dir.clientIds.has(msg.clientId) &&
|
|
1764
|
+
dir.seqData.seq <= msg.sequenceNumber) {
|
|
1765
|
+
dir.clientIds.add(msg.clientId);
|
|
1766
|
+
}
|
|
1260
1767
|
}
|
|
1261
1768
|
}
|
|
1262
1769
|
return false;
|
|
@@ -1266,18 +1773,21 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1266
1773
|
/**
|
|
1267
1774
|
* Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
|
|
1268
1775
|
*/
|
|
1269
|
-
clearExceptPendingKeys() {
|
|
1776
|
+
clearExceptPendingKeys(local) {
|
|
1270
1777
|
// Assuming the pendingKeys is small and the map is large
|
|
1271
1778
|
// we will get the value for the pendingKeys and clear the map
|
|
1272
1779
|
const temp = new Map();
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1780
|
+
for (const [key] of this.pendingKeys) {
|
|
1781
|
+
const value = this._storage.get(key);
|
|
1782
|
+
// If this key is already deleted, then we don't need to add it again.
|
|
1783
|
+
if (value !== undefined) {
|
|
1784
|
+
temp.set(key, value);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
this.clearCore(local);
|
|
1788
|
+
for (const [key, value] of temp.entries()) {
|
|
1279
1789
|
this.setCore(key, value, true);
|
|
1280
|
-
}
|
|
1790
|
+
}
|
|
1281
1791
|
}
|
|
1282
1792
|
/**
|
|
1283
1793
|
* Clear implementation used for both locally sourced clears as well as incoming remote clears.
|
|
@@ -1295,7 +1805,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1295
1805
|
*/
|
|
1296
1806
|
deleteCore(key, local) {
|
|
1297
1807
|
const previousLocalValue = this._storage.get(key);
|
|
1298
|
-
const previousValue = previousLocalValue
|
|
1808
|
+
const previousValue = previousLocalValue?.value;
|
|
1299
1809
|
const successfullyRemoved = this._storage.delete(key);
|
|
1300
1810
|
if (successfullyRemoved) {
|
|
1301
1811
|
const event = { key, path: this.absolutePath, previousValue };
|
|
@@ -1314,7 +1824,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1314
1824
|
*/
|
|
1315
1825
|
setCore(key, value, local) {
|
|
1316
1826
|
const previousLocalValue = this._storage.get(key);
|
|
1317
|
-
const previousValue = previousLocalValue
|
|
1827
|
+
const previousValue = previousLocalValue?.value;
|
|
1318
1828
|
this._storage.set(key, value);
|
|
1319
1829
|
const event = { key, path: this.absolutePath, previousValue };
|
|
1320
1830
|
this.directory.emit("valueChanged", event, local, this.directory);
|
|
@@ -1326,17 +1836,33 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1326
1836
|
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
1327
1837
|
* @param subdirName - The name of the subdirectory being created
|
|
1328
1838
|
* @param local - Whether the message originated from the local client
|
|
1329
|
-
* @
|
|
1839
|
+
* @param seqData - Sequence number and client sequence number at which this directory is created
|
|
1840
|
+
* @param clientId - Id of client which created this directory.
|
|
1841
|
+
* @returns True if is newly created, false if it already existed.
|
|
1330
1842
|
*/
|
|
1331
|
-
createSubDirectoryCore(subdirName, local) {
|
|
1332
|
-
|
|
1843
|
+
createSubDirectoryCore(subdirName, local, seqData, clientId) {
|
|
1844
|
+
const subdir = this._subdirectories.get(subdirName);
|
|
1845
|
+
if (subdir === undefined) {
|
|
1333
1846
|
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
1334
|
-
const subDir = new SubDirectory(this.directory, this.runtime, this.serializer, absolutePath);
|
|
1847
|
+
const subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath);
|
|
1848
|
+
/**
|
|
1849
|
+
* Store the sequnce numbers of newly created subdirectory to the proper creation tracker, based
|
|
1850
|
+
* on whether the creation behavior has been ack'd or not
|
|
1851
|
+
*/
|
|
1852
|
+
if (!isAcknowledgedOrDetached(seqData)) {
|
|
1853
|
+
this.localCreationSeqTracker.set(subdirName, { ...seqData });
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
|
|
1857
|
+
}
|
|
1335
1858
|
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
1336
1859
|
this._subdirectories.set(subdirName, subDir);
|
|
1337
1860
|
this.emit("subDirectoryCreated", subdirName, local, this);
|
|
1338
1861
|
return true;
|
|
1339
1862
|
}
|
|
1863
|
+
else {
|
|
1864
|
+
subdir.clientIds.add(clientId);
|
|
1865
|
+
}
|
|
1340
1866
|
return false;
|
|
1341
1867
|
}
|
|
1342
1868
|
registerEventsOnSubDirectory(subDirectory, subDirName) {
|
|
@@ -1358,6 +1884,16 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1358
1884
|
// Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
|
|
1359
1885
|
if (previousValue !== undefined) {
|
|
1360
1886
|
this._subdirectories.delete(subdirName);
|
|
1887
|
+
/**
|
|
1888
|
+
* Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
|
|
1889
|
+
* ack'd already or still not committed yet (could be both).
|
|
1890
|
+
*/
|
|
1891
|
+
if (this.ackedCreationSeqTracker.has(subdirName)) {
|
|
1892
|
+
this.ackedCreationSeqTracker.delete(subdirName);
|
|
1893
|
+
}
|
|
1894
|
+
if (this.localCreationSeqTracker.has(subdirName)) {
|
|
1895
|
+
this.localCreationSeqTracker.delete(subdirName);
|
|
1896
|
+
}
|
|
1361
1897
|
this.disposeSubDirectoryTree(previousValue);
|
|
1362
1898
|
this.emit("subDirectoryDeleted", subdirName, local, this);
|
|
1363
1899
|
}
|
|
@@ -1377,11 +1913,12 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1377
1913
|
}
|
|
1378
1914
|
}
|
|
1379
1915
|
undeleteSubDirectoryTree(directory) {
|
|
1380
|
-
// Restore deleted subdirectory tree.
|
|
1381
|
-
|
|
1916
|
+
// Restore deleted subdirectory tree. Need to undispose the current directory first, then get access to the iterator.
|
|
1917
|
+
// This will unmark "deleted" from the subdirectories from top to bottom.
|
|
1918
|
+
directory.undispose();
|
|
1919
|
+
for (const [_, subDirectory] of directory.subdirectories()) {
|
|
1382
1920
|
this.undeleteSubDirectoryTree(subDirectory);
|
|
1383
1921
|
}
|
|
1384
|
-
directory.undispose();
|
|
1385
1922
|
}
|
|
1386
1923
|
}
|
|
1387
|
-
//# sourceMappingURL=directory.
|
|
1924
|
+
//# sourceMappingURL=directory.mjs.map
|