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