@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.
- package/dist/{chunk-TNWTIAON.js → chunk-YSC7NJWV.js} +314 -216
- package/dist/chunk-YSC7NJWV.js.map +7 -0
- package/dist/core/audit.d.ts +56 -3
- package/dist/core/combine-many.d.ts +13 -4
- package/dist/core/consistency.d.ts +10 -4
- package/dist/core/crbm/fingerprint.d.ts +18 -0
- package/dist/core/crbm/format.d.ts +2 -4
- package/dist/core/crbm/frame.d.ts +10 -0
- package/dist/core/crbm/reader.d.ts +1 -3
- package/dist/core/crbm-storage-source.d.ts +249 -96
- package/dist/core/engine.d.ts +22 -14
- package/dist/core/erase-id.d.ts +62 -26
- package/dist/core/generation-gc.d.ts +3 -2
- package/dist/core/keys.d.ts +28 -0
- package/dist/core/leases.d.ts +7 -0
- package/dist/core/load.d.ts +11 -7
- package/dist/core/metrics.d.ts +5 -5
- package/dist/core/pinned-storage-source.d.ts +3 -3
- package/dist/core/pointer-id.d.ts +19 -0
- package/dist/core/ports.d.ts +131 -35
- package/dist/core/reader-defaults.d.ts +14 -3
- package/dist/core/rollback.d.ts +2 -1
- package/dist/core/summary.d.ts +38 -21
- package/dist/core/token.d.ts +13 -8
- package/dist/driver-kit.d.ts +2 -1
- package/dist/driver-kit.js +11 -4
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +1 -1
- package/dist/drivers/_shared/registry.d.ts +26 -17
- package/dist/drivers/localfs/storage.d.ts +7 -6
- package/dist/drivers/memory.d.ts +8 -6
- package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
- package/dist/export/index.d.ts +16 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1288 -1080
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-TNWTIAON.js.map +0 -7
- 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/
|
|
502
|
-
var
|
|
503
|
-
var
|
|
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
|
|
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
|
|
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({
|
|
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({
|
|
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 =
|
|
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 <
|
|
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,
|
|
904
|
-
const
|
|
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
|
|
983
|
+
`registry row ${what} is not one a shipped registry writes (${shown(JSON.stringify(token))}): ${ctx}`
|
|
910
984
|
);
|
|
911
985
|
}
|
|
912
|
-
const
|
|
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
|
|
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,
|
|
938
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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 =
|
|
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-${
|
|
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-
|
|
1279
|
+
//# sourceMappingURL=chunk-YSC7NJWV.js.map
|