@fluidframework/id-compressor 3.3.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/idCompressor.d.ts +10 -17
  3. package/dist/idCompressor.d.ts.map +1 -1
  4. package/dist/idCompressor.js +68 -35
  5. package/dist/idCompressor.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/packageVersion.d.ts +1 -1
  10. package/dist/packageVersion.js +1 -1
  11. package/dist/packageVersion.js.map +1 -1
  12. package/dist/test/sharding.fuzz.spec.js.map +1 -1
  13. package/dist/test/sharding.spec.js.map +1 -1
  14. package/dist/test/types/validateIdCompressorPrevious.generated.js.map +1 -1
  15. package/dist/types/idCompressor.d.ts +110 -31
  16. package/dist/types/idCompressor.d.ts.map +1 -1
  17. package/dist/types/idCompressor.js.map +1 -1
  18. package/dist/types/index.d.ts +1 -1
  19. package/dist/types/index.d.ts.map +1 -1
  20. package/dist/types/index.js.map +1 -1
  21. package/lib/idCompressor.d.ts +10 -17
  22. package/lib/idCompressor.d.ts.map +1 -1
  23. package/lib/idCompressor.js +69 -36
  24. package/lib/idCompressor.js.map +1 -1
  25. package/lib/index.d.ts +1 -1
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js.map +1 -1
  28. package/lib/packageVersion.d.ts +1 -1
  29. package/lib/packageVersion.js +1 -1
  30. package/lib/packageVersion.js.map +1 -1
  31. package/lib/test/sharding.fuzz.spec.js.map +1 -1
  32. package/lib/test/sharding.spec.js.map +1 -1
  33. package/lib/test/types/validateIdCompressorPrevious.generated.js.map +1 -1
  34. package/lib/types/idCompressor.d.ts +110 -31
  35. package/lib/types/idCompressor.d.ts.map +1 -1
  36. package/lib/types/idCompressor.js.map +1 -1
  37. package/lib/types/index.d.ts +1 -1
  38. package/lib/types/index.d.ts.map +1 -1
  39. package/lib/types/index.js.map +1 -1
  40. package/package.json +9 -42
  41. package/src/idCompressor.ts +90 -42
  42. package/src/index.ts +10 -7
  43. package/src/packageVersion.ts +1 -1
  44. package/src/test/types/validateIdCompressorPrevious.generated.ts +1 -121
  45. package/src/types/idCompressor.ts +119 -31
  46. package/src/types/index.ts +3 -0
@@ -3,8 +3,8 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { bufferToString, stringToBuffer } from "@fluid-internal/client-utils";
7
- import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
6
+ import { bufferToString, createEmitter, stringToBuffer } from "@fluid-internal/client-utils";
7
+ import type { ITelemetryBaseLogger, Listenable } from "@fluidframework/core-interfaces";
8
8
  import { assert } from "@fluidframework/core-utils/internal";
