@fluidframework/map 2.0.0-dev-rc.1.0.0.228517 → 2.0.0-dev-rc.2.0.0.245554

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.
Files changed (132) hide show
  1. package/{.eslintrc.js → .eslintrc.cjs} +10 -1
  2. package/{.mocharc.js → .mocharc.cjs} +1 -1
  3. package/CHANGELOG.md +11 -0
  4. package/{api-extractor-esm.json → api-extractor-cjs.json} +5 -1
  5. package/api-extractor-lint.json +1 -1
  6. package/api-extractor.json +1 -1
  7. package/api-report/map.api.md +14 -57
  8. package/dist/directory.d.ts +14 -51
  9. package/dist/directory.d.ts.map +1 -1
  10. package/dist/directory.js +109 -172
  11. package/dist/directory.js.map +1 -1
  12. package/dist/index.d.ts +45 -4
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +43 -8
  15. package/dist/index.js.map +1 -1
  16. package/dist/interfaces.d.ts.map +1 -1
  17. package/dist/interfaces.js.map +1 -1
  18. package/dist/internalInterfaces.d.ts +2 -2
  19. package/dist/internalInterfaces.d.ts.map +1 -1
  20. package/dist/internalInterfaces.js.map +1 -1
  21. package/dist/localValues.d.ts +3 -5
  22. package/dist/localValues.d.ts.map +1 -1
  23. package/dist/localValues.js +9 -8
  24. package/dist/localValues.js.map +1 -1
  25. package/dist/map-alpha.d.ts +31 -116
  26. package/dist/map-beta.d.ts +24 -105
  27. package/dist/map-public.d.ts +24 -105
  28. package/dist/map-untrimmed.d.ts +31 -116
  29. package/dist/map.d.ts +4 -23
  30. package/dist/map.d.ts.map +1 -1
  31. package/dist/map.js +6 -29
  32. package/dist/map.js.map +1 -1
  33. package/dist/mapKernel.d.ts +3 -4
  34. package/dist/mapKernel.d.ts.map +1 -1
  35. package/dist/mapKernel.js +30 -35
  36. package/dist/mapKernel.js.map +1 -1
  37. package/dist/package.json +3 -0
  38. package/dist/packageVersion.d.ts +1 -1
  39. package/dist/packageVersion.js +1 -1
  40. package/dist/packageVersion.js.map +1 -1
  41. package/dist/tsdoc-metadata.json +1 -1
  42. package/lib/{directory.d.mts → directory.d.ts} +15 -52
  43. package/lib/directory.d.ts.map +1 -0
  44. package/lib/{directory.mjs → directory.js} +110 -173
  45. package/lib/directory.js.map +1 -0
  46. package/lib/index.d.ts +61 -0
  47. package/lib/index.d.ts.map +1 -0
  48. package/lib/index.js +55 -0
  49. package/lib/index.js.map +1 -0
  50. package/lib/{interfaces.d.mts → interfaces.d.ts} +1 -1
  51. package/lib/interfaces.d.ts.map +1 -0
  52. package/lib/{interfaces.mjs → interfaces.js} +1 -1
  53. package/lib/interfaces.js.map +1 -0
  54. package/lib/{internalInterfaces.d.mts → internalInterfaces.d.ts} +3 -3
  55. package/lib/internalInterfaces.d.ts.map +1 -0
  56. package/lib/{internalInterfaces.mjs → internalInterfaces.js} +1 -1
  57. package/lib/internalInterfaces.js.map +1 -0
  58. package/lib/{localValues.d.mts → localValues.d.ts} +4 -6
  59. package/lib/localValues.d.ts.map +1 -0
  60. package/lib/{localValues.mjs → localValues.js} +10 -9
  61. package/lib/localValues.js.map +1 -0
  62. package/lib/{map-alpha.d.mts → map-alpha.d.ts} +43 -116
  63. package/lib/{map-beta.d.mts → map-beta.d.ts} +36 -105
  64. package/lib/{map-public.d.mts → map-public.d.ts} +36 -105
  65. package/lib/{map-untrimmed.d.mts → map-untrimmed.d.ts} +43 -116
  66. package/lib/{map.d.mts → map.d.ts} +5 -24
  67. package/lib/map.d.ts.map +1 -0
  68. package/lib/{map.mjs → map.js} +5 -28
  69. package/lib/map.js.map +1 -0
  70. package/lib/{mapKernel.d.mts → mapKernel.d.ts} +4 -5
  71. package/lib/mapKernel.d.ts.map +1 -0
  72. package/lib/{mapKernel.mjs → mapKernel.js} +32 -37
  73. package/lib/mapKernel.js.map +1 -0
  74. package/lib/{packageVersion.d.mts → packageVersion.d.ts} +2 -2
  75. package/lib/packageVersion.d.ts.map +1 -0
  76. package/lib/{packageVersion.mjs → packageVersion.js} +2 -2
  77. package/lib/packageVersion.js.map +1 -0
  78. package/lib/test/memory/directory.spec.js +71 -0
  79. package/lib/test/memory/directory.spec.js.map +1 -0
  80. package/lib/test/memory/map.spec.js +71 -0
  81. package/lib/test/memory/map.spec.js.map +1 -0
  82. package/lib/test/mocha/directory.order.spec.js +422 -0
  83. package/lib/test/mocha/directory.order.spec.js.map +1 -0
  84. package/lib/test/mocha/directory.snapshot.spec.js +111 -0
  85. package/lib/test/mocha/directory.snapshot.spec.js.map +1 -0
  86. package/lib/test/mocha/directory.spec.js +1406 -0
  87. package/lib/test/mocha/directory.spec.js.map +1 -0
  88. package/lib/test/mocha/directoryEquivalenceUtils.js +36 -0
  89. package/lib/test/mocha/directoryEquivalenceUtils.js.map +1 -0
  90. package/lib/test/mocha/directoryFuzzTests.spec.js +337 -0
  91. package/lib/test/mocha/directoryFuzzTests.spec.js.map +1 -0
  92. package/lib/test/mocha/dirname.cjs +16 -0
  93. package/lib/test/mocha/dirname.cjs.map +1 -0
  94. package/lib/test/mocha/map.fuzz.spec.js +114 -0
  95. package/lib/test/mocha/map.fuzz.spec.js.map +1 -0
  96. package/lib/test/mocha/map.spec.js +687 -0
  97. package/lib/test/mocha/map.spec.js.map +1 -0
  98. package/lib/test/mocha/rebasing.spec.js +158 -0
  99. package/lib/test/mocha/rebasing.spec.js.map +1 -0
  100. package/lib/test/mocha/reconnection.spec.js +327 -0
  101. package/lib/test/mocha/reconnection.spec.js.map +1 -0
  102. package/lib/test/types/validateMapPrevious.generated.js +66 -0
  103. package/lib/test/types/validateMapPrevious.generated.js.map +1 -0
  104. package/package.json +55 -52
  105. package/src/directory.ts +159 -226
  106. package/src/index.ts +57 -4
  107. package/src/interfaces.ts +2 -2
  108. package/src/internalInterfaces.ts +2 -2
  109. package/src/localValues.ts +14 -9
  110. package/src/map.ts +7 -32
  111. package/src/mapKernel.ts +40 -42
  112. package/src/packageVersion.ts +1 -1
  113. package/tsconfig.cjs.json +7 -0
  114. package/tsconfig.json +2 -5
  115. package/lib/directory.d.mts.map +0 -1
  116. package/lib/directory.mjs.map +0 -1
  117. package/lib/index.d.mts +0 -9
  118. package/lib/index.d.mts.map +0 -1
  119. package/lib/index.mjs +0 -8
  120. package/lib/index.mjs.map +0 -1
  121. package/lib/interfaces.d.mts.map +0 -1
  122. package/lib/interfaces.mjs.map +0 -1
  123. package/lib/internalInterfaces.d.mts.map +0 -1
  124. package/lib/internalInterfaces.mjs.map +0 -1
  125. package/lib/localValues.d.mts.map +0 -1
  126. package/lib/localValues.mjs.map +0 -1
  127. package/lib/map.d.mts.map +0 -1
  128. package/lib/map.mjs.map +0 -1
  129. package/lib/mapKernel.d.mts.map +0 -1
  130. package/lib/mapKernel.mjs.map +0 -1
  131. package/lib/packageVersion.d.mts.map +0 -1
  132. package/lib/packageVersion.mjs.map +0 -1
