@cloudbitmaps/core 0.9.0 → 0.11.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.
Files changed (132) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +33 -16
  3. package/dist/chunk-5OMC5U7U.js +508 -0
  4. package/dist/chunk-5OMC5U7U.js.map +7 -0
  5. package/dist/core/audit.d.ts +128 -0
  6. package/dist/core/bit-route.d.ts +21 -0
  7. package/dist/core/blob.d.ts +34 -0
  8. package/dist/core/budget.d.ts +68 -0
  9. package/dist/core/codec.d.ts +122 -0
  10. package/dist/core/concurrency.d.ts +15 -0
  11. package/dist/core/consistency.d.ts +73 -0
  12. package/dist/core/cooperative.d.ts +78 -0
  13. package/dist/core/cost.d.ts +254 -0
  14. package/dist/core/crbm/crc32c.d.ts +10 -0
  15. package/dist/core/crbm/format.d.ts +114 -0
  16. package/dist/core/crbm/reader.d.ts +86 -0
  17. package/dist/core/crbm/varint.d.ts +13 -0
  18. package/dist/core/crbm/writer.d.ts +39 -0
  19. package/dist/core/crbm-storage-source.d.ts +522 -0
  20. package/dist/core/crypto.d.ts +80 -0
  21. package/dist/core/determinism.d.ts +42 -0
  22. package/dist/core/discover.d.ts +77 -0
  23. package/dist/core/due-index.d.ts +53 -0
  24. package/dist/core/engine.d.ts +246 -0
  25. package/dist/core/erase-id.d.ts +176 -0
  26. package/dist/core/erasure.d.ts +185 -0
  27. package/dist/core/errors.d.ts +113 -0
  28. package/dist/core/generation-gc.d.ts +56 -0
  29. package/dist/core/keys.d.ts +23 -0
  30. package/dist/core/load.d.ts +121 -0
  31. package/dist/core/lru.d.ts +69 -0
  32. package/dist/core/metrics.d.ts +136 -0
  33. package/dist/core/name-codec.d.ts +69 -0
  34. package/dist/core/pinned-storage-source.d.ts +90 -0
  35. package/dist/core/ports.d.ts +423 -0
  36. package/dist/core/reader-defaults.d.ts +18 -0
  37. package/dist/core/registry-scan.d.ts +47 -0
  38. package/dist/core/retention-sweep.d.ts +198 -0
  39. package/dist/core/retention.d.ts +67 -0
  40. package/dist/core/retry.d.ts +69 -0
  41. package/dist/core/rollback.d.ts +75 -0
  42. package/dist/core/validate.d.ts +44 -0
  43. package/dist/driver-kit.d.ts +9 -0
  44. package/dist/driver-kit.js +275 -0
  45. package/dist/driver-kit.js.map +7 -0
  46. package/dist/drivers/_shared/keys.d.ts +26 -0
  47. package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
  48. package/dist/drivers/_shared/object-registry.d.ts +89 -0
  49. package/dist/drivers/_shared/registry.d.ts +66 -0
  50. package/dist/drivers/backends.d.ts +43 -0
  51. package/dist/drivers/crypto.d.ts +38 -0
  52. package/dist/drivers/localfs/fs-util.d.ts +21 -0
  53. package/dist/drivers/localfs/paths.d.ts +33 -0
  54. package/dist/drivers/localfs/registry.d.ts +28 -0
  55. package/dist/drivers/localfs/storage.d.ts +19 -0
  56. package/dist/drivers/memory.d.ts +49 -0
  57. package/dist/drivers/retry/retrying-chunk-source.d.ts +59 -0
  58. package/dist/export/index.d.ts +93 -0
  59. package/dist/index.d.ts +76 -2760
  60. package/dist/index.js +3527 -3133
  61. package/dist/index.js.map +7 -1
  62. package/dist/testing/fuzz-core.d.ts +14 -0
  63. package/package.json +19 -115
  64. package/dist/azure/index.cjs +0 -368
  65. package/dist/azure/index.cjs.map +0 -1
  66. package/dist/azure/index.d.cts +0 -48
  67. package/dist/azure/index.d.ts +0 -48
  68. package/dist/azure/index.js +0 -304
  69. package/dist/azure/index.js.map +0 -1
  70. package/dist/cassandra/index.cjs +0 -265
  71. package/dist/cassandra/index.cjs.map +0 -1
  72. package/dist/cassandra/index.d.cts +0 -52
  73. package/dist/cassandra/index.d.ts +0 -52
  74. package/dist/cassandra/index.js +0 -204
  75. package/dist/cassandra/index.js.map +0 -1
  76. package/dist/chunk-7LMLYSVJ.js +0 -43
  77. package/dist/chunk-7LMLYSVJ.js.map +0 -1
  78. package/dist/chunk-BD22WDAN.js +0 -230
  79. package/dist/chunk-BD22WDAN.js.map +0 -1
  80. package/dist/chunk-NUIDEEFZ.js +0 -91
  81. package/dist/chunk-NUIDEEFZ.js.map +0 -1
  82. package/dist/chunk-OZ6LAFV4.js +0 -6
  83. package/dist/chunk-OZ6LAFV4.js.map +0 -1
  84. package/dist/chunk-SNJVZ227.js +0 -35
  85. package/dist/chunk-SNJVZ227.js.map +0 -1
  86. package/dist/dynamodb/index.cjs +0 -767
  87. package/dist/dynamodb/index.cjs.map +0 -1
  88. package/dist/dynamodb/index.d.cts +0 -106
  89. package/dist/dynamodb/index.d.ts +0 -106
  90. package/dist/dynamodb/index.js +0 -469
  91. package/dist/dynamodb/index.js.map +0 -1
  92. package/dist/gcs/index.cjs +0 -343
  93. package/dist/gcs/index.cjs.map +0 -1
  94. package/dist/gcs/index.d.cts +0 -46
  95. package/dist/gcs/index.d.ts +0 -46
  96. package/dist/gcs/index.js +0 -279
  97. package/dist/gcs/index.js.map +0 -1
  98. package/dist/index.cjs +0 -4990
  99. package/dist/index.cjs.map +0 -1
  100. package/dist/index.d.cts +0 -2761
  101. package/dist/mongodb/index.cjs +0 -260
  102. package/dist/mongodb/index.cjs.map +0 -1
  103. package/dist/mongodb/index.d.cts +0 -45
  104. package/dist/mongodb/index.d.ts +0 -45
  105. package/dist/mongodb/index.js +0 -199
  106. package/dist/mongodb/index.js.map +0 -1
  107. package/dist/mysql/index.cjs +0 -281
  108. package/dist/mysql/index.cjs.map +0 -1
  109. package/dist/mysql/index.d.cts +0 -56
  110. package/dist/mysql/index.d.ts +0 -56
  111. package/dist/mysql/index.js +0 -211
  112. package/dist/mysql/index.js.map +0 -1
  113. package/dist/ports-CvcvJWf6.d.cts +0 -395
  114. package/dist/ports-CvcvJWf6.d.ts +0 -395
  115. package/dist/postgres/index.cjs +0 -273
  116. package/dist/postgres/index.cjs.map +0 -1
  117. package/dist/postgres/index.d.cts +0 -54
  118. package/dist/postgres/index.d.ts +0 -54
  119. package/dist/postgres/index.js +0 -203
  120. package/dist/postgres/index.js.map +0 -1
  121. package/dist/redis/index.cjs +0 -257
  122. package/dist/redis/index.cjs.map +0 -1
  123. package/dist/redis/index.d.cts +0 -42
  124. package/dist/redis/index.d.ts +0 -42
  125. package/dist/redis/index.js +0 -197
  126. package/dist/redis/index.js.map +0 -1
  127. package/dist/s3/index.cjs +0 -918
  128. package/dist/s3/index.cjs.map +0 -1
  129. package/dist/s3/index.d.cts +0 -112
  130. package/dist/s3/index.d.ts +0 -112
  131. package/dist/s3/index.js +0 -555
  132. package/dist/s3/index.js.map +0 -1