9
9
  import type { TelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
10
10
  import {
@@ -54,6 +54,9 @@ import type {
54
54
  SessionSpaceCompressedId,
55
55
  StableId,
56
56
  ShardSynchronizationToken,
57
+ SerializedIdCompressorShard,
58
+ ParentShardSynchronizationToken,
59
+ IdCompressorEvents,
57
60
  } from "./types/index.js";
58
61
  import { SerializationVersion } from "./types/index.js";
59
62
  import {
@@ -75,10 +78,26 @@ function rangeFinalizationError(expectedStart: number, actualStart: number): Log
75
78
 
76
79
  const MAX_STRIDE_LENGTH = 1000000;
77
80
 
81
+ /**
82
+ * Validates the generation count in a shard synchronization token.
83
+ *
84
+ * @param localGenCount - The number of positions filled, including backfilled positions.
85
+ * @throws {@link TypeError} if the count is not a nonnegative safe integer.
86
+ */
87
+ function validateShardGenerationCount(localGenCount: number): void {
88
+ if (!Number.isSafeInteger(localGenCount) || localGenCount < 0) {
89
+ throw new TypeError("Invalid shard synchronization token generation count.");
90
+ }
91
+ }
92
+
78
93
  /**
79
94
  * See {@link IIdCompressor} and {@link IIdCompressorCore}
80
95
  */
81
96
  export class IdCompressor implements IIdCompressor, IIdCompressorCore {
97
+ private readonly eventEmitter = createEmitter<IdCompressorEvents>();
98
+
99
+ public readonly events: Listenable<IdCompressorEvents> = this.eventEmitter;
100
+
82
101
  /**
83
102
  * Max allowed initial cluster size.
84
103
  */
@@ -261,9 +280,6 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
261
280
  this.ongoingGhostSession = { ghostSessionId };
262
281
  }
263
282
 
264
- /**
265
- * {@inheritdoc IIdCompressorCore.beginGhostSession}
266
- */
267
283
  public beginGhostSession(ghostSessionId: SessionId, ghostSessionCallback: () => void): void {
268
284
  if (this.shardingState) {
269
285
  throw new Error("Cannot start a ghost session while sharded.");
@@ -276,10 +292,7 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
276
292
  }
277
293
  }
278
294
 
279
- /**
280
- * {@inheritdoc IIdCompressorCore.shard}
281
- */
282
- public shard(newShardCount: number): SerializedIdCompressorWithOngoingSession[] {
295
+ public shard(newShardCount: number): SerializedIdCompressorShard[] {
283
296
  if (!Number.isSafeInteger(newShardCount) || newShardCount <= 0) {
284
297
  throw new Error("Shard count must be a positive safe integer");
285
298
  }
@@ -316,7 +329,7 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
316
329
  const serialized = this.serialize(true);
317
330
  const { localGenCount: parentGenCount } = this;
318
331
 
319
- const shards: SerializedIdCompressorWithOngoingSession[] = [];
332
+ const shards: SerializedIdCompressorShard[] = [];
320
333
 
321
334
  // Children are positioned at consecutive positions within the parents current stride cycle
322
335
  // e.g. if parent owns the "evens" space (-2, -4, -6, ...) and is sharded into 3 (two children)
@@ -346,14 +359,15 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
346
359
  shardId: childShardId,
347
360
  };
348
361
 
349
- shards.push(child.serialize(true));
362
+ shards.push({
363
+ serialized: child.serialize(true),
364
+ syncToken: child.makeShardToken(false),
365
+ });
350
366
  }
351
367
 
352
368
  return shards;
353
369
  }
354
- /**
355
- * {@inheritdoc IIdCompressorCore.synchronizeWithShard}
356
- */
370
+
357
371
  public synchronizeWithShard(syncToken: ShardSynchronizationToken): void {
358
372
  const isNowLeaf = this.synchronizeChild(syncToken);
359
373
  // A disposal token additionally reclaims the child's ID space. If reclaiming the last child
@@ -366,6 +380,54 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
366
380
  }
367
381
  }
368
382
 
383
+ public getChildShardSyncToken(
384
+ childToken: ShardSynchronizationToken,
385
+ ): ParentShardSynchronizationToken {
386
+ if (!this.shardingState?.activeChildIds.has(childToken.shardId)) {
387
+ throw new Error("Cannot get a synchronization token for an inactive child shard.");
388
+ }
389
+ const token: ParentShardSynchronizationToken = {
390
+ type: "parentIdSpaceShardSyncToken",
391
+ shardId: childToken.shardId,
392
+ localGenCount: this.localGenCount,
393
+ };
394
+ return token;
395
+ }
396
+
397
+ public synchronizeWithParent(token: ParentShardSynchronizationToken): void {
398
+ const state = this.shardingState;
399
+ if (
400
+ token.type !== "parentIdSpaceShardSyncToken" ||
401
+ state?.shardId === undefined ||
402
+ state.shardId !== token.shardId
403
+ ) {
404
+ throw new Error("Invalid parent synchronization token for this child shard.");
405
+ }
406
+ validateShardGenerationCount(token.localGenCount);
407
+ if (token.localGenCount <= this.localGenCount) {
408
+ // A child can already be ahead through its own allocations. Its local range
409
+ // then includes the parent's IDs through this generation count.
410
+ return;
411
+ }
412
+ this.backfillToNextStridePosition(token.localGenCount, state.currentStride);
413
+ }
414
+
415
+ /**
416
+ * Adds IDs through the next position in this shard's stride after the given generation count.
417
+ */
418
+ private backfillToNextStridePosition(targetGenCount: number, stride: number): void {
419
+ if (targetGenCount <= this.localGenCount) {
420
+ return;
421
+ }
422
+ const count = Math.ceil((targetGenCount - this.localGenCount + 1) / stride) * stride;
423
+ const nextGenCount = this.localGenCount + count;
424
+ if (!Number.isSafeInteger(nextGenCount)) {
425
+ throw new TypeError("Shard progress exceeds the supported ID space.");
426
+ }
427
+ this.normalizer.addLocalRange(this.localGenCount + 1, count);
428
+ this.localGenCount = nextGenCount;
429
+ }
430
+
369
431
  private synchronizeChild(syncToken: ShardToken): boolean {
370
432
  const childDisposed = syncToken.disposed;
371
433
  if (this.writeVersion < SerializationVersion.V3) {
@@ -386,39 +448,27 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
386
448
  `Cannot synchronize with child with ID ${childShardId}: not in active children set`,
387
449
  );
388
450
  }
451
+ validateShardGenerationCount(syncToken.localGenCount);
452
+
453
+ // A last-child disposal realigns on the original stride. Backfill before changing
454
+ // membership so invalid progress cannot accidentally reclaim the child.
455
+ const isLeaf = childDisposed && activeChildIds.size === 1;
456
+ this.backfillToNextStridePosition(
457
+ syncToken.localGenCount,
458
+ isLeaf ? originalStride : this.shardingState.currentStride,
459
+ );
389
460
 
390
- // Only disposal removes the child from the active set. A plain synchronization leaves the
391
- // child active so it can continue generating IDs and be synchronized with again later.
461
+ // A plain synchronization leaves the child active so it can keep generating IDs.
392
462
  if (childDisposed) {
393
463
  activeChildIds.delete(childShardId);
394
- }
395
-
396
- const isLeaf = activeChildIds.size === 0;
397
- if (isLeaf && childDisposed) {
398
- this.shardingState.currentStride = originalStride;
399
- }
400
-
401
- // Read currentStride after the possible stride reset above, so that a leaf-making disposal
402
- // realigns the parent on its original (reclaimed) stride rather than the wider sharded stride.
403
- const { currentStride } = this.shardingState;
404
- const childGenCount = syncToken.localGenCount;
405
-
406
- // Realign parent to next position in its sequence if child is ahead
407
- if (childGenCount > this.localGenCount) {
408
- // Find next aligned position in parent's sequence after child's genCount
409
- const distance = childGenCount - this.localGenCount + 1;
410
- const steps = Math.ceil(distance / currentStride);
411
- const count = steps * currentStride;
412
- this.normalizer.addLocalRange(this.localGenCount + 1, count);
413
- this.localGenCount += count;
464
+ if (isLeaf) {
465
+ this.shardingState.currentStride = originalStride;
466
+ }
414
467
  }
415
468
 
416
469
  return isLeaf;
417
470
  }
