@fluidframework/id-compressor 2.0.0-rc.3.0.3 → 2.0.0-rc.4.0.1

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 (40) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/api-report/id-compressor.api.md +2 -1
  3. package/dist/idCompressor.d.ts +3 -1
  4. package/dist/idCompressor.d.ts.map +1 -1
  5. package/dist/idCompressor.js +42 -5
  6. package/dist/idCompressor.js.map +1 -1
  7. package/dist/legacy.d.ts +2 -2
  8. package/dist/packageVersion.d.ts +1 -1
  9. package/dist/packageVersion.js +1 -1
  10. package/dist/packageVersion.js.map +1 -1
  11. package/dist/public.d.ts +1 -1
  12. package/dist/test/idCompressor.spec.js.map +1 -1
  13. package/dist/test/idCompressorTestUtilities.d.ts.map +1 -1
  14. package/dist/test/idCompressorTestUtilities.js +81 -18
  15. package/dist/test/idCompressorTestUtilities.js.map +1 -1
  16. package/dist/types/idCompressor.d.ts +11 -1
  17. package/dist/types/idCompressor.d.ts.map +1 -1
  18. package/dist/types/idCompressor.js.map +1 -1
  19. package/lib/idCompressor.d.ts +3 -1
  20. package/lib/idCompressor.d.ts.map +1 -1
  21. package/lib/idCompressor.js +43 -6
  22. package/lib/idCompressor.js.map +1 -1
  23. package/lib/legacy.d.ts +2 -2
  24. package/lib/packageVersion.d.ts +1 -1
  25. package/lib/packageVersion.js +1 -1
  26. package/lib/packageVersion.js.map +1 -1
  27. package/lib/public.d.ts +1 -1
  28. package/lib/test/idCompressor.spec.js.map +1 -1
  29. package/lib/test/idCompressorTestUtilities.d.ts.map +1 -1
  30. package/lib/test/idCompressorTestUtilities.js +81 -18
  31. package/lib/test/idCompressorTestUtilities.js.map +1 -1
  32. package/lib/tsdoc-metadata.json +1 -1
  33. package/lib/types/idCompressor.d.ts +11 -1
  34. package/lib/types/idCompressor.d.ts.map +1 -1
  35. package/lib/types/idCompressor.js.map +1 -1
  36. package/package.json +11 -11
  37. package/src/idCompressor.ts +57 -8
  38. package/src/packageVersion.ts +1 -1
  39. package/src/test/idCompressorTestUtilities.ts +94 -19
  40. package/src/types/idCompressor.ts +12 -1
@@ -6,8 +6,11 @@
6
6
  import { bufferToString, stringToBuffer } from "@fluid-internal/client-utils";
7
7
  import { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
8
8
  import { assert } from "@fluidframework/core-utils/internal";
9
- import { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils";
10
- import { createChildLogger } from "@fluidframework/telemetry-utils/internal";
9
+ import {
10
+ ITelemetryLoggerExt,
11
+ LoggingError,
12
+ createChildLogger,
13
+ } from "@fluidframework/telemetry-utils/internal";
11
14
 
12
15
  import { FinalSpace } from "./finalSpace.js";
13
16
  import { FinalCompressedId, LocalCompressedId, NumericUuid, isFinalId } from "./identifiers.js";
@@ -58,6 +61,13 @@ import {
58
61
  */
59
62
  const currentWrittenVersion = 2.0;
60
63
 
64
+ function rangeFinalizationError(expectedStart: number, actualStart: number): LoggingError {
65
+ return new LoggingError("Ranges finalized out of order", {
66
+ expectedStart,
67
+ actualStart,
68
+ });
69
+ }
70
+
61
71
  /**
62
72
  * See {@link IIdCompressor} and {@link IIdCompressorCore}
63
73
  */
@@ -223,14 +233,49 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
223
233
  ),
224
234
  },
225
235
  };