package/src/directory.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { assert } from "@fluidframework/core-utils";
6
+ import { assert, unreachableCase } from "@fluidframework/core-utils";
7
7
  import { TypedEventEmitter } from "@fluid-internal/client-utils";
8
- import { UsageError } from "@fluidframework/telemetry-utils";
8
+ import { ITelemetryLoggerExt, UsageError } from "@fluidframework/telemetry-utils";
9
9
  import { readAndParse } from "@fluidframework/driver-utils";
10
10
  import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
11
11
  import {
@@ -16,7 +16,12 @@ import {
16
16
  IChannelFactory,
17
17
  } from "@fluidframework/datastore-definitions";
18
18
  import { ISummaryTreeWithStats, ITelemetryContext } from "@fluidframework/runtime-definitions";
19
- import { IFluidSerializer, SharedObject, ValueType } from "@fluidframework/shared-object-base";
19
+ import {
20
+ IFluidSerializer,
21
+ SharedObject,
22
+ ValueType,
23
+ parseHandles,
24
+ } from "@fluidframework/shared-object-base";
20
25
  import { SummaryTreeBuilder } from "@fluidframework/runtime-utils";
21
26
  import path from "path-browserify";
22
27
  import { RedBlackTree } from "@fluidframework/merge-tree";
@@ -30,9 +35,9 @@ import {
30
35
  ISharedDirectory,
31
36
  ISharedDirectoryEvents,
32
37
  IValueChanged,
33
- } from "./interfaces";
34
- import { ILocalValue, LocalValueMaker, makeSerializable } from "./localValues";
35
- import { pkgVersion } from "./packageVersion";
38
+ } from "./interfaces.js";
39
+ import { ILocalValue, LocalValueMaker, makeSerializable } from "./localValues.js";
40
+ import { pkgVersion } from "./packageVersion.js";
36
41
 
37
42
  // We use path-browserify since this code can run safely on the server or the browser.
38
43
  // We standardize on using posix slashes everywhere.
@@ -65,8 +70,6 @@ interface IDirectoryMessageHandler {
65
70
  * @param localOpMetadata - The metadata to be submitted with the message.
66
71
  */
67
72
  submit(op: IDirectoryOperation, localOpMetadata: unknown): void;
68
-
69
- applyStashedOp(op: IDirectoryOperation): unknown;
70
73
  }
71
74
 
72
75
  /**
@@ -271,7 +274,7 @@ export interface IDirectoryNewStorageFormat {
271
274
  * @sealed
272
275
  * @alpha
273
276
  */
