@cloudbitmaps/core 0.18.2 → 0.19.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-ML6AT53O.js → chunk-TNWTIAON.js} +50 -9
- package/dist/chunk-TNWTIAON.js.map +7 -0
- package/dist/core/audit.d.ts +13 -3
- package/dist/core/bit-route.d.ts +5 -5
- package/dist/core/blob.d.ts +4 -1
- package/dist/core/budget.d.ts +7 -2
- package/dist/core/combine-feed.d.ts +3 -7
- package/dist/core/combine-many.d.ts +22 -29
- package/dist/core/consistency.d.ts +0 -1
- package/dist/core/cost.d.ts +13 -2
- package/dist/core/crbm/reader.d.ts +4 -3
- package/dist/core/crbm-storage-source.d.ts +86 -13
- package/dist/core/discover.d.ts +3 -5
- package/dist/core/engine.d.ts +15 -0
- package/dist/core/erase-id.d.ts +7 -5
- package/dist/core/erasure.d.ts +22 -2
- package/dist/core/errors.d.ts +10 -2
- package/dist/core/load-input.d.ts +1 -2
- package/dist/core/load.d.ts +48 -3
- package/dist/core/metrics.d.ts +19 -7
- package/dist/core/ports.d.ts +21 -62
- package/dist/core/retention-sweep.d.ts +9 -4
- package/dist/core/retention.d.ts +17 -0
- package/dist/core/retry.d.ts +6 -0
- package/dist/driver-kit.d.ts +1 -1
- package/dist/driver-kit.js +13 -102
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +1 -19
- package/dist/drivers/_shared/registry.d.ts +5 -2
- package/dist/drivers/localfs/fs-util.d.ts +7 -0
- package/dist/drivers/localfs/registry.d.ts +1 -0
- package/dist/index.d.ts +5 -9
- package/dist/index.js +864 -600
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-ML6AT53O.js.map +0 -7
- package/dist/core/combine-held.d.ts +0 -43
- package/dist/core/registry-reap.d.ts +0 -22
package/dist/index.js
CHANGED
|
@@ -34,8 +34,11 @@ import {
|
|
|
34
34
|
entropyIsAvailable,
|
|
35
35
|
heldGenerations,
|
|
36
36
|
incarnationOf,
|
|
37
|
+
isBudgetExceededError,
|
|
38
|
+
isCapabilityError,
|
|
37
39
|
isCloudRoaringError,
|
|
38
40
|
isIntegrityError,
|
|
41
|
+
isKeyUnavailableError,
|
|
39
42
|
isLeaseExpiredError,
|
|
40
43
|
isLeaseLimitError,
|
|
41
44
|
isLive,
|
|
@@ -44,6 +47,7 @@ import {
|
|
|
44
47
|
isStaleOperandError,
|
|
45
48
|
isStorageBackend,
|
|
46
49
|
isTransientError,
|
|
50
|
+
isUnsupportedError,
|
|
47
51
|
isValidationError,
|
|
48
52
|
isWriteConflictError,
|
|
49
53
|
leaseChurn,
|
|
@@ -70,7 +74,7 @@ import {
|
|
|
70
74
|
validateUserNamespace,
|
|
71
75
|
validateUserRef,
|
|
72
76
|
webCryptoEntropy
|
|
73
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-TNWTIAON.js";
|
|
74
78
|
|
|
75
79
|
// src/core/bit-route.ts
|
|
76
80
|
var U32_MAX = 4294967295;
|
|
@@ -91,6 +95,20 @@ function joinId(chunkKey, remainder) {
|
|
|
91
95
|
var DEFAULT_BUDGET = { maxRequests: 1e6 };
|
|
92
96
|
function resolveBudget(opt, fallback) {
|
|
93
97
|
if (opt === false) return null;
|
|
98
|
+
if (opt !== void 0 && opt !== null) {
|
|
99
|
+
if (typeof opt !== "object" || Array.isArray(opt)) {
|
|
100
|
+
const got = Array.isArray(opt) ? "an array" : `a ${typeof opt}`;
|
|
101
|
+
throw new ValidationError(`budget must be { maxRequests } or false; got ${got}`);
|
|
102
|
+
}
|
|
103
|
+
const unknown = Object.keys(opt).filter(
|
|
104
|
+
(k) => k !== "maxRequests" && opt[k] !== void 0
|
|
105
|
+
);
|
|
106
|
+
if (unknown.length > 0) {
|
|
107
|
+
throw new ValidationError(
|
|
108
|
+
`budget: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; it takes { maxRequests } or false`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
94
112
|
const maxRequests = opt?.maxRequests ?? fallback.maxRequests;
|
|
95
113
|
if (!Number.isInteger(maxRequests) || maxRequests < 1) {
|
|
96
114
|
throw new ValidationError(`budget.maxRequests must be a positive integer; got ${maxRequests}`);
|
|
@@ -98,7 +116,7 @@ function resolveBudget(opt, fallback) {
|
|
|
98
116
|
return { maxRequests };
|
|
99
117
|
}
|
|
100
118
|
function resolvePerOpBudget(opt, storeBudget) {
|
|
101
|
-
if (opt === void 0) return storeBudget;
|
|
119
|
+
if (opt === void 0 || opt === null) return storeBudget;
|
|
102
120
|
return resolveBudget(opt, storeBudget ?? DEFAULT_BUDGET);
|
|
103
121
|
}
|
|
104
122
|
function checkBudget(budget, projectedRequests, op) {
|
|
@@ -326,6 +344,50 @@ function shardOf(key, totalShards) {
|
|
|
326
344
|
return (h >>> 0) % totalShards;
|
|
327
345
|
}
|
|
328
346
|
|
|
347
|
+
// src/core/audit.ts
|
|
348
|
+
var NOOP_AUDIT = {
|
|
349
|
+
onEvent() {
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
function safeAudit(sink) {
|
|
353
|
+
if (sink === NOOP_AUDIT) return sink;
|
|
354
|
+
return {
|
|
355
|
+
onEvent(event) {
|
|
356
|
+
try {
|
|
357
|
+
ignoreRejection(sink.onEvent(event));
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function checkedAuditSink(sink, op) {
|
|
364
|
+
if (sink === void 0 || sink === null) return;
|
|
365
|
+
if (typeof sink.onEvent !== "function") {
|
|
366
|
+
throw new ValidationError(
|
|
367
|
+
`${op}: audit must be a sink with an onEvent(event) method, such as a RecordingAuditSink`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function ignoreRejection(returned) {
|
|
372
|
+
if (typeof returned?.then === "function") {
|
|
373
|
+
returned.then(void 0, () => {
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
var RecordingAuditSink = class {
|
|
378
|
+
recorded = [];
|
|
379
|
+
onEvent(event) {
|
|
380
|
+
this.recorded.push(event);
|
|
381
|
+
}
|
|
382
|
+
/** An independent copy of the recorded events, in emission order. */
|
|
383
|
+
snapshot() {
|
|
384
|
+
return this.recorded.map((e) => ({ ...e }));
|
|
385
|
+
}
|
|
386
|
+
reset() {
|
|
387
|
+
this.recorded.length = 0;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
329
391
|
// src/core/metrics.ts
|
|
330
392
|
var NOOP_METRICS = {
|
|
331
393
|
onEvent() {
|
|
@@ -333,10 +395,15 @@ var NOOP_METRICS = {
|
|
|
333
395
|
};
|
|
334
396
|
function safeMetrics(sink) {
|
|
335
397
|
if (sink === NOOP_METRICS) return sink;
|
|
398
|
+
if (typeof sink?.onEvent !== "function") {
|
|
399
|
+
throw new ValidationError(
|
|
400
|
+
"metrics must be a sink with an onEvent(event) method, such as a CountingMetricsSink"
|
|
401
|
+
);
|
|
402
|
+
}
|
|
336
403
|
return {
|
|
337
404
|
onEvent(event) {
|
|
338
405
|
try {
|
|
339
|
-
sink.onEvent(event);
|
|
406
|
+
ignoreRejection(sink.onEvent(event));
|
|
340
407
|
} catch {
|
|
341
408
|
}
|
|
342
409
|
}
|
|
@@ -347,7 +414,8 @@ var OP_NAMES = [
|
|
|
347
414
|
"count",
|
|
348
415
|
"intersectInto",
|
|
349
416
|
"unionInto",
|
|
350
|
-
"andNotInto"
|
|
417
|
+
"andNotInto",
|
|
418
|
+
"materializeMany"
|
|
351
419
|
];
|
|
352
420
|
var CountingMetricsSink = class _CountingMetricsSink {
|
|
353
421
|
storage = { gets: 0, bytes: 0, totalMs: 0 };
|
|
@@ -586,6 +654,7 @@ var SegmentEngine = class {
|
|
|
586
654
|
let total2 = 0;
|
|
587
655
|
for (const [k, n] of cardinalities) {
|
|
588
656
|
assertChunkKeyInRange(k);
|
|
657
|
+
if (n !== 0) assertChunkCardinalityInRange(n);
|
|
589
658
|
total2 += n;
|
|
590
659
|
}
|
|
591
660
|
return total2;
|
|
@@ -625,7 +694,7 @@ var SegmentEngine = class {
|
|
|
625
694
|
chunkWindow(seg, chunkKeys, gen, ramp) {
|
|
626
695
|
return new ChunkWindow(
|
|
627
696
|
chunkKeys,
|
|
628
|
-
(chunkKey) => this.storageChunk({ ...seg, chunkKey }, gen),
|
|
697
|
+
(chunkKey) => this.storageChunk({ ...seg, chunkKey }, gen, true),
|
|
629
698
|
DEFAULT_INTERSECT_CONCURRENCY,
|
|
630
699
|
ramp
|
|
631
700
|
);
|
|
@@ -1092,7 +1161,9 @@ var SegmentEngine = class {
|
|
|
1092
1161
|
}
|
|
1093
1162
|
if (acc.isEmpty) return null;
|
|
1094
1163
|
if (relevant.length > 0) {
|
|
1095
|
-
const cuts = together ? fetched.slice(present.length) : await Promise.all(
|
|
1164
|
+
const cuts = together ? fetched.slice(present.length) : await Promise.all(
|
|
1165
|
+
relevant.map((e) => this.storageChunk({ ...e.seg, chunkKey }, e.gen, true))
|
|
1166
|
+
);
|
|
1096
1167
|
for (const cut of cuts) {
|
|
1097
1168
|
if (cut === null) continue;
|
|
1098
1169
|
acc.andNotInPlace(cut);
|
|
@@ -1106,7 +1177,7 @@ var SegmentEngine = class {
|
|
|
1106
1177
|
* so a stream's take is made before anything is awaited: concurrent takes of one stream must be made in key order.
|
|
1107
1178
|
*/
|
|
1108
1179
|
operandChunk(op, chunkKey) {
|
|
1109
|
-
return op.streamed === void 0 ? this.storageChunk({ ...op.seg, chunkKey }, op.gen) : this.streamedChunk(op.streamed, chunkKey);
|
|
1180
|
+
return op.streamed === void 0 ? this.storageChunk({ ...op.seg, chunkKey }, op.gen, true) : this.streamedChunk(op.streamed, chunkKey);
|
|
1110
1181
|
}
|
|
1111
1182
|
/**
|
|
1112
1183
|
* `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
|
|
@@ -1180,15 +1251,16 @@ var SegmentEngine = class {
|
|
|
1180
1251
|
if (!streamed.opened) {
|
|
1181
1252
|
const cache = this.cache;
|
|
1182
1253
|
if (cache) {
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
if (hit) return
|
|
1254
|
+
const ref = { ...streamed.seg, chunkKey };
|
|
1255
|
+
const hit = cache.get(this.chunkCacheKey(ref, streamed.gen));
|
|
1256
|
+
if (hit) return this.cachedIfCurrent(ref, streamed.gen, hit, true, streamed);
|
|
1257
|
+
if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: false });
|
|
1186
1258
|
}
|
|
1187
1259
|
this.startStream(streamed, chunkKey);
|
|
1188
1260
|
} else if (streamed.inStream !== void 0 && !streamed.inStream.has(chunkKey)) {
|
|
1189
1261
|
const ref = { ...streamed.seg, chunkKey };
|
|
1190
1262
|
const hit = this.cache?.get(this.chunkCacheKey(ref, streamed.gen));
|
|
1191
|
-
return hit ?
|
|
1263
|
+
return hit ? this.cachedIfCurrent(ref, streamed.gen, hit, false, streamed) : this.storageChunk(ref, streamed.gen, true, false);
|
|
1192
1264
|
}
|
|
1193
1265
|
return streamed.stream.take(chunkKey).then((read) => {
|
|
1194
1266
|
if (read.bytes === null) return null;
|
|
@@ -1210,6 +1282,28 @@ var SegmentEngine = class {
|
|
|
1210
1282
|
streamedVersion(planned, read) {
|
|
1211
1283
|
return planned === void 0 || this.storage.currentVersion === void 0 ? planned : read;
|
|
1212
1284
|
}
|
|
1285
|
+
/**
|
|
1286
|
+
* `hit`, a chunk cached under the version a multi-chunk read planned under, if that is still the segment's version;
|
|
1287
|
+
* else the chunk as it is now. A read resolves the segment again before it serves each chunk, which the source's
|
|
1288
|
+
* stream does for the chunks it delivers; a chunk served from the cache is checked here, or a read whose chunks are
|
|
1289
|
+
* all cached would go on serving the generation it planned under, an erased id included, for as long as it is pulled.
|
|
1290
|
+
* Within `cache.genTtlMs` the source answers from its snapshot, so the check is a lookup, not a request. `report`:
|
|
1291
|
+
* emit the `cache` event for this lookup, which a key counted when its stream opened does not. `streamed`: the read
|
|
1292
|
+
* this chunk belongs to, which a move carries to the version now current, so the rest of it looks there: a read whose
|
|
1293
|
+
* stream is not yet open reads the rest as one stream, not a chunk at a time.
|
|
1294
|
+
*/
|
|
1295
|
+
async cachedIfCurrent(ref, planned, hit, report, streamed) {
|
|
1296
|
+
const now = planned === void 0 ? void 0 : await this.cacheVersion(ref);
|
|
1297
|
+
if (now === planned) {
|
|
1298
|
+
if (report && this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: true });
|
|
1299
|
+
return hit;
|
|
1300
|
+
}
|
|
1301
|
+
if (streamed !== void 0 && streamed.gen === planned && now !== null) {
|
|
1302
|
+
streamed.gen = now;
|
|
1303
|
+
if (streamed.opened && this.storage.currentVersion === void 0) streamed.invalidated = true;
|
|
1304
|
+
}
|
|
1305
|
+
return this.storageChunk(ref, now, false, report);
|
|
1306
|
+
}
|
|
1213
1307
|
/** The cache key of a chunk: by the version it was read under, or by segment and key alone for a source with none. */
|
|
1214
1308
|
chunkCacheKey(ref, version) {
|
|
1215
1309
|
return version === void 0 ? chunkRefKey(ref) : chunkGenKey(ref, version);
|
|
@@ -1249,7 +1343,7 @@ var SegmentEngine = class {
|
|
|
1249
1343
|
if (absent.length === 0) return;
|
|
1250
1344
|
const named = absent.map((s) => s.namespace === void 0 ? `"${s.segment}"` : `"${s.namespace}/${s.segment}"`).join(", ");
|
|
1251
1345
|
throw new ValidationError(
|
|
1252
|
-
`${op}: operand ${named} names a segment that does not exist, so it would contribute nothing \u2014 an exclude would suppress no ids and an include would contribute none. Check the name and the namespace (a segment addressed without its \`namespace\` is a DIFFERENT segment). Pass \`allowAbsentOperands: true\` if you meant it.`
|
|
1346
|
+
`${op}: operand ${named} names a segment that does not exist or was dropped, so it would contribute nothing \u2014 an exclude would suppress no ids and an include would contribute none. Check the name and the namespace (a segment addressed without its \`namespace\` is a DIFFERENT segment). Pass \`allowAbsentOperands: true\` if you meant it.`
|
|
1253
1347
|
);
|
|
1254
1348
|
}
|
|
1255
1349
|
/**
|
|
@@ -1307,17 +1401,22 @@ var SegmentEngine = class {
|
|
|
1307
1401
|
*
|
|
1308
1402
|
* The returned instance is **shared** (it may be the cached one, and every caller that joined a read gets the same
|
|
1309
1403
|
* one): callers read it or clone it, never mutate it.
|
|
1404
|
+
*
|
|
1405
|
+
* `recheck`: `gen` was planned for a read of many chunks, so a cached chunk is served only while it is still the
|
|
1406
|
+
* segment's version ({@link cachedIfCurrent}). A point read resolves `gen` just before it asks, and passes none.
|
|
1407
|
+
* `report`: emit the `cache` event for this lookup; a key already counted for this read does not.
|
|
1310
1408
|
*/
|
|
1311
|
-
async storageChunk(ref, gen) {
|
|
1409
|
+
async storageChunk(ref, gen, recheck = false, report = true) {
|
|
1312
1410
|
if (gen === null) return null;
|
|
1313
1411
|
const cacheKey = this.chunkCacheKey(ref, gen);
|
|
1314
1412
|
if (this.cache) {
|
|
1315
1413
|
const cached = this.cache.get(cacheKey);
|
|
1316
1414
|
if (cached) {
|
|
1317
|
-
if (
|
|
1415
|
+
if (recheck) return this.cachedIfCurrent(ref, gen, cached, report);
|
|
1416
|
+
if (report && this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: true });
|
|
1318
1417
|
return cached;
|
|
1319
1418
|
}
|
|
1320
|
-
if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: false });
|
|
1419
|
+
if (report && this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: false });
|
|
1321
1420
|
}
|
|
1322
1421
|
const open4 = this.openReads.get(cacheKey);
|
|
1323
1422
|
if (open4) return open4.read;
|
|
@@ -1657,7 +1756,7 @@ async function reconcile(ref, deps, options) {
|
|
|
1657
1756
|
try {
|
|
1658
1757
|
await deps.registry.compareAndSwap(ref, token, { keptGens: present });
|
|
1659
1758
|
} catch (err) {
|
|
1660
|
-
if (!(err
|
|
1759
|
+
if (!isWriteConflictError(err)) throw err;
|
|
1661
1760
|
}
|
|
1662
1761
|
}
|
|
1663
1762
|
return deleted;
|
|
@@ -1741,11 +1840,11 @@ var STORE_LOAD_POINTER_READS = 2 + 2 / LIST_COLLECTION_CADENCE;
|
|
|
1741
1840
|
var STORE_LOAD_EXISTENCE_CHECKS = 2 - 1 / LIST_COLLECTION_CADENCE;
|
|
1742
1841
|
var RETENTION_SWEEP_REQUESTS = deepFreeze({
|
|
1743
1842
|
conditionalDelete: {
|
|
1744
|
-
retirement: { reads:
|
|
1843
|
+
retirement: { reads: 7, writes: 2, deletes: 1 },
|
|
1745
1844
|
purge: { reads: 4, writes: 0, deletes: 2 }
|
|
1746
1845
|
},
|
|
1747
1846
|
tombstoning: {
|
|
1748
|
-
retirement: { reads:
|
|
1847
|
+
retirement: { reads: 6, writes: 2, deletes: 0 },
|
|
1749
1848
|
purge: { reads: 3, writes: 1, deletes: 0 }
|
|
1750
1849
|
}
|
|
1751
1850
|
});
|
|
@@ -1926,6 +2025,11 @@ function buildReport(input) {
|
|
|
1926
2025
|
const storedGiB = storageBytes / GIB;
|
|
1927
2026
|
const baseline = resolveBaseline(redis, storedGiB);
|
|
1928
2027
|
const baselineUSD = baseline.monthlyUSD;
|
|
2028
|
+
if ((input.workload.cacheHitRate ?? 0) > 1) {
|
|
2029
|
+
throw new ValidationError(
|
|
2030
|
+
`cacheHitRate must be a fraction from 0 to 1; got ${String(input.workload.cacheHitRate)}`
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
1929
2033
|
const cacheHitRate = clamp01(
|
|
1930
2034
|
requireFiniteNonNeg(input.workload.cacheHitRate ?? 0, "cacheHitRate")
|
|
1931
2035
|
);
|
|
@@ -2060,7 +2164,31 @@ function buildReport(input) {
|
|
|
2060
2164
|
assumptions: { cacheHitRate, pricingName: pricing.name, grounded, notes }
|
|
2061
2165
|
};
|
|
2062
2166
|
}
|
|
2167
|
+
function checkModelInputs(op, pricing, workload) {
|
|
2168
|
+
const storage = pricing !== null && typeof pricing === "object" ? pricing.storage : void 0;
|
|
2169
|
+
if (pricing !== void 0 && (storage === null || typeof storage !== "object")) {
|
|
2170
|
+
throw new ValidationError(`${op}: pricing must be a profile with a storage price list`);
|
|
2171
|
+
}
|
|
2172
|
+
if (workload !== void 0 && workload !== null && (typeof workload !== "object" || Array.isArray(workload))) {
|
|
2173
|
+
throw new ValidationError(`${op}: workload must be an object`);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2063
2176
|
function estimateCost(input) {
|
|
2177
|
+
if (input === null || typeof input !== "object") {
|
|
2178
|
+
throw new ValidationError("estimateCost: input must be an object such as { segments }");
|
|
2179
|
+
}
|
|
2180
|
+
if (!Array.isArray(input.segments)) {
|
|
2181
|
+
throw new ValidationError(
|
|
2182
|
+
"estimateCost: segments must be an array of { sizeBytes } or { cardinality }"
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
for (let i = 0; i < input.segments.length; i++) {
|
|
2186
|
+
const spec = input.segments[i];
|
|
2187
|
+
if (spec === null || typeof spec !== "object") {
|
|
2188
|
+
throw new ValidationError(`estimateCost: segments[${i}] must be an object`);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
checkModelInputs("estimateCost", input.pricing, input.workload);
|
|
2064
2192
|
const pricing = input.pricing ?? AWS_US_EAST_1_ONDEMAND;
|
|
2065
2193
|
const workload = input.workload ?? {};
|
|
2066
2194
|
let storageBytes = 0;
|
|
@@ -2075,6 +2203,7 @@ function estimateCost(input) {
|
|
|
2075
2203
|
return buildReport({ storageBytes, workload, pricing, grounded: false, sizesFromCardinality });
|
|
2076
2204
|
}
|
|
2077
2205
|
function groundedReport(input) {
|
|
2206
|
+
checkModelInputs("costReport", input.pricing, input.workload);
|
|
2078
2207
|
return buildReport({
|
|
2079
2208
|
storageBytes: input.storageBytes,
|
|
2080
2209
|
workload: input.workload ?? {},
|
|
@@ -2185,7 +2314,22 @@ function requireCodec(codec, api) {
|
|
|
2185
2314
|
var DEFAULT_NDJSON_BATCH_BYTES = 64 * 1024;
|
|
2186
2315
|
var errMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
2187
2316
|
async function runExport(reader, registry, sink, options = {}) {
|
|
2317
|
+
if (sink === null || typeof sink !== "object" || typeof sink.open !== "function") {
|
|
2318
|
+
throw new ValidationError(
|
|
2319
|
+
"runExport: the sink must be an object with an open(ref, ext) method"
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
options ??= {};
|
|
2188
2323
|
if (options.namespace !== void 0) validateUserNamespace(options.namespace);
|
|
2324
|
+
if (options.format !== void 0 && options.format !== "roaring" && options.format !== "ndjson") {
|
|
2325
|
+
throw new ValidationError(`runExport: format must be 'roaring' or 'ndjson'`);
|
|
2326
|
+
}
|
|
2327
|
+
const batch = options.ndjsonBatchBytes;
|
|
2328
|
+
if (batch !== void 0 && (typeof batch !== "number" || !Number.isSafeInteger(batch) || batch < 1)) {
|
|
2329
|
+
throw new ValidationError(
|
|
2330
|
+
"runExport: ndjsonBatchBytes must be a whole number of bytes, 1 or more"
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2189
2333
|
const format = options.format ?? "roaring";
|
|
2190
2334
|
const ext = format === "roaring" ? ".roaring" : ".ndjson";
|
|
2191
2335
|
const batchCap = options.ndjsonBatchBytes ?? DEFAULT_NDJSON_BATCH_BYTES;
|
|
@@ -2389,6 +2533,7 @@ var MemoryRegistryDriver = class {
|
|
|
2389
2533
|
return { token };
|
|
2390
2534
|
}
|
|
2391
2535
|
async *list(namespace) {
|
|
2536
|
+
if (namespace !== void 0) validateSegmentRef({ segment: "x", namespace });
|
|
2392
2537
|
for (const row of this.rows.values()) {
|
|
2393
2538
|
if (namespace === void 0 || row.namespace === namespace) yield structuredClone(row);
|
|
2394
2539
|
}
|
|
@@ -2579,6 +2724,18 @@ async function fsyncDir(dir) {
|
|
|
2579
2724
|
} catch {
|
|
2580
2725
|
}
|
|
2581
2726
|
}
|
|
2727
|
+
async function writeAll(handle, bytes) {
|
|
2728
|
+
let offset = 0;
|
|
2729
|
+
while (offset < bytes.length) {
|
|
2730
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset);
|
|
2731
|
+
if (bytesWritten <= 0) {
|
|
2732
|
+
throw Object.assign(new Error("a write made no progress: the device is full"), {
|
|
2733
|
+
code: "ENOSPC"
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
offset += bytesWritten;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2582
2739
|
|
|
2583
2740
|
// src/drivers/localfs/exact-case.ts
|
|
2584
2741
|
function firstCaseDifference(expected, actual) {
|
|
@@ -2718,7 +2875,7 @@ var LocalFsStorageDriver = class {
|
|
|
2718
2875
|
let size = 0;
|
|
2719
2876
|
const sink = {
|
|
2720
2877
|
async write(bytes) {
|
|
2721
|
-
await handle
|
|
2878
|
+
await writeAll(handle, bytes);
|
|
2722
2879
|
hash.update(bytes);
|
|
2723
2880
|
size += bytes.length;
|
|
2724
2881
|
}
|
|
@@ -2830,8 +2987,8 @@ var LocalFsStorageDriver = class {
|
|
|
2830
2987
|
// src/drivers/localfs/registry.ts
|
|
2831
2988
|
import { constants as FS3 } from "node:fs";
|
|
2832
2989
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2833
|
-
import { mkdir as mkdir2, open as open3, readdir as readdir3, realpath as realpath2, rename, unlink as unlink2 } from "node:fs/promises";
|
|
2834
|
-
import { basename, dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
2990
|
+
import { mkdir as mkdir2, open as open3, opendir, readdir as readdir3, realpath as realpath2, rename, unlink as unlink2 } from "node:fs/promises";
|
|
2991
|
+
import { basename, dirname as dirname3, join as join3, relative as relative2, resolve as resolve2 } from "node:path";
|
|
2835
2992
|
var DEFAULT_MAX_ROW_BYTES = 1 * 1024 * 1024;
|
|
2836
2993
|
var rowChains = /* @__PURE__ */ new Map();
|
|
2837
2994
|
async function rowLockKey(path) {
|
|
@@ -2868,7 +3025,7 @@ var LocalFsRegistryDriver = class {
|
|
|
2868
3025
|
return entropyIsAvailable(this.entropy) ? { strongRead: true, conditionalDelete: true } : { strongRead: true, canWrite: false, conditionalDelete: true };
|
|
2869
3026
|
}
|
|
2870
3027
|
async get(ref) {
|
|
2871
|
-
const env = await this.readRow(registryRowPath(this.root, ref));
|
|
3028
|
+
const env = await this.readRow(registryRowPath(this.root, ref), ref);
|
|
2872
3029
|
return env && !env.deleted ? env.record : null;
|
|
2873
3030
|
}
|
|
2874
3031
|
async create(ref, record) {
|
|
@@ -2876,7 +3033,7 @@ var LocalFsRegistryDriver = class {
|
|
|
2876
3033
|
const path = registryRowPath(this.root, ref);
|
|
2877
3034
|
assertRegistryNamesFit(ref);
|
|
2878
3035
|
return this.withRowLock(path, async () => {
|
|
2879
|
-
const current = await this.readRow(path);
|
|
3036
|
+
const current = await this.readRow(path, ref);
|
|
2880
3037
|
if (current !== null && !current.deleted) {
|
|
2881
3038
|
throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);
|
|
2882
3039
|
}
|
|
@@ -2894,7 +3051,7 @@ var LocalFsRegistryDriver = class {
|
|
|
2894
3051
|
const path = registryRowPath(this.root, ref);
|
|
2895
3052
|
assertRegistryNamesFit(ref);
|
|
2896
3053
|
return this.withRowLock(path, async () => {
|
|
2897
|
-
const current = await this.readRow(path);
|
|
3054
|
+
const current = await this.readRow(path, ref);
|
|
2898
3055
|
if (current === null || current.deleted || current.record.token !== expected) {
|
|
2899
3056
|
throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
|
|
2900
3057
|
}
|
|
@@ -2911,17 +3068,18 @@ var LocalFsRegistryDriver = class {
|
|
|
2911
3068
|
for (const ns of await this.namespaceDirs(namespace)) {
|
|
2912
3069
|
const dir = registryDir(this.root, ns);
|
|
2913
3070
|
if (await this.exactCase.differs(dir)) continue;
|
|
2914
|
-
let
|
|
3071
|
+
let entries;
|
|
2915
3072
|
try {
|
|
2916
|
-
|
|
3073
|
+
entries = await opendir(dir);
|
|
2917
3074
|
} catch (err) {
|
|
2918
3075
|
if (isAbsent(err)) continue;
|
|
2919
3076
|
throw mapFsError(err);
|
|
2920
3077
|
}
|
|
2921
|
-
for (const
|
|
2922
|
-
const segment = parseRegistryRow(name);
|
|
3078
|
+
for await (const entry of entries) {
|
|
3079
|
+
const segment = parseRegistryRow(entry.name);
|
|
2923
3080
|
if (segment === null) continue;
|
|
2924
|
-
const
|
|
3081
|
+
const ref = { namespace: ns, segment };
|
|
3082
|
+
const env = await this.readRow(registryRowPath(this.root, ref), ref);
|
|
2925
3083
|
if (env && !env.deleted) yield env.record;
|
|
2926
3084
|
}
|
|
2927
3085
|
}
|
|
@@ -2929,7 +3087,7 @@ var LocalFsRegistryDriver = class {
|
|
|
2929
3087
|
async delete(ref, expected) {
|
|
2930
3088
|
const path = registryRowPath(this.root, ref);
|
|
2931
3089
|
return this.withRowLock(path, async () => {
|
|
2932
|
-
const current = await this.readRow(path);
|
|
3090
|
+
const current = await this.readRow(path, ref);
|
|
2933
3091
|
if (expected !== void 0) {
|
|
2934
3092
|
if (current === null || current.deleted || current.record.token !== expected) {
|
|
2935
3093
|
throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
|
|
@@ -2971,7 +3129,8 @@ var LocalFsRegistryDriver = class {
|
|
|
2971
3129
|
}
|
|
2972
3130
|
return namespaces;
|
|
2973
3131
|
}
|
|
2974
|
-
|
|
3132
|
+
/** Read the row at `path`, which must be the row of `ref`: one that names another segment is refused. */
|
|
3133
|
+
async readRow(path, ref) {
|
|
2975
3134
|
let handle;
|
|
2976
3135
|
try {
|
|
2977
3136
|
if (await this.exactCase.differs(path)) return null;
|
|
@@ -2986,7 +3145,7 @@ var LocalFsRegistryDriver = class {
|
|
|
2986
3145
|
throw new IntegrityError(`registry row ${size}B exceeds cap ${DEFAULT_MAX_ROW_BYTES}B`);
|
|
2987
3146
|
}
|
|
2988
3147
|
const text = (await handle.readFile()).toString("utf8");
|
|
2989
|
-
return parseRegistryEnvelope(text, path);
|
|
3148
|
+
return parseRegistryEnvelope(text, relative2(this.root, path), ref);
|
|
2990
3149
|
} finally {
|
|
2991
3150
|
await handle.close();
|
|
2992
3151
|
}
|
|
@@ -3008,11 +3167,16 @@ var LocalFsRegistryDriver = class {
|
|
|
3008
3167
|
throw mapFsError(err);
|
|
3009
3168
|
}
|
|
3010
3169
|
try {
|
|
3011
|
-
await handle
|
|
3170
|
+
await writeAll(handle, out);
|
|
3012
3171
|
await handle.sync();
|
|
3013
|
-
}
|
|
3014
|
-
await handle.close()
|
|
3172
|
+
} catch (err) {
|
|
3173
|
+
await handle.close().catch(() => {
|
|
3174
|
+
});
|
|
3175
|
+
await unlink2(tmp).catch(() => {
|
|
3176
|
+
});
|
|
3177
|
+
throw mapFsError(err);
|
|
3015
3178
|
}
|
|
3179
|
+
await handle.close();
|
|
3016
3180
|
await rename(tmp, path).catch(async (err) => {
|
|
3017
3181
|
await unlink2(tmp).catch(() => {
|
|
3018
3182
|
});
|
|
@@ -3063,36 +3227,6 @@ var LocalFsStorage = class {
|
|
|
3063
3227
|
registry;
|
|
3064
3228
|
};
|
|
3065
3229
|
|
|
3066
|
-
// src/core/audit.ts
|
|
3067
|
-
var NOOP_AUDIT = {
|
|
3068
|
-
onEvent() {
|
|
3069
|
-
}
|
|
3070
|
-
};
|
|
3071
|
-
function safeAudit(sink) {
|
|
3072
|
-
if (sink === NOOP_AUDIT) return sink;
|
|
3073
|
-
return {
|
|
3074
|
-
onEvent(event) {
|
|
3075
|
-
try {
|
|
3076
|
-
sink.onEvent(event);
|
|
3077
|
-
} catch {
|
|
3078
|
-
}
|
|
3079
|
-
}
|
|
3080
|
-
};
|
|
3081
|
-
}
|
|
3082
|
-
var RecordingAuditSink = class {
|
|
3083
|
-
recorded = [];
|
|
3084
|
-
onEvent(event) {
|
|
3085
|
-
this.recorded.push(event);
|
|
3086
|
-
}
|
|
3087
|
-
/** An independent copy of the recorded events, in emission order. */
|
|
3088
|
-
snapshot() {
|
|
3089
|
-
return this.recorded.map((e) => ({ ...e }));
|
|
3090
|
-
}
|
|
3091
|
-
reset() {
|
|
3092
|
-
this.recorded.length = 0;
|
|
3093
|
-
}
|
|
3094
|
-
};
|
|
3095
|
-
|
|
3096
3230
|
// src/core/cooperative.ts
|
|
3097
3231
|
var YIELD_EVERY = 1024;
|
|
3098
3232
|
function yieldEvery(clock, everyN = YIELD_EVERY) {
|
|
@@ -3593,8 +3727,8 @@ var CrbmReader = class _CrbmReader {
|
|
|
3593
3727
|
/**
|
|
3594
3728
|
* Range-GET one chunk's payload, verifying its CRC32C before returning. Returns `null` if the chunk is
|
|
3595
3729
|
* absent. Throws `IntegrityError` on a CRC mismatch (the bytes must never reach the native
|
|
3596
|
-
* deserializer). The returned
|
|
3597
|
-
* not
|
|
3730
|
+
* deserializer). The returned bytes may be shared (a small generation's chunks are kept by the reader and handed out
|
|
3731
|
+
* again), so callers must not change them.
|
|
3598
3732
|
*/
|
|
3599
3733
|
async getChunk(chunkKey) {
|
|
3600
3734
|
const slot = this.slotOf(chunkKey);
|
|
@@ -3682,7 +3816,8 @@ var CrbmReader = class _CrbmReader {
|
|
|
3682
3816
|
* others; `options.now` times each request for `options.onRequest`, which hears of every range request that was sent, once, when
|
|
3683
3817
|
* it settles: taken, abandoned after the consumer stopped, or failed (a reader has no clock of its own, so `ms` is 0
|
|
3684
3818
|
* without one).
|
|
3685
|
-
* A plain chunk is a
|
|
3819
|
+
* A plain chunk is a view into the range it was read in, so keeping it keeps that whole range: copy a chunk to keep it
|
|
3820
|
+
* alone. Callers must not change it, as with {@link getChunk}.
|
|
3686
3821
|
*
|
|
3687
3822
|
* A stream that fails (a range that errors, a chunk that fails its check) raises at once and sends nothing further:
|
|
3688
3823
|
* the ranges it still had in flight finish in the background, are not retried and never raise. Its `settled()`
|
|
@@ -3745,7 +3880,7 @@ var CrbmReader = class _CrbmReader {
|
|
|
3745
3880
|
moved = bytes.length;
|
|
3746
3881
|
if (bytes.length !== read.length) {
|
|
3747
3882
|
throw new IntegrityError(
|
|
3748
|
-
`.crbm range [${read.offset}, +${read.length})
|
|
3883
|
+
`.crbm range [${read.offset}, +${read.length}) answered ${bytes.length} bytes, not the ${read.length} asked for`
|
|
3749
3884
|
);
|
|
3750
3885
|
}
|
|
3751
3886
|
return { bytes };
|
|
@@ -3953,7 +4088,7 @@ var CrbmWriter = class {
|
|
|
3953
4088
|
constructor(sink, options) {
|
|
3954
4089
|
this.sink = sink;
|
|
3955
4090
|
this.options = options;
|
|
3956
|
-
if (!Number.
|
|
4091
|
+
if (!Number.isSafeInteger(options.generation) || options.generation < 0) {
|
|
3957
4092
|
throw new ValidationError(
|
|
3958
4093
|
`generation must be a non-negative integer; got ${options.generation}`
|
|
3959
4094
|
);
|
|
@@ -3995,6 +4130,11 @@ var CrbmWriter = class {
|
|
|
3995
4130
|
);
|
|
3996
4131
|
}
|
|
3997
4132
|
if (payload.length === 0) throw new ValidationError("chunk payload must be non-empty");
|
|
4133
|
+
if (payload.length > DEFAULT_MAX_BITMAP_BYTES) {
|
|
4134
|
+
throw new ValidationError(
|
|
4135
|
+
`chunk payload of ${payload.length} bytes is over the ${DEFAULT_MAX_BITMAP_BYTES}-byte cap a reader opens`
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
3998
4138
|
await this.ensurePreamble();
|
|
3999
4139
|
const crypto = this.options.crypto;
|
|
4000
4140
|
const stored = crypto ? framed(crypto.aead.seal(payload, crypto.aadFor(chunkKey))) : payload;
|
|
@@ -4190,14 +4330,14 @@ function overCap(what, length) {
|
|
|
4190
4330
|
`${what} is ${length} bytes, more than any canonical 32-bit bitmap serializes to (${MAX_SERIALIZED_LOAD_BYTES}). If it was serialized from a bitmap, call runOptimize() on the bitmap first.`
|
|
4191
4331
|
);
|
|
4192
4332
|
}
|
|
4193
|
-
function prepareLoadInput(input, codec
|
|
4333
|
+
function prepareLoadInput(input, codec) {
|
|
4194
4334
|
if (typeof input !== "object" || input === null) {
|
|
4195
|
-
throw new ValidationError(
|
|
4335
|
+
throw new ValidationError("a load takes ids, { serialized } or { bitmap }");
|
|
4196
4336
|
}
|
|
4197
4337
|
const tag = tagOf(input);
|
|
4198
4338
|
if (tag === "Uint8Array" || tag === "Uint8ClampedArray") {
|
|
4199
4339
|
throw new ValidationError(
|
|
4200
|
-
|
|
4340
|
+
`a load's ids cannot be a ${tag} or Buffer: each byte would be loaded as an id. Pass portable Roaring bytes as { serialized }, and ids as a Uint32Array or an array of numbers.`
|
|
4201
4341
|
);
|
|
4202
4342
|
}
|
|
4203
4343
|
if (Symbol.iterator in input || Symbol.asyncIterator in input) return input;
|
|
@@ -4220,7 +4360,7 @@ function prepareLoadInput(input, codec, subject = "a load") {
|
|
|
4220
4360
|
return new DecodedLoadInput(decodeSerialized(bitmap.serialize("portable"), codec, what));
|
|
4221
4361
|
}
|
|
4222
4362
|
throw new ValidationError(
|
|
4223
|
-
|
|
4363
|
+
`a load takes ids (an iterable of integers), { serialized } or { bitmap }, as the only key; got an object with keys [${keys.join(", ")}]. Load options go in the next argument.`
|
|
4224
4364
|
);
|
|
4225
4365
|
}
|
|
4226
4366
|
function decodeSerialized(bytes, codec, what = "the buffer") {
|
|
@@ -4395,10 +4535,16 @@ function once(fn) {
|
|
|
4395
4535
|
};
|
|
4396
4536
|
}
|
|
4397
4537
|
var readerIsOf = (reader, live) => reader.generation === live.target.generation && reader.lineage === live.target.lineage;
|
|
4538
|
+
async function versionNamed(snap) {
|
|
4539
|
+
if (snap.target === void 0) {
|
|
4540
|
+
const reader = await snap.reader;
|
|
4541
|
+
return reader === null ? null : versionOf(reader.generation, reader.lineage);
|
|
4542
|
+
}
|
|
4543
|
+
const live = await snap.target;
|
|
4544
|
+
return live === null ? null : versionOf(live.target.generation, live.target.lineage);
|
|
4545
|
+
}
|
|
4398
4546
|
function describe(reader) {
|
|
4399
|
-
|
|
4400
|
-
for (const n of reader.cardinalities().values()) cardinality += n;
|
|
4401
|
-
return { cardinality, metadata: reader.metadata };
|
|
4547
|
+
return { cardinality: reader.count(), metadata: reader.metadata };
|
|
4402
4548
|
}
|
|
4403
4549
|
var Snapshot = class _Snapshot {
|
|
4404
4550
|
constructor(target, open4, prior) {
|
|
@@ -4460,6 +4606,7 @@ var Snapshot = class _Snapshot {
|
|
|
4460
4606
|
var SNAPSHOT_BASE_BYTES = 256;
|
|
4461
4607
|
var BULK_FLUSH_IDS = 1 << 20;
|
|
4462
4608
|
var YIELD_EVERY_IDS = 1 << 14;
|
|
4609
|
+
var RECENT_KEYS = 8;
|
|
4463
4610
|
var CrbmStorageChunkSource = class {
|
|
4464
4611
|
constructor(driver, options = {}) {
|
|
4465
4612
|
this.driver = driver;
|
|
@@ -4488,8 +4635,13 @@ var CrbmStorageChunkSource = class {
|
|
|
4488
4635
|
this.keystore = keystore;
|
|
4489
4636
|
this.requireEncryption = requireEncryption ?? false;
|
|
4490
4637
|
this.readerOptions = readerOptions;
|
|
4491
|
-
|
|
4492
|
-
|
|
4638
|
+
if (currentGenTtlMs !== void 0 && currentGenTtlMs !== null && (typeof currentGenTtlMs !== "number" || !Number.isFinite(currentGenTtlMs) || currentGenTtlMs < 0)) {
|
|
4639
|
+
throw new ValidationError(
|
|
4640
|
+
`cache.genTtlMs must be a finite number of milliseconds, 0 or more (0: no timed refresh); got ${typeof currentGenTtlMs === "number" ? String(currentGenTtlMs) : `a ${typeof currentGenTtlMs}`}`
|
|
4641
|
+
);
|
|
4642
|
+
}
|
|
4643
|
+
this.timedRefresh = registry !== void 0 && clock !== void 0 && (currentGenTtlMs ?? DEFAULT_CURRENT_GEN_TTL_MS) > 0;
|
|
4644
|
+
this.keepChunkBytesUpTo = this.timedRefresh ? Math.floor(
|
|
4493
4645
|
(maxOpenIndexBytes ?? DEFAULT_MAX_OPEN_INDEX_BYTES) / (maxOpenSegments ?? DEFAULT_MAX_OPEN_SEGMENTS)
|
|
4494
4646
|
) : 0;
|
|
4495
4647
|
this.clock = clock;
|
|
@@ -4559,12 +4711,33 @@ var CrbmStorageChunkSource = class {
|
|
|
4559
4711
|
keepChunkBytesUpTo;
|
|
4560
4712
|
clock;
|
|
4561
4713
|
currentGenTtlMs;
|
|
4714
|
+
/**
|
|
4715
|
+
* Whether the segment's pointer is re-read on a timer: it needs a clock (the TTL), a registry (the cheap row read; with
|
|
4716
|
+
* none, resolving is a listing of the bucket) and a TTL above 0. It decides the keeping of chunk bytes, the TTL lapse
|
|
4717
|
+
* and the refresh interval the cost report prices, so it is decided once.
|
|
4718
|
+
*/
|
|
4719
|
+
timedRefresh;
|
|
4720
|
+
/**
|
|
4721
|
+
* The last few segments' keys, newest first. A stream asks for its segment's snapshot before every cached chunk it
|
|
4722
|
+
* serves, and a combine alternates between a few operands, so comparing two names beats encoding them again, which
|
|
4723
|
+
* cost about a microsecond a chunk.
|
|
4724
|
+
*/
|
|
4725
|
+
recentKeys = [];
|
|
4726
|
+
keyOf(ref) {
|
|
4727
|
+
for (const k of this.recentKeys) {
|
|
4728
|
+
if (k.segment === ref.segment && k.namespace === ref.namespace) return k.key;
|
|
4729
|
+
}
|
|
4730
|
+
const key = segmentKey(ref);
|
|
4731
|
+
this.recentKeys.unshift({ namespace: ref.namespace, segment: ref.segment, key });
|
|
4732
|
+
if (this.recentKeys.length > RECENT_KEYS) this.recentKeys.pop();
|
|
4733
|
+
return key;
|
|
4734
|
+
}
|
|
4562
4735
|
/**
|
|
4563
4736
|
* The segment's live snapshot, refreshing on the TTL. Cheap within the TTL window. The snapshot holds the resolved
|
|
4564
4737
|
* target, and its reader once a read has asked for one.
|
|
4565
4738
|
*/
|
|
4566
4739
|
liveSnapshot(ref) {
|
|
4567
|
-
const key =
|
|
4740
|
+
const key = this.keyOf(ref);
|
|
4568
4741
|
const existing = this.snapshots.get(key);
|
|
4569
4742
|
if (existing === void 0) {
|
|
4570
4743
|
return this.install(
|
|
@@ -4656,10 +4829,19 @@ var CrbmStorageChunkSource = class {
|
|
|
4656
4829
|
* this.
|
|
4657
4830
|
*/
|
|
4658
4831
|
get pointerRefreshMs() {
|
|
4659
|
-
return this.
|
|
4832
|
+
return this.timedRefresh ? this.currentGenTtlMs : 0;
|
|
4833
|
+
}
|
|
4834
|
+
/**
|
|
4835
|
+
* Whether a stream reading from `snap` must resolve its segment again before its next chunk, as a read of one chunk
|
|
4836
|
+
* would: the snapshot's TTL has lapsed, an invalidation has happened since `epoch`, or the snapshot is no longer the
|
|
4837
|
+
* one the reader cache holds for the segment (it let it go, or another read installed a newer one). Else nothing can
|
|
4838
|
+
* have moved it, and the check costs a compare and a lookup.
|
|
4839
|
+
*/
|
|
4840
|
+
mayHaveMoved(key, snap, epoch) {
|
|
4841
|
+
return this.invalidations !== epoch || this.expired(snap.installedAtMs) || this.snapshots.get(key) !== snap;
|
|
4660
4842
|
}
|
|
4661
4843
|
expired(installedAtMs) {
|
|
4662
|
-
return this.
|
|
4844
|
+
return this.timedRefresh && this.now() - installedAtMs >= this.currentGenTtlMs;
|
|
4663
4845
|
}
|
|
4664
4846
|
/**
|
|
4665
4847
|
* The segment's current generation number — the engine keys its chunk cache by this so a generation bump
|
|
@@ -4743,6 +4925,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4743
4925
|
* once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
|
|
4744
4926
|
*/
|
|
4745
4927
|
async pinGeneration(ref, lease) {
|
|
4928
|
+
validateUserRef(ref);
|
|
4746
4929
|
for (let moved = 0; ; moved++) {
|
|
4747
4930
|
try {
|
|
4748
4931
|
try {
|
|
@@ -4774,6 +4957,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4774
4957
|
* of the row's summary against it, since that check is made where the row's own generation is read.
|
|
4775
4958
|
*/
|
|
4776
4959
|
async pinGenerationAt(ref, at, lease) {
|
|
4960
|
+
validateUserRef(ref);
|
|
4777
4961
|
const { generation, fingerprint } = at;
|
|
4778
4962
|
const gone = (why) => new NotFoundError(
|
|
4779
4963
|
`segment "${ref.segment}" generation ${generation} cannot be pinned: ${why}`
|
|
@@ -5060,7 +5244,11 @@ var CrbmStorageChunkSource = class {
|
|
|
5060
5244
|
* tell apart.
|
|
5061
5245
|
*/
|
|
5062
5246
|
async exists(ref) {
|
|
5063
|
-
|
|
5247
|
+
validateUserRef(ref);
|
|
5248
|
+
if (this.registry !== void 0) {
|
|
5249
|
+
const row = await this.registry.get(ref);
|
|
5250
|
+
return row !== null && row.status !== "destroyed";
|
|
5251
|
+
}
|
|
5064
5252
|
for await (const key of this.driver.list(ref)) {
|
|
5065
5253
|
void key;
|
|
5066
5254
|
return true;
|
|
@@ -5169,7 +5357,7 @@ var CrbmStorageChunkSource = class {
|
|
|
5169
5357
|
const keystore = this.keystore;
|
|
5170
5358
|
if (keystore === void 0) {
|
|
5171
5359
|
throw new KeyUnavailableError(
|
|
5172
|
-
`segment "${ref.segment}" is encrypted
|
|
5360
|
+
`segment "${ref.segment}" is encrypted, and the store reading it was built with no keystore (encryption.keystore)`
|
|
5173
5361
|
);
|
|
5174
5362
|
}
|
|
5175
5363
|
const aead = await (unwrap ?? (() => keystore.openDek(wrappedDeks)))();
|
|
@@ -5193,10 +5381,12 @@ var CrbmStorageChunkSource = class {
|
|
|
5193
5381
|
for (const chunkKey of keys) validateChunkRef({ ...ref, chunkKey });
|
|
5194
5382
|
validateChunkKeyOrder(keys);
|
|
5195
5383
|
const retry = options?.retry;
|
|
5384
|
+
const key = segmentKey(ref);
|
|
5196
5385
|
let yielded = 0;
|
|
5197
5386
|
let healed = false;
|
|
5198
5387
|
for (; ; ) {
|
|
5199
5388
|
let snap = this.liveSnapshot(ref);
|
|
5389
|
+
let epoch = this.invalidations;
|
|
5200
5390
|
let pending = snap.reader;
|
|
5201
5391
|
let chunks;
|
|
5202
5392
|
try {
|
|
@@ -5227,14 +5417,24 @@ var CrbmStorageChunkSource = class {
|
|
|
5227
5417
|
let movedOn = false;
|
|
5228
5418
|
try {
|
|
5229
5419
|
while (await pull.advance()) {
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5420
|
+
if (this.mayHaveMoved(key, snap, epoch)) {
|
|
5421
|
+
const live = this.liveSnapshot(ref);
|
|
5422
|
+
if (live !== snap) {
|
|
5423
|
+
let now;
|
|
5424
|
+
try {
|
|
5425
|
+
now = await versionNamed(live);
|
|
5426
|
+
} catch (err) {
|
|
5427
|
+
if (!isTransientError(err)) throw err;
|
|
5428
|
+
movedOn = true;
|
|
5429
|
+
break;
|
|
5430
|
+
}
|
|
5431
|
+
if (now !== version) {
|
|
5432
|
+
movedOn = true;
|
|
5433
|
+
break;
|
|
5434
|
+
}
|
|
5435
|
+
snap = live;
|
|
5236
5436
|
}
|
|
5237
|
-
|
|
5437
|
+
epoch = this.invalidations;
|
|
5238
5438
|
}
|
|
5239
5439
|
yield pull.take((chunk) => {
|
|
5240
5440
|
yielded += 1;
|
|
@@ -5254,7 +5454,7 @@ var CrbmStorageChunkSource = class {
|
|
|
5254
5454
|
if (!isNotFoundError(err) && !await this.replacedUnder(ref, pending, err, retry))
|
|
5255
5455
|
throw err;
|
|
5256
5456
|
await chunks?.settled();
|
|
5257
|
-
this.dropStale(
|
|
5457
|
+
this.dropStale(key, snap);
|
|
5258
5458
|
healed = true;
|
|
5259
5459
|
}
|
|
5260
5460
|
}
|
|
@@ -5795,6 +5995,11 @@ async function provesOwnObject(storage, key, fingerprint) {
|
|
|
5795
5995
|
throw err;
|
|
5796
5996
|
}
|
|
5797
5997
|
}
|
|
5998
|
+
function cleartextUnderRequiredEncryption(ref, currentGen) {
|
|
5999
|
+
return new ValidationError(
|
|
6000
|
+
`requireEncryption: segment "${ref.segment}" already has generation ${currentGen} in cleartext, so this load cannot be encrypted \u2014 encryption is chosen when a segment is first loaded. Load into a new segment with the keystore wired, then drop this one.`
|
|
6001
|
+
);
|
|
6002
|
+
}
|
|
5798
6003
|
async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
|
|
5799
6004
|
if (options.keystore === void 0 && options.requireEncryption === true) {
|
|
5800
6005
|
throw new ValidationError("requireEncryption: a load needs a keystore to write encrypted");
|
|
@@ -5831,11 +6036,8 @@ async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
|
|
|
5831
6036
|
const aead = early !== void 0 && sameKeys(early.wrapped, existing.wrappedDeks) ? await early.aead : await options.keystore.openDek(existing.wrappedDeks);
|
|
5832
6037
|
crypto = { aead, aadFor: (scope) => aadFor(key, key.generation, scope) };
|
|
5833
6038
|
} else if (existing !== null && existing.currentGen !== null) {
|
|
5834
|
-
if (options.requireEncryption === true)
|
|
5835
|
-
throw
|
|
5836
|
-
`requireEncryption: segment "${ref.segment}" already has generation ${existing.currentGen} in cleartext, so this load cannot be encrypted \u2014 encryption is chosen when a segment is first loaded. Load into a new segment with the keystore wired, then drop this one.`
|
|
5837
|
-
);
|
|
5838
|
-
}
|
|
6039
|
+
if (options.requireEncryption === true)
|
|
6040
|
+
throw cleartextUnderRequiredEncryption(ref, existing.currentGen);
|
|
5839
6041
|
} else {
|
|
5840
6042
|
const minted = await options.keystore.createDek();
|
|
5841
6043
|
newWrapped = minted.wrapped;
|
|
@@ -6147,6 +6349,30 @@ function applyJitter(policy, delayMs, rng) {
|
|
|
6147
6349
|
function retryRead(op, retry) {
|
|
6148
6350
|
return retry === void 0 ? op() : withRetry(op, retry.policy ?? DEFAULT_RETRY_POLICY, retry);
|
|
6149
6351
|
}
|
|
6352
|
+
function checkedRetryPolicy(policy) {
|
|
6353
|
+
const { maxAttempts, baseDelayMs, maxDelayMs, backoffFactor, jitter } = policy;
|
|
6354
|
+
const fail = (field, rule, v) => {
|
|
6355
|
+
throw new ValidationError(
|
|
6356
|
+
`retry.${field} must be ${rule}; got ${typeof v === "number" ? String(v) : `a ${typeof v}`}`
|
|
6357
|
+
);
|
|
6358
|
+
};
|
|
6359
|
+
if (typeof maxAttempts !== "number" || !Number.isFinite(maxAttempts) || maxAttempts < 1) {
|
|
6360
|
+
fail("maxAttempts", "a finite number >= 1", maxAttempts);
|
|
6361
|
+
}
|
|
6362
|
+
if (typeof baseDelayMs !== "number" || !Number.isFinite(baseDelayMs) || baseDelayMs < 0) {
|
|
6363
|
+
fail("baseDelayMs", "a finite number of milliseconds >= 0", baseDelayMs);
|
|
6364
|
+
}
|
|
6365
|
+
if (typeof maxDelayMs !== "number" || !Number.isFinite(maxDelayMs) || maxDelayMs < 0) {
|
|
6366
|
+
fail("maxDelayMs", "a finite number of milliseconds >= 0", maxDelayMs);
|
|
6367
|
+
}
|
|
6368
|
+
if (typeof backoffFactor !== "number" || !Number.isFinite(backoffFactor) || backoffFactor < 1) {
|
|
6369
|
+
fail("backoffFactor", "a finite number >= 1", backoffFactor);
|
|
6370
|
+
}
|
|
6371
|
+
if (jitter !== "full" && jitter !== "none") {
|
|
6372
|
+
throw new ValidationError(`retry.jitter must be 'full' or 'none'`);
|
|
6373
|
+
}
|
|
6374
|
+
return policy;
|
|
6375
|
+
}
|
|
6150
6376
|
async function withRetry(op, policy, deps) {
|
|
6151
6377
|
const retryable = deps.isRetryable ?? isTransientError;
|
|
6152
6378
|
if (!Number.isFinite(policy.maxAttempts) || policy.maxAttempts < 1) {
|
|
@@ -6174,6 +6400,106 @@ async function withRetry(op, policy, deps) {
|
|
|
6174
6400
|
}
|
|
6175
6401
|
|
|
6176
6402
|
// src/core/load.ts
|
|
6403
|
+
function checkedGuard(guard, where) {
|
|
6404
|
+
if (guard === void 0 || guard === null) return void 0;
|
|
6405
|
+
const shown2 = (v) => typeof v === "number" ? String(v) : Array.isArray(v) ? "an array" : `a ${typeof v}`;
|
|
6406
|
+
if (typeof guard !== "object" || Array.isArray(guard)) {
|
|
6407
|
+
throw new ValidationError(
|
|
6408
|
+
`${where}guard must be an object of bounds, such as { minRetained: 0.5 }; got ${shown2(guard)}`
|
|
6409
|
+
);
|
|
6410
|
+
}
|
|
6411
|
+
const bounds = guard;
|
|
6412
|
+
const unknown = Object.keys(bounds).filter(
|
|
6413
|
+
(k) => k !== "minCardinality" && k !== "minRetained" && k !== "maxGrowth" && bounds[k] !== void 0
|
|
6414
|
+
);
|
|
6415
|
+
if (unknown.length > 0) {
|
|
6416
|
+
throw new ValidationError(
|
|
6417
|
+
`${where}guard: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; it takes { minCardinality, minRetained, maxGrowth }`
|
|
6418
|
+
);
|
|
6419
|
+
}
|
|
6420
|
+
const { minCardinality, minRetained, maxGrowth } = guard;
|
|
6421
|
+
if (minCardinality !== void 0 && (!Number.isInteger(minCardinality) || minCardinality < 0)) {
|
|
6422
|
+
throw new ValidationError(
|
|
6423
|
+
`${where}guard.minCardinality must be a non-negative integer; got ${shown2(minCardinality)}`
|
|
6424
|
+
);
|
|
6425
|
+
}
|
|
6426
|
+
if (minRetained !== void 0 && (!Number.isFinite(minRetained) || minRetained < 0 || minRetained > 1)) {
|
|
6427
|
+
throw new ValidationError(
|
|
6428
|
+
`${where}guard.minRetained must be a fraction in 0..1; got ${shown2(minRetained)}`
|
|
6429
|
+
);
|
|
6430
|
+
}
|
|
6431
|
+
if (maxGrowth !== void 0 && (!Number.isFinite(maxGrowth) || maxGrowth !== 0 && maxGrowth < 1)) {
|
|
6432
|
+
throw new ValidationError(
|
|
6433
|
+
`${where}guard.maxGrowth must be 0 (no bound) or a factor of at least 1; got ${shown2(maxGrowth)}`
|
|
6434
|
+
);
|
|
6435
|
+
}
|
|
6436
|
+
return Object.freeze({
|
|
6437
|
+
...minCardinality === void 0 ? {} : { minCardinality },
|
|
6438
|
+
...minRetained === void 0 ? {} : { minRetained },
|
|
6439
|
+
...maxGrowth === void 0 ? {} : { maxGrowth }
|
|
6440
|
+
});
|
|
6441
|
+
}
|
|
6442
|
+
function guardRefusal(cardinality, before, guard, allowEmpty) {
|
|
6443
|
+
if (cardinality === 0 && allowEmpty !== true && (before ?? 0) > 0) return "empty";
|
|
6444
|
+
if (guard?.minCardinality !== void 0 && cardinality < guard.minCardinality) {
|
|
6445
|
+
return "min-cardinality";
|
|
6446
|
+
}
|
|
6447
|
+
if (guard?.minRetained !== void 0 && before !== null && cardinality < before * guard.minRetained) {
|
|
6448
|
+
return "min-retained";
|
|
6449
|
+
}
|
|
6450
|
+
if (guard?.maxGrowth !== void 0 && guard.maxGrowth !== 0 && before !== null && before > 0 && cardinality > before * guard.maxGrowth) {
|
|
6451
|
+
return "max-growth";
|
|
6452
|
+
}
|
|
6453
|
+
return void 0;
|
|
6454
|
+
}
|
|
6455
|
+
async function judgeLoad(ref, cardinality, deps, options = {}) {
|
|
6456
|
+
validateUserRef(ref);
|
|
6457
|
+
if (!Number.isSafeInteger(cardinality) || cardinality < 0) {
|
|
6458
|
+
throw new ValidationError("judgeLoad: cardinality must be a non-negative integer");
|
|
6459
|
+
}
|
|
6460
|
+
const guard = checkedGuard(options.guard, "");
|
|
6461
|
+
assertRegistryCanWrite(deps.registry, "load");
|
|
6462
|
+
if (deps.requireEncryption === true && deps.keystore === void 0) {
|
|
6463
|
+
throw new ValidationError("requireEncryption: a load needs a keystore to write encrypted");
|
|
6464
|
+
}
|
|
6465
|
+
const row = await deps.registry.get(ref);
|
|
6466
|
+
if (row?.status === "destroyed") {
|
|
6467
|
+
throw new ValidationError(
|
|
6468
|
+
`segment "${ref.segment}" is destroyed (crypto-shredded) \u2014 refusing to write; use a new segment`
|
|
6469
|
+
);
|
|
6470
|
+
}
|
|
6471
|
+
const cleartext = row?.wrappedDeks === void 0 || row.wrappedDeks.length === 0;
|
|
6472
|
+
if (deps.requireEncryption === true && row !== null && row.currentGen !== null && cleartext) {
|
|
6473
|
+
throw cleartextUnderRequiredEncryption(ref, row.currentGen);
|
|
6474
|
+
}
|
|
6475
|
+
const wrapped = row?.wrappedDeks;
|
|
6476
|
+
let aead;
|
|
6477
|
+
if (row !== null && wrapped !== void 0 && wrapped.length > 0) {
|
|
6478
|
+
if (deps.keystore === void 0) {
|
|
6479
|
+
throw new KeyUnavailableError(
|
|
6480
|
+
`segment "${ref.segment}" is encrypted but load was given no keystore`
|
|
6481
|
+
);
|
|
6482
|
+
}
|
|
6483
|
+
aead = Promise.resolve(await deps.keystore.openDek(wrapped));
|
|
6484
|
+
}
|
|
6485
|
+
let current;
|
|
6486
|
+
try {
|
|
6487
|
+
current = await currentCardinality(ref, deps, row, aead);
|
|
6488
|
+
} catch (err) {
|
|
6489
|
+
if (sizeNeeded(guard, options.allowEmpty)) throw err;
|
|
6490
|
+
current = { cardinality: null, fromSummary: false };
|
|
6491
|
+
}
|
|
6492
|
+
const wouldRefuse = guardRefusal(cardinality, current.cardinality, guard, options.allowEmpty);
|
|
6493
|
+
const openedObject = row !== null && row.currentGen !== null && !current.fromSummary;
|
|
6494
|
+
return {
|
|
6495
|
+
cardinalityBefore: current.cardinality,
|
|
6496
|
+
...wouldRefuse === void 0 ? {} : { wouldRefuse },
|
|
6497
|
+
reads: openedObject ? 2 : 1
|
|
6498
|
+
};
|
|
6499
|
+
}
|
|
6500
|
+
function sizeNeeded(guard, allowEmpty) {
|
|
6501
|
+
return guard?.minRetained !== void 0 || guard?.maxGrowth !== void 0 || allowEmpty !== true;
|
|
6502
|
+
}
|
|
6177
6503
|
function settled(promise) {
|
|
6178
6504
|
promise.catch(() => void 0);
|
|
6179
6505
|
return promise;
|
|
@@ -6232,26 +6558,13 @@ function loadSegmentChunks(ref, chunks, deps, options = {}) {
|
|
|
6232
6558
|
}
|
|
6233
6559
|
async function runLoad(ref, prepare, deps, options) {
|
|
6234
6560
|
validateUserRef(ref);
|
|
6561
|
+
checkedAuditSink(options.audit, "load");
|
|
6235
6562
|
const codec = requireCodec(deps.codec, "loadSegment");
|
|
6236
6563
|
const keep = options.keep ?? 1;
|
|
6237
6564
|
if (!Number.isInteger(keep) || keep < 0) {
|
|
6238
6565
|
throw new ValidationError(`keep must be a non-negative integer; got ${String(keep)}`);
|
|
6239
6566
|
}
|
|
6240
|
-
const guard = options.guard;
|
|
6241
|
-
if (guard?.minCardinality !== void 0) {
|
|
6242
|
-
const m = guard.minCardinality;
|
|
6243
|
-
if (!Number.isInteger(m) || m < 0) {
|
|
6244
|
-
throw new ValidationError(
|
|
6245
|
-
`guard.minCardinality must be a non-negative integer; got ${String(m)}`
|
|
6246
|
-
);
|
|
6247
|
-
}
|
|
6248
|
-
}
|
|
6249
|
-
if (guard?.minRetained !== void 0) {
|
|
6250
|
-
const s = guard.minRetained;
|
|
6251
|
-
if (!Number.isFinite(s) || s < 0 || s > 1) {
|
|
6252
|
-
throw new ValidationError(`guard.minRetained must be a fraction in 0..1; got ${String(s)}`);
|
|
6253
|
-
}
|
|
6254
|
-
}
|
|
6567
|
+
const guard = checkedGuard(options.guard, "");
|
|
6255
6568
|
const metadata = copiedMetadata(options.metadata, (message) => {
|
|
6256
6569
|
throw new ValidationError(message);
|
|
6257
6570
|
});
|
|
@@ -6264,7 +6577,7 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
6264
6577
|
const wrappedKeys = row?.wrappedDeks;
|
|
6265
6578
|
const keystore = deps.keystore;
|
|
6266
6579
|
const unwrapped = row !== null && row.status !== "destroyed" && wrappedKeys !== void 0 && wrappedKeys.length > 0 && keystore !== void 0 ? { wrapped: wrappedKeys, aead: settled((async () => keystore.openDek(wrappedKeys))()) } : void 0;
|
|
6267
|
-
const needsBefore = guard
|
|
6580
|
+
const needsBefore = sizeNeeded(guard, options.allowEmpty);
|
|
6268
6581
|
const current = needsBefore ? await currentCardinality(ref, deps, row, unwrapped?.aead) : { cardinality: null, fromSummary: false };
|
|
6269
6582
|
const before = current.cardinality;
|
|
6270
6583
|
const numbering = settled(nextLoadGeneration(ref, deps, row));
|
|
@@ -6323,8 +6636,9 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
6323
6636
|
}
|
|
6324
6637
|
};
|
|
6325
6638
|
let unanswered = false;
|
|
6639
|
+
const tidy = () => reclaim().catch(() => void 0);
|
|
6326
6640
|
const refuse = async (reason) => {
|
|
6327
|
-
await
|
|
6641
|
+
await tidy();
|
|
6328
6642
|
audit.onEvent({
|
|
6329
6643
|
kind: "segment.load-refused",
|
|
6330
6644
|
segment: ref.segment,
|
|
@@ -6346,15 +6660,8 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
6346
6660
|
collected: []
|
|
6347
6661
|
};
|
|
6348
6662
|
};
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
}
|
|
6352
|
-
if (guard?.minCardinality !== void 0 && written.cardinality < guard.minCardinality) {
|
|
6353
|
-
return refuse("min-cardinality");
|
|
6354
|
-
}
|
|
6355
|
-
if (guard?.minRetained !== void 0 && before !== null) {
|
|
6356
|
-
if (written.cardinality < before * guard.minRetained) return refuse("min-retained");
|
|
6357
|
-
}
|
|
6663
|
+
const refusal = guardRefusal(written.cardinality, before, guard, options.allowEmpty);
|
|
6664
|
+
if (refusal !== void 0) return refuse(refusal);
|
|
6358
6665
|
const leasesNow = deps.clock === void 0 ? void 0 : () => deps.clock.now();
|
|
6359
6666
|
let published;
|
|
6360
6667
|
try {
|
|
@@ -6370,8 +6677,8 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
6370
6677
|
// The third case, and the one the two fences above structurally cannot cover: the guard judged a segment
|
|
6371
6678
|
// that had NO ROW. Both `expectFrom` and `expectToken` compare against a value read from a row, so with no
|
|
6372
6679
|
// row both are omitted and the publish becomes a bare forward-only advance — which lands over anything
|
|
6373
|
-
// that appeared in between. `before` was `null`, so the empty and `
|
|
6374
|
-
// judge and passed vacuously. Verified: an empty generation published over a thousand ids that a
|
|
6680
|
+
// that appeared in between. `before` was `null`, so the empty, `minRetained` and `maxGrowth` bounds had nothing
|
|
6681
|
+
// to judge and passed vacuously. Verified: an empty generation published over a thousand ids that a
|
|
6375
6682
|
// concurrent writer had created meanwhile, reporting success. A guarded write therefore has to fence on
|
|
6376
6683
|
// the ABSENCE it relied on, exactly as it fences on the pointer it relied on.
|
|
6377
6684
|
...needsBefore && row === null ? { expectAbsent: true } : {},
|
|
@@ -6386,7 +6693,7 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
6386
6693
|
}
|
|
6387
6694
|
});
|
|
6388
6695
|
} catch (err) {
|
|
6389
|
-
if (isValidationError(err) || err
|
|
6696
|
+
if (isValidationError(err) || isKeyUnavailableError(err)) await tidy();
|
|
6390
6697
|
throw err;
|
|
6391
6698
|
}
|
|
6392
6699
|
if (!published.published) return refuse("superseded");
|
|
@@ -6925,9 +7232,12 @@ var FeedCursor = class {
|
|
|
6925
7232
|
this.finished = true;
|
|
6926
7233
|
}
|
|
6927
7234
|
// ---- the checks -------------------------------------------------------------------------------------------
|
|
6928
|
-
|
|
7235
|
+
/** How many records the feed has handed over, the one being read included: what names a record in an error. */
|
|
7236
|
+
records = 0;
|
|
7237
|
+
/** Refuse the record being read, named by its position in the feed: its key narrows the ids it holds to a range. */
|
|
7238
|
+
refuse(operand, why) {
|
|
6929
7239
|
throw new ValidationError(
|
|
6930
|
-
`materializeMany feed: record
|
|
7240
|
+
`materializeMany feed: record ${this.records}${operand === void 0 ? "" : `, operand "${operand}"`}: ${why}`
|
|
6931
7241
|
);
|
|
6932
7242
|
}
|
|
6933
7243
|
charge(bytes) {
|
|
@@ -6944,20 +7254,17 @@ var FeedCursor = class {
|
|
|
6944
7254
|
this.deps.release(bytes);
|
|
6945
7255
|
}
|
|
6946
7256
|
async convert(record) {
|
|
7257
|
+
this.records += 1;
|
|
6947
7258
|
if (typeof record !== "object" || record === null) {
|
|
6948
|
-
this.refuse(
|
|
7259
|
+
this.refuse(void 0, "a record is an object with a key and operands");
|
|
6949
7260
|
}
|
|
6950
7261
|
const key = record.key;
|
|
6951
7262
|
const operands = record.operands;
|
|
6952
7263
|
if (typeof key !== "number" || !Number.isInteger(key) || key < 0 || key >= KEY_IDS) {
|
|
6953
|
-
this.refuse(
|
|
7264
|
+
this.refuse(void 0, "the key must be an integer from 0 to 65535");
|
|
6954
7265
|
}
|
|
6955
7266
|
if (key < this.prevKey) {
|
|
6956
|
-
this.refuse(
|
|
6957
|
-
key,
|
|
6958
|
-
void 0,
|
|
6959
|
-
`the key is below the previous record's (${this.prevKey}): keys never go back`
|
|
6960
|
-
);
|
|
7267
|
+
this.refuse(void 0, `the key is below the previous record's: keys never go back`);
|
|
6961
7268
|
}
|
|
6962
7269
|
if (key !== this.prevKey) {
|
|
6963
7270
|
for (const i of this.touched) this.seen[i] = 0;
|
|
@@ -6966,7 +7273,6 @@ var FeedCursor = class {
|
|
|
6966
7273
|
}
|
|
6967
7274
|
if (typeof operands !== "object" || operands === null || !isPlainObject(operands)) {
|
|
6968
7275
|
this.refuse(
|
|
6969
|
-
key,
|
|
6970
7276
|
void 0,
|
|
6971
7277
|
"operands must be a plain object of fed operand name to Uint32Array (not a Map, an array or a class instance)"
|
|
6972
7278
|
);
|
|
@@ -6976,31 +7282,29 @@ var FeedCursor = class {
|
|
|
6976
7282
|
const charged = [];
|
|
6977
7283
|
try {
|
|
6978
7284
|
for (const name of Reflect.ownKeys(operands)) {
|
|
6979
|
-
if (typeof name !== "string") this.refuse(
|
|
7285
|
+
if (typeof name !== "string") this.refuse(void 0, "operands has a symbol key");
|
|
6980
7286
|
const at = this.index.get(name);
|
|
6981
7287
|
if (at === void 0) {
|
|
6982
|
-
this.refuse(
|
|
7288
|
+
this.refuse(name, "not a declared fed operand (it is not in feed.names)");
|
|
6983
7289
|
}
|
|
6984
7290
|
const value = operands[name];
|
|
6985
7291
|
const view = plainUint32(value);
|
|
6986
7292
|
if (view === void 0) {
|
|
6987
7293
|
this.refuse(
|
|
6988
|
-
key,
|
|
6989
7294
|
name,
|
|
6990
7295
|
"the ids must be a Uint32Array (any other typed array, array or look-alike is refused)"
|
|
6991
7296
|
);
|
|
6992
7297
|
}
|
|
6993
|
-
if (view === "detached") this.refuse(
|
|
7298
|
+
if (view === "detached") this.refuse(name, "the array's buffer is detached");
|
|
6994
7299
|
if (view === "out of bounds") {
|
|
6995
7300
|
this.refuse(
|
|
6996
|
-
key,
|
|
6997
7301
|
name,
|
|
6998
7302
|
"the array is out of bounds of its resizable buffer, which shrank under it"
|
|
6999
7303
|
);
|
|
7000
7304
|
}
|
|
7001
7305
|
if (view.length === 0) continue;
|
|
7002
7306
|
if (this.seen[at] === 1) {
|
|
7003
|
-
this.refuse(
|
|
7307
|
+
this.refuse(name, "named twice at one key: an operand appears at most once per key");
|
|
7004
7308
|
}
|
|
7005
7309
|
const n = this.scan(view, key, name);
|
|
7006
7310
|
this.seen[at] = 1;
|
|
@@ -7034,17 +7338,16 @@ var FeedCursor = class {
|
|
|
7034
7338
|
*/
|
|
7035
7339
|
scan(view, key, name) {
|
|
7036
7340
|
const n = view.length;
|
|
7037
|
-
if (n > KEY_IDS) this.refuse(
|
|
7341
|
+
if (n > KEY_IDS) this.refuse(name, `${n} ids, more than one chunk key holds`);
|
|
7038
7342
|
const scratch = this.scratch;
|
|
7039
7343
|
let prev = -1;
|
|
7040
7344
|
for (let i = 0; i < n; i++) {
|
|
7041
7345
|
const id = view[i];
|
|
7042
7346
|
if (id >>> 16 !== key) {
|
|
7043
|
-
this.refuse(
|
|
7347
|
+
this.refuse(name, `the id at position ${i} is not inside the key`);
|
|
7044
7348
|
}
|
|
7045
7349
|
if (id <= prev) {
|
|
7046
7350
|
this.refuse(
|
|
7047
|
-
key,
|
|
7048
7351
|
name,
|
|
7049
7352
|
`the id at position ${i} is not above the one before it: ids are ascending and unique`
|
|
7050
7353
|
);
|
|
@@ -7191,13 +7494,6 @@ function compileCombineMany(request) {
|
|
|
7191
7494
|
}
|
|
7192
7495
|
const index = /* @__PURE__ */ new Map();
|
|
7193
7496
|
operands.forEach((op, i) => index.set(op.name, i));
|
|
7194
|
-
for (const op of operands) {
|
|
7195
|
-
if (op.held !== void 0 && op.held.chunks.empty && !op.held.mayBeEmpty) {
|
|
7196
|
-
throw new ValidationError(
|
|
7197
|
-
`materializeMany: operand "${op.name}" holds no id, so an include would contribute none and an exclude would subtract none. Check what it was made from, or name it in \`mayBeEmpty\` if it may be empty.`
|
|
7198
|
-
);
|
|
7199
|
-
}
|
|
7200
|
-
}
|
|
7201
7497
|
const feed = request.feed === void 0 ? void 0 : checkFeed(request.feed, new Set(index.keys()));
|
|
7202
7498
|
feed?.names.forEach((name, j) => index.set(name, operands.length + j));
|
|
7203
7499
|
let totalNodes = 0;
|
|
@@ -7227,24 +7523,9 @@ function compileCombineMany(request) {
|
|
|
7227
7523
|
if (output.allowEmpty !== void 0 && typeof output.allowEmpty !== "boolean") {
|
|
7228
7524
|
throw new ValidationError(`${where}.allowEmpty must be a boolean`);
|
|
7229
7525
|
}
|
|
7230
|
-
|
|
7231
|
-
if (guard
|
|
7232
|
-
|
|
7233
|
-
throw new ValidationError(`${where}.guard must be an object`);
|
|
7234
|
-
}
|
|
7235
|
-
const min = guard.minCardinality;
|
|
7236
|
-
if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
|
|
7237
|
-
throw new ValidationError(
|
|
7238
|
-
`${where}.guard.minCardinality must be a non-negative integer; got ${String(min)}`
|
|
7239
|
-
);
|
|
7240
|
-
}
|
|
7241
|
-
const retained = guard.minRetained;
|
|
7242
|
-
if (retained !== void 0 && (!Number.isFinite(retained) || retained < 0 || retained > 1)) {
|
|
7243
|
-
throw new ValidationError(
|
|
7244
|
-
`${where}.guard.minRetained must be a fraction in 0..1; got ${String(retained)}`
|
|
7245
|
-
);
|
|
7246
|
-
}
|
|
7247
|
-
}
|
|
7526
|
+
checkedAuditSink(output.audit, where);
|
|
7527
|
+
if (output.guard === null) throw new ValidationError(`${where}.guard must be an object`);
|
|
7528
|
+
const guard = checkedGuard(output.guard, `${where}.`);
|
|
7248
7529
|
const metadata = copiedMetadata(output.metadata, (message) => {
|
|
7249
7530
|
throw new ValidationError(`${where}.metadata: ${message}`);
|
|
7250
7531
|
});
|
|
@@ -7423,6 +7704,7 @@ var Run = class {
|
|
|
7423
7704
|
codec;
|
|
7424
7705
|
clock;
|
|
7425
7706
|
maxBitmapBytes;
|
|
7707
|
+
metrics;
|
|
7426
7708
|
ledger;
|
|
7427
7709
|
gate = new Gate(MAX_RANGES_IN_FLIGHT2);
|
|
7428
7710
|
operands;
|
|
@@ -7437,6 +7719,8 @@ var Run = class {
|
|
|
7437
7719
|
publishes = 0;
|
|
7438
7720
|
opens = 0;
|
|
7439
7721
|
planCharge = 0;
|
|
7722
|
+
/** The groups {@link formGroups} made, whose kept plans {@link dropKeptPlans} can release. */
|
|
7723
|
+
formed = [];
|
|
7440
7724
|
attributedGet = 0;
|
|
7441
7725
|
attributedPut = 0;
|
|
7442
7726
|
planned = 0;
|
|
@@ -7462,6 +7746,7 @@ var Run = class {
|
|
|
7462
7746
|
this.codec = deps.codec;
|
|
7463
7747
|
this.clock = deps.clock;
|
|
7464
7748
|
this.maxBitmapBytes = deps.maxBitmapBytes ?? DEFAULT_MAX_BITMAP_BYTES;
|
|
7749
|
+
this.metrics = deps.metrics === void 0 ? void 0 : safeMetrics(deps.metrics);
|
|
7465
7750
|
this.ledger = new ResidentLedger(this.req.maxBufferedBytes);
|
|
7466
7751
|
this.tick = yieldEvery(deps.clock, 256);
|
|
7467
7752
|
this.operands = this.req.operands.map((spec, index) => ({
|
|
@@ -7501,7 +7786,6 @@ var Run = class {
|
|
|
7501
7786
|
async execute() {
|
|
7502
7787
|
this.window = windowOf2(this.req.after, this.req.through);
|
|
7503
7788
|
if (this.checkedFeed !== void 0) this.feed = this.openFeed(this.checkedFeed);
|
|
7504
|
-
this.indexHeld();
|
|
7505
7789
|
await this.readIndexes();
|
|
7506
7790
|
await this.refuseAbsent();
|
|
7507
7791
|
this.planOutputs();
|
|
@@ -7565,11 +7849,13 @@ var Run = class {
|
|
|
7565
7849
|
}
|
|
7566
7850
|
}
|
|
7567
7851
|
/**
|
|
7568
|
-
* Make room in the ledger.
|
|
7569
|
-
*
|
|
7570
|
-
*
|
|
7852
|
+
* Make room in the ledger. The plans kept for later groups give way first: they are rebuilt when their group starts,
|
|
7853
|
+
* and the outputs were judged without them. A fed output cannot be run again (the feed is read once), so a fed call
|
|
7854
|
+
* gives up its fed outputs and the feed next, as soon as the ledger passes the budget; only then is a stored-only
|
|
7855
|
+
* buffer evicted, to run again alone.
|
|
7571
7856
|
*/
|
|
7572
7857
|
reclaim() {
|
|
7858
|
+
if (this.dropKeptPlans()) return true;
|
|
7573
7859
|
if (this.feed !== void 0) {
|
|
7574
7860
|
let fed = this.feed.heldBytes;
|
|
7575
7861
|
for (const o of this.running)
|
|
@@ -7585,70 +7871,13 @@ var Run = class {
|
|
|
7585
7871
|
return { ok: false, error: new Error("materializeMany: output did not run") };
|
|
7586
7872
|
}
|
|
7587
7873
|
// ---- index ------------------------------------------------------------------------------------------------
|
|
7588
|
-
|
|
7589
|
-
staleHeldError(op) {
|
|
7590
|
-
return new StaleOperandError(
|
|
7591
|
-
`materializeMany: an erasure ran in this store after operand "${op.spec.name}" was made, so it may hold an erased id and the outputs that read it were not published. Make it again from the source of truth, after the erasure, and run the call again.`,
|
|
7592
|
-
op.spec.name,
|
|
7593
|
-
"erased"
|
|
7594
|
-
);
|
|
7595
|
-
}
|
|
7596
|
-
/** The error of the first held operand `o` reads that an erasure has made stale, if any. */
|
|
7597
|
-
staleHeld(o) {
|
|
7598
|
-
for (const i of o.compiled.operands) {
|
|
7599
|
-
const op = this.operands[i];
|
|
7600
|
-
if (op.spec.held?.moved() === true) return this.staleHeldError(op);
|
|
7601
|
-
}
|
|
7602
|
-
return void 0;
|
|
7603
|
-
}
|
|
7604
|
-
/**
|
|
7605
|
-
* Index the held operands the outputs read, before any request: their keys and exact cardinalities are in memory, and
|
|
7606
|
-
* one that an erasure has made stale is failed here, so the outputs that read it fail without a read.
|
|
7607
|
-
*/
|
|
7608
|
-
indexHeld() {
|
|
7874
|
+
async readIndexes() {
|
|
7609
7875
|
const wanted = /* @__PURE__ */ new Set();
|
|
7610
7876
|
for (const o of this.outputs) for (const op of o.compiled.operands) wanted.add(op);
|
|
7611
7877
|
if (this.window === "empty") return;
|
|
7878
|
+
for (const index of wanted) this.operands[index].read = true;
|
|
7612
7879
|
const w = this.window;
|
|
7613
|
-
|
|
7614
|
-
let resident = 0;
|
|
7615
|
-
for (const index of wanted) {
|
|
7616
|
-
const op = this.operands[index];
|
|
7617
|
-
const held = op.spec.held;
|
|
7618
|
-
if (held === void 0) continue;
|
|
7619
|
-
op.read = true;
|
|
7620
|
-
if (held.moved()) {
|
|
7621
|
-
op.error = { error: this.staleHeldError(op) };
|
|
7622
|
-
continue;
|
|
7623
|
-
}
|
|
7624
|
-
if (!charged.has(held.chunks)) {
|
|
7625
|
-
charged.add(held.chunks);
|
|
7626
|
-
resident += held.chunks.residentBytes;
|
|
7627
|
-
}
|
|
7628
|
-
const all = held.chunks.keys;
|
|
7629
|
-
const cards = held.chunks.cardinalities;
|
|
7630
|
-
const keep = [];
|
|
7631
|
-
const card = [];
|
|
7632
|
-
for (let i = 0; i < all.length; i++) {
|
|
7633
|
-
const key = all[i];
|
|
7634
|
-
if (w !== null && (key < w.loKey || key > w.hiKey)) continue;
|
|
7635
|
-
keep.push(key);
|
|
7636
|
-
card.push(cards[i]);
|
|
7637
|
-
}
|
|
7638
|
-
op.bounds = { keys: Uint16Array.from(keep), card: Uint32Array.from(card) };
|
|
7639
|
-
op.indexKeys = keep.length;
|
|
7640
|
-
op.chunkless = all.length === 0;
|
|
7641
|
-
}
|
|
7642
|
-
if (resident > 0) this.chargePlan(resident, "the held operands");
|
|
7643
|
-
}
|
|
7644
|
-
async readIndexes() {
|
|
7645
|
-
const wanted = /* @__PURE__ */ new Set();
|
|
7646
|
-
for (const o of this.outputs) for (const op of o.compiled.operands) wanted.add(op);
|
|
7647
|
-
if (this.window === "empty") return;
|
|
7648
|
-
for (const index of wanted) this.operands[index].read = true;
|
|
7649
|
-
const w = this.window;
|
|
7650
|
-
const stored = [...wanted].filter((index) => this.operands[index].spec.held === void 0);
|
|
7651
|
-
await mapWithConcurrency(stored, INDEX_PARALLELISM, async (index) => {
|
|
7880
|
+
await mapWithConcurrency([...wanted], INDEX_PARALLELISM, async (index) => {
|
|
7652
7881
|
const op = this.operands[index];
|
|
7653
7882
|
try {
|
|
7654
7883
|
const listed = checkedChunkKeys(await this.source.listChunkKeys(op.spec.ref));
|
|
@@ -7686,7 +7915,7 @@ var Run = class {
|
|
|
7686
7915
|
async refuseAbsent() {
|
|
7687
7916
|
if (this.req.allowAbsentOperands === true || this.source.exists === void 0) return;
|
|
7688
7917
|
const judged = this.operands.filter(
|
|
7689
|
-
(op) => op.
|
|
7918
|
+
(op) => op.error === void 0 && (op.read ? op.chunkless : true)
|
|
7690
7919
|
);
|
|
7691
7920
|
if (judged.length === 0) return;
|
|
7692
7921
|
const exists = this.source.exists.bind(this.source);
|
|
@@ -7697,7 +7926,7 @@ var Run = class {
|
|
|
7697
7926
|
const absent = checked.filter((c) => !c.exists).map((c) => c.op.spec.name);
|
|
7698
7927
|
if (absent.length === 0) return;
|
|
7699
7928
|
throw new ValidationError(
|
|
7700
|
-
`materializeMany: operand ${absent.map((n) => `"${n}"`).join(", ")} names a segment that does not exist, so it would contribute nothing \u2014 an exclude would suppress no ids and an include would contribute none. Check the name and the namespace (a segment addressed without its \`namespace\` is a DIFFERENT segment). Pass \`allowAbsentOperands: true\` if you meant it.`
|
|
7929
|
+
`materializeMany: operand ${absent.map((n) => `"${n}"`).join(", ")} names a segment that does not exist or was dropped, so it would contribute nothing \u2014 an exclude would suppress no ids and an include would contribute none. Check the name and the namespace (a segment addressed without its \`namespace\` is a DIFFERENT segment). Pass \`allowAbsentOperands: true\` if you meant it.`
|
|
7701
7930
|
);
|
|
7702
7931
|
}
|
|
7703
7932
|
// ---- plan -------------------------------------------------------------------------------------------------
|
|
@@ -7759,12 +7988,15 @@ var Run = class {
|
|
|
7759
7988
|
o.status = "waiting";
|
|
7760
7989
|
}
|
|
7761
7990
|
}
|
|
7762
|
-
/**
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7991
|
+
/**
|
|
7992
|
+
* Outputs in call order, a group at a time while the ledger has room for the next. Every output is judged against the
|
|
7993
|
+
* room the call's own plan leaves, `base`: a group's plan kept for its start is released when it starts, or sooner when
|
|
7994
|
+
* a group needs the room ({@link dropKeptPlans}), so it is not resident while a later output runs, and counting it
|
|
7995
|
+
* would refuse an output, or cut a group short, by its position in the call.
|
|
7996
|
+
*/
|
|
7767
7997
|
formGroups(waiting, single) {
|
|
7998
|
+
const base = this.planCharge;
|
|
7999
|
+
const room = this.req.maxBufferedBytes - base;
|
|
7768
8000
|
const groups = [];
|
|
7769
8001
|
const fresh = () => new GroupCost((i) => this.operands[i].bytes, this.req.publishConcurrency);
|
|
7770
8002
|
let cost = fresh();
|
|
@@ -7790,22 +8022,36 @@ var Run = class {
|
|
|
7790
8022
|
};
|
|
7791
8023
|
for (const o of waiting) {
|
|
7792
8024
|
const alone = fresh().with(o);
|
|
7793
|
-
if (alone >
|
|
8025
|
+
if (alone > room) {
|
|
7794
8026
|
this.fail(
|
|
7795
8027
|
o,
|
|
7796
8028
|
new BudgetExceededError(
|
|
7797
|
-
`materializeMany: output ${o.index} needs about ${alone} bytes resident with its operands, and the plan holds ${
|
|
8029
|
+
`materializeMany: output ${o.index} needs about ${alone} bytes resident with its operands, and the plan holds ${base}, over maxBufferedBytes (${this.req.maxBufferedBytes}) \u2014 raise maxBufferedBytes to at least ${alone + base}`
|
|
7798
8030
|
)
|
|
7799
8031
|
);
|
|
7800
8032
|
continue;
|
|
7801
8033
|
}
|
|
7802
|
-
if (!single && cost.size > 0 && cost.with(o) >
|
|
8034
|
+
if (!single && cost.size > 0 && cost.with(o) > room) flush();
|
|
7803
8035
|
current.push(o);
|
|
7804
8036
|
cost.add(o);
|
|
7805
8037
|
}
|
|
7806
8038
|
flush();
|
|
8039
|
+
this.formed = groups;
|
|
7807
8040
|
return groups;
|
|
7808
8041
|
}
|
|
8042
|
+
/** Release every group plan still kept for its start; each is built again when its group starts. */
|
|
8043
|
+
dropKeptPlans() {
|
|
8044
|
+
let dropped = false;
|
|
8045
|
+
for (const summary of this.formed) {
|
|
8046
|
+
const plan = summary.plan;
|
|
8047
|
+
if (plan === void 0) continue;
|
|
8048
|
+
summary.plan = void 0;
|
|
8049
|
+
this.ledger.release(plan.planBytes);
|
|
8050
|
+
this.planCharge -= plan.planBytes;
|
|
8051
|
+
dropped = true;
|
|
8052
|
+
}
|
|
8053
|
+
return dropped;
|
|
8054
|
+
}
|
|
7809
8055
|
/** The keys each operand is fetched at for `members`, and the keys the pass visits. */
|
|
7810
8056
|
buildGroup(members) {
|
|
7811
8057
|
combineWork.planBuilds++;
|
|
@@ -7848,6 +8094,11 @@ var Run = class {
|
|
|
7848
8094
|
pruned += this.operands[operand].indexKeys - set.size;
|
|
7849
8095
|
fixed += this.operands[operand].bytes;
|
|
7850
8096
|
}
|
|
8097
|
+
const named = /* @__PURE__ */ new Set();
|
|
8098
|
+
for (const o of members) for (const operand of o.compiled.operands) named.add(operand);
|
|
8099
|
+
for (const operand of named) {
|
|
8100
|
+
if (!bits.has(operand)) pruned += this.operands[operand].indexKeys;
|
|
8101
|
+
}
|
|
7851
8102
|
const visited = Uint16Array.from(visit.toKeys());
|
|
7852
8103
|
return {
|
|
7853
8104
|
outputs: members.map((o) => o.index),
|
|
@@ -7892,13 +8143,13 @@ var Run = class {
|
|
|
7892
8143
|
// ---- one group --------------------------------------------------------------------------------------------
|
|
7893
8144
|
async runGroup(indices, summary) {
|
|
7894
8145
|
const members = indices.map((i) => this.outputs[i]).filter((o) => o.status === "waiting");
|
|
7895
|
-
if (members.length === 0) return;
|
|
7896
8146
|
const kept = summary?.plan;
|
|
7897
8147
|
if (summary !== void 0) summary.plan = void 0;
|
|
7898
8148
|
if (kept !== void 0) {
|
|
7899
8149
|
this.ledger.release(kept.planBytes);
|
|
7900
8150
|
this.planCharge -= kept.planBytes;
|
|
7901
8151
|
}
|
|
8152
|
+
if (members.length === 0) return;
|
|
7902
8153
|
const plan = kept !== void 0 && members.length === indices.length ? kept : this.buildGroup(members);
|
|
7903
8154
|
const group = this.groups++;
|
|
7904
8155
|
const budget = this.limit;
|
|
@@ -7918,13 +8169,13 @@ var Run = class {
|
|
|
7918
8169
|
o.chunkCount = 0;
|
|
7919
8170
|
o.peak = 0;
|
|
7920
8171
|
}
|
|
7921
|
-
this.pruned += plan.pruned;
|
|
7922
8172
|
const fixed = plan.fixedBytes + plan.planBytes;
|
|
7923
|
-
if (!this.ledger.tryCharge(fixed)) {
|
|
8173
|
+
if (!this.ledger.tryCharge(fixed) && !(this.dropKeptPlans() && this.ledger.tryCharge(fixed))) {
|
|
7924
8174
|
const error = budgetError(this.req.maxBufferedBytes, "the operand streams");
|
|
7925
8175
|
for (const o of members) this.fail(o, error);
|
|
7926
8176
|
return;
|
|
7927
8177
|
}
|
|
8178
|
+
this.pruned += plan.pruned;
|
|
7928
8179
|
this.running = members;
|
|
7929
8180
|
try {
|
|
7930
8181
|
await this.pass(plan, members);
|
|
@@ -7973,12 +8224,19 @@ var Run = class {
|
|
|
7973
8224
|
});
|
|
7974
8225
|
}
|
|
7975
8226
|
const retry = (request) => this.gate.run(request);
|
|
7976
|
-
const
|
|
8227
|
+
const onRequestOf = (op) => (r) => {
|
|
7977
8228
|
this.rangeReads++;
|
|
7978
8229
|
this.rangeBytes += r.bytes;
|
|
8230
|
+
this.metrics?.onEvent({
|
|
8231
|
+
kind: "storage.get",
|
|
8232
|
+
namespace: op.spec.ref.namespace,
|
|
8233
|
+
segment: op.spec.ref.segment,
|
|
8234
|
+
bytes: r.bytes,
|
|
8235
|
+
ms: r.ms
|
|
8236
|
+
});
|
|
7979
8237
|
};
|
|
7980
8238
|
const streamOf = (c) => {
|
|
7981
|
-
c.stream ??= new ChunkStream(this.readKeys(c.op, c.keys, retry,
|
|
8239
|
+
c.stream ??= new ChunkStream(this.readKeys(c.op, c.keys, retry, onRequestOf(c.op)));
|
|
7982
8240
|
return c.stream;
|
|
7983
8241
|
};
|
|
7984
8242
|
const stop = (c) => {
|
|
@@ -8146,22 +8404,8 @@ var Run = class {
|
|
|
8146
8404
|
/** The operand's chunks at `keys` as a stream: the source's coalesced ranges, or chunk by chunk where it has none. */
|
|
8147
8405
|
readKeys(op, wanted, retry, onRequest) {
|
|
8148
8406
|
const source = this.source;
|
|
8407
|
+
const clock = this.clock;
|
|
8149
8408
|
const keys = Array.from(wanted);
|
|
8150
|
-
const held = op.spec.held;
|
|
8151
|
-
if (held !== void 0) {
|
|
8152
|
-
const stale = () => this.staleHeldError(op);
|
|
8153
|
-
return {
|
|
8154
|
-
// Each chunk is checked and read in one step with no wait between, so a release or an erasure cannot land after
|
|
8155
|
-
// the check and before the bytes.
|
|
8156
|
-
async *[Symbol.asyncIterator]() {
|
|
8157
|
-
for (const key of keys) {
|
|
8158
|
-
if (held.moved()) throw stale();
|
|
8159
|
-
const bytes = held.chunks.payload(key);
|
|
8160
|
-
yield { key, bytes: bytes === null ? null : new Uint8Array(bytes), version: null };
|
|
8161
|
-
}
|
|
8162
|
-
}
|
|
8163
|
-
};
|
|
8164
|
-
}
|
|
8165
8409
|
const ref = op.spec.ref;
|
|
8166
8410
|
if (source.getChunks !== void 0) {
|
|
8167
8411
|
return source.getChunks(ref, keys, { concurrency: this.req.concurrency, retry, onRequest });
|
|
@@ -8169,8 +8413,9 @@ var Run = class {
|
|
|
8169
8413
|
return {
|
|
8170
8414
|
async *[Symbol.asyncIterator]() {
|
|
8171
8415
|
for (const key of keys) {
|
|
8416
|
+
const startedAt = clock.now();
|
|
8172
8417
|
const bytes = await retry(() => source.getChunk({ ...ref, chunkKey: key }));
|
|
8173
|
-
onRequest({ bytes: bytes?.length ?? 0, ms: 0 });
|
|
8418
|
+
onRequest({ bytes: bytes?.length ?? 0, ms: Math.max(0, clock.now() - startedAt) });
|
|
8174
8419
|
yield { key, bytes, version: null };
|
|
8175
8420
|
}
|
|
8176
8421
|
}
|
|
@@ -8287,13 +8532,6 @@ var Run = class {
|
|
|
8287
8532
|
wake();
|
|
8288
8533
|
continue;
|
|
8289
8534
|
}
|
|
8290
|
-
const staleHeld = this.staleHeld(o);
|
|
8291
|
-
if (staleHeld !== void 0) {
|
|
8292
|
-
this.ledger.release(transient);
|
|
8293
|
-
this.fail(o, staleHeld);
|
|
8294
|
-
wake();
|
|
8295
|
-
continue;
|
|
8296
|
-
}
|
|
8297
8535
|
if (o.compiled.fed.length > 0 && this.erased()) {
|
|
8298
8536
|
this.ledger.release(transient);
|
|
8299
8537
|
this.fail(o, this.staleError(o));
|
|
@@ -8301,7 +8539,7 @@ var Run = class {
|
|
|
8301
8539
|
continue;
|
|
8302
8540
|
}
|
|
8303
8541
|
inFlight++;
|
|
8304
|
-
this.publishes++;
|
|
8542
|
+
if (this.req.dryRun !== true) this.publishes++;
|
|
8305
8543
|
const chunks = o.chunks;
|
|
8306
8544
|
try {
|
|
8307
8545
|
const value = await o.spec.publish(
|
|
@@ -8550,106 +8788,6 @@ function evaluate(node, kc) {
|
|
|
8550
8788
|
return acc;
|
|
8551
8789
|
}
|
|
8552
8790
|
|
|
8553
|
-
// src/core/combine-held.ts
|
|
8554
|
-
var PER_CHUNK_BYTES = 96;
|
|
8555
|
-
var HeldChunks = class {
|
|
8556
|
-
/** The chunk keys, ascending. */
|
|
8557
|
-
keys;
|
|
8558
|
-
/** How many ids each chunk holds. */
|
|
8559
|
-
cardinalities;
|
|
8560
|
-
/** What it holds in memory, for a call's resident-bytes ledger. */
|
|
8561
|
-
residentBytes;
|
|
8562
|
-
payloads;
|
|
8563
|
-
constructor(chunks) {
|
|
8564
|
-
const keys = [];
|
|
8565
|
-
const cards = [];
|
|
8566
|
-
const payloads = [];
|
|
8567
|
-
let bytes = 0;
|
|
8568
|
-
let last = -1;
|
|
8569
|
-
for (const chunk of chunks) {
|
|
8570
|
-
const { chunkKey, payload, cardinality } = chunk;
|
|
8571
|
-
if (!Number.isInteger(chunkKey) || chunkKey < 0 || chunkKey > MAX_REMAINDER || chunkKey <= last) {
|
|
8572
|
-
throw new ValidationError(
|
|
8573
|
-
`a held chunk's key must be an integer in [0, 65535] above the last`
|
|
8574
|
-
);
|
|
8575
|
-
}
|
|
8576
|
-
assertChunkCardinalityInRange(cardinality);
|
|
8577
|
-
last = chunkKey;
|
|
8578
|
-
keys.push(chunkKey);
|
|
8579
|
-
cards.push(cardinality);
|
|
8580
|
-
payloads.push(payload);
|
|
8581
|
-
bytes += payload.byteLength + PER_CHUNK_BYTES;
|
|
8582
|
-
}
|
|
8583
|
-
this.keys = Uint16Array.from(keys);
|
|
8584
|
-
this.cardinalities = Uint32Array.from(cards);
|
|
8585
|
-
this.payloads = payloads;
|
|
8586
|
-
this.residentBytes = bytes;
|
|
8587
|
-
}
|
|
8588
|
-
/** Whether it holds no id. */
|
|
8589
|
-
get empty() {
|
|
8590
|
-
return this.keys.length === 0;
|
|
8591
|
-
}
|
|
8592
|
-
/** Whether {@link release} ran. */
|
|
8593
|
-
get released() {
|
|
8594
|
-
return this.payloads === void 0;
|
|
8595
|
-
}
|
|
8596
|
-
/**
|
|
8597
|
-
* The payload of the chunk at `key`, `null` when it holds none there. A view into what is held: not to be written to.
|
|
8598
|
-
* @throws {ValidationError} once released.
|
|
8599
|
-
*/
|
|
8600
|
-
payload(key) {
|
|
8601
|
-
const payloads = this.payloads;
|
|
8602
|
-
if (payloads === void 0) {
|
|
8603
|
-
throw new ValidationError(
|
|
8604
|
-
"this memory operand was released: make another with store.memory()"
|
|
8605
|
-
);
|
|
8606
|
-
}
|
|
8607
|
-
let lo = 0;
|
|
8608
|
-
let hi = this.keys.length - 1;
|
|
8609
|
-
while (lo <= hi) {
|
|
8610
|
-
const mid = lo + hi >>> 1;
|
|
8611
|
-
const at = this.keys[mid];
|
|
8612
|
-
if (at === key) return payloads[mid];
|
|
8613
|
-
if (at < key) lo = mid + 1;
|
|
8614
|
-
else hi = mid - 1;
|
|
8615
|
-
}
|
|
8616
|
-
return null;
|
|
8617
|
-
}
|
|
8618
|
-
/** Zero and drop the bytes. Every later read throws; a second call does nothing. */
|
|
8619
|
-
release() {
|
|
8620
|
-
const payloads = this.payloads;
|
|
8621
|
-
if (payloads === void 0) return;
|
|
8622
|
-
this.payloads = void 0;
|
|
8623
|
-
for (const payload of payloads) payload.fill(0);
|
|
8624
|
-
}
|
|
8625
|
-
};
|
|
8626
|
-
async function prepareHeld(input, codec, clock) {
|
|
8627
|
-
if (typeof input === "object" && input !== null && !(Symbol.iterator in input) && !(Symbol.asyncIterator in input) && Object.prototype.hasOwnProperty.call(input, "ascending")) {
|
|
8628
|
-
throw new ValidationError(
|
|
8629
|
-
"a memory operand takes ids, { serialized } or { bitmap }, with no `ascending` option: ids that arrive in chunk-key order, one key at a time, are a feed of materializeMany"
|
|
8630
|
-
);
|
|
8631
|
-
}
|
|
8632
|
-
const real = plainUint32(input);
|
|
8633
|
-
if (real === "detached") {
|
|
8634
|
-
throw new ValidationError("a memory operand's ids are a Uint32Array over a detached buffer");
|
|
8635
|
-
}
|
|
8636
|
-
if (real === "out of bounds") {
|
|
8637
|
-
throw new ValidationError(
|
|
8638
|
-
"a memory operand's ids are a resizable-buffer Uint32Array whose view is out of bounds"
|
|
8639
|
-
);
|
|
8640
|
-
}
|
|
8641
|
-
const prepared = real !== void 0 ? new DecodedLoadInput(codec.fromValues(real)) : prepareLoadInput(input, codec, "a memory operand");
|
|
8642
|
-
const encoded = await encodedChunksOfInput(prepared, codec, clock);
|
|
8643
|
-
const tick = yieldEvery(clock, 256);
|
|
8644
|
-
const chunks = [];
|
|
8645
|
-
for (const chunk of encoded) {
|
|
8646
|
-
chunks.push(chunk);
|
|
8647
|
-
const pause = tick();
|
|
8648
|
-
if (pause !== null) await pause;
|
|
8649
|
-
}
|
|
8650
|
-
return new HeldChunks(chunks);
|
|
8651
|
-
}
|
|
8652
|
-
|
|
8653
8791
|
// src/core/rollback.ts
|
|
8654
8792
|
async function listGenerations(ref, deps) {
|
|
8655
8793
|
validateUserRef(ref);
|
|
@@ -8679,6 +8817,7 @@ async function currentDescription(ref, record, deps) {
|
|
|
8679
8817
|
}
|
|
8680
8818
|
async function rollbackSegment(ref, toGeneration, deps, options = {}) {
|
|
8681
8819
|
validateUserRef(ref);
|
|
8820
|
+
checkedAuditSink(options.audit, "rollback");
|
|
8682
8821
|
if (!Number.isInteger(toGeneration) || toGeneration < 0) {
|
|
8683
8822
|
throw new ValidationError(
|
|
8684
8823
|
`rollback: generation must be a non-negative integer; got ${String(toGeneration)}`
|
|
@@ -8704,9 +8843,9 @@ async function rollbackSegment(ref, toGeneration, deps, options = {}) {
|
|
|
8704
8843
|
`rollback: generation ${toGeneration} of "${ref.segment}" is not in the bucket` + (available.length === 0 ? " (no generations remain)" : ` \u2014 present: ${available.join(", ")}`)
|
|
8705
8844
|
);
|
|
8706
8845
|
}
|
|
8707
|
-
if (record.currentGen
|
|
8846
|
+
if ((record.currentGen === null || toGeneration > record.currentGen) && options.allowForward !== true) {
|
|
8708
8847
|
throw new ValidationError(
|
|
8709
|
-
`rollback: generation ${toGeneration} of "${ref.segment}" is above the current pointer (${record.currentGen}) \u2014 it may never have been published. Pass { allowForward: true } if you are undoing an earlier rollback.`
|
|
8848
|
+
`rollback: generation ${toGeneration} of "${ref.segment}" is above the current pointer (${record.currentGen ?? "none"}) \u2014 it may never have been published. Pass { allowForward: true } if you are undoing an earlier rollback.`
|
|
8710
8849
|
);
|
|
8711
8850
|
}
|
|
8712
8851
|
const keyed = record.wrappedDeks !== void 0 && record.wrappedDeks.length > 0;
|
|
@@ -8824,7 +8963,21 @@ function describingSummary(record) {
|
|
|
8824
8963
|
// src/core/erase-id.ts
|
|
8825
8964
|
var HOLDS_CONCURRENCY = 4;
|
|
8826
8965
|
async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
8966
|
+
const first = {};
|
|
8967
|
+
const result = await eraseOnce(ref, id, deps, options, first);
|
|
8968
|
+
if (result.reason === "destroyed" && first.tombstoned === false) {
|
|
8969
|
+
const again = await eraseOnce(ref, id, deps, options, {});
|
|
8970
|
+
if (again.reason === "destroyed") return result;
|
|
8971
|
+
if (!again.erased) return again;
|
|
8972
|
+
const collected = [.../* @__PURE__ */ new Set([...result.collected, ...again.collected])].sort((a, b) => a - b);
|
|
8973
|
+
const newest = Math.max(result.fromGeneration ?? -1, again.fromGeneration ?? -1);
|
|
8974
|
+
return { ...again, collected, ...newest >= 0 ? { fromGeneration: newest } : {} };
|
|
8975
|
+
}
|
|
8976
|
+
return result;
|
|
8977
|
+
}
|
|
8978
|
+
async function eraseOnce(ref, id, deps, options, seen) {
|
|
8827
8979
|
validateUserRef(ref);
|
|
8980
|
+
checkedAuditSink(options.audit, "eraseIdFromSegment");
|
|
8828
8981
|
const { chunkKey, remainder } = splitId(id);
|
|
8829
8982
|
const codec = requireCodec(deps.codec, "eraseIdFromSegment");
|
|
8830
8983
|
const maxBytes = deps.maxBitmapBytes ?? DEFAULT_MAX_BITMAP_BYTES;
|
|
@@ -8832,21 +8985,23 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
8832
8985
|
const read = (op) => retryRead(op, deps.readRetry);
|
|
8833
8986
|
const record = await deps.registry.get(ref);
|
|
8834
8987
|
if (record === null) return { ...base, erased: false, reason: "absent", collected: [] };
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
const from = record.currentGen;
|
|
8988
|
+
const tombstoned = record.status === "destroyed";
|
|
8989
|
+
seen.tombstoned = tombstoned;
|
|
8990
|
+
const pointerless = !tombstoned && record.currentGen === null;
|
|
8991
|
+
const from = record.currentGen ?? -1;
|
|
8840
8992
|
const fromToken = record.token;
|
|
8993
|
+
let premise = record;
|
|
8994
|
+
let premiseToken = fromToken;
|
|
8841
8995
|
let aead;
|
|
8842
|
-
if (
|
|
8996
|
+
if (tombstoned) {
|
|
8997
|
+
} else if (record.wrappedDeks !== void 0 && record.wrappedDeks.length > 0) {
|
|
8843
8998
|
if (deps.keystore === void 0) {
|
|
8844
8999
|
throw new KeyUnavailableError(
|
|
8845
9000
|
`segment "${ref.segment}" is encrypted but eraseIdFromSegment has no keystore`
|
|
8846
9001
|
);
|
|
8847
9002
|
}
|
|
8848
9003
|
aead = await deps.keystore.openDek(record.wrappedDeks);
|
|
8849
|
-
} else if (deps.requireEncryption === true) {
|
|
9004
|
+
} else if (!pointerless && deps.requireEncryption === true) {
|
|
8850
9005
|
throw new ValidationError(`requireEncryption: segment "${ref.segment}" is cleartext`);
|
|
8851
9006
|
}
|
|
8852
9007
|
const cryptoAt = (generation2) => aead === void 0 ? void 0 : { aead, aadFor: (scope) => aadFor(ref, generation2, scope) };
|
|
@@ -8863,8 +9018,9 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
8863
9018
|
if (row === null) return "absent";
|
|
8864
9019
|
if (row.status === "destroyed") return "destroyed";
|
|
8865
9020
|
if (row.currentGen === null) return "no-generation";
|
|
8866
|
-
return row.currentGen === from && (row.token ===
|
|
9021
|
+
return row.currentGen === from && (row.token === premiseToken || onlyLeasesDiffer(premise, row)) ? null : "superseded";
|
|
8867
9022
|
};
|
|
9023
|
+
const sealedUnpublished = /* @__PURE__ */ new Set();
|
|
8868
9024
|
const holds = async (generation2) => {
|
|
8869
9025
|
const key2 = { ...base, generation: generation2 };
|
|
8870
9026
|
const chunkIn = (crypto) => read(async () => (await openGenerationReader(deps.storage, key2, crypto)).getChunk(chunkKey));
|
|
@@ -8873,7 +9029,15 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
8873
9029
|
try {
|
|
8874
9030
|
bytes = await chunkIn(cryptoAt(generation2));
|
|
8875
9031
|
} catch (err) {
|
|
8876
|
-
if (
|
|
9032
|
+
if (cryptoAt(generation2) === void 0) {
|
|
9033
|
+
if ((tombstoned || pointerless) && await read(() => objectIsEncrypted(deps.storage, key2))) {
|
|
9034
|
+
if (tombstoned) return false;
|
|
9035
|
+
sealedUnpublished.add(generation2);
|
|
9036
|
+
return true;
|
|
9037
|
+
}
|
|
9038
|
+
throw err;
|
|
9039
|
+
}
|
|
9040
|
+
if (!isIntegrityError(err) || await read(() => objectIsEncrypted(deps.storage, key2))) {
|
|
8877
9041
|
throw err;
|
|
8878
9042
|
}
|
|
8879
9043
|
bytes = await chunkIn(void 0);
|
|
@@ -8919,6 +9083,51 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
8919
9083
|
const cannotRemove = (generation2) => new WriteConflictError(
|
|
8920
9084
|
`erasure of segment ${ref.segment} could not remove generation ${generation2}, which still holds the id and is still in the bucket; re-run`
|
|
8921
9085
|
);
|
|
9086
|
+
const fenceInFlight = async () => {
|
|
9087
|
+
const churn = leaseChurn({ clock: deps.clock, rng: deps.rng ?? deps.readRetry?.rng });
|
|
9088
|
+
let row = await deps.registry.get(ref);
|
|
9089
|
+
for (; ; ) {
|
|
9090
|
+
const verdict = rowVerdict(row);
|
|
9091
|
+
if (verdict !== null) return verdict;
|
|
9092
|
+
const current = row;
|
|
9093
|
+
const keptGens = current.keptGens !== void 0 && current.keptGens.length === 0 ? void 0 : [];
|
|
9094
|
+
try {
|
|
9095
|
+
const { token } = await deps.registry.compareAndSwap(ref, current.token, { keptGens });
|
|
9096
|
+
premise = { ...current, keptGens, token };
|
|
9097
|
+
premiseToken = token;
|
|
9098
|
+
return null;
|
|
9099
|
+
} catch (err) {
|
|
9100
|
+
if (!isWriteConflictError(err)) throw err;
|
|
9101
|
+
}
|
|
9102
|
+
const now = await deps.registry.get(ref);
|
|
9103
|
+
const settled2 = await churn.settle(current, now, () => deps.registry.get(ref));
|
|
9104
|
+
if (settled2 === void 0) return rowVerdict(now) ?? "superseded";
|
|
9105
|
+
row = settled2.row;
|
|
9106
|
+
}
|
|
9107
|
+
};
|
|
9108
|
+
const unpublished = async () => {
|
|
9109
|
+
const generations = [];
|
|
9110
|
+
for await (const key2 of deps.storage.list(ref)) generations.push(key2.generation);
|
|
9111
|
+
const newestFirst = generations.sort((a, b) => b - a);
|
|
9112
|
+
const holder = (await holdsEach(newestFirst, true)).find((h) => h.held === true);
|
|
9113
|
+
if (holder === void 0)
|
|
9114
|
+
return { ...base, erased: false, reason: "no-generation", collected: [] };
|
|
9115
|
+
const what = sealedUnpublished.has(holder.generation) ? "is sealed under a key that load has not published, so it cannot be searched" : "holds the id";
|
|
9116
|
+
throw new WriteConflictError(
|
|
9117
|
+
`eraseIdFromSegment: segment "${ref.segment}" has no published generation, and an object a first load wrote and never published (generation ${holder.generation}) ${what}. It cannot be deleted while that load may still publish it: re-run once the segment is loaded, which makes the object collectable, or drop the segment`
|
|
9118
|
+
);
|
|
9119
|
+
};
|
|
9120
|
+
const underTombstone = async () => {
|
|
9121
|
+
const generations = [];
|
|
9122
|
+
for await (const key2 of deps.storage.list(ref)) generations.push(key2.generation);
|
|
9123
|
+
const newestFirst = generations.sort((a, b) => b - a);
|
|
9124
|
+
const holder = (await holdsEach(newestFirst, true)).find((h) => h.held === true);
|
|
9125
|
+
if (holder === void 0) return { ...base, erased: false, reason: "destroyed", collected: [] };
|
|
9126
|
+
const collected2 = await gcOrphanGenerations(ref, deps, { keep: 0 });
|
|
9127
|
+
const left2 = await holderLeft(/* @__PURE__ */ new Set());
|
|
9128
|
+
if (left2 !== void 0) throw cannotRemove(left2);
|
|
9129
|
+
return { ...base, erased: true, fromGeneration: holder.generation, collected: collected2 };
|
|
9130
|
+
};
|
|
8922
9131
|
const notInCurrent = async () => {
|
|
8923
9132
|
const notMember = {
|
|
8924
9133
|
...base,
|
|
@@ -8955,8 +9164,9 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
8955
9164
|
const newest = holdersAbove[0] ?? holderBelow;
|
|
8956
9165
|
if (newest === void 0) return notMember;
|
|
8957
9166
|
const collected2 = [...await gcOrphanGenerations(ref, deps, { keep: 0 })];
|
|
8958
|
-
let moved = null;
|
|
9167
|
+
let moved = holdersAbove.length > 0 ? await fenceInFlight() : null;
|
|
8959
9168
|
for (const generation2 of holdersAbove) {
|
|
9169
|
+
if (moved !== null) break;
|
|
8960
9170
|
moved = rowVerdict(await deps.registry.get(ref));
|
|
8961
9171
|
if (moved !== null) break;
|
|
8962
9172
|
await deps.storage.delete({ ...base, generation: generation2 });
|
|
@@ -9021,10 +9231,17 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
9021
9231
|
};
|
|
9022
9232
|
const discardRefused = async (written) => {
|
|
9023
9233
|
const row = await deps.registry.get(ref);
|
|
9024
|
-
if (row
|
|
9025
|
-
|
|
9234
|
+
if (row !== null && row.status === "destroyed") {
|
|
9235
|
+
await deps.storage.delete({ ...base, generation: written });
|
|
9236
|
+
return row;
|
|
9237
|
+
}
|
|
9238
|
+
if (row === null || row.status !== "active" || row.currentGen === null) return row;
|
|
9239
|
+
if (row.currentGen <= from || row.currentGen >= written) return row;
|
|
9026
9240
|
await deps.storage.delete({ ...base, generation: written });
|
|
9241
|
+
return row;
|
|
9027
9242
|
};
|
|
9243
|
+
if (tombstoned) return await underTombstone();
|
|
9244
|
+
if (pointerless) return await unpublished();
|
|
9028
9245
|
const staged = await stage();
|
|
9029
9246
|
if ("erased" in staged) {
|
|
9030
9247
|
if (!staged.erased && staged.generation !== void 0) await discardRefused(staged.generation);
|
|
@@ -9048,11 +9265,11 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
9048
9265
|
rng: deps.rng ?? deps.readRetry?.rng
|
|
9049
9266
|
});
|
|
9050
9267
|
if (!published) {
|
|
9051
|
-
await discardRefused(generation);
|
|
9268
|
+
const now = await discardRefused(generation);
|
|
9052
9269
|
return {
|
|
9053
9270
|
...base,
|
|
9054
9271
|
erased: false,
|
|
9055
|
-
reason: "superseded",
|
|
9272
|
+
reason: rowVerdict(now) ?? "superseded",
|
|
9056
9273
|
fromGeneration: from,
|
|
9057
9274
|
generation,
|
|
9058
9275
|
collected: []
|
|
@@ -9111,9 +9328,12 @@ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
|
9111
9328
|
var KEY_BYTES = 32;
|
|
9112
9329
|
var NONCE_BYTES2 = 12;
|
|
9113
9330
|
var TAG_BYTES2 = 16;
|
|
9331
|
+
var isBytes = (v) => Object.prototype.toString.call(v) === "[object Uint8Array]";
|
|
9114
9332
|
var NodeAead = class {
|
|
9115
9333
|
key;
|
|
9116
9334
|
constructor(key) {
|
|
9335
|
+
if (!isBytes(key))
|
|
9336
|
+
throw new ValidationError("AEAD key must be a Uint8Array of key bytes, not text");
|
|
9117
9337
|
if (key.length !== KEY_BYTES)
|
|
9118
9338
|
throw new ValidationError(`AEAD key must be ${KEY_BYTES} bytes, got ${key.length}`);
|
|
9119
9339
|
this.key = Buffer.from(key);
|
|
@@ -9161,6 +9381,8 @@ var InProcessKeystore = class {
|
|
|
9161
9381
|
const entries = Object.entries(options.keys);
|
|
9162
9382
|
if (entries.length === 0) throw new ValidationError("keystore needs at least one KEK");
|
|
9163
9383
|
for (const [id, k] of entries) {
|
|
9384
|
+
if (!isBytes(k))
|
|
9385
|
+
throw new ValidationError(`KEK "${id}" must be a Uint8Array of key bytes, not text`);
|
|
9164
9386
|
if (k.length !== KEY_BYTES)
|
|
9165
9387
|
throw new ValidationError(`KEK "${id}" must be ${KEY_BYTES} bytes, got ${k.length}`);
|
|
9166
9388
|
}
|
|
@@ -9198,20 +9420,186 @@ var InProcessKeystore = class {
|
|
|
9198
9420
|
}
|
|
9199
9421
|
}
|
|
9200
9422
|
if (anyKekHeld) throw lastUnwrapError;
|
|
9201
|
-
const have = [...this.keys.keys()].join(", ") || "(none)";
|
|
9202
9423
|
const need = wrapped.map((w) => w.keyId).join(", ") || "(none)";
|
|
9203
9424
|
throw new KeyUnavailableError(
|
|
9204
|
-
`no held KEK can unwrap this DEK \u2014
|
|
9425
|
+
`no held KEK can unwrap this DEK \u2014 the wrappings reference [${need}]. Restore the missing KEK (or its recovery KEK), or rebuild the segment from source.`
|
|
9205
9426
|
);
|
|
9206
9427
|
}
|
|
9207
9428
|
};
|
|
9208
9429
|
|
|
9430
|
+
// src/core/retention.ts
|
|
9431
|
+
var EXPIRES_AT = "expiresAt";
|
|
9432
|
+
var RETENTION_CAS_ATTEMPTS = 5;
|
|
9433
|
+
var MIN_EXPIRES_AT_MS = 1e12;
|
|
9434
|
+
var SWEEP_MARK = "retiredBySweepAt";
|
|
9435
|
+
function withoutSweepMark(meta) {
|
|
9436
|
+
if (meta === void 0 || meta === null || typeof meta !== "object" || Array.isArray(meta))
|
|
9437
|
+
return null;
|
|
9438
|
+
if (!Object.prototype.hasOwnProperty.call(meta, SWEEP_MARK)) return null;
|
|
9439
|
+
const rest = Object.fromEntries(Object.entries(meta).filter(([key]) => key !== SWEEP_MARK));
|
|
9440
|
+
return Object.keys(rest).length === 0 ? void 0 : rest;
|
|
9441
|
+
}
|
|
9442
|
+
var MAX_DATE_MS = 864e13;
|
|
9443
|
+
function validateRetentionPolicy(policy) {
|
|
9444
|
+
if (policy === null || typeof policy !== "object" || Array.isArray(policy)) {
|
|
9445
|
+
throw new ValidationError(
|
|
9446
|
+
`retention policy must be an object such as { expiresAt }; got ${policy === null ? "null" : Array.isArray(policy) ? "an array" : `a ${typeof policy}`}`
|
|
9447
|
+
);
|
|
9448
|
+
}
|
|
9449
|
+
const { expiresAt } = policy;
|
|
9450
|
+
if (typeof expiresAt !== "number" || !Number.isInteger(expiresAt)) {
|
|
9451
|
+
throw new ValidationError(
|
|
9452
|
+
`retention.expiresAt must be an integer epoch-ms; got ${String(expiresAt)}`
|
|
9453
|
+
);
|
|
9454
|
+
}
|
|
9455
|
+
if (expiresAt > MAX_DATE_MS) {
|
|
9456
|
+
throw new ValidationError(
|
|
9457
|
+
`retention.expiresAt (${expiresAt}) is past the latest time a date can name (${MAX_DATE_MS}); to keep a segment, leave it without a policy or call clearRetention`
|
|
9458
|
+
);
|
|
9459
|
+
}
|
|
9460
|
+
if (expiresAt < MIN_EXPIRES_AT_MS) {
|
|
9461
|
+
throw new ValidationError(
|
|
9462
|
+
`retention.expiresAt (${expiresAt}) is before ${MIN_EXPIRES_AT_MS} \u2014 that is almost certainly epoch SECONDS rather than milliseconds, which would read as "already expired" and retire the segment on the next sweep. Use \`Date.now() + days * 86_400_000\`. To retire a segment now, call \`dropSegment\`.`
|
|
9463
|
+
);
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
function readRetentionPolicy(meta) {
|
|
9467
|
+
if (meta === void 0) return null;
|
|
9468
|
+
if (meta === null || typeof meta !== "object" || Array.isArray(meta)) return "invalid";
|
|
9469
|
+
if (!(EXPIRES_AT in meta)) return null;
|
|
9470
|
+
const raw = meta[EXPIRES_AT];
|
|
9471
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < MIN_EXPIRES_AT_MS)
|
|
9472
|
+
return "invalid";
|
|
9473
|
+
return { expiresAt: raw };
|
|
9474
|
+
}
|
|
9475
|
+
async function getSegmentRetention(ref, deps) {
|
|
9476
|
+
validateUserRef(ref);
|
|
9477
|
+
const record = await deps.registry.get(ref);
|
|
9478
|
+
if (record === null || record.status === "destroyed") return null;
|
|
9479
|
+
return readRetentionPolicy(record.retention);
|
|
9480
|
+
}
|
|
9481
|
+
function readExpiresAt(record) {
|
|
9482
|
+
const parsed = record === null ? null : readRetentionPolicy(record.retention);
|
|
9483
|
+
return parsed === null || parsed === "invalid" ? void 0 : parsed.expiresAt;
|
|
9484
|
+
}
|
|
9485
|
+
async function reindex(ref, deps, expiresAt, previousExpiresAt) {
|
|
9486
|
+
if (!canIndex(ref)) return false;
|
|
9487
|
+
const nextBucket = expiresAt === void 0 ? void 0 : dueBucket(expiresAt);
|
|
9488
|
+
const oldBucket = previousExpiresAt === void 0 ? void 0 : dueBucket(previousExpiresAt);
|
|
9489
|
+
let indexed = expiresAt === void 0;
|
|
9490
|
+
if (nextBucket !== void 0) {
|
|
9491
|
+
try {
|
|
9492
|
+
await deps.registry.create(dueIndexRef(nextBucket, ref), { currentGen: null });
|
|
9493
|
+
indexed = true;
|
|
9494
|
+
} catch (err) {
|
|
9495
|
+
if (isWriteConflictError(err)) indexed = true;
|
|
9496
|
+
}
|
|
9497
|
+
}
|
|
9498
|
+
if (oldBucket !== void 0 && oldBucket !== nextBucket) {
|
|
9499
|
+
try {
|
|
9500
|
+
await deps.registry.delete(dueIndexRef(oldBucket, ref));
|
|
9501
|
+
} catch {
|
|
9502
|
+
}
|
|
9503
|
+
}
|
|
9504
|
+
return indexed;
|
|
9505
|
+
}
|
|
9506
|
+
async function setSegmentRetention(ref, deps, policy) {
|
|
9507
|
+
validateUserRef(ref);
|
|
9508
|
+
validateRetentionPolicy(policy);
|
|
9509
|
+
const base = { segment: ref.segment, namespace: ref.namespace };
|
|
9510
|
+
let previousExpiresAt;
|
|
9511
|
+
const churn = leaseChurn(deps);
|
|
9512
|
+
let lost;
|
|
9513
|
+
for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
|
|
9514
|
+
let record = await deps.registry.get(ref);
|
|
9515
|
+
if (lost !== void 0) {
|
|
9516
|
+
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
9517
|
+
if (settled2 !== void 0) {
|
|
9518
|
+
record = settled2.row;
|
|
9519
|
+
attempt -= 1;
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9522
|
+
try {
|
|
9523
|
+
previousExpiresAt = readExpiresAt(record);
|
|
9524
|
+
if (record === null) {
|
|
9525
|
+
await deps.registry.create(ref, {
|
|
9526
|
+
currentGen: null,
|
|
9527
|
+
retention: { [EXPIRES_AT]: policy.expiresAt }
|
|
9528
|
+
});
|
|
9529
|
+
return {
|
|
9530
|
+
...base,
|
|
9531
|
+
expiresAt: policy.expiresAt,
|
|
9532
|
+
createdRow: true,
|
|
9533
|
+
indexed: await reindex(ref, deps, policy.expiresAt, void 0)
|
|
9534
|
+
};
|
|
9535
|
+
}
|
|
9536
|
+
assertNotDestroyed(record, ref);
|
|
9537
|
+
await deps.registry.compareAndSwap(ref, record.token, {
|
|
9538
|
+
retention: { ...record.retention, [EXPIRES_AT]: policy.expiresAt }
|
|
9539
|
+
});
|
|
9540
|
+
return {
|
|
9541
|
+
...base,
|
|
9542
|
+
expiresAt: policy.expiresAt,
|
|
9543
|
+
createdRow: false,
|
|
9544
|
+
indexed: await reindex(ref, deps, policy.expiresAt, previousExpiresAt)
|
|
9545
|
+
};
|
|
9546
|
+
} catch (err) {
|
|
9547
|
+
if (!isWriteConflictError(err)) throw err;
|
|
9548
|
+
lost = record ?? void 0;
|
|
9549
|
+
}
|
|
9550
|
+
}
|
|
9551
|
+
throw new WriteConflictError(
|
|
9552
|
+
`setRetention: contention writing the retention policy for segment "${ref.segment}"`
|
|
9553
|
+
);
|
|
9554
|
+
}
|
|
9555
|
+
async function clearSegmentRetention(ref, deps) {
|
|
9556
|
+
validateUserRef(ref);
|
|
9557
|
+
const churn = leaseChurn(deps);
|
|
9558
|
+
let lost;
|
|
9559
|
+
for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
|
|
9560
|
+
let record = await deps.registry.get(ref);
|
|
9561
|
+
if (lost !== void 0) {
|
|
9562
|
+
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
9563
|
+
if (settled2 !== void 0) {
|
|
9564
|
+
record = settled2.row;
|
|
9565
|
+
attempt -= 1;
|
|
9566
|
+
}
|
|
9567
|
+
}
|
|
9568
|
+
if (record === null) return false;
|
|
9569
|
+
if (record.status === "destroyed") return false;
|
|
9570
|
+
if (record.retention === void 0 || !(EXPIRES_AT in record.retention)) return false;
|
|
9571
|
+
const rest = { ...record.retention };
|
|
9572
|
+
delete rest[EXPIRES_AT];
|
|
9573
|
+
try {
|
|
9574
|
+
await deps.registry.compareAndSwap(ref, record.token, {
|
|
9575
|
+
retention: Object.keys(rest).length === 0 ? void 0 : rest
|
|
9576
|
+
});
|
|
9577
|
+
await reindex(ref, deps, void 0, readExpiresAt(record));
|
|
9578
|
+
return true;
|
|
9579
|
+
} catch (err) {
|
|
9580
|
+
if (!isWriteConflictError(err)) throw err;
|
|
9581
|
+
lost = record;
|
|
9582
|
+
}
|
|
9583
|
+
}
|
|
9584
|
+
throw new WriteConflictError(
|
|
9585
|
+
`clearRetention: contention clearing the retention policy for segment "${ref.segment}"`
|
|
9586
|
+
);
|
|
9587
|
+
}
|
|
9588
|
+
function assertNotDestroyed(record, ref) {
|
|
9589
|
+
if (record.status === "destroyed") {
|
|
9590
|
+
throw new ValidationError(
|
|
9591
|
+
`segment "${ref.segment}" is destroyed (crypto-shredded) \u2014 refusing to set a retention policy on a tombstone; there is nothing left to retire. Use a new segment.`
|
|
9592
|
+
);
|
|
9593
|
+
}
|
|
9594
|
+
}
|
|
9595
|
+
|
|
9209
9596
|
// src/core/erasure.ts
|
|
9210
9597
|
var MAX_CAS_ATTEMPTS = 8;
|
|
9211
9598
|
var ERASE_CONCURRENCY = 8;
|
|
9212
9599
|
var MAX_STORAGE_SWEEPS = 3;
|
|
9213
9600
|
async function destroySegment(ref, deps, options) {
|
|
9214
9601
|
validateUserRef(ref);
|
|
9602
|
+
checkedAuditSink(options.audit, "destroySegment");
|
|
9215
9603
|
if (options.confirmSegment !== ref.segment) {
|
|
9216
9604
|
throw new ValidationError(
|
|
9217
9605
|
`destroySegment: confirmSegment must equal the segment name "${ref.segment}" (guard against accidental crypto-shred)`
|
|
@@ -9232,6 +9620,7 @@ async function eraseNamespace(namespace, deps, options) {
|
|
|
9232
9620
|
throw new ValidationError("eraseNamespace: namespace must be a non-empty string");
|
|
9233
9621
|
}
|
|
9234
9622
|
validateUserNamespace(namespace);
|
|
9623
|
+
checkedAuditSink(options.audit, "eraseNamespace");
|
|
9235
9624
|
if (options.confirmNamespace !== namespace) {
|
|
9236
9625
|
throw new ValidationError(
|
|
9237
9626
|
`eraseNamespace: confirmNamespace must equal the namespace "${namespace}" (guard against accidental erasure)`
|
|
@@ -9273,7 +9662,11 @@ async function eraseNamespace(namespace, deps, options) {
|
|
|
9273
9662
|
return { destroyed };
|
|
9274
9663
|
}
|
|
9275
9664
|
async function dropSegment(ref, deps, options) {
|
|
9665
|
+
return await dropSegmentFor(ref, deps, options, void 0);
|
|
9666
|
+
}
|
|
9667
|
+
async function dropSegmentFor(ref, deps, options, sweep) {
|
|
9276
9668
|
validateUserRef(ref);
|
|
9669
|
+
checkedAuditSink(options.audit, "dropSegment");
|
|
9277
9670
|
if (options.confirmSegment !== ref.segment) {
|
|
9278
9671
|
throw new ValidationError(
|
|
9279
9672
|
`dropSegment: confirmSegment must equal the segment name "${ref.segment}" (guard against accidental deletion)`
|
|
@@ -9294,7 +9687,9 @@ async function dropSegment(ref, deps, options) {
|
|
|
9294
9687
|
reason: record === null ? wouldDelete.length === 0 ? "absent" : void 0 : record.status === "destroyed" ? "already" : void 0
|
|
9295
9688
|
};
|
|
9296
9689
|
}
|
|
9297
|
-
|
|
9690
|
+
const first = await shredSegment(ref, deps, true, "dropSegment", sweep);
|
|
9691
|
+
if (first === null) return null;
|
|
9692
|
+
let shred = first;
|
|
9298
9693
|
if (shred.reason === "absent") {
|
|
9299
9694
|
const orphans = await listGenerations2(deps.storage, ref);
|
|
9300
9695
|
if (orphans.length === 0) {
|
|
@@ -9371,7 +9766,7 @@ async function listGenerations2(storage, ref) {
|
|
|
9371
9766
|
for await (const key of storage.list(ref)) generations.push(key.generation);
|
|
9372
9767
|
return generations.sort((a, b) => a - b);
|
|
9373
9768
|
}
|
|
9374
|
-
async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
9769
|
+
async function shredSegment(ref, deps, allowCleartext, op = "destroySegment", sweep) {
|
|
9375
9770
|
const base = { segment: ref.segment, namespace: ref.namespace };
|
|
9376
9771
|
const churn = leaseChurn(deps);
|
|
9377
9772
|
let lost;
|
|
@@ -9384,6 +9779,9 @@ async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
|
9384
9779
|
attempt -= 1;
|
|
9385
9780
|
}
|
|
9386
9781
|
}
|
|
9782
|
+
if (sweep !== void 0 && (record === null || record.status === "destroyed" || !sweep.wanted(record))) {
|
|
9783
|
+
return null;
|
|
9784
|
+
}
|
|
9387
9785
|
if (record === null) {
|
|
9388
9786
|
return { ...base, destroyed: false, cryptoShredded: false, reason: "absent" };
|
|
9389
9787
|
}
|
|
@@ -9394,6 +9792,7 @@ async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
|
9394
9792
|
if (!encrypted && !allowCleartext) {
|
|
9395
9793
|
return { ...base, destroyed: false, cryptoShredded: false, reason: "cleartext" };
|
|
9396
9794
|
}
|
|
9795
|
+
const unmarked = sweep === void 0 ? withoutSweepMark(record.retention) : null;
|
|
9397
9796
|
try {
|
|
9398
9797
|
await deps.registry.compareAndSwap(ref, record.token, {
|
|
9399
9798
|
status: "destroyed",
|
|
@@ -9404,7 +9803,10 @@ async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
|
9404
9803
|
// the wrappings; clear, they would outlive the segment on its tombstone.
|
|
9405
9804
|
summary: void 0,
|
|
9406
9805
|
// A tombstone holds nothing: a lease names a generation of a segment that now resolves none.
|
|
9407
|
-
leases: void 0
|
|
9806
|
+
leases: void 0,
|
|
9807
|
+
// A sweep's mark, in this same write, so no other tombstone ever carries it: any other tombstone write removes
|
|
9808
|
+
// one a restored or hand-edited row carried.
|
|
9809
|
+
...sweep !== void 0 ? { retention: sweep.stamp(record.retention) } : unmarked === null ? {} : { retention: unmarked }
|
|
9408
9810
|
});
|
|
9409
9811
|
return { ...base, destroyed: true, cryptoShredded: encrypted };
|
|
9410
9812
|
} catch (err) {
|
|
@@ -9415,153 +9817,6 @@ async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
|
9415
9817
|
throw new WriteConflictError(`${op}: contention shredding "${ref.segment}" \u2014 retry`);
|
|
9416
9818
|
}
|
|
9417
9819
|
|
|
9418
|
-
// src/core/retention.ts
|
|
9419
|
-
var EXPIRES_AT = "expiresAt";
|
|
9420
|
-
var RETENTION_CAS_ATTEMPTS = 5;
|
|
9421
|
-
var MIN_EXPIRES_AT_MS = 1e12;
|
|
9422
|
-
function validateRetentionPolicy(policy) {
|
|
9423
|
-
const { expiresAt } = policy;
|
|
9424
|
-
if (typeof expiresAt !== "number" || !Number.isInteger(expiresAt)) {
|
|
9425
|
-
throw new ValidationError(
|
|
9426
|
-
`retention.expiresAt must be an integer epoch-ms; got ${String(expiresAt)}`
|
|
9427
|
-
);
|
|
9428
|
-
}
|
|
9429
|
-
if (expiresAt < MIN_EXPIRES_AT_MS) {
|
|
9430
|
-
throw new ValidationError(
|
|
9431
|
-
`retention.expiresAt (${expiresAt}) is before ${MIN_EXPIRES_AT_MS} \u2014 that is almost certainly epoch SECONDS rather than milliseconds, which would read as "already expired" and retire the segment on the next sweep. Use \`Date.now() + days * 86_400_000\`. To retire a segment now, call \`dropSegment\`.`
|
|
9432
|
-
);
|
|
9433
|
-
}
|
|
9434
|
-
}
|
|
9435
|
-
function readRetentionPolicy(meta) {
|
|
9436
|
-
if (meta === void 0) return null;
|
|
9437
|
-
if (meta === null || typeof meta !== "object" || Array.isArray(meta)) return "invalid";
|
|
9438
|
-
if (!(EXPIRES_AT in meta)) return null;
|
|
9439
|
-
const raw = meta[EXPIRES_AT];
|
|
9440
|
-
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < MIN_EXPIRES_AT_MS)
|
|
9441
|
-
return "invalid";
|
|
9442
|
-
return { expiresAt: raw };
|
|
9443
|
-
}
|
|
9444
|
-
async function getSegmentRetention(ref, deps) {
|
|
9445
|
-
validateUserRef(ref);
|
|
9446
|
-
const record = await deps.registry.get(ref);
|
|
9447
|
-
if (record === null || record.status === "destroyed") return null;
|
|
9448
|
-
return readRetentionPolicy(record.retention);
|
|
9449
|
-
}
|
|
9450
|
-
function readExpiresAt(record) {
|
|
9451
|
-
const parsed = record === null ? null : readRetentionPolicy(record.retention);
|
|
9452
|
-
return parsed === null || parsed === "invalid" ? void 0 : parsed.expiresAt;
|
|
9453
|
-
}
|
|
9454
|
-
async function reindex(ref, deps, expiresAt, previousExpiresAt) {
|
|
9455
|
-
if (!canIndex(ref)) return false;
|
|
9456
|
-
const nextBucket = expiresAt === void 0 ? void 0 : dueBucket(expiresAt);
|
|
9457
|
-
const oldBucket = previousExpiresAt === void 0 ? void 0 : dueBucket(previousExpiresAt);
|
|
9458
|
-
let indexed = expiresAt === void 0;
|
|
9459
|
-
if (nextBucket !== void 0) {
|
|
9460
|
-
try {
|
|
9461
|
-
await deps.registry.create(dueIndexRef(nextBucket, ref), { currentGen: null });
|
|
9462
|
-
indexed = true;
|
|
9463
|
-
} catch (err) {
|
|
9464
|
-
if (isWriteConflictError(err)) indexed = true;
|
|
9465
|
-
}
|
|
9466
|
-
}
|
|
9467
|
-
if (oldBucket !== void 0 && oldBucket !== nextBucket) {
|
|
9468
|
-
try {
|
|
9469
|
-
await deps.registry.delete(dueIndexRef(oldBucket, ref));
|
|
9470
|
-
} catch {
|
|
9471
|
-
}
|
|
9472
|
-
}
|
|
9473
|
-
return indexed;
|
|
9474
|
-
}
|
|
9475
|
-
async function setSegmentRetention(ref, deps, policy) {
|
|
9476
|
-
validateUserRef(ref);
|
|
9477
|
-
validateRetentionPolicy(policy);
|
|
9478
|
-
const base = { segment: ref.segment, namespace: ref.namespace };
|
|
9479
|
-
let previousExpiresAt;
|
|
9480
|
-
const churn = leaseChurn(deps);
|
|
9481
|
-
let lost;
|
|
9482
|
-
for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
|
|
9483
|
-
let record = await deps.registry.get(ref);
|
|
9484
|
-
if (lost !== void 0) {
|
|
9485
|
-
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
9486
|
-
if (settled2 !== void 0) {
|
|
9487
|
-
record = settled2.row;
|
|
9488
|
-
attempt -= 1;
|
|
9489
|
-
}
|
|
9490
|
-
}
|
|
9491
|
-
try {
|
|
9492
|
-
previousExpiresAt = readExpiresAt(record);
|
|
9493
|
-
if (record === null) {
|
|
9494
|
-
await deps.registry.create(ref, {
|
|
9495
|
-
currentGen: null,
|
|
9496
|
-
retention: { [EXPIRES_AT]: policy.expiresAt }
|
|
9497
|
-
});
|
|
9498
|
-
return {
|
|
9499
|
-
...base,
|
|
9500
|
-
expiresAt: policy.expiresAt,
|
|
9501
|
-
createdRow: true,
|
|
9502
|
-
indexed: await reindex(ref, deps, policy.expiresAt, void 0)
|
|
9503
|
-
};
|
|
9504
|
-
}
|
|
9505
|
-
assertNotDestroyed(record, ref);
|
|
9506
|
-
await deps.registry.compareAndSwap(ref, record.token, {
|
|
9507
|
-
retention: { ...record.retention, [EXPIRES_AT]: policy.expiresAt }
|
|
9508
|
-
});
|
|
9509
|
-
return {
|
|
9510
|
-
...base,
|
|
9511
|
-
expiresAt: policy.expiresAt,
|
|
9512
|
-
createdRow: false,
|
|
9513
|
-
indexed: await reindex(ref, deps, policy.expiresAt, previousExpiresAt)
|
|
9514
|
-
};
|
|
9515
|
-
} catch (err) {
|
|
9516
|
-
if (!isWriteConflictError(err)) throw err;
|
|
9517
|
-
lost = record ?? void 0;
|
|
9518
|
-
}
|
|
9519
|
-
}
|
|
9520
|
-
throw new WriteConflictError(
|
|
9521
|
-
`setRetention: contention writing the retention policy for segment "${ref.segment}"`
|
|
9522
|
-
);
|
|
9523
|
-
}
|
|
9524
|
-
async function clearSegmentRetention(ref, deps) {
|
|
9525
|
-
validateUserRef(ref);
|
|
9526
|
-
const churn = leaseChurn(deps);
|
|
9527
|
-
let lost;
|
|
9528
|
-
for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
|
|
9529
|
-
let record = await deps.registry.get(ref);
|
|
9530
|
-
if (lost !== void 0) {
|
|
9531
|
-
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
9532
|
-
if (settled2 !== void 0) {
|
|
9533
|
-
record = settled2.row;
|
|
9534
|
-
attempt -= 1;
|
|
9535
|
-
}
|
|
9536
|
-
}
|
|
9537
|
-
if (record === null) return false;
|
|
9538
|
-
if (record.status === "destroyed") return false;
|
|
9539
|
-
if (record.retention === void 0 || !(EXPIRES_AT in record.retention)) return false;
|
|
9540
|
-
const rest = { ...record.retention };
|
|
9541
|
-
delete rest[EXPIRES_AT];
|
|
9542
|
-
try {
|
|
9543
|
-
await deps.registry.compareAndSwap(ref, record.token, {
|
|
9544
|
-
retention: Object.keys(rest).length === 0 ? void 0 : rest
|
|
9545
|
-
});
|
|
9546
|
-
await reindex(ref, deps, void 0, readExpiresAt(record));
|
|
9547
|
-
return true;
|
|
9548
|
-
} catch (err) {
|
|
9549
|
-
if (!isWriteConflictError(err)) throw err;
|
|
9550
|
-
lost = record;
|
|
9551
|
-
}
|
|
9552
|
-
}
|
|
9553
|
-
throw new WriteConflictError(
|
|
9554
|
-
`clearRetention: contention clearing the retention policy for segment "${ref.segment}"`
|
|
9555
|
-
);
|
|
9556
|
-
}
|
|
9557
|
-
function assertNotDestroyed(record, ref) {
|
|
9558
|
-
if (record.status === "destroyed") {
|
|
9559
|
-
throw new ValidationError(
|
|
9560
|
-
`segment "${ref.segment}" is destroyed (crypto-shredded) \u2014 refusing to set a retention policy on a tombstone; there is nothing left to retire. Use a new segment.`
|
|
9561
|
-
);
|
|
9562
|
-
}
|
|
9563
|
-
}
|
|
9564
|
-
|
|
9565
9820
|
// src/core/retention-sweep.ts
|
|
9566
9821
|
var DEFAULT_RETIRE_LIMIT = 100;
|
|
9567
9822
|
var MAX_CONSECUTIVE_PURGE_FAULTS = 3;
|
|
@@ -9590,7 +9845,7 @@ async function rowsFromDueIndex(registry, options) {
|
|
|
9590
9845
|
continue;
|
|
9591
9846
|
}
|
|
9592
9847
|
if (gone.has(key)) {
|
|
9593
|
-
litter.push(found);
|
|
9848
|
+
if (litter.length < options.maxScanSegments) litter.push(found);
|
|
9594
9849
|
continue;
|
|
9595
9850
|
}
|
|
9596
9851
|
if (rows.length >= options.maxScanSegments) {
|
|
@@ -9600,8 +9855,8 @@ async function rowsFromDueIndex(registry, options) {
|
|
|
9600
9855
|
}
|
|
9601
9856
|
const live = await registry.get(ref);
|
|
9602
9857
|
if (live === null) {
|
|
9603
|
-
gone.add(key);
|
|
9604
|
-
litter.push(found);
|
|
9858
|
+
if (gone.size < options.maxScanSegments) gone.add(key);
|
|
9859
|
+
if (litter.length < options.maxScanSegments) litter.push(found);
|
|
9605
9860
|
continue;
|
|
9606
9861
|
}
|
|
9607
9862
|
rows.push(live);
|
|
@@ -9667,6 +9922,23 @@ async function forgetPurgePointers(registry, ref, purgeAt, found, onFault) {
|
|
|
9667
9922
|
}
|
|
9668
9923
|
}
|
|
9669
9924
|
async function retireExpired(deps, options) {
|
|
9925
|
+
checkedAuditSink(options.audit, "retireExpired");
|
|
9926
|
+
if (options.shards !== void 0) {
|
|
9927
|
+
const total = options.totalShards;
|
|
9928
|
+
if (typeof total !== "number" || !Number.isInteger(total) || total < 1) {
|
|
9929
|
+
throw new ValidationError("retireExpired: shards needs totalShards, an integer of 1 or more");
|
|
9930
|
+
}
|
|
9931
|
+
if (!Array.isArray(options.shards)) {
|
|
9932
|
+
throw new ValidationError("retireExpired: shards must be an array of shard numbers");
|
|
9933
|
+
}
|
|
9934
|
+
for (const shard of options.shards) {
|
|
9935
|
+
if (typeof shard !== "number" || !Number.isInteger(shard) || shard < 0 || shard >= total) {
|
|
9936
|
+
throw new ValidationError(
|
|
9937
|
+
`retireExpired: a shard is an integer from 0 to totalShards - 1 (${total - 1}); got ${String(shard)}`
|
|
9938
|
+
);
|
|
9939
|
+
}
|
|
9940
|
+
}
|
|
9941
|
+
}
|
|
9670
9942
|
if (options.namespace !== void 0) validateUserNamespace(options.namespace);
|
|
9671
9943
|
const now = options.now;
|
|
9672
9944
|
if (!Number.isFinite(now)) {
|
|
@@ -9742,8 +10014,24 @@ async function retireExpired(deps, options) {
|
|
|
9742
10014
|
attempted += 1;
|
|
9743
10015
|
try {
|
|
9744
10016
|
if (!await isFullyReclaimed(deps, ref)) {
|
|
9745
|
-
|
|
10017
|
+
let collectFault;
|
|
10018
|
+
if (!dryRun) {
|
|
10019
|
+
await gcOrphanGenerations(ref, deps).catch((error) => {
|
|
10020
|
+
collectFault = { error };
|
|
10021
|
+
});
|
|
10022
|
+
}
|
|
9746
10023
|
if (!await isFullyReclaimed(deps, ref)) {
|
|
10024
|
+
const lostRace = collectFault !== void 0 && isWriteConflictError(collectFault.error);
|
|
10025
|
+
if (!dryRun && !lostRace) {
|
|
10026
|
+
attempted -= 1;
|
|
10027
|
+
purgeRun += 1;
|
|
10028
|
+
if (purgeRun >= MAX_CONSECUTIVE_PURGE_FAULTS) purging = false;
|
|
10029
|
+
if (collectFault !== void 0) noteFault(collectFault.error);
|
|
10030
|
+
else {
|
|
10031
|
+
purgeFaults += 1;
|
|
10032
|
+
firstPurgeFault ??= "failed: the tombstone's objects were still there after its collection";
|
|
10033
|
+
}
|
|
10034
|
+
}
|
|
9747
10035
|
entries.push({ ...base, action: "skipped", reason: "tombstone-not-empty" });
|
|
9748
10036
|
continue;
|
|
9749
10037
|
}
|
|
@@ -9796,11 +10084,23 @@ async function retireExpired(deps, options) {
|
|
|
9796
10084
|
continue;
|
|
9797
10085
|
}
|
|
9798
10086
|
attempted += 1;
|
|
9799
|
-
const result = await
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
audit: options.audit
|
|
9803
|
-
|
|
10087
|
+
const result = await dropSegmentFor(
|
|
10088
|
+
ref,
|
|
10089
|
+
deps,
|
|
10090
|
+
{ confirmSegment: rec.segment, dryRun, audit: options.audit },
|
|
10091
|
+
dryRun ? void 0 : {
|
|
10092
|
+
wanted: (row) => {
|
|
10093
|
+
const p = readRetentionPolicy(row.retention);
|
|
10094
|
+
return p !== null && p !== "invalid" && p.expiresAt <= now;
|
|
10095
|
+
},
|
|
10096
|
+
stamp: (retention) => ({ ...retention, [RETIRED_AT]: now })
|
|
10097
|
+
}
|
|
10098
|
+
);
|
|
10099
|
+
if (result === null) {
|
|
10100
|
+
attempted -= 1;
|
|
10101
|
+
entries.push({ ...base, action: "skipped", reason: "policy-changed" });
|
|
10102
|
+
continue;
|
|
10103
|
+
}
|
|
9804
10104
|
if (dryRun) {
|
|
9805
10105
|
wouldRetire += 1;
|
|
9806
10106
|
entries.push({ ...base, action: "would-retire", expiresAt: livePolicy.expiresAt, result });
|
|
@@ -9830,13 +10130,11 @@ async function retireExpired(deps, options) {
|
|
|
9830
10130
|
if (deleted) continue;
|
|
9831
10131
|
}
|
|
9832
10132
|
}
|
|
9833
|
-
|
|
9834
|
-
if (stamped && removesRows) await filePurgePointer(deps.registry, ref, now + grace);
|
|
10133
|
+
if (removesRows) await filePurgePointer(deps.registry, ref, now + grace);
|
|
9835
10134
|
} catch (err) {
|
|
9836
10135
|
const after = await deps.registry.get(ref).catch(() => null);
|
|
9837
|
-
if (after?.status === "destroyed") {
|
|
9838
|
-
|
|
9839
|
-
if (stamped && removesRows) await filePurgePointer(deps.registry, ref, now + grace);
|
|
10136
|
+
if (after?.status === "destroyed" && after.retention?.[RETIRED_AT] === now) {
|
|
10137
|
+
if (removesRows) await filePurgePointer(deps.registry, ref, now + grace);
|
|
9840
10138
|
retired += 1;
|
|
9841
10139
|
entries.push({
|
|
9842
10140
|
...base,
|
|
@@ -9884,7 +10182,7 @@ async function retireExpired(deps, options) {
|
|
|
9884
10182
|
entries
|
|
9885
10183
|
};
|
|
9886
10184
|
}
|
|
9887
|
-
var RETIRED_AT =
|
|
10185
|
+
var RETIRED_AT = SWEEP_MARK;
|
|
9888
10186
|
function retirementStamp(meta) {
|
|
9889
10187
|
if (meta === void 0 || meta === null || typeof meta !== "object" || Array.isArray(meta)) {
|
|
9890
10188
|
return null;
|
|
@@ -9892,21 +10190,6 @@ function retirementStamp(meta) {
|
|
|
9892
10190
|
const raw = meta[RETIRED_AT];
|
|
9893
10191
|
return typeof raw === "number" && Number.isInteger(raw) && raw >= MIN_EXPIRES_AT_MS ? raw : null;
|
|
9894
10192
|
}
|
|
9895
|
-
async function stampRetirement(registry, ref, now) {
|
|
9896
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
9897
|
-
const rec = await registry.get(ref);
|
|
9898
|
-
if (rec === null || rec.status !== "destroyed") return false;
|
|
9899
|
-
try {
|
|
9900
|
-
await registry.compareAndSwap(ref, rec.token, {
|
|
9901
|
-
retention: { ...rec.retention, [RETIRED_AT]: now }
|
|
9902
|
-
});
|
|
9903
|
-
return true;
|
|
9904
|
-
} catch (err) {
|
|
9905
|
-
if (!isWriteConflictError(err)) throw err;
|
|
9906
|
-
}
|
|
9907
|
-
}
|
|
9908
|
-
return false;
|
|
9909
|
-
}
|
|
9910
10193
|
async function isFullyReclaimed(deps, ref) {
|
|
9911
10194
|
for await (const key of deps.storage.list(ref)) {
|
|
9912
10195
|
void key;
|
|
@@ -9919,31 +10202,6 @@ function failureReason(err) {
|
|
|
9919
10202
|
return `failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
9920
10203
|
}
|
|
9921
10204
|
|
|
9922
|
-
// src/core/registry-reap.ts
|
|
9923
|
-
var DEFAULT_REAP_LIMIT = 1e3;
|
|
9924
|
-
async function reapRegistryTombstones(registry, options = {}) {
|
|
9925
|
-
const dryRun = options.dryRun === true;
|
|
9926
|
-
if (!dryRun && options.confirmNoLegacyWriters !== true) {
|
|
9927
|
-
throw new ValidationError(
|
|
9928
|
-
"reapRegistryTombstones: a real run needs `confirmNoLegacyWriters: true`, your statement that no process on a release before 0.12 writes this registry (one that did would issue a removed row\u2019s tokens again). Pass `dryRun: true` to see what would be removed."
|
|
9929
|
-
);
|
|
9930
|
-
}
|
|
9931
|
-
const limit = options.limit ?? DEFAULT_REAP_LIMIT;
|
|
9932
|
-
if (!Number.isInteger(limit) || limit < 1) {
|
|
9933
|
-
throw new ValidationError(
|
|
9934
|
-
`reapRegistryTombstones: \`limit\` must be a positive integer; got ${String(limit)}`
|
|
9935
|
-
);
|
|
9936
|
-
}
|
|
9937
|
-
if (options.namespace !== void 0) validateUserNamespace(options.namespace);
|
|
9938
|
-
if (registry.reapLegacyTombstones === void 0) {
|
|
9939
|
-
throw new UnsupportedError(
|
|
9940
|
-
"reapRegistryTombstones: this registry keeps no deleted rows of a release before 0.12 to remove (only the object-store registries, S3, GCS and Azure Blob, do)"
|
|
9941
|
-
);
|
|
9942
|
-
}
|
|
9943
|
-
const request = options.namespace === void 0 ? { dryRun, limit } : { namespace: options.namespace, dryRun, limit };
|
|
9944
|
-
return registry.reapLegacyTombstones(request);
|
|
9945
|
-
}
|
|
9946
|
-
|
|
9947
10205
|
// src/core/consistency.ts
|
|
9948
10206
|
var DEFAULT_CHECK_CONCURRENCY = 8;
|
|
9949
10207
|
async function generationsPresent(storage, ref) {
|
|
@@ -9960,7 +10218,7 @@ async function checkSummary(ref, live, deps) {
|
|
|
9960
10218
|
try {
|
|
9961
10219
|
aead = await deps.keystore.openDek(live.wrappedDeks);
|
|
9962
10220
|
} catch (err) {
|
|
9963
|
-
if (isIntegrityError(err) ||
|
|
10221
|
+
if (isIntegrityError(err) || isKeyUnavailableError(err))
|
|
9964
10222
|
return { kind: "ok", unchecked: true };
|
|
9965
10223
|
throw err;
|
|
9966
10224
|
}
|
|
@@ -10048,8 +10306,11 @@ async function runConsistencyCheck(deps, options = {}) {
|
|
|
10048
10306
|
|
|
10049
10307
|
// src/drivers/retry/retrying-chunk-source.ts
|
|
10050
10308
|
function toRetry(opts) {
|
|
10309
|
+
if (opts.onRetry !== void 0 && typeof opts.onRetry !== "function") {
|
|
10310
|
+
throw new ValidationError("retry.onRetry must be a function");
|
|
10311
|
+
}
|
|
10051
10312
|
return {
|
|
10052
|
-
policy: opts.policy ?? DEFAULT_RETRY_POLICY,
|
|
10313
|
+
policy: checkedRetryPolicy(opts.policy ?? DEFAULT_RETRY_POLICY),
|
|
10053
10314
|
deps: {
|
|
10054
10315
|
clock: opts.clock,
|
|
10055
10316
|
rng: opts.rng,
|
|
@@ -10182,24 +10443,27 @@ export {
|
|
|
10182
10443
|
excludingReservedRows,
|
|
10183
10444
|
getSegmentRetention,
|
|
10184
10445
|
groundedReport,
|
|
10446
|
+
isBudgetExceededError,
|
|
10447
|
+
isCapabilityError,
|
|
10185
10448
|
isCloudRoaringError,
|
|
10186
10449
|
isIntegrityError,
|
|
10450
|
+
isKeyUnavailableError,
|
|
10187
10451
|
isLeaseExpiredError,
|
|
10188
10452
|
isLeaseLimitError,
|
|
10189
10453
|
isNotFoundError,
|
|
10190
10454
|
isStaleOperandError,
|
|
10191
10455
|
isStorageBackend,
|
|
10192
10456
|
isTransientError,
|
|
10457
|
+
isUnsupportedError,
|
|
10193
10458
|
isValidationError,
|
|
10194
10459
|
isWriteConflictError,
|
|
10460
|
+
judgeLoad,
|
|
10195
10461
|
listGenerations,
|
|
10196
10462
|
listSegments,
|
|
10197
10463
|
loadSegment,
|
|
10198
10464
|
loadSegmentChunks,
|
|
10199
10465
|
mapWithConcurrency,
|
|
10200
|
-
prepareHeld,
|
|
10201
10466
|
readRetentionPolicy,
|
|
10202
|
-
reapRegistryTombstones,
|
|
10203
10467
|
rebindCombineMany,
|
|
10204
10468
|
releaseLease,
|
|
10205
10469
|
resolveBudget,
|