@fluidframework/id-compressor 3.2.0 → 3.4.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 (102) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/api-report/id-compressor.legacy.beta.api.md +0 -49
  3. package/dist/idCompressor.d.ts +65 -22
  4. package/dist/idCompressor.d.ts.map +1 -1
  5. package/dist/idCompressor.js +401 -60
  6. package/dist/idCompressor.js.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/legacy.d.ts +1 -12
  12. package/dist/packageVersion.d.ts +1 -1
  13. package/dist/packageVersion.js +1 -1
  14. package/dist/packageVersion.js.map +1 -1
  15. package/dist/test/idCompressor.perf.spec.js.map +1 -1
  16. package/dist/test/idCompressor.spec.js.map +1 -1
  17. package/dist/test/idCompressorTestUtilities.d.ts.map +1 -1
  18. package/dist/test/idCompressorTestUtilities.js +8 -3
  19. package/dist/test/idCompressorTestUtilities.js.map +1 -1
  20. package/dist/test/sharding.fuzz.spec.d.ts +6 -0
  21. package/dist/test/sharding.fuzz.spec.d.ts.map +1 -0
  22. package/dist/test/sharding.fuzz.spec.js.map +1 -0
  23. package/dist/test/sharding.spec.d.ts +6 -0
  24. package/dist/test/sharding.spec.d.ts.map +1 -0
  25. package/dist/test/sharding.spec.js.map +1 -0
  26. package/dist/test/snapshots/summary.spec.js.map +1 -1
  27. package/dist/test/types/validateIdCompressorPrevious.generated.js.map +1 -1
  28. package/dist/types/idCompressor.d.ts +205 -20
  29. package/dist/types/idCompressor.d.ts.map +1 -1
  30. package/dist/types/idCompressor.js +15 -0
  31. package/dist/types/idCompressor.js.map +1 -1
  32. package/dist/types/index.d.ts +2 -1
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/dist/types/index.js +3 -0
  35. package/dist/types/index.js.map +1 -1
  36. package/dist/types/persisted-types/0.0.1.d.ts +4 -4
  37. package/dist/types/persisted-types/0.0.1.js.map +1 -1
  38. package/dist/utilities.d.ts +1 -1
  39. package/dist/utilities.js +1 -1
  40. package/dist/utilities.js.map +1 -1
  41. package/lib/idCompressor.d.ts +65 -22
  42. package/lib/idCompressor.d.ts.map +1 -1
  43. package/lib/idCompressor.js +402 -61
  44. package/lib/idCompressor.js.map +1 -1
  45. package/lib/index.d.ts +2 -1
  46. package/lib/index.d.ts.map +1 -1
  47. package/lib/index.js +1 -0
  48. package/lib/index.js.map +1 -1
  49. package/lib/legacy.d.ts +1 -12
  50. package/lib/packageVersion.d.ts +1 -1
  51. package/lib/packageVersion.js +1 -1
  52. package/lib/packageVersion.js.map +1 -1
  53. package/lib/test/idCompressor.perf.spec.js.map +1 -1
  54. package/lib/test/idCompressor.spec.js.map +1 -1
  55. package/lib/test/idCompressorTestUtilities.d.ts.map +1 -1
  56. package/lib/test/idCompressorTestUtilities.js +8 -3
  57. package/lib/test/idCompressorTestUtilities.js.map +1 -1
  58. package/lib/test/sharding.fuzz.spec.d.ts +6 -0
  59. package/lib/test/sharding.fuzz.spec.d.ts.map +1 -0
  60. package/lib/test/sharding.fuzz.spec.js.map +1 -0
  61. package/lib/test/sharding.spec.d.ts +6 -0
  62. package/lib/test/sharding.spec.d.ts.map +1 -0
  63. package/lib/test/sharding.spec.js.map +1 -0
  64. package/lib/test/snapshots/summary.spec.js.map +1 -1
  65. package/lib/test/types/validateIdCompressorPrevious.generated.js.map +1 -1
  66. package/lib/types/idCompressor.d.ts +205 -20
  67. package/lib/types/idCompressor.d.ts.map +1 -1
  68. package/lib/types/idCompressor.js +14 -1
  69. package/lib/types/idCompressor.js.map +1 -1
  70. package/lib/types/index.d.ts +2 -1
  71. package/lib/types/index.d.ts.map +1 -1
  72. package/lib/types/index.js +1 -1
  73. package/lib/types/index.js.map +1 -1
  74. package/lib/types/persisted-types/0.0.1.d.ts +4 -4
  75. package/lib/types/persisted-types/0.0.1.js.map +1 -1
  76. package/lib/utilities.d.ts +1 -1
  77. package/lib/utilities.js +1 -1
  78. package/lib/utilities.js.map +1 -1
  79. package/package.json +8 -8
  80. package/src/idCompressor.ts +550 -78
  81. package/src/index.ts +11 -5
  82. package/src/packageVersion.ts +1 -1
  83. package/src/test/idCompressorTestUtilities.ts +16 -3
  84. package/src/test/snapshots/v2/compressor-with-finalized-range-from-one-client +1 -0
  85. package/src/test/snapshots/v2/compressor-with-finalized-range-from-one-client-client2 +1 -0
  86. package/src/test/snapshots/v2/compressors-with-finalized-ranges-from-two-clients +1 -0
  87. package/src/test/snapshots/v2/compressors-with-finalized-ranges-from-two-clients-client2 +1 -0
  88. package/src/test/snapshots/v2/empty-compressor-summary +1 -0
  89. package/src/test/snapshots/v2/expansion-semantics +1 -0
  90. package/src/test/snapshots/v2/expansion-semantics-client2 +1 -0
  91. package/src/test/snapshots/v3/compressor-with-finalized-range-from-one-client +1 -0
  92. package/src/test/snapshots/v3/compressor-with-finalized-range-from-one-client-client2 +1 -0
  93. package/src/test/snapshots/v3/compressors-with-finalized-ranges-from-two-clients +1 -0
  94. package/src/test/snapshots/v3/compressors-with-finalized-ranges-from-two-clients-client2 +1 -0
  95. package/src/test/snapshots/v3/empty-compressor-summary +1 -0
  96. package/src/test/snapshots/v3/expansion-semantics +1 -0
  97. package/src/test/snapshots/v3/expansion-semantics-client2 +1 -0
  98. package/src/test/types/validateIdCompressorPrevious.generated.ts +1 -109
  99. package/src/types/idCompressor.ts +225 -20
  100. package/src/types/index.ts +13 -1
  101. package/src/types/persisted-types/0.0.1.ts +4 -4
  102. package/src/utilities.ts +1 -1
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { bufferToString, stringToBuffer } from "@fluid-internal/client-utils";
5
+ import { bufferToString, createEmitter, stringToBuffer } from "@fluid-internal/client-utils";
6
6
  import { assert } from "@fluidframework/core-utils/internal";
