@cloudbitmaps/core 0.1.0-rc.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 (76) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/README.md +22 -0
  4. package/dist/azure/index.cjs +368 -0
  5. package/dist/azure/index.cjs.map +1 -0
  6. package/dist/azure/index.d.cts +48 -0
  7. package/dist/azure/index.d.ts +48 -0
  8. package/dist/azure/index.js +304 -0
  9. package/dist/azure/index.js.map +1 -0
  10. package/dist/cassandra/index.cjs +265 -0
  11. package/dist/cassandra/index.cjs.map +1 -0
  12. package/dist/cassandra/index.d.cts +52 -0
  13. package/dist/cassandra/index.d.ts +52 -0
  14. package/dist/cassandra/index.js +204 -0
  15. package/dist/cassandra/index.js.map +1 -0
  16. package/dist/chunk-2YDULGXS.js +203 -0
  17. package/dist/chunk-2YDULGXS.js.map +1 -0
  18. package/dist/chunk-7LMLYSVJ.js +43 -0
  19. package/dist/chunk-7LMLYSVJ.js.map +1 -0
  20. package/dist/chunk-AS6ODRLT.js +6 -0
  21. package/dist/chunk-AS6ODRLT.js.map +1 -0
  22. package/dist/chunk-NUIDEEFZ.js +91 -0
  23. package/dist/chunk-NUIDEEFZ.js.map +1 -0
  24. package/dist/chunk-SNJVZ227.js +35 -0
  25. package/dist/chunk-SNJVZ227.js.map +1 -0
  26. package/dist/dynamodb/index.cjs +731 -0
  27. package/dist/dynamodb/index.cjs.map +1 -0
  28. package/dist/dynamodb/index.d.cts +106 -0
  29. package/dist/dynamodb/index.d.ts +106 -0
  30. package/dist/dynamodb/index.js +460 -0
  31. package/dist/dynamodb/index.js.map +1 -0
  32. package/dist/gcs/index.cjs +343 -0
  33. package/dist/gcs/index.cjs.map +1 -0
  34. package/dist/gcs/index.d.cts +46 -0
  35. package/dist/gcs/index.d.ts +46 -0
  36. package/dist/gcs/index.js +279 -0
  37. package/dist/gcs/index.js.map +1 -0
  38. package/dist/index.cjs +4031 -0
  39. package/dist/index.cjs.map +1 -0
  40. package/dist/index.d.cts +1945 -0
  41. package/dist/index.d.ts +1945 -0
  42. package/dist/index.js +3642 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/mongodb/index.cjs +260 -0
  45. package/dist/mongodb/index.cjs.map +1 -0
  46. package/dist/mongodb/index.d.cts +45 -0
  47. package/dist/mongodb/index.d.ts +45 -0
  48. package/dist/mongodb/index.js +199 -0
  49. package/dist/mongodb/index.js.map +1 -0
  50. package/dist/mysql/index.cjs +281 -0
  51. package/dist/mysql/index.cjs.map +1 -0
  52. package/dist/mysql/index.d.cts +56 -0
  53. package/dist/mysql/index.d.ts +56 -0
  54. package/dist/mysql/index.js +211 -0
  55. package/dist/mysql/index.js.map +1 -0
  56. package/dist/ports-D3BrJ6ax.d.cts +357 -0
  57. package/dist/ports-D3BrJ6ax.d.ts +357 -0
  58. package/dist/postgres/index.cjs +273 -0
  59. package/dist/postgres/index.cjs.map +1 -0
  60. package/dist/postgres/index.d.cts +54 -0
  61. package/dist/postgres/index.d.ts +54 -0
  62. package/dist/postgres/index.js +203 -0
  63. package/dist/postgres/index.js.map +1 -0
  64. package/dist/redis/index.cjs +257 -0
  65. package/dist/redis/index.cjs.map +1 -0
  66. package/dist/redis/index.d.cts +42 -0
  67. package/dist/redis/index.d.ts +42 -0
  68. package/dist/redis/index.js +197 -0
  69. package/dist/redis/index.js.map +1 -0
  70. package/dist/s3/index.cjs +891 -0
  71. package/dist/s3/index.cjs.map +1 -0
  72. package/dist/s3/index.d.cts +112 -0
  73. package/dist/s3/index.d.ts +112 -0
  74. package/dist/s3/index.js +555 -0
  75. package/dist/s3/index.js.map +1 -0
  76. package/package.json +172 -0