274
- export class DirectoryFactory implements IChannelFactory {
277
+ export class DirectoryFactory implements IChannelFactory<ISharedDirectory> {
275
278
  /**
276
279
  * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
277
280
  */
@@ -346,25 +349,25 @@ export class DirectoryFactory implements IChannelFactory {
346
349
  * 4. A 'seq' value of zero indicates that the subdirectory was created in detached state, and it is considered acknowledged for the
347
350
  * purpose of ordering.
348
351
  */
349
- const seqDataComparator = (a: SequenceData, b: SequenceData) => {
352
+ const seqDataComparator = (a: SequenceData, b: SequenceData): number => {
350
353
  if (isAcknowledgedOrDetached(a)) {
351
354
  if (isAcknowledgedOrDetached(b)) {
352
355
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353
- return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq! - b.clientSeq!;
356
+ return a.seq === b.seq ? a.clientSeq! - b.clientSeq! : a.seq - b.seq;
354
357
  } else {
355
358
  return -1;
356
359
  }
357
360
  } 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 {
361
+ if (isAcknowledgedOrDetached(b)) {
362
362
  return 1;
363
+ } else {
364
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
365
+ return a.seq === b.seq ? a.clientSeq! - b.clientSeq! : a.seq - b.seq;
363
366
  }
364
367
  }
365
368
  };
366
369
 
367
- function isAcknowledgedOrDetached(seqData: SequenceData) {
370
+ function isAcknowledgedOrDetached(seqData: SequenceData): boolean {
368
371
  return seqData.seq >= 0;
369
372
  }
370
373
 
@@ -433,6 +436,10 @@ class DirectoryCreationTracker {
433
436
  }, keys);
434
437
  return keys;
435
438
  }
439
+
440
+ public get size(): number {
441
+ return this.keyToIndex.size;
442
+ }
436
443
  }
437
444
 
438
445
  /**
@@ -460,8 +467,8 @@ export class SharedDirectory
460
467
  * @param id - Optional name of the shared directory
461
468
  * @returns Newly create shared directory (but not attached yet)
462
469
  */
463
- public static create(runtime: IFluidDataStoreRuntime, id?: string): SharedDirectory {
464
- return runtime.createChannel(id, DirectoryFactory.Type) as SharedDirectory;
470
+ public static create(runtime: IFluidDataStoreRuntime, id?: string): ISharedDirectory {
471
+ return runtime.createChannel(id, DirectoryFactory.Type) as ISharedDirectory;
465
472
  }
466
473
 
467
474
  /**
@@ -469,7 +476,7 @@ export class SharedDirectory
469
476
  *
470
477
  * @returns A factory that creates and load SharedDirectory
471
478
  */
472
- public static getFactory(): IChannelFactory {
479
+ public static getFactory(): IChannelFactory<ISharedDirectory> {
473
480
  return new DirectoryFactory();
474
481
  }
475
482
 
@@ -498,6 +505,7 @@ export class SharedDirectory
498
505
  this.runtime,
499
506
  this.serializer,
500
507
  posix.sep,
508
+ this.logger,
501
509
  );
502
510
 
503
511
  /**
@@ -518,7 +526,7 @@ export class SharedDirectory
518
526
  attributes: IChannelAttributes,
519
527
  ) {
520
528
  super(id, runtime, attributes, "fluid_directory_");
521
- this.localValueMaker = new LocalValueMaker(this.serializer);
529
+ this.localValueMaker = new LocalValueMaker();
522
530
  this.setMessageHandlers();
523
531
  // Mirror the containedValueChanged op on the SharedDirectory
524
532
  this.root.on("containedValueChanged", (changed: IValueChanged, local: boolean) => {
@@ -781,10 +789,7 @@ export class SharedDirectory
781
789
  // guaranteed during the serialization process. As a result, it is only essential to utilize the
782
790
  // "fake" client sequence number to signify the loading order, and there is no need to retain
783
791
  // the actual client sequence number at this point.
784
- if (createInfo !== undefined && createInfo.csn > -1) {
785
- // If csn is -1, then initialize it with 0, otherwise we will never process ops for this
786
- // sub directory. This could be done at serialization time too, but we need to maintain
787
- // back compat too and also we will actually know the state when it was serialized.
792
+ if (createInfo !== undefined && createInfo.csn > 0) {
788
793
  if (!tempSeqNums.has(createInfo.csn)) {
789
794
  tempSeqNums.set(createInfo.csn, 0);
790
795
  }
@@ -792,6 +797,13 @@ export class SharedDirectory
792
797
  seqData = { seq: createInfo.csn, clientSeq: fakeClientSeq };
793
798
  tempSeqNums.set(createInfo.csn, ++fakeClientSeq);
794
799
  } else {
800
+ /**
801
+ * 1. If csn is -1, then initialize it with 0, otherwise we will never process ops for this
802
+ * sub directory. This could be done at serialization time too, but we need to maintain
803
+ * back compat too and also we will actually know the state when it was serialized.
804
+ * 2. We need to make the csn = -1 and csn = 0 share the same counter, there are cases
805
+ * where both -1 and 0 coexist within a single document.
806
+ */
795
807
  seqData = {
796
808
  seq: 0,
797
809
  clientSeq: ++currentSubDir.localCreationSeq,
@@ -799,13 +811,14 @@ export class SharedDirectory
799
811
  }
800
812
  newSubDir = new SubDirectory(
801
813
  seqData,
802
- createInfo !== undefined
803
- ? new Set<string>(createInfo.ccIds)
804
- : new Set(),
814
+ createInfo === undefined
815
+ ? new Set()
816
+ : new Set<string>(createInfo.ccIds),
805
817
  this,
806
818
  this.runtime,
807
819
  this.serializer,
808
820
  posix.join(currentSubDir.absolutePath, subdirName),
821
+ this.logger,
809
822
  );
810
823
  currentSubDir.populateSubDirectory(subdirName, newSubDir);
811
824
  // Record the newly inserted subdirectory to the creation tracker
@@ -822,7 +835,8 @@ export class SharedDirectory
822
835
  const localValue = this.makeLocal(
823
836
  key,
824
837
  currentSubDir.absolutePath,
825
- serializable,
838
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
839
+ parseHandles(serializable, this.serializer),
826
840
  );
827
841
  currentSubDir.populateStorage(key, localValue);
828
842
  }
@@ -886,7 +900,7 @@ export class SharedDirectory
886
900
  serializable.type === ValueType[ValueType.Shared],
887
901
  0x1e4 /* "Unexpected serializable type" */,
888
902
  );
889
- return this.localValueMaker.fromSerializable(serializable);
903
+ return this.localValueMaker.fromSerializable(serializable, this.serializer, this.handle);
890
904
  }
