@cloudbitmaps/core 0.8.2 → 0.10.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/NOTICE +4 -4
- package/README.md +28 -6
- package/dist/chunk-BUPDGZRR.js +461 -0
- package/dist/chunk-BUPDGZRR.js.map +7 -0
- package/dist/core/audit.d.ts +127 -0
- package/dist/core/bit-route.d.ts +21 -0
- package/dist/core/blob.d.ts +34 -0
- package/dist/core/budget.d.ts +66 -0
- package/dist/core/codec.d.ts +114 -0
- package/dist/core/concurrency.d.ts +15 -0
- package/dist/core/consistency.d.ts +73 -0
- package/dist/core/cooperative.d.ts +78 -0
- package/dist/core/cost.d.ts +102 -0
- package/dist/core/crbm/crc32c.d.ts +10 -0
- package/dist/core/crbm/format.d.ts +116 -0
- package/dist/core/crbm/reader.d.ts +69 -0
- package/dist/core/crbm/varint.d.ts +13 -0
- package/dist/core/crbm/writer.d.ts +39 -0
- package/dist/core/crbm-storage-source.d.ts +432 -0
- package/dist/core/crypto.d.ts +80 -0
- package/dist/core/determinism.d.ts +42 -0
- package/dist/core/discover.d.ts +75 -0
- package/dist/core/due-index.d.ts +55 -0
- package/dist/core/engine.d.ts +204 -0
- package/dist/core/erase-id.d.ts +141 -0
- package/dist/core/erasure.d.ts +179 -0
- package/dist/core/errors.d.ts +120 -0
- package/dist/core/generation-gc.d.ts +52 -0
- package/dist/core/keys.d.ts +23 -0
- package/dist/core/load.d.ts +102 -0
- package/dist/core/lru.d.ts +69 -0
- package/dist/core/metrics.d.ts +132 -0
- package/dist/core/name-codec.d.ts +79 -0
- package/dist/core/pinned-storage-source.d.ts +69 -0
- package/dist/core/ports.d.ts +340 -0
- package/dist/core/registry-scan.d.ts +38 -0
- package/dist/core/retention-sweep.d.ts +190 -0
- package/dist/core/retention.d.ts +67 -0
- package/dist/core/retry.d.ts +65 -0
- package/dist/core/rollback.d.ts +71 -0
- package/dist/core/validate.d.ts +20 -0
- package/dist/driver-kit.d.ts +10 -0
- package/dist/driver-kit.js +314 -0
- package/dist/driver-kit.js.map +7 -0
- package/dist/drivers/_shared/aws-errors.d.ts +18 -0
- package/dist/drivers/_shared/keys.d.ts +27 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
- package/dist/drivers/_shared/object-registry.d.ts +84 -0
- package/dist/drivers/_shared/registry.d.ts +62 -0
- package/dist/drivers/backends.d.ts +49 -0
- package/dist/drivers/crypto.d.ts +38 -0
- package/dist/drivers/localfs/fs-util.d.ts +20 -0
- package/dist/drivers/localfs/paths.d.ts +33 -0
- package/dist/drivers/localfs/registry.d.ts +28 -0
- package/dist/drivers/localfs/storage.d.ts +19 -0
- package/dist/drivers/memory.d.ts +57 -0
- package/dist/drivers/retry/retrying-drivers.d.ts +90 -0
- package/dist/export/index.d.ts +92 -0
- package/dist/index.d.ts +85 -2534
- package/dist/index.js +3076 -2978
- package/dist/index.js.map +7 -1
- package/dist/testing/fuzz-core.d.ts +14 -0
- package/package.json +19 -115
- package/dist/azure/index.cjs +0 -368
- package/dist/azure/index.cjs.map +0 -1
- package/dist/azure/index.d.cts +0 -48
- package/dist/azure/index.d.ts +0 -48
- package/dist/azure/index.js +0 -304
- package/dist/azure/index.js.map +0 -1
- package/dist/cassandra/index.cjs +0 -265
- package/dist/cassandra/index.cjs.map +0 -1
- package/dist/cassandra/index.d.cts +0 -52
- package/dist/cassandra/index.d.ts +0 -52
- package/dist/cassandra/index.js +0 -204
- package/dist/cassandra/index.js.map +0 -1
- package/dist/chunk-7LMLYSVJ.js +0 -43
- package/dist/chunk-7LMLYSVJ.js.map +0 -1
- package/dist/chunk-L54FL5DW.js +0 -203
- package/dist/chunk-L54FL5DW.js.map +0 -1
- package/dist/chunk-NUIDEEFZ.js +0 -91
- package/dist/chunk-NUIDEEFZ.js.map +0 -1
- package/dist/chunk-SNJVZ227.js +0 -35
- package/dist/chunk-SNJVZ227.js.map +0 -1
- package/dist/chunk-YEHMG6W2.js +0 -6
- package/dist/chunk-YEHMG6W2.js.map +0 -1
- package/dist/dynamodb/index.cjs +0 -740
- package/dist/dynamodb/index.cjs.map +0 -1
- package/dist/dynamodb/index.d.cts +0 -106
- package/dist/dynamodb/index.d.ts +0 -106
- package/dist/dynamodb/index.js +0 -469
- package/dist/dynamodb/index.js.map +0 -1
- package/dist/gcs/index.cjs +0 -343
- package/dist/gcs/index.cjs.map +0 -1
- package/dist/gcs/index.d.cts +0 -46
- package/dist/gcs/index.d.ts +0 -46
- package/dist/gcs/index.js +0 -279
- package/dist/gcs/index.js.map +0 -1
- package/dist/index.cjs +0 -4608
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2535
- package/dist/mongodb/index.cjs +0 -260
- package/dist/mongodb/index.cjs.map +0 -1
- package/dist/mongodb/index.d.cts +0 -45
- package/dist/mongodb/index.d.ts +0 -45
- package/dist/mongodb/index.js +0 -199
- package/dist/mongodb/index.js.map +0 -1
- package/dist/mysql/index.cjs +0 -281
- package/dist/mysql/index.cjs.map +0 -1
- package/dist/mysql/index.d.cts +0 -56
- package/dist/mysql/index.d.ts +0 -56
- package/dist/mysql/index.js +0 -211
- package/dist/mysql/index.js.map +0 -1
- package/dist/ports-5BQTGKze.d.cts +0 -356
- package/dist/ports-5BQTGKze.d.ts +0 -356
- package/dist/postgres/index.cjs +0 -273
- package/dist/postgres/index.cjs.map +0 -1
- package/dist/postgres/index.d.cts +0 -54
- package/dist/postgres/index.d.ts +0 -54
- package/dist/postgres/index.js +0 -203
- package/dist/postgres/index.js.map +0 -1
- package/dist/redis/index.cjs +0 -257
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -42
- package/dist/redis/index.d.ts +0 -42
- package/dist/redis/index.js +0 -197
- package/dist/redis/index.js.map +0 -1
- package/dist/s3/index.cjs +0 -891
- package/dist/s3/index.cjs.map +0 -1
- package/dist/s3/index.d.cts +0 -112
- package/dist/s3/index.d.ts +0 -112
- package/dist/s3/index.js +0 -555
- package/dist/s3/index.js.map +0 -1
package/dist/cassandra/index.js
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { NO_ROW } from '../chunk-YEHMG6W2.js';
|
|
2
|
-
import { ValidationError, validateChunkRef, namespacePart, WriteConflictError, validateSegmentRef, IntegrityError, TransientError } from '../chunk-NUIDEEFZ.js';
|
|
3
|
-
import { randomUUID } from 'crypto';
|
|
4
|
-
|
|
5
|
-
// src/drivers/cassandra/keys.ts
|
|
6
|
-
var IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,47}$/;
|
|
7
|
-
function quoteIdent(id, what) {
|
|
8
|
-
if (!IDENT.test(id)) {
|
|
9
|
-
throw new ValidationError(
|
|
10
|
-
`invalid ${what} ${JSON.stringify(id)} \u2014 expected a CQL identifier (letters, digits, underscore; \u226448 chars; leading letter/underscore)`
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
return `"${id}"`;
|
|
14
|
-
}
|
|
15
|
-
function validateAndQuoteTable(keyspace, table) {
|
|
16
|
-
return `${quoteIdent(keyspace, "keyspace")}.${quoteIdent(table, "table")}`;
|
|
17
|
-
}
|
|
18
|
-
function normalizeKeyPrefix(prefix) {
|
|
19
|
-
if (prefix === void 0 || prefix === "") return "";
|
|
20
|
-
for (const ch of prefix) {
|
|
21
|
-
if (ch.charCodeAt(0) < 32) {
|
|
22
|
-
throw new ValidationError("keyPrefix must not contain control characters");
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return prefix;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// src/drivers/cassandra/cassandra-errors.ts
|
|
29
|
-
function networkCode(err) {
|
|
30
|
-
const code = err?.code;
|
|
31
|
-
return typeof code === "string" && /^E[A-Z_]+$/.test(code) ? code : void 0;
|
|
32
|
-
}
|
|
33
|
-
var TRANSIENT_NAMES = /* @__PURE__ */ new Set([
|
|
34
|
-
"NoHostAvailableError",
|
|
35
|
-
"OperationTimedOutError",
|
|
36
|
-
"BusyConnectionError",
|
|
37
|
-
// connection-pool exhaustion — a transient back-pressure fault
|
|
38
|
-
"DriverError"
|
|
39
|
-
// generic connection-level driver fault (e.g. socket closed mid-request)
|
|
40
|
-
]);
|
|
41
|
-
var RETRYABLE_RESPONSE_CODES = /* @__PURE__ */ new Set([
|
|
42
|
-
4096,
|
|
43
|
-
// Unavailable
|
|
44
|
-
4097,
|
|
45
|
-
// Overloaded
|
|
46
|
-
4098,
|
|
47
|
-
// IsBootstrapping
|
|
48
|
-
4099,
|
|
49
|
-
// TruncateError
|
|
50
|
-
4352,
|
|
51
|
-
// WriteTimeout
|
|
52
|
-
4608
|
|
53
|
-
// ReadTimeout
|
|
54
|
-
]);
|
|
55
|
-
function isTransient(err) {
|
|
56
|
-
const name = err?.name;
|
|
57
|
-
if (typeof name === "string" && TRANSIENT_NAMES.has(name)) return true;
|
|
58
|
-
if (name === "ResponseError") {
|
|
59
|
-
const code = err.code;
|
|
60
|
-
if (typeof code === "number" && RETRYABLE_RESPONSE_CODES.has(code)) return true;
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
const net = networkCode(err);
|
|
64
|
-
return net === "ECONNRESET" || net === "ECONNABORTED" || net === "ETIMEDOUT" || net === "ESOCKETTIMEDOUT" || net === "ECONNREFUSED" || net === "EHOSTUNREACH" || net === "ENETUNREACH" || net === "EPIPE" || net === "EAI_AGAIN" || net === "ENOTFOUND";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// src/drivers/cassandra/warm.ts
|
|
68
|
-
var DEFAULT_TABLE = "cloud_roaring_warm";
|
|
69
|
-
var DEFAULT_FETCH_SIZE = 500;
|
|
70
|
-
var CONSISTENCY_LOCAL_SERIAL = 9;
|
|
71
|
-
var CONSISTENCY_LOCAL_ONE = 10;
|
|
72
|
-
function cassandraWarmTableDDL(keyspace, table = DEFAULT_TABLE) {
|
|
73
|
-
const t = validateAndQuoteTable(keyspace, table);
|
|
74
|
-
return `CREATE TABLE IF NOT EXISTS ${t} (
|
|
75
|
-
kp text,
|
|
76
|
-
ns text,
|
|
77
|
-
seg text,
|
|
78
|
-
ck int,
|
|
79
|
-
tok text,
|
|
80
|
-
payload blob,
|
|
81
|
-
PRIMARY KEY ((kp, ns, seg), ck)
|
|
82
|
-
);`;
|
|
83
|
-
}
|
|
84
|
-
var CassandraWarmDriver = class {
|
|
85
|
-
client;
|
|
86
|
-
table;
|
|
87
|
-
// validated + quoted "keyspace"."table"
|
|
88
|
-
keyPrefix;
|
|
89
|
-
fetchSize;
|
|
90
|
-
constructor(options) {
|
|
91
|
-
this.client = options.client;
|
|
92
|
-
this.table = validateAndQuoteTable(options.keyspace, options.table ?? DEFAULT_TABLE);
|
|
93
|
-
this.keyPrefix = normalizeKeyPrefix(options.keyPrefix);
|
|
94
|
-
const fetch = options.listPageSize ?? DEFAULT_FETCH_SIZE;
|
|
95
|
-
if (!Number.isSafeInteger(fetch) || fetch < 1) {
|
|
96
|
-
throw new ValidationError(`listPageSize must be a positive safe integer; got ${fetch}`);
|
|
97
|
-
}
|
|
98
|
-
this.fetchSize = fetch;
|
|
99
|
-
}
|
|
100
|
-
/** Read consistency: `LOCAL_SERIAL` (linearizable, read-your-writes) unless the caller opts into eventual. */
|
|
101
|
-
readConsistency(opts) {
|
|
102
|
-
return opts?.consistent === false ? CONSISTENCY_LOCAL_ONE : CONSISTENCY_LOCAL_SERIAL;
|
|
103
|
-
}
|
|
104
|
-
async get(ref, opts) {
|
|
105
|
-
validateChunkRef(ref);
|
|
106
|
-
try {
|
|
107
|
-
const res = await this.client.execute(
|
|
108
|
-
`SELECT tok, payload FROM ${this.table} WHERE kp = ? AND ns = ? AND seg = ? AND ck = ?`,
|
|
109
|
-
[this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey],
|
|
110
|
-
{ prepare: true, consistency: this.readConsistency(opts) }
|
|
111
|
-
);
|
|
112
|
-
const row = res.rows[0];
|
|
113
|
-
return row === void 0 ? null : this.rowFrom(row, ref.chunkKey);
|
|
114
|
-
} catch (err) {
|
|
115
|
-
throw this.mapError(err);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
async putConditional(ref, bytes, expected) {
|
|
119
|
-
validateChunkRef(ref);
|
|
120
|
-
const token = randomUUID();
|
|
121
|
-
const payload = Buffer.from(bytes);
|
|
122
|
-
const ns = namespacePart(ref.namespace);
|
|
123
|
-
let applied;
|
|
124
|
-
try {
|
|
125
|
-
const res = expected === NO_ROW ? await this.client.execute(
|
|
126
|
-
`INSERT INTO ${this.table} (kp, ns, seg, ck, tok, payload) VALUES (?, ?, ?, ?, ?, ?) IF NOT EXISTS`,
|
|
127
|
-
[this.keyPrefix, ns, ref.segment, ref.chunkKey, token, payload],
|
|
128
|
-
{ prepare: true }
|
|
129
|
-
) : await this.client.execute(
|
|
130
|
-
`UPDATE ${this.table} SET tok = ?, payload = ? WHERE kp = ? AND ns = ? AND seg = ? AND ck = ? IF tok = ?`,
|
|
131
|
-
[token, payload, this.keyPrefix, ns, ref.segment, ref.chunkKey, expected],
|
|
132
|
-
{ prepare: true }
|
|
133
|
-
);
|
|
134
|
-
applied = res.wasApplied();
|
|
135
|
-
} catch (err) {
|
|
136
|
-
throw this.mapError(err);
|
|
137
|
-
}
|
|
138
|
-
if (!applied) {
|
|
139
|
-
throw new WriteConflictError(
|
|
140
|
-
expected === NO_ROW ? `chunk ${ref.chunkKey} already exists (create-if-absent)` : `OCC conflict on chunk ${ref.chunkKey}`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
return { token };
|
|
144
|
-
}
|
|
145
|
-
async deleteConditional(ref, expected) {
|
|
146
|
-
validateChunkRef(ref);
|
|
147
|
-
let applied;
|
|
148
|
-
try {
|
|
149
|
-
const res = await this.client.execute(
|
|
150
|
-
`DELETE FROM ${this.table} WHERE kp = ? AND ns = ? AND seg = ? AND ck = ? IF tok = ?`,
|
|
151
|
-
[this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey, expected],
|
|
152
|
-
{ prepare: true }
|
|
153
|
-
);
|
|
154
|
-
applied = res.wasApplied();
|
|
155
|
-
} catch (err) {
|
|
156
|
-
throw this.mapError(err);
|
|
157
|
-
}
|
|
158
|
-
if (!applied) {
|
|
159
|
-
throw new WriteConflictError(`fenced-delete conflict on chunk ${ref.chunkKey}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
async *listChunks(ref, opts) {
|
|
163
|
-
validateSegmentRef(ref);
|
|
164
|
-
const stream = this.client.stream(
|
|
165
|
-
`SELECT ck, tok, payload FROM ${this.table} WHERE kp = ? AND ns = ? AND seg = ?`,
|
|
166
|
-
[this.keyPrefix, namespacePart(ref.namespace), ref.segment],
|
|
167
|
-
{ prepare: true, fetchSize: this.fetchSize, consistency: this.readConsistency(opts) }
|
|
168
|
-
);
|
|
169
|
-
try {
|
|
170
|
-
for await (const raw of stream) {
|
|
171
|
-
if (typeof raw.ck !== "number") {
|
|
172
|
-
throw new IntegrityError("warm row is missing its numeric chunk key (ck)");
|
|
173
|
-
}
|
|
174
|
-
yield { chunkKey: raw.ck, ...this.rowFrom(raw, raw.ck) };
|
|
175
|
-
}
|
|
176
|
-
} catch (err) {
|
|
177
|
-
throw this.mapError(err);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
/** Build a {@link WarmRow} from a raw row; reject one missing/mistyped fields (corrupt/foreign). */
|
|
181
|
-
rowFrom(row, chunkKey) {
|
|
182
|
-
if (typeof row.tok !== "string" || row.tok === "") {
|
|
183
|
-
throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);
|
|
184
|
-
}
|
|
185
|
-
if (!(row.payload instanceof Uint8Array)) {
|
|
186
|
-
throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its payload`);
|
|
187
|
-
}
|
|
188
|
-
return { token: row.tok, bytes: new Uint8Array(row.payload) };
|
|
189
|
-
}
|
|
190
|
-
/** Reclassify a transient Cassandra fault as a retryable {@link TransientError}; else propagate unchanged. */
|
|
191
|
-
mapError(err) {
|
|
192
|
-
if (isTransient(err)) {
|
|
193
|
-
return new TransientError(
|
|
194
|
-
`transient Cassandra fault: ${err?.name ?? "unknown"}`,
|
|
195
|
-
{ cause: err }
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
return err;
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
export { CassandraWarmDriver, cassandraWarmTableDDL };
|
|
203
|
-
//# sourceMappingURL=index.js.map
|
|
204
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/drivers/cassandra/keys.ts","../../src/drivers/cassandra/cassandra-errors.ts","../../src/drivers/cassandra/warm.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Identifier + key validation for {@link CassandraWarmDriver} (Phase 7).\n *\n * Pure, SDK-free string logic — unit-testable without a live Cassandra. The logical ref is stored across\n * **bound (`?`) columns** (`kp`/`ns`/`seg`/`ck`), so ref/prefix values are never concatenated into CQL. The\n * only values that CANNOT be bind parameters are the **keyspace + table identifiers**, so they are validated\n * against a strict grammar and quoted before interpolation — the sole CQL-injection vector, closed.\n */\nimport { ValidationError } from '@/core/errors';\n\n/** A CQL identifier: leading letter/underscore, then letters/digits/underscores (≤48 = Cassandra max). */\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,47}$/;\n\nfunction quoteIdent(id: string, what: string): string {\n if (!IDENT.test(id)) {\n throw new ValidationError(\n `invalid ${what} ${JSON.stringify(id)} — expected a CQL identifier ` +\n `(letters, digits, underscore; ≤48 chars; leading letter/underscore)`,\n );\n }\n return `\"${id}\"`;\n}\n\n/** Validate + quote `keyspace` and `table` into a fully-qualified `\"keyspace\".\"table\"` for interpolation. */\nexport function validateAndQuoteTable(keyspace: string, table: string): string {\n return `${quoteIdent(keyspace, 'keyspace')}.${quoteIdent(table, 'table')}`;\n}\n\n/**\n * Validate a caller-supplied key prefix (bound as a `?` value, so this is a sanity guard, not an\n * anti-injection boundary): no control characters. Empty/undefined ⇒ no prefix (stored as `''`).\n */\nexport function normalizeKeyPrefix(prefix: string | undefined): string {\n if (prefix === undefined || prefix === '') return '';\n for (const ch of prefix) {\n if (ch.charCodeAt(0) < 0x20) {\n throw new ValidationError('keyPrefix must not contain control characters');\n }\n }\n return prefix;\n}\n","/**\n * Pure helpers for classifying Cassandra / ScyllaDB (`cassandra-driver`) errors (Phase 7; transient class\n * mirrors the other drivers).\n *\n * SDK-free + side-effect-free — they read only structural shapes off the thrown value (`err.name`, `err.code`).\n * The driver signals an OCC conflict itself (an LWT that returns `wasApplied() === false`), so there's no\n * \"conflict\" classifier here — only transient-vs-not. Everything not transient propagates unchanged so a real\n * bug is never blind-retried.\n */\n\n/** A network-level error `code` string (starts with `E`, uppercase letters + underscore, no digits). */\nfunction networkCode(err: unknown): string | undefined {\n const code = (err as { code?: unknown } | null)?.code;\n return typeof code === 'string' && /^E[A-Z_]+$/.test(code) ? code : undefined;\n}\n\n/**\n * Transient `cassandra-driver` error classes: no coordinator reachable, a client-side operation timeout, or a\n * server response for a timeout / overload / unavailable / bootstrapping node. All safe to retry (the\n * resilience layer rides them out). A deterministic server error (syntax, invalid query, unauthorized) is a\n * `ResponseError` with a non-retryable code and is NOT matched here, so it surfaces.\n */\nconst TRANSIENT_NAMES = new Set<string>([\n 'NoHostAvailableError',\n 'OperationTimedOutError',\n 'BusyConnectionError', // connection-pool exhaustion — a transient back-pressure fault\n 'DriverError', // generic connection-level driver fault (e.g. socket closed mid-request)\n]);\n\n// ResponseError `code` values (CQL binary protocol) that are retryable: server overloaded / bootstrapping,\n// truncate, and read/write TIMEOUTS + unavailable. NOTE: an LWT write-timeout retry is ambiguous — the write\n// may have committed, so a retry can surface a spurious WriteConflictError (the caller's OCC re-read converges;\n// no data loss). ReadFailure/WriteFailure (0x1300/0x1500) are deliberately EXCLUDED — they signal a replica-\n// side *failure* (e.g. a tombstone-threshold breach), not a timeout, so a blind retry wouldn't help. Other\n// deterministic codes (SyntaxError 0x2000, Invalid 0x2200, Unauthorized 0x2100, AlreadyExists 0x2400) are\n// likewise excluded so a real bug surfaces.\nconst RETRYABLE_RESPONSE_CODES = new Set<number>([\n 0x1000, // Unavailable\n 0x1001, // Overloaded\n 0x1002, // IsBootstrapping\n 0x1003, // TruncateError\n 0x1100, // WriteTimeout\n 0x1200, // ReadTimeout\n]);\n\nexport function isTransient(err: unknown): boolean {\n const name = (err as { name?: unknown } | null)?.name;\n if (typeof name === 'string' && TRANSIENT_NAMES.has(name)) return true;\n if (name === 'ResponseError') {\n const code = (err as { code?: unknown }).code;\n if (typeof code === 'number' && RETRYABLE_RESPONSE_CODES.has(code)) return true;\n return false; // a ResponseError with any other code is deterministic → must surface\n }\n const net = networkCode(err);\n return (\n net === 'ECONNRESET' ||\n net === 'ECONNABORTED' ||\n net === 'ETIMEDOUT' ||\n net === 'ESOCKETTIMEDOUT' ||\n net === 'ECONNREFUSED' ||\n net === 'EHOSTUNREACH' ||\n net === 'ENETUNREACH' ||\n net === 'EPIPE' ||\n net === 'EAI_AGAIN' ||\n net === 'ENOTFOUND'\n );\n}\n","/**\n * `CassandraWarmDriver` — an {@link IWarmDriver} over Cassandra / ScyllaDB (Phase 7).\n *\n * Uses the official `cassandra-driver`, an **optional peer dependency** — only consumers of\n * `cloud-roaring/cassandra` install it. A `Client` is **injected** (dependency injection): the driver owns no\n * connection/credential logic, so it's thin and reuses the caller's client.\n *\n * Each chunk is one row in a table partitioned by `(kp, ns, seg)` and clustered by `ck` — so all of a\n * segment's chunks share one partition and `listChunks` is a single partition read already ordered by\n * `ck` ascending. The opaque OCC **token** (a random UUID per write) lives in a `tok` column (`token` is a CQL\n * reserved word), and OCC is a **lightweight transaction** (LWT — Paxos-linearizable compare-and-set):\n * - **create-if-absent** = `INSERT … IF NOT EXISTS`; not applied ⇒ {@link WriteConflictError}.\n * - **token-fenced update / delete** = `UPDATE … / DELETE … IF tok = ?`; not applied (token moved or row\n * absent) ⇒ {@link WriteConflictError}.\n *\n * The LWT's `IF` is evaluated under Paxos against the latest committed value, so the compare-and-set is\n * correct even if a prior `get` read a slightly stale value (a doomed write is simply not applied → retry).\n * **Reads observe the latest committed LWT value:** `get`/`listChunks` run at `LOCAL_SERIAL` by default (a\n * linearizable read that completes any in-flight Paxos round — the read-your-writes the OCC path needs on a\n * multi-node RF>1 cluster), downgrading to `LOCAL_ONE` only when the caller passes `{ consistent: false }`.\n * Tokens are not reused across delete→recreate (ABA-safe, D3 — probabilistic random UUID). Drivers may use\n * `node:crypto`.\n */\nimport { randomUUID } from 'node:crypto';\nimport type { Client } from 'cassandra-driver';\nimport { IntegrityError, TransientError, ValidationError, WriteConflictError } from '@/core/errors';\nimport { NO_ROW } from '@/core/ports';\nimport type {\n ChunkRef,\n IWarmDriver,\n NoRow,\n SegmentRef,\n Token,\n WarmReadOptions,\n WarmRow,\n} from '@/core/ports';\nimport { validateChunkRef, validateSegmentRef } from '@/core/validate';\nimport { namespacePart } from '../_shared/keys';\nimport { normalizeKeyPrefix, validateAndQuoteTable } from './keys';\nimport { isTransient } from './cassandra-errors';\n\nconst DEFAULT_TABLE = 'cloud_roaring_warm';\n/** listChunks page size (CQL `fetchSize`) — the stream auto-pages, bounding resident rows on a wide segment. */\nconst DEFAULT_FETCH_SIZE = 500;\n// `cassandra-driver` `types.consistencies` values — stable CQL binary-protocol constants, inlined so the built\n// subpath keeps NO runtime `cassandra-driver` import (the SDK stays a pure type + optional peer, as with the\n// other drivers). LOCAL_SERIAL = a linearizable read (observes the latest committed LWT value / read-your-\n// writes); LOCAL_ONE = a fast, possibly-stale read used only when the caller opts into eventual consistency.\nconst CONSISTENCY_LOCAL_SERIAL = 9;\nconst CONSISTENCY_LOCAL_ONE = 10;\n\nexport interface CassandraWarmDriverOptions {\n /**\n * A connected `cassandra-driver` `Client`. The driver never connects/shuts it down — the caller owns it.\n * Notes: identifiers (keyspace/table) are **case-sensitive** here (quoted), so pass them exactly as stored\n * (lowercase unless created quoted). OCC reads use `LOCAL_SERIAL` — linearizable **within one datacenter**;\n * a **multi-DC** deployment that needs cross-DC linearizable OCC must run at `SERIAL` (configure it on the\n * client and note this driver defaults to the local variant). An LWT that times out is ambiguous: the retry\n * layer may re-issue it and surface a spurious `WriteConflictError` for a write that actually committed —\n * the caller's OCC re-read converges and no data is lost.\n */\n readonly client: Client;\n /** Keyspace holding the warm table (must already exist — replication is a deployment decision). */\n readonly keyspace: string;\n /** Warm table name (default `cloud_roaring_warm`). Must already exist (see `cassandraWarmTableDDL`). */\n readonly table?: string;\n /** Optional key-prefix column value so several logical stores can share one table (bound as data). */\n readonly keyPrefix?: string;\n /** Advanced: `listChunks` fetch (page) size (default 500). Tunes peak memory on wide segments. */\n readonly listPageSize?: number;\n}\n\n/** One raw warm row as selected (cassandra-driver decodes `blob`→Buffer, `int`→number, `text`→string).\n * NB: the token column is named `tok`, not `token` — `token` is a reserved word in CQL (`token()` function). */\ninterface RawRow {\n ck?: unknown;\n tok?: unknown;\n payload?: unknown;\n}\n\n/**\n * The idempotent CQL DDL for the warm table (identifier-validated + quoted). The driver does **not** create\n * schema at runtime (it stays thin); run this once at deploy time against an existing keyspace, e.g.\n * `await client.execute(cassandraWarmTableDDL('my_ks'))`.\n */\nexport function cassandraWarmTableDDL(keyspace: string, table: string = DEFAULT_TABLE): string {\n const t = validateAndQuoteTable(keyspace, table);\n return (\n `CREATE TABLE IF NOT EXISTS ${t} (\\n` +\n // `tok`, not `token` — `token` is a reserved word in CQL.\n ` kp text,\\n ns text,\\n seg text,\\n ck int,\\n tok text,\\n payload blob,\\n` +\n ` PRIMARY KEY ((kp, ns, seg), ck)\\n` +\n `);`\n );\n}\n\nexport class CassandraWarmDriver implements IWarmDriver {\n private readonly client: Client;\n private readonly table: string; // validated + quoted \"keyspace\".\"table\"\n private readonly keyPrefix: string;\n private readonly fetchSize: number;\n\n constructor(options: CassandraWarmDriverOptions) {\n this.client = options.client;\n this.table = validateAndQuoteTable(options.keyspace, options.table ?? DEFAULT_TABLE);\n this.keyPrefix = normalizeKeyPrefix(options.keyPrefix);\n const fetch = options.listPageSize ?? DEFAULT_FETCH_SIZE;\n if (!Number.isSafeInteger(fetch) || fetch < 1) {\n throw new ValidationError(`listPageSize must be a positive safe integer; got ${fetch}`);\n }\n this.fetchSize = fetch;\n }\n\n /** Read consistency: `LOCAL_SERIAL` (linearizable, read-your-writes) unless the caller opts into eventual. */\n private readConsistency(opts?: WarmReadOptions): number {\n return opts?.consistent === false ? CONSISTENCY_LOCAL_ONE : CONSISTENCY_LOCAL_SERIAL;\n }\n\n async get(ref: ChunkRef, opts?: WarmReadOptions): Promise<WarmRow | null> {\n validateChunkRef(ref);\n try {\n const res = await this.client.execute(\n `SELECT tok, payload FROM ${this.table} WHERE kp = ? AND ns = ? AND seg = ? AND ck = ?`,\n [this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey],\n { prepare: true, consistency: this.readConsistency(opts) },\n );\n const row = res.rows[0] as RawRow | undefined;\n return row === undefined ? null : this.rowFrom(row, ref.chunkKey);\n } catch (err) {\n throw this.mapError(err);\n }\n }\n\n async putConditional(\n ref: ChunkRef,\n bytes: Uint8Array,\n expected: Token | NoRow,\n ): Promise<{ token: Token }> {\n validateChunkRef(ref);\n const token = randomUUID();\n const payload = Buffer.from(bytes); // copy: severs any caller-owned/reused input buffer\n const ns = namespacePart(ref.namespace);\n let applied: boolean;\n try {\n const res =\n expected === NO_ROW\n ? await this.client.execute(\n `INSERT INTO ${this.table} (kp, ns, seg, ck, tok, payload) ` +\n `VALUES (?, ?, ?, ?, ?, ?) IF NOT EXISTS`,\n [this.keyPrefix, ns, ref.segment, ref.chunkKey, token, payload],\n { prepare: true },\n )\n : await this.client.execute(\n `UPDATE ${this.table} SET tok = ?, payload = ? ` +\n `WHERE kp = ? AND ns = ? AND seg = ? AND ck = ? IF tok = ?`,\n [token, payload, this.keyPrefix, ns, ref.segment, ref.chunkKey, expected],\n { prepare: true },\n );\n applied = res.wasApplied();\n } catch (err) {\n throw this.mapError(err);\n }\n if (!applied) {\n throw new WriteConflictError(\n expected === NO_ROW\n ? `chunk ${ref.chunkKey} already exists (create-if-absent)`\n : `OCC conflict on chunk ${ref.chunkKey}`,\n );\n }\n return { token };\n }\n\n async deleteConditional(ref: ChunkRef, expected: Token): Promise<void> {\n validateChunkRef(ref);\n let applied: boolean;\n try {\n const res = await this.client.execute(\n `DELETE FROM ${this.table} WHERE kp = ? AND ns = ? AND seg = ? AND ck = ? IF tok = ?`,\n [this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey, expected],\n { prepare: true },\n );\n applied = res.wasApplied();\n } catch (err) {\n throw this.mapError(err);\n }\n if (!applied) {\n throw new WriteConflictError(`fenced-delete conflict on chunk ${ref.chunkKey}`);\n }\n }\n\n async *listChunks(\n ref: SegmentRef,\n opts?: WarmReadOptions,\n ): AsyncIterable<{ chunkKey: number } & WarmRow> {\n validateSegmentRef(ref);\n // A single-partition read, already ordered by the `ck` clustering key (ascending). `client.stream` is an\n // object-mode Readable that auto-pages at `fetchSize`, so peak memory is bounded on a wide segment.\n const stream = this.client.stream(\n `SELECT ck, tok, payload FROM ${this.table} WHERE kp = ? AND ns = ? AND seg = ?`,\n [this.keyPrefix, namespacePart(ref.namespace), ref.segment],\n { prepare: true, fetchSize: this.fetchSize, consistency: this.readConsistency(opts) },\n );\n try {\n // `client.stream` returns an object-mode Readable (async-iterable at runtime); its type defs expose only\n // the EventEmitter surface, so cast via `unknown`.\n for await (const raw of stream as unknown as AsyncIterable<RawRow>) {\n // Untrusted-data posture (like the sibling drivers): a corrupt row with a non-numeric `ck` must not\n // surface as `{ chunkKey: undefined }`.\n if (typeof raw.ck !== 'number') {\n throw new IntegrityError('warm row is missing its numeric chunk key (ck)');\n }\n yield { chunkKey: raw.ck, ...this.rowFrom(raw, raw.ck) };\n }\n } catch (err) {\n throw this.mapError(err);\n }\n }\n\n /** Build a {@link WarmRow} from a raw row; reject one missing/mistyped fields (corrupt/foreign). */\n private rowFrom(row: RawRow, chunkKey: number): WarmRow {\n if (typeof row.tok !== 'string' || row.tok === '') {\n throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);\n }\n if (!(row.payload instanceof Uint8Array)) {\n throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its payload`);\n }\n return { token: row.tok, bytes: new Uint8Array(row.payload) };\n }\n\n /** Reclassify a transient Cassandra fault as a retryable {@link TransientError}; else propagate unchanged. */\n private mapError(err: unknown): unknown {\n if (isTransient(err)) {\n return new TransientError(\n `transient Cassandra fault: ${(err as { name?: unknown } | null)?.name ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n }\n}\n"]}
|
package/dist/chunk-7LMLYSVJ.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// src/drivers/_shared/aws-errors.ts
|
|
2
|
-
function httpStatus(err) {
|
|
3
|
-
return err?.$metadata?.httpStatusCode;
|
|
4
|
-
}
|
|
5
|
-
function errorName(err) {
|
|
6
|
-
return err?.name;
|
|
7
|
-
}
|
|
8
|
-
function errorCode(err) {
|
|
9
|
-
return err?.code;
|
|
10
|
-
}
|
|
11
|
-
function isSdkRetryable(err) {
|
|
12
|
-
return err?.$retryable != null;
|
|
13
|
-
}
|
|
14
|
-
function isServerSide(err) {
|
|
15
|
-
const status = httpStatus(err);
|
|
16
|
-
return status !== void 0 && status >= 500 && status <= 599;
|
|
17
|
-
}
|
|
18
|
-
var NETWORK_NAMES = /* @__PURE__ */ new Set([
|
|
19
|
-
"TimeoutError",
|
|
20
|
-
"RequestTimeout",
|
|
21
|
-
"RequestTimeoutException",
|
|
22
|
-
"NetworkingError",
|
|
23
|
-
"AbortError"
|
|
24
|
-
]);
|
|
25
|
-
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
26
|
-
"ETIMEDOUT",
|
|
27
|
-
"ECONNRESET",
|
|
28
|
-
"ECONNREFUSED",
|
|
29
|
-
"EPIPE",
|
|
30
|
-
"ENOTFOUND",
|
|
31
|
-
"EAI_AGAIN",
|
|
32
|
-
"ECONNABORTED"
|
|
33
|
-
]);
|
|
34
|
-
var NETWORK_MESSAGE = /socket hang up|network (error|failure)|(connection|request|socket|operation|read|write)\s+tim(e|ed)\s?out|connection (reset|refused|aborted|closed)/i;
|
|
35
|
-
function isNetworkOrTimeout(err) {
|
|
36
|
-
if (NETWORK_NAMES.has(errorName(err) ?? "")) return true;
|
|
37
|
-
if (NETWORK_CODES.has(errorCode(err) ?? "")) return true;
|
|
38
|
-
return NETWORK_MESSAGE.test(err?.message ?? "");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export { errorName, httpStatus, isNetworkOrTimeout, isSdkRetryable, isServerSide };
|
|
42
|
-
//# sourceMappingURL=chunk-7LMLYSVJ.js.map
|
|
43
|
-
//# sourceMappingURL=chunk-7LMLYSVJ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/drivers/_shared/aws-errors.ts"],"names":[],"mappings":";AASO,SAAS,WAAW,GAAA,EAAkC;AAC3D,EAAA,OAAQ,KAA4D,SAAA,EAAW,cAAA;AACjF;AAEO,SAAS,UAAU,GAAA,EAAkC;AAC1D,EAAA,OAAQ,GAAA,EAAkC,IAAA;AAC5C;AAGO,SAAS,UAAU,GAAA,EAAkC;AAC1D,EAAA,OAAQ,GAAA,EAAkC,IAAA;AAC5C;AAGO,SAAS,eAAe,GAAA,EAAuB;AACpD,EAAA,OAAQ,KAAyC,UAAA,IAAc,IAAA;AACjE;AAGO,SAAS,aAAa,GAAA,EAAuB;AAClD,EAAA,MAAM,MAAA,GAAS,WAAW,GAAG,CAAA;AAC7B,EAAA,OAAO,MAAA,KAAW,MAAA,IAAa,MAAA,IAAU,GAAA,IAAO,MAAA,IAAU,GAAA;AAC5D;AAEA,IAAM,aAAA,uBAAoB,GAAA,CAAI;AAAA,EAC5B,cAAA;AAAA,EACA,gBAAA;AAAA,EACA,yBAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACF,CAAC,CAAA;AACD,IAAM,aAAA,uBAAoB,GAAA,CAAI;AAAA,EAC5B,WAAA;AAAA,EACA,YAAA;AAAA,EACA,cAAA;AAAA,EACA,OAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAC,CAAA;AAMD,IAAM,eAAA,GACJ,sJAAA;AAGK,SAAS,mBAAmB,GAAA,EAAuB;AACxD,EAAA,IAAI,cAAc,GAAA,CAAI,SAAA,CAAU,GAAG,CAAA,IAAK,EAAE,GAAG,OAAO,IAAA;AACpD,EAAA,IAAI,cAAc,GAAA,CAAI,SAAA,CAAU,GAAG,CAAA,IAAK,EAAE,GAAG,OAAO,IAAA;AACpD,EAAA,OAAO,eAAA,CAAgB,IAAA,CAAM,GAAA,EAAqC,OAAA,IAAW,EAAE,CAAA;AACjF","file":"chunk-7LMLYSVJ.js","sourcesContent":["/**\n * Shared, SDK-free helpers for classifying AWS-style errors (used by the S3 + DynamoDB drivers, Phase 4b).\n *\n * These only read structural shapes an AWS SDK v3 error carries — `name`, `$metadata.httpStatusCode`, a\n * lower-level `code`/`errno`, and the SDK's own `$retryable` marker — so the (subtle, easy-to-get-wrong)\n * transient-vs-fatal decision is unit-testable without a live backend or even the SDK installed. They live\n * under `drivers/_shared` (part of the SDK-free core bundle): a driver may import them; they import no SDK.\n */\n\nexport function httpStatus(err: unknown): number | undefined {\n return (err as { $metadata?: { httpStatusCode?: number } } | null)?.$metadata?.httpStatusCode;\n}\n\nexport function errorName(err: unknown): string | undefined {\n return (err as { name?: string } | null)?.name;\n}\n\n/** A lower-level transport code (e.g. `ECONNRESET`) — the Node networking layer sets `code`. */\nexport function errorCode(err: unknown): string | undefined {\n return (err as { code?: string } | null)?.code;\n}\n\n/** The AWS SDK v3 tags retryable errors with a `$retryable` object (throttling faults carry `.throttling`). */\nexport function isSdkRetryable(err: unknown): boolean {\n return (err as { $retryable?: unknown } | null)?.$retryable != null;\n}\n\n/** Any 5xx is a server-side fault that's safe to retry (the request didn't deterministically fail). */\nexport function isServerSide(err: unknown): boolean {\n const status = httpStatus(err);\n return status !== undefined && status >= 500 && status <= 599;\n}\n\nconst NETWORK_NAMES = new Set([\n 'TimeoutError',\n 'RequestTimeout',\n 'RequestTimeoutException',\n 'NetworkingError',\n 'AbortError',\n]);\nconst NETWORK_CODES = new Set([\n 'ETIMEDOUT',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'EPIPE',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'ECONNABORTED',\n]);\n\n// Message-text fallback for when the structural signals (name/code/$metadata/$retryable) are absent. Kept\n// SPECIFIC on purpose: a loose `/timed? ?out/` matches deterministic messages like \"value timed out of\n// range\" and would wrongly retry them, so we only match timeout/network phrases anchored to a transport word\n// (connection/request/socket/read/write) plus the unambiguous standalone phrases.\nconst NETWORK_MESSAGE =\n /socket hang up|network (error|failure)|(connection|request|socket|operation|read|write)\\s+tim(e|ed)\\s?out|connection (reset|refused|aborted|closed)/i;\n\n/** A dropped/timed-out connection — transient by nature; a retry on a fresh connection usually succeeds. */\nexport function isNetworkOrTimeout(err: unknown): boolean {\n if (NETWORK_NAMES.has(errorName(err) ?? '')) return true;\n if (NETWORK_CODES.has(errorCode(err) ?? '')) return true;\n return NETWORK_MESSAGE.test((err as { message?: string } | null)?.message ?? '');\n}\n"]}
|
package/dist/chunk-L54FL5DW.js
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { IntegrityError, UnsupportedError, ValidationError } from './chunk-NUIDEEFZ.js';
|
|
2
|
-
|
|
3
|
-
// src/drivers/_shared/registry.ts
|
|
4
|
-
var STATUSES = ["active", "compacting", "erasing", "destroyed"];
|
|
5
|
-
var MAX_GOVERNANCE_BYTES = 64 * 1024;
|
|
6
|
-
var MAX_WRAPPED_DEKS = 8;
|
|
7
|
-
var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
|
|
8
|
-
function validateGeneration(gen) {
|
|
9
|
-
if (!Number.isInteger(gen) || gen < 0) {
|
|
10
|
-
throw new ValidationError(`currentGen must be a non-negative integer; got ${gen}`);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function validateCount(count, field = "dirtyChunkCount") {
|
|
14
|
-
if (!Number.isInteger(count) || count < 0) {
|
|
15
|
-
throw new ValidationError(`${field} must be a non-negative integer; got ${count}`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
function validateTimestamp(ms, field) {
|
|
19
|
-
if (!Number.isFinite(ms) || ms < 0) {
|
|
20
|
-
throw new ValidationError(`${field} must be a non-negative finite number; got ${ms}`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function validateStatus(status) {
|
|
24
|
-
if (!STATUSES.includes(status)) {
|
|
25
|
-
throw new ValidationError(`status must be one of ${STATUSES.join("/")}; got ${status}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function validateGovernance(meta, field) {
|
|
29
|
-
if (meta === void 0) return;
|
|
30
|
-
let json;
|
|
31
|
-
try {
|
|
32
|
-
json = JSON.stringify(meta);
|
|
33
|
-
} catch {
|
|
34
|
-
throw new ValidationError(`${field} must be JSON-serializable`);
|
|
35
|
-
}
|
|
36
|
-
if (json.length > MAX_GOVERNANCE_BYTES) {
|
|
37
|
-
throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function validateWrappedDeks(value, isStored) {
|
|
41
|
-
if (value === void 0) return;
|
|
42
|
-
const fail = (msg) => {
|
|
43
|
-
throw isStored ? new IntegrityError(msg) : new ValidationError(msg);
|
|
44
|
-
};
|
|
45
|
-
if (!Array.isArray(value)) fail("wrappedDeks must be an array");
|
|
46
|
-
const list = value;
|
|
47
|
-
if (list.length === 0) fail("wrappedDeks must be non-empty when present");
|
|
48
|
-
if (list.length > MAX_WRAPPED_DEKS)
|
|
49
|
-
fail(`wrappedDeks has ${list.length} entries, cap ${MAX_WRAPPED_DEKS}`);
|
|
50
|
-
for (const w of list) {
|
|
51
|
-
if (w === null || typeof w !== "object") fail("wrappedDeks entry must be an object");
|
|
52
|
-
const { keyId, wrapped } = w;
|
|
53
|
-
if (typeof keyId !== "string" || keyId.length === 0)
|
|
54
|
-
fail("wrappedDeks entry needs a non-empty keyId");
|
|
55
|
-
if (typeof wrapped !== "string" || wrapped.length === 0)
|
|
56
|
-
fail("wrappedDeks entry needs a non-empty wrapped blob");
|
|
57
|
-
if (wrapped.length > MAX_WRAPPED_DEK_BYTES)
|
|
58
|
-
fail(`wrappedDeks entry is ${wrapped.length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function validateNewRegistryRecord(rec) {
|
|
62
|
-
validateGeneration(rec.currentGen);
|
|
63
|
-
if (rec.dirtyChunkCount !== void 0) validateCount(rec.dirtyChunkCount);
|
|
64
|
-
if (rec.status !== void 0) validateStatus(rec.status);
|
|
65
|
-
validateWrappedDeks(rec.wrappedDeks, false);
|
|
66
|
-
validateGovernance(rec.retention, "retention");
|
|
67
|
-
validateGovernance(rec.residency, "residency");
|
|
68
|
-
}
|
|
69
|
-
function validateRegistryPatch(patch) {
|
|
70
|
-
if (patch.currentGen !== void 0) validateGeneration(patch.currentGen);
|
|
71
|
-
if (patch.dirtyChunkCount !== void 0) validateCount(patch.dirtyChunkCount);
|
|
72
|
-
if (patch.consecutiveFailures !== void 0)
|
|
73
|
-
validateCount(patch.consecutiveFailures, "consecutiveFailures");
|
|
74
|
-
if (patch.lastCompactedAt !== void 0)
|
|
75
|
-
validateTimestamp(patch.lastCompactedAt, "lastCompactedAt");
|
|
76
|
-
if (patch.status !== void 0) validateStatus(patch.status);
|
|
77
|
-
if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, false);
|
|
78
|
-
if ("retention" in patch) validateGovernance(patch.retention, "retention");
|
|
79
|
-
if ("residency" in patch) validateGovernance(patch.residency, "residency");
|
|
80
|
-
}
|
|
81
|
-
var REGISTRY_SCHEMA_VERSION = 1;
|
|
82
|
-
function assertRegistrySchemaVersion(raw, ctx) {
|
|
83
|
-
if (raw === void 0) return;
|
|
84
|
-
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
|
|
85
|
-
throw new IntegrityError(`registry row has a malformed schemaVersion (${String(raw)}): ${ctx}`);
|
|
86
|
-
}
|
|
87
|
-
if (raw > REGISTRY_SCHEMA_VERSION) {
|
|
88
|
-
throw new UnsupportedError(
|
|
89
|
-
`registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function serializeRegistryEnvelope(env) {
|
|
94
|
-
return JSON.stringify({
|
|
95
|
-
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
96
|
-
deleted: env.deleted,
|
|
97
|
-
record: env.record
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
function registryCounterOf(record) {
|
|
101
|
-
if (!/^(0|[1-9]\d*)$/.test(record.token)) {
|
|
102
|
-
throw new IntegrityError(`registry row token is not a canonical counter: ${record.token}`);
|
|
103
|
-
}
|
|
104
|
-
const n = Number(record.token);
|
|
105
|
-
if (!Number.isSafeInteger(n)) {
|
|
106
|
-
throw new IntegrityError(`registry row token is out of safe-integer range: ${record.token}`);
|
|
107
|
-
}
|
|
108
|
-
return n;
|
|
109
|
-
}
|
|
110
|
-
function parseRegistryEnvelope(text, ctx) {
|
|
111
|
-
let parsed;
|
|
112
|
-
try {
|
|
113
|
-
parsed = JSON.parse(text);
|
|
114
|
-
} catch {
|
|
115
|
-
throw new IntegrityError(`registry row is not valid JSON: ${ctx}`);
|
|
116
|
-
}
|
|
117
|
-
if (parsed === null || typeof parsed !== "object") {
|
|
118
|
-
throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
|
|
119
|
-
}
|
|
120
|
-
const env = parsed;
|
|
121
|
-
assertRegistrySchemaVersion(env.schemaVersion, ctx);
|
|
122
|
-
if (typeof env.deleted !== "boolean" || env.record === null || typeof env.record !== "object") {
|
|
123
|
-
throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
|
|
124
|
-
}
|
|
125
|
-
const r = env.record;
|
|
126
|
-
assertStoredRecordShape(r, ctx);
|
|
127
|
-
if (typeof r.token !== "string") {
|
|
128
|
-
throw new IntegrityError(`registry row is missing its token: ${ctx}`);
|
|
129
|
-
}
|
|
130
|
-
return { deleted: env.deleted, record: env.record };
|
|
131
|
-
}
|
|
132
|
-
function recordFromNew(ref, rec, now, token) {
|
|
133
|
-
return {
|
|
134
|
-
namespace: ref.namespace,
|
|
135
|
-
segment: ref.segment,
|
|
136
|
-
currentGen: rec.currentGen,
|
|
137
|
-
wrappedDeks: rec.wrappedDeks,
|
|
138
|
-
keyId: rec.keyId,
|
|
139
|
-
dirtyChunkCount: rec.dirtyChunkCount ?? 0,
|
|
140
|
-
status: rec.status ?? "active",
|
|
141
|
-
consecutiveFailures: 0,
|
|
142
|
-
// daemon health (Phase D); lastCompactedAt stays absent until first compaction
|
|
143
|
-
retention: rec.retention,
|
|
144
|
-
residency: rec.residency,
|
|
145
|
-
createdAt: now,
|
|
146
|
-
updatedAt: now,
|
|
147
|
-
token
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function assertStoredRecordShape(r, ctx) {
|
|
151
|
-
if (typeof r.segment !== "string" || r.namespace !== void 0 && typeof r.namespace !== "string" || typeof r.currentGen !== "number" || typeof r.dirtyChunkCount !== "number" || typeof r.status !== "string" || typeof r.createdAt !== "number" || typeof r.updatedAt !== "number") {
|
|
152
|
-
throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
|
|
153
|
-
}
|
|
154
|
-
if (!Number.isInteger(r.currentGen) || r.currentGen < 0) {
|
|
155
|
-
throw new IntegrityError(`registry record has an invalid currentGen (${r.currentGen}): ${ctx}`);
|
|
156
|
-
}
|
|
157
|
-
if (!Number.isInteger(r.dirtyChunkCount) || r.dirtyChunkCount < 0) {
|
|
158
|
-
throw new IntegrityError(`registry record has an invalid dirtyChunkCount: ${ctx}`);
|
|
159
|
-
}
|
|
160
|
-
if (!STATUSES.includes(r.status)) {
|
|
161
|
-
throw new IntegrityError(`registry record has an unknown status (${r.status}): ${ctx}`);
|
|
162
|
-
}
|
|
163
|
-
if (r.leaseOwner !== void 0 && typeof r.leaseOwner !== "string") {
|
|
164
|
-
throw new IntegrityError(`registry record has an invalid leaseOwner: ${ctx}`);
|
|
165
|
-
}
|
|
166
|
-
if (r.leaseExpiresAt !== void 0 && typeof r.leaseExpiresAt !== "number") {
|
|
167
|
-
throw new IntegrityError(`registry record has an invalid leaseExpiresAt: ${ctx}`);
|
|
168
|
-
}
|
|
169
|
-
if (r.consecutiveFailures !== void 0 && (!Number.isInteger(r.consecutiveFailures) || r.consecutiveFailures < 0)) {
|
|
170
|
-
throw new IntegrityError(`registry record has an invalid consecutiveFailures: ${ctx}`);
|
|
171
|
-
}
|
|
172
|
-
if (r.lastCompactedAt !== void 0 && (typeof r.lastCompactedAt !== "number" || !Number.isFinite(r.lastCompactedAt) || r.lastCompactedAt < 0)) {
|
|
173
|
-
throw new IntegrityError(`registry record has an invalid lastCompactedAt: ${ctx}`);
|
|
174
|
-
}
|
|
175
|
-
if (r.keyId !== void 0 && typeof r.keyId !== "string") {
|
|
176
|
-
throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);
|
|
177
|
-
}
|
|
178
|
-
validateWrappedDeks(r.wrappedDeks, true);
|
|
179
|
-
}
|
|
180
|
-
function applyRegistryPatch(prev, patch, now, token) {
|
|
181
|
-
return {
|
|
182
|
-
namespace: prev.namespace,
|
|
183
|
-
segment: prev.segment,
|
|
184
|
-
currentGen: patch.currentGen ?? prev.currentGen,
|
|
185
|
-
wrappedDeks: "wrappedDeks" in patch ? patch.wrappedDeks : prev.wrappedDeks,
|
|
186
|
-
keyId: "keyId" in patch ? patch.keyId : prev.keyId,
|
|
187
|
-
dirtyChunkCount: patch.dirtyChunkCount ?? prev.dirtyChunkCount,
|
|
188
|
-
status: patch.status ?? prev.status,
|
|
189
|
-
leaseOwner: "leaseOwner" in patch ? patch.leaseOwner : prev.leaseOwner,
|
|
190
|
-
leaseExpiresAt: "leaseExpiresAt" in patch ? patch.leaseExpiresAt : prev.leaseExpiresAt,
|
|
191
|
-
lastCompactedAt: "lastCompactedAt" in patch ? patch.lastCompactedAt : prev.lastCompactedAt,
|
|
192
|
-
consecutiveFailures: patch.consecutiveFailures ?? prev.consecutiveFailures ?? 0,
|
|
193
|
-
retention: "retention" in patch ? patch.retention : prev.retention,
|
|
194
|
-
residency: "residency" in patch ? patch.residency : prev.residency,
|
|
195
|
-
createdAt: prev.createdAt,
|
|
196
|
-
updatedAt: now,
|
|
197
|
-
token
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export { REGISTRY_SCHEMA_VERSION, applyRegistryPatch, assertRegistrySchemaVersion, assertStoredRecordShape, parseRegistryEnvelope, recordFromNew, registryCounterOf, serializeRegistryEnvelope, validateNewRegistryRecord, validateRegistryPatch };
|
|
202
|
-
//# sourceMappingURL=chunk-L54FL5DW.js.map
|
|
203
|
-
//# sourceMappingURL=chunk-L54FL5DW.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/drivers/_shared/registry.ts"],"names":[],"mappings":"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","file":"chunk-L54FL5DW.js","sourcesContent":["/**\n * Shared, SDK-free helpers for assembling + validating {@link RegistryRecord}s (Phase 4c).\n *\n * The registry's record-construction, patch-application, and field-validation are identical across every\n * backend (memory / LocalFs / DynamoDB) — only the *storage* + OCC mechanics differ. This is the one home\n * for that logic so the three drivers can't drift. Pure: no I/O, no SDK, no clock (the caller passes `now`).\n */\nimport { IntegrityError, UnsupportedError, ValidationError } from '@/core/errors';\nimport type {\n NewRegistryRecord,\n RegistryPatch,\n RegistryRecord,\n SegmentRef,\n Token,\n} from '@/core/ports';\n\n/** The valid {@link RegistryStatus} values — used to validate both caller input and stored bytes. */\nconst STATUSES: readonly string[] = ['active', 'compacting', 'erasing', 'destroyed'];\n/** Cap on a serialized governance blob (retention/residency) — bounds row size so a row can't be bricked. */\nconst MAX_GOVERNANCE_BYTES = 64 * 1024;\n/** Bounds on the wrapped-DEK list (one DEK wrapped under a few KEKs) — keeps the row small + rejects abuse. */\nconst MAX_WRAPPED_DEKS = 8;\nconst MAX_WRAPPED_DEK_BYTES = 4 * 1024;\n\nfunction validateGeneration(gen: number): void {\n if (!Number.isInteger(gen) || gen < 0) {\n throw new ValidationError(`currentGen must be a non-negative integer; got ${gen}`);\n }\n}\n\nfunction validateCount(count: number, field = 'dirtyChunkCount'): void {\n if (!Number.isInteger(count) || count < 0) {\n throw new ValidationError(`${field} must be a non-negative integer; got ${count}`);\n }\n}\n\n/** Validate an epoch-ms timestamp (non-negative, finite). */\nfunction validateTimestamp(ms: number, field: string): void {\n if (!Number.isFinite(ms) || ms < 0) {\n throw new ValidationError(`${field} must be a non-negative finite number; got ${ms}`);\n }\n}\n\nfunction validateStatus(status: string): void {\n if (!STATUSES.includes(status)) {\n throw new ValidationError(`status must be one of ${STATUSES.join('/')}; got ${status}`);\n }\n}\n\n/**\n * Validate a governance blob (retention/residency) at the write boundary: it must be JSON-serializable\n * (a `BigInt`/function/circular value fails fast with a typed error instead of a cryptic one deep in a\n * driver) and within a size cap (so an oversized blob can't write a row that's later unreadable — a brick).\n */\nfunction validateGovernance(meta: unknown, field: string): void {\n if (meta === undefined) return;\n let json: string;\n try {\n json = JSON.stringify(meta);\n } catch {\n throw new ValidationError(`${field} must be JSON-serializable`);\n }\n if (json.length > MAX_GOVERNANCE_BYTES) {\n throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);\n }\n}\n\n/**\n * Validate the wrapped-DEK list at the write/read boundary: a bounded array of `{ keyId, wrapped }` with\n * non-empty string fields and a per-entry size cap (so a row can't be bricked, and corrupt bytes are rejected\n * rather than reaching the keystore). `isStored` toggles the error class (write = ValidationError; read =\n * IntegrityError, invariant 5).\n */\nfunction validateWrappedDeks(value: unknown, isStored: boolean): void {\n if (value === undefined) return;\n const fail = (msg: string): never => {\n throw isStored ? new IntegrityError(msg) : new ValidationError(msg);\n };\n if (!Array.isArray(value)) fail('wrappedDeks must be an array');\n const list = value as unknown[];\n if (list.length === 0) fail('wrappedDeks must be non-empty when present');\n if (list.length > MAX_WRAPPED_DEKS)\n fail(`wrappedDeks has ${list.length} entries, cap ${MAX_WRAPPED_DEKS}`);\n for (const w of list) {\n if (w === null || typeof w !== 'object') fail('wrappedDeks entry must be an object');\n const { keyId, wrapped } = w as { keyId?: unknown; wrapped?: unknown };\n if (typeof keyId !== 'string' || keyId.length === 0)\n fail('wrappedDeks entry needs a non-empty keyId');\n if (typeof wrapped !== 'string' || wrapped.length === 0)\n fail('wrappedDeks entry needs a non-empty wrapped blob');\n if ((wrapped as string).length > MAX_WRAPPED_DEK_BYTES)\n fail(`wrappedDeks entry is ${(wrapped as string).length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);\n }\n}\n\n/** Validate the caller-settable fields at `create`. */\nexport function validateNewRegistryRecord(rec: NewRegistryRecord): void {\n validateGeneration(rec.currentGen);\n if (rec.dirtyChunkCount !== undefined) validateCount(rec.dirtyChunkCount);\n if (rec.status !== undefined) validateStatus(rec.status);\n validateWrappedDeks(rec.wrappedDeks, false);\n validateGovernance(rec.retention, 'retention');\n validateGovernance(rec.residency, 'residency');\n}\n\n/** Validate the caller-settable fields in a `compareAndSwap` patch. */\nexport function validateRegistryPatch(patch: RegistryPatch): void {\n if (patch.currentGen !== undefined) validateGeneration(patch.currentGen);\n if (patch.dirtyChunkCount !== undefined) validateCount(patch.dirtyChunkCount);\n if (patch.consecutiveFailures !== undefined)\n validateCount(patch.consecutiveFailures, 'consecutiveFailures');\n if (patch.lastCompactedAt !== undefined)\n validateTimestamp(patch.lastCompactedAt, 'lastCompactedAt');\n if (patch.status !== undefined) validateStatus(patch.status);\n if ('wrappedDeks' in patch) validateWrappedDeks(patch.wrappedDeks, false);\n if ('retention' in patch) validateGovernance(patch.retention, 'retention');\n if ('residency' in patch) validateGovernance(patch.residency, 'residency');\n}\n\n/**\n * The persisted envelope for a registry row: the record plus a tombstone flag. A **deleted** row keeps its\n * record (with an advanced counter) rather than being removed, so the monotonic token survives a\n * delete→recreate — ABA-safety. Shared by the persistent drivers (LocalFs file, S3 object).\n *\n * `schemaVersion` is a wire-only concern — it stamps the persisted bytes, not the in-memory domain object —\n * so it lives on the serialize/parse boundary ({@link serializeRegistryEnvelope} /\n * {@link parseRegistryEnvelope}), never on this interface.\n */\nexport interface RegistryEnvelope {\n readonly deleted: boolean;\n readonly record: RegistryRecord;\n}\n\n/**\n * Current registry-row schema version — a pre-1.0 format-freeze prerequisite (Phase G). Every persistent\n * registry driver — the LocalFs/S3 `{ deleted, record }` envelope and the DynamoDB body — stamps its rows\n * with this so a reader can fail-closed on a future, incompatible layout instead of misparsing it. Bump only\n * on a backward-incompatible change. Policy: a **higher** stamp than this build knows → `UnsupportedError`\n * (fail-closed); an **absent** stamp → legacy pre-freeze row, tolerated as v1.\n */\nexport const REGISTRY_SCHEMA_VERSION = 1;\n\n/**\n * Validate a persisted registry row's `schemaVersion` (untrusted bytes, invariant 5): absent → legacy v1\n * (tolerated); a malformed value → `IntegrityError`; a version newer than this build → `UnsupportedError`\n * (fail-closed rather than misread a format we don't understand).\n */\nexport function assertRegistrySchemaVersion(raw: unknown, ctx: string): void {\n if (raw === undefined) return; // pre-stamp rows are v1\n if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {\n throw new IntegrityError(`registry row has a malformed schemaVersion (${String(raw)}): ${ctx}`);\n }\n if (raw > REGISTRY_SCHEMA_VERSION) {\n throw new UnsupportedError(\n `registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`,\n );\n }\n}\n\n/** Serialize a registry envelope for persistence, stamping the current {@link REGISTRY_SCHEMA_VERSION}. */\nexport function serializeRegistryEnvelope(env: RegistryEnvelope): string {\n return JSON.stringify({\n schemaVersion: REGISTRY_SCHEMA_VERSION,\n deleted: env.deleted,\n record: env.record,\n });\n}\n\n/**\n * The OCC counter encoded in a record's token. A published row always has a **canonical decimal** token\n * (`String(counter)`), so anything else (`\"1e3\"`, `\"0x10\"`, `\" 5 \"`, `\"\"`, a >2^53 value) is corruption /\n * tampering — reject it (invariant 5) rather than let `Number()` coerce it and silently break monotonicity.\n */\nexport function registryCounterOf(record: RegistryRecord): number {\n if (!/^(0|[1-9]\\d*)$/.test(record.token)) {\n throw new IntegrityError(`registry row token is not a canonical counter: ${record.token}`);\n }\n const n = Number(record.token);\n if (!Number.isSafeInteger(n)) {\n throw new IntegrityError(`registry row token is out of safe-integer range: ${record.token}`);\n }\n return n;\n}\n\n/**\n * Parse + structurally validate a persisted `{ deleted, record }` envelope from stored bytes. A published row\n * is always whole (atomic write), so a parse failure or a missing/mistyped field means corruption/tampering —\n * fail fast (invariant 5), never silently report \"absent\". `ctx` names the source (path/key) for the message.\n */\nexport function parseRegistryEnvelope(text: string, ctx: string): RegistryEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new IntegrityError(`registry row is not valid JSON: ${ctx}`);\n }\n // Guard before any property access: `JSON.parse` yields `null`/primitives for hostile bytes (e.g. `\"null\"`,\n // `\"5\"`), which would throw an *untyped* TypeError on the dereference below (invariant 5 wants a typed error).\n if (parsed === null || typeof parsed !== 'object') {\n throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);\n }\n const env = parsed as { schemaVersion?: unknown; deleted?: unknown; record?: unknown };\n assertRegistrySchemaVersion(env.schemaVersion, ctx);\n if (typeof env.deleted !== 'boolean' || env.record === null || typeof env.record !== 'object') {\n throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);\n }\n const r = env.record as Record<string, unknown>;\n assertStoredRecordShape(r, ctx);\n if (typeof r.token !== 'string') {\n throw new IntegrityError(`registry row is missing its token: ${ctx}`);\n }\n return { deleted: env.deleted, record: env.record as RegistryRecord };\n}\n\n/** Build a full record from a {@link NewRegistryRecord} plus identity, audit timestamps, and an OCC token. */\nexport function recordFromNew(\n ref: SegmentRef,\n rec: NewRegistryRecord,\n now: number,\n token: Token,\n): RegistryRecord {\n return {\n namespace: ref.namespace,\n segment: ref.segment,\n currentGen: rec.currentGen,\n wrappedDeks: rec.wrappedDeks,\n keyId: rec.keyId,\n dirtyChunkCount: rec.dirtyChunkCount ?? 0,\n status: rec.status ?? 'active',\n consecutiveFailures: 0, // daemon health (Phase D); lastCompactedAt stays absent until first compaction\n retention: rec.retention,\n residency: rec.residency,\n createdAt: now,\n updatedAt: now,\n token,\n };\n}\n\n/**\n * Fail-fast structural check on a record read back from a persistent tier (untrusted bytes, invariant 5):\n * a published row is always whole, so a missing/mistyped required field means corruption/tampering — reject\n * it rather than silently report \"absent\". `token` is checked by the caller (its source differs per backend:\n * an in-body field vs a derived OCC counter).\n */\nexport function assertStoredRecordShape(r: Record<string, unknown>, ctx: string): void {\n if (\n typeof r.segment !== 'string' ||\n (r.namespace !== undefined && typeof r.namespace !== 'string') ||\n typeof r.currentGen !== 'number' ||\n typeof r.dirtyChunkCount !== 'number' ||\n typeof r.status !== 'string' ||\n typeof r.createdAt !== 'number' ||\n typeof r.updatedAt !== 'number'\n ) {\n throw new IntegrityError(`registry record is missing required fields: ${ctx}`);\n }\n // Enforce the same value invariants the write path checks, so corrupt/tampered bytes are rejected at the\n // read boundary (invariant 5) rather than leaking a bad currentGen/status downstream.\n if (!Number.isInteger(r.currentGen) || r.currentGen < 0) {\n throw new IntegrityError(`registry record has an invalid currentGen (${r.currentGen}): ${ctx}`);\n }\n if (!Number.isInteger(r.dirtyChunkCount) || r.dirtyChunkCount < 0) {\n throw new IntegrityError(`registry record has an invalid dirtyChunkCount: ${ctx}`);\n }\n if (!STATUSES.includes(r.status)) {\n throw new IntegrityError(`registry record has an unknown status (${r.status}): ${ctx}`);\n }\n if (r.leaseOwner !== undefined && typeof r.leaseOwner !== 'string') {\n throw new IntegrityError(`registry record has an invalid leaseOwner: ${ctx}`);\n }\n if (r.leaseExpiresAt !== undefined && typeof r.leaseExpiresAt !== 'number') {\n throw new IntegrityError(`registry record has an invalid leaseExpiresAt: ${ctx}`);\n }\n if (\n r.consecutiveFailures !== undefined &&\n (!Number.isInteger(r.consecutiveFailures) || (r.consecutiveFailures as number) < 0)\n ) {\n throw new IntegrityError(`registry record has an invalid consecutiveFailures: ${ctx}`);\n }\n if (\n r.lastCompactedAt !== undefined &&\n (typeof r.lastCompactedAt !== 'number' ||\n !Number.isFinite(r.lastCompactedAt) ||\n (r.lastCompactedAt as number) < 0)\n ) {\n throw new IntegrityError(`registry record has an invalid lastCompactedAt: ${ctx}`);\n }\n if (r.keyId !== undefined && typeof r.keyId !== 'string') {\n throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);\n }\n validateWrappedDeks(r.wrappedDeks, true); // invariant 5: reject a corrupt wrapped-DEK list on read-back\n}\n\n/**\n * Apply a patch to an existing record, returning a new one with a fresh `updatedAt` + `token` (identity and\n * `createdAt` are preserved). Optional fields use `'k' in patch` so a patch can *clear* them (set to\n * `undefined`, e.g. dropping `keyId` on crypto-shred); required fields use `??` (they always have a value).\n */\nexport function applyRegistryPatch(\n prev: RegistryRecord,\n patch: RegistryPatch,\n now: number,\n token: Token,\n): RegistryRecord {\n return {\n namespace: prev.namespace,\n segment: prev.segment,\n currentGen: patch.currentGen ?? prev.currentGen,\n wrappedDeks: 'wrappedDeks' in patch ? patch.wrappedDeks : prev.wrappedDeks,\n keyId: 'keyId' in patch ? patch.keyId : prev.keyId,\n dirtyChunkCount: patch.dirtyChunkCount ?? prev.dirtyChunkCount,\n status: patch.status ?? prev.status,\n leaseOwner: 'leaseOwner' in patch ? patch.leaseOwner : prev.leaseOwner,\n leaseExpiresAt: 'leaseExpiresAt' in patch ? patch.leaseExpiresAt : prev.leaseExpiresAt,\n lastCompactedAt: 'lastCompactedAt' in patch ? patch.lastCompactedAt : prev.lastCompactedAt,\n consecutiveFailures: patch.consecutiveFailures ?? prev.consecutiveFailures ?? 0,\n retention: 'retention' in patch ? patch.retention : prev.retention,\n residency: 'residency' in patch ? patch.residency : prev.residency,\n createdAt: prev.createdAt,\n updatedAt: now,\n token,\n };\n}\n"]}
|