236
+ return this.updateToRange(range);
237
+ }
238
+
239
+ public takeUnfinalizedCreationRange(): IdCreationRange {
240
+ const lastLocalCluster = this.localSession.getLastCluster();
241
+ let count: number;
242
+ let firstGenCount: number;
243
+ if (lastLocalCluster === undefined) {
244
+ firstGenCount = 1;
245
+ count = this.localGenCount;
246
+ } else {
247
+ firstGenCount = genCountFromLocalId(
248
+ (lastLocalCluster.baseLocalId - lastLocalCluster.count) as LocalCompressedId,
249
+ );
250
+ count = this.localGenCount - firstGenCount + 1;
251
+ }
252
+
253
+ if (count === 0) {
254
+ return {
255
+ sessionId: this.localSessionId,
256
+ };
257
+ }
258
+
259
+ const range: IdCreationRange = {
260
+ ids: {
261
+ count,
262
+ firstGenCount,
263
+ localIdRanges: this.normalizer.getRangesBetween(firstGenCount, this.localGenCount),
264
+ requestedClusterSize: this.nextRequestedClusterSize,
265
+ },
266
+ sessionId: this.localSessionId,
267
+ };
268
+ return this.updateToRange(range);
269
+ }
270
+
271
+ private updateToRange(range: IdCreationRange): IdCreationRange {
226
272
  this.nextRangeBaseGenCount = this.localGenCount + 1;
227
- IdCompressor.assertValidRange(range);
228
- return range;
273
+ return IdCompressor.assertValidRange(range);
229
274
  }
230
275
 
231
- private static assertValidRange(range: IdCreationRange): void {
276
+ private static assertValidRange(range: IdCreationRange): IdCreationRange {
232
277
  if (range.ids === undefined) {
233
- return;
278
+ return range;
234
279
  }
235
280
  const { count, requestedClusterSize } = range.ids;
236
281
  assert(count > 0, 0x755 /* Malformed ID Range. */);
@@ -239,6 +284,7 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
239
284
  requestedClusterSize <= IdCompressor.maxClusterSize,
240
285
  0x877 /* Clusters must not exceed max cluster size. */,
241
286
  );
287
+ return range;
242
288
  }
243
289
 