891
905
 
892
906
  /**
@@ -940,12 +954,6 @@ export class SharedDirectory
940
954
  subdir.resubmitClearMessage(op, localOpMetadata);
941
955
  }
942
956
  },
943
- applyStashedOp: (op: IDirectoryClearOperation): IClearLocalOpMetadata | undefined => {
944
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
945
- if (subdir) {
946
- return subdir.applyStashedClearMessage(op);
947
- }
948
- },
949
957
  });
950
958
  this.messageHandlers.set("delete", {
951
959
  process: (
@@ -967,14 +975,6 @@ export class SharedDirectory
967
975
  subdir.resubmitKeyMessage(op, localOpMetadata);
968
976
  }
969
977
  },
970
- applyStashedOp: (
971
- op: IDirectoryDeleteOperation,
972
- ): IKeyEditLocalOpMetadata | undefined => {
973
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
974
- if (subdir) {
975
- return subdir.applyStashedDeleteMessage(op);
976
- }
977
- },
978
978
  });
979
979
  this.messageHandlers.set("set", {
980
980
  process: (
@@ -997,13 +997,6 @@ export class SharedDirectory
997
997
  subdir.resubmitKeyMessage(op, localOpMetadata);
998
998
  }
999
999
  },
1000
- applyStashedOp: (op: IDirectorySetOperation): IKeyEditLocalOpMetadata | undefined => {
1001
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1002
- if (subdir) {
1003
- const context = this.makeLocal(op.key, op.path, op.value);
1004
- return subdir.applyStashedSetMessage(op, context);
1005
- }
1006
- },
1007
1000
  });
1008
1001
 
1009
1002
  this.messageHandlers.set("createSubDirectory", {
@@ -1027,14 +1020,6 @@ export class SharedDirectory
1027
1020
  parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
1028
1021
  }
1029
1022
  },
1030
- applyStashedOp: (
1031
- op: IDirectoryCreateSubDirectoryOperation,
1032
- ): ICreateSubDirLocalOpMetadata | undefined => {
1033
- const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1034
- if (parentSubdir) {
1035
- return parentSubdir.applyStashedCreateSubDirMessage(op);
1036
- }
1037
- },
1038
1023
  });
1039
1024
 
1040
1025
  this.messageHandlers.set("deleteSubDirectory", {
@@ -1058,26 +1043,47 @@ export class SharedDirectory
1058
1043
  parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
1059
1044
  }
1060
1045
  },
1061
- applyStashedOp: (
1062
- op: IDirectoryDeleteSubDirectoryOperation,
1063
- ): IDeleteSubDirLocalOpMetadata | undefined => {
1064
- const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1065
- if (parentSubdir) {
1066
- return parentSubdir.applyStashedDeleteSubDirMessage(op);
1067
- }
1068
- },
1069
1046
  });
1070
1047
  }
1071
1048
 
1072
1049
  /**
1073
1050
  * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
1074
1051
  */
