@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.
- package/README.md +85 -0
- package/dist/{chunk-RSLVTRJY.js → chunk-IGLIZBR7.js} +982 -281
- package/dist/chunk-IGLIZBR7.js.map +7 -0
- package/dist/core/absent-operands.d.ts +8 -0
- package/dist/core/audit.d.ts +67 -4
- package/dist/core/bit-route.d.ts +2 -1
- package/dist/core/blob.d.ts +12 -1
- package/dist/core/bulk-load.d.ts +148 -0
- package/dist/core/cas-retry.d.ts +27 -0
- package/dist/core/check.d.ts +22 -0
- package/dist/core/chunk-checks.d.ts +6 -1
- package/dist/core/chunk-window.d.ts +11 -4
- package/dist/core/combine-expr.d.ts +1 -3
- package/dist/core/combine-feed.d.ts +2 -16
- package/dist/core/combine-many.d.ts +21 -15
- package/dist/core/concurrency.d.ts +8 -0
- package/dist/core/consistency.d.ts +76 -18
- package/dist/core/crbm/fingerprint.d.ts +23 -0
- package/dist/core/crbm/format.d.ts +2 -5
- package/dist/core/crbm/frame.d.ts +10 -0
- package/dist/core/crbm/plan-reads.d.ts +9 -3
- package/dist/core/crbm/reader.d.ts +51 -12
- package/dist/core/crbm/varint.d.ts +2 -1
- package/dist/core/crbm/writer.d.ts +5 -1
- package/dist/core/crbm-open.d.ts +154 -0
- package/dist/core/crbm-storage-source.d.ts +243 -490
- package/dist/core/crbm-write.d.ts +75 -0
- package/dist/core/crypto.d.ts +6 -1
- package/dist/core/engine.d.ts +239 -47
- package/dist/core/erase-id.d.ts +97 -27
- package/dist/core/erasure.d.ts +4 -3
- package/dist/core/errors.d.ts +8 -2
- package/dist/core/generation-gc.d.ts +131 -16
- package/dist/core/generation-list.d.ts +11 -0
- package/dist/core/id-window.d.ts +46 -0
- package/dist/core/invalidation-scope.d.ts +77 -0
- package/dist/core/known-generations-shape.d.ts +13 -0
- package/dist/core/known-generations.d.ts +147 -0
- package/dist/core/leases.d.ts +9 -1
- package/dist/core/ledger-seed.d.ts +17 -0
- package/dist/core/load.d.ts +17 -36
- package/dist/core/lru.d.ts +2 -15
- package/dist/core/metrics.d.ts +9 -7
- package/dist/core/order.d.ts +2 -0
- package/dist/core/pinned-storage-source.d.ts +7 -7
- package/dist/core/pointer-id.d.ts +19 -0
- package/dist/core/ports.d.ts +210 -45
- package/dist/core/publish.d.ts +207 -0
- package/dist/core/reader-defaults.d.ts +14 -3
- package/dist/core/registry-scan.d.ts +0 -2
- package/dist/core/retention-sweep.d.ts +69 -65
- package/dist/core/retention.d.ts +1 -1
- package/dist/core/retry.d.ts +12 -19
- package/dist/core/rollback.d.ts +62 -32
- package/dist/core/summary.d.ts +74 -25
- package/dist/core/token.d.ts +13 -8
- package/dist/core/validate.d.ts +2 -0
- package/dist/driver-kit.d.ts +6 -1
- package/dist/driver-kit.js +90 -10
- package/dist/driver-kit.js.map +3 -3
- package/dist/drivers/_shared/bytes.d.ts +6 -0
- package/dist/drivers/_shared/faults.d.ts +15 -0
- package/dist/drivers/_shared/object-keys.d.ts +18 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
- package/dist/drivers/_shared/object-registry.d.ts +3 -1
- package/dist/drivers/_shared/options.d.ts +19 -0
- package/dist/drivers/_shared/registry.d.ts +31 -19
- package/dist/drivers/localfs/exact-case.d.ts +0 -3
- package/dist/drivers/localfs/fs-util.d.ts +22 -0
- package/dist/drivers/localfs/registry.d.ts +10 -4
- package/dist/drivers/localfs/storage.d.ts +16 -8
- package/dist/drivers/memory.d.ts +14 -9
- 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 +4303 -2938
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-RSLVTRJY.js.map +0 -7
- package/dist/core/cost.d.ts +0 -351
- 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
|
|
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
|
|
261
|
+
for (const b of UTF8.encode(encoded[i])) bytes.push(b);
|
|
146
262
|
i += 1;
|
|
147
263
|
}
|
|
148
264
|
}
|
|
149
|
-
return
|
|
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
|
-
|
|
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
|
|
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/
|
|
353
|
-
var
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
360
|
-
return
|
|
702
|
+
function namespacePathPart(namespace) {
|
|
703
|
+
return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);
|
|
361
704
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
`${
|
|
710
|
+
`${name} needs an options object \u2014 got ${options === null ? "null" : typeof options}`
|
|
383
711
|
);
|
|
384
712
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
717
|
+
`${name} does not take ${list(unknown)}. It takes ${list(keys)}${hint === void 0 ? "" : `; ${hint}`}.`
|
|
399
718
|
);
|
|
400
719
|
}
|
|
401
720
|
}
|
|
402
|
-
function
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
}
|
|
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
|
|
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 :
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
1269
|
+
function validateWrappedDeks(value, origin) {
|
|
671
1270
|
if (value === void 0) return;
|
|
672
1271
|
const fail = (msg) => {
|
|
673
|
-
throw
|
|
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,
|
|
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
|
|
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" || !
|
|
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({
|
|
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 (
|
|
1363
|
+
if (origin === "stored") return s;
|
|
747
1364
|
const metadata = Object.freeze(JSON.parse(canonical));
|
|
748
|
-
return Object.freeze({
|
|
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
|
|
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
|
|
1394
|
+
throw origin === "stored" ? new IntegrityError(`registry record knownGens: ${msg}${where}`) : new ValidationError(`knownGens: ${msg}`);
|
|
772
1395
|
};
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
1441
|
+
origin === "stored" ? entry : Object.freeze({
|
|
822
1442
|
holder,
|
|
823
1443
|
generation,
|
|
824
1444
|
until
|
|
825
1445
|
})
|
|
826
1446
|
);
|
|
827
1447
|
}
|
|
828
|
-
if (
|
|
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
|
|
837
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
844
|
-
|
|
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
|
-
|
|
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,
|
|
1491
|
+
validateWrappedDeks(rec.wrappedDeks, "caller");
|
|
854
1492
|
validateGovernance(rec.retention, "retention");
|
|
855
1493
|
validateGovernance(rec.residency, "residency");
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
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 { ...
|
|
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,
|
|
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
|
|
872
|
-
const withLeases = "leases" in
|
|
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,
|
|
1514
|
+
return { ...withLeases, summary: validateSummary(patch.summary, "caller") };
|
|
875
1515
|
}
|
|
876
|
-
var REGISTRY_SCHEMA_VERSION =
|
|
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 <
|
|
883
|
-
throw new IntegrityError(`registry row has a malformed schemaVersion (${
|
|
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,
|
|
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;
|
|
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
|
|
1549
|
+
`registry row ${what} is not one a shipped registry writes (${shown3(JSON.stringify(token))}): ${ctx}`
|
|
910
1550
|
);
|
|
911
1551
|
}
|
|
912
|
-
const
|
|
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
|
|
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,
|
|
938
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 (${
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 (${
|
|
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 (${
|
|
1662
|
+
throw new IntegrityError(`registry record has an unknown status (${shown3(r.status)}): ${ctx}`);
|
|
1013
1663
|
}
|
|
1014
|
-
const declared =
|
|
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-${
|
|
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,
|
|
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,
|
|
1032
|
-
|
|
1033
|
-
validateLeases(r.leases,
|
|
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
|
|
1048
|
-
if ("
|
|
1049
|
-
|
|
1050
|
-
|
|
1697
|
+
let knownGens;
|
|
1698
|
+
if ("knownGens" in patch) {
|
|
1699
|
+
validateKnownGensFit(patch.knownGens, currentGen, summary);
|
|
1700
|
+
knownGens = patch.knownGens;
|
|
1051
1701
|
} else {
|
|
1052
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1882
|
+
//# sourceMappingURL=chunk-IGLIZBR7.js.map
|