@@ -1,767 +0,0 @@
1
- 'use strict';
2
-
3
- var clientDynamodb = require('@aws-sdk/client-dynamodb');
4
-
5
- // src/drivers/dynamodb/warm.ts
6
-
7
- // src/core/errors.ts
8
- var ERROR_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error");
9
- var TRANSIENT_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error.transient");
10
- var CloudRoaringError = class extends Error {
11
- /** Cross-bundle brand — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ not in JSON). */
12
- [ERROR_BRAND] = true;
13
- constructor(message) {
14
- super(message);
15
- this.name = new.target.name;
16
- }
17
- };
18
- var ValidationError = class extends CloudRoaringError {
19
- };
20
- var WriteConflictError = class extends CloudRoaringError {
21
- };
22
- var IntegrityError = class extends CloudRoaringError {
23
- };
24
- var UnsupportedError = class extends CloudRoaringError {
25
- };
26
- var TransientError = class extends CloudRoaringError {
27
- /** A second brand so the whole transient subtree (incl. {@link TimeoutError}) is classifiable cross-bundle. */
28
- [TRANSIENT_BRAND] = true;
29
- constructor(message, options) {
30
- super(message);
31
- if (options && "cause" in options) this.cause = options.cause;
32
- }
33
- };
34
- function hasBrand(err, brand) {
35
- return typeof err === "object" && err !== null && err[brand] === true;
36
- }
37
- function isCloudRoaringError(err) {
38
- return hasBrand(err, ERROR_BRAND);
39
- }
40
- function isIntegrityError(err) {
41
- return isCloudRoaringError(err) && err.name === "IntegrityError";
42
- }
43
-
44
- // src/core/ports.ts
45
- var NO_ROW = /* @__PURE__ */ Symbol.for("cloud-roaring.no-row");
46
-
47
- // src/drivers/_shared/aws-errors.ts
48
- function httpStatus(err) {
49
- return err?.$metadata?.httpStatusCode;
50
- }
51
- function errorName(err) {
52
- return err?.name;
53
- }
54
- function errorCode(err) {
55
- return err?.code;
56
- }
57
- function isSdkRetryable(err) {
58
- return err?.$retryable != null;
59
- }
60
- function isServerSide(err) {
61
- const status = httpStatus(err);
62
- return status !== void 0 && status >= 500 && status <= 599;
63
- }
64
- var NETWORK_NAMES = /* @__PURE__ */ new Set([
65
- "TimeoutError",
66
- "RequestTimeout",
67
- "RequestTimeoutException",
68
- "NetworkingError",
69
- "AbortError"
70
- ]);
71
- var NETWORK_CODES = /* @__PURE__ */ new Set([
72
- "ETIMEDOUT",
73
- "ECONNRESET",
74
- "ECONNREFUSED",
75
- "EPIPE",
76
- "ENOTFOUND",
77
- "EAI_AGAIN",
78
- "ECONNABORTED"
79
- ]);
80
- 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;
81
- function isNetworkOrTimeout(err) {
82
- if (NETWORK_NAMES.has(errorName(err) ?? "")) return true;
83
- if (NETWORK_CODES.has(errorCode(err) ?? "")) return true;
84
- return NETWORK_MESSAGE.test(err?.message ?? "");
85
- }
86
-
87
- // src/drivers/dynamodb/dynamodb-errors.ts
88
- function isConditionalCheckFailed(err) {
89
- return errorName(err) === "ConditionalCheckFailedException";
90
- }
91
- var TRANSIENT_NAMES = /* @__PURE__ */ new Set([
92
- "ThrottlingException",
93
- "ProvisionedThroughputExceededException",
94
- "RequestLimitExceeded",
95
- "InternalServerError",
96
- "ServiceUnavailable",
97
- "LimitExceededException"
98
- ]);
99
- function isTransient(err) {
100
- if (isConditionalCheckFailed(err)) return false;
101
- return TRANSIENT_NAMES.has(errorName(err) ?? "") || isServerSide(err) || isNetworkOrTimeout(err) || isSdkRetryable(err);
102
- }
103
-
104
- // src/core/validate.ts
105
- var CHUNK_KEY_MAX = 65535;
106
- var NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
107
- function validatePart(value, field) {
108
- if (typeof value !== "string" || !NAME.test(value) || value.includes("..")) {
109
- throw new ValidationError(
110
- `${field} must match ${String(NAME)} and contain no "..": got ${JSON.stringify(value)}`
111
- );
112
- }
113
- }
114
- function validateSegmentRef(ref) {
115
- validatePart(ref.segment, "segment");
116
- if (ref.namespace !== void 0) validatePart(ref.namespace, "namespace");
117
- }
118
- function validateChunkRef(ref) {
119
- validateSegmentRef(ref);
120
- if (!Number.isInteger(ref.chunkKey) || ref.chunkKey < 0 || ref.chunkKey > CHUNK_KEY_MAX) {
121
- throw new ValidationError(
122
- `chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]; got ${ref.chunkKey}`
123
- );
124
- }
125
- }
126
-
127
- // src/drivers/_shared/keys.ts
128
- var DEFAULT_NAMESPACE = "_default";
129
- function namespacePart(namespace) {
130
- return namespace ?? DEFAULT_NAMESPACE;
131
- }
132
-
133
- // src/drivers/dynamodb/keys.ts
134
- var CHUNK_SK_PREFIX = "chunk#";
135
- var CHUNK_KEY_DIGITS = 5;
136
- function assertValidKeyPrefix(prefix) {
137
- if (prefix === void 0 || prefix === "") return;
138
- for (const ch of prefix) {
139
- const code = ch.charCodeAt(0);
140
- if (ch === "|" || ch === "#" || code < 32) {
141
- throw new ValidationError(`keyPrefix must not contain "|", "#", or control characters`);
142
- }
143
- }
144
- }
145
- function partitionKey(ref, prefix) {
146
- validateSegmentRef(ref);
147
- const base = `ns#${namespacePart(ref.namespace)}|seg#${ref.segment}`;
148
- return prefix === void 0 || prefix === "" ? base : `${prefix}|${base}`;
149
- }
150
- function chunkSortKey(chunkKey) {
151
- return `${CHUNK_SK_PREFIX}${String(chunkKey).padStart(CHUNK_KEY_DIGITS, "0")}`;
152
- }
153
- function chunkKeyPair(ref, prefix) {
154
- validateChunkRef(ref);
155
- return { pk: partitionKey(ref, prefix), sk: chunkSortKey(ref.chunkKey) };
156
- }
157
- function chunkSortKeyPrefix() {
158
- return CHUNK_SK_PREFIX;
159
- }
160
- var REGISTRY_SK = "reg#";
161
- function registrySortKey() {
162
- return REGISTRY_SK;
163
- }
164
- function registryKeyPair(ref, prefix) {
165
- validateSegmentRef(ref);
166
- return { pk: partitionKey(ref, prefix), sk: REGISTRY_SK };
167
- }
168
- function parseChunkSortKey(sk) {
169
- if (!sk.startsWith(CHUNK_SK_PREFIX)) return null;
170
- const middle = sk.slice(CHUNK_SK_PREFIX.length);
171
- if (!new RegExp(`^\\d{${CHUNK_KEY_DIGITS}}$`).test(middle)) return null;
172
- const chunkKey = Number(middle);
173
- return chunkKey <= 65535 ? chunkKey : null;
174
- }
175
-
176
- // src/drivers/dynamodb/warm.ts
177
- var MUTATE_NAMES = { "#v": "v", "#b": "b", "#del": "del" };
178
- var QUERY_NAMES = { "#pk": "PK", "#sk": "SK", "#del": "del" };
179
- var ONE = { N: "1" };
180
- var TRUE = { BOOL: true };
181
- var FALSE = { BOOL: false };
182
- var DynamoDbWarmDriver = class {
183
- client;
184
- table;
185
- keyPrefix;
186
- constructor(options) {
187
- assertValidKeyPrefix(options.keyPrefix);
188
- this.client = options.client;
189
- this.table = options.tableName;
190
- this.keyPrefix = options.keyPrefix;
191
- }
192
- async get(ref, opts) {
193
- const { pk, sk } = chunkKeyPair(ref, this.keyPrefix);
194
- let res;
195
- try {
196
- res = await this.client.send(
197
- new clientDynamodb.GetItemCommand({
198
- TableName: this.table,
199
- Key: { PK: { S: pk }, SK: { S: sk } },
200
- // Strong by default (the OCC read-modify-write must see the latest committed value); a read path
201
- // may opt into an eventually-consistent read (~½ RCU) via `warmReadConsistency: 'eventual'` (gap #9).
202
- ConsistentRead: opts?.consistent ?? true
203
- })
204
- );
205
- } catch (err) {
206
- throw this.mapError(err, ref.chunkKey);
207
- }
208
- if (res.Item === void 0 || res.Item.del?.BOOL === true) return null;
209
- return this.rowFrom(res.Item, ref.chunkKey);
210
- }
211
- async putConditional(ref, bytes, expected) {
212
- const { pk, sk } = chunkKeyPair(ref, this.keyPrefix);
213
- const command = expected === NO_ROW ? new clientDynamodb.UpdateItemCommand({
214
- TableName: this.table,
215
- Key: { PK: { S: pk }, SK: { S: sk } },
216
- ConditionExpression: "attribute_not_exists(#v) OR #del = :true",
217
- UpdateExpression: "ADD #v :one SET #b = :b, #del = :false",
218
- ExpressionAttributeNames: MUTATE_NAMES,
219
- ExpressionAttributeValues: {
220
- ":one": ONE,
221
- ":b": { B: bytes },
222
- ":true": TRUE,
223
- ":false": FALSE
224
- },
225
- ReturnValues: "UPDATED_NEW"
226
- }) : new clientDynamodb.UpdateItemCommand({
227
- TableName: this.table,
228
- Key: { PK: { S: pk }, SK: { S: sk } },
229
- ConditionExpression: "#v = :expected AND #del = :false",
230
- UpdateExpression: "ADD #v :one SET #b = :b",
231
- ExpressionAttributeNames: MUTATE_NAMES,
232
- ExpressionAttributeValues: {
233
- ":expected": { N: expected },
234
- ":one": ONE,
235
- ":b": { B: bytes },
236
- ":false": FALSE
237
- },
238
- ReturnValues: "UPDATED_NEW"
239
- });
240
- try {
241
- const res = await this.client.send(command);
242
- const token = res.Attributes?.v?.N;
243
- if (token === void 0) throw new IntegrityError("DynamoDB UpdateItem returned no token");
244
- return { token };
245
- } catch (err) {
246
- throw this.mapError(err, ref.chunkKey);
247
- }
248
- }
249
- async deleteConditional(ref, expected) {
250
- const { pk, sk } = chunkKeyPair(ref, this.keyPrefix);
251
- try {
252
- await this.client.send(
253
- new clientDynamodb.UpdateItemCommand({
254
- TableName: this.table,
255
- Key: { PK: { S: pk }, SK: { S: sk } },
256
- // Tombstone (advance the counter, drop the payload) — keeps the token monotonic for ABA-safety.
257
- ConditionExpression: "#v = :expected AND #del = :false",
258
- UpdateExpression: "ADD #v :one SET #del = :true REMOVE #b",
259
- ExpressionAttributeNames: MUTATE_NAMES,
260
- ExpressionAttributeValues: {
261
- ":expected": { N: expected },
262
- ":one": ONE,
263
- ":true": TRUE,
264
- ":false": FALSE
265
- }
266
- })
267
- );
268
- } catch (err) {
269
- throw this.mapError(err, ref.chunkKey);
270
- }
271
- }
272
- async *listChunks(ref, opts) {
273
- const pk = partitionKey(ref, this.keyPrefix);
274
- let startKey;
275
- do {
276
- let res;
277
- try {
278
- res = await this.client.send(
279
- new clientDynamodb.QueryCommand({
280
- TableName: this.table,
281
- KeyConditionExpression: "#pk = :pk AND begins_with(#sk, :prefix)",
282
- FilterExpression: "#del = :false",
283
- // exclude tombstones (server-side)
284
- ExpressionAttributeNames: QUERY_NAMES,
285
- ExpressionAttributeValues: {
286
- ":pk": { S: pk },
287
- ":prefix": { S: chunkSortKeyPrefix() },
288
- ":false": FALSE
289
- },
290
- ConsistentRead: opts?.consistent ?? true,
291
- // strong by default; eventual when a read path opts in (gap #9)
292
- ExclusiveStartKey: startKey
293
- })
294
- );
295
- } catch (err) {
296
- throw this.mapError(err, -1);
297
- }
298
- for (const item of res.Items ?? []) {
299
- const chunkKey = parseChunkSortKey(item.SK?.S ?? "");
300
- if (chunkKey === null) continue;
301
- yield { chunkKey, ...this.rowFrom(item, chunkKey) };
302
- }
303
- startKey = res.LastEvaluatedKey;
304
- } while (startKey !== void 0);
305
- }
306
- /**
307
- * Build a {@link WarmRow} from a raw item. Only ever called for a **live** row (callers filter
308
- * tombstones first), so both `v` and `b` MUST be present — a missing attribute means a corrupt or
309
- * foreign item, which we reject (untrusted-bytes posture, invariant 5) rather than paper over.
310
- */
311
- rowFrom(item, chunkKey) {
312
- const token = item.v?.N;
313
- if (token === void 0) {
314
- throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);
315
- }
316
- const bytes = item.b?.B;
317
- if (bytes === void 0) {
318
- throw new IntegrityError(`live warm row for chunk ${chunkKey} is missing its payload`);
319
- }
320
- return { token, bytes };
321
- }
322
- /**
323
- * Translate a raw SDK/driver error to our typed vocabulary: a failed condition is an OCC conflict; a
324
- * throttle/5xx/dropped-connection is a (retryable) {@link TransientError}; everything else propagates
325
- * unchanged. Applied at every `client.send` site so callers (and the retry decorator) only ever see typed
326
- * errors. Order matters: a conflict is **never** transient (it's deterministic — the engine's OCC loop
327
- * owns that retry), so it's classified first.
328
- */
329
- mapError(err, chunkKey) {
330
- if (isIntegrityError(err)) return err;
331
- if (err instanceof clientDynamodb.ConditionalCheckFailedException || err?.name === "ConditionalCheckFailedException") {
332
- return new WriteConflictError(`OCC conflict on chunk ${chunkKey}`);
333
- }
334
- if (isTransient(err)) {
335
- return new TransientError(
336
- `transient DynamoDB fault on chunk ${chunkKey}: ${err?.name ?? "unknown"}`,
337
- { cause: err }
338
- );
339
- }
340
- return err;
341
- }
342
- };
343
-
344
- // src/drivers/_shared/registry.ts
345
- var STATUSES = ["active", "compacting", "erasing", "destroyed"];
346
- var MAX_GOVERNANCE_BYTES = 64 * 1024;
347
- var MAX_WRAPPED_DEKS = 8;
348
- var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
349
- function validateGeneration(gen) {
350
- if (gen === null) return;
351
- if (!Number.isInteger(gen) || gen < 0) {
352
- throw new ValidationError(
353
- `currentGen must be a non-negative integer or null (no Cold generation yet); got ${gen}`
354
- );
355
- }
356
- }
357
- function validatePatchGeneration(patch) {
358
- if (!("currentGen" in patch)) return;
359
- if (patch.currentGen === void 0) {
360
- throw new ValidationError(
361
- `currentGen was present in the patch but undefined. Pass \`null\` to clear the pointer (the segment has no Cold generation), or omit the key to leave it unchanged \u2014 an undefined value is refused because it used to be a no-op and would now un-publish the segment's Cold data.`
362
- );
363
- }
364
- validateGeneration(patch.currentGen);
365
- }
366
- function validateCount(count, field = "dirtyChunkCount") {
367
- if (!Number.isInteger(count) || count < 0) {
368
- throw new ValidationError(`${field} must be a non-negative integer; got ${count}`);
369
- }
370
- }
371
- function validateTimestamp(ms, field) {
372
- if (!Number.isFinite(ms) || ms < 0) {
373
- throw new ValidationError(`${field} must be a non-negative finite number; got ${ms}`);
374
- }
375
- }
376
- function validateStatus(status) {
377
- if (!STATUSES.includes(status)) {
378
- throw new ValidationError(`status must be one of ${STATUSES.join("/")}; got ${status}`);
379
- }
380
- }
381
- function validateGovernance(meta, field) {
382
- if (meta === void 0) return;
383
- if (meta === null || typeof meta !== "object" || Array.isArray(meta)) {
384
- throw new ValidationError(
385
- `${field} must be a plain object (got ${meta === null ? "null" : typeof meta})`
386
- );
387
- }
388
- let json;
389
- try {
390
- json = JSON.stringify(meta);
391
- } catch {
392
- throw new ValidationError(`${field} must be JSON-serializable`);
393
- }
394
- if (json.length > MAX_GOVERNANCE_BYTES) {
395
- throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);
396
- }
397
- }
398
- function validateWrappedDeks(value, isStored) {
399
- if (value === void 0) return;
400
- const fail = (msg) => {
401
- throw isStored ? new IntegrityError(msg) : new ValidationError(msg);
402
- };
403
- if (!Array.isArray(value)) fail("wrappedDeks must be an array");
404
- const list = value;
405
- if (list.length === 0) fail("wrappedDeks must be non-empty when present");
406
- if (list.length > MAX_WRAPPED_DEKS)
407
- fail(`wrappedDeks has ${list.length} entries, cap ${MAX_WRAPPED_DEKS}`);
408
- for (const w of list) {
409
- if (w === null || typeof w !== "object") fail("wrappedDeks entry must be an object");
410
- const { keyId, wrapped } = w;
411
- if (typeof keyId !== "string" || keyId.length === 0)
412
- fail("wrappedDeks entry needs a non-empty keyId");
413
- if (typeof wrapped !== "string" || wrapped.length === 0)
414
- fail("wrappedDeks entry needs a non-empty wrapped blob");
415
- if (wrapped.length > MAX_WRAPPED_DEK_BYTES)
416
- fail(`wrappedDeks entry is ${wrapped.length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);
417
- }
418
- }
419
- function validateNewRegistryRecord(rec) {
420
- validateGeneration(rec.currentGen);
421
- if (rec.dirtyChunkCount !== void 0) validateCount(rec.dirtyChunkCount);
422
- if (rec.status !== void 0) validateStatus(rec.status);
423
- validateWrappedDeks(rec.wrappedDeks, false);
424
- validateGovernance(rec.retention, "retention");
425
- validateGovernance(rec.residency, "residency");
426
- }
427
- function validateRegistryPatch(patch) {
428
- validatePatchGeneration(patch);
429
- if (patch.dirtyChunkCount !== void 0) validateCount(patch.dirtyChunkCount);
430
- if (patch.consecutiveFailures !== void 0)
431
- validateCount(patch.consecutiveFailures, "consecutiveFailures");
432
- if (patch.lastCompactedAt !== void 0)
433
- validateTimestamp(patch.lastCompactedAt, "lastCompactedAt");
434
- if (patch.status !== void 0) validateStatus(patch.status);
435
- if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, false);
436
- if ("retention" in patch) validateGovernance(patch.retention, "retention");
437
- if ("residency" in patch) validateGovernance(patch.residency, "residency");
438
- }
439
- var REGISTRY_SCHEMA_VERSION = 1;
440
- function assertRegistrySchemaVersion(raw, ctx) {
441
- if (raw === void 0) return;
442
- if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
443
- throw new IntegrityError(`registry row has a malformed schemaVersion (${String(raw)}): ${ctx}`);
444
- }
445
- if (raw > REGISTRY_SCHEMA_VERSION) {
446
- throw new UnsupportedError(
447
- `registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`
448
- );
449
- }
450
- }
451
- function recordFromNew(ref, rec, now, token) {
452
- return {
453
- namespace: ref.namespace,
454
- segment: ref.segment,
455
- currentGen: rec.currentGen,
456
- wrappedDeks: rec.wrappedDeks,
457
- keyId: rec.keyId,
458
- dirtyChunkCount: rec.dirtyChunkCount ?? 0,
459
- status: rec.status ?? "active",
460
- consecutiveFailures: 0,
461
- // daemon health (Phase D); lastCompactedAt stays absent until first compaction
462
- retention: rec.retention,
463
- residency: rec.residency,
464
- createdAt: now,
465
- updatedAt: now,
466
- token
467
- };
468
- }
469
- function assertStoredRecordShape(r, ctx) {
470
- if (typeof r.segment !== "string" || r.namespace !== void 0 && typeof r.namespace !== "string" || r.currentGen !== null && typeof r.currentGen !== "number" || typeof r.dirtyChunkCount !== "number" || typeof r.status !== "string" || typeof r.createdAt !== "number" || typeof r.updatedAt !== "number") {
471
- throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
472
- }
473
- if (r.currentGen !== null && (!Number.isInteger(r.currentGen) || r.currentGen < 0)) {
474
- throw new IntegrityError(`registry record has an invalid currentGen (${r.currentGen}): ${ctx}`);
475
- }
476
- if (!Number.isInteger(r.dirtyChunkCount) || r.dirtyChunkCount < 0) {
477
- throw new IntegrityError(`registry record has an invalid dirtyChunkCount: ${ctx}`);
478
- }
479
- if (!STATUSES.includes(r.status)) {
480
- throw new IntegrityError(`registry record has an unknown status (${r.status}): ${ctx}`);
481
- }
482
- if (r.leaseOwner !== void 0 && typeof r.leaseOwner !== "string") {
483
- throw new IntegrityError(`registry record has an invalid leaseOwner: ${ctx}`);
484
- }
485
- if (r.leaseExpiresAt !== void 0 && typeof r.leaseExpiresAt !== "number") {
486
- throw new IntegrityError(`registry record has an invalid leaseExpiresAt: ${ctx}`);
487
- }
488
- if (r.consecutiveFailures !== void 0 && (!Number.isInteger(r.consecutiveFailures) || r.consecutiveFailures < 0)) {
489
- throw new IntegrityError(`registry record has an invalid consecutiveFailures: ${ctx}`);
490
- }
491
- if (r.lastCompactedAt !== void 0 && (typeof r.lastCompactedAt !== "number" || !Number.isFinite(r.lastCompactedAt) || r.lastCompactedAt < 0)) {
492
- throw new IntegrityError(`registry record has an invalid lastCompactedAt: ${ctx}`);
493
- }
494
- if (r.keyId !== void 0 && typeof r.keyId !== "string") {
495
- throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);
496
- }
497
- validateWrappedDeks(r.wrappedDeks, true);
498
- for (const field of ["retention", "residency"]) {
499
- const meta = r[field];
500
- if (meta !== void 0 && (meta === null || typeof meta !== "object" || Array.isArray(meta))) {
501
- throw new IntegrityError(`registry record has a non-object ${field}: ${ctx}`);
502
- }
503
- }
504
- }
505
- function applyRegistryPatch(prev, patch, now, token) {
506
- return {
507
- namespace: prev.namespace,
508
- segment: prev.segment,
509
- // `'currentGen' in patch`, NOT `patch.currentGen ?? prev.currentGen`. `null` is a legal, meaningful value
510
- // here (no Cold generation yet), and `??` treats it as absent — so the nullish form would silently ignore a
511
- // patch that clears the pointer and leave the old generation in place. The same reason `wrappedDeks` and
512
- // `keyId` below use presence: any field whose null is a *value* cannot be merged with `??`.
513
- currentGen: "currentGen" in patch ? patch.currentGen ?? null : prev.currentGen,
514
- wrappedDeks: "wrappedDeks" in patch ? patch.wrappedDeks : prev.wrappedDeks,
515
- keyId: "keyId" in patch ? patch.keyId : prev.keyId,
516
- dirtyChunkCount: patch.dirtyChunkCount ?? prev.dirtyChunkCount,
517
- status: patch.status ?? prev.status,
518
- leaseOwner: "leaseOwner" in patch ? patch.leaseOwner : prev.leaseOwner,
519
- leaseExpiresAt: "leaseExpiresAt" in patch ? patch.leaseExpiresAt : prev.leaseExpiresAt,
520
- lastCompactedAt: "lastCompactedAt" in patch ? patch.lastCompactedAt : prev.lastCompactedAt,
521
- consecutiveFailures: patch.consecutiveFailures ?? prev.consecutiveFailures ?? 0,
522
- retention: "retention" in patch ? patch.retention : prev.retention,
523
- residency: "residency" in patch ? patch.residency : prev.residency,
524
- createdAt: prev.createdAt,
525
- updatedAt: now,
526
- token
527
- };
528
- }
529
-
530
- // src/drivers/dynamodb/registry.ts
531
- var MUTATE_NAMES2 = { "#v": "v", "#r": "r", "#del": "del" };
532
- var SCAN_NAMES = { "#sk": "SK", "#del": "del", "#pk": "PK" };
533
- var ONE2 = { N: "1" };
534
- var TRUE2 = { BOOL: true };
535
- var FALSE2 = { BOOL: false };
536
- var DynamoDbRegistryDriver = class {
537
- client;
538
- table;
539
- keyPrefix;
540
- now;
541
- constructor(options) {
542
- assertValidKeyPrefix(options.keyPrefix);
543
- this.client = options.client;
544
- this.table = options.tableName;
545
- this.keyPrefix = options.keyPrefix;
546
- this.now = options.now ?? (() => Date.now());
547
- }
548
- capabilities() {
549
- return { strongRead: true };
550
- }
551
- async get(ref) {
552
- const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
553
- let res;
554
- try {
555
- res = await this.client.send(
556
- new clientDynamodb.GetItemCommand({
557
- TableName: this.table,
558
- Key: { PK: { S: pk }, SK: { S: sk } },
559
- ConsistentRead: true
560
- // currentGen feeds correctness — must be the latest committed value
561
- })
562
- );
563
- } catch (err) {
564
- throw this.mapError(err, ref);
565
- }
566
- if (res.Item === void 0 || res.Item.del?.BOOL === true) return null;
567
- return rowToRecord(res.Item, ref);
568
- }
569
- async create(ref, record) {
570
- validateNewRegistryRecord(record);
571
- const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
572
- const now = this.now();
573
- const body = serializeBody(recordFromNew(ref, record, now, ""));
574
- try {
575
- const res = await this.client.send(
576
- new clientDynamodb.UpdateItemCommand({
577
- TableName: this.table,
578
- Key: { PK: { S: pk }, SK: { S: sk } },
579
- ConditionExpression: "attribute_not_exists(#v) OR #del = :true",
580
- UpdateExpression: "ADD #v :one SET #r = :r, #del = :false",
581
- ExpressionAttributeNames: MUTATE_NAMES2,
582
- ExpressionAttributeValues: {
583
- ":one": ONE2,
584
- ":r": { S: body },
585
- ":true": TRUE2,
586
- ":false": FALSE2
587
- },
588
- ReturnValues: "UPDATED_NEW"
589
- })
590
- );
591
- return { token: tokenOf(res.Attributes) };
592
- } catch (err) {
593
- throw this.mapError(err, ref, "already exists");
594
- }
595
- }
596
- async compareAndSwap(ref, expected, patch) {
597
- validateRegistryPatch(patch);
598
- const current = await this.get(ref);
599
- if (current === null || current.token !== expected) {
600
- throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
601
- }
602
- const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
603
- const body = serializeBody(applyRegistryPatch(current, patch, this.now(), ""));
604
- try {
605
- const res = await this.client.send(
606
- new clientDynamodb.UpdateItemCommand({
607
- TableName: this.table,
608
- Key: { PK: { S: pk }, SK: { S: sk } },
609
- ConditionExpression: "#v = :expected AND #del = :false",
610
- UpdateExpression: "ADD #v :one SET #r = :r",
611
- ExpressionAttributeNames: MUTATE_NAMES2,
612
- ExpressionAttributeValues: {
613
- ":expected": { N: expected },
614
- ":one": ONE2,
615
- ":r": { S: body },
616
- ":false": FALSE2
617
- },
618
- ReturnValues: "UPDATED_NEW"
619
- })
620
- );
621
- return { token: tokenOf(res.Attributes) };
622
- } catch (err) {
623
- throw this.mapError(err, ref);
624
- }
625
- }
626
- async *list(namespace) {
627
- const filter = ["#sk = :reg", "#del = :false"];
628
- const values = {
629
- ":reg": { S: registrySortKey() },
630
- ":false": FALSE2
631
- };
632
- const pkPrefix = this.scanPkPrefix(namespace);
633
- if (pkPrefix !== void 0) {
634
- filter.push("begins_with(#pk, :pk)");
635
- values[":pk"] = { S: pkPrefix };
636
- }
637
- let startKey;
638
- do {
639
- let res;
640
- try {
641
- res = await this.client.send(
642
- new clientDynamodb.ScanCommand({
643
- TableName: this.table,
644
- FilterExpression: filter.join(" AND "),
645
- ExpressionAttributeNames: pkPrefix !== void 0 ? SCAN_NAMES : { "#sk": "SK", "#del": "del" },
646
- ExpressionAttributeValues: values,
647
- ExclusiveStartKey: startKey,
648
- // Strong, to match `get()` on the same table. A Scan defaults to eventually consistent, and every
649
- // caller here treats the result as the COMPLETE segment set: `eraseSubject` builds its erasure
650
- // ledger from it (a segment registered seconds earlier would be silently absent — a GDPR Art. 17
651
- // miss with no error), `subjectReport` answers Art. 15 from it, and `runExport` writes its
652
- // manifest from it (a missed segment lands in neither the manifest nor `failed[]`, so "a manifest
653
- // exists ⇒ the run finished" would be false). `runConsistencyCheck` already re-read each row
654
- // strongly for exactly this reason; this makes the enumeration itself trustworthy instead.
655
- // Doubles this Scan's RCU cost, which is the correct trade for a correctness-critical enumeration.
656
- ConsistentRead: true
657
- })
658
- );
659
- } catch (err) {
660
- throw this.mapError(err);
661
- }
662
- for (const item of res.Items ?? []) {
663
- yield rowToRecord(item, namespaceOf(item));
664
- }
665
- startKey = res.LastEvaluatedKey;
666
- } while (startKey !== void 0);
667
- }
668
- async delete(ref) {
669
- const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
670
- try {
671
- await this.client.send(
672
- new clientDynamodb.UpdateItemCommand({
673
- TableName: this.table,
674
- Key: { PK: { S: pk }, SK: { S: sk } },
675
- UpdateExpression: "ADD #v :one SET #del = :true REMOVE #r",
676
- ExpressionAttributeNames: MUTATE_NAMES2,
677
- ExpressionAttributeValues: { ":one": ONE2, ":true": TRUE2 }
678
- })
679
- );
680
- } catch (err) {
681
- throw this.mapError(err, ref);
682
- }
683
- }
684
- /** The PK prefix to scope a `list` scan, or `undefined` for an unscoped scan (no namespace, no keyPrefix). */
685
- scanPkPrefix(namespace) {
686
- if (namespace !== void 0) {
687
- const full = partitionKey({ namespace, segment: "x" }, this.keyPrefix);
688
- return full.slice(0, full.length - "x".length);
689
- }
690
- return this.keyPrefix !== void 0 ? `${this.keyPrefix}|` : void 0;
691
- }
692
- /** Map a raw SDK error to the typed vocabulary (conflict / transient / passthrough). */
693
- mapError(err, ref, conflictHint = "OCC token mismatch") {
694
- if (isIntegrityError(err)) return err;
695
- if (err instanceof clientDynamodb.ConditionalCheckFailedException || err?.name === "ConditionalCheckFailedException") {
696
- return new WriteConflictError(
697
- `registry ${conflictHint}${ref ? ` for segment ${ref.segment}` : ""}`
698
- );
699
- }
700
- if (isTransient(err)) {
701
- return new TransientError(
702
- `transient DynamoDB fault on registry${ref ? ` ${ref.segment}` : ""}: ${err?.name ?? "unknown"}`,
703
- { cause: err }
704
- );
705
- }
706
- return err;
707
- }
708
- };
709
- function tokenOf(attrs) {
710
- const token = attrs?.v?.N;
711
- if (token === void 0)
712
- throw new IntegrityError("DynamoDB registry UpdateItem returned no token");
713
- return token;
714
- }
715
- function serializeBody(record) {
716
- const body = {
717
- namespace: record.namespace,
718
- segment: record.segment,
719
- currentGen: record.currentGen,
720
- wrappedDeks: record.wrappedDeks,
721
- keyId: record.keyId,
722
- dirtyChunkCount: record.dirtyChunkCount,
723
- status: record.status,
724
- leaseOwner: record.leaseOwner,
725
- leaseExpiresAt: record.leaseExpiresAt,
726
- lastCompactedAt: record.lastCompactedAt,
727
- consecutiveFailures: record.consecutiveFailures,
728
- retention: record.retention,
729
- residency: record.residency,
730
- createdAt: record.createdAt,
731
- updatedAt: record.updatedAt
732
- };
733
- return JSON.stringify({ schemaVersion: REGISTRY_SCHEMA_VERSION, ...body });
734
- }
735
- function namespaceOf(item) {
736
- const body = parseBody(item.r?.S, "scan");
737
- return { namespace: body.namespace, segment: body.segment };
738
- }
739
- function rowToRecord(item, ref) {
740
- const token = item.v?.N;
741
- if (token === void 0) {
742
- throw new IntegrityError(`registry row for segment ${ref.segment} is missing its token`);
743
- }
744
- const body = parseBody(item.r?.S, ref.segment);
745
- return { ...body, token };
746
- }
747
- function parseBody(json, ctx) {
748
- if (json === void 0) throw new IntegrityError(`registry row ${ctx} is missing its body`);
749
- let parsed;
750
- try {
751
- parsed = JSON.parse(json);
752
- } catch {
753
- throw new IntegrityError(`registry row ${ctx} body is not valid JSON`);
754
- }
755
- if (parsed === null || typeof parsed !== "object") {
756
- throw new IntegrityError(`registry row ${ctx} body is malformed`);
757
- }
758
- const { schemaVersion, ...body } = parsed;
759
- assertRegistrySchemaVersion(schemaVersion, ctx);
760
- assertStoredRecordShape(body, ctx);
761
- return body;
762
- }
763
-
764
- exports.DynamoDbRegistryDriver = DynamoDbRegistryDriver;
765
- exports.DynamoDbWarmDriver = DynamoDbWarmDriver;
766
- //# sourceMappingURL=index.cjs.map
767
- //# sourceMappingURL=index.cjs.map