@cloudbitmaps/core 0.19.0 → 0.20.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 (39) hide show
  1. package/dist/{chunk-TNWTIAON.js → chunk-YSC7NJWV.js} +314 -216
  2. package/dist/chunk-YSC7NJWV.js.map +7 -0
  3. package/dist/core/audit.d.ts +56 -3
  4. package/dist/core/combine-many.d.ts +13 -4
  5. package/dist/core/consistency.d.ts +10 -4
  6. package/dist/core/crbm/fingerprint.d.ts +18 -0
  7. package/dist/core/crbm/format.d.ts +2 -4
  8. package/dist/core/crbm/frame.d.ts +10 -0
  9. package/dist/core/crbm/reader.d.ts +1 -3
  10. package/dist/core/crbm-storage-source.d.ts +249 -96
  11. package/dist/core/engine.d.ts +22 -14
  12. package/dist/core/erase-id.d.ts +62 -26
  13. package/dist/core/generation-gc.d.ts +3 -2
  14. package/dist/core/keys.d.ts +28 -0
  15. package/dist/core/leases.d.ts +7 -0
  16. package/dist/core/load.d.ts +11 -7
  17. package/dist/core/metrics.d.ts +5 -5
  18. package/dist/core/pinned-storage-source.d.ts +3 -3
  19. package/dist/core/pointer-id.d.ts +19 -0
  20. package/dist/core/ports.d.ts +131 -35
  21. package/dist/core/reader-defaults.d.ts +14 -3
  22. package/dist/core/rollback.d.ts +2 -1
  23. package/dist/core/summary.d.ts +38 -21
  24. package/dist/core/token.d.ts +13 -8
  25. package/dist/driver-kit.d.ts +2 -1
  26. package/dist/driver-kit.js +11 -4
  27. package/dist/driver-kit.js.map +2 -2
  28. package/dist/drivers/_shared/object-registry.d.ts +1 -1
  29. package/dist/drivers/_shared/registry.d.ts +26 -17
  30. package/dist/drivers/localfs/storage.d.ts +7 -6
  31. package/dist/drivers/memory.d.ts +8 -6
  32. package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
  33. package/dist/export/index.d.ts +16 -0
  34. package/dist/index.d.ts +2 -5
  35. package/dist/index.js +1288 -1080
  36. package/dist/index.js.map +4 -4
  37. package/package.json +1 -1
  38. package/dist/chunk-TNWTIAON.js.map +0 -7
  39. package/dist/core/cost.d.ts +0 -351
@@ -172,11 +172,179 @@ function decodeNameFromPath(encoded) {
172
172
  return decodePercent(encoded);
173
173
  }
174
174
 
