@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.
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3
- "extends": "@fluidframework/build-common/api-extractor-base.json",
3
+ "extends": "../../../common/build/build-common/api-extractor-base.json",
4
4
  "dtsRollup": {
5
5
  "enabled": true
6
+ },
7
+ "messages": {
8
+ "extractorMessageReporting": {
9
+ // TODO: Add missing documentation and remove this rule override
10
+ "ae-undocumented": {
11
+ "logLevel": "none"
12
+ }
13
+ }
6
14
  }
7
15
  }
@@ -37,6 +37,7 @@ const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
37
37
  const shared_object_base_1 = require("@fluidframework/shared-object-base");
38
38
  const runtime_utils_1 = require("@fluidframework/runtime-utils");
39
39
  const path = __importStar(require("path-browserify"));
40
+ const merge_tree_1 = require("@fluidframework/merge-tree");
40
41
  const localValues_1 = require("./localValues.cjs");
41
42
  const packageVersion_1 = require("./packageVersion.cjs");
42
43
  // We use path-browserify since this code can run safely on the server or the browser.
@@ -92,6 +93,100 @@ DirectoryFactory.Attributes = {
92
93
  snapshotFormatVersion: "0.1",
93
94
  packageVersion: packageVersion_1.pkgVersion,
94
95
  };
96
+ /**
97
+ * The comparator essentially performs the following procedure to determine the order of subdirectory creation:
98
+ * 1. If subdirectory A has a non-negative 'seq' and subdirectory B has a negative 'seq', subdirectory A is always placed first due to
99
+ * the policy that acknowledged subdirectories precede locally created ones that have not been committed yet.
100
+ *
101
+ * 2. When both subdirectories A and B have a non-negative 'seq', they are compared as follows:
102
+ * - 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
103
+ * should not be applied in the directory ordering, since the lowest 'seq' is -1, when the directory is created locally but not acknowledged yet.
104
+ * - 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
105
+ * batching is enabled, and a lower 'clientSeq' indicates that it was processed earlier after the batch was ungrouped.
106
+ *
107
+ * 3. When both subdirectories A and B have a negative 'seq', they are compared as follows:
108
+ * - If A and B have different 'seq', the one with lower 'seq' will be positioned ahead, which indicates the corresponding creation message was
109
+ * acknowledged by the server earlier.
110
+ * - 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
111
+ * and B were created locally and not acknowledged yet, with the one possessing the lower 'clientSeq' being created earlier.
112
+ *
113
+ * 4. A 'seq' value of zero indicates that the subdirectory was created in detached state, and it is considered acknowledged for the
114
+ * purpose of ordering.
115
+ */
116
+ const seqDataComparator = (a, b) => {
117
+ if (isAcknowledgedOrDetached(a)) {
118
+ if (isAcknowledgedOrDetached(b)) {
119
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
120
+ return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq - b.clientSeq;
121
+ }
122
+ else {
123
+ return -1;
124
+ }
125
+ }
126
+ else {
127
+ if (!isAcknowledgedOrDetached(b)) {
128
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129
+ return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq - b.clientSeq;
130
+ }
131
+ else {
132
+ return 1;
133
+ }
134
+ }
135
+ };
136
+ function isAcknowledgedOrDetached(seqData) {
137
+ return seqData.seq >= 0;
138
+ }
139
+ /**
140
+ * A utility class for tracking associations between keys and their creation indices.
141
+ * This is relevant to support map iteration in insertion order, see
142
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
143
+ *
144
+ * TODO: It can be combined with the creation tracker utilized in SharedMap
145
+ */
146
+ class DirectoryCreationTracker {
147
+ constructor() {
148
+ this.indexToKey = new merge_tree_1.RedBlackTree(seqDataComparator);
149
+ this.keyToIndex = new Map();
150
+ }
151
+ set(key, seqData) {
152
+ this.indexToKey.put(seqData, key);
153
+ this.keyToIndex.set(key, seqData);
154
+ }
155
+ has(keyOrSeqData) {
156
+ return typeof keyOrSeqData === "string"
157
+ ? this.keyToIndex.has(keyOrSeqData)
158
+ : this.indexToKey.get(keyOrSeqData) !== undefined;
159
+ }
160
+ delete(keyOrSeqData) {
161
+ if (this.has(keyOrSeqData)) {
162
+ if (typeof keyOrSeqData === "string") {
163
+ const seqData = this.keyToIndex.get(keyOrSeqData);
164
+ this.keyToIndex.delete(keyOrSeqData);
165
+ this.indexToKey.remove(seqData);
166
+ }
167
+ else {
168
+ const key = this.indexToKey.get(keyOrSeqData)?.data;
169
+ this.indexToKey.remove(keyOrSeqData);
170
+ this.keyToIndex.delete(key);
171
+ }
172
+ }
173
+ }
174
+ /**
175
+ * Retrieves all subdirectories with creation order that satisfy an optional constraint function.
176
+ * @param constraint - An optional constraint function that filters keys.
177
+ * @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
178
+ */
179
+ keys(constraint) {
180
+ const keys = [];
181
+ this.indexToKey.mapRange((node) => {
182
+ if (!constraint || constraint(node.data)) {
183
+ keys.push(node.data);
184
+ }
185
+ return true;
186
+ }, keys);
187
+ return keys;
188
+ }
189
+ }
95
190
  /**
96
191
  * {@inheritDoc ISharedDirectory}
97
192
  *
@@ -147,7 +242,7 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
147
242
  /**
148
243
  * Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
149
244
  */
