@fluidframework/map 2.0.0-dev.4.4.0.162574 → 2.0.0-dev.5.3.2.178189

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/lib/directory.js CHANGED
@@ -349,7 +349,11 @@ export class SharedDirectory extends SharedObject {
349
349
  let newSubDir = currentSubDir.getSubDirectory(subdirName);
350
350
  if (!newSubDir) {
351
351
  const createInfo = subdirObject.ci;
352
- newSubDir = new SubDirectory(createInfo !== undefined ? createInfo.csn : 0, createInfo !== undefined
352
+ newSubDir = new SubDirectory(
353
+ // If csn is -1, then initialize it with 0, otherwise we will never process ops for this
354
+ // sub directory. This could be done at serialization time too, but we need to maintain
355
+ // back compat too and also we will actually know the state when it was serialized.
356
+ createInfo !== undefined && createInfo.csn > -1 ? createInfo.csn : 0, createInfo !== undefined
353
357
  ? new Set(createInfo.ccIds)
354
358
  : new Set(), this, this.runtime, this.serializer, posix.join(currentSubDir.absolutePath, subdirName));
355
359
  currentSubDir.populateSubDirectory(subdirName, newSubDir);
@@ -637,16 +641,12 @@ function isClearLocalOpMetadata(metadata) {
637
641
  }
638
642
  function isSubDirLocalOpMetadata(metadata) {
639
643
  return (metadata !== undefined &&
640
- typeof metadata.pendingMessageId === "number" &&
641
644
  (metadata.type === "createSubDir" || metadata.type === "deleteSubDir"));
642
645
  }
643
646
  function isDirectoryLocalOpMetadata(metadata) {
644
- return (metadata !== undefined &&
645
- typeof metadata.pendingMessageId === "number" &&
646
- (metadata.type === "edit" ||
647
- metadata.type === "deleteSubDir" ||
648
- (metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
649
- metadata.type === "createSubDir"));
647
+ return (isKeyEditLocalOpMetadata(metadata) ||
648
+ isClearLocalOpMetadata(metadata) ||
649
+ isSubDirLocalOpMetadata(metadata));
650
650
  }
651
651
  /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
652
652
  function assertNonNullClientId(clientId) {
@@ -691,18 +691,24 @@ class SubDirectory extends TypedEventEmitter {
691
691
  */
692
692
  this._subdirectories = new Map();
693
693
  /**
694
- * Keys that have been modified locally but not yet ack'd from the server.
694
+ * Keys that have been modified locally but not yet ack'd from the server. This is for operations on keys like
695
+ * set/delete operations on keys. The value of this map is list of pendingMessageIds at which that key
696
+ * was modified. We don't store the type of ops, and behaviour of key ops are different from behaviour of sub
697
+ * directory ops, so we have separate map from subDirectories tracker.
695
698
  */
696
699
  this.pendingKeys = new Map();
697
700
  /**
698
- * Subdirectories that have been created/deleted locally but not yet ack'd from the server.
701
+ * Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
702
+ * of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
703
+ * deleted sub directory.
699
704
  */
700
- this.pendingSubDirectories = new Map();
705
+ this.pendingDeleteSubDirectoriesTracker = new Map();
701
706
  /**
702
- * Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the count
703
- * of delete op that are pending or yet to be acked from server.
707
+ * Subdirectories that have been created locally but not yet ack'd from the server. This maintains the record
708
+ * of create op that are pending or yet to be acked from server. This is maintained just to track the locally
709
+ * created sub directory.
704
710
  */
705
- this.pendingDeleteSubDirectoriesCount = new Map();
711
+ this.pendingCreateSubDirectoriesTracker = new Map();
706
712
  /**
707
713
  * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
708
714
  */
@@ -869,8 +875,7 @@ class SubDirectory extends TypedEventEmitter {
869
875
  * @returns - true if there is pending delete.
870
876
  */
871
877
  isSubDirectoryDeletePending(subDirName) {
872
- const pendingDeleteSubDirectory = this.pendingDeleteSubDirectoriesCount.get(subDirName);
873
- if (pendingDeleteSubDirectory !== undefined && pendingDeleteSubDirectory > 0) {
878
+ if (this.pendingDeleteSubDirectoriesTracker.has(subDirName)) {
874
879
  return true;
875
880
  }
876
881
  return false;
@@ -1110,7 +1115,8 @@ class SubDirectory extends TypedEventEmitter {
1110
1115
  */
1111
1116
  processCreateSubDirectoryMessage(msg, op, local, localOpMetadata) {
1112
1117
  this.throwIfDisposed();
1113
- if (!this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)) {
1118
+ if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1119
+ this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
1114
1120
  return;
1115
1121
  }
1116
1122
  assertNonNullClientId(msg.clientId);
@@ -1126,10 +1132,9 @@ class SubDirectory extends TypedEventEmitter {
1126
1132
  this.throwIfDisposed();
1127
1133
  // Create the sub directory locally first.
1128
1134
  this.createSubDirectoryCore(op.subdirName, true, -1, (_c = this.runtime.clientId) !== null && _c !== void 0 ? _c : "detached");
1129
- const newMessageId = this.getSubDirMessageId(op);
1135
+ this.updatePendingSubDirMessageCount(op);
1130
1136
  const localOpMetadata = {
1131
1137
  type: "createSubDir",
1132
- pendingMessageId: newMessageId,
1133
1138
  };
1134
1139
  return localOpMetadata;
1135
1140
  }
@@ -1158,10 +1163,9 @@ class SubDirectory extends TypedEventEmitter {
1158
1163
  applyStashedDeleteSubDirMessage(op) {
1159
1164
  this.throwIfDisposed();
1160
1165
  const subDir = this.deleteSubDirectoryCore(op.subdirName, true);
1161
- const newMessageId = this.getSubDirMessageId(op);
1166
+ this.updatePendingSubDirMessageCount(op);
1162
1167
  const metadata = {
1163
1168
  type: "deleteSubDir",
1164
- pendingMessageId: newMessageId,
1165
1169
  subDirectory: subDir,
1166
1170
  };
1167
1171
  return metadata;
@@ -1190,8 +1194,11 @@ class SubDirectory extends TypedEventEmitter {
1190
1194
  assert(isClearLocalOpMetadata(localOpMetadata), 0x32b /* Invalid localOpMetadata for clear */);
1191
1195
  // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1192
1196
  const pendingClearMessageId = this.pendingClearMessageIds.shift();
1193
- assert(pendingClearMessageId === localOpMetadata.pendingMessageId, 0x32c /* pendingMessageId does not match */);
1194
- this.submitClearMessage(op, localOpMetadata.previousStorage);
1197
+ // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1198
+ // is already deleted, in which case we don't need to submit the op.
1199
+ if (pendingClearMessageId === localOpMetadata.pendingMessageId) {
1200
+ this.submitClearMessage(op, localOpMetadata.previousStorage);
1201
+ }
1195
1202
  }
1196
1203
  /**
1197
1204
  * Get a new pending message id for the op and cache it to track the pending op
@@ -1229,33 +1236,41 @@ class SubDirectory extends TypedEventEmitter {
1229
1236
  assert(isKeyEditLocalOpMetadata(localOpMetadata), 0x32d /* Invalid localOpMetadata in submit */);
1230
1237
  // clear the old pending message id
1231
1238
  const pendingMessageIds = this.pendingKeys.get(op.key);
1232
- assert(pendingMessageIds !== undefined &&
1233
- pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x32e /* Unexpected pending message received */);
1234
- pendingMessageIds.shift();
1235
- if (pendingMessageIds.length === 0) {
1236
- this.pendingKeys.delete(op.key);
1239
+ // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1240
+ // is already deleted, in which case we don't need to submit the op.
1241
+ if (pendingMessageIds !== undefined &&
1242
+ pendingMessageIds[0] === localOpMetadata.pendingMessageId) {
1243
+ pendingMessageIds.shift();
1244
+ if (pendingMessageIds.length === 0) {
1245
+ this.pendingKeys.delete(op.key);
1246
+ }
1247
+ this.submitKeyMessage(op, localOpMetadata.previousValue);
1237
1248
  }
1238
- this.submitKeyMessage(op, localOpMetadata.previousValue);
1239
1249
  }
1240
- /**
1241
- * Get a new pending message id for the op and cache it to track the pending op
1242
- */
1243
- getSubDirMessageId(op) {
1250
+ incrementPendingSubDirCount(map, subDirName) {
1244
1251
  var _c;
1245
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1246
- const newMessageId = ++this.pendingMessageId;
1247
- const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1248
- if (pendingMessageIds !== undefined) {
1249
- pendingMessageIds.push(newMessageId);
1250
- }
1251
- else {
1252
- this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
1252
+ const count = (_c = map.get(subDirName)) !== null && _c !== void 0 ? _c : 0;
1253
+ map.set(subDirName, count + 1);
1254
+ }
1255
+ decrementPendingSubDirCount(map, subDirName) {
1256
+ var _c;
1257
+ const count = (_c = map.get(subDirName)) !== null && _c !== void 0 ? _c : 0;
1258
+ map.set(subDirName, count - 1);
1259
+ if (count <= 1) {
1260
+ map.delete(subDirName);
1253
1261
  }
1262
+ }
1263
+ /**
1264
+ * Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
1265
+ * or while resubmitting the op.
1266
+ */
1267
+ updatePendingSubDirMessageCount(op) {
1254
1268
  if (op.type === "deleteSubDirectory") {
1255
- const count = (_c = this.pendingDeleteSubDirectoriesCount.get(op.subdirName)) !== null && _c !== void 0 ? _c : 0;
1256
- this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count + 1);
1269
+ this.incrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1270
+ }
1271
+ else if (op.type === "createSubDirectory") {
1272
+ this.incrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1257
1273
  }
1258
- return newMessageId;
1259
1274
  }
1260
1275
  /**
1261
1276
  * Submit a create subdirectory operation.
@@ -1263,10 +1278,9 @@ class SubDirectory extends TypedEventEmitter {
1263
1278
  */
1264
1279
  submitCreateSubDirectoryMessage(op) {
1265
1280
  this.throwIfDisposed();
1266
- const newMessageId = this.getSubDirMessageId(op);
1281
+ this.updatePendingSubDirMessageCount(op);
1267
1282
  const localOpMetadata = {
1268
1283
  type: "createSubDir",
1269
- pendingMessageId: newMessageId,
1270
1284
  };
1271
1285
  this.directory.submitDirectoryMessage(op, localOpMetadata);
1272
1286
  }
@@ -1277,10 +1291,9 @@ class SubDirectory extends TypedEventEmitter {
1277
1291
  */
1278
1292
  submitDeleteSubDirectoryMessage(op, subDir) {
1279
1293
  this.throwIfDisposed();
1280
- const newMessageId = this.getSubDirMessageId(op);
1294
+ this.updatePendingSubDirMessageCount(op);
1281
1295
  const localOpMetadata = {
1282
1296
  type: "deleteSubDir",
1283
- pendingMessageId: newMessageId,
1284
1297
  subDirectory: subDir,
1285
1298
  };
1286
1299
  this.directory.submitDirectoryMessage(op, localOpMetadata);
@@ -1293,18 +1306,22 @@ class SubDirectory extends TypedEventEmitter {
1293
1306
  */
1294
1307
  resubmitSubDirectoryMessage(op, localOpMetadata) {
1295
1308
  assert(isSubDirLocalOpMetadata(localOpMetadata), 0x32f /* Invalid localOpMetadata for sub directory op */);
1296
- // clear the old pending message id
1297
- const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1298
- assert(pendingMessageIds !== undefined &&
1299
- pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x330 /* Unexpected pending message received */);
1300
- pendingMessageIds.shift();
1301
- if (pendingMessageIds.length === 0) {
1302
- this.pendingSubDirectories.delete(op.subdirName);
1309
+ // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1310
+ // is already deleted, in which case we don't need to submit the op.
1311
+ if (localOpMetadata.type === "createSubDir" &&
1312
+ !this.pendingCreateSubDirectoriesTracker.has(op.subdirName)) {
1313
+ return;
1314
+ }
1315
+ else if (localOpMetadata.type === "deleteSubDir" &&
1316
+ !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
1317
+ return;
1303
1318
  }
1304
1319
  if (localOpMetadata.type === "createSubDir") {
1320
+ this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1305
1321
  this.submitCreateSubDirectoryMessage(op);
1306
1322
  }
1307
1323
  else {
1324
+ this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1308
1325
  this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1309
1326
  }
1310
1327
  }
@@ -1408,7 +1425,7 @@ class SubDirectory extends TypedEventEmitter {
1408
1425
  }
1409
1426
  else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
1410
1427
  this.deleteSubDirectoryCore(op.subdirName, true);
1411
- this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1428
+ this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1412
1429
  }
1413
1430
  else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
1414
1431
  if (localOpMetadata.subDirectory !== undefined) {
@@ -1417,13 +1434,7 @@ class SubDirectory extends TypedEventEmitter {
1417
1434
  this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
1418
1435
  this.emit("subDirectoryCreated", op.subdirName, true, this);
1419
1436
  }
1420
- this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1421
- const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName);
1422
- assert(count !== undefined && count > 0, 0x5ab /* should have record for delete op */);
1423
- this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count - 1);
1424
- if (count === 1) {
1425
- this.pendingDeleteSubDirectoriesCount.delete(op.subdirName);
1426
- }
1437
+ this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subDirName);
1427
1438
  }
1428
1439
  else {
1429
1440
  throw new Error("Unsupported op for rollback");
@@ -1516,51 +1527,63 @@ class SubDirectory extends TypedEventEmitter {
1516
1527
  * @returns True if the operation should be processed, false otherwise
1517
1528
  */
1518
1529
  needProcessSubDirectoryOperation(msg, op, local, localOpMetadata) {
1519
- const pendingSubDirectoryMessageId = this.pendingSubDirectories.get(op.subdirName);
1520
1530
  assertNonNullClientId(msg.clientId);
1521
- if (pendingSubDirectoryMessageId !== undefined) {
1531
+ const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
1532
+ const pendingCreateCount = this.pendingCreateSubDirectoriesTracker.get(op.subdirName);
1533
+ if ((pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
1534
+ (pendingCreateCount !== undefined && pendingCreateCount > 0)) {
1522
1535
  if (local) {
1523
1536
  assert(isSubDirLocalOpMetadata(localOpMetadata), 0x012 /* pendingMessageId is missing from the local client's operation */);
1524
- const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1525
- assert(pendingMessageIds !== undefined &&
1526
- pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x332 /* Unexpected pending message received */);
1527
- pendingMessageIds.shift();
1528
- if (pendingMessageIds.length === 0) {
1529
- this.pendingSubDirectories.delete(op.subdirName);
1537
+ if (localOpMetadata.type === "deleteSubDir") {
1538
+ assert(pendingDeleteCount !== undefined && pendingDeleteCount > 0, 0x6c2 /* pendingDeleteCount should exist */);
1539
+ this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1530
1540
  }
1531
- if (op.type === "deleteSubDirectory") {
1532
- const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName);
1533
- assert(count !== undefined && count > 0, 0x5ac /* should have record for delete op */);
1534
- this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count - 1);
1535
- if (count === 1) {
1536
- this.pendingDeleteSubDirectoriesCount.delete(op.subdirName);
1537
- }
1541
+ else if (localOpMetadata.type === "createSubDir") {
1542
+ assert(pendingCreateCount !== undefined && pendingCreateCount > 0, 0x6c3 /* pendingCreateCount should exist */);
1543
+ this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1538
1544
  }
1539
1545
  }
1540
- else if (op.type === "deleteSubDirectory") {
1541
- // If this is remote delete op and we have keys in this subDirectory, then we need to delete these
1542
- // keys except the pending ones as they will be sequenced after this delete.
1543
- const subDirectory = this._subdirectories.get(op.subdirName);
1544
- if (subDirectory) {
1545
- subDirectory.clearExceptPendingKeys(local);
1546
- // In case of remote delete op, we need to reset the creation seq number and client ids of
1546
+ if (op.type === "deleteSubDirectory") {
1547
+ const resetSubDirectoryTree = (directory) => {
1548
+ if (!directory) {
1549
+ return;
1550
+ }
1551
+ // If this is delete op and we have keys in this subDirectory, then we need to delete these
1552
+ // keys except the pending ones as they will be sequenced after this delete.
1553
+ directory.clearExceptPendingKeys(local);
1554
+ // In case of delete op, we need to reset the creation seq number and client ids of
1547
1555
  // creators as the previous directory is getting deleted and we will initialize again when
1548
1556
  // we will receive op for the create again.
1549
- subDirectory.sequenceNumber = -1;
1550
- subDirectory.clientIds.clear();
1551
- }
1557
+ directory.sequenceNumber = -1;
1558
+ directory.clientIds.clear();
1559
+ // Do the same thing for the subtree of the directory. If create is not pending for a child, then just
1560
+ // delete it.
1561
+ const subDirectories = directory.subdirectories();
1562
+ for (const [subDirName, subDir] of subDirectories) {
1563
+ if (directory.pendingCreateSubDirectoriesTracker.has(subDirName)) {
1564
+ resetSubDirectoryTree(subDir);
1565
+ continue;
1566
+ }
1567
+ directory.deleteSubDirectoryCore(subDirName, false);
1568
+ }
1569
+ };
1570
+ const subDirectory = this._subdirectories.get(op.subdirName);
1571
+ resetSubDirectoryTree(subDirectory);
1552
1572
  }
1553
1573
  if (op.type === "createSubDirectory") {
1554
1574
  const dir = this._subdirectories.get(op.subdirName);
1555
- if ((dir === null || dir === void 0 ? void 0 : dir.sequenceNumber) === -1) {
1556
- // Only set the seq on the first message, could be more
1557
- dir.sequenceNumber = msg.sequenceNumber;
1558
- }
1559
- // The client created the dir at or after the dirs seq, so list its client id as a creator.
1560
- if (dir !== undefined &&
1561
- !dir.clientIds.has(msg.clientId) &&
1562
- dir.sequenceNumber <= msg.sequenceNumber) {
1563
- dir.clientIds.add(msg.clientId);
1575
+ // Child sub directory create seq number can't be lower than the parent subdirectory.
1576
+ if (this.sequenceNumber !== -1 && this.sequenceNumber < msg.sequenceNumber) {
1577
+ if ((dir === null || dir === void 0 ? void 0 : dir.sequenceNumber) === -1) {
1578
+ // Only set the seq on the first message, could be more
1579
+ dir.sequenceNumber = msg.sequenceNumber;
1580
+ }
1581
+ // The client created the dir at or after the dirs seq, so list its client id as a creator.
1582
+ if (dir !== undefined &&
1583
+ !dir.clientIds.has(msg.clientId) &&
1584
+ dir.sequenceNumber <= msg.sequenceNumber) {
1585
+ dir.clientIds.add(msg.clientId);
1586
+ }
1564
1587
  }
1565
1588
  }
1566
1589
  return false;