@fluidframework/map 2.53.0 → 2.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/api-report/{map.legacy.alpha.api.md → map.legacy.beta.api.md} +15 -15
- package/dist/directory.d.ts +41 -91
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +448 -465
- package/dist/directory.js.map +1 -1
- package/dist/directoryFactory.d.ts +3 -6
- package/dist/directoryFactory.d.ts.map +1 -1
- package/dist/directoryFactory.js +2 -4
- package/dist/directoryFactory.js.map +1 -1
- package/dist/interfaces.d.ts +4 -8
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/internalInterfaces.d.ts +1 -2
- package/dist/internalInterfaces.d.ts.map +1 -1
- package/dist/internalInterfaces.js.map +1 -1
- package/dist/mapFactory.d.ts +3 -6
- package/dist/mapFactory.d.ts.map +1 -1
- package/dist/mapFactory.js +2 -4
- package/dist/mapFactory.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/lib/directory.d.ts +41 -91
- package/lib/directory.d.ts.map +1 -1
- package/lib/directory.js +441 -458
- package/lib/directory.js.map +1 -1
- package/lib/directoryFactory.d.ts +3 -6
- package/lib/directoryFactory.d.ts.map +1 -1
- package/lib/directoryFactory.js +2 -4
- package/lib/directoryFactory.js.map +1 -1
- package/lib/interfaces.d.ts +4 -8
- package/lib/interfaces.d.ts.map +1 -1
- package/lib/interfaces.js.map +1 -1
- package/lib/internalInterfaces.d.ts +1 -2
- package/lib/internalInterfaces.d.ts.map +1 -1
- package/lib/internalInterfaces.js.map +1 -1
- package/lib/mapFactory.d.ts +3 -6
- package/lib/mapFactory.d.ts.map +1 -1
- package/lib/mapFactory.js +2 -4
- package/lib/mapFactory.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/package.json +18 -19
- package/src/directory.ts +565 -574
- package/src/directoryFactory.ts +3 -6
- package/src/interfaces.ts +4 -8
- package/src/internalInterfaces.ts +1 -2
- package/src/mapFactory.ts +3 -6
- package/src/packageVersion.ts +1 -1
package/lib/directory.js
CHANGED
|
@@ -7,10 +7,9 @@ import { TypedEventEmitter } from "@fluid-internal/client-utils";
|
|
|
7
7
|
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
|
|
8
8
|
import { MessageType, } from "@fluidframework/driver-definitions/internal";
|
|
9
9
|
import { readAndParse } from "@fluidframework/driver-utils/internal";
|
|
10
|
-
import { RedBlackTree } from "@fluidframework/merge-tree/internal";
|
|
11
10
|
import { SummaryTreeBuilder } from "@fluidframework/runtime-utils/internal";
|
|
12
11
|
import { SharedObject, ValueType, bindHandles, parseHandles, } from "@fluidframework/shared-object-base/internal";
|
|
13
|
-
import { UsageError, } from "@fluidframework/telemetry-utils/internal";
|
|
12
|
+
import { createChildMonitoringContext, UsageError, } from "@fluidframework/telemetry-utils/internal";
|
|
14
13
|
import path from "path-browserify";
|
|
15
14
|
import { serializeValue, migrateIfSharedSerializable } from "./localValues.js";
|
|
16
15
|
import { findLast, findLastIndex } from "./utils.js";
|
|
@@ -61,60 +60,6 @@ const seqDataComparator = (a, b) => {
|
|
|
61
60
|
function isAcknowledgedOrDetached(seqData) {
|
|
62
61
|
return seqData.seq >= 0;
|
|
63
62
|
}
|
|
64
|
-
/**
|
|
65
|
-
* A utility class for tracking associations between keys and their creation indices.
|
|
66
|
-
* This is relevant to support map iteration in insertion order, see
|
|
67
|
-
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
|
|
68
|
-
*
|
|
69
|
-
* TODO: It can be combined with the creation tracker utilized in SharedMap
|
|
70
|
-
*/
|
|
71
|
-
class DirectoryCreationTracker {
|
|
72
|
-
constructor() {
|
|
73
|
-
this.indexToKey = new RedBlackTree(seqDataComparator);
|
|
74
|
-
this.keyToIndex = new Map();
|
|
75
|
-
}
|
|
76
|
-
set(key, seqData) {
|
|
77
|
-
this.indexToKey.put(seqData, key);
|
|
78
|
-
this.keyToIndex.set(key, seqData);
|
|
79
|
-
}
|
|
80
|
-
has(keyOrSeqData) {
|
|
81
|
-
return typeof keyOrSeqData === "string"
|
|
82
|
-
? this.keyToIndex.has(keyOrSeqData)
|
|
83
|
-
: this.indexToKey.get(keyOrSeqData) !== undefined;
|
|
84
|
-
}
|
|
85
|
-
delete(keyOrSeqData) {
|
|
86
|
-
if (this.has(keyOrSeqData)) {
|
|
87
|
-
if (typeof keyOrSeqData === "string") {
|
|
88
|
-
const seqData = this.keyToIndex.get(keyOrSeqData);
|
|
89
|
-
this.keyToIndex.delete(keyOrSeqData);
|
|
90
|
-
this.indexToKey.remove(seqData);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
const key = this.indexToKey.get(keyOrSeqData)?.data;
|
|
94
|
-
this.indexToKey.remove(keyOrSeqData);
|
|
95
|
-
this.keyToIndex.delete(key);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Retrieves all subdirectories with creation order that satisfy an optional constraint function.
|
|
101
|
-
* @param constraint - An optional constraint function that filters keys.
|
|
102
|
-
* @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
|
|
103
|
-
*/
|
|
104
|
-
keys(constraint) {
|
|
105
|
-
const keys = [];
|
|
106
|
-
this.indexToKey.mapRange((node) => {
|
|
107
|
-
if (!constraint || constraint(node.data)) {
|
|
108
|
-
keys.push(node.data);
|
|
109
|
-
}
|
|
110
|
-
return true;
|
|
111
|
-
}, keys);
|
|
112
|
-
return keys;
|
|
113
|
-
}
|
|
114
|
-
get size() {
|
|
115
|
-
return this.keyToIndex.size;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
63
|
/**
|
|
119
64
|
* {@inheritDoc ISharedDirectory}
|
|
120
65
|
*
|
|
@@ -315,6 +260,25 @@ export class SharedDirectory extends SharedObject {
|
|
|
315
260
|
}
|
|
316
261
|
return currentSubDir;
|
|
317
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Similar to `getWorkingDirectory`, but only returns directories that are sequenced.
|
|
265
|
+
* This can be useful for op processing since we only process ops on sequenced directories.
|
|
266
|
+
*/
|
|
267
|
+
getSequencedWorkingDirectory(relativePath) {
|
|
268
|
+
const absolutePath = this.makeAbsolute(relativePath);
|
|
269
|
+
if (absolutePath === posix.sep) {
|
|
270
|
+
return this.root;
|
|
271
|
+
}
|
|
272
|
+
let currentSubDir = this.root;
|
|
273
|
+
const subdirs = absolutePath.slice(1).split(posix.sep);
|
|
274
|
+
for (const subdir of subdirs) {
|
|
275
|
+
currentSubDir = currentSubDir.sequencedSubdirectories.get(subdir);
|
|
276
|
+
if (!currentSubDir) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return currentSubDir;
|
|
281
|
+
}
|
|
318
282
|
/**
|
|
319
283
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
|
|
320
284
|
*/
|
|
@@ -407,10 +371,6 @@ export class SharedDirectory extends SharedObject {
|
|
|
407
371
|
}
|
|
408
372
|
newSubDir = new SubDirectory(seqData, createInfo === undefined ? new Set() : new Set(createInfo.ccIds), this, this.runtime, this.serializer, posix.join(currentSubDir.absolutePath, subdirName), this.logger);
|
|
409
373
|
currentSubDir.populateSubDirectory(subdirName, newSubDir);
|
|
410
|
-
// Record the newly inserted subdirectory to the creation tracker
|
|
411
|
-
currentSubDir.ackedCreationSeqTracker.set(subdirName, {
|
|
412
|
-
...seqData,
|
|
413
|
-
});
|
|
414
374
|
}
|
|
415
375
|
stack.push([newSubDir, subdirObject]);
|
|
416
376
|
}
|
|
@@ -453,114 +413,87 @@ export class SharedDirectory extends SharedObject {
|
|
|
453
413
|
makeAbsolute(relativePath) {
|
|
454
414
|
return posix.resolve(posix.sep, relativePath);
|
|
455
415
|
}
|
|
456
|
-
/**
|
|
457
|
-
* This checks if there is pending delete op for local delete for a any subdir in the relative path.
|
|
458
|
-
* @param relativePath - path of sub directory.
|
|
459
|
-
* @returns `true` if there is pending delete, `false` otherwise.
|
|
460
|
-
*/
|
|
461
|
-
isSubDirectoryDeletePending(relativePath) {
|
|
462
|
-
const absolutePath = this.makeAbsolute(relativePath);
|
|
463
|
-
if (absolutePath === posix.sep) {
|
|
464
|
-
return false;
|
|
465
|
-
}
|
|
466
|
-
let currentParent = this.root;
|
|
467
|
-
const pathParts = absolutePath.split(posix.sep).slice(1);
|
|
468
|
-
for (const dirName of pathParts) {
|
|
469
|
-
if (currentParent.isSubDirectoryDeletePending(dirName)) {
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
currentParent = currentParent.getSubDirectory(dirName);
|
|
473
|
-
if (currentParent === undefined) {
|
|
474
|
-
return true;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return false;
|
|
478
|
-
}
|
|
479
416
|
/**
|
|
480
417
|
* Set the message handlers for the directory.
|
|
481
418
|
*/
|
|
482
419
|
setMessageHandlers() {
|
|
420
|
+
// Notes on how we target the correct subdirectory:
|
|
421
|
+
// `process`: When processing ops, we only ever want to process ops on sequenced directories. This prevents
|
|
422
|
+
// scenarios where ops could be processed on a pending directory instead of a sequenced directory,
|
|
423
|
+
// leading to ops effectively being processed out of order.
|
|
424
|
+
// `resubmit`: When resubmitting ops, we use `localOpMetadata` to get a reference to the subdirectory that
|
|
425
|
+
// the op was originally targeting.
|
|
483
426
|
this.messageHandlers.set("clear", {
|
|
484
427
|
process: (msg, op, local, localOpMetadata) => {
|
|
485
|
-
const subdir = this.
|
|
486
|
-
|
|
487
|
-
// as we are going to delete this subDirectory.
|
|
488
|
-
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
428
|
+
const subdir = this.getSequencedWorkingDirectory(op.path);
|
|
429
|
+
if (subdir !== undefined && !subdir?.disposed) {
|
|
489
430
|
subdir.processClearMessage(msg, op, local, localOpMetadata);
|
|
490
431
|
}
|
|
491
432
|
},
|
|
492
433
|
resubmit: (op, localOpMetadata) => {
|
|
493
|
-
const
|
|
494
|
-
if (
|
|
495
|
-
|
|
434
|
+
const targetSubdir = localOpMetadata.subdir;
|
|
435
|
+
if (!targetSubdir.disposed) {
|
|
436
|
+
targetSubdir.resubmitClearMessage(op, localOpMetadata);
|
|
496
437
|
}
|
|
497
438
|
},
|
|
498
439
|
});
|
|
499
440
|
this.messageHandlers.set("delete", {
|
|
500
441
|
process: (msg, op, local, localOpMetadata) => {
|
|
501
|
-
const subdir = this.
|
|
502
|
-
|
|
503
|
-
// as we are going to delete this subDirectory.
|
|
504
|
-
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
442
|
+
const subdir = this.getSequencedWorkingDirectory(op.path);
|
|
443
|
+
if (subdir !== undefined && !subdir?.disposed) {
|
|
505
444
|
subdir.processDeleteMessage(msg, op, local, localOpMetadata);
|
|
506
445
|
}
|
|
507
446
|
},
|
|
508
447
|
resubmit: (op, localOpMetadata) => {
|
|
509
|
-
const
|
|
510
|
-
if (
|
|
511
|
-
|
|
448
|
+
const targetSubdir = localOpMetadata.subdir;
|
|
449
|
+
if (!targetSubdir.disposed) {
|
|
450
|
+
targetSubdir.resubmitKeyMessage(op, localOpMetadata);
|
|
512
451
|
}
|
|
513
452
|
},
|
|
514
453
|
});
|
|
515
454
|
this.messageHandlers.set("set", {
|
|
516
455
|
process: (msg, op, local, localOpMetadata) => {
|
|
517
|
-
const subdir = this.
|
|
518
|
-
|
|
519
|
-
// as we are going to delete this subDirectory.
|
|
520
|
-
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
456
|
+
const subdir = this.getSequencedWorkingDirectory(op.path);
|
|
457
|
+
if (subdir !== undefined && !subdir?.disposed) {
|
|
521
458
|
migrateIfSharedSerializable(op.value, this.serializer, this.handle);
|
|
522
459
|
const localValue = local ? undefined : op.value.value;
|
|
523
460
|
subdir.processSetMessage(msg, op, localValue, local, localOpMetadata);
|
|
524
461
|
}
|
|
525
462
|
},
|
|
526
463
|
resubmit: (op, localOpMetadata) => {
|
|
527
|
-
const
|
|
528
|
-
if (
|
|
529
|
-
|
|
464
|
+
const targetSubdir = localOpMetadata.subdir;
|
|
465
|
+
if (!targetSubdir.disposed) {
|
|
466
|
+
targetSubdir.resubmitKeyMessage(op, localOpMetadata);
|
|
530
467
|
}
|
|
531
468
|
},
|
|
532
469
|
});
|
|
533
470
|
this.messageHandlers.set("createSubDirectory", {
|
|
534
471
|
process: (msg, op, local, localOpMetadata) => {
|
|
535
|
-
const parentSubdir = this.
|
|
536
|
-
|
|
537
|
-
// as we are going to delete this subDirectory.
|
|
538
|
-
if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
472
|
+
const parentSubdir = this.getSequencedWorkingDirectory(op.path);
|
|
473
|
+
if (parentSubdir !== undefined && !parentSubdir?.disposed) {
|
|
539
474
|
parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
540
475
|
}
|
|
541
476
|
},
|
|
542
477
|
resubmit: (op, localOpMetadata) => {
|
|
543
|
-
const
|
|
544
|
-
if (
|
|
478
|
+
const targetSubdir = localOpMetadata.parentSubdir;
|
|
479
|
+
if (!targetSubdir.disposed) {
|
|
545
480
|
// We don't reuse the metadata but send a new one on each submit.
|
|
546
|
-
|
|
481
|
+
targetSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
547
482
|
}
|
|
548
483
|
},
|
|
549
484
|
});
|
|
550
485
|
this.messageHandlers.set("deleteSubDirectory", {
|
|
551
486
|
process: (msg, op, local, localOpMetadata) => {
|
|
552
|
-
const parentSubdir = this.
|
|
553
|
-
|
|
554
|
-
// as we are going to delete this subDirectory.
|
|
555
|
-
if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
487
|
+
const parentSubdir = this.getSequencedWorkingDirectory(op.path);
|
|
488
|
+
if (parentSubdir !== undefined && !parentSubdir?.disposed) {
|
|
556
489
|
parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
557
490
|
}
|
|
558
491
|
},
|
|
559
492
|
resubmit: (op, localOpMetadata) => {
|
|
560
|
-
const
|
|
561
|
-
if (
|
|
493
|
+
const targetSubdir = localOpMetadata.parentSubdir;
|
|
494
|
+
if (!targetSubdir.disposed) {
|
|
562
495
|
// We don't reuse the metadata but send a new one on each submit.
|
|
563
|
-
|
|
496
|
+
targetSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
564
497
|
}
|
|
565
498
|
},
|
|
566
499
|
});
|
|
@@ -660,7 +593,6 @@ export class SharedDirectory extends SharedObject {
|
|
|
660
593
|
function assertNonNullClientId(clientId) {
|
|
661
594
|
assert(clientId !== null, 0x6af /* client id should never be null */);
|
|
662
595
|
}
|
|
663
|
-
let hasLoggedDirectoryInconsistency = false;
|
|
664
596
|
/**
|
|
665
597
|
* Node of the directory tree.
|
|
666
598
|
* @sealed
|
|
@@ -683,7 +615,6 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
683
615
|
this.runtime = runtime;
|
|
684
616
|
this.serializer = serializer;
|
|
685
617
|
this.absolutePath = absolutePath;
|
|
686
|
-
this.logger = logger;
|
|
687
618
|
/**
|
|
688
619
|
* Tells if the sub directory is deleted or not.
|
|
689
620
|
*/
|
|
@@ -693,21 +624,10 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
693
624
|
*/
|
|
694
625
|
this[_b] = "SubDirectory";
|
|
695
626
|
/**
|
|
696
|
-
* The subdirectories the directory is holding
|
|
627
|
+
* The sequenced subdirectories the directory is holding independent of any pending
|
|
628
|
+
* create/delete subdirectory operations.
|
|
697
629
|
*/
|
|
698
|
-
this.
|
|
699
|
-
/**
|
|
700
|
-
* Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
|
|
701
|
-
* of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
702
|
-
* deleted sub directory.
|
|
703
|
-
*/
|
|
704
|
-
this.pendingDeleteSubDirectoriesTracker = new Map();
|
|
705
|
-
/**
|
|
706
|
-
* Subdirectories that have been created locally but not yet ack'd from the server. This maintains the record
|
|
707
|
-
* of create op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
708
|
-
* created sub directory.
|
|
709
|
-
*/
|
|
710
|
-
this.pendingCreateSubDirectoriesTracker = new Map();
|
|
630
|
+
this._sequencedSubdirectories = new Map();
|
|
711
631
|
/**
|
|
712
632
|
* Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
|
|
713
633
|
* of the creation order.
|
|
@@ -726,6 +646,11 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
726
646
|
* even across remote operations and rollbacks.
|
|
727
647
|
*/
|
|
728
648
|
this.pendingStorageData = [];
|
|
649
|
+
/**
|
|
650
|
+
* A data structure containing all local pending subdirectory create/deletes, which is used in combination
|
|
651
|
+
* with the _sequencedSubdirectories to compute optimistic values.
|
|
652
|
+
*/
|
|
653
|
+
this.pendingSubDirectoryData = [];
|
|
729
654
|
/**
|
|
730
655
|
* An internal iterator that iterates over the entries in the directory.
|
|
731
656
|
*/
|
|
@@ -814,8 +739,33 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
814
739
|
? this.sequencedStorageData.has(key)
|
|
815
740
|
: latestPendingEntry.type === "lifetime";
|
|
816
741
|
};
|
|
817
|
-
|
|
818
|
-
|
|
742
|
+
/**
|
|
743
|
+
* Get the optimistic local subdirectory. This combines the sequenced data with
|
|
744
|
+
* any pending changes that have not yet been sequenced. By default, we do not
|
|
745
|
+
* consider disposed directories as optimistically existing, but if `getIfDisposed`
|
|
746
|
+
* is true, we will include them since some scenarios require this.
|
|
747
|
+
*/
|
|
748
|
+
this.getOptimisticSubDirectory = (subdirName, getIfDisposed = false) => {
|
|
749
|
+
const latestPendingEntry = findLast(this.pendingSubDirectoryData, (entry) => entry.subdirName === subdirName);
|
|
750
|
+
let subdir;
|
|
751
|
+
if (latestPendingEntry === undefined) {
|
|
752
|
+
subdir = this._sequencedSubdirectories.get(subdirName);
|
|
753
|
+
}
|
|
754
|
+
else if (latestPendingEntry.type === "createSubDirectory") {
|
|
755
|
+
subdir = latestPendingEntry.subdir;
|
|
756
|
+
assert(subdir !== undefined, 0xc2f /* Subdirectory should exist in pending data */);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
// Pending delete
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
// If the subdirectory is disposed, treat it as non-existent for optimistic reads (unless specified otherwise)
|
|
763
|
+
if (subdir?.disposed && !getIfDisposed) {
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
return subdir;
|
|
767
|
+
};
|
|
768
|
+
this.mc = createChildMonitoringContext({ logger, namespace: "Directory" });
|
|
819
769
|
}
|
|
820
770
|
dispose(error) {
|
|
821
771
|
this._deleted = true;
|
|
@@ -861,8 +811,12 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
861
811
|
throw new Error("Undefined and null keys are not supported");
|
|
862
812
|
}
|
|
863
813
|
const previousOptimisticLocalValue = this.getOptimisticValue(key);
|
|
864
|
-
|
|
865
|
-
|
|
814
|
+
const detachedBind = this.mc.config.getBoolean("Fluid.Directory.AllowDetachedResolve") ?? false;
|
|
815
|
+
if (detachedBind) {
|
|
816
|
+
// Create a local value and serialize it.
|
|
817
|
+
// AB#47081: This will be removed once we can validate that it is no longer needed.
|
|
818
|
+
bindHandles(value, this.serializer, this.directory.handle);
|
|
819
|
+
}
|
|
866
820
|
// If we are not attached, don't submit the op.
|
|
867
821
|
if (!this.directory.isAttached()) {
|
|
868
822
|
this.sequencedStorageData.set(key, value);
|
|
@@ -887,13 +841,20 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
887
841
|
if (latestPendingEntry === undefined ||
|
|
888
842
|
latestPendingEntry.type === "delete" ||
|
|
889
843
|
latestPendingEntry.type === "clear") {
|
|
890
|
-
latestPendingEntry = {
|
|
844
|
+
latestPendingEntry = {
|
|
845
|
+
type: "lifetime",
|
|
846
|
+
path: this.absolutePath,
|
|
847
|
+
key,
|
|
848
|
+
keySets: [],
|
|
849
|
+
subdir: this,
|
|
850
|
+
};
|
|
891
851
|
this.pendingStorageData.push(latestPendingEntry);
|
|
892
852
|
}
|
|
893
853
|
const pendingKeySet = {
|
|
894
854
|
type: "set",
|
|
895
855
|
path: this.absolutePath,
|
|
896
856
|
value,
|
|
857
|
+
subdir: this,
|
|
897
858
|
};
|
|
898
859
|
latestPendingEntry.keySets.push(pendingKeySet);
|
|
899
860
|
const op = {
|
|
@@ -920,7 +881,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
920
881
|
* {@inheritDoc IDirectory.countSubDirectory}
|
|
921
882
|
*/
|
|
922
883
|
countSubDirectory() {
|
|
923
|
-
return this.
|
|
884
|
+
return [...this.subdirectories()].length;
|
|
924
885
|
}
|
|
925
886
|
/**
|
|
926
887
|
* {@inheritDoc IDirectory.createSubDirectory}
|
|
@@ -934,22 +895,47 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
934
895
|
if (subdirName.includes(posix.sep)) {
|
|
935
896
|
throw new Error(`SubDirectory name may not contain ${posix.sep}`);
|
|
936
897
|
}
|
|
937
|
-
|
|
938
|
-
const
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
898
|
+
let subDir = this.getOptimisticSubDirectory(subdirName, true);
|
|
899
|
+
const seqData = this.getLocalSeq();
|
|
900
|
+
const clientId = this.runtime.clientId ?? "detached";
|
|
901
|
+
const isNewSubDirectory = subDir === undefined;
|
|
902
|
+
if (subDir === undefined) {
|
|
903
|
+
// If we do not have optimistically have this subdirectory yet, we should create a new one
|
|
904
|
+
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
905
|
+
subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath, this.mc.logger);
|
|
944
906
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
}
|
|
952
|
-
|
|
907
|
+
else {
|
|
908
|
+
if (subDir.disposed) {
|
|
909
|
+
// In the case that the subdir exists but is disposed, we should
|
|
910
|
+
// still use the existing subdir to maintain any pending changes but
|
|
911
|
+
// ensure it is no longer disposed.
|
|
912
|
+
this.undisposeSubdirectoryTree(subDir);
|
|
913
|
+
}
|
|
914
|
+
subDir.clientIds.add(clientId);
|
|
915
|
+
}
|
|
916
|
+
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
917
|
+
// Only submit the op/emit event if we actually created a new subdir.
|
|
918
|
+
if (isNewSubDirectory) {
|
|
919
|
+
if (this.directory.isAttached()) {
|
|
920
|
+
const pendingSubDirectoryCreate = {
|
|
921
|
+
type: "createSubDirectory",
|
|
922
|
+
subdirName,
|
|
923
|
+
subdir: subDir,
|
|
924
|
+
};
|
|
925
|
+
this.pendingSubDirectoryData.push(pendingSubDirectoryCreate);
|
|
926
|
+
const op = {
|
|
927
|
+
subdirName,
|
|
928
|
+
path: this.absolutePath,
|
|
929
|
+
type: "createSubDirectory",
|
|
930
|
+
};
|
|
931
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
// If we are detached, don't submit the op and directly commit
|
|
935
|
+
// the subdir to _sequencedSubdirectories.
|
|
936
|
+
this._sequencedSubdirectories.set(subdirName, subDir);
|
|
937
|
+
}
|
|
938
|
+
this.emit("subDirectoryCreated", subdirName, true, this);
|
|
953
939
|
}
|
|
954
940
|
return subDir;
|
|
955
941
|
}
|
|
@@ -974,81 +960,88 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
974
960
|
*/
|
|
975
961
|
getSubDirectory(subdirName) {
|
|
976
962
|
this.throwIfDisposed();
|
|
977
|
-
return this.
|
|
963
|
+
return this.getOptimisticSubDirectory(subdirName);
|
|
978
964
|
}
|
|
979
965
|
/**
|
|
980
966
|
* {@inheritDoc IDirectory.hasSubDirectory}
|
|
981
967
|
*/
|
|
982
968
|
hasSubDirectory(subdirName) {
|
|
983
969
|
this.throwIfDisposed();
|
|
984
|
-
return this.
|
|
970
|
+
return this.getOptimisticSubDirectory(subdirName) !== undefined;
|
|
985
971
|
}
|
|
986
972
|
/**
|
|
987
973
|
* {@inheritDoc IDirectory.deleteSubDirectory}
|
|
988
974
|
*/
|
|
989
975
|
deleteSubDirectory(subdirName) {
|
|
990
976
|
this.throwIfDisposed();
|
|
991
|
-
// Delete the sub directory locally first.
|
|
992
|
-
const subDir = this.deleteSubDirectoryCore(subdirName, true);
|
|
993
|
-
// If we are not attached, don't submit the op.
|
|
994
977
|
if (!this.directory.isAttached()) {
|
|
995
|
-
|
|
978
|
+
const previousValue = this._sequencedSubdirectories.get(subdirName);
|
|
979
|
+
const successfullyRemoved = this._sequencedSubdirectories.delete(subdirName);
|
|
980
|
+
// Only emit if we actually deleted something.
|
|
981
|
+
if (successfullyRemoved) {
|
|
982
|
+
this.disposeSubDirectoryTree(previousValue);
|
|
983
|
+
this.emit("subDirectoryDeleted", subdirName, true, this);
|
|
984
|
+
}
|
|
985
|
+
return successfullyRemoved;
|
|
996
986
|
}
|
|
997
|
-
|
|
998
|
-
if (
|
|
999
|
-
|
|
1000
|
-
path: this.absolutePath,
|
|
1001
|
-
subdirName,
|
|
1002
|
-
type: "deleteSubDirectory",
|
|
1003
|
-
};
|
|
1004
|
-
this.submitDeleteSubDirectoryMessage(op, subDir);
|
|
987
|
+
const previousOptimisticSubDirectory = this.getOptimisticSubDirectory(subdirName);
|
|
988
|
+
if (previousOptimisticSubDirectory === undefined) {
|
|
989
|
+
return false;
|
|
1005
990
|
}
|
|
1006
|
-
|
|
991
|
+
const pendingSubdirDelete = {
|
|
992
|
+
type: "deleteSubDirectory",
|
|
993
|
+
subdirName,
|
|
994
|
+
subdir: this,
|
|
995
|
+
};
|
|
996
|
+
this.pendingSubDirectoryData.push(pendingSubdirDelete);
|
|
997
|
+
const op = {
|
|
998
|
+
subdirName,
|
|
999
|
+
type: "deleteSubDirectory",
|
|
1000
|
+
path: this.absolutePath,
|
|
1001
|
+
};
|
|
1002
|
+
this.submitDeleteSubDirectoryMessage(op, previousOptimisticSubDirectory);
|
|
1003
|
+
this.emit("subDirectoryDeleted", subdirName, true, this);
|
|
1004
|
+
// We don't want to fully dispose the subdir tree since this is only a pending
|
|
1005
|
+
// local delete. Instead we will only emit the dispose event to reflect the
|
|
1006
|
+
// local state.
|
|
1007
|
+
this.emitDisposeForSubdirTree(previousOptimisticSubDirectory);
|
|
1008
|
+
return true;
|
|
1007
1009
|
}
|
|
1008
1010
|
/**
|
|
1009
1011
|
* {@inheritDoc IDirectory.subdirectories}
|
|
1010
1012
|
*/
|
|
1011
1013
|
subdirectories() {
|
|
1012
1014
|
this.throwIfDisposed();
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
this.logger.sendTelemetryEvent({
|
|
1023
|
-
eventName: "inconsistentSubdirectoryOrdering",
|
|
1024
|
-
localKeyCount: this.localCreationSeqTracker.size,
|
|
1025
|
-
ackedKeyCount: this.ackedCreationSeqTracker.size,
|
|
1026
|
-
subdirNamesLength: subdirNames.length,
|
|
1027
|
-
subdirectoriesSize: this._subdirectories.size,
|
|
1028
|
-
});
|
|
1029
|
-
hasLoggedDirectoryInconsistency = true;
|
|
1015
|
+
// subdirectories() should reflect the optimistic state of subdirectories.
|
|
1016
|
+
// This means that we should return both sequenced and pending subdirectories
|
|
1017
|
+
// that do not also have a pending deletion.
|
|
1018
|
+
const sequencedSubdirs = [];
|
|
1019
|
+
const sequencedSubdirNames = new Set([...this._sequencedSubdirectories.keys()]);
|
|
1020
|
+
for (const subdirName of sequencedSubdirNames) {
|
|
1021
|
+
const optimisticSubdir = this.getOptimisticSubDirectory(subdirName);
|
|
1022
|
+
if (optimisticSubdir !== undefined) {
|
|
1023
|
+
sequencedSubdirs.push([subdirName, optimisticSubdir]);
|
|
1030
1024
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
[
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
return entriesIterator;
|
|
1025
|
+
}
|
|
1026
|
+
const pendingSubdirNames = [
|
|
1027
|
+
...new Set(this.pendingSubDirectoryData
|
|
1028
|
+
.map((entry) => entry.subdirName)
|
|
1029
|
+
.filter((subdirName) => !sequencedSubdirNames.has(subdirName))),
|
|
1030
|
+
];
|
|
1031
|
+
const pendingSubdirs = [];
|
|
1032
|
+
for (const subdirName of pendingSubdirNames) {
|
|
1033
|
+
const optimisticSubdir = this.getOptimisticSubDirectory(subdirName);
|
|
1034
|
+
if (optimisticSubdir !== undefined) {
|
|
1035
|
+
pendingSubdirs.push([subdirName, optimisticSubdir]);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const allSubdirs = [...sequencedSubdirs, ...pendingSubdirs];
|
|
1039
|
+
const orderedSubdirs = allSubdirs.sort((a, b) => {
|
|
1040
|
+
const aSeqData = a[1].seqData;
|
|
1041
|
+
const bSeqData = b[1].seqData;
|
|
1042
|
+
return seqDataComparator(aSeqData, bSeqData);
|
|
1043
|
+
});
|
|
1044
|
+
return orderedSubdirs[Symbol.iterator]();
|
|
1052
1045
|
}
|
|
1053
1046
|
/**
|
|
1054
1047
|
* {@inheritDoc IDirectory.getWorkingDirectory}
|
|
@@ -1063,10 +1056,10 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1063
1056
|
* @returns true if there is pending delete.
|
|
1064
1057
|
*/
|
|
1065
1058
|
isSubDirectoryDeletePending(subDirName) {
|
|
1066
|
-
|
|
1067
|
-
return
|
|
1068
|
-
}
|
|
1069
|
-
return
|
|
1059
|
+
const lastPendingEntry = findLast(this.pendingSubDirectoryData, (entry) => {
|
|
1060
|
+
return entry.subdirName === subDirName && entry.type === "deleteSubDirectory";
|
|
1061
|
+
});
|
|
1062
|
+
return lastPendingEntry !== undefined;
|
|
1070
1063
|
}
|
|
1071
1064
|
/**
|
|
1072
1065
|
* Deletes the given key from within this IDirectory.
|
|
@@ -1098,6 +1091,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1098
1091
|
type: "delete",
|
|
1099
1092
|
path: this.absolutePath,
|
|
1100
1093
|
key,
|
|
1094
|
+
subdir: this,
|
|
1101
1095
|
};
|
|
1102
1096
|
this.pendingStorageData.push(pendingKeyDelete);
|
|
1103
1097
|
const op = {
|
|
@@ -1137,6 +1131,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1137
1131
|
const pendingClear = {
|
|
1138
1132
|
type: "clear",
|
|
1139
1133
|
path: this.absolutePath,
|
|
1134
|
+
subdir: this,
|
|
1140
1135
|
};
|
|
1141
1136
|
this.pendingStorageData.push(pendingClear);
|
|
1142
1137
|
this.directory.emit("clear", true, this.directory);
|
|
@@ -1153,7 +1148,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1153
1148
|
forEach(callback) {
|
|
1154
1149
|
this.throwIfDisposed();
|
|
1155
1150
|
for (const [key, localValue] of this.internalIterator()) {
|
|
1156
|
-
callback(localValue
|
|
1151
|
+
callback(localValue, key, this);
|
|
1157
1152
|
}
|
|
1158
1153
|
}
|
|
1159
1154
|
/**
|
|
@@ -1241,6 +1236,10 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1241
1236
|
this.throwIfDisposed();
|
|
1242
1237
|
return this.internalIterator();
|
|
1243
1238
|
}
|
|
1239
|
+
get sequencedSubdirectories() {
|
|
1240
|
+
this.throwIfDisposed();
|
|
1241
|
+
return this._sequencedSubdirectories;
|
|
1242
|
+
}
|
|
1244
1243
|
/**
|
|
1245
1244
|
* Process a clear operation.
|
|
1246
1245
|
* @param msg - The message from the server to apply.
|
|
@@ -1251,7 +1250,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1251
1250
|
*/
|
|
1252
1251
|
processClearMessage(msg, op, local, localOpMetadata) {
|
|
1253
1252
|
this.throwIfDisposed();
|
|
1254
|
-
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1253
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
|
|
1255
1254
|
return;
|
|
1256
1255
|
}
|
|
1257
1256
|
if (local) {
|
|
@@ -1295,7 +1294,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1295
1294
|
*/
|
|
1296
1295
|
processDeleteMessage(msg, op, local, localOpMetadata) {
|
|
1297
1296
|
this.throwIfDisposed();
|
|
1298
|
-
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1297
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
|
|
1299
1298
|
return;
|
|
1300
1299
|
}
|
|
1301
1300
|
if (local) {
|
|
@@ -1333,7 +1332,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1333
1332
|
*/
|
|
1334
1333
|
processSetMessage(msg, op, value, local, localOpMetadata) {
|
|
1335
1334
|
this.throwIfDisposed();
|
|
1336
|
-
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1335
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
|
|
1337
1336
|
return;
|
|
1338
1337
|
}
|
|
1339
1338
|
const { key } = op;
|
|
@@ -1371,12 +1370,58 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1371
1370
|
*/
|
|
1372
1371
|
processCreateSubDirectoryMessage(msg, op, local, localOpMetadata) {
|
|
1373
1372
|
this.throwIfDisposed();
|
|
1374
|
-
if (!
|
|
1375
|
-
this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
|
|
1373
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.parentSubdir)) {
|
|
1376
1374
|
return;
|
|
1377
1375
|
}
|
|
1378
1376
|
assertNonNullClientId(msg.clientId);
|
|
1379
|
-
|
|
1377
|
+
let subDir;
|
|
1378
|
+
if (local) {
|
|
1379
|
+
const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName);
|
|
1380
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
1381
|
+
assert(pendingEntry !== undefined && pendingEntry.type === "createSubDirectory", 0xc30 /* Got a local subdir create message we weren't expecting */);
|
|
1382
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
1383
|
+
subDir = pendingEntry.subdir;
|
|
1384
|
+
const existingSubdir = this._sequencedSubdirectories.get(op.subdirName);
|
|
1385
|
+
if (existingSubdir !== undefined) {
|
|
1386
|
+
// If the subdirectory already exists, we don't need to create it again.
|
|
1387
|
+
// This can happen if remote clients also create the same subdir and we processed
|
|
1388
|
+
// that message first.
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (subDir.disposed) {
|
|
1392
|
+
this.undisposeSubdirectoryTree(subDir);
|
|
1393
|
+
}
|
|
1394
|
+
this._sequencedSubdirectories.set(op.subdirName, subDir);
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
subDir = this.getOptimisticSubDirectory(op.subdirName, true);
|
|
1398
|
+
if (subDir === undefined) {
|
|
1399
|
+
const absolutePath = posix.join(this.absolutePath, op.subdirName);
|
|
1400
|
+
subDir = new SubDirectory({ seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber }, new Set([msg.clientId]), this.directory, this.runtime, this.serializer, absolutePath, this.mc.logger);
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
// If the subdirectory already optimistically exists, we don't need to create it again.
|
|
1404
|
+
// This can happen if remote clients also created the same subdir.
|
|
1405
|
+
if (subDir.disposed) {
|
|
1406
|
+
this.undisposeSubdirectoryTree(subDir);
|
|
1407
|
+
}
|
|
1408
|
+
subDir.clientIds.add(msg.clientId);
|
|
1409
|
+
}
|
|
1410
|
+
this.registerEventsOnSubDirectory(subDir, op.subdirName);
|
|
1411
|
+
this._sequencedSubdirectories.set(op.subdirName, subDir);
|
|
1412
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
1413
|
+
if (!this.pendingSubDirectoryData.some((entry) => entry.subdirName === op.subdirName)) {
|
|
1414
|
+
this.emit("subDirectoryCreated", op.subdirName, local, this);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
// Ensure correct seqData. This can be necessary if in scenarios where a subdir was created, deleted, and
|
|
1418
|
+
// then later recreated.
|
|
1419
|
+
if (this.seqData.seq !== -1 &&
|
|
1420
|
+
this.seqData.seq <= msg.sequenceNumber &&
|
|
1421
|
+
subDir.seqData.seq === -1) {
|
|
1422
|
+
subDir.seqData.seq = msg.sequenceNumber;
|
|
1423
|
+
subDir.seqData.clientSeq = msg.clientSequenceNumber;
|
|
1424
|
+
}
|
|
1380
1425
|
}
|
|
1381
1426
|
/**
|
|
1382
1427
|
* Process a delete subdirectory operation.
|
|
@@ -1388,11 +1433,43 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1388
1433
|
*/
|
|
1389
1434
|
processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata) {
|
|
1390
1435
|
this.throwIfDisposed();
|
|
1391
|
-
if (!
|
|
1392
|
-
this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
|
|
1436
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.parentSubdir)) {
|
|
1393
1437
|
return;
|
|
1394
1438
|
}
|
|
1395
|
-
this.
|
|
1439
|
+
const previousValue = this._sequencedSubdirectories.get(op.subdirName);
|
|
1440
|
+
if (previousValue === undefined) {
|
|
1441
|
+
// We are trying to delete a subdirectory that does not exist.
|
|
1442
|
+
// If this is a local delete, we should remove the pending delete entry.
|
|
1443
|
+
// This could happen if we already processed a remote delete op for
|
|
1444
|
+
// the same subdirectory.
|
|
1445
|
+
if (local) {
|
|
1446
|
+
const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName);
|
|
1447
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
1448
|
+
assert(pendingEntry !== undefined &&
|
|
1449
|
+
pendingEntry.type === "deleteSubDirectory" &&
|
|
1450
|
+
pendingEntry.subdirName === op.subdirName, 0xc31 /* Got a local deleteSubDirectory message we weren't expecting */);
|
|
1451
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
1452
|
+
}
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
this._sequencedSubdirectories.delete(op.subdirName);
|
|
1456
|
+
this.disposeSubDirectoryTree(previousValue);
|
|
1457
|
+
if (local) {
|
|
1458
|
+
const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName);
|
|
1459
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
1460
|
+
assert(pendingEntry !== undefined &&
|
|
1461
|
+
pendingEntry.type === "deleteSubDirectory" &&
|
|
1462
|
+
pendingEntry.subdirName === op.subdirName, 0xc32 /* Got a local deleteSubDirectory message we weren't expecting */);
|
|
1463
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
1464
|
+
}
|
|
1465
|
+
else {
|
|
1466
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
1467
|
+
const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName && entry.type === "deleteSubDirectory");
|
|
1468
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
1469
|
+
if (pendingEntry === undefined) {
|
|
1470
|
+
this.emit("subDirectoryDeleted", op.subdirName, local, this);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1396
1473
|
}
|
|
1397
1474
|
/**
|
|
1398
1475
|
* Submit a clear operation.
|
|
@@ -1433,44 +1510,28 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1433
1510
|
resubmitKeyMessage(op, localOpMetadata) {
|
|
1434
1511
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1435
1512
|
// is already deleted, in which case we don't need to submit the op.
|
|
1436
|
-
const pendingEntryIndex = this.pendingStorageData.findIndex((entry) =>
|
|
1513
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => {
|
|
1514
|
+
return op.type === "set"
|
|
1515
|
+
? entry.type === "lifetime" &&
|
|
1516
|
+
entry.key === op.key &&
|
|
1517
|
+
// We also check that the keySets include the localOpMetadata. It's possible we have new
|
|
1518
|
+
// pending key sets that are not the op we are looking for.
|
|
1519
|
+
entry.keySets.includes(localOpMetadata)
|
|
1520
|
+
: entry.type === "delete" && entry.key === op.key;
|
|
1521
|
+
});
|
|
1437
1522
|
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
1438
1523
|
if (pendingEntry !== undefined) {
|
|
1439
1524
|
this.submitKeyMessage(op, localOpMetadata);
|
|
1440
1525
|
}
|
|
1441
1526
|
}
|
|
1442
|
-
incrementPendingSubDirCount(map, subDirName) {
|
|
1443
|
-
const count = map.get(subDirName) ?? 0;
|
|
1444
|
-
map.set(subDirName, count + 1);
|
|
1445
|
-
}
|
|
1446
|
-
decrementPendingSubDirCount(map, subDirName) {
|
|
1447
|
-
const count = map.get(subDirName) ?? 0;
|
|
1448
|
-
map.set(subDirName, count - 1);
|
|
1449
|
-
if (count <= 1) {
|
|
1450
|
-
map.delete(subDirName);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
/**
|
|
1454
|
-
* Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
|
|
1455
|
-
* or while resubmitting the op.
|
|
1456
|
-
*/
|
|
1457
|
-
updatePendingSubDirMessageCount(op) {
|
|
1458
|
-
if (op.type === "deleteSubDirectory") {
|
|
1459
|
-
this.incrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
1460
|
-
}
|
|
1461
|
-
else if (op.type === "createSubDirectory") {
|
|
1462
|
-
this.incrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
1527
|
/**
|
|
1466
1528
|
* Submit a create subdirectory operation.
|
|
1467
1529
|
* @param op - The operation
|
|
1468
1530
|
*/
|
|
1469
1531
|
submitCreateSubDirectoryMessage(op) {
|
|
1470
|
-
this.throwIfDisposed();
|
|
1471
|
-
this.updatePendingSubDirMessageCount(op);
|
|
1472
1532
|
const localOpMetadata = {
|
|
1473
1533
|
type: "createSubDir",
|
|
1534
|
+
parentSubdir: this,
|
|
1474
1535
|
};
|
|
1475
1536
|
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1476
1537
|
}
|
|
@@ -1480,11 +1541,10 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1480
1541
|
* @param subDir - Any subdirectory deleted by the op
|
|
1481
1542
|
*/
|
|
1482
1543
|
submitDeleteSubDirectoryMessage(op, subDir) {
|
|
1483
|
-
this.throwIfDisposed();
|
|
1484
|
-
this.updatePendingSubDirMessageCount(op);
|
|
1485
1544
|
const localOpMetadata = {
|
|
1486
1545
|
type: "deleteSubDir",
|
|
1487
1546
|
subDirectory: subDir,
|
|
1547
|
+
parentSubdir: this,
|
|
1488
1548
|
};
|
|
1489
1549
|
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1490
1550
|
}
|
|
@@ -1496,22 +1556,25 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1496
1556
|
resubmitSubDirectoryMessage(op, localOpMetadata) {
|
|
1497
1557
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
1498
1558
|
// is already deleted, in which case we don't need to submit the op.
|
|
1499
|
-
if (localOpMetadata.type === "createSubDir" &&
|
|
1500
|
-
!this.pendingCreateSubDirectoriesTracker.has(op.subdirName)) {
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
else if (localOpMetadata.type === "deleteSubDir" &&
|
|
1504
|
-
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
1559
|
if (localOpMetadata.type === "createSubDir") {
|
|
1508
|
-
|
|
1509
|
-
this.
|
|
1560
|
+
// For create operations, look specifically for createSubDirectory entries
|
|
1561
|
+
const pendingEntry = findLast(this.pendingSubDirectoryData, (entry) => entry.subdirName === op.subdirName && entry.type === "createSubDirectory");
|
|
1562
|
+
if (pendingEntry !== undefined) {
|
|
1563
|
+
assert(pendingEntry.type === "createSubDirectory", 0xc33 /* pending entry should be createSubDirectory */);
|
|
1564
|
+
// We should add the client id, since when reconnecting it can have a different client id.
|
|
1565
|
+
pendingEntry.subdir.clientIds.add(this.runtime.clientId ?? "detached");
|
|
1566
|
+
// We also need to undelete the subdirectory tree if it was previously deleted
|
|
1567
|
+
this.undisposeSubdirectoryTree(pendingEntry.subdir);
|
|
1568
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
1569
|
+
}
|
|
1510
1570
|
}
|
|
1511
1571
|
else if (localOpMetadata.type === "deleteSubDir") {
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
this.
|
|
1572
|
+
assert(localOpMetadata.subDirectory !== undefined, 0xc34 /* Subdirectory should exist */);
|
|
1573
|
+
// For delete operations, look specifically for deleteSubDirectory entries
|
|
1574
|
+
const pendingEntry = findLast(this.pendingSubDirectoryData, (entry) => entry.subdirName === op.subdirName && entry.type === "deleteSubDirectory");
|
|
1575
|
+
if (pendingEntry !== undefined) {
|
|
1576
|
+
this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
|
|
1577
|
+
}
|
|
1515
1578
|
}
|
|
1516
1579
|
}
|
|
1517
1580
|
/**
|
|
@@ -1552,7 +1615,7 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1552
1615
|
populateSubDirectory(subdirName, newSubDir) {
|
|
1553
1616
|
this.throwIfDisposed();
|
|
1554
1617
|
this.registerEventsOnSubDirectory(newSubDir, subdirName);
|
|
1555
|
-
this.
|
|
1618
|
+
this._sequencedSubdirectories.set(subdirName, newSubDir);
|
|
1556
1619
|
}
|
|
1557
1620
|
/**
|
|
1558
1621
|
* Rollback a local op
|
|
@@ -1565,9 +1628,13 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1565
1628
|
if (directoryOp.type === "clear") {
|
|
1566
1629
|
// A pending clear will be last in the list, since it terminates all prior lifetimes.
|
|
1567
1630
|
const pendingClear = this.pendingStorageData.pop();
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1631
|
+
if (pendingClear === undefined) {
|
|
1632
|
+
// If we can't find a pending entry then it's possible that we deleted an ack'd subdir
|
|
1633
|
+
// from a remote delete subdir op. If that's the case then there is nothing to rollback
|
|
1634
|
+
// since the pending data was removed with the subdirectory deletion.
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
assert(pendingClear.type === "clear" && localOpMetadata.type === "clear", 0xc35 /* Unexpected clear rollback */);
|
|
1571
1638
|
for (const [key] of this.internalIterator()) {
|
|
1572
1639
|
const event = {
|
|
1573
1640
|
key,
|
|
@@ -1585,8 +1652,13 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1585
1652
|
// they were created, not when they were last modified.
|
|
1586
1653
|
const pendingEntryIndex = findLastIndex(this.pendingStorageData, (entry) => entry.type !== "clear" && entry.key === directoryOp.key);
|
|
1587
1654
|
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
1588
|
-
|
|
1589
|
-
|
|
1655
|
+
if (pendingEntry === undefined) {
|
|
1656
|
+
// If we can't find a pending entry then it's possible that we deleted an ack'd subdir
|
|
1657
|
+
// from a remote delete subdir op. If that's the case then there is nothing to rollback
|
|
1658
|
+
// since the pending data was removed with the subdirectory deletion.
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
assert(pendingEntry.type === "delete" || pendingEntry.type === "lifetime", 0xc36 /* Unexpected pending data for set/delete op */);
|
|
1590
1662
|
if (pendingEntry.type === "delete") {
|
|
1591
1663
|
assert(pendingEntry === localOpMetadata, 0xc0b /* Unexpected delete rollback */);
|
|
1592
1664
|
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
@@ -1627,34 +1699,33 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1627
1699
|
else if (directoryOp.type === "createSubDirectory" &&
|
|
1628
1700
|
localOpMetadata.type === "createSubDir") {
|
|
1629
1701
|
const subdirName = directoryOp.subdirName;
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1702
|
+
const pendingEntryIndex = findLastIndex(this.pendingSubDirectoryData, (entry) => entry.type === "createSubDirectory" && entry.subdirName === subdirName);
|
|
1703
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
1704
|
+
assert(pendingEntry !== undefined && pendingEntry.type === "createSubDirectory", 0xc37 /* Unexpected pending data for createSubDirectory op */);
|
|
1705
|
+
// We still need to emit the disposed event for any locally created (and now
|
|
1706
|
+
// rolled back) subdirectory trees so listeners can observer the lifecycle
|
|
1707
|
+
// changes properly. We don't want to fully delete in case there is another
|
|
1708
|
+
// operation that references the same subdirectory.
|
|
1709
|
+
this.emitDisposeForSubdirTree(pendingEntry.subdir);
|
|
1710
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
1711
|
+
this.emit("subDirectoryDeleted", subdirName, true, this);
|
|
1634
1712
|
}
|
|
1635
1713
|
else if (directoryOp.type === "deleteSubDirectory" &&
|
|
1636
1714
|
localOpMetadata.type === "deleteSubDir") {
|
|
1637
1715
|
const subdirName = directoryOp.subdirName;
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
this.localCreationSeqTracker.set(subdirName, {
|
|
1652
|
-
...localOpMetadata.subDirectory.seqData,
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
this.emit("subDirectoryCreated", subdirName, true, this);
|
|
1656
|
-
}
|
|
1657
|
-
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, subdirName);
|
|
1716
|
+
const pendingEntryIndex = findLastIndex(this.pendingSubDirectoryData, (entry) => entry.type === "deleteSubDirectory" && entry.subdirName === subdirName);
|
|
1717
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
1718
|
+
assert(pendingEntry !== undefined && pendingEntry.type === "deleteSubDirectory", 0xc38 /* Unexpected pending data for deleteSubDirectory op */);
|
|
1719
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
1720
|
+
// Restore the subdirectory
|
|
1721
|
+
const subDirectoryToRestore = localOpMetadata.subDirectory;
|
|
1722
|
+
assert(subDirectoryToRestore !== undefined, 0xc39 /* Subdirectory should exist */);
|
|
1723
|
+
// Recursively undispose all nested subdirectories before adding to the map
|
|
1724
|
+
// This ensures the subdirectory is properly restored before being exposed
|
|
1725
|
+
this.undisposeSubdirectoryTree(subDirectoryToRestore);
|
|
1726
|
+
// Re-register events
|
|
1727
|
+
this.registerEventsOnSubDirectory(subDirectoryToRestore, subdirName);
|
|
1728
|
+
this.emit("subDirectoryCreated", subdirName, true, this);
|
|
1658
1729
|
}
|
|
1659
1730
|
else {
|
|
1660
1731
|
throw new Error("Unsupported op for rollback");
|
|
@@ -1672,138 +1743,17 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1672
1743
|
* This return true if the message is for the current instance of this sub directory. As the sub directory
|
|
1673
1744
|
* can be deleted and created again, then this finds if the message is for current instance of directory or not.
|
|
1674
1745
|
* @param msg - message for the directory
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
//
|
|
1679
|
-
//
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
* not process the incoming operation.
|
|
1687
|
-
* @param op - Operation to check
|
|
1688
|
-
* @param local - Whether the message originated from the local client
|
|
1689
|
-
* @param message - The message
|
|
1690
|
-
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
1691
|
-
* For messages from a remote client, this will be undefined.
|
|
1692
|
-
* @returns True if the operation should be processed, false otherwise
|
|
1693
|
-
*/
|
|
1694
|
-
needProcessSubDirectoryOperation(msg, op, local, localOpMetadata) {
|
|
1695
|
-
assertNonNullClientId(msg.clientId);
|
|
1696
|
-
const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
|
|
1697
|
-
const pendingCreateCount = this.pendingCreateSubDirectoriesTracker.get(op.subdirName);
|
|
1698
|
-
if ((pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
|
|
1699
|
-
(pendingCreateCount !== undefined && pendingCreateCount > 0)) {
|
|
1700
|
-
if (local) {
|
|
1701
|
-
assert(localOpMetadata !== undefined, 0xc0d /* localOpMetadata should be defined */);
|
|
1702
|
-
if (localOpMetadata.type === "deleteSubDir") {
|
|
1703
|
-
assert(pendingDeleteCount !== undefined && pendingDeleteCount > 0, 0x6c2 /* pendingDeleteCount should exist */);
|
|
1704
|
-
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
1705
|
-
}
|
|
1706
|
-
else if (localOpMetadata.type === "createSubDir") {
|
|
1707
|
-
assert(pendingCreateCount !== undefined && pendingCreateCount > 0, 0x6c3 /* pendingCreateCount should exist */);
|
|
1708
|
-
this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
if (op.type === "deleteSubDirectory") {
|
|
1712
|
-
const resetSubDirectoryTree = (directory) => {
|
|
1713
|
-
if (!directory) {
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
// If this is delete op and we have keys in this subDirectory, then we need to delete these
|
|
1717
|
-
// keys except the pending ones as they will be sequenced after this delete.
|
|
1718
|
-
directory.sequencedStorageData.clear();
|
|
1719
|
-
directory.emit("clear", true, directory);
|
|
1720
|
-
// In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
|
|
1721
|
-
// creators as the previous directory is getting deleted and we will initialize again when
|
|
1722
|
-
// we will receive op for the create again.
|
|
1723
|
-
directory.seqData.seq = -1;
|
|
1724
|
-
directory.seqData.clientSeq = -1;
|
|
1725
|
-
directory.clientIds.clear();
|
|
1726
|
-
// Do the same thing for the subtree of the directory. If create is not pending for a child, then just
|
|
1727
|
-
// delete it.
|
|
1728
|
-
const subDirectories = directory.subdirectories();
|
|
1729
|
-
for (const [subDirName, subDir] of subDirectories) {
|
|
1730
|
-
if (directory.pendingCreateSubDirectoriesTracker.has(subDirName)) {
|
|
1731
|
-
resetSubDirectoryTree(subDir);
|
|
1732
|
-
continue;
|
|
1733
|
-
}
|
|
1734
|
-
directory.deleteSubDirectoryCore(subDirName, false);
|
|
1735
|
-
}
|
|
1736
|
-
};
|
|
1737
|
-
const subDirectory = this._subdirectories.get(op.subdirName);
|
|
1738
|
-
// Clear the creation tracker record
|
|
1739
|
-
this.ackedCreationSeqTracker.delete(op.subdirName);
|
|
1740
|
-
resetSubDirectoryTree(subDirectory);
|
|
1741
|
-
}
|
|
1742
|
-
if (op.type === "createSubDirectory") {
|
|
1743
|
-
const dir = this._subdirectories.get(op.subdirName);
|
|
1744
|
-
// Child sub directory create seq number can't be lower than the parent subdirectory.
|
|
1745
|
-
// The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
|
|
1746
|
-
if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
|
|
1747
|
-
if (dir?.seqData.seq === -1) {
|
|
1748
|
-
// Only set the sequence data based on the first message
|
|
1749
|
-
dir.seqData.seq = msg.sequenceNumber;
|
|
1750
|
-
dir.seqData.clientSeq = msg.clientSequenceNumber;
|
|
1751
|
-
// set the creation seq in tracker
|
|
1752
|
-
if (!this.ackedCreationSeqTracker.has(op.subdirName) &&
|
|
1753
|
-
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
|
|
1754
|
-
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
1755
|
-
seq: msg.sequenceNumber,
|
|
1756
|
-
clientSeq: msg.clientSequenceNumber,
|
|
1757
|
-
});
|
|
1758
|
-
if (local) {
|
|
1759
|
-
this.localCreationSeqTracker.delete(op.subdirName);
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
// The client created the dir at or after the dirs seq, so list its client id as a creator.
|
|
1764
|
-
if (dir !== undefined &&
|
|
1765
|
-
!dir.clientIds.has(msg.clientId) &&
|
|
1766
|
-
dir.seqData.seq <= msg.sequenceNumber) {
|
|
1767
|
-
dir.clientIds.add(msg.clientId);
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
return false;
|
|
1772
|
-
}
|
|
1773
|
-
return !local;
|
|
1774
|
-
}
|
|
1775
|
-
/**
|
|
1776
|
-
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
1777
|
-
* @param subdirName - The name of the subdirectory being created
|
|
1778
|
-
* @param local - Whether the message originated from the local client
|
|
1779
|
-
* @param seqData - Sequence number and client sequence number at which this directory is created
|
|
1780
|
-
* @param clientId - Id of client which created this directory.
|
|
1781
|
-
* @returns True if is newly created, false if it already existed.
|
|
1782
|
-
*/
|
|
1783
|
-
createSubDirectoryCore(subdirName, local, seqData, clientId) {
|
|
1784
|
-
const subdir = this._subdirectories.get(subdirName);
|
|
1785
|
-
if (subdir === undefined) {
|
|
1786
|
-
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
1787
|
-
const subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath, this.logger);
|
|
1788
|
-
/**
|
|
1789
|
-
* Store the sequence numbers of newly created subdirectory to the proper creation tracker, based
|
|
1790
|
-
* on whether the creation behavior has been ack'd or not
|
|
1791
|
-
*/
|
|
1792
|
-
if (isAcknowledgedOrDetached(seqData)) {
|
|
1793
|
-
this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
|
|
1794
|
-
}
|
|
1795
|
-
else {
|
|
1796
|
-
this.localCreationSeqTracker.set(subdirName, { ...seqData });
|
|
1797
|
-
}
|
|
1798
|
-
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
1799
|
-
this._subdirectories.set(subdirName, subDir);
|
|
1800
|
-
this.emit("subDirectoryCreated", subdirName, local, this);
|
|
1801
|
-
return true;
|
|
1802
|
-
}
|
|
1803
|
-
else {
|
|
1804
|
-
subdir.clientIds.add(clientId);
|
|
1805
|
-
}
|
|
1806
|
-
return false;
|
|
1746
|
+
* @param targetSubdir - subdirectory instance we are targeting from local op metadata (if a local op)
|
|
1747
|
+
*/
|
|
1748
|
+
isMessageForCurrentInstanceOfSubDirectory(msg, targetSubdir) {
|
|
1749
|
+
// The message must be from this instance of the directory (if a local op) AND one of the following must be true:
|
|
1750
|
+
// 1. The message was from the creator of this directory
|
|
1751
|
+
// 2. This directory was created while detached
|
|
1752
|
+
// 3. This directory was already live (known to other clients) and the op was created after the directory was created.
|
|
1753
|
+
return ((targetSubdir === undefined || targetSubdir === this) &&
|
|
1754
|
+
((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
|
|
1755
|
+
this.clientIds.has("detached") ||
|
|
1756
|
+
(this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber)));
|
|
1807
1757
|
}
|
|
1808
1758
|
registerEventsOnSubDirectory(subDirectory, subDirName) {
|
|
1809
1759
|
subDirectory.on("subDirectoryCreated", (relativePath, local) => {
|
|
@@ -1813,34 +1763,8 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1813
1763
|
this.emit("subDirectoryDeleted", posix.join(subDirName, relativePath), local, this);
|
|
1814
1764
|
});
|
|
1815
1765
|
}
|
|
1816
|
-
/**
|
|
1817
|
-
* Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
1818
|
-
* @param subdirName - The name of the subdirectory being deleted
|
|
1819
|
-
* @param local - Whether the message originated from the local client
|
|
1820
|
-
*/
|
|
1821
|
-
deleteSubDirectoryCore(subdirName, local) {
|
|
1822
|
-
const previousValue = this._subdirectories.get(subdirName);
|
|
1823
|
-
// This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
|
|
1824
|
-
// Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
|
|
1825
|
-
if (previousValue !== undefined) {
|
|
1826
|
-
this._subdirectories.delete(subdirName);
|
|
1827
|
-
/**
|
|
1828
|
-
* Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
|
|
1829
|
-
* ack'd already or still not committed yet (could be both).
|
|
1830
|
-
*/
|
|
1831
|
-
if (this.ackedCreationSeqTracker.has(subdirName)) {
|
|
1832
|
-
this.ackedCreationSeqTracker.delete(subdirName);
|
|
1833
|
-
}
|
|
1834
|
-
if (this.localCreationSeqTracker.has(subdirName)) {
|
|
1835
|
-
this.localCreationSeqTracker.delete(subdirName);
|
|
1836
|
-
}
|
|
1837
|
-
this.disposeSubDirectoryTree(previousValue);
|
|
1838
|
-
this.emit("subDirectoryDeleted", subdirName, local, this);
|
|
1839
|
-
}
|
|
1840
|
-
return previousValue;
|
|
1841
|
-
}
|
|
1842
1766
|
disposeSubDirectoryTree(directory) {
|
|
1843
|
-
if (
|
|
1767
|
+
if (directory === undefined) {
|
|
1844
1768
|
return;
|
|
1845
1769
|
}
|
|
1846
1770
|
// Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
|
|
@@ -1848,17 +1772,76 @@ class SubDirectory extends TypedEventEmitter {
|
|
|
1848
1772
|
for (const [_, subDirectory] of subDirectories) {
|
|
1849
1773
|
this.disposeSubDirectoryTree(subDirectory);
|
|
1850
1774
|
}
|
|
1775
|
+
// We need to reset the sequenced data as the previous directory is getting deleted and we will
|
|
1776
|
+
// initialize again when we will receive op for the create again.
|
|
1777
|
+
directory.clearSubDirectorySequencedData();
|
|
1778
|
+
directory.dispose();
|
|
1779
|
+
}
|
|
1780
|
+
emitDisposeForSubdirTree(directory) {
|
|
1781
|
+
if (directory === undefined || directory.disposed) {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
// Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
|
|
1785
|
+
const subDirectories = directory.subdirectories();
|
|
1786
|
+
for (const [_, subDirectory] of subDirectories) {
|
|
1787
|
+
this.emitDisposeForSubdirTree(subDirectory);
|
|
1788
|
+
}
|
|
1851
1789
|
if (typeof directory.dispose === "function") {
|
|
1852
|
-
directory.
|
|
1790
|
+
directory.emit("disposed", directory);
|
|
1853
1791
|
}
|
|
1854
1792
|
}
|
|
1855
|
-
|
|
1856
|
-
//
|
|
1857
|
-
|
|
1793
|
+
undisposeSubdirectoryTree(directory) {
|
|
1794
|
+
// This will unmark "deleted" from the subdirectories from bottom to top.
|
|
1795
|
+
for (const [_, subDirectory] of directory.getSubdirectoriesEvenIfDisposed()) {
|
|
1796
|
+
this.undisposeSubdirectoryTree(subDirectory);
|
|
1797
|
+
}
|
|
1858
1798
|
directory.undispose();
|
|
1859
|
-
|
|
1860
|
-
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Similar to {@link subdirectories}, but also includes subdirectories that are disposed.
|
|
1802
|
+
*/
|
|
1803
|
+
getSubdirectoriesEvenIfDisposed() {
|
|
1804
|
+
const sequencedSubdirs = [];
|
|
1805
|
+
const sequencedSubdirNames = new Set([...this._sequencedSubdirectories.keys()]);
|
|
1806
|
+
for (const subdirName of sequencedSubdirNames) {
|
|
1807
|
+
const optimisticSubdir = this.getOptimisticSubDirectory(subdirName, true);
|
|
1808
|
+
if (optimisticSubdir !== undefined) {
|
|
1809
|
+
sequencedSubdirs.push([subdirName, optimisticSubdir]);
|
|
1810
|
+
}
|
|
1861
1811
|
}
|
|
1812
|
+
const pendingSubdirNames = [
|
|
1813
|
+
...new Set(this.pendingSubDirectoryData
|
|
1814
|
+
.map((entry) => entry.subdirName)
|
|
1815
|
+
.filter((subdirName) => !sequencedSubdirNames.has(subdirName))),
|
|
1816
|
+
];
|
|
1817
|
+
const pendingSubdirs = [];
|
|
1818
|
+
for (const subdirName of pendingSubdirNames) {
|
|
1819
|
+
const optimisticSubdir = this.getOptimisticSubDirectory(subdirName, true);
|
|
1820
|
+
if (optimisticSubdir !== undefined) {
|
|
1821
|
+
pendingSubdirs.push([subdirName, optimisticSubdir]);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
const allSubdirs = [...sequencedSubdirs, ...pendingSubdirs];
|
|
1825
|
+
const orderedSubdirs = allSubdirs.sort((a, b) => {
|
|
1826
|
+
const aSeqData = a[1].seqData;
|
|
1827
|
+
const bSeqData = b[1].seqData;
|
|
1828
|
+
assert(aSeqData !== undefined && bSeqData !== undefined, 0xc3a /* seqData should be defined */);
|
|
1829
|
+
return seqDataComparator(aSeqData, bSeqData);
|
|
1830
|
+
});
|
|
1831
|
+
return orderedSubdirs[Symbol.iterator]();
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Clears the sequenced data of a subdirectory but notably retains the pending
|
|
1835
|
+
* storage data. This is done when disposing of a directory so if we need to
|
|
1836
|
+
* re-create it, then we still have the pending ops.
|
|
1837
|
+
*/
|
|
1838
|
+
clearSubDirectorySequencedData() {
|
|
1839
|
+
this.seqData.seq = -1;
|
|
1840
|
+
this.seqData.clientSeq = -1;
|
|
1841
|
+
this.sequencedStorageData.clear();
|
|
1842
|
+
this._sequencedSubdirectories.clear();
|
|
1843
|
+
this.clientIds.clear();
|
|
1844
|
+
this.clientIds.add(this.runtime.clientId ?? "detached");
|
|
1862
1845
|
}
|
|
1863
1846
|
}
|
|
1864
1847
|
//# sourceMappingURL=directory.js.map
|