175
+ // src/core/validate.ts
176
+ var CHUNK_KEY_MAX = 65535;
177
+ var MAX_NAME_LENGTH = 256;
178
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
179
+ function isWellFormedString(value) {
180
+ return !LONE_SURROGATE.test(value);
181
+ }
182
+ function encodedNameLength(value) {
183
+ return Math.max(encodeNameForKey(value).length, encodeNameForPath(value).length);
184
+ }
185
+ function validatePart(value, field) {
186
+ if (typeof value !== "string") {
187
+ throw new ValidationError(`${field} must be a string; got ${typeof value}`);
188
+ }
189
+ if (value.length === 0) {
190
+ throw new ValidationError(`${field} must not be empty`);
191
+ }
192
+ if (value.length > MAX_NAME_LENGTH) {
193
+ throw new ValidationError(
194
+ `${field} is too long: ${value.length} characters (limit ${MAX_NAME_LENGTH} once encoded for a storage key)`
195
+ );
196
+ }
197
+ if (LONE_SURROGATE.test(value)) {
198
+ throw new ValidationError(
199
+ `${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.`
200
+ );
201
+ }
202
+ const encoded = encodedNameLength(value);
203
+ if (encoded > MAX_NAME_LENGTH) {
204
+ throw new ValidationError(
205
+ `${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.`
206
+ );
207
+ }
208
+ }
209
+ function validateSegmentRef(ref) {
210
+ validatePart(ref.segment, "segment");
211
+ if (ref.namespace !== void 0) validatePart(ref.namespace, "namespace");
212
+ }
213
+ var RESERVED_NAMESPACE_PREFIX = "cbm.due.";
214
+ function isReservedNamespace(namespace) {
215
+ return namespace !== void 0 && namespace.startsWith(RESERVED_NAMESPACE_PREFIX);
216
+ }
217
+ function validateUserNamespace(namespace) {
218
+ validatePart(namespace, "namespace");
219
+ if (isReservedNamespace(namespace)) {
220
+ throw new ValidationError(
221
+ `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.`
222
+ );
223
+ }
224
+ }
225
+ function validateUserRef(ref) {
226
+ validatePart(ref.segment, "segment");
227
+ if (ref.namespace !== void 0) validateUserNamespace(ref.namespace);
228
+ }
229
+ function validateChunkKeyOrder(keys) {
230
+ for (let i = 1; i < keys.length; i++) {
231
+ if (!(keys[i] >= keys[i - 1]))
232
+ throw new ValidationError("chunk keys must be in ascending order");
233
+ }
234
+ }
235
+ function validateChunkRef(ref) {
236
+ validateSegmentRef(ref);
237
+ if (!Number.isInteger(ref.chunkKey) || ref.chunkKey < 0 || ref.chunkKey > CHUNK_KEY_MAX) {
238
+ throw new ValidationError(`chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]`);
239
+ }
240
+ }
241
+
242
+ // src/core/concurrency.ts
243
+ async function mapWithConcurrency(items, limit, fn) {
244
+ if (!Number.isInteger(limit) || limit < 1) {
245
+ throw new ValidationError(`concurrency limit must be a positive integer; got ${limit}`);
246
+ }
247
+ const results = new Array(items.length);
248
+ let next = 0;
249
+ let failed = false;
250
+ let firstError;
251
+ const worker = async () => {
252
+ while (!failed) {
253
+ const i = next++;
254
+ if (i >= items.length) return;
255
+ try {
256
+ results[i] = await fn(items[i], i);
257
+ } catch (err) {
258
+ if (!failed) {
259
+ failed = true;
260
+ firstError = err;
261
+ }
262
+ return;
263
+ }
264
+ }
265
+ };
266
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
267
+ await Promise.all(workers);
268
+ if (failed) throw firstError;
269
+ return results;
270
+ }
271
+
272
+ // src/core/ports.ts
273
+ function assertRegistryCanWrite(registry, what) {
274
+ if (registry.capabilities().canWrite === false) {
275
+ throw new UnsupportedError(
276
+ `${what}: the registry reports it cannot write a row in this runtime (capabilities().canWrite is false), so nothing is written`
277
+ );
278
+ }
279
+ }
280
+ var STORAGE_BACKEND_BRAND = /* @__PURE__ */ Symbol.for("cloudbitmaps.storage-backend");
281
+ var STORAGE_BACKEND = STORAGE_BACKEND_BRAND;
282
+ function brandAsBackend(target) {
283
+ if (target === null || typeof target !== "object") {
284
+ throw new ValidationError("brandAsBackend needs `{ storage, registry }`");
285
+ }
286
+ const { storage, registry } = target;
287
+ if (storage === null || typeof storage !== "object" || typeof storage.putImmutable !== "function") {
288
+ throw new ValidationError(
289
+ "brandAsBackend: `storage` must be an IStorageDriver (it has no `putImmutable`)"
290
+ );
291
+ }
292
+ if (registry === null || typeof registry !== "object" || typeof registry.compareAndSwap !== "function") {
293
+ throw new ValidationError(
294
+ "brandAsBackend: `registry` must be an IRegistryDriver (it has no `compareAndSwap`)"
295
+ );
296
+ }
297
+ try {
298
+ Object.defineProperty(target, STORAGE_BACKEND_BRAND, {
299
+ value: true,
300
+ enumerable: false,
301
+ writable: false,
302
+ configurable: false
303
+ });
304
+ } catch {
305
+ throw new ValidationError(
306
+ "brandAsBackend: cannot brand a frozen or non-extensible object \u2014 pass a plain object or a class instance"
307
+ );
308
+ }
309
+ return target;
310
+ }
311
+ function isStorageBackend(value) {
312
+ return typeof value === "object" && value !== null && value[STORAGE_BACKEND_BRAND] === true;
313
+ }
314
+
315
+ // src/core/pointer-id.ts
316
+ var RESOLVED_FIELDS = [
317
+ "currentGen",
318
+ "status",
319
+ "wrappedDeks",
320
+ "keyId",
321
+ "summary"
322
+ ];
323
+ function renewsPointer(patch) {
324
+ return RESOLVED_FIELDS.some((field) => field in patch);
325
+ }
326
+ function renewPointer(row) {
327
+ return { currentGen: row.currentGen };
328
+ }
329
+ function pointerIdOf(row) {
330
+ const pointerId = row.pointerId;
331
+ if (typeof pointerId !== "string" || pointerId.length === 0) {
332
+ throw new UnsupportedError(
333
+ `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)`
334
+ );
335
+ }
336
+ return pointerId;
337
+ }
338
+
175
339
  // src/core/token.ts
