@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.
- package/LICENSE +202 -0
- package/NOTICE +12 -0
- package/README.md +22 -0
- package/dist/azure/index.cjs +368 -0
- package/dist/azure/index.cjs.map +1 -0
- package/dist/azure/index.d.cts +48 -0
- package/dist/azure/index.d.ts +48 -0
- package/dist/azure/index.js +304 -0
- package/dist/azure/index.js.map +1 -0
- package/dist/cassandra/index.cjs +265 -0
- package/dist/cassandra/index.cjs.map +1 -0
- package/dist/cassandra/index.d.cts +52 -0
- package/dist/cassandra/index.d.ts +52 -0
- package/dist/cassandra/index.js +204 -0
- package/dist/cassandra/index.js.map +1 -0
- package/dist/chunk-2YDULGXS.js +203 -0
- package/dist/chunk-2YDULGXS.js.map +1 -0
- package/dist/chunk-7LMLYSVJ.js +43 -0
- package/dist/chunk-7LMLYSVJ.js.map +1 -0
- package/dist/chunk-AS6ODRLT.js +6 -0
- package/dist/chunk-AS6ODRLT.js.map +1 -0
- package/dist/chunk-NUIDEEFZ.js +91 -0
- package/dist/chunk-NUIDEEFZ.js.map +1 -0
- package/dist/chunk-SNJVZ227.js +35 -0
- package/dist/chunk-SNJVZ227.js.map +1 -0
- package/dist/dynamodb/index.cjs +731 -0
- package/dist/dynamodb/index.cjs.map +1 -0
- package/dist/dynamodb/index.d.cts +106 -0
- package/dist/dynamodb/index.d.ts +106 -0
- package/dist/dynamodb/index.js +460 -0
- package/dist/dynamodb/index.js.map +1 -0
- package/dist/gcs/index.cjs +343 -0
- package/dist/gcs/index.cjs.map +1 -0
- package/dist/gcs/index.d.cts +46 -0
- package/dist/gcs/index.d.ts +46 -0
- package/dist/gcs/index.js +279 -0
- package/dist/gcs/index.js.map +1 -0
- package/dist/index.cjs +4031 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1945 -0
- package/dist/index.d.ts +1945 -0
- package/dist/index.js +3642 -0
- package/dist/index.js.map +1 -0
- package/dist/mongodb/index.cjs +260 -0
- package/dist/mongodb/index.cjs.map +1 -0
- package/dist/mongodb/index.d.cts +45 -0
- package/dist/mongodb/index.d.ts +45 -0
- package/dist/mongodb/index.js +199 -0
- package/dist/mongodb/index.js.map +1 -0
- package/dist/mysql/index.cjs +281 -0
- package/dist/mysql/index.cjs.map +1 -0
- package/dist/mysql/index.d.cts +56 -0
- package/dist/mysql/index.d.ts +56 -0
- package/dist/mysql/index.js +211 -0
- package/dist/mysql/index.js.map +1 -0
- package/dist/ports-D3BrJ6ax.d.cts +357 -0
- package/dist/ports-D3BrJ6ax.d.ts +357 -0
- package/dist/postgres/index.cjs +273 -0
- package/dist/postgres/index.cjs.map +1 -0
- package/dist/postgres/index.d.cts +54 -0
- package/dist/postgres/index.d.ts +54 -0
- package/dist/postgres/index.js +203 -0
- package/dist/postgres/index.js.map +1 -0
- package/dist/redis/index.cjs +257 -0
- package/dist/redis/index.cjs.map +1 -0
- package/dist/redis/index.d.cts +42 -0
- package/dist/redis/index.d.ts +42 -0
- package/dist/redis/index.js +197 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/s3/index.cjs +891 -0
- package/dist/s3/index.cjs.map +1 -0
- package/dist/s3/index.d.cts +112 -0
- package/dist/s3/index.d.ts +112 -0
- package/dist/s3/index.js +555 -0
- package/dist/s3/index.js.map +1 -0
- package/package.json +172 -0
|
@@ -0,0 +1,731 @@
|
|
|
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 (!Number.isInteger(gen) || gen < 0) {
|
|
351
|
+
throw new ValidationError(`currentGen must be a non-negative integer; got ${gen}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function validateCount(count, field = "dirtyChunkCount") {
|
|
355
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
356
|
+
throw new ValidationError(`${field} must be a non-negative integer; got ${count}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function validateTimestamp(ms, field) {
|
|
360
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
361
|
+
throw new ValidationError(`${field} must be a non-negative finite number; got ${ms}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function validateStatus(status) {
|
|
365
|
+
if (!STATUSES.includes(status)) {
|
|
366
|
+
throw new ValidationError(`status must be one of ${STATUSES.join("/")}; got ${status}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function validateGovernance(meta, field) {
|
|
370
|
+
if (meta === void 0) return;
|
|
371
|
+
let json;
|
|
372
|
+
try {
|
|
373
|
+
json = JSON.stringify(meta);
|
|
374
|
+
} catch {
|
|
375
|
+
throw new ValidationError(`${field} must be JSON-serializable`);
|
|
376
|
+
}
|
|
377
|
+
if (json.length > MAX_GOVERNANCE_BYTES) {
|
|
378
|
+
throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function validateWrappedDeks(value, isStored) {
|
|
382
|
+
if (value === void 0) return;
|
|
383
|
+
const fail = (msg) => {
|
|
384
|
+
throw isStored ? new IntegrityError(msg) : new ValidationError(msg);
|
|
385
|
+
};
|
|
386
|
+
if (!Array.isArray(value)) fail("wrappedDeks must be an array");
|
|
387
|
+
const list = value;
|
|
388
|
+
if (list.length === 0) fail("wrappedDeks must be non-empty when present");
|
|
389
|
+
if (list.length > MAX_WRAPPED_DEKS)
|
|
390
|
+
fail(`wrappedDeks has ${list.length} entries, cap ${MAX_WRAPPED_DEKS}`);
|
|
391
|
+
for (const w of list) {
|
|
392
|
+
if (w === null || typeof w !== "object") fail("wrappedDeks entry must be an object");
|
|
393
|
+
const { keyId, wrapped } = w;
|
|
394
|
+
if (typeof keyId !== "string" || keyId.length === 0)
|
|
395
|
+
fail("wrappedDeks entry needs a non-empty keyId");
|
|
396
|
+
if (typeof wrapped !== "string" || wrapped.length === 0)
|
|
397
|
+
fail("wrappedDeks entry needs a non-empty wrapped blob");
|
|
398
|
+
if (wrapped.length > MAX_WRAPPED_DEK_BYTES)
|
|
399
|
+
fail(`wrappedDeks entry is ${wrapped.length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function validateNewRegistryRecord(rec) {
|
|
403
|
+
validateGeneration(rec.currentGen);
|
|
404
|
+
if (rec.dirtyChunkCount !== void 0) validateCount(rec.dirtyChunkCount);
|
|
405
|
+
if (rec.status !== void 0) validateStatus(rec.status);
|
|
406
|
+
validateWrappedDeks(rec.wrappedDeks, false);
|
|
407
|
+
validateGovernance(rec.retention, "retention");
|
|
408
|
+
validateGovernance(rec.residency, "residency");
|
|
409
|
+
}
|
|
410
|
+
function validateRegistryPatch(patch) {
|
|
411
|
+
if (patch.currentGen !== void 0) validateGeneration(patch.currentGen);
|
|
412
|
+
if (patch.dirtyChunkCount !== void 0) validateCount(patch.dirtyChunkCount);
|
|
413
|
+
if (patch.consecutiveFailures !== void 0)
|
|
414
|
+
validateCount(patch.consecutiveFailures, "consecutiveFailures");
|
|
415
|
+
if (patch.lastCompactedAt !== void 0)
|
|
416
|
+
validateTimestamp(patch.lastCompactedAt, "lastCompactedAt");
|
|
417
|
+
if (patch.status !== void 0) validateStatus(patch.status);
|
|
418
|
+
if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, false);
|
|
419
|
+
if ("retention" in patch) validateGovernance(patch.retention, "retention");
|
|
420
|
+
if ("residency" in patch) validateGovernance(patch.residency, "residency");
|
|
421
|
+
}
|
|
422
|
+
var REGISTRY_SCHEMA_VERSION = 1;
|
|
423
|
+
function assertRegistrySchemaVersion(raw, ctx) {
|
|
424
|
+
if (raw === void 0) return;
|
|
425
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
|
|
426
|
+
throw new IntegrityError(`registry row has a malformed schemaVersion (${String(raw)}): ${ctx}`);
|
|
427
|
+
}
|
|
428
|
+
if (raw > REGISTRY_SCHEMA_VERSION) {
|
|
429
|
+
throw new UnsupportedError(
|
|
430
|
+
`registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function recordFromNew(ref, rec, now, token) {
|
|
435
|
+
return {
|
|
436
|
+
namespace: ref.namespace,
|
|
437
|
+
segment: ref.segment,
|
|
438
|
+
currentGen: rec.currentGen,
|
|
439
|
+
wrappedDeks: rec.wrappedDeks,
|
|
440
|
+
keyId: rec.keyId,
|
|
441
|
+
dirtyChunkCount: rec.dirtyChunkCount ?? 0,
|
|
442
|
+
status: rec.status ?? "active",
|
|
443
|
+
consecutiveFailures: 0,
|
|
444
|
+
// daemon health (Phase D); lastCompactedAt stays absent until first compaction
|
|
445
|
+
retention: rec.retention,
|
|
446
|
+
residency: rec.residency,
|
|
447
|
+
createdAt: now,
|
|
448
|
+
updatedAt: now,
|
|
449
|
+
token
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function assertStoredRecordShape(r, ctx) {
|
|
453
|
+
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") {
|
|
454
|
+
throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
|
|
455
|
+
}
|
|
456
|
+
if (!Number.isInteger(r.currentGen) || r.currentGen < 0) {
|
|
457
|
+
throw new IntegrityError(`registry record has an invalid currentGen (${r.currentGen}): ${ctx}`);
|
|
458
|
+
}
|
|
459
|
+
if (!Number.isInteger(r.dirtyChunkCount) || r.dirtyChunkCount < 0) {
|
|
460
|
+
throw new IntegrityError(`registry record has an invalid dirtyChunkCount: ${ctx}`);
|
|
461
|
+
}
|
|
462
|
+
if (!STATUSES.includes(r.status)) {
|
|
463
|
+
throw new IntegrityError(`registry record has an unknown status (${r.status}): ${ctx}`);
|
|
464
|
+
}
|
|
465
|
+
if (r.leaseOwner !== void 0 && typeof r.leaseOwner !== "string") {
|
|
466
|
+
throw new IntegrityError(`registry record has an invalid leaseOwner: ${ctx}`);
|
|
467
|
+
}
|
|
468
|
+
if (r.leaseExpiresAt !== void 0 && typeof r.leaseExpiresAt !== "number") {
|
|
469
|
+
throw new IntegrityError(`registry record has an invalid leaseExpiresAt: ${ctx}`);
|
|
470
|
+
}
|
|
471
|
+
if (r.consecutiveFailures !== void 0 && (!Number.isInteger(r.consecutiveFailures) || r.consecutiveFailures < 0)) {
|
|
472
|
+
throw new IntegrityError(`registry record has an invalid consecutiveFailures: ${ctx}`);
|
|
473
|
+
}
|
|
474
|
+
if (r.lastCompactedAt !== void 0 && (typeof r.lastCompactedAt !== "number" || !Number.isFinite(r.lastCompactedAt) || r.lastCompactedAt < 0)) {
|
|
475
|
+
throw new IntegrityError(`registry record has an invalid lastCompactedAt: ${ctx}`);
|
|
476
|
+
}
|
|
477
|
+
if (r.keyId !== void 0 && typeof r.keyId !== "string") {
|
|
478
|
+
throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);
|
|
479
|
+
}
|
|
480
|
+
validateWrappedDeks(r.wrappedDeks, true);
|
|
481
|
+
}
|
|
482
|
+
function applyRegistryPatch(prev, patch, now, token) {
|
|
483
|
+
return {
|
|
484
|
+
namespace: prev.namespace,
|
|
485
|
+
segment: prev.segment,
|
|
486
|
+
currentGen: patch.currentGen ?? prev.currentGen,
|
|
487
|
+
wrappedDeks: "wrappedDeks" in patch ? patch.wrappedDeks : prev.wrappedDeks,
|
|
488
|
+
keyId: "keyId" in patch ? patch.keyId : prev.keyId,
|
|
489
|
+
dirtyChunkCount: patch.dirtyChunkCount ?? prev.dirtyChunkCount,
|
|
490
|
+
status: patch.status ?? prev.status,
|
|
491
|
+
leaseOwner: "leaseOwner" in patch ? patch.leaseOwner : prev.leaseOwner,
|
|
492
|
+
leaseExpiresAt: "leaseExpiresAt" in patch ? patch.leaseExpiresAt : prev.leaseExpiresAt,
|
|
493
|
+
lastCompactedAt: "lastCompactedAt" in patch ? patch.lastCompactedAt : prev.lastCompactedAt,
|
|
494
|
+
consecutiveFailures: patch.consecutiveFailures ?? prev.consecutiveFailures ?? 0,
|
|
495
|
+
retention: "retention" in patch ? patch.retention : prev.retention,
|
|
496
|
+
residency: "residency" in patch ? patch.residency : prev.residency,
|
|
497
|
+
createdAt: prev.createdAt,
|
|
498
|
+
updatedAt: now,
|
|
499
|
+
token
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/drivers/dynamodb/registry.ts
|
|
504
|
+
var MUTATE_NAMES2 = { "#v": "v", "#r": "r", "#del": "del" };
|
|
505
|
+
var SCAN_NAMES = { "#sk": "SK", "#del": "del", "#pk": "PK" };
|
|
506
|
+
var ONE2 = { N: "1" };
|
|
507
|
+
var TRUE2 = { BOOL: true };
|
|
508
|
+
var FALSE2 = { BOOL: false };
|
|
509
|
+
var DynamoDbRegistryDriver = class {
|
|
510
|
+
client;
|
|
511
|
+
table;
|
|
512
|
+
keyPrefix;
|
|
513
|
+
now;
|
|
514
|
+
constructor(options) {
|
|
515
|
+
assertValidKeyPrefix(options.keyPrefix);
|
|
516
|
+
this.client = options.client;
|
|
517
|
+
this.table = options.tableName;
|
|
518
|
+
this.keyPrefix = options.keyPrefix;
|
|
519
|
+
this.now = options.now ?? (() => Date.now());
|
|
520
|
+
}
|
|
521
|
+
capabilities() {
|
|
522
|
+
return { strongRead: true };
|
|
523
|
+
}
|
|
524
|
+
async get(ref) {
|
|
525
|
+
const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
|
|
526
|
+
let res;
|
|
527
|
+
try {
|
|
528
|
+
res = await this.client.send(
|
|
529
|
+
new clientDynamodb.GetItemCommand({
|
|
530
|
+
TableName: this.table,
|
|
531
|
+
Key: { PK: { S: pk }, SK: { S: sk } },
|
|
532
|
+
ConsistentRead: true
|
|
533
|
+
// currentGen feeds correctness — must be the latest committed value
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
throw this.mapError(err, ref);
|
|
538
|
+
}
|
|
539
|
+
if (res.Item === void 0 || res.Item.del?.BOOL === true) return null;
|
|
540
|
+
return rowToRecord(res.Item, ref);
|
|
541
|
+
}
|
|
542
|
+
async create(ref, record) {
|
|
543
|
+
validateNewRegistryRecord(record);
|
|
544
|
+
const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
|
|
545
|
+
const now = this.now();
|
|
546
|
+
const body = serializeBody(recordFromNew(ref, record, now, ""));
|
|
547
|
+
try {
|
|
548
|
+
const res = await this.client.send(
|
|
549
|
+
new clientDynamodb.UpdateItemCommand({
|
|
550
|
+
TableName: this.table,
|
|
551
|
+
Key: { PK: { S: pk }, SK: { S: sk } },
|
|
552
|
+
ConditionExpression: "attribute_not_exists(#v) OR #del = :true",
|
|
553
|
+
UpdateExpression: "ADD #v :one SET #r = :r, #del = :false",
|
|
554
|
+
ExpressionAttributeNames: MUTATE_NAMES2,
|
|
555
|
+
ExpressionAttributeValues: {
|
|
556
|
+
":one": ONE2,
|
|
557
|
+
":r": { S: body },
|
|
558
|
+
":true": TRUE2,
|
|
559
|
+
":false": FALSE2
|
|
560
|
+
},
|
|
561
|
+
ReturnValues: "UPDATED_NEW"
|
|
562
|
+
})
|
|
563
|
+
);
|
|
564
|
+
return { token: tokenOf(res.Attributes) };
|
|
565
|
+
} catch (err) {
|
|
566
|
+
throw this.mapError(err, ref, "already exists");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async compareAndSwap(ref, expected, patch) {
|
|
570
|
+
validateRegistryPatch(patch);
|
|
571
|
+
const current = await this.get(ref);
|
|
572
|
+
if (current === null || current.token !== expected) {
|
|
573
|
+
throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
|
|
574
|
+
}
|
|
575
|
+
const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
|
|
576
|
+
const body = serializeBody(applyRegistryPatch(current, patch, this.now(), ""));
|
|
577
|
+
try {
|
|
578
|
+
const res = await this.client.send(
|
|
579
|
+
new clientDynamodb.UpdateItemCommand({
|
|
580
|
+
TableName: this.table,
|
|
581
|
+
Key: { PK: { S: pk }, SK: { S: sk } },
|
|
582
|
+
ConditionExpression: "#v = :expected AND #del = :false",
|
|
583
|
+
UpdateExpression: "ADD #v :one SET #r = :r",
|
|
584
|
+
ExpressionAttributeNames: MUTATE_NAMES2,
|
|
585
|
+
ExpressionAttributeValues: {
|
|
586
|
+
":expected": { N: expected },
|
|
587
|
+
":one": ONE2,
|
|
588
|
+
":r": { S: body },
|
|
589
|
+
":false": FALSE2
|
|
590
|
+
},
|
|
591
|
+
ReturnValues: "UPDATED_NEW"
|
|
592
|
+
})
|
|
593
|
+
);
|
|
594
|
+
return { token: tokenOf(res.Attributes) };
|
|
595
|
+
} catch (err) {
|
|
596
|
+
throw this.mapError(err, ref);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async *list(namespace) {
|
|
600
|
+
const filter = ["#sk = :reg", "#del = :false"];
|
|
601
|
+
const values = {
|
|
602
|
+
":reg": { S: registrySortKey() },
|
|
603
|
+
":false": FALSE2
|
|
604
|
+
};
|
|
605
|
+
const pkPrefix = this.scanPkPrefix(namespace);
|
|
606
|
+
if (pkPrefix !== void 0) {
|
|
607
|
+
filter.push("begins_with(#pk, :pk)");
|
|
608
|
+
values[":pk"] = { S: pkPrefix };
|
|
609
|
+
}
|
|
610
|
+
let startKey;
|
|
611
|
+
do {
|
|
612
|
+
let res;
|
|
613
|
+
try {
|
|
614
|
+
res = await this.client.send(
|
|
615
|
+
new clientDynamodb.ScanCommand({
|
|
616
|
+
TableName: this.table,
|
|
617
|
+
FilterExpression: filter.join(" AND "),
|
|
618
|
+
ExpressionAttributeNames: pkPrefix !== void 0 ? SCAN_NAMES : { "#sk": "SK", "#del": "del" },
|
|
619
|
+
ExpressionAttributeValues: values,
|
|
620
|
+
ExclusiveStartKey: startKey
|
|
621
|
+
})
|
|
622
|
+
);
|
|
623
|
+
} catch (err) {
|
|
624
|
+
throw this.mapError(err);
|
|
625
|
+
}
|
|
626
|
+
for (const item of res.Items ?? []) {
|
|
627
|
+
yield rowToRecord(item, namespaceOf(item));
|
|
628
|
+
}
|
|
629
|
+
startKey = res.LastEvaluatedKey;
|
|
630
|
+
} while (startKey !== void 0);
|
|
631
|
+
}
|
|
632
|
+
async delete(ref) {
|
|
633
|
+
const { pk, sk } = registryKeyPair(ref, this.keyPrefix);
|
|
634
|
+
try {
|
|
635
|
+
await this.client.send(
|
|
636
|
+
new clientDynamodb.UpdateItemCommand({
|
|
637
|
+
TableName: this.table,
|
|
638
|
+
Key: { PK: { S: pk }, SK: { S: sk } },
|
|
639
|
+
UpdateExpression: "ADD #v :one SET #del = :true REMOVE #r",
|
|
640
|
+
ExpressionAttributeNames: MUTATE_NAMES2,
|
|
641
|
+
ExpressionAttributeValues: { ":one": ONE2, ":true": TRUE2 }
|
|
642
|
+
})
|
|
643
|
+
);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
throw this.mapError(err, ref);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/** The PK prefix to scope a `list` scan, or `undefined` for an unscoped scan (no namespace, no keyPrefix). */
|
|
649
|
+
scanPkPrefix(namespace) {
|
|
650
|
+
if (namespace !== void 0) {
|
|
651
|
+
const full = partitionKey({ namespace, segment: "x" }, this.keyPrefix);
|
|
652
|
+
return full.slice(0, full.length - "x".length);
|
|
653
|
+
}
|
|
654
|
+
return this.keyPrefix !== void 0 ? `${this.keyPrefix}|` : void 0;
|
|
655
|
+
}
|
|
656
|
+
/** Map a raw SDK error to the typed vocabulary (conflict / transient / passthrough). */
|
|
657
|
+
mapError(err, ref, conflictHint = "OCC token mismatch") {
|
|
658
|
+
if (isIntegrityError(err)) return err;
|
|
659
|
+
if (err instanceof clientDynamodb.ConditionalCheckFailedException || err?.name === "ConditionalCheckFailedException") {
|
|
660
|
+
return new WriteConflictError(
|
|
661
|
+
`registry ${conflictHint}${ref ? ` for segment ${ref.segment}` : ""}`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
if (isTransient(err)) {
|
|
665
|
+
return new TransientError(
|
|
666
|
+
`transient DynamoDB fault on registry${ref ? ` ${ref.segment}` : ""}: ${err?.name ?? "unknown"}`,
|
|
667
|
+
{ cause: err }
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
return err;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
function tokenOf(attrs) {
|
|
674
|
+
const token = attrs?.v?.N;
|
|
675
|
+
if (token === void 0)
|
|
676
|
+
throw new IntegrityError("DynamoDB registry UpdateItem returned no token");
|
|
677
|
+
return token;
|
|
678
|
+
}
|
|
679
|
+
function serializeBody(record) {
|
|
680
|
+
const body = {
|
|
681
|
+
namespace: record.namespace,
|
|
682
|
+
segment: record.segment,
|
|
683
|
+
currentGen: record.currentGen,
|
|
684
|
+
wrappedDeks: record.wrappedDeks,
|
|
685
|
+
keyId: record.keyId,
|
|
686
|
+
dirtyChunkCount: record.dirtyChunkCount,
|
|
687
|
+
status: record.status,
|
|
688
|
+
leaseOwner: record.leaseOwner,
|
|
689
|
+
leaseExpiresAt: record.leaseExpiresAt,
|
|
690
|
+
lastCompactedAt: record.lastCompactedAt,
|
|
691
|
+
consecutiveFailures: record.consecutiveFailures,
|
|
692
|
+
retention: record.retention,
|
|
693
|
+
residency: record.residency,
|
|
694
|
+
createdAt: record.createdAt,
|
|
695
|
+
updatedAt: record.updatedAt
|
|
696
|
+
};
|
|
697
|
+
return JSON.stringify({ schemaVersion: REGISTRY_SCHEMA_VERSION, ...body });
|
|
698
|
+
}
|
|
699
|
+
function namespaceOf(item) {
|
|
700
|
+
const body = parseBody(item.r?.S, "scan");
|
|
701
|
+
return { namespace: body.namespace, segment: body.segment };
|
|
702
|
+
}
|
|
703
|
+
function rowToRecord(item, ref) {
|
|
704
|
+
const token = item.v?.N;
|
|
705
|
+
if (token === void 0) {
|
|
706
|
+
throw new IntegrityError(`registry row for segment ${ref.segment} is missing its token`);
|
|
707
|
+
}
|
|
708
|
+
const body = parseBody(item.r?.S, ref.segment);
|
|
709
|
+
return { ...body, token };
|
|
710
|
+
}
|
|
711
|
+
function parseBody(json, ctx) {
|
|
712
|
+
if (json === void 0) throw new IntegrityError(`registry row ${ctx} is missing its body`);
|
|
713
|
+
let parsed;
|
|
714
|
+
try {
|
|
715
|
+
parsed = JSON.parse(json);
|
|
716
|
+
} catch {
|
|
717
|
+
throw new IntegrityError(`registry row ${ctx} body is not valid JSON`);
|
|
718
|
+
}
|
|
719
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
720
|
+
throw new IntegrityError(`registry row ${ctx} body is malformed`);
|
|
721
|
+
}
|
|
722
|
+
const { schemaVersion, ...body } = parsed;
|
|
723
|
+
assertRegistrySchemaVersion(schemaVersion, ctx);
|
|
724
|
+
assertStoredRecordShape(body, ctx);
|
|
725
|
+
return body;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
exports.DynamoDbRegistryDriver = DynamoDbRegistryDriver;
|
|
729
|
+
exports.DynamoDbWarmDriver = DynamoDbWarmDriver;
|
|
730
|
+
//# sourceMappingURL=index.cjs.map
|
|
731
|
+
//# sourceMappingURL=index.cjs.map
|