150
- this.root = new SubDirectory(0, new Set(), this, this.runtime, this.serializer, posix.sep);
245
+ this.root = new SubDirectory({ seq: 0, clientSeq: 0 }, new Set(), this, this.runtime, this.serializer, posix.sep);
151
246
  /**
152
247
  * Mapping of op types to message handlers.
153
248
  */
@@ -376,18 +471,42 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
376
471
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
377
472
  const [currentSubDir, currentSubDirObject] = stack.pop();
378
473
  if (currentSubDirObject.subdirectories) {
474
+ // Utilize a map to store the seq -> clientSeq for the newly created subdirectory
475
+ const tempSeqNums = new Map();
379
476
  for (const [subdirName, subdirObject] of Object.entries(currentSubDirObject.subdirectories)) {
380
477
  let newSubDir = currentSubDir.getSubDirectory(subdirName);
478
+ let seqData;
381
479
  if (!newSubDir) {
382
480
  const createInfo = subdirObject.ci;
383
- newSubDir = new SubDirectory(
384
- // If csn is -1, then initialize it with 0, otherwise we will never process ops for this
385
- // sub directory. This could be done at serialization time too, but we need to maintain
386
- // back compat too and also we will actually know the state when it was serialized.
387
- createInfo !== undefined && createInfo.csn > -1 ? createInfo.csn : 0, createInfo !== undefined
481
+ // We do not store the client sequence number in the storage because the order has already been
482
+ // guaranteed during the serialization process. As a result, it is only essential to utilize the
483
+ // "fake" client sequence number to signify the loading order, and there is no need to retain
484
+ // the actual client sequence number at this point.
485
+ if (createInfo !== undefined && createInfo.csn > -1) {
486
+ // If csn is -1, then initialize it with 0, otherwise we will never process ops for this
487
+ // sub directory. This could be done at serialization time too, but we need to maintain
488
+ // back compat too and also we will actually know the state when it was serialized.
489
+ if (!tempSeqNums.has(createInfo.csn)) {
490
+ tempSeqNums.set(createInfo.csn, 0);
491
+ }
492
+ let fakeClientSeq = tempSeqNums.get(createInfo.csn);
493
+ seqData = { seq: createInfo.csn, clientSeq: fakeClientSeq };
494
+ tempSeqNums.set(createInfo.csn, ++fakeClientSeq);
495
+ }
496
+ else {
497
+ seqData = {
498
+ seq: 0,
499
+ clientSeq: ++currentSubDir.localCreationSeq,
500
+ };
501
+ }
502
+ newSubDir = new SubDirectory(seqData, createInfo !== undefined
388
503
  ? new Set(createInfo.ccIds)
389
504
  : new Set(), this, this.runtime, this.serializer, posix.join(currentSubDir.absolutePath, subdirName));
390
505
  currentSubDir.populateSubDirectory(subdirName, newSubDir);
506
+ // Record the newly inserted subdirectory to the creation tracker
507
+ currentSubDir.ackedCreationSeqTracker.set(subdirName, {
508
+ ...seqData,
509
+ });
391
510
  }
392
511
  stack.push([newSubDir, subdirObject]);
393
512
  }
@@ -701,9 +820,9 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
701
820
  * @param serializer - The serializer to serialize / parse handles
702
821
  * @param absolutePath - The absolute path of this IDirectory
703
822
  */
704
- constructor(sequenceNumber, clientIds, directory, runtime, serializer, absolutePath) {
823
+ constructor(seqData, clientIds, directory, runtime, serializer, absolutePath) {
705
824
  super();
706
- this.sequenceNumber = sequenceNumber;
825
+ this.seqData = seqData;
707
826
  this.clientIds = clientIds;
708
827
  this.directory = directory;
709
828
  this.runtime = runtime;
@@ -752,6 +871,13 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
752
871
  * The pending ids of any clears that have been performed locally but not yet ack'd from the server
753
872
  */
754
873
  this.pendingClearMessageIds = [];
874
+ /**
875
+ * Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
876
+ * of the creation order.
877
+ */
878
+ this.localCreationSeq = 0;
879
+ this.localCreationSeqTracker = new DirectoryCreationTracker();
880
+ this.ackedCreationSeqTracker = new DirectoryCreationTracker();
755
881
  }
756
882
  dispose(error) {
757
883
  this._deleted = true;
@@ -853,14 +979,18 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
853
979
  return subDir;
854
980
  }
855
981
  /**
856
- * @returns A sequenceNumber which should be used for local changes.
982
+ * @returns The Sequence Data which should be used for local changes.
857
983
  * @remarks While detached, 0 is used rather than -1 to represent a change which should be universally known (as opposed to known
858
984
  * only by the local client). This ensures that if the directory is later attached, none of its data needs to be updated (the values
859
985
  * last set while detached will now be known to any new client, until they are changed).
986
+ *
987
+ * The client sequence number is incremented by 1 for maintaining the internal order of locally created subdirectories
860
988
  * TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
861
989
  */
862
990
  getLocalSeq() {
863
- return this.directory.isAttached() ? -1 : 0;
991
+ return this.directory.isAttached()
992
+ ? { seq: -1, clientSeq: ++this.localCreationSeq }
993
+ : { seq: 0, clientSeq: ++this.localCreationSeq };
864
994
  }
865
995
  /**
866
996
  * {@inheritDoc IDirectory.getSubDirectory}
@@ -903,7 +1033,26 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
903
1033
  */
904
1034
  subdirectories() {
905
1035
  this.throwIfDisposed();
906
- return this._subdirectories.entries();
1036
+ const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
1037
+ const localSubdirsInOrder = this.localCreationSeqTracker.keys((key) => !this.ackedCreationSeqTracker.has(key));
1038
+ const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
1039
+ (0, core_utils_1.assert)(subdirNames.length === this._subdirectories.size, "The count of keys for iteration should be consistent with the size of actual data");
1040
+ const entriesIterator = {
1041
+ index: 0,
1042
+ dirs: this._subdirectories,
1043
+ next() {
1044
+ if (this.index < subdirNames.length) {
1045
+ const subdirName = subdirNames[this.index++];
1046
+ const subdir = this.dirs.get(subdirName);
1047
+ return { value: [subdirName, subdir], done: false };
1048
+ }
1049
+ return { value: undefined, done: true };
1050
+ },
1051
+ [Symbol.iterator]() {
1052
+ return this;
1053
+ },
1054
+ };
1055
+ return entriesIterator;
907
1056
  }
908
1057
  /**
909
1058
  * {@inheritDoc IDirectory.getWorkingDirectory}
@@ -1163,7 +1312,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1163
1312
  return;
1164
1313
  }
1165
1314
  assertNonNullClientId(msg.clientId);
1166
- this.createSubDirectoryCore(op.subdirName, local, msg.sequenceNumber, msg.clientId);
1315
+ this.createSubDirectoryCore(op.subdirName, local, { seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber }, msg.clientId);
1167
1316
  }
1168
1317
  /**
1169
1318
  * Apply createSubDirectory operation locally and generate metadata
@@ -1385,7 +1534,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1385
1534
  getSerializableCreateInfo() {
1386
1535
  this.throwIfDisposed();
1387
1536
  const createInfo = {
1388
- csn: this.sequenceNumber,
1537
+ csn: this.seqData.seq,
1389
1538
  ccIds: Array.from(this.clientIds),
1390
1539
  };
1391
1540
  return createInfo;
@@ -1475,6 +1624,17 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1475
1624
  this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
1476
1625
  // don't need to register events because deleting never unregistered
1477
1626
  this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
1627
+ // Restore the record in creation tracker
1628
+ if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
1629
+ this.ackedCreationSeqTracker.set(op.subdirName, {
1630
+ ...localOpMetadata.subDirectory.seqData,
1631
+ });
1632
+ }
1633
+ else {
1634
+ this.localCreationSeqTracker.set(op.subdirName, {
1635
+ ...localOpMetadata.subDirectory.seqData,
1636
+ });
1637
+ }
1478
1638
  this.emit("subDirectoryCreated", op.subdirName, true, this);
1479
1639
  }
1480
1640
  this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subDirName);
@@ -1555,7 +1715,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1555
1715
  // and the op was created after the directory was created then apply this op.
1556
1716
  return ((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
1557
1717
  this.clientIds.has("detached") ||
1558
- (this.sequenceNumber !== -1 && this.sequenceNumber <= msg.referenceSequenceNumber));
1718
+ (this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber));
1559
1719
  }
1560
1720
  /**
1561
1721
  * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
@@ -1592,10 +1752,11 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1592
1752
  // If this is delete op and we have keys in this subDirectory, then we need to delete these
1593
1753
  // keys except the pending ones as they will be sequenced after this delete.
1594
1754
  directory.clearExceptPendingKeys(local);
1595
- // In case of delete op, we need to reset the creation seq number and client ids of
1755
+ // In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
1596
1756
  // creators as the previous directory is getting deleted and we will initialize again when
1597
1757
  // we will receive op for the create again.
1598
- directory.sequenceNumber = -1;
1758
+ directory.seqData.seq = -1;
1759
+ directory.seqData.clientSeq = -1;
1599
1760
  directory.clientIds.clear();
1600
1761
  // Do the same thing for the subtree of the directory. If create is not pending for a child, then just
1601
1762
  // delete it.
@@ -1609,21 +1770,35 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1609
1770
  }
1610
1771
  };
1611
1772
  const subDirectory = this._subdirectories.get(op.subdirName);
1773
+ // Clear the creation tracker record
1774
+ this.ackedCreationSeqTracker.delete(op.subdirName);
1612
1775
  resetSubDirectoryTree(subDirectory);
1613
1776
  }
1614
1777
  if (op.type === "createSubDirectory") {
1615
1778
  const dir = this._subdirectories.get(op.subdirName);
1616
1779
  // Child sub directory create seq number can't be lower than the parent subdirectory.
1617
1780
  // The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
1618
- if (this.sequenceNumber !== -1 && this.sequenceNumber <= msg.sequenceNumber) {
1619
- if (dir?.sequenceNumber === -1) {
1620
- // Only set the seq on the first message, could be more
1621
- dir.sequenceNumber = msg.sequenceNumber;
1781
+ if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
1782
+ if (dir?.seqData.seq === -1) {
1783
+ // Only set the sequence data based on the first message
1784
+ dir.seqData.seq = msg.sequenceNumber;
1785
+ dir.seqData.clientSeq = msg.clientSequenceNumber;
1786
+ // set the creation seq in tracker
1787
+ if (!this.ackedCreationSeqTracker.has(op.subdirName) &&
1788
+ !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
1789
+ this.ackedCreationSeqTracker.set(op.subdirName, {
1790
+ seq: msg.sequenceNumber,
1791
+ clientSeq: msg.clientSequenceNumber,
1792
+ });
1793
+ if (local) {
1794
+ this.localCreationSeqTracker.delete(op.subdirName);
1795
+ }
1796
+ }
1622
1797
  }
1623
1798
  // The client created the dir at or after the dirs seq, so list its client id as a creator.
1624
1799
  if (dir !== undefined &&
1625
1800
  !dir.clientIds.has(msg.clientId) &&
1626
- dir.sequenceNumber <= msg.sequenceNumber) {
1801
+ dir.seqData.seq <= msg.sequenceNumber) {
1627
1802
  dir.clientIds.add(msg.clientId);
1628
1803
  }
1629
1804
  }
@@ -1698,15 +1873,25 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1698
1873
  * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1699
1874
  * @param subdirName - The name of the subdirectory being created
1700
1875
  * @param local - Whether the message originated from the local client
1701
- * @param seq - Sequence number at which this directory is created
1876
+ * @param seqData - Sequence number and client sequence number at which this directory is created
1702
1877
  * @param clientId - Id of client which created this directory.
1703
1878
  * @returns True if is newly created, false if it already existed.
1704
1879
  */
1705
- createSubDirectoryCore(subdirName, local, seq, clientId) {
1880
+ createSubDirectoryCore(subdirName, local, seqData, clientId) {
1706
1881
  const subdir = this._subdirectories.get(subdirName);
1707
1882
  if (subdir === undefined) {
1708
1883
  const absolutePath = posix.join(this.absolutePath, subdirName);
1709
- const subDir = new SubDirectory(seq, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath);
1884
+ const subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath);
1885
+ /**
1886
+ * Store the sequnce numbers of newly created subdirectory to the proper creation tracker, based
1887
+ * on whether the creation behavior has been ack'd or not
1888
+ */
1889
+ if (!isAcknowledgedOrDetached(seqData)) {
1890
+ this.localCreationSeqTracker.set(subdirName, { ...seqData });
1891
+ }
1892
+ else {
1893
+ this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
1894
+ }
1710
1895
  this.registerEventsOnSubDirectory(subDir, subdirName);
1711
1896
  this._subdirectories.set(subdirName, subDir);
1712
1897
  this.emit("subDirectoryCreated", subdirName, local, this);
@@ -1736,6 +1921,16 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1736
1921
  // Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
1737
1922
  if (previousValue !== undefined) {
1738
1923
  this._subdirectories.delete(subdirName);
1924
+ /**
1925
+ * Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
1926
+ * ack'd already or still not committed yet (could be both).
1927
+ */
1928
+ if (this.ackedCreationSeqTracker.has(subdirName)) {
1929
+ this.ackedCreationSeqTracker.delete(subdirName);
1930
+ }
1931
+ if (this.localCreationSeqTracker.has(subdirName)) {
1932
+ this.localCreationSeqTracker.delete(subdirName);
1933
+ }
1739
1934
  this.disposeSubDirectoryTree(previousValue);
1740
1935
  this.emit("subDirectoryDeleted", subdirName, local, this);
1741
1936
  }