176
340
  var INCARNATION_TOKEN = /^([0-9a-f]{32})\.(0|[1-9]\d*)\.([0-9a-f]{16})$/;
177
341
  function incarnationOf(token) {
178
342
  return INCARNATION_TOKEN.exec(token)?.[1];
179
343
  }
344
+ function incarnationField(token) {
345
+ const incarnation = token === void 0 ? void 0 : incarnationOf(token);
346
+ return incarnation === void 0 ? {} : { incarnation };
347
+ }
180
348
  function sameIncarnation(a, b) {
181
349
  const ia = incarnationOf(a.token);
182
350
  const ib = incarnationOf(b.token);
@@ -349,146 +517,6 @@ async function releaseLease(ref, deps, holder) {
349
517
  throw new WriteConflictError(`lease: contention releasing a lease on segment "${ref.segment}"`);
350
518
  }
351
519
 
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);
358
- }
359
- function encodedNameLength(value) {
360
- return Math.max(encodeNameForKey(value).length, encodeNameForPath(value).length);
361
- }
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) {
381
- 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.`
383
- );
384
- }
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)) {
397
- 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.`
399
- );
400
- }
401
- }
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}]`);
416
- }
417
- }
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;
447
- }
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
- );
455
- }
456
- }
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 }`");
462
- }
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
520
  // src/drivers/_shared/keys.ts
493
521
  var DEFAULT_NAMESPACE = "_default";
494
522
  function namespaceKeyPart(namespace) {
@@ -498,26 +526,9 @@ function namespacePathPart(namespace) {
498
526
  return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);
499
527
  }
500
528
 
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
- }
529
+ // src/core/crbm/frame.ts
530
+ var PREAMBLE_BYTES = 8;
531
+ var FOOTER_BYTES = 104;
521
532
 
522
533
  // src/core/metadata.ts
523
534
  var MAX_METADATA_BYTES = 1024;
@@ -597,9 +608,50 @@ function metadataFromBytes(bytes, fail) {
597
608
  return Object.freeze(parsed);
598
609
  }
599
610
 
611
+ // src/core/crbm/fingerprint.ts
612
+ var sizePart = (size) => `${size}:`;
613
+ var fingerprintFor = (size, footerCrc) => `${sizePart(size)}${footerCrc}`;
614
+ var FINGERPRINT = /^(0|[1-9]\d*):(0|[1-9]\d*)$/;
615
+ function fingerprintParts(fingerprint) {
616
+ const match = typeof fingerprint === "string" ? FINGERPRINT.exec(fingerprint) : null;
617
+ if (match === null) return void 0;
618
+ const size = Number(match[1]);
619
+ const footerCrc = Number(match[2]);
620
+ if (!Number.isSafeInteger(size) || size < PREAMBLE_BYTES + FOOTER_BYTES) return void 0;
621
+ if (footerCrc > 4294967295) return void 0;
622
+ return { size, footerCrc };
623
+ }
624
+ function compactFingerprint(fingerprint) {
625
+ const colon = fingerprint.indexOf(":");
626
+ const size = Number(fingerprint.slice(0, colon));
627
+ const crc = Number(fingerprint.slice(colon + 1));
628
+ return `${size.toString(36)}.${crc.toString(36)}`;
629
+ }
630
+
631
+ // src/core/kept-generations.ts
632
+ var MAX_KEPT_GENERATIONS = 64;
633
+ var MAX_STORED_KEPT_GENERATIONS = 256;
634
+ function usableKeptGens(record) {
635
+ const list = record?.keptGens;
636
+ if (record === null || list === void 0 || record.currentGen === null) return void 0;
637
+ for (const g of list) if (g >= record.currentGen) return void 0;
638
+ return list;
639
+ }
640
+ function nextKept(prev, generation, keep) {
641
+ const none = { list: void 0, evict: [] };
642
+ if (keep === void 0 || keep > MAX_KEPT_GENERATIONS) return none;
643
+ if (generation === 0) return { list: [], evict: [] };
644
+ const previous = prev?.currentGen ?? null;
645
+ const known = usableKeptGens(prev);
646
+ if (previous === null || known === void 0) return none;
647
+ const all = [...known, previous];
648
+ const cut = Math.max(0, all.length - keep);
649
+ return { list: all.slice(cut), evict: all.slice(0, cut) };
650
+ }
651
+
600
652
  // src/drivers/_shared/registry.ts
