@cloudbitmaps/core 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -5
- package/dist/{chunk-YSC7NJWV.js → chunk-OSU7W2Y3.js} +811 -164
- package/dist/chunk-OSU7W2Y3.js.map +7 -0
- package/dist/core/audit.d.ts +12 -2
- package/dist/core/bit-route.d.ts +9 -1
- package/dist/core/blob.d.ts +12 -1
- package/dist/core/codec.d.ts +1 -1
- package/dist/core/combine-expr.d.ts +10 -8
- package/dist/core/combine-feed.d.ts +12 -18
- package/dist/core/combine-many.d.ts +29 -22
- package/dist/core/concurrency.d.ts +8 -0
- package/dist/core/consistency.d.ts +69 -17
- package/dist/core/crbm/reader.d.ts +52 -6
- package/dist/core/crbm-storage-source.d.ts +65 -452
- package/dist/core/crypto.d.ts +6 -1
- package/dist/core/determinism.d.ts +2 -1
- package/dist/core/engine.d.ts +222 -33
- package/dist/core/erase-id.d.ts +60 -15
- package/dist/core/erasure.d.ts +5 -4
- package/dist/core/errors.d.ts +61 -11
- package/dist/core/leases.d.ts +4 -3
- package/dist/core/load.d.ts +58 -45
- package/dist/core/lru.d.ts +2 -15
- package/dist/core/metrics.d.ts +5 -3
- package/dist/core/pinned-storage-source.d.ts +4 -4
- package/dist/core/ports.d.ts +96 -22
- package/dist/core/registry-scan.d.ts +0 -7
- package/dist/core/retention-sweep.d.ts +70 -66
- package/dist/core/retention.d.ts +2 -2
- package/dist/core/retry.d.ts +15 -18
- package/dist/core/rollback.d.ts +61 -31
- package/dist/core/validate.d.ts +2 -0
- package/dist/driver-kit.d.ts +11 -2
- package/dist/driver-kit.js +84 -16
- package/dist/driver-kit.js.map +3 -3
- package/dist/drivers/_shared/bytes.d.ts +6 -0
- package/dist/drivers/_shared/faults.d.ts +15 -0
- package/dist/drivers/_shared/object-keys.d.ts +18 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
- package/dist/drivers/_shared/object-registry.d.ts +2 -0
- package/dist/drivers/_shared/options.d.ts +19 -0
- package/dist/drivers/backends.d.ts +2 -0
- package/dist/drivers/crypto.d.ts +1 -1
- package/dist/export/index.d.ts +9 -8
- package/dist/index.d.ts +11 -10
- package/dist/index.js +4202 -2833
- package/dist/index.js.map +4 -4
- package/package.json +7 -2
- package/dist/chunk-YSC7NJWV.js.map +0 -7
- package/dist/core/chunk-checks.d.ts +0 -34
- package/dist/core/chunk-stream.d.ts +0 -22
- package/dist/core/chunk-window.d.ts +0 -30
- package/dist/core/combine-ledger.d.ts +0 -43
- package/dist/core/crbm/crc32c.d.ts +0 -10
- package/dist/core/crbm/fingerprint.d.ts +0 -18
- package/dist/core/crbm/format.d.ts +0 -151
- package/dist/core/crbm/frame.d.ts +0 -10
- package/dist/core/crbm/plan-reads.d.ts +0 -44
- package/dist/core/crbm/varint.d.ts +0 -13
- package/dist/core/crbm/writer.d.ts +0 -68
- package/dist/core/due-index.d.ts +0 -53
- package/dist/core/generation-gc.d.ts +0 -179
- package/dist/core/item-pull.d.ts +0 -29
- package/dist/core/kept-generations.d.ts +0 -40
- package/dist/core/metadata.d.ts +0 -42
- package/dist/core/reader-defaults.d.ts +0 -29
- package/dist/core/summary.d.ts +0 -78
- package/dist/core/token.d.ts +0 -41
- package/dist/drivers/_shared/entropy.d.ts +0 -5
- package/dist/drivers/_shared/registry.d.ts +0 -104
- package/dist/drivers/localfs/exact-case.d.ts +0 -37
- package/dist/drivers/localfs/fs-util.d.ts +0 -33
- package/dist/drivers/localfs/paths.d.ts +0 -40
- package/dist/drivers/localfs/registry.d.ts +0 -44
- package/dist/drivers/localfs/storage.d.ts +0 -21
- package/dist/drivers/memory.d.ts +0 -61
- package/dist/testing/fuzz-core.d.ts +0 -15
|
@@ -3,7 +3,7 @@ var ERROR_BRAND = /* @__PURE__ */ Symbol.for("cloudbitmaps.error");
|
|
|
3
3
|
var TRANSIENT_BRAND = /* @__PURE__ */ Symbol.for("cloudbitmaps.error.transient");
|
|
4
4
|
var NAMES = /* @__PURE__ */ new WeakMap();
|
|
5
5
|
var CloudRoaringError = class extends Error {
|
|
6
|
-
/** Cross-bundle brand — see
|
|
6
|
+
/** Cross-bundle brand — see {@link errorKind}. Non-enumerable-ish (symbol key ⇒ not in JSON). */
|
|
7
7
|
[ERROR_BRAND] = true;
|
|
8
8
|
constructor(message) {
|
|
9
9
|
super(message);
|
|
@@ -12,6 +12,7 @@ var CloudRoaringError = class extends Error {
|
|
|
12
12
|
};
|
|
13
13
|
var ValidationError = class extends CloudRoaringError {
|
|
14
14
|
};
|
|
15
|
+
var CONTENTION_NEXT_STEP = "; another writer changed the segment's row while this call ran. Re-run the call: it reads the row afresh";
|
|
15
16
|
var WriteConflictError = class extends CloudRoaringError {
|
|
16
17
|
};
|
|
17
18
|
var IntegrityError = class extends CloudRoaringError {
|
|
@@ -48,6 +49,21 @@ var UnsupportedError = class extends CloudRoaringError {
|
|
|
48
49
|
var CapabilityError = class extends CloudRoaringError {
|
|
49
50
|
};
|
|
50
51
|
var BudgetExceededError = class extends CloudRoaringError {
|
|
52
|
+
/** The option to raise to let the call through. */
|
|
53
|
+
option;
|
|
54
|
+
/** The bound in force: requests, bytes or rows, by `option`. */
|
|
55
|
+
limit;
|
|
56
|
+
/**
|
|
57
|
+
* What the call had reached when refused. By `option`: the requests the call needs (`'budget.maxRequests'`), a
|
|
58
|
+
* lower bound on the resident bytes it needs (`'maxBufferedBytes'`), or `limit + 1` (`'maxScanSegments'`).
|
|
59
|
+
*/
|
|
60
|
+
spent;
|
|
61
|
+
constructor(message, details) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.option = details.option;
|
|
64
|
+
this.limit = details.limit;
|
|
65
|
+
this.spent = details.spent;
|
|
66
|
+
}
|
|
51
67
|
};
|
|
52
68
|
var KeyUnavailableError = class extends CloudRoaringError {
|
|
53
69
|
};
|
|
@@ -84,15 +100,6 @@ function isWriteConflictError(err) {
|
|
|
84
100
|
function isTransientError(err) {
|
|
85
101
|
return hasBrand(err, TRANSIENT_BRAND);
|
|
86
102
|
}
|
|
87
|
-
function isLeaseExpiredError(err) {
|
|
88
|
-
return isCloudRoaringError(err) && err.name === "LeaseExpiredError";
|
|
89
|
-
}
|
|
90
|
-
function isStaleOperandError(err) {
|
|
91
|
-
return isCloudRoaringError(err) && err.name === "StaleOperandError";
|
|
92
|
-
}
|
|
93
|
-
function isLeaseLimitError(err) {
|
|
94
|
-
return isCloudRoaringError(err) && err.name === "LeaseLimitError";
|
|
95
|
-
}
|
|
96
103
|
function isNotFoundError(err) {
|
|
97
104
|
return isCloudRoaringError(err) && err.name === "NotFoundError";
|
|
98
105
|
}
|
|
@@ -102,23 +109,169 @@ function isIntegrityError(err) {
|
|
|
102
109
|
function isValidationError(err) {
|
|
103
110
|
return isCloudRoaringError(err) && err.name === "ValidationError";
|
|
104
111
|
}
|
|
105
|
-
function
|
|
106
|
-
return isCloudRoaringError(err) && err.name === "
|
|
112
|
+
function isKeyUnavailableError(err) {
|
|
113
|
+
return isCloudRoaringError(err) && err.name === "KeyUnavailableError";
|
|
107
114
|
}
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
var KIND_NAMES = {
|
|
116
|
+
CloudRoaringError: true,
|
|
117
|
+
ValidationError: true,
|
|
118
|
+
WriteConflictError: true,
|
|
119
|
+
IntegrityError: true,
|
|
120
|
+
NotFoundError: true,
|
|
121
|
+
UnsupportedError: true,
|
|
122
|
+
CapabilityError: true,
|
|
123
|
+
TransientError: true,
|
|
124
|
+
KeyUnavailableError: true,
|
|
125
|
+
BudgetExceededError: true,
|
|
126
|
+
LeaseExpiredError: true,
|
|
127
|
+
LeaseLimitError: true,
|
|
128
|
+
StaleOperandError: true
|
|
129
|
+
};
|
|
130
|
+
function errorKind(err) {
|
|
131
|
+
if (hasBrand(err, TRANSIENT_BRAND)) return "TransientError";
|
|
132
|
+
if (!hasBrand(err, ERROR_BRAND)) return void 0;
|
|
133
|
+
const name = err.name;
|
|
134
|
+
return typeof name === "string" && Object.hasOwn(KIND_NAMES, name) ? name : "CloudRoaringError";
|
|
110
135
|
}
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
|
|
137
|
+
// src/core/bit-route.ts
|
|
138
|
+
var U32_MAX = 4294967295;
|
|
139
|
+
var CHUNK_COUNT = 65536;
|
|
140
|
+
var LOW_16 = 65535;
|
|
141
|
+
var idErrors = /* @__PURE__ */ new WeakSet();
|
|
142
|
+
function isBadIdError(err) {
|
|
143
|
+
return err instanceof ValidationError && idErrors.has(err);
|
|
144
|
+
}
|
|
145
|
+
function badId(id) {
|
|
146
|
+
let got;
|
|
147
|
+
if (id === null) got = "null";
|
|
148
|
+
else if (id === void 0) got = "undefined";
|
|
149
|
+
else if (typeof id === "object") got = "an object";
|
|
150
|
+
else if (typeof id !== "number") got = `${/^[aeiou]/.test(typeof id) ? "an" : "a"} ${typeof id}`;
|
|
151
|
+
else if (!Number.isFinite(id)) got = "NaN or an infinity";
|
|
152
|
+
else if (id < 0) got = "a negative number";
|
|
153
|
+
else if (!Number.isInteger(id)) got = "a number with a fraction";
|
|
154
|
+
else got = `a number above ${U32_MAX} (ids are 32-bit)`;
|
|
155
|
+
const err = new ValidationError(`an id must be an integer from 0 to ${U32_MAX}; got ${got}`);
|
|
156
|
+
idErrors.add(err);
|
|
157
|
+
return err;
|
|
158
|
+
}
|
|
159
|
+
function splitId(id) {
|
|
160
|
+
if (!Number.isInteger(id) || id < 0 || id > U32_MAX) {
|
|
161
|
+
throw badId(id);
|
|
162
|
+
}
|
|
163
|
+
return { chunkKey: id >>> 16, remainder: id & LOW_16 };
|
|
164
|
+
}
|
|
165
|
+
var MAX_REMAINDER = CHUNK_COUNT - 1;
|
|
166
|
+
function joinId(chunkKey, remainder) {
|
|
167
|
+
return ((chunkKey & LOW_16) << 16 >>> 0) + (remainder & LOW_16);
|
|
113
168
|
}
|
|
114
|
-
|
|
115
|
-
|
|
169
|
+
|
|
170
|
+
// src/core/check.ts
|
|
171
|
+
function shown(value) {
|
|
172
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
173
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
174
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
175
|
+
return value === null ? "null" : typeof value;
|
|
176
|
+
}
|
|
177
|
+
function rangeOf(min, max, note) {
|
|
178
|
+
let range = "";
|
|
179
|
+
if (min !== void 0 && max !== void 0) range = ` from ${min} to ${max}`;
|
|
180
|
+
else if (min !== void 0) range = ` of at least ${min}`;
|
|
181
|
+
else if (max !== void 0) range = ` of at most ${max}`;
|
|
182
|
+
return note === void 0 ? range : `${range} (${note})`;
|
|
183
|
+
}
|
|
184
|
+
function requireInteger(value, bounds) {
|
|
185
|
+
const { min, max, what, note } = bounds;
|
|
186
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || max !== void 0 && value > max) {
|
|
187
|
+
throw new ValidationError(
|
|
188
|
+
`${what} must be an integer${rangeOf(min, max, note)}; got ${shown(value)}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
function requirePositiveInteger(value, what) {
|
|
194
|
+
return requireInteger(value, { min: 1, what });
|
|
195
|
+
}
|
|
196
|
+
function requireFiniteNumber(value, bounds) {
|
|
197
|
+
const { min, max, what, note } = bounds;
|
|
198
|
+
if (typeof value !== "number" || !Number.isFinite(value) || min !== void 0 && value < min || max !== void 0 && value > max) {
|
|
199
|
+
throw new ValidationError(
|
|
200
|
+
`${what} must be a finite number${rangeOf(min, max, note)}; got ${shown(value)}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/core/concurrency.ts
|
|
207
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
208
|
+
requirePositiveInteger(limit, "concurrency limit");
|
|
209
|
+
const results = new Array(items.length);
|
|
210
|
+
let next = 0;
|
|
211
|
+
let failed = false;
|
|
212
|
+
let firstError;
|
|
213
|
+
const worker = async () => {
|
|
214
|
+
while (!failed) {
|
|
215
|
+
const i = next++;
|
|
216
|
+
if (i >= items.length) return;
|
|
217
|
+
try {
|
|
218
|
+
results[i] = await fn(items[i], i);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (!failed) {
|
|
221
|
+
failed = true;
|
|
222
|
+
firstError = err;
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
229
|
+
await Promise.all(workers);
|
|
230
|
+
if (failed) throw firstError;
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
function boundedAll(items, limit, fn) {
|
|
234
|
+
try {
|
|
235
|
+
requirePositiveInteger(limit, "concurrency limit");
|
|
236
|
+
} catch (err) {
|
|
237
|
+
return Promise.reject(err);
|
|
238
|
+
}
|
|
239
|
+
if (items.length > limit) return inWaves(items, limit, fn);
|
|
240
|
+
const calls = [];
|
|
241
|
+
for (const item of items) {
|
|
242
|
+
try {
|
|
243
|
+
calls.push(fn(item));
|
|
244
|
+
} catch (err) {
|
|
245
|
+
void Promise.allSettled(calls);
|
|
246
|
+
return Promise.reject(err);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return Promise.all(calls);
|
|
250
|
+
}
|
|
251
|
+
async function inWaves(items, limit, fn) {
|
|
252
|
+
const results = new Array(items.length);
|
|
253
|
+
let next = 0;
|
|
254
|
+
let failed = false;
|
|
255
|
+
const worker = async () => {
|
|
256
|
+
while (!failed && next < items.length) {
|
|
257
|
+
const i = next++;
|
|
258
|
+
try {
|
|
259
|
+
results[i] = await fn(items[i]);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
failed = true;
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
267
|
+
return results;
|
|
116
268
|
}
|
|
117
269
|
|
|
118
270
|
// src/core/name-codec.ts
|
|
119
271
|
var KEY_SAFE = /[A-Za-z0-9._:-]/;
|
|
120
272
|
var PATH_SAFE = /[A-Za-z0-9._-]/;
|
|
121
273
|
var UTF8 = new TextEncoder();
|
|
274
|
+
var UTF8_LENIENT = new TextDecoder("utf-8", { fatal: false, ignoreBOM: true });
|
|
122
275
|
function escapeChar(ch) {
|
|
123
276
|
let out = "";
|
|
124
277
|
for (const byte of UTF8.encode(ch)) {
|
|
@@ -136,17 +289,17 @@ function encodeWith(name, safe) {
|
|
|
136
289
|
function decodePercent(encoded) {
|
|
137
290
|
const bytes = [];
|
|
138
291
|
for (let i = 0; i < encoded.length; ) {
|
|
139
|
-
if (encoded[i] === "%" && i + 2
|
|
292
|
+
if (encoded[i] === "%" && i + 2 <= encoded.length) {
|
|
140
293
|
const hex = encoded.slice(i + 1, i + 3);
|
|
141
294
|
if (!/^[0-9A-Fa-f]{2}$/.test(hex)) return encoded;
|
|
142
295
|
bytes.push(parseInt(hex, 16));
|
|
143
296
|
i += 3;
|
|
144
297
|
} else {
|
|
145
|
-
for (const b of
|
|
298
|
+
for (const b of UTF8.encode(encoded[i])) bytes.push(b);
|
|
146
299
|
i += 1;
|
|
147
300
|
}
|
|
148
301
|
}
|
|
149
|
-
return
|
|
302
|
+
return UTF8_LENIENT.decode(new Uint8Array(bytes));
|
|
150
303
|
}
|
|
151
304
|
function encodeNameForKey(name) {
|
|
152
305
|
return defuseDotComponent(encodeWith(name, KEY_SAFE));
|
|
@@ -206,7 +359,13 @@ function validatePart(value, field) {
|
|
|
206
359
|
);
|
|
207
360
|
}
|
|
208
361
|
}
|
|
362
|
+
function requireRefObject(ref, hint = "") {
|
|
363
|
+
if (typeof ref !== "object" || ref === null) {
|
|
364
|
+
throw new ValidationError(`a segment ref is an object such as { segment: 'name' }${hint}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
209
367
|
function validateSegmentRef(ref) {
|
|
368
|
+
requireRefObject(ref);
|
|
210
369
|
validatePart(ref.segment, "segment");
|
|
211
370
|
if (ref.namespace !== void 0) validatePart(ref.namespace, "namespace");
|
|
212
371
|
}
|
|
@@ -223,6 +382,7 @@ function validateUserNamespace(namespace) {
|
|
|
223
382
|
}
|
|
224
383
|
}
|
|
225
384
|
function validateUserRef(ref) {
|
|
385
|
+
requireRefObject(ref, "; store.segment() takes the bare name");
|
|
226
386
|
validatePart(ref.segment, "segment");
|
|
227
387
|
if (ref.namespace !== void 0) validateUserNamespace(ref.namespace);
|
|
228
388
|
}
|
|
@@ -234,39 +394,12 @@ function validateChunkKeyOrder(keys) {
|
|
|
234
394
|
}
|
|
235
395
|
function validateChunkRef(ref) {
|
|
236
396
|
validateSegmentRef(ref);
|
|
237
|
-
|
|
238
|
-
throw new ValidationError(`chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]`);
|
|
239
|
-
}
|
|
397
|
+
validateChunkKey(ref.chunkKey);
|
|
240
398
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (!Number.isInteger(limit) || limit < 1) {
|
|
245
|
-
throw new ValidationError(`concurrency limit must be a positive integer; got ${limit}`);
|
|
399
|
+
function validateChunkKey(chunkKey) {
|
|
400
|
+
if (!(typeof chunkKey === "number" && chunkKey >= 0 && chunkKey <= CHUNK_KEY_MAX && (chunkKey | 0) === chunkKey)) {
|
|
401
|
+
throw new ValidationError(`chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]`);
|
|
246
402
|
}
|
|
247
|
-
const results = new Array(items.length);
|
|
248
|
-
let next = 0;
|
|
249
|
-
let failed = false;
|
|
250
|
-
let firstError;
|
|
251
|
-
const worker = async () => {
|
|
252
|
-
while (!failed) {
|
|
253
|
-
const i = next++;
|
|
254
|
-
if (i >= items.length) return;
|
|
255
|
-
try {
|
|
256
|
-
results[i] = await fn(items[i], i);
|
|
257
|
-
} catch (err) {
|
|
258
|
-
if (!failed) {
|
|
259
|
-
failed = true;
|
|
260
|
-
firstError = err;
|
|
261
|
-
}
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
267
|
-
await Promise.all(workers);
|
|
268
|
-
if (failed) throw firstError;
|
|
269
|
-
return results;
|
|
270
403
|
}
|
|
271
404
|
|
|
272
405
|
// src/core/ports.ts
|
|
@@ -336,6 +469,97 @@ function pointerIdOf(row) {
|
|
|
336
469
|
return pointerId;
|
|
337
470
|
}
|
|
338
471
|
|
|
472
|
+
// src/core/crypto.ts
|
|
473
|
+
var UTF82 = new TextEncoder();
|
|
474
|
+
var AAD_VERSION = 1;
|
|
475
|
+
var SCOPE_CHUNK = 0;
|
|
476
|
+
var SCOPE_INDEX = 1;
|
|
477
|
+
var SCOPE_METADATA = 2;
|
|
478
|
+
var SCOPE_SUMMARY = 3;
|
|
479
|
+
function aadFor(ref, generation, scope) {
|
|
480
|
+
const ns = UTF82.encode(ref.namespace ?? "");
|
|
481
|
+
const seg = UTF82.encode(ref.segment);
|
|
482
|
+
const out = new Uint8Array(1 + 2 + ns.length + 2 + seg.length + 8 + 1 + 4);
|
|
483
|
+
const view = new DataView(out.buffer);
|
|
484
|
+
let o = 0;
|
|
485
|
+
out[o] = AAD_VERSION;
|
|
486
|
+
o += 1;
|
|
487
|
+
view.setUint16(o, ns.length, true);
|
|
488
|
+
o += 2;
|
|
489
|
+
out.set(ns, o);
|
|
490
|
+
o += ns.length;
|
|
491
|
+
view.setUint16(o, seg.length, true);
|
|
492
|
+
o += 2;
|
|
493
|
+
out.set(seg, o);
|
|
494
|
+
o += seg.length;
|
|
495
|
+
view.setBigUint64(o, BigInt(generation), true);
|
|
496
|
+
o += 8;
|
|
497
|
+
out[o] = scope === "index" ? SCOPE_INDEX : scope === "metadata" ? SCOPE_METADATA : scope === "summary" ? SCOPE_SUMMARY : SCOPE_CHUNK;
|
|
498
|
+
o += 1;
|
|
499
|
+
view.setUint32(o, typeof scope === "number" ? scope : 0, true);
|
|
500
|
+
return out;
|
|
501
|
+
}
|
|
502
|
+
var sameKeys = (a, b) => a !== void 0 && JSON.stringify(a) === JSON.stringify(b);
|
|
503
|
+
|
|
504
|
+
// src/core/retry.ts
|
|
505
|
+
var DEFAULT_RETRY_POLICY = {
|
|
506
|
+
maxAttempts: 4,
|
|
507
|
+
baseDelayMs: 50,
|
|
508
|
+
maxDelayMs: 2e3,
|
|
509
|
+
backoffFactor: 2,
|
|
510
|
+
jitter: "full"
|
|
511
|
+
};
|
|
512
|
+
function backoffDelayMs(policy, attempt) {
|
|
513
|
+
const raw = policy.baseDelayMs * policy.backoffFactor ** (attempt - 1);
|
|
514
|
+
return Math.min(policy.maxDelayMs, raw);
|
|
515
|
+
}
|
|
516
|
+
function applyJitter(policy, delayMs, rng) {
|
|
517
|
+
if (policy.jitter === "none") return delayMs;
|
|
518
|
+
return rng.next() * delayMs;
|
|
519
|
+
}
|
|
520
|
+
function jitteredWait(clock, rng, boundMs) {
|
|
521
|
+
const sleep = clock?.sleep;
|
|
522
|
+
return sleep === void 0 ? Promise.resolve() : sleep.call(clock, Math.floor((rng?.next() ?? 1) * boundMs));
|
|
523
|
+
}
|
|
524
|
+
function retryRead(op, retry) {
|
|
525
|
+
return retry === void 0 ? op() : withRetry(op, retry.policy ?? DEFAULT_RETRY_POLICY, retry);
|
|
526
|
+
}
|
|
527
|
+
function checkedRetryPolicy(policy) {
|
|
528
|
+
const { maxAttempts, baseDelayMs, maxDelayMs, backoffFactor, jitter } = policy;
|
|
529
|
+
requireInteger(maxAttempts, { min: 1, what: "retry.maxAttempts" });
|
|
530
|
+
requireFiniteNumber(baseDelayMs, { min: 0, what: "retry.baseDelayMs" });
|
|
531
|
+
requireFiniteNumber(maxDelayMs, {
|
|
532
|
+
min: baseDelayMs,
|
|
533
|
+
what: "retry.maxDelayMs",
|
|
534
|
+
note: "retry.baseDelayMs"
|
|
535
|
+
});
|
|
536
|
+
requireFiniteNumber(backoffFactor, { min: 1, what: "retry.backoffFactor" });
|
|
537
|
+
if (jitter !== "full" && jitter !== "none") {
|
|
538
|
+
throw new ValidationError(`retry.jitter must be 'full' or 'none'`);
|
|
539
|
+
}
|
|
540
|
+
return policy;
|
|
541
|
+
}
|
|
542
|
+
async function withRetry(op, policy, deps) {
|
|
543
|
+
const retryable = deps.isRetryable ?? isTransientError;
|
|
544
|
+
const attempts = requireInteger(policy.maxAttempts, { min: 1, what: "retry.maxAttempts" });
|
|
545
|
+
let lastErr;
|
|
546
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
547
|
+
try {
|
|
548
|
+
return await op();
|
|
549
|
+
} catch (err) {
|
|
550
|
+
lastErr = err;
|
|
551
|
+
if (attempt >= attempts || !retryable(err)) throw err;
|
|
552
|
+
const delayMs = applyJitter(policy, backoffDelayMs(policy, attempt), deps.rng);
|
|
553
|
+
try {
|
|
554
|
+
deps.onRetry?.({ attempt, delayMs, err });
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
await deps.clock.sleep(delayMs);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
throw lastErr;
|
|
561
|
+
}
|
|
562
|
+
|
|
339
563
|
// src/core/token.ts
|
|
340
564
|
var INCARNATION_TOKEN = /^([0-9a-f]{32})\.(0|[1-9]\d*)\.([0-9a-f]{16})$/;
|
|
341
565
|
function incarnationOf(token) {
|
|
@@ -403,10 +627,7 @@ function leaseChurn(deps) {
|
|
|
403
627
|
if (used >= LEASE_ONLY_RETRIES || !onlyLeasesDiffer(held, now, alsoIgnore)) return void 0;
|
|
404
628
|
const bound = Math.min(CONFLICT_BASE_MS * 2 ** Math.min(used, 4), CONFLICT_CAP_MS);
|
|
405
629
|
used += 1;
|
|
406
|
-
|
|
407
|
-
if (sleep !== void 0) {
|
|
408
|
-
await sleep.call(deps.clock, Math.floor((deps.rng?.next() ?? 1) * bound));
|
|
409
|
-
}
|
|
630
|
+
await jitteredWait(deps.clock, deps.rng, bound);
|
|
410
631
|
return { row: await reread() };
|
|
411
632
|
}
|
|
412
633
|
};
|
|
@@ -417,7 +638,7 @@ function listOf(entries) {
|
|
|
417
638
|
(a, b) => a.generation !== b.generation ? a.generation - b.generation : a.holder < b.holder ? -1 : a.holder > b.holder ? 1 : 0
|
|
418
639
|
);
|
|
419
640
|
}
|
|
420
|
-
var wait = (deps, bound) => deps.clock
|
|
641
|
+
var wait = (deps, bound) => jitteredWait(deps.clock, deps.rng, bound);
|
|
421
642
|
var heldBy = (row, take) => (row?.leases ?? []).some(
|
|
422
643
|
(e) => e.holder === take.holder && e.generation === take.generation && e.until === take.until
|
|
423
644
|
);
|
|
@@ -480,7 +701,9 @@ async function takeLease(ref, deps, take) {
|
|
|
480
701
|
}
|
|
481
702
|
}
|
|
482
703
|
if (unanswered !== void 0) throw unanswered;
|
|
483
|
-
throw new WriteConflictError(
|
|
704
|
+
throw new WriteConflictError(
|
|
705
|
+
`lease: contention taking a lease on segment "${ref.segment}"${CONTENTION_NEXT_STEP}`
|
|
706
|
+
);
|
|
484
707
|
}
|
|
485
708
|
async function releaseLease(ref, deps, holder) {
|
|
486
709
|
let resends = 0;
|
|
@@ -514,7 +737,9 @@ async function releaseLease(ref, deps, holder) {
|
|
|
514
737
|
}
|
|
515
738
|
}
|
|
516
739
|
if (unanswered !== void 0) throw unanswered;
|
|
517
|
-
throw new WriteConflictError(
|
|
740
|
+
throw new WriteConflictError(
|
|
741
|
+
`lease: contention releasing a lease on segment "${ref.segment}"${CONTENTION_NEXT_STEP}`
|
|
742
|
+
);
|
|
518
743
|
}
|
|
519
744
|
|
|
520
745
|
// src/drivers/_shared/keys.ts
|
|
@@ -526,6 +751,44 @@ function namespacePathPart(namespace) {
|
|
|
526
751
|
return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);
|
|
527
752
|
}
|
|
528
753
|
|
|
754
|
+
// src/drivers/_shared/options.ts
|
|
755
|
+
function refuseUnknownOptions(name, options, keys, hint) {
|
|
756
|
+
if (options === null || typeof options !== "object") {
|
|
757
|
+
throw new ValidationError(
|
|
758
|
+
`${name} needs an options object \u2014 got ${options === null ? "null" : typeof options}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const unknown = Object.keys(options).filter((k) => !keys.includes(k));
|
|
762
|
+
if (unknown.length > 0) {
|
|
763
|
+
const list = (ks) => ks.map((k) => `\`${k}\``).join(", ");
|
|
764
|
+
throw new ValidationError(
|
|
765
|
+
`${name} does not take ${list(unknown)}. It takes ${list(keys)}${hint === void 0 ? "" : `; ${hint}`}.`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function checkNow(name, now) {
|
|
770
|
+
if (now !== void 0 && typeof now !== "function") {
|
|
771
|
+
throw new ValidationError(`${name}: \`now\` must be a function returning epoch milliseconds`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
function requirePositiveInt(name, value) {
|
|
775
|
+
return value === void 0 ? void 0 : requirePositiveInteger(value, name);
|
|
776
|
+
}
|
|
777
|
+
function requireBool(name, value) {
|
|
778
|
+
if (value === void 0) return void 0;
|
|
779
|
+
if (typeof value !== "boolean") {
|
|
780
|
+
throw new ValidationError(`${name} must be a boolean; got ${shown2(value)}`);
|
|
781
|
+
}
|
|
782
|
+
return value;
|
|
783
|
+
}
|
|
784
|
+
function shown2(value) {
|
|
785
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
786
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
787
|
+
return String(value);
|
|
788
|
+
}
|
|
789
|
+
return value === null ? "null" : typeof value;
|
|
790
|
+
}
|
|
791
|
+
|
|
529
792
|
// src/core/crbm/frame.ts
|
|
530
793
|
var PREAMBLE_BYTES = 8;
|
|
531
794
|
var FOOTER_BYTES = 104;
|
|
@@ -533,7 +796,9 @@ var FOOTER_BYTES = 104;
|
|
|
533
796
|
// src/core/metadata.ts
|
|
534
797
|
var MAX_METADATA_BYTES = 1024;
|
|
535
798
|
var MAX_METADATA_KEY_BYTES = 128;
|
|
536
|
-
var
|
|
799
|
+
var UTF83 = new TextEncoder();
|
|
800
|
+
var UTF8_STRICT = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
801
|
+
var utf8Length = (s) => UTF83.encode(s).length;
|
|
537
802
|
function canonicalMetadataJson(value, fail) {
|
|
538
803
|
if (value === null || typeof value !== "object") {
|
|
539
804
|
fail(`metadata must be a plain object (got ${value === null ? "null" : typeof value})`);
|
|
@@ -580,7 +845,7 @@ function canonicalMetadataJson(value, fail) {
|
|
|
580
845
|
function metadataBytes(value, fail) {
|
|
581
846
|
if (value === void 0) return void 0;
|
|
582
847
|
const json = canonicalMetadataJson(value, fail);
|
|
583
|
-
return json === "{}" ? void 0 :
|
|
848
|
+
return json === "{}" ? void 0 : UTF83.encode(json);
|
|
584
849
|
}
|
|
585
850
|
function copiedMetadata(value, fail) {
|
|
586
851
|
const bytes = metadataBytes(value, fail);
|
|
@@ -592,7 +857,7 @@ function metadataFromBytes(bytes, fail) {
|
|
|
592
857
|
}
|
|
593
858
|
let text;
|
|
594
859
|
try {
|
|
595
|
-
text =
|
|
860
|
+
text = UTF8_STRICT.decode(bytes);
|
|
596
861
|
} catch {
|
|
597
862
|
fail("metadata is not valid UTF-8");
|
|
598
863
|
}
|
|
@@ -621,6 +886,13 @@ function fingerprintParts(fingerprint) {
|
|
|
621
886
|
if (footerCrc > 4294967295) return void 0;
|
|
622
887
|
return { size, footerCrc };
|
|
623
888
|
}
|
|
889
|
+
function footerCrcOf(fingerprint) {
|
|
890
|
+
const parts = fingerprintParts(fingerprint);
|
|
891
|
+
if (parts === void 0) {
|
|
892
|
+
throw new IntegrityError(`not an object's fingerprint: ${String(fingerprint).slice(0, 64)}`);
|
|
893
|
+
}
|
|
894
|
+
return parts.footerCrc;
|
|
895
|
+
}
|
|
624
896
|
function compactFingerprint(fingerprint) {
|
|
625
897
|
const colon = fingerprint.indexOf(":");
|
|
626
898
|
const size = Number(fingerprint.slice(0, colon));
|
|
@@ -628,25 +900,352 @@ function compactFingerprint(fingerprint) {
|
|
|
628
900
|
return `${size.toString(36)}.${crc.toString(36)}`;
|
|
629
901
|
}
|
|
630
902
|
|
|
631
|
-
// src/core/
|
|
903
|
+
// src/core/summary.ts
|
|
904
|
+
var NONCE_BYTES = 12;
|
|
905
|
+
var TAG_BYTES = 16;
|
|
906
|
+
var COUNT_BYTES = 8;
|
|
907
|
+
var SIZE_BYTES = 8;
|
|
908
|
+
var CRC_BYTES = 4;
|
|
909
|
+
var TAIL_BYTES = 4;
|
|
910
|
+
var FIXED_BYTES = COUNT_BYTES + SIZE_BYTES + CRC_BYTES + TAIL_BYTES;
|
|
911
|
+
var MAX_SUMMARY_CARDINALITY = U32_MAX + 1;
|
|
912
|
+
var MAX_TAIL_BYTES = 8392820;
|
|
913
|
+
function tailBytesFit(tailBytes, size) {
|
|
914
|
+
return typeof tailBytes === "number" && Number.isInteger(tailBytes) && tailBytes >= FOOTER_BYTES && tailBytes <= size - PREAMBLE_BYTES && tailBytes <= MAX_TAIL_BYTES;
|
|
915
|
+
}
|
|
916
|
+
function sizeOfFingerprint(fingerprint) {
|
|
917
|
+
const parts = fingerprintParts(fingerprint);
|
|
918
|
+
if (parts === void 0)
|
|
919
|
+
throw new IntegrityError("registry record summary: a malformed fingerprint");
|
|
920
|
+
return parts.size;
|
|
921
|
+
}
|
|
922
|
+
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
923
|
+
function toBase64(bytes) {
|
|
924
|
+
let out = "";
|
|
925
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
926
|
+
const a = bytes[i];
|
|
927
|
+
const b = bytes[i + 1];
|
|
928
|
+
const c = bytes[i + 2];
|
|
929
|
+
out += BASE64[a >> 2] + BASE64[(a & 3) << 4 | (b ?? 0) >> 4];
|
|
930
|
+
out += b === void 0 ? "=" : BASE64[(b & 15) << 2 | (c ?? 0) >> 6];
|
|
931
|
+
out += c === void 0 ? "=" : BASE64[c & 63];
|
|
932
|
+
}
|
|
933
|
+
return out;
|
|
934
|
+
}
|
|
935
|
+
function fromBase64(text) {
|
|
936
|
+
if (text.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(text)) return void 0;
|
|
937
|
+
const pad = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
|
|
938
|
+
const out = new Uint8Array(text.length / 4 * 3 - pad);
|
|
939
|
+
let o = 0;
|
|
940
|
+
for (let i = 0; i < text.length; i += 4) {
|
|
941
|
+
const n = BASE64.indexOf(text[i]) << 18 | BASE64.indexOf(text[i + 1]) << 12 | Math.max(BASE64.indexOf(text[i + 2]), 0) << 6 | Math.max(BASE64.indexOf(text[i + 3]), 0);
|
|
942
|
+
if (o < out.length) out[o++] = n >> 16 & 255;
|
|
943
|
+
if (o < out.length) out[o++] = n >> 8 & 255;
|
|
944
|
+
if (o < out.length) out[o++] = n & 255;
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
function clearSummary(generation, cardinality, fingerprint, tailBytes, metadata) {
|
|
949
|
+
return metadata === void 0 || Object.keys(metadata).length === 0 ? { generation, cardinality, fingerprint, tailBytes } : { generation, cardinality, fingerprint, tailBytes, metadata };
|
|
950
|
+
}
|
|
951
|
+
function sealSummary(aead, ref, generation, cardinality, fingerprint, tailBytes, metadata) {
|
|
952
|
+
const meta = metadataBytes(metadata, (message) => {
|
|
953
|
+
throw new IntegrityError(`summary metadata: ${message}`);
|
|
954
|
+
});
|
|
955
|
+
const parts = fingerprintParts(fingerprint);
|
|
956
|
+
if (parts === void 0) throw new IntegrityError("summary: a malformed fingerprint");
|
|
957
|
+
const plain = new Uint8Array(FIXED_BYTES + (meta?.length ?? 0));
|
|
958
|
+
const view = new DataView(plain.buffer);
|
|
959
|
+
view.setBigUint64(0, BigInt(cardinality), true);
|
|
960
|
+
view.setBigUint64(COUNT_BYTES, BigInt(parts.size), true);
|
|
961
|
+
view.setUint32(COUNT_BYTES + SIZE_BYTES, parts.footerCrc, true);
|
|
962
|
+
view.setUint32(COUNT_BYTES + SIZE_BYTES + CRC_BYTES, tailBytes, true);
|
|
963
|
+
if (meta !== void 0) plain.set(meta, FIXED_BYTES);
|
|
964
|
+
const { nonce, ciphertext, tag } = aead.seal(plain, aadFor(ref, generation, "summary"));
|
|
965
|
+
const framed = new Uint8Array(nonce.length + ciphertext.length + tag.length);
|
|
966
|
+
framed.set(nonce, 0);
|
|
967
|
+
framed.set(ciphertext, nonce.length);
|
|
968
|
+
framed.set(tag, nonce.length + ciphertext.length);
|
|
969
|
+
return { generation, sealed: toBase64(framed) };
|
|
970
|
+
}
|
|
971
|
+
function openSummary(aead, ref, summary) {
|
|
972
|
+
const fail = (message) => {
|
|
973
|
+
throw new IntegrityError(`registry record summary: ${message}`);
|
|
974
|
+
};
|
|
975
|
+
const framed = fromBase64(summary.sealed);
|
|
976
|
+
const framing = NONCE_BYTES + TAG_BYTES;
|
|
977
|
+
if (framed === void 0 || framed.length < framing + FIXED_BYTES || framed.length > framing + FIXED_BYTES + MAX_METADATA_BYTES) {
|
|
978
|
+
fail(
|
|
979
|
+
"the sealed summary is not a nonce, a count, a fingerprint, a tail size, up to 1 KiB of metadata and a tag"
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
const bytes = framed;
|
|
983
|
+
let plain;
|
|
984
|
+
try {
|
|
985
|
+
plain = aead.open(
|
|
986
|
+
{
|
|
987
|
+
nonce: bytes.subarray(0, NONCE_BYTES),
|
|
988
|
+
ciphertext: bytes.subarray(NONCE_BYTES, bytes.length - TAG_BYTES),
|
|
989
|
+
tag: bytes.subarray(bytes.length - TAG_BYTES)
|
|
990
|
+
},
|
|
991
|
+
aadFor(ref, summary.generation, "summary")
|
|
992
|
+
);
|
|
993
|
+
} catch (err) {
|
|
994
|
+
if (!isIntegrityError(err)) throw err;
|
|
995
|
+
return fail(
|
|
996
|
+
"the sealed summary does not open: tampered bytes, or a summary moved from another generation"
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (plain.length < FIXED_BYTES)
|
|
1000
|
+
fail("the sealed summary is shorter than its count, fingerprint and tail size");
|
|
1001
|
+
const view = new DataView(plain.buffer, plain.byteOffset, plain.byteLength);
|
|
1002
|
+
const count = view.getBigUint64(0, true);
|
|
1003
|
+
if (count > BigInt(MAX_SUMMARY_CARDINALITY)) fail(`cardinality ${count} is outside 0 to 2^32`);
|
|
1004
|
+
const size = view.getBigUint64(COUNT_BYTES, true);
|
|
1005
|
+
const fingerprint = fingerprintFor(Number(size), view.getUint32(COUNT_BYTES + SIZE_BYTES, true));
|
|
1006
|
+
if (size > BigInt(Number.MAX_SAFE_INTEGER) || fingerprintParts(fingerprint) === void 0) {
|
|
1007
|
+
fail(`the object size ${size} is not one a .crbm object can have`);
|
|
1008
|
+
}
|
|
1009
|
+
const tailBytes = view.getUint32(COUNT_BYTES + SIZE_BYTES + CRC_BYTES, true);
|
|
1010
|
+
if (!tailBytesFit(tailBytes, Number(size))) {
|
|
1011
|
+
fail(`the tail size ${tailBytes} is not one a .crbm object of ${size} bytes can have`);
|
|
1012
|
+
}
|
|
1013
|
+
const metadata = plain.length === FIXED_BYTES ? void 0 : metadataFromBytes(plain.subarray(FIXED_BYTES), fail);
|
|
1014
|
+
return { cardinality: Number(count), metadata, fingerprint, tailBytes };
|
|
1015
|
+
}
|
|
1016
|
+
function usableSummary(ref, row, aead) {
|
|
1017
|
+
const summary = row.summary;
|
|
1018
|
+
if (summary === void 0 || row.status !== "active" || summary.generation !== row.currentGen) {
|
|
1019
|
+
return void 0;
|
|
1020
|
+
}
|
|
1021
|
+
const keyed = row.wrappedDeks !== void 0 && row.wrappedDeks.length > 0;
|
|
1022
|
+
if ("sealed" in summary) {
|
|
1023
|
+
if (!keyed || aead === void 0) return void 0;
|
|
1024
|
+
try {
|
|
1025
|
+
return openSummary(aead, ref, summary);
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
if (isIntegrityError(err)) return void 0;
|
|
1028
|
+
throw err;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (keyed) return void 0;
|
|
1032
|
+
const parts = fingerprintParts(summary.fingerprint);
|
|
1033
|
+
if (parts === void 0 || !tailBytesFit(summary.tailBytes, parts.size)) return void 0;
|
|
1034
|
+
return {
|
|
1035
|
+
cardinality: summary.cardinality,
|
|
1036
|
+
metadata: summary.metadata,
|
|
1037
|
+
fingerprint: summary.fingerprint,
|
|
1038
|
+
tailBytes: summary.tailBytes
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function canonicalOrNone(metadata) {
|
|
1042
|
+
if (metadata === void 0) return void 0;
|
|
1043
|
+
const json = canonicalMetadataJson(metadata, (message) => {
|
|
1044
|
+
throw new IntegrityError(`metadata: ${message}`);
|
|
1045
|
+
});
|
|
1046
|
+
return json === "{}" ? void 0 : json;
|
|
1047
|
+
}
|
|
1048
|
+
function summaryAgrees(summary, object) {
|
|
1049
|
+
return summary.cardinality === object.cardinality && canonicalOrNone(summary.metadata) === canonicalOrNone(object.metadata) && (summary.tailBytes === void 0 || object.tailBytes === void 0 || summary.tailBytes === object.tailBytes);
|
|
1050
|
+
}
|
|
1051
|
+
function metadataToCarry(object, authenticated) {
|
|
1052
|
+
const empty = (metadata) => metadata === void 0 || Object.keys(metadata).length === 0;
|
|
1053
|
+
return empty(object) && authenticated !== void 0 && !empty(authenticated.metadata) ? authenticated.metadata : object;
|
|
1054
|
+
}
|
|
1055
|
+
function summaryOf(ref, generation, description, aead) {
|
|
1056
|
+
const { cardinality, fingerprint, tailBytes, metadata } = description;
|
|
1057
|
+
if (tailBytes > MAX_TAIL_BYTES) return void 0;
|
|
1058
|
+
return aead === void 0 ? clearSummary(generation, cardinality, fingerprint, tailBytes, metadata) : sealSummary(aead, ref, generation, cardinality, fingerprint, tailBytes, metadata);
|
|
1059
|
+
}
|
|
1060
|
+
function ownSummary(ref, generation, description, aead) {
|
|
1061
|
+
const summary = summaryOf(ref, generation, description, aead);
|
|
1062
|
+
if (summary === void 0) {
|
|
1063
|
+
throw new IntegrityError(
|
|
1064
|
+
`the object written for generation ${generation} of "${ref.segment}" has a tail of ${description.tailBytes} bytes, above the ${MAX_TAIL_BYTES} a summary records`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
return summary;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/core/known-generations-shape.ts
|
|
1071
|
+
var ENTRY_KEYS = ["generation", "footerCrc"];
|
|
1072
|
+
function knownGensShapeFault(value, max, show = String) {
|
|
1073
|
+
if (value === void 0) return void 0;
|
|
1074
|
+
if (!Array.isArray(value)) return "must be an array";
|
|
1075
|
+
const list = value;
|
|
1076
|
+
if (list.length > max) return `has ${list.length} entries, cap ${max}`;
|
|
1077
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1078
|
+
for (const e of list) {
|
|
1079
|
+
if (typeof e !== "object" || e === null || Array.isArray(e))
|
|
1080
|
+
return "an entry must be an object";
|
|
1081
|
+
const entry = e;
|
|
1082
|
+
const extra = Object.keys(entry).filter((k) => !ENTRY_KEYS.includes(k));
|
|
1083
|
+
if (extra.length > 0) {
|
|
1084
|
+
return `an entry carries an undeclared field (${show(extra.join(", "))})`;
|
|
1085
|
+
}
|
|
1086
|
+
const { generation, footerCrc } = entry;
|
|
1087
|
+
if (!Number.isSafeInteger(generation) || generation < 0) {
|
|
1088
|
+
return `a generation must be a non-negative safe integer (got ${show(generation)})`;
|
|
1089
|
+
}
|
|
1090
|
+
if (!Number.isInteger(footerCrc) || footerCrc < 0 || footerCrc > 4294967295) {
|
|
1091
|
+
return `a footerCrc must be an integer from 0 to 2^32 - 1 (got ${show(footerCrc)})`;
|
|
1092
|
+
}
|
|
1093
|
+
if (seen.has(generation)) return `generation ${String(generation)} is repeated`;
|
|
1094
|
+
seen.add(generation);
|
|
1095
|
+
}
|
|
1096
|
+
return void 0;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/core/order.ts
|
|
1100
|
+
var ascending = (a, b) => a - b;
|
|
1101
|
+
|
|
1102
|
+
// src/core/known-generations.ts
|
|
632
1103
|
var MAX_KEPT_GENERATIONS = 64;
|
|
633
|
-
var
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1104
|
+
var KNOWN_FILL = 128;
|
|
1105
|
+
var MAX_KNOWN_GENERATIONS = 192;
|
|
1106
|
+
var MAX_STORED_KNOWN_GENERATIONS = 384;
|
|
1107
|
+
function usableKnownGens(record) {
|
|
1108
|
+
const known = record?.knownGens;
|
|
1109
|
+
if (record === null || known === void 0 || known.length === 0 || record.status !== "active" || record.currentGen === null) {
|
|
1110
|
+
return void 0;
|
|
1111
|
+
}
|
|
1112
|
+
const last = known[known.length - 1];
|
|
1113
|
+
if (last.generation !== record.currentGen) return void 0;
|
|
1114
|
+
const summary = record.summary;
|
|
1115
|
+
if (summary !== void 0 && summary.generation === record.currentGen && "fingerprint" in summary && fingerprintParts(summary.fingerprint)?.footerCrc !== last.footerCrc) {
|
|
1116
|
+
return void 0;
|
|
1117
|
+
}
|
|
1118
|
+
return known;
|
|
1119
|
+
}
|
|
1120
|
+
function windowOf(record) {
|
|
1121
|
+
const known = usableKnownGens(record);
|
|
1122
|
+
if (known === void 0) return void 0;
|
|
1123
|
+
const pointer = record.currentGen;
|
|
1124
|
+
return known.filter((e) => e.generation < pointer).map((e) => e.generation);
|
|
1125
|
+
}
|
|
1126
|
+
function carriesWindow(record, leased) {
|
|
1127
|
+
const below = windowOf(record);
|
|
1128
|
+
return below !== void 0 && below.filter((g) => !leased.has(g)).length <= MAX_KEPT_GENERATIONS;
|
|
1129
|
+
}
|
|
1130
|
+
var KNOWN_SPAN = 127;
|
|
1131
|
+
function keptWindow(below, superseded, keep) {
|
|
1132
|
+
const oldestFirst = [...below].sort(ascending);
|
|
1133
|
+
const first = keep > 0 && superseded !== void 0 && oldestFirst.includes(superseded) ? superseded : void 0;
|
|
1134
|
+
const held = new Set(first === void 0 ? [] : [first]);
|
|
1135
|
+
for (let i = oldestFirst.length - 1, room = keep - held.size; i >= 0 && room > 0; i--) {
|
|
1136
|
+
if (oldestFirst[i] === first) continue;
|
|
1137
|
+
held.add(oldestFirst[i]);
|
|
1138
|
+
room -= 1;
|
|
1139
|
+
}
|
|
1140
|
+
return {
|
|
1141
|
+
kept: oldestFirst.filter((g) => held.has(g)),
|
|
1142
|
+
evict: oldestFirst.filter((g) => !held.has(g))
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
function capKnown(known, protect, leased = /* @__PURE__ */ new Set()) {
|
|
1146
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1147
|
+
for (const e of known) if (protect.has(e.generation)) keep.add(e.generation);
|
|
1148
|
+
for (let i = known.length - 1; i >= 0 && keep.size < MAX_KNOWN_GENERATIONS; i--) {
|
|
1149
|
+
if (leased.has(known[i].generation)) keep.add(known[i].generation);
|
|
1150
|
+
}
|
|
1151
|
+
for (let i = known.length - 1; i >= 0 && keep.size < KNOWN_FILL; i--)
|
|
1152
|
+
keep.add(known[i].generation);
|
|
1153
|
+
return known.filter((e) => keep.has(e.generation));
|
|
1154
|
+
}
|
|
1155
|
+
function capInsert(known, protect, leased) {
|
|
1156
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1157
|
+
let unleased = 0;
|
|
1158
|
+
for (const e of known) {
|
|
1159
|
+
if (!protect.has(e.generation)) continue;
|
|
1160
|
+
keep.add(e.generation);
|
|
1161
|
+
if (!leased.has(e.generation)) unleased += 1;
|
|
1162
|
+
}
|
|
1163
|
+
for (let i = known.length - 1; i >= 0 && unleased < KNOWN_FILL; i--) {
|
|
1164
|
+
const g = known[i].generation;
|
|
1165
|
+
if (keep.has(g) || leased.has(g)) continue;
|
|
1166
|
+
keep.add(g);
|
|
1167
|
+
unleased += 1;
|
|
1168
|
+
}
|
|
1169
|
+
for (let i = known.length - 1; i >= 0 && keep.size < MAX_KNOWN_GENERATIONS; i--) {
|
|
1170
|
+
if (leased.has(known[i].generation)) keep.add(known[i].generation);
|
|
1171
|
+
}
|
|
1172
|
+
return known.filter((e) => keep.has(e.generation));
|
|
1173
|
+
}
|
|
1174
|
+
function makeCurrent(known, entry) {
|
|
1175
|
+
return [...known.filter((e) => e.generation !== entry.generation), entry];
|
|
1176
|
+
}
|
|
1177
|
+
function nextKnown(prev, generation, footerCrc, keep, options = {}) {
|
|
1178
|
+
const self = { generation, footerCrc };
|
|
1179
|
+
const clears = options.clearsLeases === true;
|
|
1180
|
+
const byName = keep !== void 0 && keep <= MAX_KEPT_GENERATIONS;
|
|
1181
|
+
if (options.seed !== void 0 && !clears) {
|
|
1182
|
+
const seed = options.seed.filter((e) => e.generation !== generation);
|
|
1183
|
+
const protect = /* @__PURE__ */ new Set([...seed.map((e) => e.generation), generation]);
|
|
1184
|
+
return {
|
|
1185
|
+
known: capKnown([...seed, self], protect, options.leased ?? /* @__PURE__ */ new Set()),
|
|
1186
|
+
list: void 0,
|
|
1187
|
+
evict: []
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
const found = usableKnownGens(prev);
|
|
1191
|
+
if (found === void 0 && prev !== null && prev.currentGen !== null) {
|
|
1192
|
+
return { known: [self], list: void 0, evict: [] };
|
|
1193
|
+
}
|
|
1194
|
+
const all = found ?? [];
|
|
1195
|
+
const below = all.filter((e) => e.generation < generation);
|
|
1196
|
+
const above = clears ? [] : all.filter((e) => e.generation > generation);
|
|
1197
|
+
const span = clears ? 0 : Math.min(keep ?? Number.MAX_SAFE_INTEGER, KNOWN_SPAN);
|
|
1198
|
+
const vouch = new Set((span === 0 ? [] : below.slice(-span)).map((e) => e.generation));
|
|
1199
|
+
const leased = clears ? /* @__PURE__ */ new Set() : options.leased ?? /* @__PURE__ */ new Set();
|
|
1200
|
+
const held = below.filter((e) => !vouch.has(e.generation) && leased.has(e.generation));
|
|
1201
|
+
const chosen = /* @__PURE__ */ new Set([
|
|
1202
|
+
...vouch,
|
|
1203
|
+
...held.map((e) => e.generation),
|
|
1204
|
+
...above.map((e) => e.generation)
|
|
1205
|
+
]);
|
|
1206
|
+
const known = capKnown(
|
|
1207
|
+
[...all.filter((e) => chosen.has(e.generation)), self],
|
|
1208
|
+
/* @__PURE__ */ new Set([...vouch, generation]),
|
|
1209
|
+
leased
|
|
1210
|
+
);
|
|
1211
|
+
const wide = !clears && found !== void 0 && !carriesWindow(prev, leased);
|
|
1212
|
+
if (!byName || wide) return { known, list: void 0, evict: [] };
|
|
1213
|
+
const named = new Set(known.map((e) => e.generation));
|
|
1214
|
+
return {
|
|
1215
|
+
known,
|
|
1216
|
+
list: known.filter((e) => e.generation < generation).map((e) => e.generation).sort(ascending),
|
|
1217
|
+
evict: below.filter((e) => !named.has(e.generation)).map((e) => e.generation).sort(ascending)
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function rollbackKnown(prev, target) {
|
|
1221
|
+
const found = usableKnownGens(prev);
|
|
1222
|
+
if (found === void 0) return void 0;
|
|
1223
|
+
const moved = makeCurrent(found, target);
|
|
1224
|
+
const inserted = !found.some((e) => e.generation === target.generation);
|
|
1225
|
+
if (!inserted && moved.length <= MAX_KNOWN_GENERATIONS) return moved;
|
|
1226
|
+
return capInsert(moved, /* @__PURE__ */ new Set([target.generation, prev.currentGen]), leasedByRow(prev));
|
|
1227
|
+
}
|
|
1228
|
+
function pruneKnown(record, doomed) {
|
|
1229
|
+
const found = usableKnownGens(record);
|
|
1230
|
+
if (found === void 0) return void 0;
|
|
1231
|
+
const rest = found.filter((e) => e.generation === record.currentGen || !doomed.has(e.generation));
|
|
1232
|
+
if (rest.length <= MAX_KNOWN_GENERATIONS) return rest;
|
|
1233
|
+
return capKnown(rest, /* @__PURE__ */ new Set([record.currentGen]), leasedByRow(record));
|
|
1234
|
+
}
|
|
1235
|
+
function describingKnown(record, movedTo) {
|
|
1236
|
+
const found = usableKnownGens(record);
|
|
1237
|
+
if (found === void 0 || found.length <= MAX_KNOWN_GENERATIONS) return found;
|
|
1238
|
+
return capKnown(found, /* @__PURE__ */ new Set([record.currentGen, movedTo]), leasedByRow(record));
|
|
1239
|
+
}
|
|
1240
|
+
function isFullLedger(known) {
|
|
1241
|
+
return known.length >= KNOWN_FILL;
|
|
1242
|
+
}
|
|
1243
|
+
function fallbackBound(record, known) {
|
|
1244
|
+
const leased = leasedByRow(record);
|
|
1245
|
+
return (known.find((e) => !leased.has(e.generation)) ?? known[0]).generation;
|
|
1246
|
+
}
|
|
1247
|
+
function leasedByRow(record) {
|
|
1248
|
+
return new Set((record.leases ?? []).map((e) => e.generation));
|
|
650
1249
|
}
|
|
651
1250
|
|
|
652
1251
|
// src/drivers/_shared/registry.ts
|
|
@@ -664,7 +1263,7 @@ var RECORD_FIELDS = [
|
|
|
664
1263
|
"createdAt",
|
|
665
1264
|
"updatedAt",
|
|
666
1265
|
"token",
|
|
667
|
-
"
|
|
1266
|
+
"knownGens",
|
|
668
1267
|
"leases",
|
|
669
1268
|
"pointerId"
|
|
670
1269
|
];
|
|
@@ -673,10 +1272,9 @@ var ENVELOPE_FIELDS = ["schemaVersion", "deleted", "record"];
|
|
|
673
1272
|
var MAX_GOVERNANCE_BYTES = 64 * 1024;
|
|
674
1273
|
var MAX_WRAPPED_DEKS = 8;
|
|
675
1274
|
var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
|
|
676
|
-
var
|
|
677
|
-
var SEALED_SUMMARY_MIN_BYTES = 12 + 8 + 8 + 4 + 16;
|
|
1275
|
+
var SEALED_SUMMARY_MIN_BYTES = 12 + 8 + 8 + 4 + 4 + 16;
|
|
678
1276
|
var SEALED_SUMMARY_MAX_BYTES = SEALED_SUMMARY_MIN_BYTES + MAX_METADATA_BYTES;
|
|
679
|
-
var
|
|
1277
|
+
var BASE642 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
680
1278
|
function validateGeneration(gen) {
|
|
681
1279
|
if (gen === null) return;
|
|
682
1280
|
if (!Number.isSafeInteger(gen) || gen < 0) {
|
|
@@ -716,10 +1314,10 @@ function validateGovernance(meta, field) {
|
|
|
716
1314
|
throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);
|
|
717
1315
|
}
|
|
718
1316
|
}
|
|
719
|
-
function validateWrappedDeks(value,
|
|
1317
|
+
function validateWrappedDeks(value, origin) {
|
|
720
1318
|
if (value === void 0) return;
|
|
721
1319
|
const fail = (msg) => {
|
|
722
|
-
throw
|
|
1320
|
+
throw origin === "stored" ? new IntegrityError(msg) : new ValidationError(msg);
|
|
723
1321
|
};
|
|
724
1322
|
if (!Array.isArray(value)) fail("wrappedDeks must be an array");
|
|
725
1323
|
const list = value;
|
|
@@ -744,11 +1342,11 @@ function base64PadBitsAreZero(text) {
|
|
|
744
1342
|
const last = BASE64_ALPHABET.indexOf(text[text.length - pad - 1]);
|
|
745
1343
|
return pad === 2 ? (last & 15) === 0 : (last & 3) === 0;
|
|
746
1344
|
}
|
|
747
|
-
function validateSummary(value,
|
|
1345
|
+
function validateSummary(value, origin, ctx) {
|
|
748
1346
|
if (value === void 0) return void 0;
|
|
749
1347
|
const fail = (msg) => {
|
|
750
1348
|
const where = ctx === void 0 ? "" : `: ${ctx}`;
|
|
751
|
-
throw
|
|
1349
|
+
throw origin === "stored" ? new IntegrityError(`registry record summary: ${msg}${where}`) : new ValidationError(`summary: ${msg}${where}`);
|
|
752
1350
|
};
|
|
753
1351
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
754
1352
|
fail(
|
|
@@ -758,7 +1356,7 @@ function validateSummary(value, isStored, ctx) {
|
|
|
758
1356
|
const s = value;
|
|
759
1357
|
const keys = Object.keys(s);
|
|
760
1358
|
const isSealed = keys.includes("sealed");
|
|
761
|
-
const allowed = isSealed ? ["generation", "sealed"] : ["generation", "cardinality", "fingerprint", "metadata"];
|
|
1359
|
+
const allowed = isSealed ? ["generation", "sealed"] : ["generation", "cardinality", "fingerprint", "tailBytes", "metadata"];
|
|
762
1360
|
const extra = keys.filter((k) => !allowed.includes(k));
|
|
763
1361
|
if (extra.length > 0) {
|
|
764
1362
|
fail(
|
|
@@ -771,14 +1369,14 @@ function validateSummary(value, isStored, ctx) {
|
|
|
771
1369
|
}
|
|
772
1370
|
if (isSealed) {
|
|
773
1371
|
const sealed = s.sealed;
|
|
774
|
-
if (typeof sealed !== "string" || !
|
|
1372
|
+
if (typeof sealed !== "string" || !BASE642.test(sealed) || !base64PadBitsAreZero(sealed)) {
|
|
775
1373
|
fail("sealed must be canonical, padded standard base64");
|
|
776
1374
|
}
|
|
777
1375
|
const text = sealed;
|
|
778
1376
|
const bytes = text.length / 4 * 3 - (text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0);
|
|
779
1377
|
if (bytes < SEALED_SUMMARY_MIN_BYTES || bytes > SEALED_SUMMARY_MAX_BYTES) {
|
|
780
1378
|
fail(
|
|
781
|
-
`sealed is ${bytes}B, outside ${SEALED_SUMMARY_MIN_BYTES}..${SEALED_SUMMARY_MAX_BYTES}B (a nonce, a fixed-width count and
|
|
1379
|
+
`sealed is ${bytes}B, outside ${SEALED_SUMMARY_MIN_BYTES}..${SEALED_SUMMARY_MAX_BYTES}B (a nonce, a fixed-width count, fingerprint and tail size, a tag, and metadata up to its cap)`
|
|
782
1380
|
);
|
|
783
1381
|
}
|
|
784
1382
|
return Object.freeze({ generation, sealed: text });
|
|
@@ -788,26 +1386,35 @@ function validateSummary(value, isStored, ctx) {
|
|
|
788
1386
|
fail(`cardinality must be an integer from 0 to 2^32 (got ${String(n)})`);
|
|
789
1387
|
}
|
|
790
1388
|
const fingerprint = s.fingerprint;
|
|
791
|
-
|
|
1389
|
+
const parts = fingerprintParts(fingerprint);
|
|
1390
|
+
if (parts === void 0) {
|
|
1391
|
+
fail(
|
|
1392
|
+
`fingerprint must be an object's, <size>:<footer checksum> in decimal (got ${shown3(JSON.stringify(fingerprint))})`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
const tailBytes = s.tailBytes;
|
|
1396
|
+
if (!tailBytesFit(tailBytes, parts.size)) {
|
|
792
1397
|
fail(
|
|
793
|
-
`
|
|
1398
|
+
`tailBytes must be an integer from the footer's size to the object's size less its preamble, and at most the cap of one open's tail read (got ${shown3(JSON.stringify(tailBytes))} for an object of ${parts.size} bytes)`
|
|
794
1399
|
);
|
|
795
1400
|
}
|
|
796
1401
|
if (s.metadata === void 0) {
|
|
797
1402
|
return Object.freeze({
|
|
798
1403
|
generation,
|
|
799
1404
|
cardinality: n,
|
|
800
|
-
fingerprint
|
|
1405
|
+
fingerprint,
|
|
1406
|
+
tailBytes
|
|
801
1407
|
});
|
|
802
1408
|
}
|
|
803
1409
|
const canonical = canonicalMetadataJson(s.metadata, fail);
|
|
804
1410
|
if (canonical === "{}") fail("metadata is empty; a generation without metadata carries none");
|
|
805
|
-
if (
|
|
1411
|
+
if (origin === "stored") return s;
|
|
806
1412
|
const metadata = Object.freeze(JSON.parse(canonical));
|
|
807
1413
|
return Object.freeze({
|
|
808
1414
|
generation,
|
|
809
1415
|
cardinality: n,
|
|
810
1416
|
fingerprint,
|
|
1417
|
+
tailBytes,
|
|
811
1418
|
metadata
|
|
812
1419
|
});
|
|
813
1420
|
}
|
|
@@ -828,33 +1435,30 @@ function validateSummaryKeys(summary, wrappedDeks) {
|
|
|
828
1435
|
);
|
|
829
1436
|
}
|
|
830
1437
|
}
|
|
831
|
-
function
|
|
1438
|
+
function validateKnownGens(value, origin, ctx) {
|
|
832
1439
|
if (value === void 0) return void 0;
|
|
833
1440
|
const fail = (msg) => {
|
|
834
1441
|
const where = ctx === void 0 ? "" : `: ${ctx}`;
|
|
835
|
-
throw
|
|
1442
|
+
throw origin === "stored" ? new IntegrityError(`registry record knownGens: ${msg}${where}`) : new ValidationError(`knownGens: ${msg}`);
|
|
836
1443
|
};
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
846
|
-
if (g <= last) fail("entries must be strictly ascending");
|
|
847
|
-
last = g;
|
|
1444
|
+
const max = origin === "stored" ? MAX_STORED_KNOWN_GENERATIONS : MAX_KNOWN_GENERATIONS;
|
|
1445
|
+
const fault = knownGensShapeFault(value, max, shown3);
|
|
1446
|
+
if (fault !== void 0) fail(fault);
|
|
1447
|
+
const out = [];
|
|
1448
|
+
for (const entry of value) {
|
|
1449
|
+
out.push(
|
|
1450
|
+
origin === "stored" ? entry : Object.freeze({ generation: entry.generation, footerCrc: entry.footerCrc })
|
|
1451
|
+
);
|
|
848
1452
|
}
|
|
849
|
-
return
|
|
1453
|
+
return origin === "stored" ? out : Object.freeze(out);
|
|
850
1454
|
}
|
|
851
1455
|
var HOLDER = /^[0-9a-f]{16}$/;
|
|
852
1456
|
var LEASE_ENTRY_KEYS = ["holder", "generation", "until"];
|
|
853
|
-
function validateLeases(value,
|
|
1457
|
+
function validateLeases(value, origin, ctx) {
|
|
854
1458
|
if (value === void 0) return void 0;
|
|
855
1459
|
const fail = (msg) => {
|
|
856
1460
|
const where = ctx === void 0 ? "" : `: ${ctx}`;
|
|
857
|
-
throw
|
|
1461
|
+
throw origin === "stored" ? new IntegrityError(`registry record leases: ${msg}${where}`) : new ValidationError(`leases: ${msg}`);
|
|
858
1462
|
};
|
|
859
1463
|
if (!Array.isArray(value)) fail("must be an array");
|
|
860
1464
|
const list = value;
|
|
@@ -882,14 +1486,14 @@ function validateLeases(value, isStored, ctx) {
|
|
|
882
1486
|
}
|
|
883
1487
|
}
|
|
884
1488
|
out.push(
|
|
885
|
-
|
|
1489
|
+
origin === "stored" ? entry : Object.freeze({
|
|
886
1490
|
holder,
|
|
887
1491
|
generation,
|
|
888
1492
|
until
|
|
889
1493
|
})
|
|
890
1494
|
);
|
|
891
1495
|
}
|
|
892
|
-
if (
|
|
1496
|
+
if (origin === "stored") return out;
|
|
893
1497
|
return out.length === 0 ? void 0 : Object.freeze(out);
|
|
894
1498
|
}
|
|
895
1499
|
function validateLeasesPointer(leases, currentGen) {
|
|
@@ -897,17 +1501,20 @@ function validateLeasesPointer(leases, currentGen) {
|
|
|
897
1501
|
throw new ValidationError("leases: a row with no pointer has no generation to lease");
|
|
898
1502
|
}
|
|
899
1503
|
}
|
|
900
|
-
function
|
|
901
|
-
if (
|
|
1504
|
+
function validateKnownGensFit(knownGens, currentGen, summary) {
|
|
1505
|
+
if (knownGens === void 0) return;
|
|
902
1506
|
if (currentGen === null) {
|
|
1507
|
+
throw new ValidationError("knownGens: a row with no pointer has no generation to know");
|
|
1508
|
+
}
|
|
1509
|
+
const last = knownGens[knownGens.length - 1];
|
|
1510
|
+
if (last === void 0 || last.generation !== currentGen) {
|
|
903
1511
|
throw new ValidationError(
|
|
904
|
-
|
|
1512
|
+
`knownGens must end with the row's pointer ${currentGen}, in the order the generations were made current (its last entry is ${last === void 0 ? "missing" : last.generation})`
|
|
905
1513
|
);
|
|
906
1514
|
}
|
|
907
|
-
|
|
908
|
-
if (top !== void 0 && top >= currentGen) {
|
|
1515
|
+
if (summary !== void 0 && summary.generation === currentGen && "fingerprint" in summary && fingerprintParts(summary.fingerprint)?.footerCrc !== last.footerCrc) {
|
|
909
1516
|
throw new ValidationError(
|
|
910
|
-
`
|
|
1517
|
+
`knownGens records footerCrc ${last.footerCrc} for generation ${currentGen}, but the summary's fingerprint ${summary.fingerprint} carries another: both describe the one current object`
|
|
911
1518
|
);
|
|
912
1519
|
}
|
|
913
1520
|
}
|
|
@@ -919,45 +1526,52 @@ function refuseDriverManaged(write, what) {
|
|
|
919
1526
|
);
|
|
920
1527
|
}
|
|
921
1528
|
}
|
|
1529
|
+
function validateKeyId(keyId) {
|
|
1530
|
+
if (keyId !== void 0 && (typeof keyId !== "string" || keyId.length === 0)) {
|
|
1531
|
+
throw new ValidationError("keyId must be a non-empty string, or absent");
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
922
1534
|
function validateNewRegistryRecord(rec) {
|
|
923
1535
|
refuseDriverManaged(rec, "a new record");
|
|
1536
|
+
validateKeyId(rec.keyId);
|
|
924
1537
|
validateGeneration(rec.currentGen);
|
|
925
1538
|
if (rec.status !== void 0) validateStatus(rec.status);
|
|
926
|
-
validateWrappedDeks(rec.wrappedDeks,
|
|
1539
|
+
validateWrappedDeks(rec.wrappedDeks, "caller");
|
|
927
1540
|
validateGovernance(rec.retention, "retention");
|
|
928
1541
|
validateGovernance(rec.residency, "residency");
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
1542
|
+
const knownGens = validateKnownGens(rec.knownGens, "caller");
|
|
1543
|
+
const withKnown = knownGens === void 0 ? rec : { ...rec, knownGens };
|
|
1544
|
+
const summary = "summary" in rec ? validateSummary(rec.summary, "caller") : void 0;
|
|
1545
|
+
validateKnownGensFit(knownGens, rec.currentGen, summary);
|
|
1546
|
+
if (!("summary" in rec)) return withKnown;
|
|
934
1547
|
validateSummaryNames(summary, rec.currentGen);
|
|
935
1548
|
validateSummaryKeys(summary, rec.wrappedDeks);
|
|
936
|
-
return { ...
|
|
1549
|
+
return { ...withKnown, summary };
|
|
937
1550
|
}
|
|
938
1551
|
function validateRegistryPatch(patch) {
|
|
939
1552
|
refuseDriverManaged(patch, "a patch");
|
|
1553
|
+
if ("keyId" in patch) validateKeyId(patch.keyId);
|
|
940
1554
|
validatePatchGeneration(patch);
|
|
941
1555
|
if (patch.status !== void 0) validateStatus(patch.status);
|
|
942
|
-
if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks,
|
|
1556
|
+
if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, "caller");
|
|
943
1557
|
if ("retention" in patch) validateGovernance(patch.retention, "retention");
|
|
944
1558
|
if ("residency" in patch) validateGovernance(patch.residency, "residency");
|
|
945
|
-
const
|
|
946
|
-
const withLeases = "leases" in
|
|
1559
|
+
const withKnown = patch.knownGens === void 0 ? patch : { ...patch, knownGens: validateKnownGens(patch.knownGens, "caller") };
|
|
1560
|
+
const withLeases = "leases" in withKnown ? { ...withKnown, leases: validateLeases(withKnown.leases, "caller") } : withKnown;
|
|
947
1561
|
if (!("summary" in patch)) return withLeases;
|
|
948
|
-
return { ...withLeases, summary: validateSummary(patch.summary,
|
|
1562
|
+
return { ...withLeases, summary: validateSummary(patch.summary, "caller") };
|
|
949
1563
|
}
|
|
950
|
-
var REGISTRY_SCHEMA_VERSION =
|
|
1564
|
+
var REGISTRY_SCHEMA_VERSION = 5;
|
|
951
1565
|
function assertRegistrySchemaVersion(raw, ctx) {
|
|
952
1566
|
if (raw === void 0) {
|
|
953
1567
|
throw new IntegrityError(`registry row has no schemaVersion: ${ctx}`);
|
|
954
1568
|
}
|
|
955
1569
|
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
|
|
956
|
-
throw new IntegrityError(`registry row has a malformed schemaVersion (${
|
|
1570
|
+
throw new IntegrityError(`registry row has a malformed schemaVersion (${shown3(raw)}): ${ctx}`);
|
|
957
1571
|
}
|
|
958
1572
|
if (raw < REGISTRY_SCHEMA_VERSION) {
|
|
959
1573
|
throw new UnsupportedError(
|
|
960
|
-
`registry row schemaVersion ${raw} is one this build does not read: it reads schema ${REGISTRY_SCHEMA_VERSION} only. Export the segments with the release that wrote the row and load them with this one into a new prefix (
|
|
1574
|
+
`registry row schemaVersion ${raw} is one this build does not read: it reads schema ${REGISTRY_SCHEMA_VERSION} only. Export the segments with the release that wrote the row and load them with this one into a new prefix (steps: https://github.com/cloudbitmaps/cloudbitmaps/blob/main/CHANGELOG.md, the Breaking entry on registry schema 5): ${ctx}`
|
|
961
1575
|
);
|
|
962
1576
|
}
|
|
963
1577
|
if (raw > REGISTRY_SCHEMA_VERSION) {
|
|
@@ -980,7 +1594,7 @@ function tokenParts(token, ctx, what = "token") {
|
|
|
980
1594
|
const match = typeof token === "string" ? INCARNATION_TOKEN.exec(token) : null;
|
|
981
1595
|
if (match === null) {
|
|
982
1596
|
throw new IntegrityError(
|
|
983
|
-
`registry row ${what} is not one a shipped registry writes (${
|
|
1597
|
+
`registry row ${what} is not one a shipped registry writes (${shown3(JSON.stringify(token))}): ${ctx}`
|
|
984
1598
|
);
|
|
985
1599
|
}
|
|
986
1600
|
const counter = Number(match[2]);
|
|
@@ -1025,7 +1639,7 @@ function newIncarnationToken(entropy, tombstone) {
|
|
|
1025
1639
|
const counter = tombstone === void 0 ? 0 : tokenParts(tombstone.token, tombstone.segment).counter + 1;
|
|
1026
1640
|
return `${drawIncarnation(entropy)}.${counter}.${drawWrite(entropy)}`;
|
|
1027
1641
|
}
|
|
1028
|
-
function
|
|
1642
|
+
function shown3(v) {
|
|
1029
1643
|
const text = String(v);
|
|
1030
1644
|
return text.length > 64 ? `${text.slice(0, 64)}\u2026` : text;
|
|
1031
1645
|
}
|
|
@@ -1044,7 +1658,7 @@ function parseRegistryEnvelope(text, ctx, expected) {
|
|
|
1044
1658
|
const extra = Object.keys(env).filter((k) => !ENVELOPE_FIELDS.includes(k));
|
|
1045
1659
|
if (extra.length > 0) {
|
|
1046
1660
|
throw new IntegrityError(
|
|
1047
|
-
`registry row has fields its envelope does not declare (${
|
|
1661
|
+
`registry row has fields its envelope does not declare (${shown3(extra.join(", "))}): ${ctx}`
|
|
1048
1662
|
);
|
|
1049
1663
|
}
|
|
1050
1664
|
if (typeof env.deleted !== "boolean" || env.record === null || typeof env.record !== "object") {
|
|
@@ -1074,7 +1688,7 @@ function recordFromNew(ref, rec, now, token) {
|
|
|
1074
1688
|
retention: rec.retention,
|
|
1075
1689
|
residency: rec.residency,
|
|
1076
1690
|
summary: rec.summary,
|
|
1077
|
-
|
|
1691
|
+
knownGens: rec.knownGens,
|
|
1078
1692
|
// A new row has no holder: a lease is written only against a row that already exists.
|
|
1079
1693
|
leases: void 0,
|
|
1080
1694
|
createdAt: now,
|
|
@@ -1089,32 +1703,32 @@ function assertStoredRecordShape(r, ctx) {
|
|
|
1089
1703
|
}
|
|
1090
1704
|
if (r.currentGen !== null && (!Number.isSafeInteger(r.currentGen) || r.currentGen < 0)) {
|
|
1091
1705
|
throw new IntegrityError(
|
|
1092
|
-
`registry record has an invalid currentGen (${
|
|
1706
|
+
`registry record has an invalid currentGen (${shown3(r.currentGen)}): ${ctx}`
|
|
1093
1707
|
);
|
|
1094
1708
|
}
|
|
1095
1709
|
if (!STATUSES.includes(r.status)) {
|
|
1096
|
-
throw new IntegrityError(`registry record has an unknown status (${
|
|
1710
|
+
throw new IntegrityError(`registry record has an unknown status (${shown3(r.status)}): ${ctx}`);
|
|
1097
1711
|
}
|
|
1098
1712
|
const declared = RECORD_FIELDS;
|
|
1099
1713
|
const extra = Object.keys(r).filter((k) => !declared.includes(k));
|
|
1100
1714
|
if (extra.length > 0) {
|
|
1101
1715
|
throw new IntegrityError(
|
|
1102
|
-
`registry record has fields a schema-${REGISTRY_SCHEMA_VERSION} row does not declare (${
|
|
1716
|
+
`registry record has fields a schema-${REGISTRY_SCHEMA_VERSION} row does not declare (${shown3(extra.join(", "))}): ${ctx}`
|
|
1103
1717
|
);
|
|
1104
1718
|
}
|
|
1105
1719
|
if (r.keyId !== void 0 && typeof r.keyId !== "string") {
|
|
1106
1720
|
throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);
|
|
1107
1721
|
}
|
|
1108
|
-
validateWrappedDeks(r.wrappedDeks,
|
|
1722
|
+
validateWrappedDeks(r.wrappedDeks, "stored");
|
|
1109
1723
|
for (const field of ["retention", "residency"]) {
|
|
1110
1724
|
const meta = r[field];
|
|
1111
1725
|
if (meta !== void 0 && (meta === null || typeof meta !== "object" || Array.isArray(meta))) {
|
|
1112
1726
|
throw new IntegrityError(`registry record has a non-object ${field}: ${ctx}`);
|
|
1113
1727
|
}
|
|
1114
1728
|
}
|
|
1115
|
-
validateSummary(r.summary,
|
|
1116
|
-
|
|
1117
|
-
validateLeases(r.leases,
|
|
1729
|
+
validateSummary(r.summary, "stored", ctx);
|
|
1730
|
+
validateKnownGens(r.knownGens, "stored", ctx);
|
|
1731
|
+
validateLeases(r.leases, "stored", ctx);
|
|
1118
1732
|
}
|
|
1119
1733
|
function applyRegistryPatch(prev, patch, now, token) {
|
|
1120
1734
|
const currentGen = "currentGen" in patch ? patch.currentGen ?? null : prev.currentGen;
|
|
@@ -1128,12 +1742,12 @@ function applyRegistryPatch(prev, patch, now, token) {
|
|
|
1128
1742
|
const kept = currentGen === prev.currentGen ? prev.summary : void 0;
|
|
1129
1743
|
summary = kept !== void 0 && summaryAgreesWithKeys(kept, wrappedDeks) ? kept : void 0;
|
|
1130
1744
|
}
|
|
1131
|
-
let
|
|
1132
|
-
if ("
|
|
1133
|
-
|
|
1134
|
-
|
|
1745
|
+
let knownGens;
|
|
1746
|
+
if ("knownGens" in patch) {
|
|
1747
|
+
validateKnownGensFit(patch.knownGens, currentGen, summary);
|
|
1748
|
+
knownGens = patch.knownGens;
|
|
1135
1749
|
} else {
|
|
1136
|
-
|
|
1750
|
+
knownGens = currentGen === prev.currentGen ? prev.knownGens : void 0;
|
|
1137
1751
|
}
|
|
1138
1752
|
let leases;
|
|
1139
1753
|
if ("leases" in patch) {
|
|
@@ -1156,7 +1770,7 @@ function applyRegistryPatch(prev, patch, now, token) {
|
|
|
1156
1770
|
retention: "retention" in patch ? patch.retention : prev.retention,
|
|
1157
1771
|
residency: "residency" in patch ? patch.residency : prev.residency,
|
|
1158
1772
|
summary,
|
|
1159
|
-
|
|
1773
|
+
knownGens,
|
|
1160
1774
|
leases,
|
|
1161
1775
|
createdAt: prev.createdAt,
|
|
1162
1776
|
updatedAt: now,
|
|
@@ -1186,6 +1800,7 @@ function entropyIsAvailable(entropy) {
|
|
|
1186
1800
|
export {
|
|
1187
1801
|
CloudRoaringError,
|
|
1188
1802
|
ValidationError,
|
|
1803
|
+
CONTENTION_NEXT_STEP,
|
|
1189
1804
|
WriteConflictError,
|
|
1190
1805
|
IntegrityError,
|
|
1191
1806
|
NotFoundError,
|
|
@@ -1197,19 +1812,24 @@ export {
|
|
|
1197
1812
|
BudgetExceededError,
|
|
1198
1813
|
KeyUnavailableError,
|
|
1199
1814
|
TransientError,
|
|
1200
|
-
isCloudRoaringError,
|
|
1201
1815
|
isWriteConflictError,
|
|
1202
1816
|
isTransientError,
|
|
1203
|
-
isLeaseExpiredError,
|
|
1204
|
-
isStaleOperandError,
|
|
1205
|
-
isLeaseLimitError,
|
|
1206
1817
|
isNotFoundError,
|
|
1207
1818
|
isIntegrityError,
|
|
1208
1819
|
isValidationError,
|
|
1209
|
-
isUnsupportedError,
|
|
1210
|
-
isCapabilityError,
|
|
1211
|
-
isBudgetExceededError,
|
|
1212
1820
|
isKeyUnavailableError,
|
|
1821
|
+
errorKind,
|
|
1822
|
+
requireInteger,
|
|
1823
|
+
requirePositiveInteger,
|
|
1824
|
+
requireFiniteNumber,
|
|
1825
|
+
U32_MAX,
|
|
1826
|
+
CHUNK_COUNT,
|
|
1827
|
+
isBadIdError,
|
|
1828
|
+
splitId,
|
|
1829
|
+
MAX_REMAINDER,
|
|
1830
|
+
joinId,
|
|
1831
|
+
mapWithConcurrency,
|
|
1832
|
+
boundedAll,
|
|
1213
1833
|
PREAMBLE_BYTES,
|
|
1214
1834
|
FOOTER_BYTES,
|
|
1215
1835
|
encodeNameForKey,
|
|
@@ -1225,19 +1845,19 @@ export {
|
|
|
1225
1845
|
validateUserRef,
|
|
1226
1846
|
validateChunkKeyOrder,
|
|
1227
1847
|
validateChunkRef,
|
|
1228
|
-
|
|
1848
|
+
validateChunkKey,
|
|
1229
1849
|
assertRegistryCanWrite,
|
|
1230
1850
|
STORAGE_BACKEND,
|
|
1231
1851
|
brandAsBackend,
|
|
1232
1852
|
isStorageBackend,
|
|
1233
1853
|
MAX_METADATA_BYTES,
|
|
1234
|
-
canonicalMetadataJson,
|
|
1235
1854
|
metadataBytes,
|
|
1236
1855
|
copiedMetadata,
|
|
1237
1856
|
metadataFromBytes,
|
|
1238
1857
|
sizePart,
|
|
1239
1858
|
fingerprintFor,
|
|
1240
1859
|
fingerprintParts,
|
|
1860
|
+
footerCrcOf,
|
|
1241
1861
|
compactFingerprint,
|
|
1242
1862
|
incarnationOf,
|
|
1243
1863
|
incarnationField,
|
|
@@ -1246,6 +1866,19 @@ export {
|
|
|
1246
1866
|
renewsPointer,
|
|
1247
1867
|
renewPointer,
|
|
1248
1868
|
pointerIdOf,
|
|
1869
|
+
aadFor,
|
|
1870
|
+
sameKeys,
|
|
1871
|
+
sizeOfFingerprint,
|
|
1872
|
+
usableSummary,
|
|
1873
|
+
summaryAgrees,
|
|
1874
|
+
metadataToCarry,
|
|
1875
|
+
summaryOf,
|
|
1876
|
+
ownSummary,
|
|
1877
|
+
DEFAULT_RETRY_POLICY,
|
|
1878
|
+
jitteredWait,
|
|
1879
|
+
retryRead,
|
|
1880
|
+
checkedRetryPolicy,
|
|
1881
|
+
withRetry,
|
|
1249
1882
|
MAX_LEASE_MS,
|
|
1250
1883
|
LEASE_SKEW_MS,
|
|
1251
1884
|
MAX_LEASES_PER_SEGMENT,
|
|
@@ -1257,9 +1890,19 @@ export {
|
|
|
1257
1890
|
leaseChurn,
|
|
1258
1891
|
takeLease,
|
|
1259
1892
|
releaseLease,
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1893
|
+
knownGensShapeFault,
|
|
1894
|
+
ascending,
|
|
1895
|
+
MAX_STORED_KNOWN_GENERATIONS,
|
|
1896
|
+
usableKnownGens,
|
|
1897
|
+
windowOf,
|
|
1898
|
+
KNOWN_SPAN,
|
|
1899
|
+
keptWindow,
|
|
1900
|
+
nextKnown,
|
|
1901
|
+
rollbackKnown,
|
|
1902
|
+
pruneKnown,
|
|
1903
|
+
describingKnown,
|
|
1904
|
+
isFullLedger,
|
|
1905
|
+
fallbackBound,
|
|
1263
1906
|
validateNewRegistryRecord,
|
|
1264
1907
|
validateRegistryPatch,
|
|
1265
1908
|
serializeRegistryEnvelope,
|
|
@@ -1274,6 +1917,10 @@ export {
|
|
|
1274
1917
|
entropyIsAvailable,
|
|
1275
1918
|
DEFAULT_NAMESPACE,
|
|
1276
1919
|
namespaceKeyPart,
|
|
1277
|
-
namespacePathPart
|
|
1920
|
+
namespacePathPart,
|
|
1921
|
+
refuseUnknownOptions,
|
|
1922
|
+
checkNow,
|
|
1923
|
+
requirePositiveInt,
|
|
1924
|
+
requireBool
|
|
1278
1925
|
};
|
|
1279
|
-
//# sourceMappingURL=chunk-
|
|
1926
|
+
//# sourceMappingURL=chunk-OSU7W2Y3.js.map
|