1075
- protected applyStashedOp(op: unknown): unknown {
1076
- const handler = this.messageHandlers.get((op as IDirectoryOperation).type);
1077
- if (handler === undefined) {
1078
- throw new Error("no apply stashed op handler");
1052
+ protected applyStashedOp(op: unknown): void {
1053
+ const directoryOp = op as IDirectoryOperation;
1054
+ const dir = this.getWorkingDirectory(directoryOp.path);
1055
+ switch (directoryOp.type) {
1056
+ case "clear": {
1057
+ dir?.clear();
1058
+ break;
1059
+ }
1060
+ case "createSubDirectory": {
1061
+ dir?.createSubDirectory(directoryOp.subdirName);
1062
+ break;
1063
+ }
1064
+ case "delete": {
1065
+ dir?.delete(directoryOp.key);
1066
+ break;
1067
+ }
1068
+ case "deleteSubDirectory": {
1069
+ dir?.deleteSubDirectory(directoryOp.subdirName);
1070
+ break;
1071
+ }
1072
+ case "set": {
1073
+ dir?.set(
1074
+ directoryOp.key,
1075
+ this.localValueMaker.fromSerializable(
1076
+ directoryOp.value,
1077
+ this.serializer,
1078
+ this.handle,
1079
+ ).value,
1080
+ );
1081
+ break;
1082
+ }
1083
+ default: {
1084
+ unreachableCase(directoryOp);
1085
+ }
1079
1086
  }
1080
- return handler.applyStashedOp(op as IDirectoryOperation);
1081
1087
  }
1082
1088
 
1083
1089
  private serializeDirectory(
@@ -1211,10 +1217,13 @@ function isDirectoryLocalOpMetadata(metadata: any): metadata is DirectoryLocalOp
1211
1217
 
1212
1218
  /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
1213
1219
 
1220
+ // eslint-disable-next-line @rushstack/no-new-null
1214
1221
  function assertNonNullClientId(clientId: string | null): asserts clientId is string {
1215
1222
  assert(clientId !== null, 0x6af /* client id should never be null */);
1216
1223
  }
1217
1224
 
1225
+ let hasLoggedDirectoryInconsistency = false;
1226
+
1218
1227
  /**
1219
1228
  * Node of the directory tree.
1220
1229
  * @sealed
@@ -1305,6 +1314,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1305
1314
  private readonly runtime: IFluidDataStoreRuntime,
1306
1315
  private readonly serializer: IFluidSerializer,
1307
1316
  public readonly absolutePath: string,
1317
+ private readonly logger: ITelemetryLoggerExt,
1308
1318
  ) {
1309
1319
  super();
1310
1320
  this.localCreationSeqTracker = new DirectoryCreationTracker();
@@ -1438,13 +1448,15 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1438
1448
  }
1439
1449
 
1440
1450
  /**
1441
- * @returns The Sequence Data which should be used for local changes.
1451
+ * Gets the Sequence Data which should be used for local changes.
1452
+ *
1442
1453
  * @remarks While detached, 0 is used rather than -1 to represent a change which should be universally known (as opposed to known
1443
1454
  * only by the local client). This ensures that if the directory is later attached, none of its data needs to be updated (the values
1444
1455
  * last set while detached will now be known to any new client, until they are changed).
1445
1456
  *
1446
1457
  * The client sequence number is incremented by 1 for maintaining the internal order of locally created subdirectories
1447
- * TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
1458
+ *
1459
+ * @privateRemarks TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
1448
1460
  */
1449
1461
  private getLocalSeq(): SequenceData {
1450
1462
  return this.directory.isAttached()
@@ -1506,23 +1518,41 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1506
1518
 
1507
1519
  const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
1508
1520
 
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
- );
1521
+ if (subdirNames.length !== this._subdirectories.size) {
1522
+ // TODO: AB#7022: Hitting this block indicates that the eventual consistency scheme for ordering subdirectories
1523
+ // has failed. Fall back to previous directory behavior, which didn't guarantee ordering.
1524
+ // It's not currently clear how to reach this state, so log some diagnostics to help understand the issue.
1525
+ // This whole block should eventually be replaced by an assert that the two sizes align.
1526
+ if (!hasLoggedDirectoryInconsistency) {
1527
+ this.logger.sendTelemetryEvent({
1528
+ eventName: "inconsistentSubdirectoryOrdering",
1529
+ localKeyCount: this.localCreationSeqTracker.size,
1530
+ ackedKeyCount: this.ackedCreationSeqTracker.size,
1531
+ subdirNamesLength: subdirNames.length,
1532
+ subdirectoriesSize: this._subdirectories.size,
1533
+ });
1534
+ hasLoggedDirectoryInconsistency = true;
1535
+ }
1536
+
1537
+ return this._subdirectories.entries();
1538
+ }
1513
1539
 
1514
1540
  const entriesIterator = {
1515
1541
  index: 0,
1516
1542
  dirs: this._subdirectories,
1517
- next(): IteratorResult<[string, any]> {
1543
+ next(): IteratorResult<[string, IDirectory]> {
1518
1544
  if (this.index < subdirNames.length) {
1519
1545
  const subdirName = subdirNames[this.index++];
1520
1546
  const subdir = this.dirs.get(subdirName);
1547
+ assert(
1548
+ subdir !== undefined,
1549
+ 0x8ac /* Could not find expected sub-directory. */,
1550
+ );
1521
1551
  return { value: [subdirName, subdir], done: false };
1522
1552
  }
1523
1553
  return { value: undefined, done: true };
1524
1554
  },
1525
- [Symbol.iterator](): IterableIterator<[string, any]> {
1555
+ [Symbol.iterator](): IterableIterator<[string, IDirectory]> {
1526
1556
  return this;
1527
1557
  },
1528
1558
  };
@@ -1685,7 +1715,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1685
1715
  * @param local - Whether the message originated from the local client
1686
1716
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1687
1717
  * For messages from a remote client, this will be undefined.
1688
- * @internal
1689
1718
  */
1690
1719
  public processClearMessage(
1691
1720
  msg: ISequencedDocumentMessage,
@@ -1712,25 +1741,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1712
1741
  this.clearExceptPendingKeys(false);
1713
1742
  }
1714
1743
 
1715
- /**
1716
- * Apply clear operation locally and generate metadata
1717
- * @param op - Op to apply
1718
- * @returns metadata generated for stahed op
1719
- */
1720
- public applyStashedClearMessage(op: IDirectoryClearOperation): IClearLocalOpMetadata {
1721
- this.throwIfDisposed();
1722
- const previousValue = new Map<string, ILocalValue>(this._storage);
1723
- this.clearExceptPendingKeys(true);
1724
- const pendingMsgId = ++this.pendingMessageId;
1725
- this.pendingClearMessageIds.push(pendingMsgId);
1726
- const metadata: IClearLocalOpMetadata = {
1727
- type: "clear",
1728
- pendingMessageId: pendingMsgId,
1729
- previousStorage: previousValue,
1730
- };
1731
- return metadata;
1732
- }
1733
-
1734
1744
  /**
1735
1745
  * Process a delete operation.
1736
1746
  * @param msg - The message from the server to apply.
@@ -1738,7 +1748,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1738
1748
  * @param local - Whether the message originated from the local client
1739
1749
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1740
1750
  * For messages from a remote client, this will be undefined.
1741
- * @internal
1742
1751
  */
1743
1752
  public processDeleteMessage(
1744
1753
  msg: ISequencedDocumentMessage,
@@ -1758,23 +1767,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1758
1767
  this.deleteCore(op.key, local);
1759
1768
  }
1760
1769
 
1761
- /**
1762
- * Apply delete operation locally and generate metadata
1763
- * @param op - Op to apply
1764
- * @returns metadata generated for stahed op
1765
- */
1766
- public applyStashedDeleteMessage(op: IDirectoryDeleteOperation): IKeyEditLocalOpMetadata {
1767
- this.throwIfDisposed();
1768
- const previousValue = this.deleteCore(op.key, true);
1769
- const pendingMessageId = this.getKeyMessageId(op);
1770
- const localMetadata: IKeyEditLocalOpMetadata = {
1771
- type: "edit",
1772
- pendingMessageId,
1773
- previousValue,
1774
- };
1775
- return localMetadata;
1776
- }
1777
-
1778
1770
  /**
1779
1771
  * Process a set operation.
1780
1772
  * @param msg - The message from the server to apply.
@@ -1782,7 +1774,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1782
1774
  * @param local - Whether the message originated from the local client
1783
1775
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1784
1776
  * For messages from a remote client, this will be undefined.
1785
- * @internal
1786
1777
  */
1787
1778
  public processSetMessage(
1788
1779
  msg: ISequencedDocumentMessage,
@@ -1807,28 +1798,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1807
1798
  this.setCore(op.key, context!, local);
1808
1799
  }
1809
1800
 
1810
- /**
1811
- * Apply set operation locally and generate metadata
1812
- * @param op - Op to apply
1813
- * @returns metadata generated for stahed op
1814
- */
1815
- public applyStashedSetMessage(
1816
- op: IDirectorySetOperation,
1817
- context: ILocalValue,
1818
- ): IKeyEditLocalOpMetadata {
1819
- this.throwIfDisposed();
1820
- // Set the value locally.
1821
- const previousValue = this.setCore(op.key, context, true);
1822
-
1823
- // Create metadata
1824
- const pendingMessageId = this.getKeyMessageId(op);
1825
- const localMetadata: IKeyEditLocalOpMetadata = {
1826
- type: "edit",
1827
- pendingMessageId,
1828
- previousValue,
1829
- };
1830
- return localMetadata;
1831
- }
1832
1801
  /**
1833
1802
  * Process a create subdirectory operation.
1834
1803
  * @param msg - The message from the server to apply.
@@ -1836,7 +1805,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1836
1805
  * @param local - Whether the message originated from the local client
1837
1806
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1838
1807
  * For messages from a remote client, this will be undefined.
1839
- * @internal
1840
1808
  */
1841
1809
  public processCreateSubDirectoryMessage(
1842
1810
  msg: ISequencedDocumentMessage,
@@ -1862,30 +1830,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1862
1830
  );
1863
1831
  }
1864
1832
 
1865
- /**
1866
- * Apply createSubDirectory operation locally and generate metadata
1867
- * @param op - Op to apply
1868
- * @returns metadata generated for stahed op
1869
- */
1870
- public applyStashedCreateSubDirMessage(
1871
- op: IDirectoryCreateSubDirectoryOperation,
1872
- ): ICreateSubDirLocalOpMetadata {
1873
- this.throwIfDisposed();
1874
- // Create the sub directory locally first.
1875
- this.createSubDirectoryCore(
1876
- op.subdirName,
1877
- true,
1878
- this.getLocalSeq(),
1879
- this.runtime.clientId ?? "detached",
1880
- );
1881
- this.updatePendingSubDirMessageCount(op);
1882
-
1883
- const localOpMetadata: ICreateSubDirLocalOpMetadata = {
1884
- type: "createSubDir",
1885
- };
1886
- return localOpMetadata;
1887
- }
1888
-
1889
1833
  /**
1890
1834
  * Process a delete subdirectory operation.
1891
1835
  * @param msg - The message from the server to apply.
@@ -1893,7 +1837,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1893
1837
  * @param local - Whether the message originated from the local client
1894
1838
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1895
1839
  * For messages from a remote client, this will be undefined.
1896
- * @internal
1897
1840
  */
1898
1841
  public processDeleteSubDirectoryMessage(
1899
1842
  msg: ISequencedDocumentMessage,
@@ -1913,24 +1856,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1913
1856
  this.deleteSubDirectoryCore(op.subdirName, local);
1914
1857
  }
1915
1858
 
1916
- /**
1917
- * Apply deleteSubDirectory operation locally and generate metadata
1918
- * @param op - Op to apply
1919
- * @returns metadata generated for stahed op
1920
- */
1921
- public applyStashedDeleteSubDirMessage(
1922
- op: IDirectoryDeleteSubDirectoryOperation,
1923
- ): IDeleteSubDirLocalOpMetadata {
1924
- this.throwIfDisposed();
1925
- const subDir = this.deleteSubDirectoryCore(op.subdirName, true);
1926
- this.updatePendingSubDirMessageCount(op);
1927
- const metadata: IDeleteSubDirLocalOpMetadata = {
1928
- type: "deleteSubDir",
1929
- subDirectory: subDir,
1930
- };
1931
- return metadata;
1932
- }
1933
-
1934
1859
  /**
1935
1860
  * Submit a clear operation.
1936
1861
  * @param op - The operation
@@ -1953,7 +1878,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1953
1878
  /**
1954
1879
  * Resubmit a clear operation.
1955
1880
  * @param op - The operation
1956
- * @internal
1957
1881
  */
1958
1882
  public resubmitClearMessage(op: IDirectoryClearOperation, localOpMetadata: unknown): void {
1959
1883
  assert(
@@ -1976,10 +1900,10 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1976
1900
  // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1977
1901
  const pendingMessageId = ++this.pendingMessageId;
1978
1902
  const pendingMessageIds = this.pendingKeys.get(op.key);
1979
- if (pendingMessageIds !== undefined) {
1980
- pendingMessageIds.push(pendingMessageId);
1981
- } else {
1903
+ if (pendingMessageIds === undefined) {
1982
1904
  this.pendingKeys.set(op.key, [pendingMessageId]);
1905
+ } else {
1906
+ pendingMessageIds.push(pendingMessageId);
1983
1907
  }
1984
1908
  return pendingMessageId;
1985
1909
  }
@@ -2000,7 +1924,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2000
1924
  * Submit a key message to remote clients based on a previous submit.
2001
1925
  * @param op - The map key message
2002
1926
  * @param localOpMetadata - Metadata from the previous submit
2003
- * @internal
2004
1927
  */
2005
1928
  public resubmitKeyMessage(op: IDirectoryKeyOperation, localOpMetadata: unknown): void {
2006
1929
  assert(
@@ -2013,9 +1936,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2013
1936
  // Only submit the op, if we have record for it, otherwise it is possible that the older instance
2014
1937
  // is already deleted, in which case we don't need to submit the op.
2015
1938
  if (pendingMessageIds !== undefined) {
2016
- const index = pendingMessageIds.findIndex(
2017
- (id) => id === localOpMetadata.pendingMessageId,
2018
- );
1939
+ const index = pendingMessageIds.indexOf(localOpMetadata.pendingMessageId);
2019
1940
  if (index === -1) {
2020
1941
  return;
2021
1942
  }
@@ -2027,12 +1948,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2027
1948
  }
2028
1949
  }
2029
1950
 
2030
- private incrementPendingSubDirCount(map: Map<string, number>, subDirName: string) {
1951
+ private incrementPendingSubDirCount(map: Map<string, number>, subDirName: string): void {
2031
1952
  const count = map.get(subDirName) ?? 0;
2032
1953
  map.set(subDirName, count + 1);
2033
1954
  }
2034
1955
 
2035
- private decrementPendingSubDirCount(map: Map<string, number>, subDirName: string) {
1956
+ private decrementPendingSubDirCount(map: Map<string, number>, subDirName: string): void {
2036
1957
  const count = map.get(subDirName) ?? 0;
2037
1958
  map.set(subDirName, count - 1);
2038
1959
  if (count <= 1) {
@@ -2044,7 +1965,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2044
1965
  * Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
2045
1966
  * or while resubmitting the op.
2046
1967
  */
2047
- private updatePendingSubDirMessageCount(op: IDirectorySubDirectoryOperation) {
1968
+ private updatePendingSubDirMessageCount(op: IDirectorySubDirectoryOperation): void {
2048
1969
  if (op.type === "deleteSubDirectory") {
2049
1970
  this.incrementPendingSubDirCount(
2050
1971
  this.pendingDeleteSubDirectoriesTracker,
@@ -2095,7 +2016,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2095
2016
  * Submit a subdirectory operation again
2096
2017
  * @param op - The operation
2097
2018
  * @param localOpMetadata - metadata submitted with the op originally
2098
- * @internal
2099
2019
  */
2100
2020
  public resubmitSubDirectoryMessage(
2101
2021
  op: IDirectorySubDirectoryOperation,
@@ -2139,7 +2059,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2139
2059
  * Get the storage of this subdirectory in a serializable format, to be used in snapshotting.
2140
2060
  * @param serializer - The serializer to use to serialize handles in its values.
2141
2061
  * @returns The JSONable string representing the storage of this subdirectory
2142
- * @internal
2143
2062
  */
2144
2063
  public *getSerializedStorage(
2145
2064
  serializer: IFluidSerializer,
@@ -2152,11 +2071,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2152
2071
  }
2153
2072
  }
2154
2073
 
2155
- public getSerializableCreateInfo() {
2074
+ public getSerializableCreateInfo(): ICreateInfo {
2156
2075
  this.throwIfDisposed();
2157
2076
  const createInfo: ICreateInfo = {
2158
2077
  csn: this.seqData.seq,
2159
- ccIds: Array.from(this.clientIds),
2078
+ ccIds: [...this.clientIds],
2160
2079
  };
2161
2080
  return createInfo;
2162
2081
  }
@@ -2165,7 +2084,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2165
2084
  * Populate a key value in this subdirectory's storage, to be used when loading from snapshot.
2166
2085
  * @param key - The key to populate
2167
2086
  * @param localValue - The local value to populate into it
2168
- * @internal
2169
2087
  */
2170
2088
  public populateStorage(key: string, localValue: ILocalValue): void {
2171
2089
  this.throwIfDisposed();
@@ -2176,7 +2094,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2176
2094
  * Populate a subdirectory into this subdirectory, to be used when loading from snapshot.
2177
2095
  * @param subdirName - The name of the subdirectory to add
2178
2096
  * @param newSubDir - The new subdirectory to add
2179
- * @internal
2180
2097
  */
2181
2098
  public populateSubDirectory(subdirName: string, newSubDir: SubDirectory): void {
2182
2099
  this.throwIfDisposed();
@@ -2188,7 +2105,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2188
2105
  * value so op handlers can be retrieved
2189
2106
  * @param key - The key to retrieve from
2190
2107
  * @returns The local value
2191
- * @internal
2192
2108
  */
2193
2109
  public getLocalValue<T extends ILocalValue = ILocalValue>(key: string): T {
2194
2110
  this.throwIfDisposed();
@@ -2241,46 +2157,62 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2241
2157
  throw new Error("Rollback op does match last clear");
2242
2158
  }
2243
2159
  } else if ((op.type === "delete" || op.type === "set") && localOpMetadata.type === "edit") {
2160
+ const key: unknown = op.key;
2161
+ assert(key !== undefined, 0x8ad /* "key" property is missing from edit operation. */);
2162
+ assert(
2163
+ typeof key === "string",
2164
+ 0x8ae /* "key" property in edit operation is misconfigured. Expected a string. */,
2165
+ );
2166
+
2244
2167
  if (localOpMetadata.previousValue === undefined) {
2245
- this.deleteCore(op.key as string, true);
2168
+ this.deleteCore(key, true);
2246
2169
  } else {
2247
- this.setCore(op.key as string, localOpMetadata.previousValue, true);
2170
+ this.setCore(key, localOpMetadata.previousValue, true);
2248
2171
  }
2249
2172
 
2250
- this.rollbackPendingMessageId(
2251
- this.pendingKeys,
2252
- op.key as string,
2253
- localOpMetadata.pendingMessageId,
2254
- );
2173
+ this.rollbackPendingMessageId(this.pendingKeys, key, localOpMetadata.pendingMessageId);
2255
2174
  } else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
2256
- this.deleteSubDirectoryCore(op.subdirName as string, true);
2257
-
2258
- this.decrementPendingSubDirCount(
2259
- this.pendingCreateSubDirectoriesTracker,
2260
- op.subdirName as string,
2175
+ const subdirName: unknown = op.subdirName;
2176
+ assert(
2177
+ subdirName !== undefined,
2178
+ 0x8af /* "subdirName" property is missing from "createSubDirectory" operation. */,
2179
+ );
2180
+ assert(
2181
+ typeof subdirName === "string",
2182
+ 0x8b0 /* "subdirName" property in "createSubDirectory" operation is misconfigured. Expected a string. */,
2261
2183
  );
2184
+
2185
+ this.deleteSubDirectoryCore(subdirName, true);
2186
+ this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, subdirName);
2262
2187
  } else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
2188
+ const subdirName: unknown = op.subdirName;
2189
+ assert(
2190
+ subdirName !== undefined,
2191
+ 0x8b1 /* "subdirName" property is missing from "deleteSubDirectory" operation. */,
2192
+ );
2193
+ assert(
2194
+ typeof subdirName === "string",
2195
+ 0x8b2 /* "subdirName" property in "deleteSubDirectory" operation is misconfigured. Expected a string. */,
2196
+ );
2197
+
2263
2198
  if (localOpMetadata.subDirectory !== undefined) {
2264
2199
  this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
2265
2200
  // don't need to register events because deleting never unregistered
2266
- this._subdirectories.set(op.subdirName as string, localOpMetadata.subDirectory);
2201
+ this._subdirectories.set(subdirName, localOpMetadata.subDirectory);
2267
2202
  // Restore the record in creation tracker
2268
2203
  if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
2269
- this.ackedCreationSeqTracker.set(op.subdirName, {
2204
+ this.ackedCreationSeqTracker.set(subdirName, {
2270
2205
  ...localOpMetadata.subDirectory.seqData,
2271
2206
  });
2272
2207
  } else {
2273
- this.localCreationSeqTracker.set(op.subdirName, {
2208
+ this.localCreationSeqTracker.set(subdirName, {
2274
2209
  ...localOpMetadata.subDirectory.seqData,
2275
2210
  });
2276
2211
  }
2277
- this.emit("subDirectoryCreated", op.subdirName, true, this);
2212
+ this.emit("subDirectoryCreated", subdirName, true, this);
2278
2213
  }
2279
2214
 
2280
- this.decrementPendingSubDirCount(
2281
- this.pendingDeleteSubDirectoriesTracker,
2282
- op.subDirName as string,
2283
- );
2215
+ this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, subdirName);
2284
2216
  } else {
2285
2217
  throw new Error("Unsupported op for rollback");
2286
2218
  }
@@ -2370,7 +2302,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2370
2302
  * can be deleted and created again, then this finds if the message is for current instance of directory or not.
2371
2303
  * @param msg - message for the directory
2372
2304
  */
2373
- private isMessageForCurrentInstanceOfSubDirectory(msg: ISequencedDocumentMessage) {
2305
+ private isMessageForCurrentInstanceOfSubDirectory(msg: ISequencedDocumentMessage): boolean {
2374
2306
  // If the message is either from the creator of directory or this directory was created when
2375
2307
  // container was detached or in case this directory is already live(known to other clients)
2376
2308
  // and the op was created after the directory was created then apply this op.
@@ -2592,15 +2524,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2592
2524
  this.runtime,
2593
2525
  this.serializer,
2594
2526
  absolutePath,
2527
+ this.logger,
2595
2528
  );
2596
2529
  /**
2597
2530
  * Store the sequnce numbers of newly created subdirectory to the proper creation tracker, based
2598
2531
  * on whether the creation behavior has been ack'd or not
2599
2532
  */
2600
- if (!isAcknowledgedOrDetached(seqData)) {
2601
- this.localCreationSeqTracker.set(subdirName, { ...seqData });
2602
- } else {
2533
+ if (isAcknowledgedOrDetached(seqData)) {
2603
2534
  this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
2535
+ } else {
2536
+ this.localCreationSeqTracker.set(subdirName, { ...seqData });
2604
2537
  }
2605
2538
 
2606
2539
  this.registerEventsOnSubDirectory(subDir, subdirName);