244
290
  public finalizeCreationRange(range: IdCreationRange): void {
@@ -261,7 +307,7 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
261
307
  if (lastCluster === undefined) {
262
308
  // This is the first cluster in the session space
263
309
  if (rangeBaseLocal !== -1) {
264
- throw new Error("Ranges finalized out of order.");
310
+ throw rangeFinalizationError(-1, rangeBaseLocal);
265
311
  }
266
312
  lastCluster = this.addEmptyCluster(session, requestedClusterSize + count);
267
313
  if (isLocal) {
@@ -274,7 +320,10 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
274
320
 
275
321
  const remainingCapacity = lastCluster.capacity - lastCluster.count;
276
322
  if (lastCluster.baseLocalId - lastCluster.count !== rangeBaseLocal) {
277
- throw new Error("Ranges finalized out of order.");
323
+ throw rangeFinalizationError(
324
+ lastCluster.baseLocalId - lastCluster.count,
325
+ rangeBaseLocal,
326
+ );
278
327
  }
279
328
 
280
329
  if (remainingCapacity >= count) {
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/id-compressor";
9
- export const pkgVersion = "2.0.0-rc.3.0.3";
9
+ export const pkgVersion = "2.0.0-rc.4.0.1";
@@ -33,6 +33,7 @@ import {
33
33
  } from "../index.js";
34
34
  import { assertIsSessionId, createSessionId, localIdFromGenCount } from "../utilities.js";
35
35
 
36
+ import { SessionSpaceNormalizer } from "../sessionSpaceNormalizer.js";
36
37
  import {
37
38
  FinalCompressedId,
38
39
  ReadonlyIdCompressor,
@@ -286,8 +287,7 @@ export class IdCompressorTestNetwork {
286
287
  * Changes the capacity request amount for a client. It will take effect immediately.
287
288
  */
288
289
  public changeCapacity(client: Client, newClusterCapacity: number): void {
289
- // eslint-disable-next-line @typescript-eslint/dot-notation
290
- this.compressors.get(client)["nextRequestedClusterSize"] = newClusterCapacity;
290
+ changeCapacity(this.compressors.get(client), newClusterCapacity);
291
291
  }
292
292
 
293
293
  private addNewId(
@@ -427,26 +427,89 @@ export class IdCompressorTestNetwork {
427
427
  (client) => [this.compressors.get(client), this.getSequencedIdLog(client)] as const,
428
428
  );
429
429
 
430
- // Ensure creation ranges for clients we track contain the correct local ID ranges
431
- this.serverOperations.forEach(([range, opSpaceIds, clientFrom]) => {
430
+ const getLocalIdsInRange = (
431
+ range: IdCreationRange,
432
+ opSpaceIds?: OpSpaceCompressedId[],
433
+ ): Set<SessionSpaceCompressedId> => {
432
434
  const localIdsInCreationRange = new Set<SessionSpaceCompressedId>();
433
- if (clientFrom !== OriginatingClient.Remote) {
434
- const ids = range.ids;
435
- if (ids !== undefined) {
436
- const { firstGenCount, localIdRanges } = ids;
437
- for (const [genCount, count] of localIdRanges) {
438
- for (let g = genCount; g < genCount + count; g++) {
439
- const local = localIdFromGenCount(g);
435
+ const ids = range.ids;
436
+ if (ids !== undefined) {
437
+ const { firstGenCount, localIdRanges } = ids;
438
+ for (const [genCount, count] of localIdRanges) {
439
+ for (let g = genCount; g < genCount + count; g++) {
440
+ const local = localIdFromGenCount(g);
441
+ if (opSpaceIds) {
440
442
  assert.strictEqual(opSpaceIds[g - firstGenCount], local);
441
- localIdsInCreationRange.add(local);
442
443
  }
444
+ localIdsInCreationRange.add(local);
443
445
  }
444
446
  }
447
+ }
448
+ return localIdsInCreationRange;
449
+ };
450
+
451
+ // Ensure creation ranges for clients we track contain the correct local ID ranges
452
+ this.serverOperations.forEach(([range, opSpaceIds, clientFrom]) => {
453
+ if (clientFrom !== OriginatingClient.Remote) {
454
+ const localIdsInCreationRange = getLocalIdsInRange(range, opSpaceIds);
455
+ let localCount = 0;
445
456
  opSpaceIds.forEach((id) => {
446
457
  if (isLocalId(id)) {
458
+ localCount++;
447
459
  assert(localIdsInCreationRange.has(id), "Local ID not in creation range");
448
460
  }
449
461
  });
462
+ assert.strictEqual(
463
+ localCount,
464
+ localIdsInCreationRange.size,
465
+ "Local ID count mismatch",
466
+ );
467
+ }
468
+ });
469
+
470
+ const undeliveredRanges = new Map<Client, IdCreationRange[]>();
471
+ this.clientProgress.forEach((progress, client) => {
472
+ const ranges = this.serverOperations
473
+ .slice(progress)
474
+ .filter((op) => op[2] === client)
475
+ .map(([range]) => range);
476
+ undeliveredRanges.set(client, ranges);
477
+ });
478
+ undeliveredRanges.forEach((ranges, client) => {
479
+ const compressor = this.compressors.get(client);
480
+ let firstGenCount: number | undefined;
481
+ let totalCount = 0;
482
+ const unionedLocalRanges = new SessionSpaceNormalizer();
483
+ ranges.forEach((range) => {
484
+ assert(range.sessionId === compressor.localSessionId);
485
+ if (range.ids !== undefined) {
486
+ // initialize firstGenCount if not set
487
+ if (firstGenCount === undefined) {
488
+ firstGenCount = range.ids.firstGenCount;
489
+ }
490
+ totalCount += range.ids.count;
491
+ range.ids.localIdRanges.forEach(([genCount, count]) => {
492
+ unionedLocalRanges.addLocalRange(genCount, count);
493
+ });
494
+ }
495
+ });
496
+
497
+ const retakenRange = compressor.takeUnfinalizedCreationRange();
498
+ if (retakenRange.ids !== undefined) {
499
+ const retakenLocalIds = new SessionSpaceNormalizer();
500
+ retakenRange.ids.localIdRanges.forEach(([genCount, count]) => {
501
+ retakenLocalIds.addLocalRange(genCount, count);
502
+ });
503
+ assert.strictEqual(
504
+ retakenLocalIds.equals(unionedLocalRanges),
505
+ true,
506
+ "Local ID ranges mismatch",
507
+ );
508
+ assert.strictEqual(retakenRange.ids.count, totalCount, "Count mismatch");
509
+ assert.strictEqual(retakenRange.ids.firstGenCount, firstGenCount, "Count mismatch");
510
+ } else {
511
+ assert.strictEqual(totalCount, 0);
512
+ assert.strictEqual(unionedLocalRanges.idRanges.size, 0);
450
513
  }
451
514
  });
452
515
 
@@ -588,6 +651,11 @@ export class IdCompressorTestNetwork {
588
651
  }
589
652
  }
590
653
 
654
+ function changeCapacity(compressor: IdCompressor, newClusterCapacity: number): void {
655
+ // eslint-disable-next-line @typescript-eslint/dot-notation
656
+ compressor["nextRequestedClusterSize"] = newClusterCapacity;
657
+ }
658
+
591
659
  /**
592
660
  * Roundtrips the supplied compressor through serialization and deserialization.
593
661
  */
@@ -608,13 +676,21 @@ export function roundtrip(
608
676
  compressor: ReadonlyIdCompressor,
609
677
  withSession: boolean,
610
678
  ): [SerializedIdCompressorWithOngoingSession | SerializedIdCompressorWithNoSession, IdCompressor] {
679
+ // preserve the capacity request as this property is normally private and resets
680
+ // to a default on construction (deserialization)
681
+ // eslint-disable-next-line @typescript-eslint/dot-notation
682
+ const capacity = compressor["nextRequestedClusterSize"];
611
683
  if (withSession) {
612
684
  const serialized = compressor.serialize(withSession);
613
- return [serialized, IdCompressor.deserialize(serialized)];
685
+ const roundtripped = IdCompressor.deserialize(serialized);
686
+ changeCapacity(roundtripped, capacity);
687
+ return [serialized, roundtripped];
688
+ } else {
689
+ const nonLocalSerialized = compressor.serialize(withSession);
690
+ const roundtripped = IdCompressor.deserialize(nonLocalSerialized, createSessionId());
691
+ changeCapacity(roundtripped, capacity);
692
+ return [nonLocalSerialized, roundtripped];
614
693
  }
615
-
616
- const nonLocalSerialized = compressor.serialize(withSession);
617
- return [nonLocalSerialized, IdCompressor.deserialize(nonLocalSerialized, createSessionId())];
618
694
  }
619
695
 
620
696
  /**
@@ -811,13 +887,13 @@ export function makeOpGenerator(
811
887
  return { type: "reconnect", client: random.pick(activeClients) };
812
888
  }
813
889
 
814
- const allocationWeight = 16;
890
+ const allocationWeight = 20;
815
891
  return interleave(
816
892
  createWeightedGenerator<Operation, FuzzTestState>([
817
893
  [changeCapacityGenerator, 1],
818
894
  [allocateIdsGenerator, Math.round(allocationWeight * (1 - outsideAllocationFraction))],
819
895
  [allocateOutsideIdsGenerator, Math.round(allocationWeight * outsideAllocationFraction)],
820
- [deliverAllOperationsGenerator, 2],
896
+ [deliverAllOperationsGenerator, 1],
821
897
  [deliverSomeOperationsGenerator, 6],
822
898
  [reconnectGenerator, 1],
823
899
  ]),
@@ -889,7 +965,6 @@ export function performFuzzActions(
889
965
  return state;
890
966
  },
891
967
  validate: (state) => {
892
- network.deliverOperations(DestinationClient.All);
893
968
  validator?.(network);
894
969
  return state;
895
970
  },
@@ -80,12 +80,23 @@ import {
80
80
  export interface IIdCompressorCore {
81
81
  /**
82
82
  * Returns a range of IDs created by this session in a format for sending to the server for finalizing.
83
- * The range will include all IDs generated via calls to `generateCompressedId` since the last time this method was called.
83
+ * The range will include all IDs generated via calls to `generateCompressedId` since the last time a
84
+ * range was taken (via this method or `takeUnfinalizedCreationRange`).
84
85
  * @returns the range of IDs, which may be empty. This range must be sent to the server for ordering before
85
86
  * it is finalized. Ranges must be sent to the server in the order that they are taken via calls to this method.
86
87
  */
87
88
  takeNextCreationRange(): IdCreationRange;
88
89
 
90
+ /**
91
+ * Returns a range of IDs created by this session in a format for sending to the server for finalizing.
92
+ * The range will include all unfinalized IDs generated via calls to `generateCompressedId`.
93
+ * @returns the range of IDs, which may be empty. This range must be sent to the server for ordering before
94
+ * it is finalized. Ranges must be sent to the server in the order that they are taken via calls to this method.
95
+ * Note: after finalizing the range returned by this method, finalizing any ranges that had been previously taken
96
+ * will result in an error.
97
+ */
98
+ takeUnfinalizedCreationRange(): IdCreationRange;
99
+
89
100
  /**
90
101
  * Finalizes the supplied range of IDs (which may be from either a remote or local session).
91
102
  * @param range - the range of session-local IDs to finalize.