@@ -0,0 +1,460 @@
1
+ import { errorName, isServerSide, isNetworkOrTimeout, isSdkRetryable } from '../chunk-7LMLYSVJ.js';
2
+ import { validateNewRegistryRecord, recordFromNew, validateRegistryPatch, applyRegistryPatch, REGISTRY_SCHEMA_VERSION, assertRegistrySchemaVersion, assertStoredRecordShape } from '../chunk-2YDULGXS.js';
3
+ import { NO_ROW } from '../chunk-AS6ODRLT.js';
4
+ import { IntegrityError, isIntegrityError, WriteConflictError, TransientError, ValidationError, validateChunkRef, validateSegmentRef, namespacePart } from '../chunk-NUIDEEFZ.js';
5
+ import { GetItemCommand, UpdateItemCommand, QueryCommand, ConditionalCheckFailedException, ScanCommand } from '@aws-sdk/client-dynamodb';
6
+
7
+ // src/drivers/dynamodb/dynamodb-errors.ts
8
+ function isConditionalCheckFailed(err) {
9
+ return errorName(err) === "ConditionalCheckFailedException";
10
+ }
11
+ var TRANSIENT_NAMES = /* @__PURE__ */ new Set([
12
+ "ThrottlingException",
13
+ "ProvisionedThroughputExceededException",
14
+ "RequestLimitExceeded",
15
+ "InternalServerError",
16
+ "ServiceUnavailable",
17
+ "LimitExceededException"
18
+ ]);
19
+ function isTransient(err) {
20
+ if (isConditionalCheckFailed(err)) return false;
21
+ return TRANSIENT_NAMES.has(errorName(err) ?? "") || isServerSide(err) || isNetworkOrTimeout(err) || isSdkRetryable(err);
22
+ }
23
+
24
+ // src/drivers/dynamodb/keys.ts
25
+ var CHUNK_SK_PREFIX = "chunk#";
26
+ var CHUNK_KEY_DIGITS = 5;
27
+ function assertValidKeyPrefix(prefix) {
28
+ if (prefix === void 0 || prefix === "") return;
29
+ for (const ch of prefix) {
30
+ const code = ch.charCodeAt(0);
31
+ if (ch === "|" || ch === "#" || code < 32) {
32
+ throw new ValidationError(`keyPrefix must not contain "|", "#", or control characters`);
33
+ }
34
+ }
35
+ }
36
+ function partitionKey(ref, prefix) {
37
+ validateSegmentRef(ref);
38
+ const base = `ns#${namespacePart(ref.namespace)}|seg#${ref.segment}`;
39
+ return prefix === void 0 || prefix === "" ? base : `${prefix}|${base}`;
40
+ }
41
+ function chunkSortKey(chunkKey) {
42
+ return `${CHUNK_SK_PREFIX}${String(chunkKey).padStart(CHUNK_KEY_DIGITS, "0")}`;
43
+ }
44
+ function chunkKeyPair(ref, prefix) {
45
+ validateChunkRef(ref);
46
+ return { pk: partitionKey(ref, prefix), sk: chunkSortKey(ref.chunkKey) };
47
+ }
48
+ function chunkSortKeyPrefix() {
49
+ return CHUNK_SK_PREFIX;
50
+ }
51
+ var REGISTRY_SK = "reg#";
52
+ function registrySortKey() {
53
+ return REGISTRY_SK;
54
+ }
55
+ function registryKeyPair(ref, prefix) {
56
+ validateSegmentRef(ref);
57
+ return { pk: partitionKey(ref, prefix), sk: REGISTRY_SK };
58
+ }
59
+ function parseChunkSortKey(sk) {
60
+ if (!sk.startsWith(CHUNK_SK_PREFIX)) return null;
61
+ const middle = sk.slice(CHUNK_SK_PREFIX.length);
62
+ if (!new RegExp(`^\\d{${CHUNK_KEY_DIGITS}}$`).test(middle)) return null;
63
+ const chunkKey = Number(middle);
64
+ return chunkKey <= 65535 ? chunkKey : null;
65
+ }
66
+
67
+ // src/drivers/dynamodb/warm.ts
68
+ var MUTATE_NAMES = { "#v": "v", "#b": "b", "#del": "del" };
69
+ var QUERY_NAMES = { "#pk": "PK", "#sk": "SK", "#del": "del" };
70
+ var ONE = { N: "1" };
71
+ var TRUE = { BOOL: true };
72
+ var FALSE = { BOOL: false };
73
+ var DynamoDbWarmDriver = class {
74
+ client;
75
+ table;
76
+ keyPrefix;
77
+ constructor(options) {
78
+ assertValidKeyPrefix(options.keyPrefix);
79
+ this.client = options.client;
80
+ this.table = options.tableName;
81
+ this.keyPrefix = options.keyPrefix;
82
+ }
83
+ async get(ref, opts) {
84
+ const { pk, sk } = chunkKeyPair(ref, this.keyPrefix);
85
+ let res;
86
+ try {
87
+ res = await this.client.send(
88
+ new GetItemCommand({
89
+ TableName: this.table,
90
+ Key: { PK: { S: pk }, SK: { S: sk } },
91
+ // Strong by default (the OCC read-modify-write must see the latest committed value); a read path
92
+ // may opt into an eventually-consistent read (~½ RCU) via `warmReadConsistency: 'eventual'` (gap #9).
93
+ ConsistentRead: opts?.consistent ?? true
94
+ })
95
+ );
96
+ } catch (err) {
97
+ throw this.mapError(err, ref.chunkKey);
98
+ }
99
+ if (res.Item === void 0 || res.Item.del?.BOOL === true) return null;
100
+ return this.rowFrom(res.Item, ref.chunkKey);
101
+ }
102
+ async putConditional(ref, bytes, expected) {
103
+ const { pk, sk } = chunkKeyPair(ref, this.keyPrefix);
104
+ const command = expected === NO_ROW ? new UpdateItemCommand({
105
+ TableName: this.table,
106
+ Key: { PK: { S: pk }, SK: { S: sk } },
107
+ ConditionExpression: "attribute_not_exists(#v) OR #del = :true",
108
+ UpdateExpression: "ADD #v :one SET #b = :b, #del = :false",
109
+ ExpressionAttributeNames: MUTATE_NAMES,
110
+ ExpressionAttributeValues: {
111
+ ":one": ONE,
112
+ ":b": { B: bytes },
113
+ ":true": TRUE,
114
+ ":false": FALSE
115
+ },
116
+ ReturnValues: "UPDATED_NEW"
117
+ }) : new UpdateItemCommand({
118
+ TableName: this.table,
119
+ Key: { PK: { S: pk }, SK: { S: sk } },
120
+ ConditionExpression: "#v = :expected AND #del = :false",
121
+ UpdateExpression: "ADD #v :one SET #b = :b",
122
+ ExpressionAttributeNames: MUTATE_NAMES,
123
+ ExpressionAttributeValues: {
124
+ ":expected": { N: expected },
125
+ ":one": ONE,
126
+ ":b": { B: bytes },
127
+ ":false": FALSE
128
+ },
129
+ ReturnValues: "UPDATED_NEW"
130
+ });
131
+ try {
132
+ const res = await this.client.send(command);
133
+ const token = res.Attributes?.v?.N;
134
+ if (token === void 0) throw new IntegrityError("DynamoDB UpdateItem returned no token");
135
+ return { token };
136
+ } catch (err) {
137
+ throw this.mapError(err, ref.chunkKey);
138
+ }
139
+ }
140
+ async deleteConditional(ref, expected) {
141
+ const { pk, sk } = chunkKeyPair(ref, this.keyPrefix);
142
+ try {
143
+ await this.client.send(
144
+ new UpdateItemCommand({
145
+ TableName: this.table,
146
+ Key: { PK: { S: pk }, SK: { S: sk } },
147
+ // Tombstone (advance the counter, drop the payload) — keeps the token monotonic for ABA-safety.
148
+ ConditionExpression: "#v = :expected AND #del = :false",
149
+ UpdateExpression: "ADD #v :one SET #del = :true REMOVE #b",
150
+ ExpressionAttributeNames: MUTATE_NAMES,
151
+ ExpressionAttributeValues: {
152
+ ":expected": { N: expected },
153
+ ":one": ONE,
154
+ ":true": TRUE,
155
+ ":false": FALSE
156
+ }
157
+ })
158
+ );
159
+ } catch (err) {
160
+ throw this.mapError(err, ref.chunkKey);
161
+ }
162
+ }
163
+ async *listChunks(ref, opts) {
164
+ const pk = partitionKey(ref, this.keyPrefix);
165
+ let startKey;
166
+ do {
167
+ let res;
168
+ try {
169
+ res = await this.client.send(
170
+ new QueryCommand({
171
+ TableName: this.table,
172
+ KeyConditionExpression: "#pk = :pk AND begins_with(#sk, :prefix)",
173
+ FilterExpression: "#del = :false",
174
+ // exclude tombstones (server-side)
175
+ ExpressionAttributeNames: QUERY_NAMES,
176
+ ExpressionAttributeValues: {
177
+ ":pk": { S: pk },
178
+ ":prefix": { S: chunkSortKeyPrefix() },
179
+ ":false": FALSE
180
+ },
181
+ ConsistentRead: opts?.consistent ?? true,
182
+ // strong by default; eventual when a read path opts in (gap #9)
183
+ ExclusiveStartKey: startKey
184
+ })
185
+ );
186
+ } catch (err) {
187
+ throw this.mapError(err, -1);
188
+ }
189
+ for (const item of res.Items ?? []) {
190
+ const chunkKey = parseChunkSortKey(item.SK?.S ?? "");
191
+ if (chunkKey === null) continue;
192
+ yield { chunkKey, ...this.rowFrom(item, chunkKey) };
193
+ }
194
+ startKey = res.LastEvaluatedKey;
195
+ } while (startKey !== void 0);
196
+ }
197
+ /**
198
+ * Build a {@link WarmRow} from a raw item. Only ever called for a **live** row (callers filter
199
+ * tombstones first), so both `v` and `b` MUST be present — a missing attribute means a corrupt or
200
+ * foreign item, which we reject (untrusted-bytes posture, invariant 5) rather than paper over.
201
+ */
202
+ rowFrom(item, chunkKey) {
203
+ const token = item.v?.N;
204
+ if (token === void 0) {
205
+ throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);
206
+ }
207
+ const bytes = item.b?.B;
208
+ if (bytes === void 0) {
209
+ throw new IntegrityError(`live warm row for chunk ${chunkKey} is missing its payload`);
210
+ }
211
+ return { token, bytes };
212
+ }
213
+ /**
214
+ * Translate a raw SDK/driver error to our typed vocabulary: a failed condition is an OCC conflict; a
215
+ * throttle/5xx/dropped-connection is a (retryable) {@link TransientError}; everything else propagates
216
+ * unchanged. Applied at every `client.send` site so callers (and the retry decorator) only ever see typed
217
+ * errors. Order matters: a conflict is **never** transient (it's deterministic — the engine's OCC loop
218
+ * owns that retry), so it's classified first.
219
+ */
220
+ mapError(err, chunkKey) {
221
+ if (isIntegrityError(err)) return err;
222
+ if (err instanceof ConditionalCheckFailedException || err?.name === "ConditionalCheckFailedException") {
223
+ return new WriteConflictError(`OCC conflict on chunk ${chunkKey}`);
224
+ }
225
+ if (isTransient(err)) {
226
+ return new TransientError(
227
+ `transient DynamoDB fault on chunk ${chunkKey}: ${err?.name ?? "unknown"}`,
228
+ { cause: err }
229
+ );
230
+ }
231
+ return err;
232
+ }
233
+ };
234
+ var MUTATE_NAMES2 = { "#v": "v", "#r": "r", "#del": "del" };
235
+ var SCAN_NAMES = { "#sk": "SK", "#del": "del", "#pk": "PK" };
236
+ var ONE2 = { N: "1" };
237
+ var TRUE2 = { BOOL: true };
238
+ var FALSE2 = { BOOL: false };
239
+ var DynamoDbRegistryDriver = class {
240
+ client;
241
+ table;
242
+ keyPrefix;
243
+ now;
244
+ constructor(options) {
245
+ assertValidKeyPrefix(options.keyPrefix);
246
+ this.client = options.client;
247
+ this.table = options.tableName;
248
+ this.keyPrefix = options.keyPrefix;
249
+ this.now = options.now ?? (() => Date.now());
250
+ }
251
+ capabilities() {
252
+ return { strongRead: true };
253
+ }
254
+ async get(ref) {
255
+ const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
256
+ let res;
257
+ try {
258
+ res = await this.client.send(
259
+ new GetItemCommand({
260
+ TableName: this.table,
261
+ Key: { PK: { S: pk }, SK: { S: sk } },
262
+ ConsistentRead: true
263
+ // currentGen feeds correctness — must be the latest committed value
264
+ })
265
+ );
266
+ } catch (err) {
267
+ throw this.mapError(err, ref);
268
+ }
269
+ if (res.Item === void 0 || res.Item.del?.BOOL === true) return null;
270
+ return rowToRecord(res.Item, ref);
271
+ }
272
+ async create(ref, record) {
273
+ validateNewRegistryRecord(record);
274
+ const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
275
+ const now = this.now();
276
+ const body = serializeBody(recordFromNew(ref, record, now, ""));
277
+ try {
278
+ const res = await this.client.send(
279
+ new UpdateItemCommand({
280
+ TableName: this.table,
281
+ Key: { PK: { S: pk }, SK: { S: sk } },
282
+ ConditionExpression: "attribute_not_exists(#v) OR #del = :true",
283
+ UpdateExpression: "ADD #v :one SET #r = :r, #del = :false",
284
+ ExpressionAttributeNames: MUTATE_NAMES2,
285
+ ExpressionAttributeValues: {
286
+ ":one": ONE2,
287
+ ":r": { S: body },
288
+ ":true": TRUE2,
289
+ ":false": FALSE2
290
+ },
291
+ ReturnValues: "UPDATED_NEW"
292
+ })
293
+ );
294
+ return { token: tokenOf(res.Attributes) };
295
+ } catch (err) {
296
+ throw this.mapError(err, ref, "already exists");
297
+ }
298
+ }
299
+ async compareAndSwap(ref, expected, patch) {
300
+ validateRegistryPatch(patch);
301
+ const current = await this.get(ref);
302
+ if (current === null || current.token !== expected) {
303
+ throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
304
+ }
305
+ const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
306
+ const body = serializeBody(applyRegistryPatch(current, patch, this.now(), ""));
307
+ try {
308
+ const res = await this.client.send(
309
+ new UpdateItemCommand({
310
+ TableName: this.table,
311
+ Key: { PK: { S: pk }, SK: { S: sk } },
312
+ ConditionExpression: "#v = :expected AND #del = :false",
313
+ UpdateExpression: "ADD #v :one SET #r = :r",
314
+ ExpressionAttributeNames: MUTATE_NAMES2,
315
+ ExpressionAttributeValues: {
316
+ ":expected": { N: expected },
317
+ ":one": ONE2,
318
+ ":r": { S: body },
319
+ ":false": FALSE2
320
+ },
321
+ ReturnValues: "UPDATED_NEW"
322
+ })
323
+ );
324
+ return { token: tokenOf(res.Attributes) };
325
+ } catch (err) {
326
+ throw this.mapError(err, ref);
327
+ }
328
+ }
329
+ async *list(namespace) {
330
+ const filter = ["#sk = :reg", "#del = :false"];
331
+ const values = {
332
+ ":reg": { S: registrySortKey() },
333
+ ":false": FALSE2
334
+ };
335
+ const pkPrefix = this.scanPkPrefix(namespace);
336
+ if (pkPrefix !== void 0) {
337
+ filter.push("begins_with(#pk, :pk)");
338
+ values[":pk"] = { S: pkPrefix };
339
+ }
340
+ let startKey;
341
+ do {
342
+ let res;
343
+ try {
344
+ res = await this.client.send(
345
+ new ScanCommand({
346
+ TableName: this.table,
347
+ FilterExpression: filter.join(" AND "),
348
+ ExpressionAttributeNames: pkPrefix !== void 0 ? SCAN_NAMES : { "#sk": "SK", "#del": "del" },
349
+ ExpressionAttributeValues: values,
350
+ ExclusiveStartKey: startKey
351
+ })
352
+ );
353
+ } catch (err) {
354
+ throw this.mapError(err);
355
+ }
356
+ for (const item of res.Items ?? []) {
357
+ yield rowToRecord(item, namespaceOf(item));
358
+ }
359
+ startKey = res.LastEvaluatedKey;
360
+ } while (startKey !== void 0);
361
+ }
362
+ async delete(ref) {
363
+ const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
364
+ try {
365
+ await this.client.send(
366
+ new UpdateItemCommand({
367
+ TableName: this.table,
368
+ Key: { PK: { S: pk }, SK: { S: sk } },
369
+ UpdateExpression: "ADD #v :one SET #del = :true REMOVE #r",
370
+ ExpressionAttributeNames: MUTATE_NAMES2,
371
+ ExpressionAttributeValues: { ":one": ONE2, ":true": TRUE2 }
372
+ })
373
+ );
374
+ } catch (err) {
375
+ throw this.mapError(err, ref);
376
+ }
377
+ }
378
+ /** The PK prefix to scope a `list` scan, or `undefined` for an unscoped scan (no namespace, no keyPrefix). */
379
+ scanPkPrefix(namespace) {
380
+ if (namespace !== void 0) {
381
+ const full = partitionKey({ namespace, segment: "x" }, this.keyPrefix);
382
+ return full.slice(0, full.length - "x".length);
383
+ }
384
+ return this.keyPrefix !== void 0 ? `${this.keyPrefix}|` : void 0;
385
+ }
386
+ /** Map a raw SDK error to the typed vocabulary (conflict / transient / passthrough). */
387
+ mapError(err, ref, conflictHint = "OCC token mismatch") {
388
+ if (isIntegrityError(err)) return err;
389
+ if (err instanceof ConditionalCheckFailedException || err?.name === "ConditionalCheckFailedException") {
390
+ return new WriteConflictError(
391
+ `registry ${conflictHint}${ref ? ` for segment ${ref.segment}` : ""}`
392
+ );
393
+ }
394
+ if (isTransient(err)) {
395
+ return new TransientError(
396
+ `transient DynamoDB fault on registry${ref ? ` ${ref.segment}` : ""}: ${err?.name ?? "unknown"}`,
397
+ { cause: err }
398
+ );
399
+ }
400
+ return err;
401
+ }
402
+ };
403
+ function tokenOf(attrs) {
404
+ const token = attrs?.v?.N;
405
+ if (token === void 0)
406
+ throw new IntegrityError("DynamoDB registry UpdateItem returned no token");
407
+ return token;
408
+ }
409
+ function serializeBody(record) {
410
+ const body = {
411
+ namespace: record.namespace,
412
+ segment: record.segment,
413
+ currentGen: record.currentGen,
414
+ wrappedDeks: record.wrappedDeks,
415
+ keyId: record.keyId,
416
+ dirtyChunkCount: record.dirtyChunkCount,
417
+ status: record.status,
418
+ leaseOwner: record.leaseOwner,
419
+ leaseExpiresAt: record.leaseExpiresAt,
420
+ lastCompactedAt: record.lastCompactedAt,
421
+ consecutiveFailures: record.consecutiveFailures,
422
+ retention: record.retention,
423
+ residency: record.residency,
424
+ createdAt: record.createdAt,
425
+ updatedAt: record.updatedAt
426
+ };
427
+ return JSON.stringify({ schemaVersion: REGISTRY_SCHEMA_VERSION, ...body });
428
+ }
429
+ function namespaceOf(item) {
430
+ const body = parseBody(item.r?.S, "scan");
431
+ return { namespace: body.namespace, segment: body.segment };
432
+ }
433
+ function rowToRecord(item, ref) {
434
+ const token = item.v?.N;
435
+ if (token === void 0) {
436
+ throw new IntegrityError(`registry row for segment ${ref.segment} is missing its token`);
437
+ }
438
+ const body = parseBody(item.r?.S, ref.segment);
439
+ return { ...body, token };
440
+ }
441
+ function parseBody(json, ctx) {
442
+ if (json === void 0) throw new IntegrityError(`registry row ${ctx} is missing its body`);
443
+ let parsed;
444
+ try {
445
+ parsed = JSON.parse(json);
446
+ } catch {
447
+ throw new IntegrityError(`registry row ${ctx} body is not valid JSON`);
448
+ }
449
+ if (parsed === null || typeof parsed !== "object") {
450
+ throw new IntegrityError(`registry row ${ctx} body is malformed`);
451
+ }
452
+ const { schemaVersion, ...body } = parsed;
453
+ assertRegistrySchemaVersion(schemaVersion, ctx);
454
+ assertStoredRecordShape(body, ctx);
455
+ return body;
456
+ }
457
+
458
+ export { DynamoDbRegistryDriver, DynamoDbWarmDriver };
459
+ //# sourceMappingURL=index.js.map
460
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/drivers/dynamodb/dynamodb-errors.ts","../../src/drivers/dynamodb/keys.ts","../../src/drivers/dynamodb/warm.ts","../../src/drivers/dynamodb/registry.ts"],"names":["MUTATE_NAMES","ONE","TRUE","FALSE","GetItemCommand","UpdateItemCommand","ConditionalCheckFailedException"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Pure helpers for classifying DynamoDB SDK errors (Phase 4a conflict mapping; transient class 4b).\n *\n * SDK-free + side-effect-free (read structural shapes only), so the transient-vs-fatal decision is\n * unit-testable without DynamoDB-Local or the SDK. Shared AWS shapes come from `_shared/aws-errors`.\n */\nimport { errorName, isNetworkOrTimeout, isSdkRetryable, isServerSide } from '../_shared/aws-errors';\n\n/** A conditional `UpdateItem`/`DeleteItem` failed its condition — an OCC conflict (or write-once collision). */\nexport function isConditionalCheckFailed(err: unknown): boolean {\n return errorName(err) === 'ConditionalCheckFailedException';\n}\n\n/** Names DynamoDB uses for capacity/throttling + server-side faults that are safe to retry. */\nconst TRANSIENT_NAMES = new Set([\n 'ThrottlingException',\n 'ProvisionedThroughputExceededException',\n 'RequestLimitExceeded',\n 'InternalServerError',\n 'ServiceUnavailable',\n 'LimitExceededException',\n]);\n\n/**\n * A transient DynamoDB fault that is safe to retry: throttling / capacity-exceeded, any 5xx, a dropped or\n * timed-out connection, or anything the SDK itself marks retryable. A failed condition is **never** transient\n * (it's a deterministic OCC outcome — the engine's read-modify-write loop owns that retry, not the wrapper).\n */\nexport function isTransient(err: unknown): boolean {\n if (isConditionalCheckFailed(err)) return false;\n return (\n TRANSIENT_NAMES.has(errorName(err) ?? '') ||\n isServerSide(err) ||\n isNetworkOrTimeout(err) ||\n isSdkRetryable(err)\n );\n}\n","/**\n * Logical-ref → DynamoDB key mapping for {@link DynamoDbWarmDriver} (Phase 4a).\n *\n * **Single-table design** (DECISIONS #15): every item is partitioned by segment (`PK`), and the sort key\n * (`SK`) prefix distinguishes entity types — `chunk#<key>` for warm chunk rows, `reg#…` for registry rows\n * (Phase 4b). Pure string logic, no SDK dependency, so it's unit-testable without DynamoDB-Local. Names are\n * re-validated at the boundary (defense in depth — S2); the absent namespace maps to `_default`, which can't\n * collide with a real namespace (the grammar forbids a leading underscore).\n */\nimport { ValidationError } from '@/core/errors';\nimport { validateChunkRef, validateSegmentRef } from '@/core/validate';\nimport type { ChunkRef, SegmentRef } from '@/core/ports';\nimport { namespacePart } from '../_shared/keys';\n\nconst CHUNK_SK_PREFIX = 'chunk#';\n/** chunkKey ∈ [0, 65535] → zero-pad to 5 digits so SK lexicographic order == ascending chunkKey. */\nconst CHUNK_KEY_DIGITS = 5;\n\n/**\n * Validate a caller-supplied `keyPrefix` so prefix-isolation is *structural*, not convention: it must not\n * contain the PK delimiters (`|`, `#`) — which would let one prefix's PK alias another's — or control\n * characters. Empty/undefined means no prefix.\n */\nexport function assertValidKeyPrefix(prefix: string | undefined): void {\n if (prefix === undefined || prefix === '') return;\n for (const ch of prefix) {\n const code = ch.charCodeAt(0);\n if (ch === '|' || ch === '#' || code < 0x20) {\n throw new ValidationError(`keyPrefix must not contain \"|\", \"#\", or control characters`);\n }\n }\n}\n\n/**\n * Partition key for all of a segment's items: `[<prefix>|]ns#<ns>|seg#<segment>`. An optional caller\n * `prefix` lets several logical stores share one physical table (and gives tests cheap isolation); it's\n * opaque (the PK is matched exactly, never parsed back), so no traversal concerns apply.\n */\nexport function partitionKey(ref: SegmentRef, prefix?: string): string {\n validateSegmentRef(ref);\n const base = `ns#${namespacePart(ref.namespace)}|seg#${ref.segment}`;\n return prefix === undefined || prefix === '' ? base : `${prefix}|${base}`;\n}\n\n/** Sort key for one chunk's warm row: `chunk#<zero-padded chunkKey>`. */\nexport function chunkSortKey(chunkKey: number): string {\n return `${CHUNK_SK_PREFIX}${String(chunkKey).padStart(CHUNK_KEY_DIGITS, '0')}`;\n}\n\n/** The `(PK, SK)` of one chunk's warm row, validating the full ref. */\nexport function chunkKeyPair(ref: ChunkRef, prefix?: string): { pk: string; sk: string } {\n validateChunkRef(ref);\n return { pk: partitionKey(ref, prefix), sk: chunkSortKey(ref.chunkKey) };\n}\n\n/** The `begins_with` prefix for querying a segment's chunk rows (excludes `reg#…` items). */\nexport function chunkSortKeyPrefix(): string {\n return CHUNK_SK_PREFIX;\n}\n\n/** Sort key of a segment's single registry row — co-located with its chunk rows (DECISIONS #15). */\nconst REGISTRY_SK = 'reg#';\nexport function registrySortKey(): string {\n return REGISTRY_SK;\n}\n\n/** The `(PK, SK)` of a segment's registry row, validating the ref. */\nexport function registryKeyPair(ref: SegmentRef, prefix?: string): { pk: string; sk: string } {\n validateSegmentRef(ref);\n return { pk: partitionKey(ref, prefix), sk: REGISTRY_SK };\n}\n\n/** Parse a chunkKey out of a `chunk#<n>` sort key, or `null` if it doesn't match (canonical 5-digit). */\nexport function parseChunkSortKey(sk: string): number | null {\n if (!sk.startsWith(CHUNK_SK_PREFIX)) return null;\n const middle = sk.slice(CHUNK_SK_PREFIX.length);\n // Exactly CHUNK_KEY_DIGITS digits — the canonical form the writer emits (so nothing else aliases it).\n if (!new RegExp(`^\\\\d{${CHUNK_KEY_DIGITS}}$`).test(middle)) return null;\n const chunkKey = Number(middle);\n return chunkKey <= 0xffff ? chunkKey : null;\n}\n","/**\n * `DynamoDbWarmDriver` — an {@link IWarmDriver} over DynamoDB (Phase 4a).\n *\n * The production warm tier: real **cross-process** optimistic concurrency via DynamoDB conditional writes\n * (unlike the LocalFs driver's in-process mutex). Each chunk is one item in a **single table** (DECISIONS\n * #15): `PK = ns#…|seg#…`, `SK = chunk#<key>`, with attributes `v` (the OCC token — a monotonic counter),\n * `b` (the delta payload), `del` (tombstone flag). Every mutation is a conditional `UpdateItem` that does\n * the compare-and-set server-side: `ADD v :one` makes the counter strictly increasing and **never reused**,\n * even across delete→recreate (ABA-safe, D3), because a delete *tombstones* (`del=true`, counter advances)\n * rather than removing the item.\n *\n * `@aws-sdk/client-dynamodb` is an **optional peer dependency** (DECISIONS #13) — only consumers of\n * `cloud-roaring/dynamodb` install it; the client is injected. Drivers may use cloud SDKs; only `core/` is\n * SDK-free (lint-enforced).\n */\nimport {\n ConditionalCheckFailedException,\n GetItemCommand,\n QueryCommand,\n UpdateItemCommand,\n type AttributeValue,\n type DynamoDBClient,\n} from '@aws-sdk/client-dynamodb';\nimport {\n IntegrityError,\n TransientError,\n WriteConflictError,\n isIntegrityError,\n} 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 { isTransient } from './dynamodb-errors';\nimport {\n assertValidKeyPrefix,\n chunkKeyPair,\n chunkSortKeyPrefix,\n parseChunkSortKey,\n partitionKey,\n} from './keys';\n\nexport interface DynamoDbWarmDriverOptions {\n /** A constructed DynamoDB client (point its `endpoint` at DynamoDB-Local for tests). */\n readonly client: DynamoDBClient;\n /** The single table holding warm rows (and, in Phase 4b, registry rows). Must already exist. */\n readonly tableName: string;\n /** Optional partition-key prefix so several logical stores can share one table. */\n readonly keyPrefix?: string;\n}\n\n// Attribute names are aliased in every expression (defense against DynamoDB reserved words). DynamoDB\n// rejects any name placeholder that is declared but unreferenced (\"ExpressionAttributeNames unused in\n// expressions\"), so each command declares ONLY the names its own expressions use — no shared catch-all map.\nconst MUTATE_NAMES = { '#v': 'v', '#b': 'b', '#del': 'del' } as const; // put/delete expressions\nconst QUERY_NAMES = { '#pk': 'PK', '#sk': 'SK', '#del': 'del' } as const; // listChunks key + filter\nconst ONE: AttributeValue = { N: '1' };\nconst TRUE: AttributeValue = { BOOL: true };\nconst FALSE: AttributeValue = { BOOL: false };\n\nexport class DynamoDbWarmDriver implements IWarmDriver {\n private readonly client: DynamoDBClient;\n private readonly table: string;\n private readonly keyPrefix: string | undefined;\n\n constructor(options: DynamoDbWarmDriverOptions) {\n assertValidKeyPrefix(options.keyPrefix); // fail fast: a prefix with PK delimiters could alias stores\n this.client = options.client;\n this.table = options.tableName;\n this.keyPrefix = options.keyPrefix;\n }\n\n async get(ref: ChunkRef, opts?: WarmReadOptions): Promise<WarmRow | null> {\n const { pk, sk } = chunkKeyPair(ref, this.keyPrefix);\n let res;\n try {\n res = await this.client.send(\n new GetItemCommand({\n TableName: this.table,\n Key: { PK: { S: pk }, SK: { S: sk } },\n // Strong by default (the OCC read-modify-write must see the latest committed value); a read path\n // may opt into an eventually-consistent read (~½ RCU) via `warmReadConsistency: 'eventual'` (gap #9).\n ConsistentRead: opts?.consistent ?? true,\n }),\n );\n } catch (err) {\n throw this.mapError(err, ref.chunkKey);\n }\n if (res.Item === undefined || res.Item.del?.BOOL === true) return null;\n return this.rowFrom(res.Item, ref.chunkKey);\n }\n\n async putConditional(\n ref: ChunkRef,\n bytes: Uint8Array,\n expected: Token | NoRow,\n ): Promise<{ token: Token }> {\n const { pk, sk } = chunkKeyPair(ref, this.keyPrefix);\n // Create-if-absent (or over a tombstone) vs token-fenced update — both server-side CAS via a condition.\n const command =\n expected === NO_ROW\n ? new UpdateItemCommand({\n TableName: this.table,\n Key: { PK: { S: pk }, SK: { S: sk } },\n ConditionExpression: 'attribute_not_exists(#v) OR #del = :true',\n UpdateExpression: 'ADD #v :one SET #b = :b, #del = :false',\n ExpressionAttributeNames: MUTATE_NAMES,\n ExpressionAttributeValues: {\n ':one': ONE,\n ':b': { B: bytes },\n ':true': TRUE,\n ':false': FALSE,\n },\n ReturnValues: 'UPDATED_NEW',\n })\n : new UpdateItemCommand({\n TableName: this.table,\n Key: { PK: { S: pk }, SK: { S: sk } },\n ConditionExpression: '#v = :expected AND #del = :false',\n UpdateExpression: 'ADD #v :one SET #b = :b',\n ExpressionAttributeNames: MUTATE_NAMES,\n ExpressionAttributeValues: {\n ':expected': { N: expected },\n ':one': ONE,\n ':b': { B: bytes },\n ':false': FALSE,\n },\n ReturnValues: 'UPDATED_NEW',\n });\n try {\n const res = await this.client.send(command);\n const token = res.Attributes?.v?.N;\n if (token === undefined) throw new IntegrityError('DynamoDB UpdateItem returned no token');\n return { token };\n } catch (err) {\n throw this.mapError(err, ref.chunkKey);\n }\n }\n\n async deleteConditional(ref: ChunkRef, expected: Token): Promise<void> {\n const { pk, sk } = chunkKeyPair(ref, this.keyPrefix);\n try {\n await this.client.send(\n new UpdateItemCommand({\n TableName: this.table,\n Key: { PK: { S: pk }, SK: { S: sk } },\n // Tombstone (advance the counter, drop the payload) — keeps the token monotonic for ABA-safety.\n ConditionExpression: '#v = :expected AND #del = :false',\n UpdateExpression: 'ADD #v :one SET #del = :true REMOVE #b',\n ExpressionAttributeNames: MUTATE_NAMES,\n ExpressionAttributeValues: {\n ':expected': { N: expected },\n ':one': ONE,\n ':true': TRUE,\n ':false': FALSE,\n },\n }),\n );\n } catch (err) {\n throw this.mapError(err, ref.chunkKey);\n }\n }\n\n async *listChunks(\n ref: SegmentRef,\n opts?: WarmReadOptions,\n ): AsyncIterable<{ chunkKey: number } & WarmRow> {\n const pk = partitionKey(ref, this.keyPrefix);\n let startKey: Record<string, AttributeValue> | undefined;\n do {\n let res;\n try {\n res = await this.client.send(\n new QueryCommand({\n TableName: this.table,\n KeyConditionExpression: '#pk = :pk AND begins_with(#sk, :prefix)',\n FilterExpression: '#del = :false', // exclude tombstones (server-side)\n ExpressionAttributeNames: QUERY_NAMES,\n ExpressionAttributeValues: {\n ':pk': { S: pk },\n ':prefix': { S: chunkSortKeyPrefix() },\n ':false': FALSE,\n },\n ConsistentRead: opts?.consistent ?? true, // strong by default; eventual when a read path opts in (gap #9)\n ExclusiveStartKey: startKey,\n }),\n );\n } catch (err) {\n // -1: a list error isn't scoped to one chunk. The decorator re-enumerates the whole listing on a\n // transient fault, so a mid-pagination blip is survived without re-yielding earlier pages downstream.\n throw this.mapError(err, -1);\n }\n // Query returns items in ascending SK order; zero-padded chunk# ⇒ ascending chunkKey.\n for (const item of res.Items ?? []) {\n const chunkKey = parseChunkSortKey(item.SK?.S ?? '');\n if (chunkKey === null) continue; // a reg#… or foreign row slipped the filter — skip\n yield { chunkKey, ...this.rowFrom(item, chunkKey) };\n }\n startKey = res.LastEvaluatedKey;\n } while (startKey !== undefined);\n }\n\n /**\n * Build a {@link WarmRow} from a raw item. Only ever called for a **live** row (callers filter\n * tombstones first), so both `v` and `b` MUST be present — a missing attribute means a corrupt or\n * foreign item, which we reject (untrusted-bytes posture, invariant 5) rather than paper over.\n */\n private rowFrom(item: Record<string, AttributeValue>, chunkKey: number): WarmRow {\n const token = item.v?.N;\n if (token === undefined) {\n throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);\n }\n const bytes = item.b?.B;\n if (bytes === undefined) {\n throw new IntegrityError(`live warm row for chunk ${chunkKey} is missing its payload`);\n }\n return { token, bytes };\n }\n\n /**\n * Translate a raw SDK/driver error to our typed vocabulary: a failed condition is an OCC conflict; a\n * throttle/5xx/dropped-connection is a (retryable) {@link TransientError}; everything else propagates\n * unchanged. Applied at every `client.send` site so callers (and the retry decorator) only ever see typed\n * errors. Order matters: a conflict is **never** transient (it's deterministic — the engine's OCC loop\n * owns that retry), so it's classified first.\n */\n private mapError(err: unknown, chunkKey: number): unknown {\n if (isIntegrityError(err)) return err;\n // `instanceof` is the primary check; the name fallback covers an SDK that re-wraps the error so the\n // class identity is lost — a conflict must never silently leak as a generic error.\n if (\n err instanceof ConditionalCheckFailedException ||\n (err as { name?: string } | null)?.name === 'ConditionalCheckFailedException'\n ) {\n return new WriteConflictError(`OCC conflict on chunk ${chunkKey}`);\n }\n if (isTransient(err)) {\n return new TransientError(\n `transient DynamoDB fault on chunk ${chunkKey}: ${(err as { name?: string } | null)?.name ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n }\n}\n","/**\n * `DynamoDbRegistryDriver` — an {@link IRegistryDriver} over DynamoDB (Phase 4c).\n *\n * One item per segment, **co-located with that segment's warm rows** in the single table (DECISIONS #15):\n * `PK = ns#…|seg#…`, `SK = reg#`. OCC mirrors the warm tier — a conditional `UpdateItem` with `ADD v :one`\n * gives a monotonic, never-reused token; a delete **tombstones** (`del=true`, counter advances) so a\n * recreate's token is always greater (ABA-safe). The record body is stored as a JSON string (`r`); the OCC\n * token is the counter `v` (so the body never has to encode its own token).\n *\n * `compareAndSwap` is a read-merge-conditional-write: it fetches the current body to apply the caller's\n * `patch`, then writes under `#v = :expected` — so a concurrent change between the read and the write fails\n * the condition (no lost update), exactly like the engine's chunk OCC.\n *\n * `list` is a `Scan` with a `reg#` filter (discovery is infrequent — the compaction daemon, Phase 4d). On a\n * large shared table that reads every partition; a namespace-keyed GSI is the scale-up, deferred until a\n * deployment needs it (YAGNI). `@aws-sdk/client-dynamodb` is an optional peer dependency (DECISIONS #13).\n */\nimport {\n ConditionalCheckFailedException,\n GetItemCommand,\n ScanCommand,\n UpdateItemCommand,\n type AttributeValue,\n type DynamoDBClient,\n} from '@aws-sdk/client-dynamodb';\nimport {\n IntegrityError,\n TransientError,\n WriteConflictError,\n isIntegrityError,\n} from '@/core/errors';\nimport type {\n IRegistryDriver,\n NewRegistryRecord,\n RegCaps,\n RegistryPatch,\n RegistryRecord,\n SegmentRef,\n Token,\n} from '@/core/ports';\nimport {\n applyRegistryPatch,\n assertRegistrySchemaVersion,\n assertStoredRecordShape,\n recordFromNew,\n REGISTRY_SCHEMA_VERSION,\n validateNewRegistryRecord,\n validateRegistryPatch,\n} from '../_shared/registry';\nimport { isTransient } from './dynamodb-errors';\nimport { assertValidKeyPrefix, partitionKey, registryKeyPair, registrySortKey } from './keys';\n\n// Only the names each command's expressions reference (DynamoDB rejects unused declared placeholders).\nconst MUTATE_NAMES = { '#v': 'v', '#r': 'r', '#del': 'del' } as const;\nconst SCAN_NAMES = { '#sk': 'SK', '#del': 'del', '#pk': 'PK' } as const;\nconst ONE: AttributeValue = { N: '1' };\nconst TRUE: AttributeValue = { BOOL: true };\nconst FALSE: AttributeValue = { BOOL: false };\n\nexport interface DynamoDbRegistryDriverOptions {\n readonly client: DynamoDBClient;\n /** The single table holding warm + registry rows. Must already exist. */\n readonly tableName: string;\n /** Optional partition-key prefix so several logical stores can share one table. */\n readonly keyPrefix?: string;\n /** Injected clock for `createdAt`/`updatedAt`; defaults to `Date.now`. */\n readonly now?: () => number;\n}\n\nexport class DynamoDbRegistryDriver implements IRegistryDriver {\n private readonly client: DynamoDBClient;\n private readonly table: string;\n private readonly keyPrefix: string | undefined;\n private readonly now: () => number;\n\n constructor(options: DynamoDbRegistryDriverOptions) {\n assertValidKeyPrefix(options.keyPrefix);\n this.client = options.client;\n this.table = options.tableName;\n this.keyPrefix = options.keyPrefix;\n this.now = options.now ?? (() => Date.now());\n }\n\n capabilities(): RegCaps {\n return { strongRead: true };\n }\n\n async get(ref: SegmentRef): Promise<RegistryRecord | null> {\n const { pk, sk } = registryKeyPair(ref, this.keyPrefix);\n let res;\n try {\n res = await this.client.send(\n new GetItemCommand({\n TableName: this.table,\n Key: { PK: { S: pk }, SK: { S: sk } },\n ConsistentRead: true, // currentGen feeds correctness — must be the latest committed value\n }),\n );\n } catch (err) {\n throw this.mapError(err, ref);\n }\n if (res.Item === undefined || res.Item.del?.BOOL === true) return null;\n return rowToRecord(res.Item, ref);\n }\n\n async create(ref: SegmentRef, record: NewRegistryRecord): Promise<{ token: Token }> {\n validateNewRegistryRecord(record);\n const { pk, sk } = registryKeyPair(ref, this.keyPrefix);\n const now = this.now();\n const body = serializeBody(recordFromNew(ref, record, now, '')); // token derived from `v`, not stored\n try {\n const res = await this.client.send(\n new UpdateItemCommand({\n TableName: this.table,\n Key: { PK: { S: pk }, SK: { S: sk } },\n ConditionExpression: 'attribute_not_exists(#v) OR #del = :true',\n UpdateExpression: 'ADD #v :one SET #r = :r, #del = :false',\n ExpressionAttributeNames: MUTATE_NAMES,\n ExpressionAttributeValues: {\n ':one': ONE,\n ':r': { S: body },\n ':true': TRUE,\n ':false': FALSE,\n },\n ReturnValues: 'UPDATED_NEW',\n }),\n );\n return { token: tokenOf(res.Attributes) };\n } catch (err) {\n throw this.mapError(err, ref, 'already exists');\n }\n }\n\n async compareAndSwap(\n ref: SegmentRef,\n expected: Token,\n patch: RegistryPatch,\n ): Promise<{ token: Token }> {\n validateRegistryPatch(patch);\n const current = await this.get(ref); // fetch the body to merge; the conditional write fences concurrency\n if (current === null || current.token !== expected) {\n throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);\n }\n const { pk, sk } = registryKeyPair(ref, this.keyPrefix);\n const body = serializeBody(applyRegistryPatch(current, patch, this.now(), ''));\n try {\n const res = await this.client.send(\n new UpdateItemCommand({\n TableName: this.table,\n Key: { PK: { S: pk }, SK: { S: sk } },\n ConditionExpression: '#v = :expected AND #del = :false',\n UpdateExpression: 'ADD #v :one SET #r = :r',\n ExpressionAttributeNames: MUTATE_NAMES,\n ExpressionAttributeValues: {\n ':expected': { N: expected },\n ':one': ONE,\n ':r': { S: body },\n ':false': FALSE,\n },\n ReturnValues: 'UPDATED_NEW',\n }),\n );\n return { token: tokenOf(res.Attributes) };\n } catch (err) {\n throw this.mapError(err, ref);\n }\n }\n\n async *list(namespace?: string): AsyncIterable<RegistryRecord> {\n const filter = ['#sk = :reg', '#del = :false'];\n const values: Record<string, AttributeValue> = {\n ':reg': { S: registrySortKey() },\n ':false': FALSE,\n };\n // Scope by namespace (and/or this store's keyPrefix) via a PK begins_with — items still scanned, filtered.\n const pkPrefix = this.scanPkPrefix(namespace);\n if (pkPrefix !== undefined) {\n filter.push('begins_with(#pk, :pk)');\n values[':pk'] = { S: pkPrefix };\n }\n let startKey: Record<string, AttributeValue> | undefined;\n do {\n let res;\n try {\n res = await this.client.send(\n new ScanCommand({\n TableName: this.table,\n FilterExpression: filter.join(' AND '),\n ExpressionAttributeNames:\n pkPrefix !== undefined ? SCAN_NAMES : { '#sk': 'SK', '#del': 'del' },\n ExpressionAttributeValues: values,\n ExclusiveStartKey: startKey,\n }),\n );\n } catch (err) {\n throw this.mapError(err);\n }\n for (const item of res.Items ?? []) {\n yield rowToRecord(item, namespaceOf(item));\n }\n startKey = res.LastEvaluatedKey;\n } while (startKey !== undefined);\n }\n\n async delete(ref: SegmentRef): Promise<void> {\n const { pk, sk } = registryKeyPair(ref, this.keyPrefix);\n // Tombstone (advance the counter, drop the body) — idempotent, and keeps the token monotonic (ABA-safe).\n try {\n await this.client.send(\n new UpdateItemCommand({\n TableName: this.table,\n Key: { PK: { S: pk }, SK: { S: sk } },\n UpdateExpression: 'ADD #v :one SET #del = :true REMOVE #r',\n ExpressionAttributeNames: MUTATE_NAMES,\n ExpressionAttributeValues: { ':one': ONE, ':true': TRUE },\n }),\n );\n } catch (err) {\n throw this.mapError(err, ref);\n }\n }\n\n /** The PK prefix to scope a `list` scan, or `undefined` for an unscoped scan (no namespace, no keyPrefix). */\n private scanPkPrefix(namespace: string | undefined): string | undefined {\n if (namespace !== undefined) {\n // partitionKey for a sentinel segment, minus the segment, gives the `…|seg#` prefix shared by the ns.\n const full = partitionKey({ namespace, segment: 'x' }, this.keyPrefix);\n return full.slice(0, full.length - 'x'.length);\n }\n return this.keyPrefix !== undefined ? `${this.keyPrefix}|` : undefined;\n }\n\n /** Map a raw SDK error to the typed vocabulary (conflict / transient / passthrough). */\n private mapError(err: unknown, ref?: SegmentRef, conflictHint = 'OCC token mismatch'): unknown {\n if (isIntegrityError(err)) return err;\n if (\n err instanceof ConditionalCheckFailedException ||\n (err as { name?: string } | null)?.name === 'ConditionalCheckFailedException'\n ) {\n return new WriteConflictError(\n `registry ${conflictHint}${ref ? ` for segment ${ref.segment}` : ''}`,\n );\n }\n if (isTransient(err)) {\n return new TransientError(\n `transient DynamoDB fault on registry${ref ? ` ${ref.segment}` : ''}: ${(err as { name?: string } | null)?.name ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n }\n}\n\n/** The OCC token from an `UPDATED_NEW` response (the post-increment counter). */\nfunction tokenOf(attrs: Record<string, AttributeValue> | undefined): Token {\n const token = attrs?.v?.N;\n if (token === undefined)\n throw new IntegrityError('DynamoDB registry UpdateItem returned no token');\n return token;\n}\n\n/** Serialize the record body (everything except the token, which is the OCC counter `v`). */\nfunction serializeBody(record: RegistryRecord): string {\n const body: Omit<RegistryRecord, 'token'> = {\n namespace: record.namespace,\n segment: record.segment,\n currentGen: record.currentGen,\n wrappedDeks: record.wrappedDeks,\n keyId: record.keyId,\n dirtyChunkCount: record.dirtyChunkCount,\n status: record.status,\n leaseOwner: record.leaseOwner,\n leaseExpiresAt: record.leaseExpiresAt,\n lastCompactedAt: record.lastCompactedAt,\n consecutiveFailures: record.consecutiveFailures,\n retention: record.retention,\n residency: record.residency,\n createdAt: record.createdAt,\n updatedAt: record.updatedAt,\n };\n // `schemaVersion` stamps the persisted body (format freeze) alongside the LocalFs/S3 envelope. JSON.stringify\n // drops `undefined` fields (keyId/retention/residency).\n return JSON.stringify({ schemaVersion: REGISTRY_SCHEMA_VERSION, ...body });\n}\n\n/** Reconstruct the namespace from a scanned item's body (so `list` doesn't have to parse the PK). */\nfunction namespaceOf(item: Record<string, AttributeValue>): SegmentRef {\n const body = parseBody(item.r?.S, 'scan');\n return { namespace: body.namespace, segment: body.segment };\n}\n\n/** Build a {@link RegistryRecord} from a live item: parse the JSON body, attach the token from `v`. */\nfunction rowToRecord(item: Record<string, AttributeValue>, ref: SegmentRef): RegistryRecord {\n const token = item.v?.N;\n if (token === undefined) {\n throw new IntegrityError(`registry row for segment ${ref.segment} is missing its token`);\n }\n const body = parseBody(item.r?.S, ref.segment);\n return { ...body, token };\n}\n\n/** Parse + structurally validate the stored JSON body (untrusted bytes, invariant 5). */\nfunction parseBody(json: string | undefined, ctx: string): Omit<RegistryRecord, 'token'> {\n if (json === undefined) throw new IntegrityError(`registry row ${ctx} is missing its body`);\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new IntegrityError(`registry row ${ctx} body is not valid JSON`);\n }\n if (parsed === null || typeof parsed !== 'object') {\n throw new IntegrityError(`registry row ${ctx} body is malformed`);\n }\n // `schemaVersion` is a wire-only stamp (format freeze): validate it, then strip it so the in-memory record stays\n // pure (absent → legacy v1; a newer version → UnsupportedError).\n const { schemaVersion, ...body } = parsed as Record<string, unknown>;\n assertRegistrySchemaVersion(schemaVersion, ctx);\n assertStoredRecordShape(body, ctx);\n return body as Omit<RegistryRecord, 'token'>;\n}\n"]}