601
653
  var STATUSES = ["active", "destroyed"];
602
- var BASE_FIELDS = [
654
+ var RECORD_FIELDS = [
603
655
  "namespace",
604
656
  "segment",
605
657
  "currentGen",
@@ -611,21 +663,18 @@ var BASE_FIELDS = [
611
663
  "summary",
612
664
  "createdAt",
613
665
  "updatedAt",
614
- "token"
666
+ "token",
667
+ "keptGens",
668
+ "leases",
669
+ "pointerId"
615
670
  ];
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
- }
671
+ var DRIVER_MANAGED = ["token", "pointerId"];
623
672
  var ENVELOPE_FIELDS = ["schemaVersion", "deleted", "record"];
624
673
  var MAX_GOVERNANCE_BYTES = 64 * 1024;
625
674
  var MAX_WRAPPED_DEKS = 8;
626
675
  var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
627
676
  var MAX_SUMMARY_CARDINALITY = 2 ** 32;
628
- var SEALED_SUMMARY_MIN_BYTES = 12 + 8 + 16;
677
+ var SEALED_SUMMARY_MIN_BYTES = 12 + 8 + 8 + 4 + 16;
629
678
  var SEALED_SUMMARY_MAX_BYTES = SEALED_SUMMARY_MIN_BYTES + MAX_METADATA_BYTES;
630
679
  var BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
631
680
  function validateGeneration(gen) {
@@ -709,7 +758,7 @@ function validateSummary(value, isStored, ctx) {
709
758
  const s = value;
710
759
  const keys = Object.keys(s);
711
760
  const isSealed = keys.includes("sealed");
712
- const allowed = isSealed ? ["generation", "sealed"] : ["generation", "cardinality", "metadata"];
761
+ const allowed = isSealed ? ["generation", "sealed"] : ["generation", "cardinality", "fingerprint", "metadata"];
713
762
  const extra = keys.filter((k) => !allowed.includes(k));
714
763
  if (extra.length > 0) {
715
764
  fail(
@@ -729,7 +778,7 @@ function validateSummary(value, isStored, ctx) {
729
778
  const bytes = text.length / 4 * 3 - (text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0);
730
779
  if (bytes < SEALED_SUMMARY_MIN_BYTES || bytes > SEALED_SUMMARY_MAX_BYTES) {
731
780
  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)`
781
+ `sealed is ${bytes}B, outside ${SEALED_SUMMARY_MIN_BYTES}..${SEALED_SUMMARY_MAX_BYTES}B (a nonce, a fixed-width count and fingerprint, a tag, and metadata up to its cap)`
733
782
  );
734
783
  }
735
784
  return Object.freeze({ generation, sealed: text });
@@ -738,14 +787,29 @@ function validateSummary(value, isStored, ctx) {
738
787
  if (!Number.isInteger(n) || n < 0 || n > MAX_SUMMARY_CARDINALITY) {
739
788
  fail(`cardinality must be an integer from 0 to 2^32 (got ${String(n)})`);
740
789
  }
790
+ const fingerprint = s.fingerprint;
791
+ if (fingerprintParts(fingerprint) === void 0) {
792
+ fail(
793
+ `fingerprint must be an object's, <size>:<footer checksum> in decimal (got ${shown(JSON.stringify(fingerprint))})`
794
+ );
795
+ }
741
796
  if (s.metadata === void 0) {
742
- return Object.freeze({ generation, cardinality: n });
797
+ return Object.freeze({
798
+ generation,
799
+ cardinality: n,
800
+ fingerprint
801
+ });
743
802
  }
744
803
  const canonical = canonicalMetadataJson(s.metadata, fail);
745
804
  if (canonical === "{}") fail("metadata is empty; a generation without metadata carries none");
746
805
  if (isStored) return s;
747
806
  const metadata = Object.freeze(JSON.parse(canonical));
748
- return Object.freeze({ generation, cardinality: n, metadata });
807
+ return Object.freeze({
808
+ generation,
809
+ cardinality: n,
810
+ fingerprint,
811
+ metadata
812
+ });
749
813
  }
750
814
  function validateSummaryNames(summary, currentGen) {
751
815
  if (summary !== void 0 && summary.generation !== currentGen) {
@@ -847,7 +911,16 @@ function validateKeptGensBelow(keptGens, currentGen) {
847
911
  );
848
912
  }
849
913
  }
914
+ function refuseDriverManaged(write, what) {
915
+ const named = DRIVER_MANAGED.filter((field) => field in write);
916
+ if (named.length > 0) {
917
+ throw new ValidationError(
918
+ `${what} names ${named.join(" and ")}, which the registry sets on every write and a caller never does`
919
+ );
920
+ }
921
+ }
850
922
  function validateNewRegistryRecord(rec) {
923
+ refuseDriverManaged(rec, "a new record");
851
924
  validateGeneration(rec.currentGen);
852
925
  if (rec.status !== void 0) validateStatus(rec.status);
853
926
  validateWrappedDeks(rec.wrappedDeks, false);
@@ -863,6 +936,7 @@ function validateNewRegistryRecord(rec) {
863
936
  return { ...withKept, summary };
864
937
  }
865
938
  function validateRegistryPatch(patch) {
939
+ refuseDriverManaged(patch, "a patch");
866
940
  validatePatchGeneration(patch);
867
941
  if (patch.status !== void 0) validateStatus(patch.status);
868
942
  if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, false);
@@ -873,15 +947,19 @@ function validateRegistryPatch(patch) {
873
947
  if (!("summary" in patch)) return withLeases;
874
948
  return { ...withLeases, summary: validateSummary(patch.summary, false) };
875
949
  }
876
- var REGISTRY_SCHEMA_VERSION = 3;
877
- var OLDEST_REGISTRY_SCHEMA_VERSION = 1;
950
+ var REGISTRY_SCHEMA_VERSION = 4;
878
951
  function assertRegistrySchemaVersion(raw, ctx) {
879
952
  if (raw === void 0) {
880
953
  throw new IntegrityError(`registry row has no schemaVersion: ${ctx}`);
881
954
  }
882
- if (typeof raw !== "number" || !Number.isInteger(raw) || raw < OLDEST_REGISTRY_SCHEMA_VERSION) {
955
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
883
956
  throw new IntegrityError(`registry row has a malformed schemaVersion (${shown(raw)}): ${ctx}`);
884
957
  }
958
+ if (raw < REGISTRY_SCHEMA_VERSION) {
959
+ throw new UnsupportedError(
960
+ `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 (the CHANGELOG gives the steps): ${ctx}`
961
+ );
962
+ }
885
963
  if (raw > REGISTRY_SCHEMA_VERSION) {
886
964
  throw new UnsupportedError(
887
965
  `registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`
@@ -896,25 +974,31 @@ function serializeRegistryEnvelope(env) {
896
974
  record: env.record
897
975
  });
898
976
  }
899
- var COUNTER_TOKEN = /^(0|[1-9]\d*)$/;
900
- var WRITTEN_COUNTER_TOKEN = /^(0|[1-9]\d*)\.([0-9a-f]{16})$/;
901
977
  var INCARNATION_BYTES = 16;
902
978
  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;
979
+ function tokenParts(token, ctx, what = "token") {
980
+ const match = typeof token === "string" ? INCARNATION_TOKEN.exec(token) : null;
907
981
  if (match === null) {
908
982
  throw new IntegrityError(
909
- `registry row token is not one a schema-${schemaVersion ?? REGISTRY_SCHEMA_VERSION} row holds (${shown(JSON.stringify(token))}): ${ctx}`
983
+ `registry row ${what} is not one a shipped registry writes (${shown(JSON.stringify(token))}): ${ctx}`
910
984
  );
911
985
  }
912
- const born = match.length === 4;
913
- const counter = Number(born ? match[2] : match[1]);
986
+ const counter = Number(match[2]);
914
987
  if (!Number.isSafeInteger(counter)) {
915
- throw new IntegrityError(`registry row token's counter is out of safe-integer range: ${ctx}`);
988
+ throw new IntegrityError(`registry row ${what}'s counter is out of safe-integer range: ${ctx}`);
989
+ }
990
+ return { incarnation: match[1], counter };
991
+ }
992
+ function checkPointerId(pointerId, token, ctx) {
993
+ const parts = tokenParts(pointerId, ctx, "pointerId");
994
+ if (parts.incarnation !== token.incarnation) {
995
+ throw new IntegrityError(
996
+ `registry row pointerId names another incarnation than its token: ${ctx}`
997
+ );
998
+ }
999
+ if (parts.counter > token.counter) {
1000
+ throw new IntegrityError(`registry row pointerId is a write after its token: ${ctx}`);
916
1001
  }
917
- return { incarnation: born ? match[1] : void 0, counter };
918
1002
  }
919
1003
  function drawHex(entropy, bytes) {
920
1004
  const drawn = entropy(bytes);
@@ -934,12 +1018,11 @@ function drawWrite(entropy) {
934
1018
  return drawHex(entropy, WRITE_BYTES);
935
1019
  }
936
1020
  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}`;
1021
+ const { incarnation, counter } = tokenParts(record.token, record.segment);
1022
+ return `${incarnation}.${counter + 1}.${drawWrite(entropy)}`;
940
1023
  }
941
1024
  function newIncarnationToken(entropy, tombstone) {
942
- const counter = tombstone === void 0 ? 0 : tokenParts(tombstone.token, void 0, tombstone.segment).counter + 1;
1025
+ const counter = tombstone === void 0 ? 0 : tokenParts(tombstone.token, tombstone.segment).counter + 1;
943
1026
  return `${drawIncarnation(entropy)}.${counter}.${drawWrite(entropy)}`;
944
1027
  }
945
1028
  function shown(v) {
@@ -957,7 +1040,7 @@ function parseRegistryEnvelope(text, ctx, expected) {
957
1040
  throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
958
1041
  }
959
1042
  const env = parsed;
960
- const schemaVersion = assertRegistrySchemaVersion(env.schemaVersion, ctx);
1043
+ assertRegistrySchemaVersion(env.schemaVersion, ctx);
961
1044
  const extra = Object.keys(env).filter((k) => !ENVELOPE_FIELDS.includes(k));
962
1045
  if (extra.length > 0) {
963
1046
  throw new IntegrityError(
@@ -968,11 +1051,11 @@ function parseRegistryEnvelope(text, ctx, expected) {
968
1051
  throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
969
1052
  }
970
1053
  const r = env.record;
971
- assertStoredRecordShape(r, ctx, schemaVersion);
1054
+ assertStoredRecordShape(r, ctx);
972
1055
  if (typeof r.token !== "string") {
973
1056
  throw new IntegrityError(`registry row is missing its token: ${ctx}`);
974
1057
  }
975
- tokenParts(r.token, schemaVersion, ctx);
1058
+ checkPointerId(r.pointerId, tokenParts(r.token, ctx), ctx);
976
1059
  if (expected !== void 0 && (r.segment !== expected.segment || (r.namespace ?? void 0) !== (expected.namespace ?? void 0))) {
977
1060
  throw new IntegrityError(
978
1061
  `registry row names another segment than the one it is stored for: ${ctx}`
@@ -996,11 +1079,12 @@ function recordFromNew(ref, rec, now, token) {
996
1079
  leases: void 0,
997
1080
  createdAt: now,
998
1081
  updatedAt: now,
999
- token
1082
+ token,
1083
+ pointerId: token
1000
1084
  };
1001
1085
  }
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") {
1086
+ function assertStoredRecordShape(r, ctx) {
1087
+ 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
1088
  throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
1005
1089
  }
1006
1090
  if (r.currentGen !== null && (!Number.isSafeInteger(r.currentGen) || r.currentGen < 0)) {
@@ -1011,11 +1095,11 @@ function assertStoredRecordShape(r, ctx, schemaVersion = REGISTRY_SCHEMA_VERSION
1011
1095
  if (!STATUSES.includes(r.status)) {
1012
1096
  throw new IntegrityError(`registry record has an unknown status (${shown(r.status)}): ${ctx}`);
1013
1097
  }
1014
- const declared = fieldsOf(schemaVersion);
1098
+ const declared = RECORD_FIELDS;
1015
1099
  const extra = Object.keys(r).filter((k) => !declared.includes(k));
1016
1100
  if (extra.length > 0) {
1017
1101
  throw new IntegrityError(
1018
- `registry record has fields a schema-${schemaVersion} row does not declare (${shown(extra.join(", "))}): ${ctx}`
1102
+ `registry record has fields a schema-${REGISTRY_SCHEMA_VERSION} row does not declare (${shown(extra.join(", "))}): ${ctx}`
1019
1103
  );
1020
1104
  }
1021
1105
  if (r.keyId !== void 0 && typeof r.keyId !== "string") {
@@ -1076,7 +1160,8 @@ function applyRegistryPatch(prev, patch, now, token) {
1076
1160
  leases,
1077
1161
  createdAt: prev.createdAt,
1078
1162
  updatedAt: now,
1079
- token
1163
+ token,
1164
+ pointerId: renewsPointer(patch) ? token : prev.pointerId
1080
1165
  };
1081
1166
  }
1082
1167
 
@@ -1125,24 +1210,12 @@ export {
1125
1210
  isCapabilityError,
1126
1211
  isBudgetExceededError,
1127
1212
  isKeyUnavailableError,
1213
+ PREAMBLE_BYTES,
1214
+ FOOTER_BYTES,
1128
1215
  encodeNameForKey,
1129
1216
  decodeNameFromKey,
1130
1217
  encodeNameForPath,
1131
1218
  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
1219
  MAX_NAME_LENGTH,
1147
1220
  encodedNameLength,
1148
1221
  validateSegmentRef,
@@ -1162,6 +1235,31 @@ export {
1162
1235
  metadataBytes,
1163
1236
  copiedMetadata,
1164
1237
  metadataFromBytes,
1238
+ sizePart,
1239
+ fingerprintFor,
1240
+ fingerprintParts,
1241
+ compactFingerprint,
1242
+ incarnationOf,
1243
+ incarnationField,
1244
+ sameIncarnation,
1245
+ RESOLVED_FIELDS,
1246
+ renewsPointer,
1247
+ renewPointer,
1248
+ pointerIdOf,
1249
+ MAX_LEASE_MS,
1250
+ LEASE_SKEW_MS,
1251
+ MAX_LEASES_PER_SEGMENT,
1252
+ UNANSWERED_RESENDS,
1253
+ UNANSWERED_BASE_MS,
1254
+ isLive,
1255
+ heldGenerations,
1256
+ onlyLeasesDiffer,
1257
+ leaseChurn,
1258
+ takeLease,
1259
+ releaseLease,
1260
+ MAX_KEPT_GENERATIONS,
1261
+ usableKeptGens,
1262
+ nextKept,
1165
1263
  validateNewRegistryRecord,
1166
1264
  validateRegistryPatch,
1167
1265
  serializeRegistryEnvelope,
@@ -1178,4 +1276,4 @@ export {
1178
1276
  namespaceKeyPart,
1179
1277
  namespacePathPart
1180
1278
  };
1181
- //# sourceMappingURL=chunk-TNWTIAON.js.map
1279
+ //# sourceMappingURL=chunk-YSC7NJWV.js.map