7
7
  import { LoggingError, createChildLogger, extractTelemetryLoggerExt, } from "@fluidframework/telemetry-utils/internal";
8
8
  import { FinalSpace } from "./finalSpace.js";
@@ -10,23 +10,34 @@ import { isFinalId, } from "./identifiers.js";
10
10
  import { readBoolean, readNumber, readNumericUuid, writeBoolean, writeNumber, writeNumericUuid, } from "./persistanceUtilities.js";
11
11
  import { SessionSpaceNormalizer } from "./sessionSpaceNormalizer.js";
12
12
  import { Session, Sessions, getAlignedFinal, getAlignedLocal, lastFinalizedFinal, lastFinalizedLocal, } from "./sessions.js";
13
+ import { SerializationVersion } from "./types/index.js";
13
14
  import { createSessionId, genCountFromLocalId, localIdFromGenCount, numericUuidFromStableId, offsetNumericUuid, stableIdFromNumericUuid, subtractNumericUuids, } from "./utilities.js";
14
- /**
15
- * The version of IdCompressor that is currently persisted.
16
- * This should not be changed without careful consideration to compatibility.
17
- */
18
- const currentWrittenVersion = 2;
19
15
  function rangeFinalizationError(expectedStart, actualStart) {
20
16
  return new LoggingError("Ranges finalized out of order", {
21
17
  expectedStart,
22
18
  actualStart,
23
19
  });
24
20
  }
21
+ const MAX_STRIDE_LENGTH = 1000000;
22
+ /**
23
+ * Validates the generation count in a shard synchronization token.
24
+ *
25
+ * @param localGenCount - The number of positions filled, including backfilled positions.
26
+ * @throws {@link TypeError} if the count is not a nonnegative safe integer.
27
+ */
28
+ function validateShardGenerationCount(localGenCount) {
29
+ if (!Number.isSafeInteger(localGenCount) || localGenCount < 0) {
30
+ throw new TypeError("Invalid shard synchronization token generation count.");
31
+ }
32
+ }
25
33
  /**
26
34
  * See {@link IIdCompressor} and {@link IIdCompressorCore}
27
35
  */
