@fluidframework/map 2.0.0-internal.7.2.2 → 2.0.0-internal.7.4.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 +8 -0
- package/api-extractor-lint.json +13 -0
- package/api-extractor.json +9 -1
- package/api-report/map.api.md +42 -44
- package/dist/{directory.js → directory.cjs} +228 -41
- package/dist/directory.cjs.map +1 -0
- package/dist/directory.d.ts +499 -38
- package/dist/directory.d.ts.map +1 -1
- package/dist/{index.js → index.cjs} +4 -4
- package/dist/index.cjs.map +1 -0
- package/dist/{interfaces.js → interfaces.cjs} +1 -1
- package/dist/interfaces.cjs.map +1 -0
- package/dist/interfaces.d.ts +10 -20
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/{internalInterfaces.js → internalInterfaces.cjs} +1 -1
- package/dist/internalInterfaces.cjs.map +1 -0
- package/dist/{localValues.js → localValues.cjs} +2 -3
- package/dist/localValues.cjs.map +1 -0
- package/dist/localValues.d.ts +2 -4
- package/dist/localValues.d.ts.map +1 -1
- package/dist/map-alpha.d.ts +979 -0
- package/dist/map-beta.d.ts +119 -0
- package/dist/map-public.d.ts +119 -0
- package/dist/map-untrimmed.d.ts +993 -0
- package/dist/{map.js → map.cjs} +5 -13
- package/dist/map.cjs.map +1 -0
- package/dist/map.d.ts +2 -10
- package/dist/map.d.ts.map +1 -1
- package/dist/{mapKernel.js → mapKernel.cjs} +2 -2
- package/dist/mapKernel.cjs.map +1 -0
- package/dist/{packageVersion.js → packageVersion.cjs} +2 -2
- package/dist/packageVersion.cjs.map +1 -0
- package/dist/packageVersion.d.ts +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/lib/directory.d.ts +499 -38
- package/lib/directory.d.ts.map +1 -1
- package/lib/{directory.js → directory.mjs} +228 -41
- package/lib/directory.mjs.map +1 -0
- package/lib/index.d.ts +4 -15
- package/lib/index.d.ts.map +1 -1
- package/lib/index.mjs +8 -0
- package/lib/index.mjs.map +1 -0
- package/lib/interfaces.d.ts +10 -20
- package/lib/interfaces.d.ts.map +1 -1
- package/lib/{interfaces.js → interfaces.mjs} +1 -1
- package/lib/interfaces.mjs.map +1 -0
- package/lib/internalInterfaces.d.ts +2 -2
- package/lib/internalInterfaces.d.ts.map +1 -1
- package/lib/{internalInterfaces.js → internalInterfaces.mjs} +1 -1
- package/{dist/internalInterfaces.js.map → lib/internalInterfaces.mjs.map} +1 -1
- package/lib/localValues.d.ts +3 -5
- package/lib/localValues.d.ts.map +1 -1
- package/lib/{localValues.js → localValues.mjs} +2 -3
- package/lib/localValues.mjs.map +1 -0
- package/lib/map-alpha.d.ts +979 -0
- package/lib/map-beta.d.ts +119 -0
- package/lib/map-public.d.ts +119 -0
- package/lib/map-untrimmed.d.ts +993 -0
- package/lib/map.d.ts +3 -11
- package/lib/map.d.ts.map +1 -1
- package/lib/{map.js → map.mjs} +5 -13
- package/lib/map.mjs.map +1 -0
- package/lib/mapKernel.d.ts +2 -2
- package/lib/mapKernel.d.ts.map +1 -1
- package/lib/{mapKernel.js → mapKernel.mjs} +2 -2
- package/lib/mapKernel.mjs.map +1 -0
- package/lib/packageVersion.d.ts +1 -1
- package/lib/{packageVersion.js → packageVersion.mjs} +2 -2
- package/lib/packageVersion.mjs.map +1 -0
- package/map.test-files.tar +0 -0
- package/package.json +54 -33
- package/src/directory.ts +280 -62
- package/src/interfaces.ts +10 -20
- package/src/localValues.ts +2 -4
- package/src/map.ts +2 -10
- package/src/packageVersion.ts +1 -1
- package/tsc-multi.test.json +4 -0
- package/tsconfig.json +6 -5
- package/dist/directory.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/interfaces.js.map +0 -1
- package/dist/localValues.js.map +0 -1
- package/dist/map.js.map +0 -1
- package/dist/mapKernel.js.map +0 -1
- package/dist/packageVersion.js.map +0 -1
- package/lib/directory.js.map +0 -1
- package/lib/index.js +0 -19
- package/lib/index.js.map +0 -1
- package/lib/interfaces.js.map +0 -1
- package/lib/internalInterfaces.js.map +0 -1
- package/lib/localValues.js.map +0 -1
- package/lib/map.js.map +0 -1
- package/lib/mapKernel.js.map +0 -1
- package/lib/packageVersion.js.map +0 -1
- package/tsconfig.esnext.json +0 -7
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,
|
|
@@ -70,8 +71,7 @@ interface IDirectoryMessageHandler {
|
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
73
|
* Operation indicating a value should be set for a key.
|
|
73
|
-
*
|
|
74
|
-
* @public
|
|
74
|
+
* @alpha
|
|
75
75
|
*/
|
|
76
76
|
export interface IDirectorySetOperation {
|
|
77
77
|
/**
|
|
@@ -98,8 +98,7 @@ export interface IDirectorySetOperation {
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Operation indicating a key should be deleted from the directory.
|
|
101
|
-
*
|
|
102
|
-
* @public
|
|
101
|
+
* @alpha
|
|
103
102
|
*/
|
|
104
103
|
export interface IDirectoryDeleteOperation {
|
|
105
104
|
/**
|
|
@@ -120,15 +119,13 @@ export interface IDirectoryDeleteOperation {
|
|
|
120
119
|
|
|
121
120
|
/**
|
|
122
121
|
* An operation on a specific key within a directory.
|
|
123
|
-
*
|
|
124
|
-
* @public
|
|
122
|
+
* @alpha
|
|
125
123
|
*/
|
|
126
124
|
export type IDirectoryKeyOperation = IDirectorySetOperation | IDirectoryDeleteOperation;
|
|
127
125
|
|
|
128
126
|
/**
|
|
129
127
|
* Operation indicating the directory should be cleared.
|
|
130
|
-
*
|
|
131
|
-
* @public
|
|
128
|
+
* @alpha
|
|
132
129
|
*/
|
|
133
130
|
export interface IDirectoryClearOperation {
|
|
134
131
|
/**
|
|
@@ -144,15 +141,13 @@ export interface IDirectoryClearOperation {
|
|
|
144
141
|
|
|
145
142
|
/**
|
|
146
143
|
* An operation on one or more of the keys within a directory.
|
|
147
|
-
*
|
|
148
|
-
* @public
|
|
144
|
+
* @alpha
|
|
149
145
|
*/
|
|
150
146
|
export type IDirectoryStorageOperation = IDirectoryKeyOperation | IDirectoryClearOperation;
|
|
151
147
|
|
|
152
148
|
/**
|
|
153
149
|
* Operation indicating a subdirectory should be created.
|
|
154
|
-
*
|
|
155
|
-
* @public
|
|
150
|
+
* @alpha
|
|
156
151
|
*/
|
|
157
152
|
export interface IDirectoryCreateSubDirectoryOperation {
|
|
158
153
|
/**
|
|
@@ -173,8 +168,7 @@ export interface IDirectoryCreateSubDirectoryOperation {
|
|
|
173
168
|
|
|
174
169
|
/**
|
|
175
170
|
* Operation indicating a subdirectory should be deleted.
|
|
176
|
-
*
|
|
177
|
-
* @public
|
|
171
|
+
* @alpha
|
|
178
172
|
*/
|
|
179
173
|
export interface IDirectoryDeleteSubDirectoryOperation {
|
|
180
174
|
/**
|
|
@@ -195,8 +189,7 @@ export interface IDirectoryDeleteSubDirectoryOperation {
|
|
|
195
189
|
|
|
196
190
|
/**
|
|
197
191
|
* An operation on the subdirectories within a directory.
|
|
198
|
-
*
|
|
199
|
-
* @public
|
|
192
|
+
* @alpha
|
|
200
193
|
*/
|
|
201
194
|
export type IDirectorySubDirectoryOperation =
|
|
202
195
|
| IDirectoryCreateSubDirectoryOperation
|
|
@@ -204,15 +197,13 @@ export type IDirectorySubDirectoryOperation =
|
|
|
204
197
|
|
|
205
198
|
/**
|
|
206
199
|
* Any operation on a directory.
|
|
207
|
-
*
|
|
208
|
-
* @public
|
|
200
|
+
* @alpha
|
|
209
201
|
*/
|
|
210
202
|
export type IDirectoryOperation = IDirectoryStorageOperation | IDirectorySubDirectoryOperation;
|
|
211
203
|
|
|
212
204
|
/**
|
|
213
205
|
* Create info for the subdirectory.
|
|
214
|
-
*
|
|
215
|
-
* @public
|
|
206
|
+
* @alpha
|
|
216
207
|
*/
|
|
217
208
|
export interface ICreateInfo {
|
|
218
209
|
/**
|
|
@@ -233,8 +224,7 @@ export interface ICreateInfo {
|
|
|
233
224
|
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
|
|
234
225
|
* | JSON.stringify}, direct result from
|
|
235
226
|
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse | JSON.parse}.
|
|
236
|
-
*
|
|
237
|
-
* @public
|
|
227
|
+
* @alpha
|
|
238
228
|
*/
|
|
239
229
|
export interface IDirectoryDataObject {
|
|
240
230
|
/**
|
|
@@ -279,7 +269,7 @@ export interface IDirectoryNewStorageFormat {
|
|
|
279
269
|
* {@link @fluidframework/datastore-definitions#IChannelFactory} for {@link SharedDirectory}.
|
|
280
270
|
*
|
|
281
271
|
* @sealed
|
|
282
|
-
* @
|
|
272
|
+
* @alpha
|
|
283
273
|
*/
|
|
284
274
|
export class DirectoryFactory implements IChannelFactory {
|
|
285
275
|
/**
|
|
@@ -336,6 +326,115 @@ export class DirectoryFactory implements IChannelFactory {
|
|
|
336
326
|
}
|
|
337
327
|
}
|
|
338
328
|
|
|
329
|
+
/**
|
|
330
|
+
* The comparator essentially performs the following procedure to determine the order of subdirectory creation:
|
|
331
|
+
* 1. If subdirectory A has a non-negative 'seq' and subdirectory B has a negative 'seq', subdirectory A is always placed first due to
|
|
332
|
+
* the policy that acknowledged subdirectories precede locally created ones that have not been committed yet.
|
|
333
|
+
*
|
|
334
|
+
* 2. When both subdirectories A and B have a non-negative 'seq', they are compared as follows:
|
|
335
|
+
* - 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
|
|
336
|
+
* should not be applied in the directory ordering, since the lowest 'seq' is -1, when the directory is created locally but not acknowledged yet.
|
|
337
|
+
* - 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
|
|
338
|
+
* batching is enabled, and a lower 'clientSeq' indicates that it was processed earlier after the batch was ungrouped.
|
|
339
|
+
*
|
|
340
|
+
* 3. When both subdirectories A and B have a negative 'seq', they are compared as follows:
|
|
341
|
+
* - If A and B have different 'seq', the one with lower 'seq' will be positioned ahead, which indicates the corresponding creation message was
|
|
342
|
+
* acknowledged by the server earlier.
|
|
343
|
+
* - 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
|
|
344
|
+
* and B were created locally and not acknowledged yet, with the one possessing the lower 'clientSeq' being created earlier.
|
|
345
|
+
*
|
|
346
|
+
* 4. A 'seq' value of zero indicates that the subdirectory was created in detached state, and it is considered acknowledged for the
|
|
347
|
+
* purpose of ordering.
|
|
348
|
+
*/
|
|
349
|
+
const seqDataComparator = (a: SequenceData, b: SequenceData) => {
|
|
350
|
+
if (isAcknowledgedOrDetached(a)) {
|
|
351
|
+
if (isAcknowledgedOrDetached(b)) {
|
|
352
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
353
|
+
return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq! - b.clientSeq!;
|
|
354
|
+
} else {
|
|
355
|
+
return -1;
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
if (!isAcknowledgedOrDetached(b)) {
|
|
359
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
360
|
+
return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq! - b.clientSeq!;
|
|
361
|
+
} else {
|
|
362
|
+
return 1;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
function isAcknowledgedOrDetached(seqData: SequenceData) {
|
|
368
|
+
return seqData.seq >= 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The combination of sequence numebr and client sequence number of a subdirectory
|
|
373
|
+
*/
|
|
374
|
+
interface SequenceData {
|
|
375
|
+
seq: number;
|
|
376
|
+
clientSeq?: number;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* A utility class for tracking associations between keys and their creation indices.
|
|
381
|
+
* This is relevant to support map iteration in insertion order, see
|
|
382
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
|
|
383
|
+
*
|
|
384
|
+
* TODO: It can be combined with the creation tracker utilized in SharedMap
|
|
385
|
+
*/
|
|
386
|
+
class DirectoryCreationTracker {
|
|
387
|
+
readonly indexToKey: RedBlackTree<SequenceData, string>;
|
|
388
|
+
|
|
389
|
+
readonly keyToIndex: Map<string, SequenceData>;
|
|
390
|
+
|
|
391
|
+
constructor() {
|
|
392
|
+
this.indexToKey = new RedBlackTree<SequenceData, string>(seqDataComparator);
|
|
393
|
+
this.keyToIndex = new Map<string, SequenceData>();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
set(key: string, seqData: SequenceData): void {
|
|
397
|
+
this.indexToKey.put(seqData, key);
|
|
398
|
+
this.keyToIndex.set(key, seqData);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
has(keyOrSeqData: string | SequenceData): boolean {
|
|
402
|
+
return typeof keyOrSeqData === "string"
|
|
403
|
+
? this.keyToIndex.has(keyOrSeqData)
|
|
404
|
+
: this.indexToKey.get(keyOrSeqData) !== undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
delete(keyOrSeqData: string | SequenceData): void {
|
|
408
|
+
if (this.has(keyOrSeqData)) {
|
|
409
|
+
if (typeof keyOrSeqData === "string") {
|
|
410
|
+
const seqData = this.keyToIndex.get(keyOrSeqData) as SequenceData;
|
|
411
|
+
this.keyToIndex.delete(keyOrSeqData);
|
|
412
|
+
this.indexToKey.remove(seqData);
|
|
413
|
+
} else {
|
|
414
|
+
const key = this.indexToKey.get(keyOrSeqData)?.data as string;
|
|
415
|
+
this.indexToKey.remove(keyOrSeqData);
|
|
416
|
+
this.keyToIndex.delete(key);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Retrieves all subdirectories with creation order that satisfy an optional constraint function.
|
|
423
|
+
* @param constraint - An optional constraint function that filters keys.
|
|
424
|
+
* @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
|
|
425
|
+
*/
|
|
426
|
+
keys(constraint?: (key: string) => boolean): string[] {
|
|
427
|
+
const keys: string[] = [];
|
|
428
|
+
this.indexToKey.mapRange((node) => {
|
|
429
|
+
if (!constraint || constraint(node.data)) {
|
|
430
|
+
keys.push(node.data);
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
}, keys);
|
|
434
|
+
return keys;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
339
438
|
/**
|
|
340
439
|
* {@inheritDoc ISharedDirectory}
|
|
341
440
|
*
|
|
@@ -348,7 +447,7 @@ export class DirectoryFactory implements IChannelFactory {
|
|
|
348
447
|
* ```
|
|
349
448
|
*
|
|
350
449
|
* @sealed
|
|
351
|
-
* @
|
|
450
|
+
* @alpha
|
|
352
451
|
*/
|
|
353
452
|
export class SharedDirectory
|
|
354
453
|
extends SharedObject<ISharedDirectoryEvents>
|
|
@@ -386,16 +485,14 @@ export class SharedDirectory
|
|
|
386
485
|
return this.root.absolutePath;
|
|
387
486
|
}
|
|
388
487
|
|
|
389
|
-
|
|
390
|
-
* @internal
|
|
391
|
-
*/
|
|
488
|
+
/***/
|
|
392
489
|
public readonly localValueMaker: LocalValueMaker;
|
|
393
490
|
|
|
394
491
|
/**
|
|
395
492
|
* Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
|
|
396
493
|
*/
|
|
397
494
|
private readonly root: SubDirectory = new SubDirectory(
|
|
398
|
-
0,
|
|
495
|
+
{ seq: 0, clientSeq: 0 },
|
|
399
496
|
new Set(),
|
|
400
497
|
this,
|
|
401
498
|
this.runtime,
|
|
@@ -605,7 +702,6 @@ export class SharedDirectory
|
|
|
605
702
|
|
|
606
703
|
/**
|
|
607
704
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
|
|
608
|
-
* @internal
|
|
609
705
|
*/
|
|
610
706
|
protected summarizeCore(
|
|
611
707
|
serializer: IFluidSerializer,
|
|
@@ -619,7 +715,6 @@ export class SharedDirectory
|
|
|
619
715
|
* @param op - Op to submit
|
|
620
716
|
* @param localOpMetadata - The local metadata associated with the op. We send a unique id that is used to track
|
|
621
717
|
* this op while it has not been ack'd. This will be sent when we receive this op back from the server.
|
|
622
|
-
* @internal
|
|
623
718
|
*/
|
|
624
719
|
public submitDirectoryMessage(op: IDirectoryOperation, localOpMetadata: unknown): void {
|
|
625
720
|
this.submitLocalMessage(op, localOpMetadata);
|
|
@@ -627,13 +722,11 @@ export class SharedDirectory
|
|
|
627
722
|
|
|
628
723
|
/**
|
|
629
724
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
|
|
630
|
-
* @internal
|
|
631
725
|
*/
|
|
632
726
|
protected onDisconnect(): void {}
|
|
633
727
|
|
|
634
728
|
/**
|
|
635
729
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
|
|
636
|
-
* @internal
|
|
637
730
|
*/
|
|
638
731
|
protected reSubmitCore(content: unknown, localOpMetadata: unknown): void {
|
|
639
732
|
const message = content as IDirectoryOperation;
|
|
@@ -644,7 +737,6 @@ export class SharedDirectory
|
|
|
644
737
|
|
|
645
738
|
/**
|
|
646
739
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
|
|
647
|
-
* @internal
|
|
648
740
|
*/
|
|
649
741
|
protected async loadCore(storage: IChannelStorageService): Promise<void> {
|
|
650
742
|
const data = await readAndParse(storage, snapshotFileName);
|
|
@@ -667,26 +759,46 @@ export class SharedDirectory
|
|
|
667
759
|
/**
|
|
668
760
|
* Populate the directory with the given directory data.
|
|
669
761
|
* @param data - A JSON string containing serialized directory data
|
|
670
|
-
* @internal
|
|
671
762
|
*/
|
|
672
763
|
protected populate(data: IDirectoryDataObject): void {
|
|
673
764
|
const stack: [SubDirectory, IDirectoryDataObject][] = [];
|
|
674
765
|
stack.push([this.root, data]);
|
|
766
|
+
|
|
675
767
|
while (stack.length > 0) {
|
|
676
768
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
677
769
|
const [currentSubDir, currentSubDirObject] = stack.pop()!;
|
|
678
770
|
if (currentSubDirObject.subdirectories) {
|
|
771
|
+
// Utilize a map to store the seq -> clientSeq for the newly created subdirectory
|
|
772
|
+
const tempSeqNums = new Map<number, number>();
|
|
679
773
|
for (const [subdirName, subdirObject] of Object.entries(
|
|
680
774
|
currentSubDirObject.subdirectories,
|
|
681
775
|
)) {
|
|
682
776
|
let newSubDir = currentSubDir.getSubDirectory(subdirName) as SubDirectory;
|
|
777
|
+
let seqData: SequenceData;
|
|
683
778
|
if (!newSubDir) {
|
|
684
779
|
const createInfo = subdirObject.ci;
|
|
685
|
-
|
|
780
|
+
// We do not store the client sequence number in the storage because the order has already been
|
|
781
|
+
// guaranteed during the serialization process. As a result, it is only essential to utilize the
|
|
782
|
+
// "fake" client sequence number to signify the loading order, and there is no need to retain
|
|
783
|
+
// the actual client sequence number at this point.
|
|
784
|
+
if (createInfo !== undefined && createInfo.csn > -1) {
|
|
686
785
|
// If csn is -1, then initialize it with 0, otherwise we will never process ops for this
|
|
687
786
|
// sub directory. This could be done at serialization time too, but we need to maintain
|
|
688
787
|
// back compat too and also we will actually know the state when it was serialized.
|
|
689
|
-
|
|
788
|
+
if (!tempSeqNums.has(createInfo.csn)) {
|
|
789
|
+
tempSeqNums.set(createInfo.csn, 0);
|
|
790
|
+
}
|
|
791
|
+
let fakeClientSeq = tempSeqNums.get(createInfo.csn) as number;
|
|
792
|
+
seqData = { seq: createInfo.csn, clientSeq: fakeClientSeq };
|
|
793
|
+
tempSeqNums.set(createInfo.csn, ++fakeClientSeq);
|
|
794
|
+
} else {
|
|
795
|
+
seqData = {
|
|
796
|
+
seq: 0,
|
|
797
|
+
clientSeq: ++currentSubDir.localCreationSeq,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
newSubDir = new SubDirectory(
|
|
801
|
+
seqData,
|
|
690
802
|
createInfo !== undefined
|
|
691
803
|
? new Set<string>(createInfo.ccIds)
|
|
692
804
|
: new Set(),
|
|
@@ -696,6 +808,10 @@ export class SharedDirectory
|
|
|
696
808
|
posix.join(currentSubDir.absolutePath, subdirName),
|
|
697
809
|
);
|
|
698
810
|
currentSubDir.populateSubDirectory(subdirName, newSubDir);
|
|
811
|
+
// Record the newly inserted subdirectory to the creation tracker
|
|
812
|
+
currentSubDir.ackedCreationSeqTracker.set(subdirName, {
|
|
813
|
+
...seqData,
|
|
814
|
+
});
|
|
699
815
|
}
|
|
700
816
|
stack.push([newSubDir, subdirObject]);
|
|
701
817
|
}
|
|
@@ -716,7 +832,6 @@ export class SharedDirectory
|
|
|
716
832
|
|
|
717
833
|
/**
|
|
718
834
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
|
|
719
|
-
* @internal
|
|
720
835
|
*/
|
|
721
836
|
protected processCore(
|
|
722
837
|
message: ISequencedDocumentMessage,
|
|
@@ -733,7 +848,6 @@ export class SharedDirectory
|
|
|
733
848
|
|
|
734
849
|
/**
|
|
735
850
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
|
|
736
|
-
* @internal
|
|
737
851
|
*/
|
|
738
852
|
protected rollback(content: unknown, localOpMetadata: unknown): void {
|
|
739
853
|
const op: IDirectoryOperation = content as IDirectoryOperation;
|
|
@@ -957,7 +1071,6 @@ export class SharedDirectory
|
|
|
957
1071
|
|
|
958
1072
|
/**
|
|
959
1073
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
|
|
960
|
-
* @internal
|
|
961
1074
|
*/
|
|
962
1075
|
protected applyStashedOp(op: unknown): unknown {
|
|
963
1076
|
const handler = this.messageHandlers.get((op as IDirectoryOperation).type);
|
|
@@ -1057,7 +1170,7 @@ interface IDeleteSubDirLocalOpMetadata {
|
|
|
1057
1170
|
}
|
|
1058
1171
|
|
|
1059
1172
|
type SubDirLocalOpMetadata = ICreateSubDirLocalOpMetadata | IDeleteSubDirLocalOpMetadata;
|
|
1060
|
-
type DirectoryLocalOpMetadata =
|
|
1173
|
+
export type DirectoryLocalOpMetadata =
|
|
1061
1174
|
| IClearLocalOpMetadata
|
|
1062
1175
|
| IKeyEditLocalOpMetadata
|
|
1063
1176
|
| SubDirLocalOpMetadata;
|
|
@@ -1159,6 +1272,23 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1159
1272
|
*/
|
|
1160
1273
|
private readonly pendingClearMessageIds: number[] = [];
|
|
1161
1274
|
|
|
1275
|
+
/**
|
|
1276
|
+
* Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
|
|
1277
|
+
* of the creation order.
|
|
1278
|
+
*/
|
|
1279
|
+
public localCreationSeq: number = 0;
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Maintains a bidirectional association between ack'd subdirectories and their seqData.
|
|
1283
|
+
* This helps to ensure iteration order which is consistent with the JS map spec.
|
|
1284
|
+
*/
|
|
1285
|
+
public readonly ackedCreationSeqTracker: DirectoryCreationTracker;
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Similar to {@link ackedCreationSeqTracker}, but for local (unacked) entries.
|
|
1289
|
+
*/
|
|
1290
|
+
public readonly localCreationSeqTracker: DirectoryCreationTracker;
|
|
1291
|
+
|
|
1162
1292
|
/**
|
|
1163
1293
|
* Constructor.
|
|
1164
1294
|
* @param sequenceNumber - Message seq number at which this was created.
|
|
@@ -1169,7 +1299,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1169
1299
|
* @param absolutePath - The absolute path of this IDirectory
|
|
1170
1300
|
*/
|
|
1171
1301
|
public constructor(
|
|
1172
|
-
private
|
|
1302
|
+
private readonly seqData: SequenceData,
|
|
1173
1303
|
private readonly clientIds: Set<string>,
|
|
1174
1304
|
private readonly directory: SharedDirectory,
|
|
1175
1305
|
private readonly runtime: IFluidDataStoreRuntime,
|
|
@@ -1177,6 +1307,8 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1177
1307
|
public readonly absolutePath: string,
|
|
1178
1308
|
) {
|
|
1179
1309
|
super();
|
|
1310
|
+
this.localCreationSeqTracker = new DirectoryCreationTracker();
|
|
1311
|
+
this.ackedCreationSeqTracker = new DirectoryCreationTracker();
|
|
1180
1312
|
}
|
|
1181
1313
|
|
|
1182
1314
|
public dispose(error?: Error): void {
|
|
@@ -1306,14 +1438,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1306
1438
|
}
|
|
1307
1439
|
|
|
1308
1440
|
/**
|
|
1309
|
-
* @returns
|
|
1441
|
+
* @returns The Sequence Data which should be used for local changes.
|
|
1310
1442
|
* @remarks While detached, 0 is used rather than -1 to represent a change which should be universally known (as opposed to known
|
|
1311
1443
|
* 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
1444
|
* last set while detached will now be known to any new client, until they are changed).
|
|
1445
|
+
*
|
|
1446
|
+
* The client sequence number is incremented by 1 for maintaining the internal order of locally created subdirectories
|
|
1313
1447
|
* TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
|
|
1314
1448
|
*/
|
|
1315
|
-
private getLocalSeq():
|
|
1316
|
-
return this.directory.isAttached()
|
|
1449
|
+
private getLocalSeq(): SequenceData {
|
|
1450
|
+
return this.directory.isAttached()
|
|
1451
|
+
? { seq: -1, clientSeq: ++this.localCreationSeq }
|
|
1452
|
+
: { seq: 0, clientSeq: ++this.localCreationSeq };
|
|
1317
1453
|
}
|
|
1318
1454
|
|
|
1319
1455
|
/**
|
|
@@ -1363,7 +1499,35 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1363
1499
|
*/
|
|
1364
1500
|
public subdirectories(): IterableIterator<[string, IDirectory]> {
|
|
1365
1501
|
this.throwIfDisposed();
|
|
1366
|
-
|
|
1502
|
+
const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
|
|
1503
|
+
const localSubdirsInOrder = this.localCreationSeqTracker.keys(
|
|
1504
|
+
(key) => !this.ackedCreationSeqTracker.has(key),
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
|
|
1508
|
+
|
|
1509
|
+
assert(
|
|
1510
|
+
subdirNames.length === this._subdirectories.size,
|
|
1511
|
+
0x85c /* The count of keys for iteration should be consistent with the size of actual data */,
|
|
1512
|
+
);
|
|
1513
|
+
|
|
1514
|
+
const entriesIterator = {
|
|
1515
|
+
index: 0,
|
|
1516
|
+
dirs: this._subdirectories,
|
|
1517
|
+
next(): IteratorResult<[string, any]> {
|
|
1518
|
+
if (this.index < subdirNames.length) {
|
|
1519
|
+
const subdirName = subdirNames[this.index++];
|
|
1520
|
+
const subdir = this.dirs.get(subdirName);
|
|
1521
|
+
return { value: [subdirName, subdir], done: false };
|
|
1522
|
+
}
|
|
1523
|
+
return { value: undefined, done: true };
|
|
1524
|
+
},
|
|
1525
|
+
[Symbol.iterator](): IterableIterator<[string, any]> {
|
|
1526
|
+
return this;
|
|
1527
|
+
},
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
return entriesIterator;
|
|
1367
1531
|
}
|
|
1368
1532
|
|
|
1369
1533
|
/**
|
|
@@ -1690,7 +1854,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1690
1854
|
return;
|
|
1691
1855
|
}
|
|
1692
1856
|
assertNonNullClientId(msg.clientId);
|
|
1693
|
-
this.createSubDirectoryCore(
|
|
1857
|
+
this.createSubDirectoryCore(
|
|
1858
|
+
op.subdirName,
|
|
1859
|
+
local,
|
|
1860
|
+
{ seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber },
|
|
1861
|
+
msg.clientId,
|
|
1862
|
+
);
|
|
1694
1863
|
}
|
|
1695
1864
|
|
|
1696
1865
|
/**
|
|
@@ -1986,7 +2155,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1986
2155
|
public getSerializableCreateInfo() {
|
|
1987
2156
|
this.throwIfDisposed();
|
|
1988
2157
|
const createInfo: ICreateInfo = {
|
|
1989
|
-
csn: this.
|
|
2158
|
+
csn: this.seqData.seq,
|
|
1990
2159
|
ccIds: Array.from(this.clientIds),
|
|
1991
2160
|
};
|
|
1992
2161
|
return createInfo;
|
|
@@ -2095,6 +2264,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2095
2264
|
this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
|
|
2096
2265
|
// don't need to register events because deleting never unregistered
|
|
2097
2266
|
this._subdirectories.set(op.subdirName as string, localOpMetadata.subDirectory);
|
|
2267
|
+
// Restore the record in creation tracker
|
|
2268
|
+
if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
|
|
2269
|
+
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
2270
|
+
...localOpMetadata.subDirectory.seqData,
|
|
2271
|
+
});
|
|
2272
|
+
} else {
|
|
2273
|
+
this.localCreationSeqTracker.set(op.subdirName, {
|
|
2274
|
+
...localOpMetadata.subDirectory.seqData,
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2098
2277
|
this.emit("subDirectoryCreated", op.subdirName, true, this);
|
|
2099
2278
|
}
|
|
2100
2279
|
|
|
@@ -2198,7 +2377,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2198
2377
|
return (
|
|
2199
2378
|
(msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
|
|
2200
2379
|
this.clientIds.has("detached") ||
|
|
2201
|
-
(this.
|
|
2380
|
+
(this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber)
|
|
2202
2381
|
);
|
|
2203
2382
|
}
|
|
2204
2383
|
|
|
@@ -2258,10 +2437,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2258
2437
|
// If this is delete op and we have keys in this subDirectory, then we need to delete these
|
|
2259
2438
|
// keys except the pending ones as they will be sequenced after this delete.
|
|
2260
2439
|
directory.clearExceptPendingKeys(local);
|
|
2261
|
-
// In case of delete op, we need to reset the creation
|
|
2440
|
+
// In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
|
|
2262
2441
|
// creators as the previous directory is getting deleted and we will initialize again when
|
|
2263
2442
|
// we will receive op for the create again.
|
|
2264
|
-
directory.
|
|
2443
|
+
directory.seqData.seq = -1;
|
|
2444
|
+
directory.seqData.clientSeq = -1;
|
|
2265
2445
|
directory.clientIds.clear();
|
|
2266
2446
|
// Do the same thing for the subtree of the directory. If create is not pending for a child, then just
|
|
2267
2447
|
// delete it.
|
|
@@ -2275,22 +2455,39 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2275
2455
|
}
|
|
2276
2456
|
};
|
|
2277
2457
|
const subDirectory = this._subdirectories.get(op.subdirName);
|
|
2458
|
+
// Clear the creation tracker record
|
|
2459
|
+
this.ackedCreationSeqTracker.delete(op.subdirName);
|
|
2278
2460
|
resetSubDirectoryTree(subDirectory);
|
|
2279
2461
|
}
|
|
2280
2462
|
if (op.type === "createSubDirectory") {
|
|
2281
2463
|
const dir = this._subdirectories.get(op.subdirName);
|
|
2282
2464
|
// Child sub directory create seq number can't be lower than the parent subdirectory.
|
|
2283
2465
|
// 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.
|
|
2466
|
+
if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
|
|
2467
|
+
if (dir?.seqData.seq === -1) {
|
|
2468
|
+
// Only set the sequence data based on the first message
|
|
2469
|
+
dir.seqData.seq = msg.sequenceNumber;
|
|
2470
|
+
dir.seqData.clientSeq = msg.clientSequenceNumber;
|
|
2471
|
+
|
|
2472
|
+
// set the creation seq in tracker
|
|
2473
|
+
if (
|
|
2474
|
+
!this.ackedCreationSeqTracker.has(op.subdirName) &&
|
|
2475
|
+
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)
|
|
2476
|
+
) {
|
|
2477
|
+
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
2478
|
+
seq: msg.sequenceNumber,
|
|
2479
|
+
clientSeq: msg.clientSequenceNumber,
|
|
2480
|
+
});
|
|
2481
|
+
if (local) {
|
|
2482
|
+
this.localCreationSeqTracker.delete(op.subdirName);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2288
2485
|
}
|
|
2289
2486
|
// The client created the dir at or after the dirs seq, so list its client id as a creator.
|
|
2290
2487
|
if (
|
|
2291
2488
|
dir !== undefined &&
|
|
2292
2489
|
!dir.clientIds.has(msg.clientId) &&
|
|
2293
|
-
dir.
|
|
2490
|
+
dir.seqData.seq <= msg.sequenceNumber
|
|
2294
2491
|
) {
|
|
2295
2492
|
dir.clientIds.add(msg.clientId);
|
|
2296
2493
|
}
|
|
@@ -2375,27 +2572,37 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2375
2572
|
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
2376
2573
|
* @param subdirName - The name of the subdirectory being created
|
|
2377
2574
|
* @param local - Whether the message originated from the local client
|
|
2378
|
-
* @param
|
|
2575
|
+
* @param seqData - Sequence number and client sequence number at which this directory is created
|
|
2379
2576
|
* @param clientId - Id of client which created this directory.
|
|
2380
2577
|
* @returns True if is newly created, false if it already existed.
|
|
2381
2578
|
*/
|
|
2382
2579
|
private createSubDirectoryCore(
|
|
2383
2580
|
subdirName: string,
|
|
2384
2581
|
local: boolean,
|
|
2385
|
-
|
|
2582
|
+
seqData: SequenceData,
|
|
2386
2583
|
clientId: string,
|
|
2387
2584
|
): boolean {
|
|
2388
2585
|
const subdir = this._subdirectories.get(subdirName);
|
|
2389
2586
|
if (subdir === undefined) {
|
|
2390
2587
|
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
2391
2588
|
const subDir = new SubDirectory(
|
|
2392
|
-
|
|
2589
|
+
{ ...seqData },
|
|
2393
2590
|
new Set([clientId]),
|
|
2394
2591
|
this.directory,
|
|
2395
2592
|
this.runtime,
|
|
2396
2593
|
this.serializer,
|
|
2397
2594
|
absolutePath,
|
|
2398
2595
|
);
|
|
2596
|
+
/**
|
|
2597
|
+
* Store the sequnce numbers of newly created subdirectory to the proper creation tracker, based
|
|
2598
|
+
* on whether the creation behavior has been ack'd or not
|
|
2599
|
+
*/
|
|
2600
|
+
if (!isAcknowledgedOrDetached(seqData)) {
|
|
2601
|
+
this.localCreationSeqTracker.set(subdirName, { ...seqData });
|
|
2602
|
+
} else {
|
|
2603
|
+
this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2399
2606
|
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
2400
2607
|
this._subdirectories.set(subdirName, subDir);
|
|
2401
2608
|
this.emit("subDirectoryCreated", subdirName, local, this);
|
|
@@ -2426,6 +2633,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2426
2633
|
// Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
|
|
2427
2634
|
if (previousValue !== undefined) {
|
|
2428
2635
|
this._subdirectories.delete(subdirName);
|
|
2636
|
+
/**
|
|
2637
|
+
* Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
|
|
2638
|
+
* ack'd already or still not committed yet (could be both).
|
|
2639
|
+
*/
|
|
2640
|
+
if (this.ackedCreationSeqTracker.has(subdirName)) {
|
|
2641
|
+
this.ackedCreationSeqTracker.delete(subdirName);
|
|
2642
|
+
}
|
|
2643
|
+
if (this.localCreationSeqTracker.has(subdirName)) {
|
|
2644
|
+
this.localCreationSeqTracker.delete(subdirName);
|
|
2645
|
+
}
|
|
2429
2646
|
this.disposeSubDirectoryTree(previousValue);
|
|
2430
2647
|
this.emit("subDirectoryDeleted", subdirName, local, this);
|
|
2431
2648
|
}
|
|
@@ -2447,10 +2664,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2447
2664
|
}
|
|
2448
2665
|
|
|
2449
2666
|
private undeleteSubDirectoryTree(directory: SubDirectory): void {
|
|
2450
|
-
// Restore deleted subdirectory tree.
|
|
2451
|
-
|
|
2452
|
-
this.undeleteSubDirectoryTree(subDirectory);
|
|
2453
|
-
}
|
|
2667
|
+
// Restore deleted subdirectory tree. Need to undispose the current directory first, then get access to the iterator.
|
|
2668
|
+
// This will unmark "deleted" from the subdirectories from top to bottom.
|
|
2454
2669
|
directory.undispose();
|
|
2670
|
+
for (const [_, subDirectory] of directory.subdirectories()) {
|
|
2671
|
+
this.undeleteSubDirectoryTree(subDirectory as SubDirectory);
|
|
2672
|
+
}
|
|
2455
2673
|
}
|
|
2456
2674
|
}
|