418
471
 
419
- /**
420
- * {@inheritdoc IIdCompressorCore.dispose}
421
- */
422
472
  public disposeShard(): ShardSynchronizationToken | undefined {
423
473
  if (this.shardingState === undefined) {
424
474
  return undefined;
@@ -438,9 +488,6 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
438
488
  return token;
439
489
  }
440
490
 
441
- /**
442
- * {@inheritdoc IIdCompressorCore.getShardSyncToken}
443
- */
444
491
  public getShardSyncToken(): ShardSynchronizationToken | undefined {
445
492
  if (this.shardingState === undefined) {
446
493
  return undefined;
@@ -639,6 +686,7 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore {
639
686
  }
640
687
 
641
688
  assert(!session.isEmpty(), 0x757 /* Empty sessions should not be created. */);
689
+ this.eventEmitter.emit("rangeFinalized", range);
642
690
  }
643
691
 
644
692
  private addEmptyCluster(session: Session, capacity: number): IdCluster {
package/src/index.ts CHANGED
@@ -22,16 +22,19 @@ export {
22
22
  isStableId,
23
23
  } from "./utilities.js";
24
24
  export type {
25
- IIdCompressorCore,
25
+ IdCreationRange,
26
26
  IIdCompressor,
27
+ IIdCompressorCore,
28
+ IdCompressorEvents,
29
+ OpSpaceCompressedId,
30
+ ParentShardSynchronizationToken,
27
31
  SerializedIdCompressor,
28
- SerializedIdCompressorWithOngoingSession,
29
32
  SerializedIdCompressorWithNoSession,
30
- SessionSpaceCompressedId,
31
- OpSpaceCompressedId,
33
+ SerializedIdCompressorWithOngoingSession,
34
+ SerializedIdCompressorShard,
32
35
  SessionId,
33
- StableId,
34
- IdCreationRange,
35
- ShardToken,
36
+ SessionSpaceCompressedId,
36
37
  ShardSynchronizationToken,
38
+ ShardToken,
39
+ StableId,
37
40
  } from "./types/index.js";
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/id-compressor";
9
- export const pkgVersion = "3.3.0";
9
+ export const pkgVersion = "3.5.0";
@@ -7,7 +7,7 @@
7
7
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
8
8
  * Generated by "flub generate typetests" from @fluid-tools/build-cli.
9
9
  *
10
- * Baseline (previous) version: 3.2.0
10
+ * Baseline (previous) version: 3.4.0
11
11
  */
12
12
 
13
13
  import type { TypeOnly, MinimalType, FullType, requireAssignableTo } from "@fluidframework/build-tools";
@@ -17,66 +17,6 @@ import type * as current from "@fluidframework/id-compressor/legacy";
17
17
 
18
18
  declare type MakeUnusedImportErrorsGoAway<T> = TypeOnly<T> | MinimalType<T> | FullType<T> | typeof old | typeof current | requireAssignableTo<true, true>;
19
19
 
20
- /*
21
- * Validate backward compatibility by using the current type in place of the old type.
22
- * If this test starts failing, it indicates a change that is not backward compatible.
23
- * To acknowledge the breaking change, add the following to package.json under
24
- * typeValidation.broken:
25
- * "Function_createIdCompressor": {"backCompat": false}
26
- */
27
- // @ts-expect-error compatibility expected to be broken
28
- declare type current_as_old_for_Function_createIdCompressor = requireAssignableTo<TypeOnly<typeof current.createIdCompressor>, TypeOnly<typeof old.createIdCompressor>>
29
-
30
- /*
31
- * Validate backward compatibility by using the current type in place of the old type.
32
- * If this test starts failing, it indicates a change that is not backward compatible.
33
- * To acknowledge the breaking change, add the following to package.json under
34
- * typeValidation.broken:
35
- * "Function_createSessionId": {"backCompat": false}
36
- */
37
- // @ts-expect-error compatibility expected to be broken
38
- declare type current_as_old_for_Function_createSessionId = requireAssignableTo<TypeOnly<typeof current.createSessionId>, TypeOnly<typeof old.createSessionId>>
39
-
40
- /*
41
- * Validate backward compatibility by using the current type in place of the old type.
42
- * If this test starts failing, it indicates a change that is not backward compatible.
43
- * To acknowledge the breaking change, add the following to package.json under
44
- * typeValidation.broken:
45
- * "Function_deserializeIdCompressor": {"backCompat": false}
46
- */
47
- // @ts-expect-error compatibility expected to be broken
48
- declare type current_as_old_for_Function_deserializeIdCompressor = requireAssignableTo<TypeOnly<typeof current.deserializeIdCompressor>, TypeOnly<typeof old.deserializeIdCompressor>>
49
-
50
- /*
51
- * Validate backward compatibility by using the current type in place of the old type.
52
- * If this test starts failing, it indicates a change that is not backward compatible.
53
- * To acknowledge the breaking change, add the following to package.json under
54
- * typeValidation.broken:
55
- * "Function_serializeIdCompressor": {"backCompat": false}
56
- */
57
- // @ts-expect-error compatibility expected to be broken
58
- declare type current_as_old_for_Function_serializeIdCompressor = requireAssignableTo<TypeOnly<typeof current.serializeIdCompressor>, TypeOnly<typeof old.serializeIdCompressor>>
59
-
60
- /*
61
- * Validate forward compatibility by using the old type in place of the current type.
62
- * If this test starts failing, it indicates a change that is not forward compatible.
63
- * To acknowledge the breaking change, add the following to package.json under
64
- * typeValidation.broken:
65
- * "Interface_IdCreationRange": {"forwardCompat": false}
66
- */
67
- // @ts-expect-error compatibility expected to be broken
68
- declare type old_as_current_for_Interface_IdCreationRange = requireAssignableTo<TypeOnly<old.IdCreationRange>, TypeOnly<current.IdCreationRange>>
69
-
70
- /*
71
- * Validate backward compatibility by using the current type in place of the old type.
72
- * If this test starts failing, it indicates a change that is not backward compatible.
73
- * To acknowledge the breaking change, add the following to package.json under
74
- * typeValidation.broken:
75
- * "Interface_IdCreationRange": {"backCompat": false}
76
- */
77
- // @ts-expect-error compatibility expected to be broken
78
- declare type current_as_old_for_Interface_IdCreationRange = requireAssignableTo<TypeOnly<current.IdCreationRange>, TypeOnly<old.IdCreationRange>>
79
-
80
20
  /*
81
21
  * Validate backward compatibility by using the current type in place of the old type.
82
22
  * If this test starts failing, it indicates a change that is not backward compatible.
@@ -104,66 +44,6 @@ declare type old_as_current_for_TypeAlias_OpSpaceCompressedId = requireAssignabl
104
44
  */
105
45
  declare type current_as_old_for_TypeAlias_OpSpaceCompressedId = requireAssignableTo<TypeOnly<current.OpSpaceCompressedId>, TypeOnly<old.OpSpaceCompressedId>>
106
46
 
107
- /*
108
- * Validate forward compatibility by using the old type in place of the current type.
109
- * If this test starts failing, it indicates a change that is not forward compatible.
110
- * To acknowledge the breaking change, add the following to package.json under
111
- * typeValidation.broken:
112
- * "TypeAlias_SerializedIdCompressor": {"forwardCompat": false}
113
- */
114
- // @ts-expect-error compatibility expected to be broken
115
- declare type old_as_current_for_TypeAlias_SerializedIdCompressor = requireAssignableTo<TypeOnly<old.SerializedIdCompressor>, TypeOnly<current.SerializedIdCompressor>>
116
-
117
- /*
118
- * Validate backward compatibility by using the current type in place of the old type.
119
- * If this test starts failing, it indicates a change that is not backward compatible.
120
- * To acknowledge the breaking change, add the following to package.json under
121
- * typeValidation.broken:
122
- * "TypeAlias_SerializedIdCompressor": {"backCompat": false}
123
- */
124
- // @ts-expect-error compatibility expected to be broken
125
- declare type current_as_old_for_TypeAlias_SerializedIdCompressor = requireAssignableTo<TypeOnly<current.SerializedIdCompressor>, TypeOnly<old.SerializedIdCompressor>>
126
-
127
- /*
128
- * Validate forward compatibility by using the old type in place of the current type.
129
- * If this test starts failing, it indicates a change that is not forward compatible.
130
- * To acknowledge the breaking change, add the following to package.json under
131
- * typeValidation.broken:
132
- * "TypeAlias_SerializedIdCompressorWithNoSession": {"forwardCompat": false}
133
- */
134
- // @ts-expect-error compatibility expected to be broken
135
- declare type old_as_current_for_TypeAlias_SerializedIdCompressorWithNoSession = requireAssignableTo<TypeOnly<old.SerializedIdCompressorWithNoSession>, TypeOnly<current.SerializedIdCompressorWithNoSession>>
136
-
137
- /*
138
- * Validate backward compatibility by using the current type in place of the old type.
139
- * If this test starts failing, it indicates a change that is not backward compatible.
140
- * To acknowledge the breaking change, add the following to package.json under
141
- * typeValidation.broken:
142
- * "TypeAlias_SerializedIdCompressorWithNoSession": {"backCompat": false}
143
- */
144
- // @ts-expect-error compatibility expected to be broken
145
- declare type current_as_old_for_TypeAlias_SerializedIdCompressorWithNoSession = requireAssignableTo<TypeOnly<current.SerializedIdCompressorWithNoSession>, TypeOnly<old.SerializedIdCompressorWithNoSession>>
146
-
147
- /*
148
- * Validate forward compatibility by using the old type in place of the current type.
149
- * If this test starts failing, it indicates a change that is not forward compatible.
150
- * To acknowledge the breaking change, add the following to package.json under
151
- * typeValidation.broken:
152
- * "TypeAlias_SerializedIdCompressorWithOngoingSession": {"forwardCompat": false}
153
- */
154
- // @ts-expect-error compatibility expected to be broken
155
- declare type old_as_current_for_TypeAlias_SerializedIdCompressorWithOngoingSession = requireAssignableTo<TypeOnly<old.SerializedIdCompressorWithOngoingSession>, TypeOnly<current.SerializedIdCompressorWithOngoingSession>>
156
-
157
- /*
158
- * Validate backward compatibility by using the current type in place of the old type.
159
- * If this test starts failing, it indicates a change that is not backward compatible.
160
- * To acknowledge the breaking change, add the following to package.json under
161
- * typeValidation.broken:
162
- * "TypeAlias_SerializedIdCompressorWithOngoingSession": {"backCompat": false}
163
- */
164
- // @ts-expect-error compatibility expected to be broken
165
- declare type current_as_old_for_TypeAlias_SerializedIdCompressorWithOngoingSession = requireAssignableTo<TypeOnly<current.SerializedIdCompressorWithOngoingSession>, TypeOnly<old.SerializedIdCompressorWithOngoingSession>>
166
-
167
47
  /*
168
48
  * Validate forward compatibility by using the old type in place of the current type.
169
49
  * If this test starts failing, it indicates a change that is not forward compatible.
@@ -9,6 +9,7 @@ import type {
9
9
  SessionSpaceCompressedId,
10
10
  StableId,
11
11
  } from "./identifiers.js";
12
+ import type { Listenable } from "@fluidframework/core-interfaces";
12
13
  import type {
13
14
  IdCreationRange,
14
15
  SerializedIdCompressorWithNoSession,
@@ -101,6 +102,11 @@ export type SerializationVersion =
101
102
  * @internal
102
103
  */
103
104
  export interface IIdCompressorCore {
105
+ /**
106
+ * Events emitted by the ID compressor.
107
+ */
108
+ readonly events: Listenable<IdCompressorEvents>;
109
+
104
110
  /**
105
111
  * Returns a range of IDs created by this session in a format for sending to the server for finalizing.
106
112
  * The range will include all IDs generated via calls to `generateCompressedId` since the last time a
@@ -174,12 +180,11 @@ export interface IIdCompressorCore {
174
180
  * @param newShardCount - The number of additional different shards to split this compressor into.
175
181
  * Must be a positive safe integer.
176
182
  * @throws If `newShardCount` is not a positive safe integer or the resulting stride exceeds the sharding limit.
177
- * @returns An array of serialized compressors of size `newShardCount`.
178
- * These can be passed across a marshalling boundary and rehydrated on the other side, and will safely share the ID space of `this`.
183
+ * @returns An array of `newShardCount` {@link SerializedIdCompressorShard} objects.
179
184
  * Note that this method should only be needed when multiple JS runtimes are in play, as sharded compressors essentially
180
185
  * attempt to emulate a single static compressor and any code running in the same JS runtime can simply use statics.
181
186
  */
182
- shard(newShardCount: number): SerializedIdCompressorWithOngoingSession[];
187
+ shard(newShardCount: number): SerializedIdCompressorShard[];
183
188
 
184
189
  /**
185
190
  * Synchronizes `this` compressor with a child shard. Synchronization will occur for the state of the child at the time `syncToken`
@@ -192,9 +197,37 @@ export interface IIdCompressorCore {
192
197
  * be disposed from the leaves upwards.
193
198
  * @param syncToken - The token for the shard, obtained by calling {@link IIdCompressorCore.getShardSyncToken} (non-destructive
194
199
  * synchronization) or {@link IIdCompressorCore.disposeShard} (synchronization plus reclamation of the disposed shard's ID space).
200
+ * @throws `TypeError` if the token's generation count is not a nonnegative safe integer.
195
201
  */
196
202
  synchronizeWithShard(syncToken: ShardSynchronizationToken): void;
197
203
 
204
+ /**
205
+ * Gets a synchronization token from this compressor for an active child shard.
206
+ *
207
+ * @remarks
208
+ * The child can apply this token without replacing its compressor.
209
+ *
210
+ * @param childToken - A token from the child shard that will receive this token.
211
+ * @returns This parent's synchronization token for that child.
212
+ * @throws If the child shard is not active on this compressor.
213
+ */
214
+ getChildShardSyncToken(
215
+ childToken: ShardSynchronizationToken,
216
+ ): ParentShardSynchronizationToken;
217
+
218
+ /**
219
+ * Synchronizes this child shard with its parent's token.
220
+ *
221
+ * @remarks
222
+ * This method keeps the child's allocation stride and does not finalize ID creation ranges.
223
+ * Apply creation ranges from {@link IIdCompressorCore.events} separately, in order.
224
+ *
225
+ * @param token - Synchronization token obtained from this child's parent.
226
+ * @throws If the token names another child or contains invalid state.
227
+ * @throws `TypeError` if the token's generation count is not a nonnegative safe integer.
228
+ */
229
+ synchronizeWithParent(token: ParentShardSynchronizationToken): void;
230
+
198
231
  /**
199
232
  * Returns undefined if this compressor is not part of a shard group, and otherwise returns a synchronization token for this shard
200
233
  * that can be used when calling {@link IIdCompressorCore.synchronizeWithShard}. This does NOT dispose the shard.
@@ -234,24 +267,47 @@ export interface IIdCompressorCore {
234
267
  }
235
268
 
236
269
  /**
237
- * The state shared by all shard tokens: enough information to identify a shard and its progress
238
- * through its stride pattern. This is used to track which shard generated which IDs and to manage
239
- * reclamation of a disposed shard's ID space. The {@link ShardToken.disposed} flag distinguishes a
240
- * plain synchronization token from a disposal token. The branded {@link ShardSynchronizationToken}
241
- * is the concrete type handed to consumers.
270
+ * The serialized state of a new child compressor and its initial synchronization token.
271
+ *
272
+ * @sealed
273
+ * @internal
274
+ */
275
+ export interface SerializedIdCompressorShard {
276
+ /**
277
+ * Serialized child state that can be rehydrated across a marshalling boundary.
278
+ */
279
+ readonly serialized: SerializedIdCompressorWithOngoingSession;
280
+
281
+ /**
282
+ * Initial, non-disposing token for this child shard.
283
+ *
284
+ * @remarks
285
+ * After the child generates IDs, obtain its updated token before synchronizing
286
+ * with the parent or reclaiming the child's ID space.
287
+ *
288
+ * This can be used to immediately dispose of a new shard if it is not used.
289
+ */
290
+ readonly syncToken: ShardSynchronizationToken;
291
+ }
292
+
293
+ /**
294
+ * State shared by shard synchronization tokens.
295
+ * @remarks
296
+ * Treat these tokens as opaque: the compressor owns their interpretation and may add state in the future.
297
+ * The {@link ShardToken.disposed} flag distinguishes ordinary synchronization from shard reclamation.
298
+ * The branded {@link ShardSynchronizationToken} is the concrete child-to-parent token.
242
299
  * @internal
243
300
  */
244
301
  export interface ShardToken {
245
302
  /**
246
- * The number of positions filled in this shard's stride pattern.
247
- * This tracks progress through the stride cycle, not the count of IDs actually generated.
248
- * For example, when a shard is created, it backfills entries for positions in its stride,
249
- * so this value may be non-zero even if the shard hasn't generated any IDs yet.
303
+ * The sending compressor's generation count, which can include backfilled positions
304
+ * rather than only IDs it generated.
305
+ * @remarks Must be a nonnegative safe integer.
250
306
  */
251
307
  localGenCount: number;
252
308
 
253
309
  /**
254
- * Unique identifier for this shard within its parent.
310
+ * Identifies the child shard involved in synchronization with its parent.
255
311
  */
256
312
  shardId: SessionId;
257
313
 
@@ -272,27 +328,50 @@ export type ShardSynchronizationToken = ShardToken & {
272
328
  readonly ShardSynchronizationToken: "c79724e1-9103-4415-95b5-bebb932be404";
273
329
  };
274
330
 
331
+ /**
332
+ * A synchronization token from a parent to one of its active child shards.
333
+ * @remarks
334
+ * Unlike the child-to-parent {@link ShardSynchronizationToken}, this token cannot dispose a shard.
335
+ * It does not finalize ID creation ranges.
336
+ * @internal
337
+ */
338
+ export interface ParentShardSynchronizationToken
339
+ extends Readonly<Omit<ShardToken, "disposed">> {
340
+ /** Identifies the parent-to-child token across a message boundary. */
341
+ readonly type: "parentIdSpaceShardSyncToken";
342
+ }
343
+
344
+ /**
345
+ * Events for finalized ID creation ranges.
346
+ * @internal
347
+ */
348
+ export interface IdCompressorEvents {
349
+ /** Reports a range after finalization succeeds. */
350
+ rangeFinalized: (range: IdCreationRange) => void;
351
+ }
275
352
  /**
276
353
  * A distributed UUID generator and compressor.
354
+ *
355
+ * @remarks
277
356
  * `IdCompressor` offers the ability to generate arbitrary non-colliding v4 UUIDs, called stable IDs, while compressing them into small integers
278
357
  * for efficient storage and transmission. It also provides the ability to decompress these integers back into their original UUIDs.
279
358
  *
280
359
  * The compressor is designed to be used in a distributed environment, where multiple clients may be generating IDs concurrently.
281
- * IDs generated by a compressor, via calls to `generateCompressedId`, are created in the compressor's "local" session.
360
+ * IDs generated by a compressor, via calls to {@link IIdCompressor.generateCompressedId}, are created in the compressor's "local" session.
282
361
  * These IDs are unique within the session, but may not be unique across sessions. In the context of Fluid, the scope of a session
283
362
  * is the same as the scope of a container. This means that anytime IDs are transferred between sessions, they must be translated
284
363
  * through a process called "normalization":
364
+ *
285
365
  * - The scope of a local session (in which local IDs are unique) is called the "session space" of the client.
286
366
  * - The context of persisted state (in Fluid, this is the context of ops and summaries) is called "op space".
287
- * - `SessionSpaceCompressedId`s, generated via calls to `generateCompressedId`, should NEVER be serialized directly.
367
+ * - {@link SessionSpaceCompressedId | SessionSpaceCompressedIds}, generated via calls to {@link IIdCompressor.generateCompressedId}, should NEVER be serialized directly.
288
368
  * In Fluid, this means they should not be included in ops or summaries.
289
369
  * - Before serialization, IDs must be normalized to op space to ensure they are interpretable by other clients.
290
370
  * - Upon receipt, IDs should be normalized back to session space before use.
291
371
  *
292
- * Example Usage:
293
- *
294
- * ### Client A (Sender)
372
+ * @example
295
373
  *
374
+ * Client A (Sender)
296
375
  * ```typescript
297
376
  * // Generate several local IDs
298
377
  * const localId1 = idCompressor.generateCompressedId();
@@ -312,7 +391,7 @@ export type ShardSynchronizationToken = ShardToken & {
312
391
  * };
313
392
  * ```
314
393
  *
315
- * ### Client B (Receiver)
394
+ * Client B (Receiver)
316
395
  *
317
396
  * ```typescript
318
397
  * // Receive the message from Client A
@@ -334,27 +413,34 @@ export interface IIdCompressor {
334
413
 
335
414
  /**
336
415
  * Generates a new compressed ID.
337
- * The returned ID is in session space and should not be serialized directly. See `IIdCompressor` for more details.
416
+ *
417
+ * @remarks The returned ID is in session space and should not be serialized directly.
418
+ * @see {@link IIdCompressor} for more details.
338
419
  * @returns A new local ID in session space.
339
420
  */
340
421
  generateCompressedId(): SessionSpaceCompressedId;
341
422
 
342
423
  /**
343
424
  * Generates a new ID that is guaranteed to be unique across all sessions known to this compressor without the need for any
344
- * normalization. The returned ID is not guaranteed to be a compressed ID (small number); it may be a stable ID (UUID string).
425
+ * normalization.
426
+ *
427
+ * @remarks The returned ID is not guaranteed to be a compressed ID (small number); it may be a stable ID (UUID string).
345
428
  * In Fluid, the likelihood of generating the bulkier stable ID is dictated by network conditions and is highly probable in
346
429
  * scenarios such as offline. This is still useful for use cases where simplicity is more important than performance and
347
430
  * this approach will often be superior to generating a UUID.
348
- * If small numbers are a requirement, `generateCompressedId` and normalization should be used instead.
349
- * See `IIdCompressor` for more details.
431
+ *
432
+ * If small numbers are a requirement, {@link IIdCompressor.generateCompressedId} and normalization should be used instead.
433
+ * @see {@link IIdCompressor} for more details.
350
434
  * @returns A new local ID in session space.
351
435
  */
352
436
  generateDocumentUniqueId(): (SessionSpaceCompressedId & OpSpaceCompressedId) | StableId;
353
437
 
354
438
  /**
355
439
  * Normalizes a session space ID into op space.
356
- * The returned ID is in op space and can be safely serialized.
440
+ *
441
+ * @remarks The returned ID is in op space and can be safely serialized.
357
442
  * However, it should be {@link IIdCompressor.normalizeToSessionSpace|normalized back to session space} before use.
443
+ *
358
444
  * @param id - The local ID to normalize.
359
445
  * @returns The ID in op space.
360
446
  */
@@ -362,23 +448,25 @@ export interface IIdCompressor {
362
448
 
363
449
  /**
364
450
  * Normalizes an ID into session space.
365
- * @param id - The ID to normalize.
366
- * @param originSessionId - The {@link IIdCompressor.localSessionId} of the compressor which {@link IIdCompressor.normalizeToOpSpace|encoded} `id`.
367
- * This might not be the client that created `id` in the first place, but rather the client that serialized it.
368
- * This is an important distinction in the case of a reference, where a client might refer to an ID created by another client.
369
- * @returns The session-space ID in the local session corresponding to `id`.
451
+ *
370
452
  * @remarks
371
453
  * For this to be valid, `originSessionId` must be the {@link IIdCompressor.localSessionId} of the compressor that normalized `id` to op space.
372
- * Additionally, this compressor must know about the relevant {@link IdCreationRange} from the encoding `IIdCompressor`.
454
+ * Additionally, this compressor must know about the relevant {@link IdCreationRange} from the encoding {@link IIdCompressor}.
373
455
  * This can be accomplished using creating the compressor using {@link (deserializeIdCompressor:1)},
374
456
  * or relying on something (like the Fluid runtime) to synchronize the range information using internal mechanisms.
375
457
  *
376
- * The {@link IIdCompressor} provided by the Fluid Framework runtime will automatically track {@link IdCreationRange| Id Creation Ranges} (via its own ops) from other clients in the container.
458
+ * The {@link IIdCompressor} provided by the Fluid Framework runtime will automatically track {@link IdCreationRange | Id Creation Ranges} (via its own ops) from other clients in the container.
377
459
  * It is up to the user of the {@link IIdCompressor} to ensure the relevant session information is available,
378
460
  * either by depending on the originating client's session ID for ops, or by explicitly storing it alongside the data containing the {@link OpSpaceCompressedId}.
379
461
  *
380
462
  * Note that attachment summaries, while they are ops, are also interpreted as summaries without the context of their originating session,
381
463
  * so they should explicitly include session IDs if required to decode them just like regular summaries and exported data.
464
+ *
465
+ * @param id - The ID to normalize.
466
+ * @param originSessionId - The {@link IIdCompressor.localSessionId} of the compressor which {@link IIdCompressor.normalizeToOpSpace|encoded} `id`.
467
+ * This might not be the client that created `id` in the first place, but rather the client that serialized it.
468
+ * This is an important distinction in the case of a reference, where a client might refer to an ID created by another client.
469
+ * @returns The session-space ID in the local session corresponding to `id`.
382
470
  */
383
471
  normalizeToSessionSpace(
384
472
  id: OpSpaceCompressedId,