28
36
  export class IdCompressor {
29
37
  logger;
38
+ writeVersion;
39
+ eventEmitter = createEmitter();
40
+ events = this.eventEmitter;
30
41
  /**
31
42
  * Max allowed initial cluster size.
32
43
  */
@@ -37,11 +48,19 @@ export class IdCompressor {
37
48
  normalizer = new SessionSpaceNormalizer();
38
49
  // The number of IDs generated by the local session
39
50
  localGenCount = 0;
51
+ /**
52
+ * The sharding state of this compressor, if it has been sharded or is a shard itself.
53
+ * When defined, this compressor operates in "sharding mode" where:
54
+ * - IDs are generated using stride-based allocation (no eager finals)
55
+ * - The normalizer backfills all IDs in each stride cycle
56
+ * - Normal operation resumes when all children are disposed
57
+ */
58
+ shardingState;
40
59
  // #endregion
41
60
  // #region Final state
42
61
  /**
43
62
  * The gen count to be annotated on the range returned by the next call to `takeNextCreationRange`.
44
- * This is advanced to `generatedIdCount` + 1 each time it is called.
63
+ * This is advanced to `localGenCount` + 1 each time it is called.
45
64
  * On the other hand, when `resetUnfinalizedCreationRange` is called,
46
65
  * this is moved back to the start of the unfinalized range, to ensure those IDs are included in the next range.
47
66
  */
@@ -65,8 +84,15 @@ export class IdCompressor {
65
84
  // The ongoing ghost session, if one exists.
66
85
  ongoingGhostSession;
67
86
  // #endregion
68
- constructor(localSessionIdOrDeserialized, logger) {
87
+ constructor(localSessionIdOrDeserialized, logger,
88
+ /**
89
+ * The version this compressor will write out.
90
+ * This applies to both `serialize` and creation range functions.
91
+ * This number also determines what features are available (e.g. sharding support starts at V3).
92
+ */
93
+ writeVersion) {
69
94
  this.logger = logger;
95
+ this.writeVersion = writeVersion;
70
96
  if (typeof localSessionIdOrDeserialized === "string") {
71
97
  this.localSessionId = localSessionIdOrDeserialized;
72
98
  this.localSession = this.sessions.getOrCreate(localSessionIdOrDeserialized);
@@ -94,29 +120,38 @@ export class IdCompressor {
94
120
  this.ongoingGhostSession.cluster.count++;
95
121
  return lastFinalizedFinal(this.ongoingGhostSession.cluster);
96
122
  }
97
- else {
98
- this.localGenCount++;
99
- const lastCluster = this.localSession.getLastCluster();
100
- if (lastCluster === undefined) {
101
- this.telemetryLocalIdCount++;
102
- return this.generateNextLocalId();
103
- }
104
- // If there exists a cluster of final IDs already claimed by the local session that still has room in it,
105
- // it is known prior to range sequencing what a local ID's corresponding final ID will be.
106
- // In this case, it is safe to return the final ID immediately. This is guaranteed to be safe because
107
- // any op that the local session sends that contains one of those final IDs are guaranteed to arrive to
108
- // collaborators *after* the one containing the creation range.
109
- const clusterOffset = this.localGenCount - genCountFromLocalId(lastCluster.baseLocalId);
110
- if (lastCluster.capacity > clusterOffset) {
111
- this.telemetryEagerFinalIdCount++;
112
- // Space in the cluster: eager final
113
- return (lastCluster.baseFinalId +
114
- clusterOffset);
115
- }
116
- // No space in the cluster, return next local
123
+ // In sharding mode, always generate local IDs with stride-based allocation.
124
+ // Eager finals are disabled because shards backfill IDs not in their stride cycle.
125
+ // If another shard generated them as eager finals, ingesting IDs from another shard
126
+ // would fail depending on timing.
127
+ if (this.shardingState !== undefined) {
128
+ this.telemetryLocalIdCount++;
129
+ const { currentStride } = this.shardingState;
130
+ this.normalizer.addLocalRange(this.localGenCount + 1, currentStride);
131
+ this.localGenCount += currentStride;
132
+ return localIdFromGenCount(this.localGenCount);
133
+ }
134
+ // Normal ID generation
135
+ this.localGenCount++;
136
+ const lastCluster = this.localSession.getLastCluster();
137
+ if (lastCluster === undefined) {
117
138
  this.telemetryLocalIdCount++;
118
139
  return this.generateNextLocalId();
119
140
  }
141
+ // If there exists a cluster of final IDs already claimed by the local session that still has room in it,
142
+ // it is known prior to range sequencing what a local ID's corresponding final ID will be.
143
+ // In this case, it is safe to return the final ID immediately. This is guaranteed to be safe because
144
+ // any op that the local session sends that contains one of those final IDs are guaranteed to arrive to
145
+ // collaborators *after* the one containing the creation range.
146
+ const clusterOffset = this.localGenCount - genCountFromLocalId(lastCluster.baseLocalId);
147
+ if (lastCluster.capacity > clusterOffset) {
148
+ this.telemetryEagerFinalIdCount++;
149
+ // Space in the cluster: eager final
150
+ return (lastCluster.baseFinalId + clusterOffset);
151
+ }
152
+ // No space in the cluster, return next local
153
+ this.telemetryLocalIdCount++;
154
+ return this.generateNextLocalId();
120
155
  }
121
156
  generateDocumentUniqueId() {
122
157
  const id = this.generateCompressedId();
@@ -130,10 +165,10 @@ export class IdCompressor {
130
165
  assert(!this.ongoingGhostSession, 0x8fe /* Ghost session already in progress. */);
131
166
  this.ongoingGhostSession = { ghostSessionId };
132
167
  }
133
- /**
134
- * {@inheritdoc IIdCompressorCore.beginGhostSession}
135
- */
136
168
  beginGhostSession(ghostSessionId, ghostSessionCallback) {
169
+ if (this.shardingState) {
170
+ throw new Error("Cannot start a ghost session while sharded.");
171
+ }
137
172
  this.startGhostSession(ghostSessionId);
138
173
  try {
139
174
  ghostSessionCallback();
@@ -142,12 +177,199 @@ export class IdCompressor {
142
177
  this.ongoingGhostSession = undefined;
143
178
  }
144
179
  }
180
+ shard(newShardCount) {
181
+ if (!Number.isSafeInteger(newShardCount) || newShardCount <= 0) {
182
+ throw new Error("Shard count must be a positive safe integer");
183
+ }
184
+ if (this.ongoingGhostSession) {
185
+ throw new Error("Cannot shard during ghost session");
186
+ }
187
+ if (this.writeVersion < SerializationVersion.V3) {
188
+ throw new Error(`Sharding requires document version ${SerializationVersion.V3} or higher, but this document is version ${this.writeVersion}`);
189
+ }
190
+ const currentStride = this.shardingState?.currentStride ?? 1;
191
+ const newStride = currentStride * (newShardCount + 1);
192
+ // ID usage is exponential in stride count. Limit to practical size to avoid overflow.
193
+ if (newStride > MAX_STRIDE_LENGTH) {
194
+ throw new Error("Sharding limit reached.");
195
+ }
196
+ assert(Number.isSafeInteger(newStride), 0xd52 /* Shard stride must be a safe integer */);
197
+ if (this.shardingState === undefined) {
198
+ // First time sharding - initialize state
199
+ this.shardingState = {
200
+ currentStride: newStride,
201
+ originalStride: 1,
202
+ activeChildIds: new Set(),
203
+ };
204
+ }
205
+ else {
206
+ this.shardingState.currentStride = newStride;
207
+ }
208
+ // Cache to avoid repeating per child
209
+ const serialized = this.serialize(true);
210
+ const { localGenCount: parentGenCount } = this;
211
+ const shards = [];
212
+ // Children are positioned at consecutive positions within the parents current stride cycle
213
+ // e.g. if parent owns the "evens" space (-2, -4, -6, ...) and is sharded into 3 (two children)
214
+ // while at -8, then the child shards will start at -10 and -12 with a stride of 6
215
+ for (let i = 1; i <= newShardCount; i++) {
216
+ const childShardId = createSessionId();
217
+ this.shardingState.activeChildIds.add(childShardId);
218
+ const childLocalGenCount = parentGenCount + i * currentStride;
219
+ // This code results in a double roundtrip, but is probably plenty fast enough for scenarios that need sharding,
220
+ // as they necessarily have process boundaries to deal with.
221
+ // If this ever became an issue we can implement a fast clone of compressors
222
+ const child = deserializeIdCompressor(serialized, this.writeVersion);
223
+ const genCountJump = childLocalGenCount - parentGenCount;
224
+ assert(child.localGenCount === parentGenCount && genCountJump > 0, 0xd53 /* Child offsets incorrectly calculated. */);
225
+ child.normalizer.addLocalRange(child.localGenCount + 1, genCountJump);
226
+ child.localGenCount = childLocalGenCount;
227
+ child.shardingState = {
228
+ currentStride: newStride,
229
+ originalStride: newStride,
230
+ activeChildIds: new Set(),
231
+ shardId: childShardId,
232
+ };
233
+ shards.push({
234
+ serialized: child.serialize(true),
235
+ syncToken: child.makeShardToken(false),
236
+ });
237
+ }
238
+ return shards;
239
+ }
240
+ synchronizeWithShard(syncToken) {
241
+ const isNowLeaf = this.synchronizeChild(syncToken);
242
+ // A disposal token additionally reclaims the child's ID space. If reclaiming the last child
243
+ // returns this compressor to a leaf and it is the root (originalStride === 1), exit sharding mode.
244
+ if (syncToken.disposed) {
245
+ assert(this.shardingState !== undefined, 0xd54 /* Must be sharded */);
246
+ if (isNowLeaf && this.shardingState.originalStride === 1) {
247
+ this.shardingState = undefined;
248
+ }
249
+ }
250
+ }
251
+ getChildShardSyncToken(childToken) {
252
+ if (!this.shardingState?.activeChildIds.has(childToken.shardId)) {
253
+ throw new Error("Cannot get a synchronization token for an inactive child shard.");
254
+ }
255
+ const token = {
256
+ type: "parentIdSpaceShardSyncToken",
257
+ shardId: childToken.shardId,
258
+ localGenCount: this.localGenCount,
259
+ };
260
+ return token;
261
+ }
262
+ synchronizeWithParent(token) {
263
+ const state = this.shardingState;
264
+ if (token.type !== "parentIdSpaceShardSyncToken" ||
265
+ state?.shardId === undefined ||
266
+ state.shardId !== token.shardId) {
267
+ throw new Error("Invalid parent synchronization token for this child shard.");
268
+ }
269
+ validateShardGenerationCount(token.localGenCount);
270
+ if (token.localGenCount <= this.localGenCount) {
271
+ // A child can already be ahead through its own allocations. Its local range
272
+ // then includes the parent's IDs through this generation count.
273
+ return;
274
+ }
275
+ this.backfillToNextStridePosition(token.localGenCount, state.currentStride);
276
+ }
277
+ /**
278
+ * Adds IDs through the next position in this shard's stride after the given generation count.
279
+ */
280
+ backfillToNextStridePosition(targetGenCount, stride) {
281
+ if (targetGenCount <= this.localGenCount) {
282
+ return;
283
+ }
284
+ const count = Math.ceil((targetGenCount - this.localGenCount + 1) / stride) * stride;
285
+ const nextGenCount = this.localGenCount + count;
286
+ if (!Number.isSafeInteger(nextGenCount)) {
287
+ throw new TypeError("Shard progress exceeds the supported ID space.");
288
+ }
289
+ this.normalizer.addLocalRange(this.localGenCount + 1, count);
290
+ this.localGenCount = nextGenCount;
291
+ }
292
+ synchronizeChild(syncToken) {
293
+ const childDisposed = syncToken.disposed;
294
+ if (this.writeVersion < SerializationVersion.V3) {
295
+ throw new Error(`Sharding requires document version ${SerializationVersion.V3} or higher, but this document is version ${this.writeVersion}`);
296
+ }
297
+ if (this.shardingState === undefined) {
298
+ throw new Error("Must be in sharding mode.");
299
+ }
300
+ const childShardId = syncToken.shardId;
301
+ const { activeChildIds, originalStride } = this.shardingState;
302
+ // Verify this child belongs to us
303
+ if (!activeChildIds.has(childShardId)) {
304
+ throw new Error(`Cannot synchronize with child with ID ${childShardId}: not in active children set`);
305
+ }
306
+ validateShardGenerationCount(syncToken.localGenCount);
307
+ // A last-child disposal realigns on the original stride. Backfill before changing
308
+ // membership so invalid progress cannot accidentally reclaim the child.
309
+ const isLeaf = childDisposed && activeChildIds.size === 1;
310
+ this.backfillToNextStridePosition(syncToken.localGenCount, isLeaf ? originalStride : this.shardingState.currentStride);
311
+ // A plain synchronization leaves the child active so it can keep generating IDs.
312
+ if (childDisposed) {
313
+ activeChildIds.delete(childShardId);
314
+ if (isLeaf) {
315
+ this.shardingState.currentStride = originalStride;
316
+ }
317
+ }
318
+ return isLeaf;
319
+ }
320
+ disposeShard() {
321
+ if (this.shardingState === undefined) {
322
+ return undefined;
323
+ }
324
+ else {
325
+ if (this.shardingState.activeChildIds.size > 0) {
326
+ throw new Error("Cannot dispose a shard with active children.");
327
+ }
328
+ }
329
+ // A disposal token is a sync token that additionally signals reclamation of this shard's ID space.
330
+ const token = this.makeShardToken(true);
331
+ // Make this compressor unusable by replacing all methods with throwing stubs
332
+ // This is a one-time mutation that avoids runtime checks in every method
333
+ makeCompressorUnusable(this);
334
+ return token;
335
+ }
336
+ getShardSyncToken() {
337
+ if (this.shardingState === undefined) {
338
+ return undefined;
339
+ }
340
+ return this.makeShardToken(false);
341
+ }
342
+ /**
343
+ * Builds a token describing this shard's current progress.
344
+ * @param disposed - Whether the token also signals disposal (reclamation of this shard's ID space).
345
+ */
346
+ makeShardToken(disposed) {
347
+ assert(this.shardingState !== undefined, 0xd55 /* Compressor is not sharded. */);
348
+ // Root cannot produce a token (shardId is undefined for root)
349
+ if (this.shardingState.shardId === undefined) {
350
+ throw new Error("Cannot get a token for a root compressor - only shards can be disposed");
351
+ }
352
+ const token = {
353
+ localGenCount: this.localGenCount,
354
+ shardId: this.shardingState.shardId,
355
+ disposed,
356
+ };
357
+ return token;
358
+ }
145
359
  generateNextLocalId() {
146
360
  // Must tell the normalizer that we generated a local ID
147
361
  this.normalizer.addLocalRange(this.localGenCount, 1);
148
362
  return localIdFromGenCount(this.localGenCount);
149
363
  }
150
364
  takeNextCreationRange() {
365
+ // Child (non-root) shards must not submit their own creation ranges. All shards in a shard tree share a
366
+ // single session, and only the root shard is responsible for finalizing that session's IDs with the server.
367
+ // A child's generated IDs are reconciled with the root via shard synchronization tokens (see
368
+ // synchronizeWithShard / disposeShard), not by independently sending ranges, which would overlap and
369
+ // conflict with the root's ranges for the shared session.
370
+ if (this.shardingState?.shardId !== undefined) {
371
+ throw new Error("Cannot take a creation range from a non-root shard.");
372
+ }
151
373
  assert(!this.ongoingGhostSession, 0x8a6 /* IdCompressor should not be operated normally when in a ghost session */);
152
374
  const count = this.localGenCount - (this.nextRangeBaseGenCount - 1);
153
375
  if (count === 0) {
@@ -268,6 +490,7 @@ export class IdCompressor {
268
490
  this.telemetryLocalIdCount = 0;
269
491
  }
270
492
  assert(!session.isEmpty(), 0x757 /* Empty sessions should not be created. */);
493
+ this.eventEmitter.emit("rangeFinalized", range);
271
494
  }
272
495
  addEmptyCluster(session, capacity) {
273
496
  assert(!this.ongoingGhostSession?.cluster, 0x8a8 /* IdCompressor should not be operated normally when in a ghost session */);
@@ -436,24 +659,36 @@ export class IdCompressor {
436
659
  sessionIndex++;
437
660
  }
438
661
  }
662
+ const sizeOfUuid = 2;
663
+ const shardingStateSize = hasLocalState && this.writeVersion >= SerializationVersion.V3
664
+ ? this.shardingState === undefined
665
+ ? 1 // Boolean: no sharding state
666
+ : 1 + // Boolean: has sharding state
667
+ 2 + // Numbers: currentStride, originalStride (Float64)
668
+ 1 + // Number: activeChildIds count (Float64)
669
+ this.shardingState.activeChildIds.size * sizeOfUuid + // UUIDs: each is 128-bit
670
+ 1 + // Boolean: hasShardId
671
+ (this.shardingState.shardId === undefined ? 0 : sizeOfUuid) // UUID: 128-bit if present
672
+ : 0;
439
673
  const localStateSize = hasLocalState
440
674
  ? 1 + // generated ID count
441
675
  1 + // next range base genCount
442
676
  1 + // count of normalizer pairs
443
- normalizer.idRanges.size * 2 // pairs
677
+ normalizer.idRanges.size * 2 + // pairs
678
+ shardingStateSize // sharding state if present
444
679
  : 0;
445
680
  // Layout size, in 8 byte increments
446
681
  const totalSize = 1 + // version
447
682
  1 + // hasLocalState
448
683
  1 + // session count
449
684
  1 + // cluster count
450
- sessionIndexMap.size * 2 + // session IDs
685
+ sessionIndexMap.size * sizeOfUuid + // session IDs
451
686
  finalSpace.clusters.length * 3 + // clusters: (sessionIndex, capacity, count)[]
452
687
  localStateSize; // local state, if present
453
688
  const serializedFloat = new Float64Array(totalSize);
454
689
  const serializedUint = new BigUint64Array(serializedFloat.buffer);
455
690
  let index = 0;
456
- index = writeNumber(serializedFloat, index, currentWrittenVersion);
691
+ index = writeNumber(serializedFloat, index, this.writeVersion);
457
692
  index = writeBoolean(serializedFloat, index, hasLocalState);
458
693
  index = writeNumber(serializedFloat, index, sessionIndexMap.size);
459
694
  index = writeNumber(serializedFloat, index, finalSpace.clusters.length);
@@ -473,6 +708,24 @@ export class IdCompressor {
473
708
  index = writeNumber(serializedFloat, index, leadingGenCount);
474
709
  index = writeNumber(serializedFloat, index, count);
475
710
  }
711
+ // Write sharding state (version 3+)
712
+ if (this.writeVersion >= SerializationVersion.V3) {
713
+ index = writeBoolean(serializedFloat, index, this.shardingState !== undefined);
714
+ if (this.shardingState !== undefined) {
715
+ index = writeNumber(serializedFloat, index, this.shardingState.currentStride);
716
+ index = writeNumber(serializedFloat, index, this.shardingState.originalStride);
717
+ // Write activeChildIds Set as count + array of NumericUuids
718
+ index = writeNumber(serializedFloat, index, this.shardingState.activeChildIds.size);
719
+ for (const childId of this.shardingState.activeChildIds) {
720
+ index = writeNumericUuid(serializedUint, index, numericUuidFromStableId(childId));
721
+ }
722
+ // Write shardId (optional)
723
+ index = writeBoolean(serializedFloat, index, this.shardingState.shardId !== undefined);
724
+ if (this.shardingState.shardId !== undefined) {
725
+ index = writeNumericUuid(serializedUint, index, numericUuidFromStableId(this.shardingState.shardId));
726
+ }
727
+ }
728
+ }
476
729
  }
477
730
  assert(index === totalSize, 0x75b /* Serialized size was incorrectly calculated. */);
478
731
  logger?.sendTelemetryEvent({
@@ -484,27 +737,29 @@ export class IdCompressor {
484
737
  return bufferToString(serializedFloat.buffer, "base64");
485
738
  }
486
739
  static deserialize(params) {
487
- const { serialized, newSessionId, logger } = params;
740
+ const { serialized, newSessionId: sessionId, logger, requestedWriteVersion } = params;
488
741
  const buffer = stringToBuffer(serialized, "base64");
489
742
  const index = {
490
743
  index: 0,
491
744
  bufferFloat: new Float64Array(buffer),
492
745
  bufferUint: new BigUint64Array(buffer),
493
746
  };
494
- const version = readNumber(index);
495
- switch (version) {
496
- case 1: {
497
- throw new Error("IdCompressor version 1.0 is no longer supported.");
498
- }
499
- case 2: {
500
- return IdCompressor.deserialize2_0(index, newSessionId, logger);
747
+ const serializedVersion = readNumber(index);
748
+ // If requested version is < the serialized version, we must write the version serialized
749
+ // to avoid losing data.
750
+ const writeVersion = Math.max(requestedWriteVersion, serializedVersion);
751
+ switch (serializedVersion) {
752
+ case 2:
753
+ case 3: {
754
+ break;
501
755
  }
502
756
  default: {
757
+ if (serializedVersion === 1) {
758
+ throw new Error("IdCompressor version 1.0 is no longer supported.");
759
+ }
503
760
  throw new Error("Unknown IdCompressor serialized version.");
504
761
  }
505
762
  }
506
- }
507
- static deserialize2_0(index, sessionId, logger) {
508
763
  const hasLocalState = readBoolean(index);
509
764
  const sessionCount = readNumber(index);
510
765
  const clusterCount = readNumber(index);
@@ -525,7 +780,7 @@ export class IdCompressor {
525
780
  const numeric = readNumericUuid(index);
526
781
  sessions.push([numeric, new Session(numeric)]);
527
782
  }
528
- const compressor = new IdCompressor(new Sessions(sessions), logger);
783
+ const compressor = new IdCompressor(new Sessions(sessions), logger, writeVersion);
529
784
  // Clusters
530
785
  let baseFinalId = 0;
531
786
  for (let i = 0; i < clusterCount; i++) {
@@ -547,6 +802,34 @@ export class IdCompressor {
547
802
  for (let i = 0; i < normalizerCount; i++) {
548
803
  compressor.normalizer.addLocalRange(readNumber(index), readNumber(index));
549
804
  }
805
+ // Sharding state (version 3+)
806
+ if (serializedVersion >= 3) {
807
+ const hasShardingState = readBoolean(index);
808
+ if (hasShardingState) {
809
+ const currentStride = readNumber(index);
810
+ const originalStride = readNumber(index);
811
+ // Read activeChildIds Set
812
+ const activeChildCount = readNumber(index);
813
+ const activeChildIds = new Set();
814
+ for (let i = 0; i < activeChildCount; i++) {
815
+ const childIdNumeric = readNumericUuid(index);
816
+ // Safe cast: these were serialized as SessionIds
817
+ activeChildIds.add(stableIdFromNumericUuid(childIdNumeric));
818
+ }
819
+ // Read shardId (optional)
820
+ const hasShardId = readBoolean(index);
821
+ const shardId = hasShardId
822
+ ? stableIdFromNumericUuid(readNumericUuid(index))
823
+ : undefined;
824
+ // Build sharding state object, conditionally including shardId
825
+ compressor.shardingState = {
826
+ currentStride,
827
+ originalStride,
828
+ activeChildIds,
829
+ ...(shardId !== undefined && { shardId }),
830
+ };
831
+ }
832
+ }
550
833
  }
551
834
  assert(index.index === index.bufferFloat.length, 0x75f /* Failed to read entire serialized compressor. */);
552
835
  return compressor;
@@ -560,43 +843,101 @@ export class IdCompressor {
560
843
  this.localGenCount !== other.localGenCount)) {
561
844
  return false;
562
845
  }
846
+ // Shard ownership affects future local ID allocation, even when the generated IDs match.
847
+ // It is not part of finalized state, so ignore it when comparing only that state.
848
+ if (includeLocalState) {
849
+ const state = this.shardingState;
850
+ const otherState = other.shardingState;
851
+ if (state === undefined || otherState === undefined) {
852
+ // Two unsharded compressors match here; a sharded and an unsharded compressor do not.
853
+ if (state !== otherState) {
854
+ return false;
855
+ }
856
+ }
857
+ else {
858
+ if (state.currentStride !== otherState.currentStride ||
859
+ state.originalStride !== otherState.originalStride ||
860
+ state.shardId !== otherState.shardId ||
861
+ state.activeChildIds.size !== otherState.activeChildIds.size) {
862
+ return false;
863
+ }
864
+ // Child ownership is an unordered set. Equal sizes and membership imply equal sets,
865
+ // regardless of insertion order or whether the state was deserialized.
866
+ for (const childId of state.activeChildIds) {
867
+ if (!otherState.activeChildIds.has(childId)) {
868
+ return false;
869
+ }
870
+ }
871
+ }
872
+ }
563
873
  return (this.sessions.equals(other.sessions, includeLocalState) &&
564
874
  this.finalSpace.equals(other.finalSpace));
565
875
  }
566
876
  }
567
- export function createIdCompressor(sessionIdOrLogger, loggerOrUndefined) {
877
+ export function createIdCompressor(sessionIdOrDocumentVersion, writeVersionOrLogger, loggerOrUndefined) {
568
878
  let localSessionId;
879
+ let writeVersion;
569
880
  let logger;
570
- if (sessionIdOrLogger === undefined) {
571
- localSessionId = createSessionId();
881
+ if (typeof sessionIdOrDocumentVersion === "string") {
882
+ // Called with sessionId, writeVersion, logger?
883
+ localSessionId = sessionIdOrDocumentVersion;
884
+ writeVersion = writeVersionOrLogger;
885
+ logger = loggerOrUndefined;
572
886
  }
573
887
  else {
574
- if (typeof sessionIdOrLogger === "string") {
575
- localSessionId = sessionIdOrLogger;
576
- logger = loggerOrUndefined;
577
- }
578
- else {
579
- localSessionId = createSessionId();
580
- logger = sessionIdOrLogger;
581
- }
888
+ // Called with writeVersion, logger?
889
+ localSessionId = createSessionId();
890
+ writeVersion = sessionIdOrDocumentVersion;
891
+ logger = writeVersionOrLogger;
582
892
  }
583
- const compressor = new IdCompressor(localSessionId, logger === undefined ? undefined : createChildLogger({ logger }));
893
+ const compressor = new IdCompressor(localSessionId, logger === undefined ? undefined : createChildLogger({ logger }), writeVersion);
584
894
  return compressor;
585
895
  }
586
- export function deserializeIdCompressor(serialized, sessionIdOrLogger, loggerOrUndefined) {
587
- if (typeof sessionIdOrLogger === "string") {
896
+ export function deserializeIdCompressor(serialized, sessionIdOrDocumentVersion, writeVersionOrLogger, loggerOrUndefined) {
897
+ if (typeof sessionIdOrDocumentVersion === "string") {
898
+ // Called with (serialized, sessionId, writeVersion, logger?)
588
899
  return IdCompressor.deserialize({
589
900
  serialized: serialized,
590
901
  logger: extractTelemetryLoggerExt(loggerOrUndefined),
591
- newSessionId: sessionIdOrLogger,
902
+ newSessionId: sessionIdOrDocumentVersion,
903
+ requestedWriteVersion: writeVersionOrLogger,
592
904
  });
593
905
  }
594
906
  assert(loggerOrUndefined === undefined, 0xc2d /* logger would be in sessionIdOrLogger in this codepath */);
907
+ // Called with (serialized, writeVersion, logger?)
595
908
  return IdCompressor.deserialize({
596
909
  serialized: serialized,
597
- logger: extractTelemetryLoggerExt(sessionIdOrLogger),
910
+ logger: extractTelemetryLoggerExt(writeVersionOrLogger),
911
+ requestedWriteVersion: sessionIdOrDocumentVersion,
598
912
  });
599
913
  }
914
+ /**
915
+ * Throws an error indicating the compressor has been disposed.
916
+ */
917
+ const throwDisposed = () => {
918
+ throw new Error("This compressor has been disposed via disposeShard() and can no longer be used.");
919
+ };
920
+ /**
921
+ * Makes an IdCompressor instance unusable by replacing all its methods with functions that throw.
922
+ * This is called once when disposeShard() is invoked to make the compressor permanently unusable.
923
+ * @param compressor - The compressor instance to make unusable
924
+ */
925
+ function makeCompressorUnusable(compressor) {
926
+ // Get all method names from the prototype (where class methods live)
927
+ const proto = Object.getPrototypeOf(compressor);
928
+ for (const key of Reflect.ownKeys(proto)) {
929
+ if (key !== "constructor" && typeof Reflect.get(proto, key) === "function") {
930
+ // Shadow the method only on this instance so other compressors remain usable.
931
+ // Omitting `enumerable` keeps new overrides non-enumerable, like class methods.
932
+ // Reflection supports string and symbol keys without adding an index signature.
933
+ Object.defineProperty(compressor, key, {
934
+ value: throwDisposed,
935
+ configurable: true,
936
+ writable: true,
937
+ });
938
+ }
939
+ }
940
+ }
600
941
  export function serializeIdCompressor(compressor, withSession) {
601
942
  const core = toIdCompressorWithCore(compressor);
602
943
  return withSession ? core.serialize(true) : core.serialize(false);