@cloudbitmaps/core 0.19.1 → 0.21.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 (81) hide show
  1. package/README.md +85 -0
  2. package/dist/{chunk-RSLVTRJY.js → chunk-IGLIZBR7.js} +982 -281
  3. package/dist/chunk-IGLIZBR7.js.map +7 -0
  4. package/dist/core/absent-operands.d.ts +8 -0
  5. package/dist/core/audit.d.ts +67 -4
  6. package/dist/core/bit-route.d.ts +2 -1
  7. package/dist/core/blob.d.ts +12 -1
  8. package/dist/core/bulk-load.d.ts +148 -0
  9. package/dist/core/cas-retry.d.ts +27 -0
  10. package/dist/core/check.d.ts +22 -0
  11. package/dist/core/chunk-checks.d.ts +6 -1
  12. package/dist/core/chunk-window.d.ts +11 -4
  13. package/dist/core/combine-expr.d.ts +1 -3
  14. package/dist/core/combine-feed.d.ts +2 -16
  15. package/dist/core/combine-many.d.ts +21 -15
  16. package/dist/core/concurrency.d.ts +8 -0
  17. package/dist/core/consistency.d.ts +76 -18
  18. package/dist/core/crbm/fingerprint.d.ts +23 -0
  19. package/dist/core/crbm/format.d.ts +2 -5
  20. package/dist/core/crbm/frame.d.ts +10 -0
  21. package/dist/core/crbm/plan-reads.d.ts +9 -3
  22. package/dist/core/crbm/reader.d.ts +51 -12
  23. package/dist/core/crbm/varint.d.ts +2 -1
  24. package/dist/core/crbm/writer.d.ts +5 -1
  25. package/dist/core/crbm-open.d.ts +154 -0
  26. package/dist/core/crbm-storage-source.d.ts +243 -490
  27. package/dist/core/crbm-write.d.ts +75 -0
  28. package/dist/core/crypto.d.ts +6 -1
  29. package/dist/core/engine.d.ts +239 -47
  30. package/dist/core/erase-id.d.ts +97 -27
  31. package/dist/core/erasure.d.ts +4 -3
  32. package/dist/core/errors.d.ts +8 -2
  33. package/dist/core/generation-gc.d.ts +131 -16
  34. package/dist/core/generation-list.d.ts +11 -0
  35. package/dist/core/id-window.d.ts +46 -0
  36. package/dist/core/invalidation-scope.d.ts +77 -0
  37. package/dist/core/known-generations-shape.d.ts +13 -0
  38. package/dist/core/known-generations.d.ts +147 -0
  39. package/dist/core/leases.d.ts +9 -1
  40. package/dist/core/ledger-seed.d.ts +17 -0
  41. package/dist/core/load.d.ts +17 -36
  42. package/dist/core/lru.d.ts +2 -15
  43. package/dist/core/metrics.d.ts +9 -7
  44. package/dist/core/order.d.ts +2 -0
  45. package/dist/core/pinned-storage-source.d.ts +7 -7
  46. package/dist/core/pointer-id.d.ts +19 -0
  47. package/dist/core/ports.d.ts +210 -45
  48. package/dist/core/publish.d.ts +207 -0
  49. package/dist/core/reader-defaults.d.ts +14 -3
  50. package/dist/core/registry-scan.d.ts +0 -2
  51. package/dist/core/retention-sweep.d.ts +69 -65
  52. package/dist/core/retention.d.ts +1 -1
  53. package/dist/core/retry.d.ts +12 -19
  54. package/dist/core/rollback.d.ts +62 -32
  55. package/dist/core/summary.d.ts +74 -25
  56. package/dist/core/token.d.ts +13 -8
  57. package/dist/core/validate.d.ts +2 -0
  58. package/dist/driver-kit.d.ts +6 -1
  59. package/dist/driver-kit.js +90 -10
  60. package/dist/driver-kit.js.map +3 -3
  61. package/dist/drivers/_shared/bytes.d.ts +6 -0
  62. package/dist/drivers/_shared/faults.d.ts +15 -0
  63. package/dist/drivers/_shared/object-keys.d.ts +18 -0
  64. package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
  65. package/dist/drivers/_shared/object-registry.d.ts +3 -1
  66. package/dist/drivers/_shared/options.d.ts +19 -0
  67. package/dist/drivers/_shared/registry.d.ts +31 -19
  68. package/dist/drivers/localfs/exact-case.d.ts +0 -3
  69. package/dist/drivers/localfs/fs-util.d.ts +22 -0
  70. package/dist/drivers/localfs/registry.d.ts +10 -4
  71. package/dist/drivers/localfs/storage.d.ts +16 -8
  72. package/dist/drivers/memory.d.ts +14 -9
  73. package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
  74. package/dist/export/index.d.ts +16 -0
  75. package/dist/index.d.ts +2 -5
  76. package/dist/index.js +4303 -2938
  77. package/dist/index.js.map +4 -4
  78. package/package.json +1 -1
  79. package/dist/chunk-RSLVTRJY.js.map +0 -7
  80. package/dist/core/cost.d.ts +0 -351
  81. package/dist/core/kept-generations.d.ts +0 -40
@@ -115,10 +115,126 @@ function isKeyUnavailableError(err) {
115
115
  return isCloudRoaringError(err) && err.name === "KeyUnavailableError";
116
116
  }
117
117
 
118
+ // src/core/bit-route.ts
119
+ var U32_MAX = 4294967295;
120
+ var CHUNK_COUNT = 65536;
121
+ var LOW_16 = 65535;
122
+ function splitId(id) {
123
+ if (!Number.isInteger(id) || id < 0 || id > U32_MAX) {
124
+ throw new ValidationError(`an id must be an integer from 0 to ${U32_MAX}`);
125
+ }
126
+ return { chunkKey: id >>> 16, remainder: id & LOW_16 };
127
+ }
128
+ var MAX_REMAINDER = CHUNK_COUNT - 1;
129
+ function joinId(chunkKey, remainder) {
130
+ return ((chunkKey & LOW_16) << 16 >>> 0) + (remainder & LOW_16);
131
+ }
132
+
133
+ // src/core/check.ts
134
+ function shown(value) {
135
+ if (typeof value === "string") return JSON.stringify(value);
136
+ if (typeof value === "bigint") return `${value}n`;
137
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
138
+ return value === null ? "null" : typeof value;
139
+ }
140
+ function rangeOf(min, max, note) {
141
+ let range = "";
142
+ if (min !== void 0 && max !== void 0) range = ` from ${min} to ${max}`;
143
+ else if (min !== void 0) range = ` of at least ${min}`;
144
+ else if (max !== void 0) range = ` of at most ${max}`;
145
+ return note === void 0 ? range : `${range} (${note})`;
146
+ }
147
+ function requireInteger(value, bounds) {
148
+ const { min, max, what, note } = bounds;
149
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || max !== void 0 && value > max) {
150
+ throw new ValidationError(
151
+ `${what} must be an integer${rangeOf(min, max, note)}; got ${shown(value)}`
152
+ );
153
+ }
154
+ return value;
155
+ }
156
+ function requirePositiveInteger(value, what) {
157
+ return requireInteger(value, { min: 1, what });
158
+ }
159
+ function requireFiniteNumber(value, bounds) {
160
+ const { min, max, what, note } = bounds;
161
+ if (typeof value !== "number" || !Number.isFinite(value) || min !== void 0 && value < min || max !== void 0 && value > max) {
162
+ throw new ValidationError(
163
+ `${what} must be a finite number${rangeOf(min, max, note)}; got ${shown(value)}`
164
+ );
165
+ }
166
+ return value;
167
+ }
168
+
169
+ // src/core/concurrency.ts
170
+ async function mapWithConcurrency(items, limit, fn) {
171
+ requirePositiveInteger(limit, "concurrency limit");
172
+ const results = new Array(items.length);
173
+ let next = 0;
174
+ let failed = false;
175
+ let firstError;
176
+ const worker = async () => {
177
+ while (!failed) {
178
+ const i = next++;
179
+ if (i >= items.length) return;
180
+ try {
181
+ results[i] = await fn(items[i], i);
182
+ } catch (err) {
183
+ if (!failed) {
184
+ failed = true;
185
+ firstError = err;
186
+ }
187
+ return;
188
+ }
189
+ }
190
+ };
191
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
192
+ await Promise.all(workers);
193
+ if (failed) throw firstError;
194
+ return results;
195
+ }
196
+ function boundedAll(items, limit, fn) {
197
+ try {
198
+ requirePositiveInteger(limit, "concurrency limit");
199
+ } catch (err) {
200
+ return Promise.reject(err);
201
+ }
202
+ if (items.length > limit) return inWaves(items, limit, fn);
203
+ const calls = [];
204
+ for (const item of items) {
205
+ try {
206
+ calls.push(fn(item));
207
+ } catch (err) {
208
+ void Promise.allSettled(calls);
209
+ return Promise.reject(err);
210
+ }
211
+ }
212
+ return Promise.all(calls);
213
+ }
214
+ async function inWaves(items, limit, fn) {
215
+ const results = new Array(items.length);
216
+ let next = 0;
217
+ let failed = false;
218
+ const worker = async () => {
219
+ while (!failed && next < items.length) {
220
+ const i = next++;
221
+ try {
222
+ results[i] = await fn(items[i]);
223
+ } catch (err) {
224
+ failed = true;
225
+ throw err;
226
+ }
227
+ }
228
+ };
229
+ await Promise.all(Array.from({ length: limit }, () => worker()));
230
+ return results;
231
+ }
232
+
118
233
  // src/core/name-codec.ts
119
234
  var KEY_SAFE = /[A-Za-z0-9._:-]/;
120
235
  var PATH_SAFE = /[A-Za-z0-9._-]/;
121
236
  var UTF8 = new TextEncoder();
