@fluidframework/map 2.0.0-dev.7.3.0.212138 → 2.0.0-dev.7.4.0.215366
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/api-extractor.json +9 -1
- package/dist/directory.cjs +219 -24
- package/dist/directory.cjs.map +1 -1
- package/dist/directory.d.ts +485 -2
- package/dist/directory.d.ts.map +1 -1
- package/dist/packageVersion.cjs +1 -1
- package/dist/packageVersion.cjs.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/lib/directory.d.ts +485 -2
- package/lib/directory.d.ts.map +1 -1
- package/lib/directory.mjs +219 -24
- package/lib/directory.mjs.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.mjs +1 -1
- package/lib/packageVersion.mjs.map +1 -1
- package/package.json +17 -16
- package/src/directory.ts +261 -22
- package/src/packageVersion.ts +1 -1
package/lib/packageVersion.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
|
|
6
6
|
*/
|
|
7
7
|
export declare const pkgName = "@fluidframework/map";
|
|
8
|
-
export declare const pkgVersion = "2.0.0-dev.7.
|
|
8
|
+
export declare const pkgVersion = "2.0.0-dev.7.4.0.215366";
|
|
9
9
|
//# sourceMappingURL=packageVersion.d.ts.map
|
package/lib/packageVersion.mjs
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
|
|
6
6
|
*/
|
|
7
7
|
export const pkgName = "@fluidframework/map";
|
|
8
|
-
export const pkgVersion = "2.0.0-dev.7.
|
|
8
|
+
export const pkgVersion = "2.0.0-dev.7.4.0.215366";
|
|
9
9
|
//# sourceMappingURL=packageVersion.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"packageVersion.mjs","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAC7C,MAAM,CAAC,MAAM,UAAU,GAAG,wBAAwB,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/map\";\nexport const pkgVersion = \"2.0.0-dev.7.
|
|
1
|
+
{"version":3,"file":"packageVersion.mjs","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAC7C,MAAM,CAAC,MAAM,UAAU,GAAG,wBAAwB,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/map\";\nexport const pkgVersion = \"2.0.0-dev.7.4.0.215366\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluidframework/map",
|
|
3
|
-
"version": "2.0.0-dev.7.
|
|
3
|
+
"version": "2.0.0-dev.7.4.0.215366",
|
|
4
4
|
"description": "Distributed map",
|
|
5
5
|
"homepage": "https://fluidframework.com",
|
|
6
6
|
"repository": {
|
|
@@ -35,29 +35,30 @@
|
|
|
35
35
|
"temp-directory": "nyc/.nyc_output"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@fluid-internal/client-utils": "2.0.0-dev.7.
|
|
39
|
-
"@fluidframework/core-interfaces": "2.0.0-dev.7.
|
|
40
|
-
"@fluidframework/core-utils": "2.0.0-dev.7.
|
|
41
|
-
"@fluidframework/datastore-definitions": "2.0.0-dev.7.
|
|
42
|
-
"@fluidframework/driver-utils": "2.0.0-dev.7.
|
|
38
|
+
"@fluid-internal/client-utils": "2.0.0-dev.7.4.0.215366",
|
|
39
|
+
"@fluidframework/core-interfaces": "2.0.0-dev.7.4.0.215366",
|
|
40
|
+
"@fluidframework/core-utils": "2.0.0-dev.7.4.0.215366",
|
|
41
|
+
"@fluidframework/datastore-definitions": "2.0.0-dev.7.4.0.215366",
|
|
42
|
+
"@fluidframework/driver-utils": "2.0.0-dev.7.4.0.215366",
|
|
43
|
+
"@fluidframework/merge-tree": "2.0.0-dev.7.4.0.215366",
|
|
43
44
|
"@fluidframework/protocol-definitions": "^3.0.0",
|
|
44
|
-
"@fluidframework/runtime-definitions": "2.0.0-dev.7.
|
|
45
|
-
"@fluidframework/runtime-utils": "2.0.0-dev.7.
|
|
46
|
-
"@fluidframework/shared-object-base": "2.0.0-dev.7.
|
|
47
|
-
"@fluidframework/telemetry-utils": "2.0.0-dev.7.
|
|
45
|
+
"@fluidframework/runtime-definitions": "2.0.0-dev.7.4.0.215366",
|
|
46
|
+
"@fluidframework/runtime-utils": "2.0.0-dev.7.4.0.215366",
|
|
47
|
+
"@fluidframework/shared-object-base": "2.0.0-dev.7.4.0.215366",
|
|
48
|
+
"@fluidframework/telemetry-utils": "2.0.0-dev.7.4.0.215366",
|
|
48
49
|
"path-browserify": "^1.0.1"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@fluid-private/stochastic-test-utils": "2.0.0-dev.7.
|
|
52
|
-
"@fluid-private/test-dds-utils": "2.0.0-dev.7.
|
|
52
|
+
"@fluid-private/stochastic-test-utils": "2.0.0-dev.7.4.0.215366",
|
|
53
|
+
"@fluid-private/test-dds-utils": "2.0.0-dev.7.4.0.215366",
|
|
53
54
|
"@fluid-tools/benchmark": "^0.48.0",
|
|
54
55
|
"@fluid-tools/build-cli": "^0.28.0",
|
|
55
56
|
"@fluidframework/build-common": "^2.0.3",
|
|
56
57
|
"@fluidframework/build-tools": "^0.28.0",
|
|
57
58
|
"@fluidframework/eslint-config-fluid": "^3.1.0",
|
|
58
59
|
"@fluidframework/map-previous": "npm:@fluidframework/map@2.0.0-internal.7.2.0",
|
|
59
|
-
"@fluidframework/mocha-test-setup": "2.0.0-dev.7.
|
|
60
|
-
"@fluidframework/test-runtime-utils": "2.0.0-dev.7.
|
|
60
|
+
"@fluidframework/mocha-test-setup": "2.0.0-dev.7.4.0.215366",
|
|
61
|
+
"@fluidframework/test-runtime-utils": "2.0.0-dev.7.4.0.215366",
|
|
61
62
|
"@microsoft/api-extractor": "^7.38.3",
|
|
62
63
|
"@types/mocha": "^9.1.1",
|
|
63
64
|
"@types/node": "^16.18.38",
|
|
@@ -108,8 +109,8 @@
|
|
|
108
109
|
"format": "npm run prettier:fix",
|
|
109
110
|
"lint": "npm run prettier && npm run eslint",
|
|
110
111
|
"lint:fix": "npm run prettier:fix && npm run eslint:fix",
|
|
111
|
-
"prettier": "prettier --check . --ignore-path ../../../.prettierignore",
|
|
112
|
-
"prettier:fix": "prettier --write . --ignore-path ../../../.prettierignore",
|
|
112
|
+
"prettier": "prettier --check . --cache --ignore-path ../../../.prettierignore",
|
|
113
|
+
"prettier:fix": "prettier --write . --cache --ignore-path ../../../.prettierignore",
|
|
113
114
|
"test": "npm run test:mocha",
|
|
114
115
|
"test:coverage": "c8 npm test",
|
|
115
116
|
"test:memory": "mocha --config ./src/test/memory/.mocharc.cjs",
|
package/src/directory.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { ISummaryTreeWithStats, ITelemetryContext } from "@fluidframework/runtim
|
|
|
19
19
|
import { IFluidSerializer, SharedObject, ValueType } from "@fluidframework/shared-object-base";
|
|
20
20
|
import { SummaryTreeBuilder } from "@fluidframework/runtime-utils";
|
|
21
21
|
import * as path from "path-browserify";
|
|
22
|
+
import { RedBlackTree } from "@fluidframework/merge-tree";
|
|
22
23
|
import {
|
|
23
24
|
IDirectory,
|
|
24
25
|
IDirectoryEvents,
|
|
@@ -336,6 +337,115 @@ export class DirectoryFactory implements IChannelFactory {
|
|
|
336
337
|
}
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
/**
|
|
341
|
+
* The comparator essentially performs the following procedure to determine the order of subdirectory creation:
|
|
342
|
+
* 1. If subdirectory A has a non-negative 'seq' and subdirectory B has a negative 'seq', subdirectory A is always placed first due to
|
|
343
|
+
* the policy that acknowledged subdirectories precede locally created ones that have not been committed yet.
|
|
344
|
+
*
|
|
345
|
+
* 2. When both subdirectories A and B have a non-negative 'seq', they are compared as follows:
|
|
346
|
+
* - 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
|
|
347
|
+
* should not be applied in the directory ordering, since the lowest 'seq' is -1, when the directory is created locally but not acknowledged yet.
|
|
348
|
+
* - 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
|
|
349
|
+
* batching is enabled, and a lower 'clientSeq' indicates that it was processed earlier after the batch was ungrouped.
|
|
350
|
+
*
|
|
351
|
+
* 3. When both subdirectories A and B have a negative 'seq', they are compared as follows:
|
|
352
|
+
* - If A and B have different 'seq', the one with lower 'seq' will be positioned ahead, which indicates the corresponding creation message was
|
|
353
|
+
* acknowledged by the server earlier.
|
|
354
|
+
* - 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
|
|
355
|
+
* and B were created locally and not acknowledged yet, with the one possessing the lower 'clientSeq' being created earlier.
|
|
356
|
+
*
|
|
357
|
+
* 4. A 'seq' value of zero indicates that the subdirectory was created in detached state, and it is considered acknowledged for the
|
|
358
|
+
* purpose of ordering.
|
|
359
|
+
*/
|
|
360
|
+
const seqDataComparator = (a: SequenceData, b: SequenceData) => {
|
|
361
|
+
if (isAcknowledgedOrDetached(a)) {
|
|
362
|
+
if (isAcknowledgedOrDetached(b)) {
|
|
363
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
364
|
+
return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq! - b.clientSeq!;
|
|
365
|
+
} else {
|
|
366
|
+
return -1;
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
if (!isAcknowledgedOrDetached(b)) {
|
|
370
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
371
|
+
return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq! - b.clientSeq!;
|
|
372
|
+
} else {
|
|
373
|
+
return 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
function isAcknowledgedOrDetached(seqData: SequenceData) {
|
|
379
|
+
return seqData.seq >= 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The combination of sequence numebr and client sequence number of a subdirectory
|
|
384
|
+
*/
|
|
385
|
+
interface SequenceData {
|
|
386
|
+
seq: number;
|
|
387
|
+
clientSeq?: number;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* A utility class for tracking associations between keys and their creation indices.
|
|
392
|
+
* This is relevant to support map iteration in insertion order, see
|
|
393
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
|
|
394
|
+
*
|
|
395
|
+
* TODO: It can be combined with the creation tracker utilized in SharedMap
|
|
396
|
+
*/
|
|
397
|
+
class DirectoryCreationTracker {
|
|
398
|
+
readonly indexToKey: RedBlackTree<SequenceData, string>;
|
|
399
|
+
|
|
400
|
+
readonly keyToIndex: Map<string, SequenceData>;
|
|
401
|
+
|
|
402
|
+
constructor() {
|
|
403
|
+
this.indexToKey = new RedBlackTree<SequenceData, string>(seqDataComparator);
|
|
404
|
+
this.keyToIndex = new Map<string, SequenceData>();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
set(key: string, seqData: SequenceData): void {
|
|
408
|
+
this.indexToKey.put(seqData, key);
|
|
409
|
+
this.keyToIndex.set(key, seqData);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
has(keyOrSeqData: string | SequenceData): boolean {
|
|
413
|
+
return typeof keyOrSeqData === "string"
|
|
414
|
+
? this.keyToIndex.has(keyOrSeqData)
|
|
415
|
+
: this.indexToKey.get(keyOrSeqData) !== undefined;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
delete(keyOrSeqData: string | SequenceData): void {
|
|
419
|
+
if (this.has(keyOrSeqData)) {
|
|
420
|
+
if (typeof keyOrSeqData === "string") {
|
|
421
|
+
const seqData = this.keyToIndex.get(keyOrSeqData) as SequenceData;
|
|
422
|
+
this.keyToIndex.delete(keyOrSeqData);
|
|
423
|
+
this.indexToKey.remove(seqData);
|
|
424
|
+
} else {
|
|
425
|
+
const key = this.indexToKey.get(keyOrSeqData)?.data as string;
|
|
426
|
+
this.indexToKey.remove(keyOrSeqData);
|
|
427
|
+
this.keyToIndex.delete(key);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Retrieves all subdirectories with creation order that satisfy an optional constraint function.
|
|
434
|
+
* @param constraint - An optional constraint function that filters keys.
|
|
435
|
+
* @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
|
|
436
|
+
*/
|
|
437
|
+
keys(constraint?: (key: string) => boolean): string[] {
|
|
438
|
+
const keys: string[] = [];
|
|
439
|
+
this.indexToKey.mapRange((node) => {
|
|
440
|
+
if (!constraint || constraint(node.data)) {
|
|
441
|
+
keys.push(node.data);
|
|
442
|
+
}
|
|
443
|
+
return true;
|
|
444
|
+
}, keys);
|
|
445
|
+
return keys;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
339
449
|
/**
|
|
340
450
|
* {@inheritDoc ISharedDirectory}
|
|
341
451
|
*
|
|
@@ -395,7 +505,7 @@ export class SharedDirectory
|
|
|
395
505
|
* Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
|
|
396
506
|
*/
|
|
397
507
|
private readonly root: SubDirectory = new SubDirectory(
|
|
398
|
-
0,
|
|
508
|
+
{ seq: 0, clientSeq: 0 },
|
|
399
509
|
new Set(),
|
|
400
510
|
this,
|
|
401
511
|
this.runtime,
|
|
@@ -672,21 +782,42 @@ export class SharedDirectory
|
|
|
672
782
|
protected populate(data: IDirectoryDataObject): void {
|
|
673
783
|
const stack: [SubDirectory, IDirectoryDataObject][] = [];
|
|
674
784
|
stack.push([this.root, data]);
|
|
785
|
+
|
|
675
786
|
while (stack.length > 0) {
|
|
676
787
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
677
788
|
const [currentSubDir, currentSubDirObject] = stack.pop()!;
|
|
678
789
|
if (currentSubDirObject.subdirectories) {
|
|
790
|
+
// Utilize a map to store the seq -> clientSeq for the newly created subdirectory
|
|
791
|
+
const tempSeqNums = new Map<number, number>();
|
|
679
792
|
for (const [subdirName, subdirObject] of Object.entries(
|
|
680
793
|
currentSubDirObject.subdirectories,
|
|
681
794
|
)) {
|
|
682
795
|
let newSubDir = currentSubDir.getSubDirectory(subdirName) as SubDirectory;
|
|
796
|
+
let seqData: SequenceData;
|
|
683
797
|
if (!newSubDir) {
|
|
684
798
|
const createInfo = subdirObject.ci;
|
|
685
|
-
|
|
799
|
+
// We do not store the client sequence number in the storage because the order has already been
|
|
800
|
+
// guaranteed during the serialization process. As a result, it is only essential to utilize the
|
|
801
|
+
// "fake" client sequence number to signify the loading order, and there is no need to retain
|
|
802
|
+
// the actual client sequence number at this point.
|
|
803
|
+
if (createInfo !== undefined && createInfo.csn > -1) {
|
|
686
804
|
// If csn is -1, then initialize it with 0, otherwise we will never process ops for this
|
|
687
805
|
// sub directory. This could be done at serialization time too, but we need to maintain
|
|
688
806
|
// back compat too and also we will actually know the state when it was serialized.
|
|
689
|
-
|
|
807
|
+
if (!tempSeqNums.has(createInfo.csn)) {
|
|
808
|
+
tempSeqNums.set(createInfo.csn, 0);
|
|
809
|
+
}
|
|
810
|
+
let fakeClientSeq = tempSeqNums.get(createInfo.csn) as number;
|
|
811
|
+
seqData = { seq: createInfo.csn, clientSeq: fakeClientSeq };
|
|
812
|
+
tempSeqNums.set(createInfo.csn, ++fakeClientSeq);
|
|
813
|
+
} else {
|
|
814
|
+
seqData = {
|
|
815
|
+
seq: 0,
|
|
816
|
+
clientSeq: ++currentSubDir.localCreationSeq,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
newSubDir = new SubDirectory(
|
|
820
|
+
seqData,
|
|
690
821
|
createInfo !== undefined
|
|
691
822
|
? new Set<string>(createInfo.ccIds)
|
|
692
823
|
: new Set(),
|
|
@@ -696,6 +827,10 @@ export class SharedDirectory
|
|
|
696
827
|
posix.join(currentSubDir.absolutePath, subdirName),
|
|
697
828
|
);
|
|
698
829
|
currentSubDir.populateSubDirectory(subdirName, newSubDir);
|
|
830
|
+
// Record the newly inserted subdirectory to the creation tracker
|
|
831
|
+
currentSubDir.ackedCreationSeqTracker.set(subdirName, {
|
|
832
|
+
...seqData,
|
|
833
|
+
});
|
|
699
834
|
}
|
|
700
835
|
stack.push([newSubDir, subdirObject]);
|
|
701
836
|
}
|
|
@@ -1057,7 +1192,7 @@ interface IDeleteSubDirLocalOpMetadata {
|
|
|
1057
1192
|
}
|
|
1058
1193
|
|
|
1059
1194
|
type SubDirLocalOpMetadata = ICreateSubDirLocalOpMetadata | IDeleteSubDirLocalOpMetadata;
|
|
1060
|
-
type DirectoryLocalOpMetadata =
|
|
1195
|
+
export type DirectoryLocalOpMetadata =
|
|
1061
1196
|
| IClearLocalOpMetadata
|
|
1062
1197
|
| IKeyEditLocalOpMetadata
|
|
1063
1198
|
| SubDirLocalOpMetadata;
|
|
@@ -1159,6 +1294,23 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1159
1294
|
*/
|
|
1160
1295
|
private readonly pendingClearMessageIds: number[] = [];
|
|
1161
1296
|
|
|
1297
|
+
/**
|
|
1298
|
+
* Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
|
|
1299
|
+
* of the creation order.
|
|
1300
|
+
*/
|
|
1301
|
+
public localCreationSeq: number = 0;
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Maintains a bidirectional association between ack'd subdirectories and their seqData.
|
|
1305
|
+
* This helps to ensure iteration order which is consistent with the JS map spec.
|
|
1306
|
+
*/
|
|
1307
|
+
public readonly ackedCreationSeqTracker: DirectoryCreationTracker;
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Similar to {@link ackedCreationSeqTracker}, but for local (unacked) entries.
|
|
1311
|
+
*/
|
|
1312
|
+
public readonly localCreationSeqTracker: DirectoryCreationTracker;
|
|
1313
|
+
|
|
1162
1314
|
/**
|
|
1163
1315
|
* Constructor.
|
|
1164
1316
|
* @param sequenceNumber - Message seq number at which this was created.
|
|
@@ -1169,7 +1321,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1169
1321
|
* @param absolutePath - The absolute path of this IDirectory
|
|
1170
1322
|
*/
|
|
1171
1323
|
public constructor(
|
|
1172
|
-
private
|
|
1324
|
+
private readonly seqData: SequenceData,
|
|
1173
1325
|
private readonly clientIds: Set<string>,
|
|
1174
1326
|
private readonly directory: SharedDirectory,
|
|
1175
1327
|
private readonly runtime: IFluidDataStoreRuntime,
|
|
@@ -1177,6 +1329,8 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1177
1329
|
public readonly absolutePath: string,
|
|
1178
1330
|
) {
|
|
1179
1331
|
super();
|
|
1332
|
+
this.localCreationSeqTracker = new DirectoryCreationTracker();
|
|
1333
|
+
this.ackedCreationSeqTracker = new DirectoryCreationTracker();
|
|
1180
1334
|
}
|
|
1181
1335
|
|
|
1182
1336
|
public dispose(error?: Error): void {
|
|
@@ -1306,14 +1460,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1306
1460
|
}
|
|
1307
1461
|
|
|
1308
1462
|
/**
|
|
1309
|
-
* @returns
|
|
1463
|
+
* @returns The Sequence Data which should be used for local changes.
|
|
1310
1464
|
* @remarks While detached, 0 is used rather than -1 to represent a change which should be universally known (as opposed to known
|
|
1311
1465
|
* only by the local client). This ensures that if the directory is later attached, none of its data needs to be updated (the values
|
|
1312
1466
|
* last set while detached will now be known to any new client, until they are changed).
|
|
1467
|
+
*
|
|
1468
|
+
* The client sequence number is incremented by 1 for maintaining the internal order of locally created subdirectories
|
|
1313
1469
|
* TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
|
|
1314
1470
|
*/
|
|
1315
|
-
private getLocalSeq():
|
|
1316
|
-
return this.directory.isAttached()
|
|
1471
|
+
private getLocalSeq(): SequenceData {
|
|
1472
|
+
return this.directory.isAttached()
|
|
1473
|
+
? { seq: -1, clientSeq: ++this.localCreationSeq }
|
|
1474
|
+
: { seq: 0, clientSeq: ++this.localCreationSeq };
|
|
1317
1475
|
}
|
|
1318
1476
|
|
|
1319
1477
|
/**
|
|
@@ -1363,7 +1521,35 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1363
1521
|
*/
|
|
1364
1522
|
public subdirectories(): IterableIterator<[string, IDirectory]> {
|
|
1365
1523
|
this.throwIfDisposed();
|
|
1366
|
-
|
|
1524
|
+
const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
|
|
1525
|
+
const localSubdirsInOrder = this.localCreationSeqTracker.keys(
|
|
1526
|
+
(key) => !this.ackedCreationSeqTracker.has(key),
|
|
1527
|
+
);
|
|
1528
|
+
|
|
1529
|
+
const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
|
|
1530
|
+
|
|
1531
|
+
assert(
|
|
1532
|
+
subdirNames.length === this._subdirectories.size,
|
|
1533
|
+
"The count of keys for iteration should be consistent with the size of actual data",
|
|
1534
|
+
);
|
|
1535
|
+
|
|
1536
|
+
const entriesIterator = {
|
|
1537
|
+
index: 0,
|
|
1538
|
+
dirs: this._subdirectories,
|
|
1539
|
+
next(): IteratorResult<[string, any]> {
|
|
1540
|
+
if (this.index < subdirNames.length) {
|
|
1541
|
+
const subdirName = subdirNames[this.index++];
|
|
1542
|
+
const subdir = this.dirs.get(subdirName);
|
|
1543
|
+
return { value: [subdirName, subdir], done: false };
|
|
1544
|
+
}
|
|
1545
|
+
return { value: undefined, done: true };
|
|
1546
|
+
},
|
|
1547
|
+
[Symbol.iterator](): IterableIterator<[string, any]> {
|
|
1548
|
+
return this;
|
|
1549
|
+
},
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
return entriesIterator;
|
|
1367
1553
|
}
|
|
1368
1554
|
|
|
1369
1555
|
/**
|
|
@@ -1690,7 +1876,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1690
1876
|
return;
|
|
1691
1877
|
}
|
|
1692
1878
|
assertNonNullClientId(msg.clientId);
|
|
1693
|
-
this.createSubDirectoryCore(
|
|
1879
|
+
this.createSubDirectoryCore(
|
|
1880
|
+
op.subdirName,
|
|
1881
|
+
local,
|
|
1882
|
+
{ seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber },
|
|
1883
|
+
msg.clientId,
|
|
1884
|
+
);
|
|
1694
1885
|
}
|
|
1695
1886
|
|
|
1696
1887
|
/**
|
|
@@ -1986,7 +2177,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1986
2177
|
public getSerializableCreateInfo() {
|
|
1987
2178
|
this.throwIfDisposed();
|
|
1988
2179
|
const createInfo: ICreateInfo = {
|
|
1989
|
-
csn: this.
|
|
2180
|
+
csn: this.seqData.seq,
|
|
1990
2181
|
ccIds: Array.from(this.clientIds),
|
|
1991
2182
|
};
|
|
1992
2183
|
return createInfo;
|
|
@@ -2095,6 +2286,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2095
2286
|
this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
|
|
2096
2287
|
// don't need to register events because deleting never unregistered
|
|
2097
2288
|
this._subdirectories.set(op.subdirName as string, localOpMetadata.subDirectory);
|
|
2289
|
+
// Restore the record in creation tracker
|
|
2290
|
+
if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
|
|
2291
|
+
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
2292
|
+
...localOpMetadata.subDirectory.seqData,
|
|
2293
|
+
});
|
|
2294
|
+
} else {
|
|
2295
|
+
this.localCreationSeqTracker.set(op.subdirName, {
|
|
2296
|
+
...localOpMetadata.subDirectory.seqData,
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2098
2299
|
this.emit("subDirectoryCreated", op.subdirName, true, this);
|
|
2099
2300
|
}
|
|
2100
2301
|
|
|
@@ -2198,7 +2399,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2198
2399
|
return (
|
|
2199
2400
|
(msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
|
|
2200
2401
|
this.clientIds.has("detached") ||
|
|
2201
|
-
(this.
|
|
2402
|
+
(this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber)
|
|
2202
2403
|
);
|
|
2203
2404
|
}
|
|
2204
2405
|
|
|
@@ -2258,10 +2459,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2258
2459
|
// If this is delete op and we have keys in this subDirectory, then we need to delete these
|
|
2259
2460
|
// keys except the pending ones as they will be sequenced after this delete.
|
|
2260
2461
|
directory.clearExceptPendingKeys(local);
|
|
2261
|
-
// In case of delete op, we need to reset the creation
|
|
2462
|
+
// In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
|
|
2262
2463
|
// creators as the previous directory is getting deleted and we will initialize again when
|
|
2263
2464
|
// we will receive op for the create again.
|
|
2264
|
-
directory.
|
|
2465
|
+
directory.seqData.seq = -1;
|
|
2466
|
+
directory.seqData.clientSeq = -1;
|
|
2265
2467
|
directory.clientIds.clear();
|
|
2266
2468
|
// Do the same thing for the subtree of the directory. If create is not pending for a child, then just
|
|
2267
2469
|
// delete it.
|
|
@@ -2275,22 +2477,39 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2275
2477
|
}
|
|
2276
2478
|
};
|
|
2277
2479
|
const subDirectory = this._subdirectories.get(op.subdirName);
|
|
2480
|
+
// Clear the creation tracker record
|
|
2481
|
+
this.ackedCreationSeqTracker.delete(op.subdirName);
|
|
2278
2482
|
resetSubDirectoryTree(subDirectory);
|
|
2279
2483
|
}
|
|
2280
2484
|
if (op.type === "createSubDirectory") {
|
|
2281
2485
|
const dir = this._subdirectories.get(op.subdirName);
|
|
2282
2486
|
// Child sub directory create seq number can't be lower than the parent subdirectory.
|
|
2283
2487
|
// The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
|
|
2284
|
-
if (this.
|
|
2285
|
-
if (dir?.
|
|
2286
|
-
// Only set the
|
|
2287
|
-
dir.
|
|
2488
|
+
if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
|
|
2489
|
+
if (dir?.seqData.seq === -1) {
|
|
2490
|
+
// Only set the sequence data based on the first message
|
|
2491
|
+
dir.seqData.seq = msg.sequenceNumber;
|
|
2492
|
+
dir.seqData.clientSeq = msg.clientSequenceNumber;
|
|
2493
|
+
|
|
2494
|
+
// set the creation seq in tracker
|
|
2495
|
+
if (
|
|
2496
|
+
!this.ackedCreationSeqTracker.has(op.subdirName) &&
|
|
2497
|
+
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)
|
|
2498
|
+
) {
|
|
2499
|
+
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
2500
|
+
seq: msg.sequenceNumber,
|
|
2501
|
+
clientSeq: msg.clientSequenceNumber,
|
|
2502
|
+
});
|
|
2503
|
+
if (local) {
|
|
2504
|
+
this.localCreationSeqTracker.delete(op.subdirName);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2288
2507
|
}
|
|
2289
2508
|
// The client created the dir at or after the dirs seq, so list its client id as a creator.
|
|
2290
2509
|
if (
|
|
2291
2510
|
dir !== undefined &&
|
|
2292
2511
|
!dir.clientIds.has(msg.clientId) &&
|
|
2293
|
-
dir.
|
|
2512
|
+
dir.seqData.seq <= msg.sequenceNumber
|
|
2294
2513
|
) {
|
|
2295
2514
|
dir.clientIds.add(msg.clientId);
|
|
2296
2515
|
}
|
|
@@ -2375,27 +2594,37 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2375
2594
|
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
2376
2595
|
* @param subdirName - The name of the subdirectory being created
|
|
2377
2596
|
* @param local - Whether the message originated from the local client
|
|
2378
|
-
* @param
|
|
2597
|
+
* @param seqData - Sequence number and client sequence number at which this directory is created
|
|
2379
2598
|
* @param clientId - Id of client which created this directory.
|
|
2380
2599
|
* @returns True if is newly created, false if it already existed.
|
|
2381
2600
|
*/
|
|
2382
2601
|
private createSubDirectoryCore(
|
|
2383
2602
|
subdirName: string,
|
|
2384
2603
|
local: boolean,
|
|
2385
|
-
|
|
2604
|
+
seqData: SequenceData,
|
|
2386
2605
|
clientId: string,
|
|
2387
2606
|
): boolean {
|
|
2388
2607
|
const subdir = this._subdirectories.get(subdirName);
|
|
2389
2608
|
if (subdir === undefined) {
|
|
2390
2609
|
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
2391
2610
|
const subDir = new SubDirectory(
|
|
2392
|
-
|
|
2611
|
+
{ ...seqData },
|
|
2393
2612
|
new Set([clientId]),
|
|
2394
2613
|
this.directory,
|
|
2395
2614
|
this.runtime,
|
|
2396
2615
|
this.serializer,
|
|
2397
2616
|
absolutePath,
|
|
2398
2617
|
);
|
|
2618
|
+
/**
|
|
2619
|
+
* Store the sequnce numbers of newly created subdirectory to the proper creation tracker, based
|
|
2620
|
+
* on whether the creation behavior has been ack'd or not
|
|
2621
|
+
*/
|
|
2622
|
+
if (!isAcknowledgedOrDetached(seqData)) {
|
|
2623
|
+
this.localCreationSeqTracker.set(subdirName, { ...seqData });
|
|
2624
|
+
} else {
|
|
2625
|
+
this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2399
2628
|
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
2400
2629
|
this._subdirectories.set(subdirName, subDir);
|
|
2401
2630
|
this.emit("subDirectoryCreated", subdirName, local, this);
|
|
@@ -2426,6 +2655,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2426
2655
|
// Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
|
|
2427
2656
|
if (previousValue !== undefined) {
|
|
2428
2657
|
this._subdirectories.delete(subdirName);
|
|
2658
|
+
/**
|
|
2659
|
+
* Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
|
|
2660
|
+
* ack'd already or still not committed yet (could be both).
|
|
2661
|
+
*/
|
|
2662
|
+
if (this.ackedCreationSeqTracker.has(subdirName)) {
|
|
2663
|
+
this.ackedCreationSeqTracker.delete(subdirName);
|
|
2664
|
+
}
|
|
2665
|
+
if (this.localCreationSeqTracker.has(subdirName)) {
|
|
2666
|
+
this.localCreationSeqTracker.delete(subdirName);
|
|
2667
|
+
}
|
|
2429
2668
|
this.disposeSubDirectoryTree(previousValue);
|
|
2430
2669
|
this.emit("subDirectoryDeleted", subdirName, local, this);
|
|
2431
2670
|
}
|
package/src/packageVersion.ts
CHANGED