237
+ var UTF8_LENIENT = new TextDecoder("utf-8", { fatal: false, ignoreBOM: true });
122
238
  function escapeChar(ch) {
123
239
  let out = "";
124
240
  for (const byte of UTF8.encode(ch)) {
@@ -136,17 +252,17 @@ function encodeWith(name, safe) {
136
252
  function decodePercent(encoded) {
137
253
  const bytes = [];
138
254
  for (let i = 0; i < encoded.length; ) {
139
- if (encoded[i] === "%" && i + 2 < encoded.length + 1) {
255
+ if (encoded[i] === "%" && i + 2 <= encoded.length) {
140
256
  const hex = encoded.slice(i + 1, i + 3);
141
257
  if (!/^[0-9A-Fa-f]{2}$/.test(hex)) return encoded;
142
258
  bytes.push(parseInt(hex, 16));
143
259
  i += 3;
144
260
  } else {
145
- for (const b of new TextEncoder().encode(encoded[i])) bytes.push(b);
261
+ for (const b of UTF8.encode(encoded[i])) bytes.push(b);
146
262
  i += 1;
147
263
  }
148
264
  }
149
- return new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }).decode(new Uint8Array(bytes));
265
+ return UTF8_LENIENT.decode(new Uint8Array(bytes));
150
266
  }
151
267
  function encodeNameForKey(name) {
152
268
  return defuseDotComponent(encodeWith(name, KEY_SAFE));
@@ -172,11 +288,243 @@ function decodeNameFromPath(encoded) {
172
288
  return decodePercent(encoded);
173
289
  }
174
290
 
291
+ // src/core/validate.ts
292
+ var CHUNK_KEY_MAX = 65535;
293
+ var MAX_NAME_LENGTH = 256;
294
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
295
+ function isWellFormedString(value) {
296
+ return !LONE_SURROGATE.test(value);
297
+ }
298
+ function encodedNameLength(value) {
299
+ return Math.max(encodeNameForKey(value).length, encodeNameForPath(value).length);
300
+ }
301
+ function validatePart(value, field) {
302
+ if (typeof value !== "string") {
303
+ throw new ValidationError(`${field} must be a string; got ${typeof value}`);
304
+ }
305
+ if (value.length === 0) {
306
+ throw new ValidationError(`${field} must not be empty`);
307
+ }
308
+ if (value.length > MAX_NAME_LENGTH) {
309
+ throw new ValidationError(
310
+ `${field} is too long: ${value.length} characters (limit ${MAX_NAME_LENGTH} once encoded for a storage key)`
311
+ );
312
+ }
313
+ if (LONE_SURROGATE.test(value)) {
314
+ throw new ValidationError(
315
+ `${field} contains an unpaired surrogate, so it has no UTF-8 encoding and cannot be stored. This is usually a string sliced through an astral character (an emoji, say) \u2014 slice by code point instead.`
316
+ );
317
+ }
318
+ const encoded = encodedNameLength(value);
319
+ if (encoded > MAX_NAME_LENGTH) {
320
+ throw new ValidationError(
321
+ `${field} is too long: ${encoded} characters once encoded for storage (limit ${MAX_NAME_LENGTH}). The name itself is ${value.length} characters \u2014 encoding expands anything outside [A-Za-z0-9._-], so a name of mostly non-ASCII text, or one full of colons, reaches the limit sooner than its length suggests.`
322
+ );
323
+ }
324
+ }
325
+ function validateSegmentRef(ref) {
326
+ validatePart(ref.segment, "segment");
327
+ if (ref.namespace !== void 0) validatePart(ref.namespace, "namespace");
328
+ }
329
+ var RESERVED_NAMESPACE_PREFIX = "cbm.due.";
330
+ function isReservedNamespace(namespace) {
331
+ return namespace !== void 0 && namespace.startsWith(RESERVED_NAMESPACE_PREFIX);
332
+ }
333
+ function validateUserNamespace(namespace) {
334
+ validatePart(namespace, "namespace");
335
+ if (isReservedNamespace(namespace)) {
336
+ throw new ValidationError(
337
+ `namespace "${namespace}" is reserved: names starting with "${RESERVED_NAMESPACE_PREFIX}" hold the library's own bookkeeping rows, which every fleet-wide scan skips, so a segment there would be invisible to an erasure, a consistency check, an export and a retention sweep. Choose another namespace.`
338
+ );
339
+ }
340
+ }
341
+ function validateUserRef(ref) {
342
+ validatePart(ref.segment, "segment");
343
+ if (ref.namespace !== void 0) validateUserNamespace(ref.namespace);
344
+ }
345
+ function validateChunkKeyOrder(keys) {
346
+ for (let i = 1; i < keys.length; i++) {
347
+ if (!(keys[i] >= keys[i - 1]))
348
+ throw new ValidationError("chunk keys must be in ascending order");
349
+ }
350
+ }
351
+ function validateChunkRef(ref) {
352
+ validateSegmentRef(ref);
353
+ validateChunkKey(ref.chunkKey);
354
+ }
355
+ function validateChunkKey(chunkKey) {
356
+ if (!(typeof chunkKey === "number" && chunkKey >= 0 && chunkKey <= CHUNK_KEY_MAX && (chunkKey | 0) === chunkKey)) {
357
+ throw new ValidationError(`chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]`);
358
+ }
359
+ }
360
+
361
+ // src/core/ports.ts
362
+ function assertRegistryCanWrite(registry, what) {
363
+ if (registry.capabilities().canWrite === false) {
364
+ throw new UnsupportedError(
365
+ `${what}: the registry reports it cannot write a row in this runtime (capabilities().canWrite is false), so nothing is written`
366
+ );
367
+ }
368
+ }
369
+ var STORAGE_BACKEND_BRAND = /* @__PURE__ */ Symbol.for("cloudbitmaps.storage-backend");
370
+ var STORAGE_BACKEND = STORAGE_BACKEND_BRAND;
371
+ function brandAsBackend(target) {
372
+ if (target === null || typeof target !== "object") {
373
+ throw new ValidationError("brandAsBackend needs `{ storage, registry }`");
374
+ }
375
+ const { storage, registry } = target;
376
+ if (storage === null || typeof storage !== "object" || typeof storage.putImmutable !== "function") {
377
+ throw new ValidationError(
378
+ "brandAsBackend: `storage` must be an IStorageDriver (it has no `putImmutable`)"
379
+ );
380
+ }
381
+ if (registry === null || typeof registry !== "object" || typeof registry.compareAndSwap !== "function") {
382
+ throw new ValidationError(
383
+ "brandAsBackend: `registry` must be an IRegistryDriver (it has no `compareAndSwap`)"
384
+ );
385
+ }
386
+ try {
387
+ Object.defineProperty(target, STORAGE_BACKEND_BRAND, {
388
+ value: true,
389
+ enumerable: false,
390
+ writable: false,
391
+ configurable: false
392
+ });
393
+ } catch {
394
+ throw new ValidationError(
395
+ "brandAsBackend: cannot brand a frozen or non-extensible object \u2014 pass a plain object or a class instance"
396
+ );
397
+ }
398
+ return target;
399
+ }
400
+ function isStorageBackend(value) {
401
+ return typeof value === "object" && value !== null && value[STORAGE_BACKEND_BRAND] === true;
402
+ }
403
+
404
+ // src/core/pointer-id.ts
405
+ var RESOLVED_FIELDS = [
406
+ "currentGen",
407
+ "status",
408
+ "wrappedDeks",
409
+ "keyId",
410
+ "summary"
411
+ ];
412
+ function renewsPointer(patch) {
413
+ return RESOLVED_FIELDS.some((field) => field in patch);
414
+ }
415
+ function renewPointer(row) {
416
+ return { currentGen: row.currentGen };
417
+ }
418
+ function pointerIdOf(row) {
419
+ const pointerId = row.pointerId;
420
+ if (typeof pointerId !== "string" || pointerId.length === 0) {
421
+ throw new UnsupportedError(
422
+ `the registry returned the row of "${row.segment}" with no pointerId: a registry must set it on every row (the token of the write that last named currentGen, status, wrappedDeks, keyId or summary; see renewsPointer in @cloudbitmaps/core/driver-kit)`
423
+ );
424
+ }
425
+ return pointerId;
426
+ }
427
+
428
+ // src/core/crypto.ts
429
+ var UTF82 = new TextEncoder();
430
+ var AAD_VERSION = 1;
431
+ var SCOPE_CHUNK = 0;
432
+ var SCOPE_INDEX = 1;
433
+ var SCOPE_METADATA = 2;
434
+ var SCOPE_SUMMARY = 3;
435
+ function aadFor(ref, generation, scope) {
436
+ const ns = UTF82.encode(ref.namespace ?? "");
437
+ const seg = UTF82.encode(ref.segment);
438
+ const out = new Uint8Array(1 + 2 + ns.length + 2 + seg.length + 8 + 1 + 4);
439
+ const view = new DataView(out.buffer);
440
+ let o = 0;
441
+ out[o] = AAD_VERSION;
442
+ o += 1;
443
+ view.setUint16(o, ns.length, true);
444
+ o += 2;
445
+ out.set(ns, o);
446
+ o += ns.length;
447
+ view.setUint16(o, seg.length, true);
448
+ o += 2;
449
+ out.set(seg, o);
450
+ o += seg.length;
451
+ view.setBigUint64(o, BigInt(generation), true);
452
+ o += 8;
453
+ out[o] = scope === "index" ? SCOPE_INDEX : scope === "metadata" ? SCOPE_METADATA : scope === "summary" ? SCOPE_SUMMARY : SCOPE_CHUNK;
454
+ o += 1;
455
+ view.setUint32(o, typeof scope === "number" ? scope : 0, true);
456
+ return out;
457
+ }
458
+ var sameKeys = (a, b) => a !== void 0 && JSON.stringify(a) === JSON.stringify(b);
459
+
460
+ // src/core/retry.ts
461
+ var DEFAULT_RETRY_POLICY = {
462
+ maxAttempts: 4,
463
+ baseDelayMs: 50,
464
+ maxDelayMs: 2e3,
465
+ backoffFactor: 2,
466
+ jitter: "full"
467
+ };
468
+ function backoffDelayMs(policy, attempt) {
469
+ const raw = policy.baseDelayMs * policy.backoffFactor ** (attempt - 1);
470
+ return Math.min(policy.maxDelayMs, raw);
471
+ }
472
+ function applyJitter(policy, delayMs, rng) {
473
+ if (policy.jitter === "none") return delayMs;
474
+ return rng.next() * delayMs;
475
+ }
476
+ function jitteredWait(clock, rng, boundMs) {
477
+ const sleep = clock?.sleep;
478
+ return sleep === void 0 ? Promise.resolve() : sleep.call(clock, Math.floor((rng?.next() ?? 1) * boundMs));
479
+ }
480
+ function retryRead(op, retry) {
481
+ return retry === void 0 ? op() : withRetry(op, retry.policy ?? DEFAULT_RETRY_POLICY, retry);
482
+ }
483
+ function checkedRetryPolicy(policy) {
484
+ const { maxAttempts, baseDelayMs, maxDelayMs, backoffFactor, jitter } = policy;
485
+ requireInteger(maxAttempts, { min: 1, what: "retry.maxAttempts" });
486
+ requireFiniteNumber(baseDelayMs, { min: 0, what: "retry.baseDelayMs" });
487
+ requireFiniteNumber(maxDelayMs, {
488
+ min: baseDelayMs,
489
+ what: "retry.maxDelayMs",
490
+ note: "retry.baseDelayMs"
491
+ });
492
+ requireFiniteNumber(backoffFactor, { min: 1, what: "retry.backoffFactor" });
493
+ if (jitter !== "full" && jitter !== "none") {
494
+ throw new ValidationError(`retry.jitter must be 'full' or 'none'`);
495
+ }
496
+ return policy;
497
+ }
498
+ async function withRetry(op, policy, deps) {
499
+ const retryable = deps.isRetryable ?? isTransientError;
500
+ const attempts = requireInteger(policy.maxAttempts, { min: 1, what: "retry.maxAttempts" });
501
+ let lastErr;
502
+ for (let attempt = 1; attempt <= attempts; attempt++) {
503
+ try {
504
+ return await op();
505
+ } catch (err) {
506
+ lastErr = err;
507
+ if (attempt >= attempts || !retryable(err)) throw err;
508
+ const delayMs = applyJitter(policy, backoffDelayMs(policy, attempt), deps.rng);
509
+ try {
510
+ deps.onRetry?.({ attempt, delayMs, err });
511
+ } catch {
512
+ }
513
+ await deps.clock.sleep(delayMs);
514
+ }
515
+ }
516
+ throw lastErr;
517
+ }
518
+
175
519
  // src/core/token.ts
176
520
  var INCARNATION_TOKEN = /^([0-9a-f]{32})\.(0|[1-9]\d*)\.([0-9a-f]{16})$/;
177
521
  function incarnationOf(token) {
178
522
  return INCARNATION_TOKEN.exec(token)?.[1];
179
523
  }
524
+ function incarnationField(token) {
525
+ const incarnation = token === void 0 ? void 0 : incarnationOf(token);
526
+ return incarnation === void 0 ? {} : { incarnation };
527
+ }
180
528
  function sameIncarnation(a, b) {
181
529
  const ia = incarnationOf(a.token);
182
530
  const ib = incarnationOf(b.token);
@@ -235,10 +583,7 @@ function leaseChurn(deps) {
235
583
  if (used >= LEASE_ONLY_RETRIES || !onlyLeasesDiffer(held, now, alsoIgnore)) return void 0;
236
584
  const bound = Math.min(CONFLICT_BASE_MS * 2 ** Math.min(used, 4), CONFLICT_CAP_MS);
237
585
  used += 1;
238
- const sleep = deps.clock?.sleep;
239
- if (sleep !== void 0) {
240
- await sleep.call(deps.clock, Math.floor((deps.rng?.next() ?? 1) * bound));
241
- }
586
+ await jitteredWait(deps.clock, deps.rng, bound);
242
587
  return { row: await reread() };
243
588
  }
244
589
  };
@@ -249,7 +594,7 @@ function listOf(entries) {
249
594
  (a, b) => a.generation !== b.generation ? a.generation - b.generation : a.holder < b.holder ? -1 : a.holder > b.holder ? 1 : 0
250
595
  );
251
596
  }
252
- var wait = (deps, bound) => deps.clock.sleep(Math.floor((deps.rng?.next() ?? 1) * bound));
597
+ var wait = (deps, bound) => jitteredWait(deps.clock, deps.rng, bound);
253
598
  var heldBy = (row, take) => (row?.leases ?? []).some(
254
599
  (e) => e.holder === take.holder && e.generation === take.generation && e.until === take.until
255
600
  );
@@ -349,180 +694,63 @@ async function releaseLease(ref, deps, holder) {
349
694
  throw new WriteConflictError(`lease: contention releasing a lease on segment "${ref.segment}"`);
350
695
  }
351
696
 
352
- // src/core/validate.ts
353
- var CHUNK_KEY_MAX = 65535;
354
- var MAX_NAME_LENGTH = 256;
355
- var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
356
- function isWellFormedString(value) {
357
- return !LONE_SURROGATE.test(value);
697
+ // src/drivers/_shared/keys.ts
698
+ var DEFAULT_NAMESPACE = "_default";
699
+ function namespaceKeyPart(namespace) {
700
+ return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForKey(namespace);
358
701
  }
359
- function encodedNameLength(value) {
360
- return Math.max(encodeNameForKey(value).length, encodeNameForPath(value).length);
702
+ function namespacePathPart(namespace) {
703
+ return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);
361
704
  }
362
- function validatePart(value, field) {
363
- if (typeof value !== "string") {
364
- throw new ValidationError(`${field} must be a string; got ${typeof value}`);
365
- }
366
- if (value.length === 0) {
367
- throw new ValidationError(`${field} must not be empty`);
368
- }
369
- if (value.length > MAX_NAME_LENGTH) {
370
- throw new ValidationError(
371
- `${field} is too long: ${value.length} characters (limit ${MAX_NAME_LENGTH} once encoded for a storage key)`
372
- );
373
- }
374
- if (LONE_SURROGATE.test(value)) {
375
- throw new ValidationError(
376
- `${field} contains an unpaired surrogate, so it has no UTF-8 encoding and cannot be stored. This is usually a string sliced through an astral character (an emoji, say) \u2014 slice by code point instead.`
377
- );
378
- }
379
- const encoded = encodedNameLength(value);
380
- if (encoded > MAX_NAME_LENGTH) {
705
+
706
+ // src/drivers/_shared/options.ts
707
+ function refuseUnknownOptions(name, options, keys, hint) {
708
+ if (options === null || typeof options !== "object") {
381
709
  throw new ValidationError(
382
- `${field} is too long: ${encoded} characters once encoded for storage (limit ${MAX_NAME_LENGTH}). The name itself is ${value.length} characters \u2014 encoding expands anything outside [A-Za-z0-9._-], so a name of mostly non-ASCII text, or one full of colons, reaches the limit sooner than its length suggests.`
710
+ `${name} needs an options object \u2014 got ${options === null ? "null" : typeof options}`
383
711
  );
384
712
  }
385
- }
386
- function validateSegmentRef(ref) {
387
- validatePart(ref.segment, "segment");
388
- if (ref.namespace !== void 0) validatePart(ref.namespace, "namespace");
389
- }
390
- var RESERVED_NAMESPACE_PREFIX = "cbm.due.";
391
- function isReservedNamespace(namespace) {
392
- return namespace !== void 0 && namespace.startsWith(RESERVED_NAMESPACE_PREFIX);
393
- }
394
- function validateUserNamespace(namespace) {
395
- validatePart(namespace, "namespace");
396
- if (isReservedNamespace(namespace)) {
713
+ const unknown = Object.keys(options).filter((k) => !keys.includes(k));
714
+ if (unknown.length > 0) {
715
+ const list = (ks) => ks.map((k) => `\`${k}\``).join(", ");
397
716
  throw new ValidationError(
398
- `namespace "${namespace}" is reserved: names starting with "${RESERVED_NAMESPACE_PREFIX}" hold the library's own bookkeeping rows, which every fleet-wide scan skips, so a segment there would be invisible to an erasure, a consistency check, an export and a retention sweep. Choose another namespace.`
717
+ `${name} does not take ${list(unknown)}. It takes ${list(keys)}${hint === void 0 ? "" : `; ${hint}`}.`
399
718
  );
400
719
  }
401
720
  }
402
- function validateUserRef(ref) {
403
- validatePart(ref.segment, "segment");
404
- if (ref.namespace !== void 0) validateUserNamespace(ref.namespace);
405
- }
406
- function validateChunkKeyOrder(keys) {
407
- for (let i = 1; i < keys.length; i++) {
408
- if (!(keys[i] >= keys[i - 1]))
409
- throw new ValidationError("chunk keys must be in ascending order");
410
- }
411
- }
412
- function validateChunkRef(ref) {
413
- validateSegmentRef(ref);
414
- if (!Number.isInteger(ref.chunkKey) || ref.chunkKey < 0 || ref.chunkKey > CHUNK_KEY_MAX) {
415
- throw new ValidationError(`chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]`);
721
+ function checkNow(name, now) {
722
+ if (now !== void 0 && typeof now !== "function") {
723
+ throw new ValidationError(`${name}: \`now\` must be a function returning epoch milliseconds`);
416
724
  }
417
725
  }
418
-
419
- // src/core/concurrency.ts
420
- async function mapWithConcurrency(items, limit, fn) {
421
- if (!Number.isInteger(limit) || limit < 1) {
422
- throw new ValidationError(`concurrency limit must be a positive integer; got ${limit}`);
423
- }
424
- const results = new Array(items.length);
425
- let next = 0;
426
- let failed = false;
427
- let firstError;
428
- const worker = async () => {
429
- while (!failed) {
430
- const i = next++;
431
- if (i >= items.length) return;
432
- try {
433
- results[i] = await fn(items[i], i);
434
- } catch (err) {
435
- if (!failed) {
436
- failed = true;
437
- firstError = err;
438
- }
439
- return;
440
- }
441
- }
442
- };
443
- const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
444
- await Promise.all(workers);
445
- if (failed) throw firstError;
446
- return results;
726
+ function requirePositiveInt(name, value) {
727
+ return value === void 0 ? void 0 : requirePositiveInteger(value, name);
447
728
  }
448
-
449
- // src/core/ports.ts
450
- function assertRegistryCanWrite(registry, what) {
451
- if (registry.capabilities().canWrite === false) {
452
- throw new UnsupportedError(
453
- `${what}: the registry reports it cannot write a row in this runtime (capabilities().canWrite is false), so nothing is written`
454
- );
729
+ function requireBool(name, value) {
730
+ if (value === void 0) return void 0;
731
+ if (typeof value !== "boolean") {
732
+ throw new ValidationError(`${name} must be a boolean; got ${shown2(value)}`);
455
733
  }
734
+ return value;
456
735
  }
457
- var STORAGE_BACKEND_BRAND = /* @__PURE__ */ Symbol.for("cloudbitmaps.storage-backend");
458
- var STORAGE_BACKEND = STORAGE_BACKEND_BRAND;
459
- function brandAsBackend(target) {
460
- if (target === null || typeof target !== "object") {
461
- throw new ValidationError("brandAsBackend needs `{ storage, registry }`");
736
+ function shown2(value) {
737
+ if (typeof value === "string") return JSON.stringify(value);
738
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
739
+ return String(value);
462
740
  }
463
- const { storage, registry } = target;
464
- if (storage === null || typeof storage !== "object" || typeof storage.putImmutable !== "function") {
465
- throw new ValidationError(
466
- "brandAsBackend: `storage` must be an IStorageDriver (it has no `putImmutable`)"
467
- );
468
- }
469
- if (registry === null || typeof registry !== "object" || typeof registry.compareAndSwap !== "function") {
470
- throw new ValidationError(
471
- "brandAsBackend: `registry` must be an IRegistryDriver (it has no `compareAndSwap`)"
472
- );
473
- }
474
- try {
475
- Object.defineProperty(target, STORAGE_BACKEND_BRAND, {
476
- value: true,
477
- enumerable: false,
478
- writable: false,
479
- configurable: false
480
- });
481
- } catch {
482
- throw new ValidationError(
483
- "brandAsBackend: cannot brand a frozen or non-extensible object \u2014 pass a plain object or a class instance"
484
- );
485
- }
486
- return target;
487
- }
488
- function isStorageBackend(value) {
489
- return typeof value === "object" && value !== null && value[STORAGE_BACKEND_BRAND] === true;
490
- }
491
-
492
- // src/drivers/_shared/keys.ts
493
- var DEFAULT_NAMESPACE = "_default";
494
- function namespaceKeyPart(namespace) {
495
- return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForKey(namespace);
496
- }
497
- function namespacePathPart(namespace) {
498
- return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);
741
+ return value === null ? "null" : typeof value;
499
742
  }
500
743
 
501
- // src/core/kept-generations.ts
502
- var MAX_KEPT_GENERATIONS = 64;
503
- var MAX_STORED_KEPT_GENERATIONS = 256;
504
- function usableKeptGens(record) {
505
- const list = record?.keptGens;
506
- if (record === null || list === void 0 || record.currentGen === null) return void 0;
507
- for (const g of list) if (g >= record.currentGen) return void 0;
508
- return list;
509
- }
510
- function nextKept(prev, generation, keep) {
511
- const none = { list: void 0, evict: [] };
512
- if (keep === void 0 || keep > MAX_KEPT_GENERATIONS) return none;
513
- if (generation === 0) return { list: [], evict: [] };
514
- const previous = prev?.currentGen ?? null;
515
- const known = usableKeptGens(prev);
516
- if (previous === null || known === void 0) return none;
517
- const all = [...known, previous];
518
- const cut = Math.max(0, all.length - keep);
519
- return { list: all.slice(cut), evict: all.slice(0, cut) };
520
- }
744
+ // src/core/crbm/frame.ts
745
+ var PREAMBLE_BYTES = 8;
746
+ var FOOTER_BYTES = 104;
521
747
 
522
748
  // src/core/metadata.ts
523
749
  var MAX_METADATA_BYTES = 1024;
524
750
  var MAX_METADATA_KEY_BYTES = 128;
525
- var utf8Length = (s) => new TextEncoder().encode(s).length;
751
+ var UTF83 = new TextEncoder();
752
+ var UTF8_STRICT = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
753
+ var utf8Length = (s) => UTF83.encode(s).length;
526
754
  function canonicalMetadataJson(value, fail) {
527
755
  if (value === null || typeof value !== "object") {
528
756
  fail(`metadata must be a plain object (got ${value === null ? "null" : typeof value})`);
@@ -569,7 +797,7 @@ function canonicalMetadataJson(value, fail) {
569
797
  function metadataBytes(value, fail) {
570
798
  if (value === void 0) return void 0;
571
799
  const json = canonicalMetadataJson(value, fail);
572
- return json === "{}" ? void 0 : new TextEncoder().encode(json);
800
+ return json === "{}" ? void 0 : UTF83.encode(json);
573
801
  }
574
802
  function copiedMetadata(value, fail) {
575
803
  const bytes = metadataBytes(value, fail);
@@ -581,7 +809,7 @@ function metadataFromBytes(bytes, fail) {
581
809
  }
582
810
  let text;
583
811
  try {
584
- text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
812
+ text = UTF8_STRICT.decode(bytes);
585
813
  } catch {
586
814
  fail("metadata is not valid UTF-8");
587
815
  }
@@ -597,9 +825,384 @@ function metadataFromBytes(bytes, fail) {
597
825
  return Object.freeze(parsed);
598
826
  }
599
827
 
828
+ // src/core/crbm/fingerprint.ts
829
+ var sizePart = (size) => `${size}:`;
830
+ var fingerprintFor = (size, footerCrc) => `${sizePart(size)}${footerCrc}`;
831
+ var FINGERPRINT = /^(0|[1-9]\d*):(0|[1-9]\d*)$/;
832
+ function fingerprintParts(fingerprint) {
833
+ const match = typeof fingerprint === "string" ? FINGERPRINT.exec(fingerprint) : null;
834
+ if (match === null) return void 0;
835
+ const size = Number(match[1]);
836
+ const footerCrc = Number(match[2]);
837
+ if (!Number.isSafeInteger(size) || size < PREAMBLE_BYTES + FOOTER_BYTES) return void 0;
838
+ if (footerCrc > 4294967295) return void 0;
839
+ return { size, footerCrc };
840
+ }
841
+ function footerCrcOf(fingerprint) {
842
+ const parts = fingerprintParts(fingerprint);
843
+ if (parts === void 0) {
844
+ throw new IntegrityError(`not an object's fingerprint: ${String(fingerprint).slice(0, 64)}`);
845
+ }
846
+ return parts.footerCrc;
847
+ }
848
+ function compactFingerprint(fingerprint) {
849
+ const colon = fingerprint.indexOf(":");
850
+ const size = Number(fingerprint.slice(0, colon));
851
+ const crc = Number(fingerprint.slice(colon + 1));
852
+ return `${size.toString(36)}.${crc.toString(36)}`;
853
+ }
854
+
855
+ // src/core/summary.ts
856
+ var NONCE_BYTES = 12;
857
+ var TAG_BYTES = 16;
858
+ var COUNT_BYTES = 8;
859
+ var SIZE_BYTES = 8;
860
+ var CRC_BYTES = 4;
861
+ var TAIL_BYTES = 4;
862
+ var FIXED_BYTES = COUNT_BYTES + SIZE_BYTES + CRC_BYTES + TAIL_BYTES;
863
+ var MAX_SUMMARY_CARDINALITY = U32_MAX + 1;
864
+ var MAX_TAIL_BYTES = 8392820;
865
+ function tailBytesFit(tailBytes, size) {
866
+ return typeof tailBytes === "number" && Number.isInteger(tailBytes) && tailBytes >= FOOTER_BYTES && tailBytes <= size - PREAMBLE_BYTES && tailBytes <= MAX_TAIL_BYTES;
867
+ }
868
+ function sizeOfFingerprint(fingerprint) {
869
+ const parts = fingerprintParts(fingerprint);
870
+ if (parts === void 0)
871
+ throw new IntegrityError("registry record summary: a malformed fingerprint");
872
+ return parts.size;
873
+ }
874
+ var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
875
+ function toBase64(bytes) {
876
+ let out = "";
877
+ for (let i = 0; i < bytes.length; i += 3) {
878
+ const a = bytes[i];
879
+ const b = bytes[i + 1];
880
+ const c = bytes[i + 2];
881
+ out += BASE64[a >> 2] + BASE64[(a & 3) << 4 | (b ?? 0) >> 4];
882
+ out += b === void 0 ? "=" : BASE64[(b & 15) << 2 | (c ?? 0) >> 6];
883
+ out += c === void 0 ? "=" : BASE64[c & 63];
884
+ }
885
+ return out;
886
+ }
887
+ function fromBase64(text) {
888
+ if (text.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(text)) return void 0;
889
+ const pad = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
890
+ const out = new Uint8Array(text.length / 4 * 3 - pad);
891
+ let o = 0;
892
+ for (let i = 0; i < text.length; i += 4) {
893
+ const n = BASE64.indexOf(text[i]) << 18 | BASE64.indexOf(text[i + 1]) << 12 | Math.max(BASE64.indexOf(text[i + 2]), 0) << 6 | Math.max(BASE64.indexOf(text[i + 3]), 0);
894
+ if (o < out.length) out[o++] = n >> 16 & 255;
895
+ if (o < out.length) out[o++] = n >> 8 & 255;
896
+ if (o < out.length) out[o++] = n & 255;
897
+ }
898
+ return out;
899
+ }
900
+ function clearSummary(generation, cardinality, fingerprint, tailBytes, metadata) {
901
+ return metadata === void 0 || Object.keys(metadata).length === 0 ? { generation, cardinality, fingerprint, tailBytes } : { generation, cardinality, fingerprint, tailBytes, metadata };
902
+ }
903
+ function sealSummary(aead, ref, generation, cardinality, fingerprint, tailBytes, metadata) {
904
+ const meta = metadataBytes(metadata, (message) => {
905
+ throw new IntegrityError(`summary metadata: ${message}`);
906
+ });
907
+ const parts = fingerprintParts(fingerprint);
908
+ if (parts === void 0) throw new IntegrityError("summary: a malformed fingerprint");
909
+ const plain = new Uint8Array(FIXED_BYTES + (meta?.length ?? 0));
910
+ const view = new DataView(plain.buffer);
911
+ view.setBigUint64(0, BigInt(cardinality), true);
912
+ view.setBigUint64(COUNT_BYTES, BigInt(parts.size), true);
913
+ view.setUint32(COUNT_BYTES + SIZE_BYTES, parts.footerCrc, true);
914
+ view.setUint32(COUNT_BYTES + SIZE_BYTES + CRC_BYTES, tailBytes, true);
915
+ if (meta !== void 0) plain.set(meta, FIXED_BYTES);
916
+ const { nonce, ciphertext, tag } = aead.seal(plain, aadFor(ref, generation, "summary"));
917
+ const framed = new Uint8Array(nonce.length + ciphertext.length + tag.length);
918
+ framed.set(nonce, 0);
919
+ framed.set(ciphertext, nonce.length);
920
+ framed.set(tag, nonce.length + ciphertext.length);
921
+ return { generation, sealed: toBase64(framed) };
922
+ }
923
+ function openSummary(aead, ref, summary) {
924
+ const fail = (message) => {
925
+ throw new IntegrityError(`registry record summary: ${message}`);
926
+ };
927
+ const framed = fromBase64(summary.sealed);
928
+ const framing = NONCE_BYTES + TAG_BYTES;
929
+ if (framed === void 0 || framed.length < framing + FIXED_BYTES || framed.length > framing + FIXED_BYTES + MAX_METADATA_BYTES) {
930
+ fail(
931
+ "the sealed summary is not a nonce, a count, a fingerprint, a tail size, up to 1 KiB of metadata and a tag"
932
+ );
933
+ }
934
+ const bytes = framed;
935
+ let plain;
936
+ try {
937
+ plain = aead.open(
938
+ {
939
+ nonce: bytes.subarray(0, NONCE_BYTES),
940
+ ciphertext: bytes.subarray(NONCE_BYTES, bytes.length - TAG_BYTES),
941
+ tag: bytes.subarray(bytes.length - TAG_BYTES)
942
+ },
943
+ aadFor(ref, summary.generation, "summary")
944
+ );
945
+ } catch (err) {
946
+ if (!isIntegrityError(err)) throw err;
947
+ return fail(
948
+ "the sealed summary does not open: tampered bytes, or a summary moved from another generation"
949
+ );
950
+ }
951
+ if (plain.length < FIXED_BYTES)
952
+ fail("the sealed summary is shorter than its count, fingerprint and tail size");
953
+ const view = new DataView(plain.buffer, plain.byteOffset, plain.byteLength);
954
+ const count = view.getBigUint64(0, true);
955
+ if (count > BigInt(MAX_SUMMARY_CARDINALITY)) fail(`cardinality ${count} is outside 0 to 2^32`);
956
+ const size = view.getBigUint64(COUNT_BYTES, true);
957
+ const fingerprint = fingerprintFor(Number(size), view.getUint32(COUNT_BYTES + SIZE_BYTES, true));
958
+ if (size > BigInt(Number.MAX_SAFE_INTEGER) || fingerprintParts(fingerprint) === void 0) {
959
+ fail(`the object size ${size} is not one a .crbm object can have`);
960
+ }
961
+ const tailBytes = view.getUint32(COUNT_BYTES + SIZE_BYTES + CRC_BYTES, true);
962
+ if (!tailBytesFit(tailBytes, Number(size))) {
963
+ fail(`the tail size ${tailBytes} is not one a .crbm object of ${size} bytes can have`);
964
+ }
965
+ const metadata = plain.length === FIXED_BYTES ? void 0 : metadataFromBytes(plain.subarray(FIXED_BYTES), fail);
966
+ return { cardinality: Number(count), metadata, fingerprint, tailBytes };
967
+ }
968
+ function usableSummary(ref, row, aead) {
969
+ const summary = row.summary;
970
+ if (summary === void 0 || row.status !== "active" || summary.generation !== row.currentGen) {
971
+ return void 0;
972
+ }
973
+ const keyed = row.wrappedDeks !== void 0 && row.wrappedDeks.length > 0;
974
+ if ("sealed" in summary) {
975
+ if (!keyed || aead === void 0) return void 0;
976
+ try {
977
+ return openSummary(aead, ref, summary);
978
+ } catch (err) {
979
+ if (isIntegrityError(err)) return void 0;
980
+ throw err;
981
+ }
982
+ }
983
+ if (keyed) return void 0;
984
+ const parts = fingerprintParts(summary.fingerprint);
985
+ if (parts === void 0 || !tailBytesFit(summary.tailBytes, parts.size)) return void 0;
986
+ return {
987
+ cardinality: summary.cardinality,
988
+ metadata: summary.metadata,
989
+ fingerprint: summary.fingerprint,
990
+ tailBytes: summary.tailBytes
991
+ };
992
+ }
993
+ function canonicalOrNone(metadata) {
994
+ if (metadata === void 0) return void 0;
995
+ const json = canonicalMetadataJson(metadata, (message) => {
996
+ throw new IntegrityError(`metadata: ${message}`);
997
+ });
998
+ return json === "{}" ? void 0 : json;
999
+ }
1000
+ function summaryAgrees(summary, object) {
1001
+ return summary.cardinality === object.cardinality && canonicalOrNone(summary.metadata) === canonicalOrNone(object.metadata) && (summary.tailBytes === void 0 || object.tailBytes === void 0 || summary.tailBytes === object.tailBytes);
1002
+ }
1003
+ function metadataToCarry(object, authenticated) {
1004
+ const empty = (metadata) => metadata === void 0 || Object.keys(metadata).length === 0;
1005
+ return empty(object) && authenticated !== void 0 && !empty(authenticated.metadata) ? authenticated.metadata : object;
1006
+ }
1007
+ function summaryOf(ref, generation, description, aead) {
1008
+ const { cardinality, fingerprint, tailBytes, metadata } = description;
1009
+ if (tailBytes > MAX_TAIL_BYTES) return void 0;
1010
+ return aead === void 0 ? clearSummary(generation, cardinality, fingerprint, tailBytes, metadata) : sealSummary(aead, ref, generation, cardinality, fingerprint, tailBytes, metadata);
1011
+ }
1012
+ function ownSummary(ref, generation, description, aead) {
1013
+ const summary = summaryOf(ref, generation, description, aead);
1014
+ if (summary === void 0) {
1015
+ throw new IntegrityError(
1016
+ `the object written for generation ${generation} of "${ref.segment}" has a tail of ${description.tailBytes} bytes, above the ${MAX_TAIL_BYTES} a summary records`
1017
+ );
1018
+ }
1019
+ return summary;
1020
+ }
1021
+
1022
+ // src/core/known-generations-shape.ts
1023
+ var ENTRY_KEYS = ["generation", "footerCrc"];
1024
+ function knownGensShapeFault(value, max, show = String) {
1025
+ if (value === void 0) return void 0;
1026
+ if (!Array.isArray(value)) return "must be an array";
1027
+ const list = value;
1028
+ if (list.length > max) return `has ${list.length} entries, cap ${max}`;
1029
+ const seen = /* @__PURE__ */ new Set();
1030
+ for (const e of list) {
1031
+ if (typeof e !== "object" || e === null || Array.isArray(e))
1032
+ return "an entry must be an object";
1033
+ const entry = e;
1034
+ const extra = Object.keys(entry).filter((k) => !ENTRY_KEYS.includes(k));
1035
+ if (extra.length > 0) {
1036
+ return `an entry carries an undeclared field (${show(extra.join(", "))})`;
1037
+ }
1038
+ const { generation, footerCrc } = entry;
1039
+ if (!Number.isSafeInteger(generation) || generation < 0) {
1040
+ return `a generation must be a non-negative safe integer (got ${show(generation)})`;
1041
+ }
1042
+ if (!Number.isInteger(footerCrc) || footerCrc < 0 || footerCrc > 4294967295) {
1043
+ return `a footerCrc must be an integer from 0 to 2^32 - 1 (got ${show(footerCrc)})`;
1044
+ }
1045
+ if (seen.has(generation)) return `generation ${String(generation)} is repeated`;
1046
+ seen.add(generation);
1047
+ }
1048
+ return void 0;
1049
+ }
1050
+
1051
+ // src/core/order.ts
1052
+ var ascending = (a, b) => a - b;
1053
+
1054
+ // src/core/known-generations.ts
1055
+ var MAX_KEPT_GENERATIONS = 64;
1056
+ var KNOWN_FILL = 128;
1057
+ var MAX_KNOWN_GENERATIONS = 192;
1058
+ var MAX_STORED_KNOWN_GENERATIONS = 384;
1059
+ function usableKnownGens(record) {
1060
+ const known = record?.knownGens;
1061
+ if (record === null || known === void 0 || known.length === 0 || record.status !== "active" || record.currentGen === null) {
1062
+ return void 0;
1063
+ }
1064
+ const last = known[known.length - 1];
1065
+ if (last.generation !== record.currentGen) return void 0;
1066
+ const summary = record.summary;
1067
+ if (summary !== void 0 && summary.generation === record.currentGen && "fingerprint" in summary && fingerprintParts(summary.fingerprint)?.footerCrc !== last.footerCrc) {
1068
+ return void 0;
1069
+ }
1070
+ return known;
1071
+ }
1072
+ function windowOf(record) {
1073
+ const known = usableKnownGens(record);
1074
+ if (known === void 0) return void 0;
1075
+ const pointer = record.currentGen;
1076
+ return known.filter((e) => e.generation < pointer).map((e) => e.generation);
1077
+ }
1078
+ function carriesWindow(record, leased) {
1079
+ const below = windowOf(record);
1080
+ return below !== void 0 && below.filter((g) => !leased.has(g)).length <= MAX_KEPT_GENERATIONS;
1081
+ }
1082
+ var KNOWN_SPAN = 127;
1083
+ function keptWindow(below, superseded, keep) {
1084
+ const oldestFirst = [...below].sort(ascending);
1085
+ const first = keep > 0 && superseded !== void 0 && oldestFirst.includes(superseded) ? superseded : void 0;
1086
+ const held = new Set(first === void 0 ? [] : [first]);
1087
+ for (let i = oldestFirst.length - 1, room = keep - held.size; i >= 0 && room > 0; i--) {
1088
+ if (oldestFirst[i] === first) continue;
1089
+ held.add(oldestFirst[i]);
1090
+ room -= 1;
1091
+ }
1092
+ return {
1093
+ kept: oldestFirst.filter((g) => held.has(g)),
1094
+ evict: oldestFirst.filter((g) => !held.has(g))
1095
+ };
1096
+ }
1097
+ function capKnown(known, protect, leased = /* @__PURE__ */ new Set()) {
1098
+ const keep = /* @__PURE__ */ new Set();
1099
+ for (const e of known) if (protect.has(e.generation)) keep.add(e.generation);
1100
+ for (let i = known.length - 1; i >= 0 && keep.size < MAX_KNOWN_GENERATIONS; i--) {
1101
+ if (leased.has(known[i].generation)) keep.add(known[i].generation);
1102
+ }
1103
+ for (let i = known.length - 1; i >= 0 && keep.size < KNOWN_FILL; i--)
1104
+ keep.add(known[i].generation);
1105
+ return known.filter((e) => keep.has(e.generation));
1106
+ }
1107
+ function capInsert(known, protect, leased) {
1108
+ const keep = /* @__PURE__ */ new Set();
1109
+ let unleased = 0;
1110
+ for (const e of known) {
1111
+ if (!protect.has(e.generation)) continue;
1112
+ keep.add(e.generation);
1113
+ if (!leased.has(e.generation)) unleased += 1;
1114
+ }
1115
+ for (let i = known.length - 1; i >= 0 && unleased < KNOWN_FILL; i--) {
1116
+ const g = known[i].generation;
1117
+ if (keep.has(g) || leased.has(g)) continue;
1118
+ keep.add(g);
1119
+ unleased += 1;
1120
+ }
1121
+ for (let i = known.length - 1; i >= 0 && keep.size < MAX_KNOWN_GENERATIONS; i--) {
1122
+ if (leased.has(known[i].generation)) keep.add(known[i].generation);
1123
+ }
1124
+ return known.filter((e) => keep.has(e.generation));
1125
+ }
1126
+ function makeCurrent(known, entry) {
1127
+ return [...known.filter((e) => e.generation !== entry.generation), entry];
1128
+ }
1129
+ function nextKnown(prev, generation, footerCrc, keep, options = {}) {
1130
+ const self = { generation, footerCrc };
1131
+ const clears = options.clearsLeases === true;
1132
+ const byName = keep !== void 0 && keep <= MAX_KEPT_GENERATIONS;
1133
+ if (options.seed !== void 0 && !clears) {
1134
+ const seed = options.seed.filter((e) => e.generation !== generation);
1135
+ const protect = /* @__PURE__ */ new Set([...seed.map((e) => e.generation), generation]);
1136
+ return {
1137
+ known: capKnown([...seed, self], protect, options.leased ?? /* @__PURE__ */ new Set()),
1138
+ list: void 0,
1139
+ evict: []
1140
+ };
1141
+ }
1142
+ const found = usableKnownGens(prev);
1143
+ if (found === void 0 && prev !== null && prev.currentGen !== null) {
1144
+ return { known: [self], list: void 0, evict: [] };
1145
+ }
1146
+ const all = found ?? [];
1147
+ const below = all.filter((e) => e.generation < generation);
1148
+ const above = clears ? [] : all.filter((e) => e.generation > generation);
1149
+ const span = clears ? 0 : Math.min(keep ?? Number.MAX_SAFE_INTEGER, KNOWN_SPAN);
1150
+ const vouch = new Set((span === 0 ? [] : below.slice(-span)).map((e) => e.generation));
1151
+ const leased = clears ? /* @__PURE__ */ new Set() : options.leased ?? /* @__PURE__ */ new Set();
1152
+ const held = below.filter((e) => !vouch.has(e.generation) && leased.has(e.generation));
1153
+ const chosen = /* @__PURE__ */ new Set([
1154
+ ...vouch,
1155
+ ...held.map((e) => e.generation),
1156
+ ...above.map((e) => e.generation)
1157
+ ]);
1158
+ const known = capKnown(
1159
+ [...all.filter((e) => chosen.has(e.generation)), self],
1160
+ /* @__PURE__ */ new Set([...vouch, generation]),
1161
+ leased
1162
+ );
1163
+ const wide = !clears && found !== void 0 && !carriesWindow(prev, leased);
1164
+ if (!byName || wide) return { known, list: void 0, evict: [] };
1165
+ const named = new Set(known.map((e) => e.generation));
1166
+ return {
1167
+ known,
1168
+ list: known.filter((e) => e.generation < generation).map((e) => e.generation).sort(ascending),
1169
+ evict: below.filter((e) => !named.has(e.generation)).map((e) => e.generation).sort(ascending)
1170
+ };
1171
+ }
1172
+ function rollbackKnown(prev, target) {
1173
+ const found = usableKnownGens(prev);
1174
+ if (found === void 0) return void 0;
1175
+ const moved = makeCurrent(found, target);
1176
+ const inserted = !found.some((e) => e.generation === target.generation);
1177
+ if (!inserted && moved.length <= MAX_KNOWN_GENERATIONS) return moved;
1178
+ return capInsert(moved, /* @__PURE__ */ new Set([target.generation, prev.currentGen]), leasedByRow(prev));
1179
+ }
1180
+ function pruneKnown(record, doomed) {
1181
+ const found = usableKnownGens(record);
1182
+ if (found === void 0) return void 0;
1183
+ const rest = found.filter((e) => e.generation === record.currentGen || !doomed.has(e.generation));
1184
+ if (rest.length <= MAX_KNOWN_GENERATIONS) return rest;
1185
+ return capKnown(rest, /* @__PURE__ */ new Set([record.currentGen]), leasedByRow(record));
1186
+ }
1187
+ function describingKnown(record, movedTo) {
1188
+ const found = usableKnownGens(record);
1189
+ if (found === void 0 || found.length <= MAX_KNOWN_GENERATIONS) return found;
1190
+ return capKnown(found, /* @__PURE__ */ new Set([record.currentGen, movedTo]), leasedByRow(record));
1191
+ }
1192
+ function isFullLedger(known) {
1193
+ return known.length >= KNOWN_FILL;
1194
+ }
1195
+ function fallbackBound(record, known) {
1196
+ const leased = leasedByRow(record);
1197
+ return (known.find((e) => !leased.has(e.generation)) ?? known[0]).generation;
1198
+ }
1199
+ function leasedByRow(record) {
1200
+ return new Set((record.leases ?? []).map((e) => e.generation));
1201
+ }
1202
+
600
1203
  // src/drivers/_shared/registry.ts
601
1204
  var STATUSES = ["active", "destroyed"];
602
- var BASE_FIELDS = [
1205
+ var RECORD_FIELDS = [
603
1206
  "namespace",
604
1207
  "segment",
605
1208
  "currentGen",
@@ -611,23 +1214,19 @@ var BASE_FIELDS = [
611
1214
  "summary",
612
1215
  "createdAt",
613
1216
  "updatedAt",
614
- "token"
1217
+ "token",
1218
+ "knownGens",
1219
+ "leases",
1220
+ "pointerId"
615
1221
  ];
616
- var SCHEMA_3_FIELDS = ["keptGens", "leases"];
617
- var RECORD_FIELDS = [...BASE_FIELDS, ...SCHEMA_3_FIELDS];
618
- function fieldsOf(schemaVersion) {
619
- if (schemaVersion >= 3) return RECORD_FIELDS;
620
- const base = BASE_FIELDS;
621
- return schemaVersion === 2 ? base : base.filter((f) => f !== "summary");
622
- }
1222
+ var DRIVER_MANAGED = ["token", "pointerId"];
623
1223
  var ENVELOPE_FIELDS = ["schemaVersion", "deleted", "record"];
624
1224
  var MAX_GOVERNANCE_BYTES = 64 * 1024;
625
1225
  var MAX_WRAPPED_DEKS = 8;
626
1226
  var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
627
- var MAX_SUMMARY_CARDINALITY = 2 ** 32;
628
- var SEALED_SUMMARY_MIN_BYTES = 12 + 8 + 16;
1227
+ var SEALED_SUMMARY_MIN_BYTES = 12 + 8 + 8 + 4 + 4 + 16;
629
1228
  var SEALED_SUMMARY_MAX_BYTES = SEALED_SUMMARY_MIN_BYTES + MAX_METADATA_BYTES;
630
- var BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
1229
+ var BASE642 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
631
1230
  function validateGeneration(gen) {
632
1231
  if (gen === null) return;
633
1232
  if (!Number.isSafeInteger(gen) || gen < 0) {
@@ -667,10 +1266,10 @@ function validateGovernance(meta, field) {
667
1266
  throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);
668
1267
  }
669
1268
  }
670
- function validateWrappedDeks(value, isStored) {
1269
+ function validateWrappedDeks(value, origin) {
671
1270
  if (value === void 0) return;
672
1271
  const fail = (msg) => {
673
- throw isStored ? new IntegrityError(msg) : new ValidationError(msg);
1272
+ throw origin === "stored" ? new IntegrityError(msg) : new ValidationError(msg);
674
1273
  };
675
1274
  if (!Array.isArray(value)) fail("wrappedDeks must be an array");
676
1275
  const list = value;
@@ -695,11 +1294,11 @@ function base64PadBitsAreZero(text) {
695
1294
  const last = BASE64_ALPHABET.indexOf(text[text.length - pad - 1]);
696
1295
  return pad === 2 ? (last & 15) === 0 : (last & 3) === 0;
697
1296
  }
698
- function validateSummary(value, isStored, ctx) {
1297
+ function validateSummary(value, origin, ctx) {
699
1298
  if (value === void 0) return void 0;
700
1299
  const fail = (msg) => {
701
1300
  const where = ctx === void 0 ? "" : `: ${ctx}`;
702
- throw isStored ? new IntegrityError(`registry record summary: ${msg}${where}`) : new ValidationError(`summary: ${msg}${where}`);
1301
+ throw origin === "stored" ? new IntegrityError(`registry record summary: ${msg}${where}`) : new ValidationError(`summary: ${msg}${where}`);
703
1302
  };
704
1303
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
705
1304
  fail(
@@ -709,7 +1308,7 @@ function validateSummary(value, isStored, ctx) {
709
1308
  const s = value;
710
1309
  const keys = Object.keys(s);
711
1310
  const isSealed = keys.includes("sealed");
712
- const allowed = isSealed ? ["generation", "sealed"] : ["generation", "cardinality", "metadata"];
1311
+ const allowed = isSealed ? ["generation", "sealed"] : ["generation", "cardinality", "fingerprint", "tailBytes", "metadata"];
713
1312
  const extra = keys.filter((k) => !allowed.includes(k));
714
1313
  if (extra.length > 0) {
715
1314
  fail(
@@ -722,14 +1321,14 @@ function validateSummary(value, isStored, ctx) {
722
1321
  }
723
1322
  if (isSealed) {
724
1323
  const sealed = s.sealed;
725
- if (typeof sealed !== "string" || !BASE64.test(sealed) || !base64PadBitsAreZero(sealed)) {
1324
+ if (typeof sealed !== "string" || !BASE642.test(sealed) || !base64PadBitsAreZero(sealed)) {
726
1325
  fail("sealed must be canonical, padded standard base64");
727
1326
  }
728
1327
  const text = sealed;
729
1328
  const bytes = text.length / 4 * 3 - (text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0);
730
1329
  if (bytes < SEALED_SUMMARY_MIN_BYTES || bytes > SEALED_SUMMARY_MAX_BYTES) {
731
1330
  fail(
732
- `sealed is ${bytes}B, outside ${SEALED_SUMMARY_MIN_BYTES}..${SEALED_SUMMARY_MAX_BYTES}B (a nonce, a fixed-width count, a tag, and metadata up to its cap)`
1331
+ `sealed is ${bytes}B, outside ${SEALED_SUMMARY_MIN_BYTES}..${SEALED_SUMMARY_MAX_BYTES}B (a nonce, a fixed-width count, fingerprint and tail size, a tag, and metadata up to its cap)`
733
1332
  );
734
1333
  }
735
1334
  return Object.freeze({ generation, sealed: text });
@@ -738,14 +1337,38 @@ function validateSummary(value, isStored, ctx) {
738
1337
  if (!Number.isInteger(n) || n < 0 || n > MAX_SUMMARY_CARDINALITY) {
739
1338
  fail(`cardinality must be an integer from 0 to 2^32 (got ${String(n)})`);
740
1339
  }
1340
+ const fingerprint = s.fingerprint;
1341
+ const parts = fingerprintParts(fingerprint);
1342
+ if (parts === void 0) {
1343
+ fail(
1344
+ `fingerprint must be an object's, <size>:<footer checksum> in decimal (got ${shown3(JSON.stringify(fingerprint))})`
1345
+ );
1346
+ }
1347
+ const tailBytes = s.tailBytes;
1348
+ if (!tailBytesFit(tailBytes, parts.size)) {
1349
+ fail(
1350
+ `tailBytes must be an integer from the footer's size to the object's size less its preamble, and at most the cap of one open's tail read (got ${shown3(JSON.stringify(tailBytes))} for an object of ${parts.size} bytes)`
1351
+ );
1352
+ }
741
1353
  if (s.metadata === void 0) {
742
- return Object.freeze({ generation, cardinality: n });
1354
+ return Object.freeze({
1355
+ generation,
1356
+ cardinality: n,
1357
+ fingerprint,
1358
+ tailBytes
1359
+ });
743
1360
  }
744
1361
  const canonical = canonicalMetadataJson(s.metadata, fail);
745
1362
  if (canonical === "{}") fail("metadata is empty; a generation without metadata carries none");
746
- if (isStored) return s;
1363
+ if (origin === "stored") return s;
747
1364
  const metadata = Object.freeze(JSON.parse(canonical));
748
- return Object.freeze({ generation, cardinality: n, metadata });
1365
+ return Object.freeze({
1366
+ generation,
1367
+ cardinality: n,
1368
+ fingerprint,
1369
+ tailBytes,
1370
+ metadata
1371
+ });
749
1372
  }
750
1373
  function validateSummaryNames(summary, currentGen) {
751
1374
  if (summary !== void 0 && summary.generation !== currentGen) {
@@ -764,33 +1387,30 @@ function validateSummaryKeys(summary, wrappedDeks) {
764
1387
  );
765
1388
  }
766
1389
  }
767
- function validateKeptGens(value, isStored, ctx) {
1390
+ function validateKnownGens(value, origin, ctx) {
768
1391
  if (value === void 0) return void 0;
769
1392
  const fail = (msg) => {
770
1393
  const where = ctx === void 0 ? "" : `: ${ctx}`;
771
- throw isStored ? new IntegrityError(`registry record keptGens: ${msg}${where}`) : new ValidationError(`keptGens: ${msg}`);
1394
+ throw origin === "stored" ? new IntegrityError(`registry record knownGens: ${msg}${where}`) : new ValidationError(`knownGens: ${msg}`);
772
1395
  };
773
- if (!Array.isArray(value)) fail("must be an array");
774
- const list = value;
775
- const max = isStored ? MAX_STORED_KEPT_GENERATIONS : MAX_KEPT_GENERATIONS;
776
- if (list.length > max) fail(`has ${list.length} entries, cap ${max}`);
777
- let last = -1;
778
- for (const g of list) {
779
- if (!Number.isSafeInteger(g) || g < 0) {
780
- fail(`entries must be non-negative safe integers (got ${String(g)})`);
781
- }
782
- if (g <= last) fail("entries must be strictly ascending");
783
- last = g;
1396
+ const max = origin === "stored" ? MAX_STORED_KNOWN_GENERATIONS : MAX_KNOWN_GENERATIONS;
1397
+ const fault = knownGensShapeFault(value, max, shown3);
1398
+ if (fault !== void 0) fail(fault);
1399
+ const out = [];
1400
+ for (const entry of value) {
1401
+ out.push(
1402
+ origin === "stored" ? entry : Object.freeze({ generation: entry.generation, footerCrc: entry.footerCrc })
1403
+ );
784
1404
  }
785
- return isStored ? list : Object.freeze([...list]);
1405
+ return origin === "stored" ? out : Object.freeze(out);
786
1406
  }
787
1407
  var HOLDER = /^[0-9a-f]{16}$/;
788
1408
  var LEASE_ENTRY_KEYS = ["holder", "generation", "until"];
789
- function validateLeases(value, isStored, ctx) {
1409
+ function validateLeases(value, origin, ctx) {
790
1410
  if (value === void 0) return void 0;
791
1411
  const fail = (msg) => {
792
1412
  const where = ctx === void 0 ? "" : `: ${ctx}`;
793
- throw isStored ? new IntegrityError(`registry record leases: ${msg}${where}`) : new ValidationError(`leases: ${msg}`);
1413
+ throw origin === "stored" ? new IntegrityError(`registry record leases: ${msg}${where}`) : new ValidationError(`leases: ${msg}`);
794
1414
  };
795
1415
  if (!Array.isArray(value)) fail("must be an array");
796
1416
  const list = value;
@@ -818,14 +1438,14 @@ function validateLeases(value, isStored, ctx) {
818
1438
  }
819
1439
  }
820
1440
  out.push(
821
- isStored ? entry : Object.freeze({
1441
+ origin === "stored" ? entry : Object.freeze({
822
1442
  holder,
823
1443
  generation,
824
1444
  until
825
1445
  })
826
1446
  );
827
1447
  }
828
- if (isStored) return out;
1448
+ if (origin === "stored") return out;
829
1449
  return out.length === 0 ? void 0 : Object.freeze(out);
830
1450
  }
831
1451
  function validateLeasesPointer(leases, currentGen) {
@@ -833,54 +1453,78 @@ function validateLeasesPointer(leases, currentGen) {
833
1453
  throw new ValidationError("leases: a row with no pointer has no generation to lease");
834
1454
  }
835
1455
  }
836
- function validateKeptGensBelow(keptGens, currentGen) {
837
- if (keptGens === void 0) return;
1456
+ function validateKnownGensFit(knownGens, currentGen, summary) {
1457
+ if (knownGens === void 0) return;
838
1458
  if (currentGen === null) {
1459
+ throw new ValidationError("knownGens: a row with no pointer has no generation to know");
1460
+ }
1461
+ const last = knownGens[knownGens.length - 1];
1462
+ if (last === void 0 || last.generation !== currentGen) {
1463
+ throw new ValidationError(
1464
+ `knownGens must end with the row's pointer ${currentGen}, in the order the generations were made current (its last entry is ${last === void 0 ? "missing" : last.generation})`
1465
+ );
1466
+ }
1467
+ if (summary !== void 0 && summary.generation === currentGen && "fingerprint" in summary && fingerprintParts(summary.fingerprint)?.footerCrc !== last.footerCrc) {
839
1468
  throw new ValidationError(
840
- "keptGens: a row with no pointer has no generations below it to keep"
1469
+ `knownGens records footerCrc ${last.footerCrc} for generation ${currentGen}, but the summary's fingerprint ${summary.fingerprint} carries another: both describe the one current object`
841
1470
  );
842
1471
  }
843
- const top = keptGens[keptGens.length - 1];
844
- if (top !== void 0 && top >= currentGen) {
1472
+ }
1473
+ function refuseDriverManaged(write, what) {
1474
+ const named = DRIVER_MANAGED.filter((field) => field in write);
1475
+ if (named.length > 0) {
845
1476
  throw new ValidationError(
846
- `keptGens names generation ${top}, which is not below the row's pointer ${currentGen}`
1477
+ `${what} names ${named.join(" and ")}, which the registry sets on every write and a caller never does`
847
1478
  );
848
1479
  }
849
1480
  }
1481
+ function validateKeyId(keyId) {
1482
+ if (keyId !== void 0 && (typeof keyId !== "string" || keyId.length === 0)) {
1483
+ throw new ValidationError("keyId must be a non-empty string, or absent");
1484
+ }
1485
+ }
850
1486
  function validateNewRegistryRecord(rec) {
1487
+ refuseDriverManaged(rec, "a new record");
1488
+ validateKeyId(rec.keyId);
851
1489
  validateGeneration(rec.currentGen);
852
1490
  if (rec.status !== void 0) validateStatus(rec.status);
853
- validateWrappedDeks(rec.wrappedDeks, false);
1491
+ validateWrappedDeks(rec.wrappedDeks, "caller");
854
1492
  validateGovernance(rec.retention, "retention");
855
1493
  validateGovernance(rec.residency, "residency");
856
- const keptGens = validateKeptGens(rec.keptGens, false);
857
- validateKeptGensBelow(keptGens, rec.currentGen);
858
- const withKept = keptGens === void 0 ? rec : { ...rec, keptGens };
859
- if (!("summary" in rec)) return withKept;
860
- const summary = validateSummary(rec.summary, false);
1494
+ const knownGens = validateKnownGens(rec.knownGens, "caller");
1495
+ const withKnown = knownGens === void 0 ? rec : { ...rec, knownGens };
1496
+ const summary = "summary" in rec ? validateSummary(rec.summary, "caller") : void 0;
1497
+ validateKnownGensFit(knownGens, rec.currentGen, summary);
1498
+ if (!("summary" in rec)) return withKnown;
861
1499
  validateSummaryNames(summary, rec.currentGen);
862
1500
  validateSummaryKeys(summary, rec.wrappedDeks);
863
- return { ...withKept, summary };
1501
+ return { ...withKnown, summary };
864
1502
  }
865
1503
  function validateRegistryPatch(patch) {
1504
+ refuseDriverManaged(patch, "a patch");
1505
+ if ("keyId" in patch) validateKeyId(patch.keyId);
866
1506
  validatePatchGeneration(patch);
867
1507
  if (patch.status !== void 0) validateStatus(patch.status);
868
- if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, false);
1508
+ if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, "caller");
869
1509
  if ("retention" in patch) validateGovernance(patch.retention, "retention");
870
1510
  if ("residency" in patch) validateGovernance(patch.residency, "residency");
871
- const withKept = patch.keptGens === void 0 ? patch : { ...patch, keptGens: validateKeptGens(patch.keptGens, false) };
872
- const withLeases = "leases" in withKept ? { ...withKept, leases: validateLeases(withKept.leases, false) } : withKept;
1511
+ const withKnown = patch.knownGens === void 0 ? patch : { ...patch, knownGens: validateKnownGens(patch.knownGens, "caller") };
1512
+ const withLeases = "leases" in withKnown ? { ...withKnown, leases: validateLeases(withKnown.leases, "caller") } : withKnown;
873
1513
  if (!("summary" in patch)) return withLeases;
874
- return { ...withLeases, summary: validateSummary(patch.summary, false) };
1514
+ return { ...withLeases, summary: validateSummary(patch.summary, "caller") };
875
1515
  }
876
- var REGISTRY_SCHEMA_VERSION = 3;
877
- var OLDEST_REGISTRY_SCHEMA_VERSION = 1;
1516
+ var REGISTRY_SCHEMA_VERSION = 5;
878
1517
  function assertRegistrySchemaVersion(raw, ctx) {
879
1518
  if (raw === void 0) {
880
1519
  throw new IntegrityError(`registry row has no schemaVersion: ${ctx}`);
881
1520
  }
882
- if (typeof raw !== "number" || !Number.isInteger(raw) || raw < OLDEST_REGISTRY_SCHEMA_VERSION) {
883
- throw new IntegrityError(`registry row has a malformed schemaVersion (${shown(raw)}): ${ctx}`);
1521
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
1522
+ throw new IntegrityError(`registry row has a malformed schemaVersion (${shown3(raw)}): ${ctx}`);
1523
+ }
1524
+ if (raw < REGISTRY_SCHEMA_VERSION) {
1525
+ throw new UnsupportedError(
1526
+ `registry row schemaVersion ${raw} is one this build does not read: it reads schema ${REGISTRY_SCHEMA_VERSION} only. Export the segments with the release that wrote the row and load them with this one into a new prefix (steps: https://github.com/cloudbitmaps/cloudbitmaps/blob/main/CHANGELOG.md, the Breaking entry on registry schema 5): ${ctx}`
1527
+ );
884
1528
  }
885
1529
  if (raw > REGISTRY_SCHEMA_VERSION) {
886
1530
  throw new UnsupportedError(
@@ -896,25 +1540,31 @@ function serializeRegistryEnvelope(env) {
896
1540
  record: env.record
897
1541
  });
898
1542
  }
899
- var COUNTER_TOKEN = /^(0|[1-9]\d*)$/;
900
- var WRITTEN_COUNTER_TOKEN = /^(0|[1-9]\d*)\.([0-9a-f]{16})$/;
901
1543
  var INCARNATION_BYTES = 16;
902
1544
  var WRITE_BYTES = 8;
903
- function tokenParts(token, schemaVersion, ctx) {
904
- const bare = schemaVersion === void 0 || schemaVersion === 1 ? COUNTER_TOKEN.exec(token) : null;
905
- const written = schemaVersion === void 0 || schemaVersion >= 2 ? WRITTEN_COUNTER_TOKEN.exec(token) ?? INCARNATION_TOKEN.exec(token) : null;
906
- const match = bare ?? written;
1545
+ function tokenParts(token, ctx, what = "token") {
1546
+ const match = typeof token === "string" ? INCARNATION_TOKEN.exec(token) : null;
907
1547
  if (match === null) {
908
1548
  throw new IntegrityError(
909
- `registry row token is not one a schema-${schemaVersion ?? REGISTRY_SCHEMA_VERSION} row holds (${shown(JSON.stringify(token))}): ${ctx}`
1549
+ `registry row ${what} is not one a shipped registry writes (${shown3(JSON.stringify(token))}): ${ctx}`
910
1550
  );
911
1551
  }
912
- const born = match.length === 4;
913
- const counter = Number(born ? match[2] : match[1]);
1552
+ const counter = Number(match[2]);
914
1553
  if (!Number.isSafeInteger(counter)) {
915
- throw new IntegrityError(`registry row token's counter is out of safe-integer range: ${ctx}`);
1554
+ throw new IntegrityError(`registry row ${what}'s counter is out of safe-integer range: ${ctx}`);
1555
+ }
1556
+ return { incarnation: match[1], counter };
1557
+ }
1558
+ function checkPointerId(pointerId, token, ctx) {
1559
+ const parts = tokenParts(pointerId, ctx, "pointerId");
1560
+ if (parts.incarnation !== token.incarnation) {
1561
+ throw new IntegrityError(
1562
+ `registry row pointerId names another incarnation than its token: ${ctx}`
1563
+ );
1564
+ }
1565
+ if (parts.counter > token.counter) {
1566
+ throw new IntegrityError(`registry row pointerId is a write after its token: ${ctx}`);
916
1567
  }
917
- return { incarnation: born ? match[1] : void 0, counter };
918
1568
  }
919
1569
  function drawHex(entropy, bytes) {
920
1570
  const drawn = entropy(bytes);
@@ -934,15 +1584,14 @@ function drawWrite(entropy) {
934
1584
  return drawHex(entropy, WRITE_BYTES);
935
1585
  }
936
1586
  function nextRegistryToken(record, entropy) {
937
- const { incarnation, counter } = tokenParts(record.token, void 0, record.segment);
938
- const tail = `${counter + 1}.${drawWrite(entropy)}`;
939
- return incarnation === void 0 ? tail : `${incarnation}.${tail}`;
1587
+ const { incarnation, counter } = tokenParts(record.token, record.segment);
1588
+ return `${incarnation}.${counter + 1}.${drawWrite(entropy)}`;
940
1589
  }
941
1590
  function newIncarnationToken(entropy, tombstone) {
942
- const counter = tombstone === void 0 ? 0 : tokenParts(tombstone.token, void 0, tombstone.segment).counter + 1;
1591
+ const counter = tombstone === void 0 ? 0 : tokenParts(tombstone.token, tombstone.segment).counter + 1;
943
1592
  return `${drawIncarnation(entropy)}.${counter}.${drawWrite(entropy)}`;
944
1593
  }
945
- function shown(v) {
1594
+ function shown3(v) {
946
1595
  const text = String(v);
947
1596
  return text.length > 64 ? `${text.slice(0, 64)}\u2026` : text;
948
1597
  }
@@ -957,22 +1606,22 @@ function parseRegistryEnvelope(text, ctx, expected) {
957
1606
  throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
958
1607
  }
959
1608
  const env = parsed;
960
- const schemaVersion = assertRegistrySchemaVersion(env.schemaVersion, ctx);
1609
+ assertRegistrySchemaVersion(env.schemaVersion, ctx);
961
1610
  const extra = Object.keys(env).filter((k) => !ENVELOPE_FIELDS.includes(k));
962
1611
  if (extra.length > 0) {
963
1612
  throw new IntegrityError(
964
- `registry row has fields its envelope does not declare (${shown(extra.join(", "))}): ${ctx}`
1613
+ `registry row has fields its envelope does not declare (${shown3(extra.join(", "))}): ${ctx}`
965
1614
  );
966
1615
  }
967
1616
  if (typeof env.deleted !== "boolean" || env.record === null || typeof env.record !== "object") {
968
1617
  throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
969
1618
  }
970
1619
  const r = env.record;
971
- assertStoredRecordShape(r, ctx, schemaVersion);
1620
+ assertStoredRecordShape(r, ctx);
972
1621
  if (typeof r.token !== "string") {
973
1622
  throw new IntegrityError(`registry row is missing its token: ${ctx}`);
974
1623
  }
975
- tokenParts(r.token, schemaVersion, ctx);
1624
+ checkPointerId(r.pointerId, tokenParts(r.token, ctx), ctx);
976
1625
  if (expected !== void 0 && (r.segment !== expected.segment || (r.namespace ?? void 0) !== (expected.namespace ?? void 0))) {
977
1626
  throw new IntegrityError(
978
1627
  `registry row names another segment than the one it is stored for: ${ctx}`
@@ -991,46 +1640,47 @@ function recordFromNew(ref, rec, now, token) {
991
1640
  retention: rec.retention,
992
1641
  residency: rec.residency,
993
1642
  summary: rec.summary,
994
- keptGens: rec.keptGens,
1643
+ knownGens: rec.knownGens,
995
1644
  // A new row has no holder: a lease is written only against a row that already exists.
996
1645
  leases: void 0,
997
1646
  createdAt: now,
998
1647
  updatedAt: now,
999
- token
1648
+ token,
1649
+ pointerId: token
1000
1650
  };
1001
1651
  }
1002
- function assertStoredRecordShape(r, ctx, schemaVersion = REGISTRY_SCHEMA_VERSION) {
1003
- if (typeof r.segment !== "string" || r.namespace !== void 0 && typeof r.namespace !== "string" || r.currentGen !== null && typeof r.currentGen !== "number" || typeof r.status !== "string" || typeof r.createdAt !== "number" || typeof r.updatedAt !== "number") {
1652
+ function assertStoredRecordShape(r, ctx) {
1653
+ if (typeof r.segment !== "string" || typeof r.pointerId !== "string" || r.namespace !== void 0 && typeof r.namespace !== "string" || r.currentGen !== null && typeof r.currentGen !== "number" || typeof r.status !== "string" || typeof r.createdAt !== "number" || typeof r.updatedAt !== "number") {
1004
1654
  throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
1005
1655
  }
1006
1656
  if (r.currentGen !== null && (!Number.isSafeInteger(r.currentGen) || r.currentGen < 0)) {
1007
1657
  throw new IntegrityError(
1008
- `registry record has an invalid currentGen (${shown(r.currentGen)}): ${ctx}`
1658
+ `registry record has an invalid currentGen (${shown3(r.currentGen)}): ${ctx}`
1009
1659
  );
1010
1660
  }
1011
1661
  if (!STATUSES.includes(r.status)) {
1012
- throw new IntegrityError(`registry record has an unknown status (${shown(r.status)}): ${ctx}`);
1662
+ throw new IntegrityError(`registry record has an unknown status (${shown3(r.status)}): ${ctx}`);
1013
1663
  }
1014
- const declared = fieldsOf(schemaVersion);
1664
+ const declared = RECORD_FIELDS;
1015
1665
  const extra = Object.keys(r).filter((k) => !declared.includes(k));
1016
1666
  if (extra.length > 0) {
1017
1667
  throw new IntegrityError(
1018
- `registry record has fields a schema-${schemaVersion} row does not declare (${shown(extra.join(", "))}): ${ctx}`
1668
+ `registry record has fields a schema-${REGISTRY_SCHEMA_VERSION} row does not declare (${shown3(extra.join(", "))}): ${ctx}`
1019
1669
  );
1020
1670
  }
1021
1671
  if (r.keyId !== void 0 && typeof r.keyId !== "string") {
1022
1672
  throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);
1023
1673
  }
1024
- validateWrappedDeks(r.wrappedDeks, true);
1674
+ validateWrappedDeks(r.wrappedDeks, "stored");
1025
1675
  for (const field of ["retention", "residency"]) {
1026
1676
  const meta = r[field];
1027
1677
  if (meta !== void 0 && (meta === null || typeof meta !== "object" || Array.isArray(meta))) {
1028
1678
  throw new IntegrityError(`registry record has a non-object ${field}: ${ctx}`);
1029
1679
  }
1030
1680
  }
1031
- validateSummary(r.summary, true, ctx);
1032
- validateKeptGens(r.keptGens, true, ctx);
1033
- validateLeases(r.leases, true, ctx);
1681
+ validateSummary(r.summary, "stored", ctx);
1682
+ validateKnownGens(r.knownGens, "stored", ctx);
1683
+ validateLeases(r.leases, "stored", ctx);
1034
1684
  }
1035
1685
  function applyRegistryPatch(prev, patch, now, token) {
1036
1686
  const currentGen = "currentGen" in patch ? patch.currentGen ?? null : prev.currentGen;
@@ -1044,12 +1694,12 @@ function applyRegistryPatch(prev, patch, now, token) {
1044
1694
  const kept = currentGen === prev.currentGen ? prev.summary : void 0;
1045
1695
  summary = kept !== void 0 && summaryAgreesWithKeys(kept, wrappedDeks) ? kept : void 0;
1046
1696
  }
1047
- let keptGens;
1048
- if ("keptGens" in patch) {
1049
- validateKeptGensBelow(patch.keptGens, currentGen);
1050
- keptGens = patch.keptGens;
1697
+ let knownGens;
1698
+ if ("knownGens" in patch) {
1699
+ validateKnownGensFit(patch.knownGens, currentGen, summary);
1700
+ knownGens = patch.knownGens;
1051
1701
  } else {
1052
- keptGens = currentGen === prev.currentGen ? prev.keptGens : void 0;
1702
+ knownGens = currentGen === prev.currentGen ? prev.knownGens : void 0;
1053
1703
  }
1054
1704
  let leases;
1055
1705
  if ("leases" in patch) {
@@ -1072,11 +1722,12 @@ function applyRegistryPatch(prev, patch, now, token) {
1072
1722
  retention: "retention" in patch ? patch.retention : prev.retention,
1073
1723
  residency: "residency" in patch ? patch.residency : prev.residency,
1074
1724
  summary,
1075
- keptGens,
1725
+ knownGens,
1076
1726
  leases,
1077
1727
  createdAt: prev.createdAt,
1078
1728
  updatedAt: now,
1079
- token
1729
+ token,
1730
+ pointerId: renewsPointer(patch) ? token : prev.pointerId
1080
1731
  };
1081
1732
  }
1082
1733
 
@@ -1125,24 +1776,22 @@ export {
1125
1776
  isCapabilityError,
1126
1777
  isBudgetExceededError,
1127
1778
  isKeyUnavailableError,
1779
+ requireInteger,
1780
+ requirePositiveInteger,
1781
+ requireFiniteNumber,
1782
+ U32_MAX,
1783
+ CHUNK_COUNT,
1784
+ splitId,
1785
+ MAX_REMAINDER,
1786
+ joinId,
1787
+ mapWithConcurrency,
1788
+ boundedAll,
1789
+ PREAMBLE_BYTES,
1790
+ FOOTER_BYTES,
1128
1791
  encodeNameForKey,
1129
1792
  decodeNameFromKey,
1130
1793
  encodeNameForPath,
1131
1794
  decodeNameFromPath,
1132
- MAX_KEPT_GENERATIONS,
1133
- usableKeptGens,
1134
- nextKept,
1135
- incarnationOf,
1136
- sameIncarnation,
1137
- MAX_LEASE_MS,
1138
- LEASE_SKEW_MS,
1139
- MAX_LEASES_PER_SEGMENT,
1140
- isLive,
1141
- heldGenerations,
1142
- onlyLeasesDiffer,
1143
- leaseChurn,
1144
- takeLease,
1145
- releaseLease,
1146
1795
  MAX_NAME_LENGTH,
1147
1796
  encodedNameLength,
1148
1797
  validateSegmentRef,
@@ -1152,16 +1801,64 @@ export {
1152
1801
  validateUserRef,
1153
1802
  validateChunkKeyOrder,
1154
1803
  validateChunkRef,
1155
- mapWithConcurrency,
1804
+ validateChunkKey,
1156
1805
  assertRegistryCanWrite,
1157
1806
  STORAGE_BACKEND,
1158
1807
  brandAsBackend,
1159
1808
  isStorageBackend,
1160
1809
  MAX_METADATA_BYTES,
1161
- canonicalMetadataJson,
1162
1810
  metadataBytes,
1163
1811
  copiedMetadata,
1164
1812
  metadataFromBytes,
1813
+ sizePart,
1814
+ fingerprintFor,
1815
+ fingerprintParts,
1816
+ footerCrcOf,
1817
+ compactFingerprint,
1818
+ incarnationOf,
1819
+ incarnationField,
1820
+ sameIncarnation,
1821
+ RESOLVED_FIELDS,
1822
+ renewsPointer,
1823
+ renewPointer,
1824
+ pointerIdOf,
1825
+ aadFor,
1826
+ sameKeys,
1827
+ sizeOfFingerprint,
1828
+ usableSummary,
1829
+ summaryAgrees,
1830
+ metadataToCarry,
1831
+ summaryOf,
1832
+ ownSummary,
1833
+ DEFAULT_RETRY_POLICY,
1834
+ jitteredWait,
1835
+ retryRead,
1836
+ checkedRetryPolicy,
1837
+ withRetry,
1838
+ MAX_LEASE_MS,
1839
+ LEASE_SKEW_MS,
1840
+ MAX_LEASES_PER_SEGMENT,
1841
+ UNANSWERED_RESENDS,
1842
+ UNANSWERED_BASE_MS,
1843
+ isLive,
1844
+ heldGenerations,
1845
+ onlyLeasesDiffer,
1846
+ leaseChurn,
1847
+ takeLease,
1848
+ releaseLease,
1849
+ knownGensShapeFault,
1850
+ ascending,
1851
+ MAX_STORED_KNOWN_GENERATIONS,
1852
+ usableKnownGens,
1853
+ windowOf,
1854
+ KNOWN_SPAN,
1855
+ keptWindow,
1856
+ nextKnown,
1857
+ rollbackKnown,
1858
+ pruneKnown,
1859
+ describingKnown,
1860
+ isFullLedger,
1861
+ fallbackBound,
1165
1862
  validateNewRegistryRecord,
1166
1863
  validateRegistryPatch,
1167
1864
  serializeRegistryEnvelope,
@@ -1176,6 +1873,10 @@ export {
1176
1873
  entropyIsAvailable,
1177
1874
  DEFAULT_NAMESPACE,
1178
1875
  namespaceKeyPart,
1179
- namespacePathPart
1876
+ namespacePathPart,
1877
+ refuseUnknownOptions,
1878
+ checkNow,
1879
+ requirePositiveInt,
1880
+ requireBool
1180
1881
  };
1181
- //# sourceMappingURL=chunk-RSLVTRJY.js.map
1882
+ //# sourceMappingURL=chunk-IGLIZBR7.js.map