@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,891 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var clientS3 = require('@aws-sdk/client-s3');
5
+
6
+ // src/drivers/s3/cold.ts
7
+
8
+ // src/core/errors.ts
9
+ var ERROR_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error");
10
+ var TRANSIENT_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error.transient");
11
+ var CloudRoaringError = class extends Error {
12
+ /** Cross-bundle brand — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ not in JSON). */
13
+ [ERROR_BRAND] = true;
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = new.target.name;
17
+ }
18
+ };
19
+ var ValidationError = class extends CloudRoaringError {
20
+ };
21
+ var WriteConflictError = class extends CloudRoaringError {
22
+ };
23
+ var IntegrityError = class extends CloudRoaringError {
24
+ };
25
+ var NotFoundError = class extends CloudRoaringError {
26
+ };
27
+ var UnsupportedError = class extends CloudRoaringError {
28
+ };
29
+ var TransientError = class extends CloudRoaringError {
30
+ /** A second brand so the whole transient subtree (incl. {@link TimeoutError}) is classifiable cross-bundle. */
31
+ [TRANSIENT_BRAND] = true;
32
+ constructor(message, options) {
33
+ super(message);
34
+ if (options && "cause" in options) this.cause = options.cause;
35
+ }
36
+ };
37
+ function hasBrand(err, brand) {
38
+ return typeof err === "object" && err !== null && err[brand] === true;
39
+ }
40
+ function isCloudRoaringError(err) {
41
+ return hasBrand(err, ERROR_BRAND);
42
+ }
43
+ function isWriteConflictError(err) {
44
+ return isCloudRoaringError(err) && err.name === "WriteConflictError";
45
+ }
46
+ function isNotFoundError(err) {
47
+ return isCloudRoaringError(err) && err.name === "NotFoundError";
48
+ }
49
+ function isValidationError(err) {
50
+ return isCloudRoaringError(err) && err.name === "ValidationError";
51
+ }
52
+
53
+ // src/core/validate.ts
54
+ var NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
55
+ function validatePart(value, field) {
56
+ if (typeof value !== "string" || !NAME.test(value) || value.includes("..")) {
57
+ throw new ValidationError(
58
+ `${field} must match ${String(NAME)} and contain no "..": got ${JSON.stringify(value)}`
59
+ );
60
+ }
61
+ }
62
+ function validateSegmentRef(ref) {
63
+ validatePart(ref.segment, "segment");
64
+ if (ref.namespace !== void 0) validatePart(ref.namespace, "namespace");
65
+ }
66
+
67
+ // src/drivers/_shared/keys.ts
68
+ var DEFAULT_NAMESPACE = "_default";
69
+ function namespacePart(namespace) {
70
+ return namespace ?? DEFAULT_NAMESPACE;
71
+ }
72
+
73
+ // src/drivers/s3/keys.ts
74
+ var SUFFIX = ".crbm";
75
+ var REGISTRY_SUFFIX = ".reg";
76
+ function prefixPart(prefix) {
77
+ if (prefix === void 0) return "";
78
+ const trimmed = prefix.replace(/^\/+|\/+$/g, "");
79
+ return trimmed === "" ? "" : `${trimmed}/`;
80
+ }
81
+ function normalizeS3Prefix(prefix) {
82
+ if (prefix === void 0) return void 0;
83
+ for (const ch of prefix) {
84
+ if (ch.charCodeAt(0) < 32) {
85
+ throw new ValidationError("prefix must not contain control characters");
86
+ }
87
+ }
88
+ for (const segment of prefix.split("/")) {
89
+ if (segment === "." || segment === "..") {
90
+ throw new ValidationError(
91
+ `prefix must not contain "." or ".." path segments: ${JSON.stringify(prefix)}`
92
+ );
93
+ }
94
+ }
95
+ return prefix;
96
+ }
97
+ function segmentObjectPrefix(prefix, ref) {
98
+ validateSegmentRef(ref);
99
+ return `${prefixPart(prefix)}${namespacePart(ref.namespace)}/segments/${ref.segment}.`;
100
+ }
101
+ function coldObjectKey(prefix, key) {
102
+ if (!Number.isInteger(key.generation) || key.generation < 0) {
103
+ throw new ValidationError(`generation must be a non-negative integer; got ${key.generation}`);
104
+ }
105
+ return `${segmentObjectPrefix(prefix, key)}${key.generation}${SUFFIX}`;
106
+ }
107
+ function registryPrefix(prefix) {
108
+ return `${prefixPart(prefix)}registry/`;
109
+ }
110
+ function registryObjectKey(prefix, ref) {
111
+ validateSegmentRef(ref);
112
+ return `${registryPrefix(prefix)}${namespacePart(ref.namespace)}/${ref.segment}${REGISTRY_SUFFIX}`;
113
+ }
114
+ function registryListPrefix(prefix, namespace) {
115
+ const base = registryPrefix(prefix);
116
+ return namespace === void 0 ? base : `${base}${namespacePart(namespace)}/`;
117
+ }
118
+ function parseRegistryKey(prefix, objectKey) {
119
+ const base = registryPrefix(prefix);
120
+ if (!objectKey.startsWith(base) || !objectKey.endsWith(REGISTRY_SUFFIX)) return null;
121
+ const rest = objectKey.slice(base.length, objectKey.length - REGISTRY_SUFFIX.length);
122
+ const slash = rest.indexOf("/");
123
+ if (slash < 0) return null;
124
+ const nsPart = rest.slice(0, slash);
125
+ const segment = rest.slice(slash + 1);
126
+ if (segment.length === 0 || segment.includes("/")) return null;
127
+ const namespace = nsPart === DEFAULT_NAMESPACE ? void 0 : nsPart;
128
+ const ref = { segment, namespace };
129
+ try {
130
+ validateSegmentRef(ref);
131
+ } catch {
132
+ return null;
133
+ }
134
+ return ref;
135
+ }
136
+ function parseGenerationFromKey(segmentPrefix, objectKey) {
137
+ if (!objectKey.startsWith(segmentPrefix) || !objectKey.endsWith(SUFFIX)) return null;
138
+ const middle = objectKey.slice(segmentPrefix.length, objectKey.length - SUFFIX.length);
139
+ if (!/^(0|[1-9]\d*)$/.test(middle)) return null;
140
+ const generation = Number(middle);
141
+ return Number.isSafeInteger(generation) ? generation : null;
142
+ }
143
+
144
+ // src/drivers/_shared/aws-errors.ts
145
+ function httpStatus(err) {
146
+ return err?.$metadata?.httpStatusCode;
147
+ }
148
+ function errorName(err) {
149
+ return err?.name;
150
+ }
151
+ function errorCode(err) {
152
+ return err?.code;
153
+ }
154
+ function isSdkRetryable(err) {
155
+ return err?.$retryable != null;
156
+ }
157
+ function isServerSide(err) {
158
+ const status = httpStatus(err);
159
+ return status !== void 0 && status >= 500 && status <= 599;
160
+ }
161
+ var NETWORK_NAMES = /* @__PURE__ */ new Set([
162
+ "TimeoutError",
163
+ "RequestTimeout",
164
+ "RequestTimeoutException",
165
+ "NetworkingError",
166
+ "AbortError"
167
+ ]);
168
+ var NETWORK_CODES = /* @__PURE__ */ new Set([
169
+ "ETIMEDOUT",
170
+ "ECONNRESET",
171
+ "ECONNREFUSED",
172
+ "EPIPE",
173
+ "ENOTFOUND",
174
+ "EAI_AGAIN",
175
+ "ECONNABORTED"
176
+ ]);
177
+ 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;
178
+ function isNetworkOrTimeout(err) {
179
+ if (NETWORK_NAMES.has(errorName(err) ?? "")) return true;
180
+ if (NETWORK_CODES.has(errorCode(err) ?? "")) return true;
181
+ return NETWORK_MESSAGE.test(err?.message ?? "");
182
+ }
183
+
184
+ // src/drivers/s3/s3-errors.ts
185
+ function isPreconditionFailed(err) {
186
+ return errorName(err) === "PreconditionFailed" || httpStatus(err) === 412;
187
+ }
188
+ function isConditionalConflict(err) {
189
+ return isPreconditionFailed(err) || errorName(err) === "ConditionalRequestConflict" || httpStatus(err) === 409;
190
+ }
191
+ function isNotFound(err) {
192
+ const name = errorName(err);
193
+ return name === "NoSuchKey" || name === "NotFound" || httpStatus(err) === 404;
194
+ }
195
+ function isInvalidRange(err) {
196
+ return errorName(err) === "InvalidRange" || httpStatus(err) === 416;
197
+ }
198
+ function isTransient(err) {
199
+ if (isConditionalConflict(err) || isNotFound(err) || isInvalidRange(err)) return false;
200
+ return errorName(err) === "SlowDown" || isServerSide(err) || isNetworkOrTimeout(err) || isSdkRetryable(err);
201
+ }
202
+ function totalFromContentRange(contentRange) {
203
+ if (contentRange === void 0) return void 0;
204
+ const match = /\/(\d+)\s*$/.exec(contentRange);
205
+ if (match === null) return void 0;
206
+ const total = Number(match[1]);
207
+ return Number.isSafeInteger(total) ? total : void 0;
208
+ }
209
+
210
+ // src/drivers/s3/cold.ts
211
+ var S3_PART_BYTES = 8 * 1024 * 1024;
212
+ var S3_MAX_PARTS = 1e4;
213
+ var S3ColdDriver = class {
214
+ client;
215
+ bucket;
216
+ prefix;
217
+ maxObjectBytes;
218
+ partBytes;
219
+ constructor(options) {
220
+ this.client = options.client;
221
+ this.bucket = options.bucket;
222
+ this.prefix = normalizeS3Prefix(options.prefix);
223
+ const requestedPart = Math.max(options.partBytes ?? S3_PART_BYTES, 5 * 1024 * 1024);
224
+ this.maxObjectBytes = options.maxObjectBytes ?? requestedPart * S3_MAX_PARTS;
225
+ this.partBytes = Math.max(requestedPart, Math.ceil(this.maxObjectBytes / S3_MAX_PARTS));
226
+ }
227
+ capabilities() {
228
+ return { rangeRead: true, maxObjectBytes: this.maxObjectBytes, conditionalPut: true };
229
+ }
230
+ async putImmutable(key, write) {
231
+ const objectKey = coldObjectKey(this.prefix, key);
232
+ const sink = new S3MultipartSink(
233
+ this.client,
234
+ this.bucket,
235
+ objectKey,
236
+ this.partBytes,
237
+ this.maxObjectBytes
238
+ );
239
+ try {
240
+ await write(sink);
241
+ return await sink.finish();
242
+ } catch (err) {
243
+ await sink.abort();
244
+ if (isConditionalConflict(err)) {
245
+ throw new WriteConflictError(
246
+ `generation already exists (write-once): ${key.segment}.${key.generation}`
247
+ );
248
+ }
249
+ if (isValidationError(err) || isWriteConflictError(err) || isNotFoundError(err)) {
250
+ throw err;
251
+ }
252
+ throw this.mapError(err);
253
+ }
254
+ }
255
+ async getRange(key, offset, length) {
256
+ if (!Number.isInteger(offset) || !Number.isInteger(length) || offset < 0 || length < 0) {
257
+ throw new ValidationError(`invalid range offset=${offset} length=${length}`);
258
+ }
259
+ const objectKey = coldObjectKey(this.prefix, key);
260
+ if (length === 0) return new Uint8Array(0);
261
+ try {
262
+ const res = await this.client.send(
263
+ new clientS3.GetObjectCommand({
264
+ Bucket: this.bucket,
265
+ Key: objectKey,
266
+ Range: `bytes=${offset}-${offset + length - 1}`
267
+ })
268
+ );
269
+ const bytes = await collect(res.Body);
270
+ if (bytes.length !== length) {
271
+ throw new ValidationError(
272
+ `range [${offset}, ${offset + length}) out of bounds (got ${bytes.length}B)`
273
+ );
274
+ }
275
+ return bytes;
276
+ } catch (err) {
277
+ throw this.mapReadError(err, key);
278
+ }
279
+ }
280
+ async getTail(key, maxBytes) {
281
+ const objectKey = coldObjectKey(this.prefix, key);
282
+ if (maxBytes <= 0) {
283
+ try {
284
+ const head = await this.client.send(
285
+ new clientS3.HeadObjectCommand({ Bucket: this.bucket, Key: objectKey })
286
+ );
287
+ return { bytes: new Uint8Array(0), size: head.ContentLength ?? 0 };
288
+ } catch (err) {
289
+ throw this.mapReadError(err, key);
290
+ }
291
+ }
292
+ try {
293
+ const res = await this.client.send(
294
+ new clientS3.GetObjectCommand({ Bucket: this.bucket, Key: objectKey, Range: `bytes=-${maxBytes}` })
295
+ );
296
+ const bytes = await collect(res.Body);
297
+ let size = totalFromContentRange(res.ContentRange);
298
+ if (size === void 0) {
299
+ if (bytes.length === maxBytes) {
300
+ const head = await this.client.send(
301
+ new clientS3.HeadObjectCommand({ Bucket: this.bucket, Key: objectKey })
302
+ );
303
+ size = head.ContentLength ?? bytes.length;
304
+ } else {
305
+ size = bytes.length;
306
+ }
307
+ }
308
+ return { bytes, size };
309
+ } catch (err) {
310
+ throw this.mapReadError(err, key);
311
+ }
312
+ }
313
+ async delete(key) {
314
+ try {
315
+ await this.client.send(
316
+ new clientS3.DeleteObjectCommand({ Bucket: this.bucket, Key: coldObjectKey(this.prefix, key) })
317
+ );
318
+ } catch (err) {
319
+ throw this.mapError(err);
320
+ }
321
+ }
322
+ async *list(ref) {
323
+ const prefix = segmentObjectPrefix(this.prefix, ref);
324
+ let token;
325
+ do {
326
+ let res;
327
+ try {
328
+ res = await this.client.send(
329
+ new clientS3.ListObjectsV2Command({
330
+ Bucket: this.bucket,
331
+ Prefix: prefix,
332
+ ContinuationToken: token
333
+ })
334
+ );
335
+ } catch (err) {
336
+ throw this.mapError(err);
337
+ }
338
+ for (const obj of res.Contents ?? []) {
339
+ if (obj.Key === void 0) continue;
340
+ const generation = parseGenerationFromKey(prefix, obj.Key);
341
+ if (generation !== null) {
342
+ yield { namespace: ref.namespace, segment: ref.segment, generation };
343
+ }
344
+ }
345
+ token = res.IsTruncated === true ? res.NextContinuationToken : void 0;
346
+ } while (token !== void 0);
347
+ }
348
+ /** Map S3 read errors to the driver vocabulary; pass everything else through {@link mapError}. */
349
+ mapReadError(err, key) {
350
+ if (isValidationError(err)) return err;
351
+ if (isNotFound(err)) {
352
+ return new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
353
+ }
354
+ if (isInvalidRange(err)) {
355
+ return new ValidationError(`range out of bounds for ${key.segment}.${key.generation}`);
356
+ }
357
+ return this.mapError(err);
358
+ }
359
+ /**
360
+ * Reclassify a transient S3 fault (throttle/5xx/dropped connection) as a retryable {@link TransientError}
361
+ * so the retry decorator can ride it out; everything else propagates unchanged. The final fallback at every
362
+ * `client.send` site, so callers and the decorator only ever see typed errors.
363
+ */
364
+ mapError(err) {
365
+ if (isTransient(err)) {
366
+ return new TransientError(
367
+ `transient S3 fault: ${err?.name ?? "unknown"}`,
368
+ { cause: err }
369
+ );
370
+ }
371
+ return err;
372
+ }
373
+ };
374
+ function concatBytes(parts, total) {
375
+ const out = new Uint8Array(total);
376
+ let offset = 0;
377
+ for (const p of parts) {
378
+ out.set(p, offset);
379
+ offset += p.length;
380
+ }
381
+ return out;
382
+ }
383
+ var S3MultipartSink = class {
384
+ constructor(client, bucket, objectKey, partBytes, maxObjectBytes) {
385
+ this.client = client;
386
+ this.bucket = bucket;
387
+ this.objectKey = objectKey;
388
+ this.partBytes = partBytes;
389
+ this.maxObjectBytes = maxObjectBytes;
390
+ }
391
+ client;
392
+ bucket;
393
+ objectKey;
394
+ partBytes;
395
+ maxObjectBytes;
396
+ hash = crypto.createHash("sha256");
397
+ pending = [];
398
+ pendingLen = 0;
399
+ total = 0;
400
+ uploadId;
401
+ partNumber = 0;
402
+ parts = [];
403
+ async write(bytes) {
404
+ if (bytes.length === 0) return;
405
+ this.total += bytes.length;
406
+ if (this.total > this.maxObjectBytes) {
407
+ throw new ValidationError(`object exceeds maxObjectBytes ${this.maxObjectBytes}`);
408
+ }
409
+ this.hash.update(bytes);
410
+ this.pending.push(bytes);
411
+ this.pendingLen += bytes.length;
412
+ if (this.pendingLen >= this.partBytes) await this.flushPart();
413
+ }
414
+ /** Upload the buffered bytes (≥ one part) as a single part, freeing them. Starts the upload on first call. */
415
+ async flushPart() {
416
+ if (this.uploadId === void 0) {
417
+ const res2 = await this.client.send(
418
+ new clientS3.CreateMultipartUploadCommand({ Bucket: this.bucket, Key: this.objectKey })
419
+ );
420
+ if (res2.UploadId === void 0) {
421
+ throw new TransientError("S3 CreateMultipartUpload returned no UploadId");
422
+ }
423
+ this.uploadId = res2.UploadId;
424
+ }
425
+ const body = concatBytes(this.pending, this.pendingLen);
426
+ this.pending.length = 0;
427
+ this.pendingLen = 0;
428
+ this.partNumber += 1;
429
+ if (this.partNumber > S3_MAX_PARTS) {
430
+ throw new ValidationError(`multipart upload exceeded the S3 ${S3_MAX_PARTS}-part limit`);
431
+ }
432
+ const res = await this.client.send(
433
+ new clientS3.UploadPartCommand({
434
+ Bucket: this.bucket,
435
+ Key: this.objectKey,
436
+ UploadId: this.uploadId,
437
+ PartNumber: this.partNumber,
438
+ Body: body
439
+ })
440
+ );
441
+ this.parts.push({ ETag: res.ETag, PartNumber: this.partNumber });
442
+ }
443
+ /** Commit the object: a single conditional PUT if it fit in one part, else complete the multipart upload. */
444
+ async finish() {
445
+ const sha256 = this.hash.digest("hex");
446
+ if (this.uploadId === void 0) {
447
+ await this.client.send(
448
+ new clientS3.PutObjectCommand({
449
+ Bucket: this.bucket,
450
+ Key: this.objectKey,
451
+ Body: concatBytes(this.pending, this.pendingLen),
452
+ IfNoneMatch: "*"
453
+ // write-once
454
+ })
455
+ );
456
+ return { size: this.total, sha256 };
457
+ }
458
+ if (this.pendingLen > 0) await this.flushPart();
459
+ await this.client.send(
460
+ new clientS3.CompleteMultipartUploadCommand({
461
+ Bucket: this.bucket,
462
+ Key: this.objectKey,
463
+ UploadId: this.uploadId,
464
+ MultipartUpload: { Parts: this.parts },
465
+ IfNoneMatch: "*"
466
+ // write-once: fail if the object already exists
467
+ })
468
+ );
469
+ this.uploadId = void 0;
470
+ return { size: this.total, sha256 };
471
+ }
472
+ /** Best-effort cleanup of an in-flight multipart upload after an error (a leaked MPU is reaped by a bucket
473
+ * lifecycle rule; never a correctness issue). No-op if nothing was started or it already completed. */
474
+ async abort() {
475
+ if (this.uploadId === void 0) return;
476
+ const id = this.uploadId;
477
+ this.uploadId = void 0;
478
+ try {
479
+ await this.client.send(
480
+ new clientS3.AbortMultipartUploadCommand({ Bucket: this.bucket, Key: this.objectKey, UploadId: id })
481
+ );
482
+ } catch {
483
+ }
484
+ }
485
+ };
486
+ async function collect(body) {
487
+ if (body === void 0) {
488
+ throw new NotFoundError("S3 GetObject returned an empty body");
489
+ }
490
+ return body.transformToByteArray();
491
+ }
492
+
493
+ // src/drivers/_shared/registry.ts
494
+ var STATUSES = ["active", "compacting", "erasing", "destroyed"];
495
+ var MAX_GOVERNANCE_BYTES = 64 * 1024;
496
+ var MAX_WRAPPED_DEKS = 8;
497
+ var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
498
+ function validateGeneration(gen) {
499
+ if (!Number.isInteger(gen) || gen < 0) {
500
+ throw new ValidationError(`currentGen must be a non-negative integer; got ${gen}`);
501
+ }
502
+ }
503
+ function validateCount(count, field = "dirtyChunkCount") {
504
+ if (!Number.isInteger(count) || count < 0) {
505
+ throw new ValidationError(`${field} must be a non-negative integer; got ${count}`);
506
+ }
507
+ }
508
+ function validateTimestamp(ms, field) {
509
+ if (!Number.isFinite(ms) || ms < 0) {
510
+ throw new ValidationError(`${field} must be a non-negative finite number; got ${ms}`);
511
+ }
512
+ }
513
+ function validateStatus(status) {
514
+ if (!STATUSES.includes(status)) {
515
+ throw new ValidationError(`status must be one of ${STATUSES.join("/")}; got ${status}`);
516
+ }
517
+ }
518
+ function validateGovernance(meta, field) {
519
+ if (meta === void 0) return;
520
+ let json;
521
+ try {
522
+ json = JSON.stringify(meta);
523
+ } catch {
524
+ throw new ValidationError(`${field} must be JSON-serializable`);
525
+ }
526
+ if (json.length > MAX_GOVERNANCE_BYTES) {
527
+ throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);
528
+ }
529
+ }
530
+ function validateWrappedDeks(value, isStored) {
531
+ if (value === void 0) return;
532
+ const fail = (msg) => {
533
+ throw isStored ? new IntegrityError(msg) : new ValidationError(msg);
534
+ };
535
+ if (!Array.isArray(value)) fail("wrappedDeks must be an array");
536
+ const list = value;
537
+ if (list.length === 0) fail("wrappedDeks must be non-empty when present");
538
+ if (list.length > MAX_WRAPPED_DEKS)
539
+ fail(`wrappedDeks has ${list.length} entries, cap ${MAX_WRAPPED_DEKS}`);
540
+ for (const w of list) {
541
+ if (w === null || typeof w !== "object") fail("wrappedDeks entry must be an object");
542
+ const { keyId, wrapped } = w;
543
+ if (typeof keyId !== "string" || keyId.length === 0)
544
+ fail("wrappedDeks entry needs a non-empty keyId");
545
+ if (typeof wrapped !== "string" || wrapped.length === 0)
546
+ fail("wrappedDeks entry needs a non-empty wrapped blob");
547
+ if (wrapped.length > MAX_WRAPPED_DEK_BYTES)
548
+ fail(`wrappedDeks entry is ${wrapped.length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);
549
+ }
550
+ }
551
+ function validateNewRegistryRecord(rec) {
552
+ validateGeneration(rec.currentGen);
553
+ if (rec.dirtyChunkCount !== void 0) validateCount(rec.dirtyChunkCount);
554
+ if (rec.status !== void 0) validateStatus(rec.status);
555
+ validateWrappedDeks(rec.wrappedDeks, false);
556
+ validateGovernance(rec.retention, "retention");
557
+ validateGovernance(rec.residency, "residency");
558
+ }
559
+ function validateRegistryPatch(patch) {
560
+ if (patch.currentGen !== void 0) validateGeneration(patch.currentGen);
561
+ if (patch.dirtyChunkCount !== void 0) validateCount(patch.dirtyChunkCount);
562
+ if (patch.consecutiveFailures !== void 0)
563
+ validateCount(patch.consecutiveFailures, "consecutiveFailures");
564
+ if (patch.lastCompactedAt !== void 0)
565
+ validateTimestamp(patch.lastCompactedAt, "lastCompactedAt");
566
+ if (patch.status !== void 0) validateStatus(patch.status);
567
+ if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, false);
568
+ if ("retention" in patch) validateGovernance(patch.retention, "retention");
569
+ if ("residency" in patch) validateGovernance(patch.residency, "residency");
570
+ }
571
+ var REGISTRY_SCHEMA_VERSION = 1;
572
+ function assertRegistrySchemaVersion(raw, ctx) {
573
+ if (raw === void 0) return;
574
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
575
+ throw new IntegrityError(`registry row has a malformed schemaVersion (${String(raw)}): ${ctx}`);
576
+ }
577
+ if (raw > REGISTRY_SCHEMA_VERSION) {
578
+ throw new UnsupportedError(
579
+ `registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`
580
+ );
581
+ }
582
+ }
583
+ function serializeRegistryEnvelope(env) {
584
+ return JSON.stringify({
585
+ schemaVersion: REGISTRY_SCHEMA_VERSION,
586
+ deleted: env.deleted,
587
+ record: env.record
588
+ });
589
+ }
590
+ function registryCounterOf(record) {
591
+ if (!/^(0|[1-9]\d*)$/.test(record.token)) {
592
+ throw new IntegrityError(`registry row token is not a canonical counter: ${record.token}`);
593
+ }
594
+ const n = Number(record.token);
595
+ if (!Number.isSafeInteger(n)) {
596
+ throw new IntegrityError(`registry row token is out of safe-integer range: ${record.token}`);
597
+ }
598
+ return n;
599
+ }
600
+ function parseRegistryEnvelope(text, ctx) {
601
+ let parsed;
602
+ try {
603
+ parsed = JSON.parse(text);
604
+ } catch {
605
+ throw new IntegrityError(`registry row is not valid JSON: ${ctx}`);
606
+ }
607
+ if (parsed === null || typeof parsed !== "object") {
608
+ throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
609
+ }
610
+ const env = parsed;
611
+ assertRegistrySchemaVersion(env.schemaVersion, ctx);
612
+ if (typeof env.deleted !== "boolean" || env.record === null || typeof env.record !== "object") {
613
+ throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
614
+ }
615
+ const r = env.record;
616
+ assertStoredRecordShape(r, ctx);
617
+ if (typeof r.token !== "string") {
618
+ throw new IntegrityError(`registry row is missing its token: ${ctx}`);
619
+ }
620
+ return { deleted: env.deleted, record: env.record };
621
+ }
622
+ function recordFromNew(ref, rec, now, token) {
623
+ return {
624
+ namespace: ref.namespace,
625
+ segment: ref.segment,
626
+ currentGen: rec.currentGen,
627
+ wrappedDeks: rec.wrappedDeks,
628
+ keyId: rec.keyId,
629
+ dirtyChunkCount: rec.dirtyChunkCount ?? 0,
630
+ status: rec.status ?? "active",
631
+ consecutiveFailures: 0,
632
+ // daemon health (Phase D); lastCompactedAt stays absent until first compaction
633
+ retention: rec.retention,
634
+ residency: rec.residency,
635
+ createdAt: now,
636
+ updatedAt: now,
637
+ token
638
+ };
639
+ }
640
+ function assertStoredRecordShape(r, ctx) {
641
+ 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") {
642
+ throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
643
+ }
644
+ if (!Number.isInteger(r.currentGen) || r.currentGen < 0) {
645
+ throw new IntegrityError(`registry record has an invalid currentGen (${r.currentGen}): ${ctx}`);
646
+ }
647
+ if (!Number.isInteger(r.dirtyChunkCount) || r.dirtyChunkCount < 0) {
648
+ throw new IntegrityError(`registry record has an invalid dirtyChunkCount: ${ctx}`);
649
+ }
650
+ if (!STATUSES.includes(r.status)) {
651
+ throw new IntegrityError(`registry record has an unknown status (${r.status}): ${ctx}`);
652
+ }
653
+ if (r.leaseOwner !== void 0 && typeof r.leaseOwner !== "string") {
654
+ throw new IntegrityError(`registry record has an invalid leaseOwner: ${ctx}`);
655
+ }
656
+ if (r.leaseExpiresAt !== void 0 && typeof r.leaseExpiresAt !== "number") {
657
+ throw new IntegrityError(`registry record has an invalid leaseExpiresAt: ${ctx}`);
658
+ }
659
+ if (r.consecutiveFailures !== void 0 && (!Number.isInteger(r.consecutiveFailures) || r.consecutiveFailures < 0)) {
660
+ throw new IntegrityError(`registry record has an invalid consecutiveFailures: ${ctx}`);
661
+ }
662
+ if (r.lastCompactedAt !== void 0 && (typeof r.lastCompactedAt !== "number" || !Number.isFinite(r.lastCompactedAt) || r.lastCompactedAt < 0)) {
663
+ throw new IntegrityError(`registry record has an invalid lastCompactedAt: ${ctx}`);
664
+ }
665
+ if (r.keyId !== void 0 && typeof r.keyId !== "string") {
666
+ throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);
667
+ }
668
+ validateWrappedDeks(r.wrappedDeks, true);
669
+ }
670
+ function applyRegistryPatch(prev, patch, now, token) {
671
+ return {
672
+ namespace: prev.namespace,
673
+ segment: prev.segment,
674
+ currentGen: patch.currentGen ?? prev.currentGen,
675
+ wrappedDeks: "wrappedDeks" in patch ? patch.wrappedDeks : prev.wrappedDeks,
676
+ keyId: "keyId" in patch ? patch.keyId : prev.keyId,
677
+ dirtyChunkCount: patch.dirtyChunkCount ?? prev.dirtyChunkCount,
678
+ status: patch.status ?? prev.status,
679
+ leaseOwner: "leaseOwner" in patch ? patch.leaseOwner : prev.leaseOwner,
680
+ leaseExpiresAt: "leaseExpiresAt" in patch ? patch.leaseExpiresAt : prev.leaseExpiresAt,
681
+ lastCompactedAt: "lastCompactedAt" in patch ? patch.lastCompactedAt : prev.lastCompactedAt,
682
+ consecutiveFailures: patch.consecutiveFailures ?? prev.consecutiveFailures ?? 0,
683
+ retention: "retention" in patch ? patch.retention : prev.retention,
684
+ residency: "residency" in patch ? patch.residency : prev.residency,
685
+ createdAt: prev.createdAt,
686
+ updatedAt: now,
687
+ token
688
+ };
689
+ }
690
+
691
+ // src/core/concurrency.ts
692
+ async function mapWithConcurrency(items, limit, fn) {
693
+ if (!Number.isInteger(limit) || limit < 1) {
694
+ throw new ValidationError(`concurrency limit must be a positive integer; got ${limit}`);
695
+ }
696
+ const results = new Array(items.length);
697
+ let next = 0;
698
+ let failed = false;
699
+ let firstError;
700
+ const worker = async () => {
701
+ while (!failed) {
702
+ const i = next++;
703
+ if (i >= items.length) return;
704
+ try {
705
+ results[i] = await fn(items[i], i);
706
+ } catch (err) {
707
+ if (!failed) {
708
+ failed = true;
709
+ firstError = err;
710
+ }
711
+ return;
712
+ }
713
+ }
714
+ };
715
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
716
+ await Promise.all(workers);
717
+ if (failed) throw firstError;
718
+ return results;
719
+ }
720
+
721
+ // src/drivers/s3/registry.ts
722
+ var MAX_ROW_BYTES = 1 * 1024 * 1024;
723
+ var MAX_DELETE_ATTEMPTS = 8;
724
+ var LIST_READ_CONCURRENCY = 16;
725
+ var S3RegistryDriver = class {
726
+ client;
727
+ bucket;
728
+ prefix;
729
+ now;
730
+ constructor(options) {
731
+ this.client = options.client;
732
+ this.bucket = options.bucket;
733
+ this.prefix = normalizeS3Prefix(options.prefix);
734
+ this.now = options.now ?? (() => Date.now());
735
+ }
736
+ capabilities() {
737
+ return { strongRead: true };
738
+ }
739
+ async get(ref) {
740
+ const current = await this.readRow(registryObjectKey(this.prefix, ref));
741
+ return current && !current.env.deleted ? current.env.record : null;
742
+ }
743
+ async create(ref, record) {
744
+ validateNewRegistryRecord(record);
745
+ const key = registryObjectKey(this.prefix, ref);
746
+ const current = await this.readRow(key);
747
+ if (current !== null && !current.env.deleted) {
748
+ throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);
749
+ }
750
+ const token = String(current ? registryCounterOf(current.env.record) + 1 : 0);
751
+ const env = {
752
+ deleted: false,
753
+ record: recordFromNew(ref, record, this.now(), token)
754
+ };
755
+ await this.putRow(key, env, current ? { ifMatch: current.etag } : { ifNoneMatch: "*" });
756
+ return { token };
757
+ }
758
+ async compareAndSwap(ref, expected, patch) {
759
+ validateRegistryPatch(patch);
760
+ const key = registryObjectKey(this.prefix, ref);
761
+ const current = await this.readRow(key);
762
+ if (current === null || current.env.deleted || current.env.record.token !== expected) {
763
+ throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
764
+ }
765
+ const token = String(registryCounterOf(current.env.record) + 1);
766
+ const env = {
767
+ deleted: false,
768
+ record: applyRegistryPatch(current.env.record, patch, this.now(), token)
769
+ };
770
+ await this.putRow(key, env, { ifMatch: current.etag });
771
+ return { token };
772
+ }
773
+ async *list(namespace) {
774
+ const prefix = registryListPrefix(this.prefix, namespace);
775
+ let token;
776
+ do {
777
+ let res;
778
+ try {
779
+ res = await this.client.send(
780
+ new clientS3.ListObjectsV2Command({
781
+ Bucket: this.bucket,
782
+ Prefix: prefix,
783
+ ContinuationToken: token
784
+ })
785
+ );
786
+ } catch (err) {
787
+ throw this.mapError(err);
788
+ }
789
+ const keys = (res.Contents ?? []).map((obj) => obj.Key).filter((k) => k !== void 0 && parseRegistryKey(this.prefix, k) !== null);
790
+ const rows = await mapWithConcurrency(
791
+ keys,
792
+ LIST_READ_CONCURRENCY,
793
+ (key) => this.readRow(key)
794
+ );
795
+ for (const current of rows) {
796
+ if (current && !current.env.deleted) yield current.env.record;
797
+ }
798
+ token = res.IsTruncated === true ? res.NextContinuationToken : void 0;
799
+ } while (token !== void 0);
800
+ }
801
+ async delete(ref) {
802
+ const key = registryObjectKey(this.prefix, ref);
803
+ for (let attempt = 0; attempt < MAX_DELETE_ATTEMPTS; attempt++) {
804
+ const current = await this.readRow(key);
805
+ if (current === null || current.env.deleted) return;
806
+ const token = String(registryCounterOf(current.env.record) + 1);
807
+ const env = {
808
+ deleted: true,
809
+ record: { ...current.env.record, token, updatedAt: this.now() }
810
+ };
811
+ try {
812
+ await this.putRow(key, env, { ifMatch: current.etag });
813
+ return;
814
+ } catch (err) {
815
+ if (isWriteConflictError(err)) continue;
816
+ throw err;
817
+ }
818
+ }
819
+ throw new WriteConflictError(
820
+ `registry delete: contention tombstoning "${ref.segment}" \u2014 retry`
821
+ );
822
+ }
823
+ /** GET + parse a registry object, returning its envelope + ETag (for a later If-Match), or null if absent. */
824
+ async readRow(objectKey) {
825
+ let res;
826
+ try {
827
+ res = await this.client.send(new clientS3.GetObjectCommand({ Bucket: this.bucket, Key: objectKey }));
828
+ } catch (err) {
829
+ if (isNotFound(err)) return null;
830
+ throw this.mapError(err);
831
+ }
832
+ if ((res.ContentLength ?? 0) > MAX_ROW_BYTES) {
833
+ throw new IntegrityError(
834
+ `registry object ${res.ContentLength}B exceeds cap ${MAX_ROW_BYTES}B`
835
+ );
836
+ }
837
+ if (res.Body === void 0) {
838
+ throw new IntegrityError(`registry object has an empty body: ${objectKey}`);
839
+ }
840
+ const bytes = await res.Body.transformToByteArray();
841
+ if (bytes.length > MAX_ROW_BYTES) {
842
+ throw new IntegrityError(`registry object ${bytes.length}B exceeds cap ${MAX_ROW_BYTES}B`);
843
+ }
844
+ const env = parseRegistryEnvelope(Buffer.from(bytes).toString("utf8"), objectKey);
845
+ if (res.ETag === void 0 || res.ETag.length === 0) {
846
+ throw new IntegrityError(
847
+ `registry object has no ETag (needed for compare-and-swap): ${objectKey}`
848
+ );
849
+ }
850
+ return { env, etag: res.ETag };
851
+ }
852
+ /** Conditional PUT of an envelope; maps a `412` precondition failure to {@link WriteConflictError}. */
853
+ async putRow(objectKey, env, cond) {
854
+ const Body = Buffer.from(serializeRegistryEnvelope(env), "utf8");
855
+ if (Body.length > MAX_ROW_BYTES) {
856
+ throw new ValidationError(`registry object ${Body.length}B exceeds cap ${MAX_ROW_BYTES}B`);
857
+ }
858
+ try {
859
+ await this.client.send(
860
+ new clientS3.PutObjectCommand({
861
+ Bucket: this.bucket,
862
+ Key: objectKey,
863
+ Body,
864
+ ContentType: "application/json",
865
+ IfNoneMatch: "ifNoneMatch" in cond ? cond.ifNoneMatch : void 0,
866
+ IfMatch: "ifMatch" in cond ? cond.ifMatch : void 0
867
+ })
868
+ );
869
+ } catch (err) {
870
+ if (isConditionalConflict(err)) {
871
+ throw new WriteConflictError(`registry OCC conflict for ${objectKey}`);
872
+ }
873
+ throw this.mapError(err);
874
+ }
875
+ }
876
+ /** Reclassify a transient S3 fault as a retryable {@link TransientError}; pass everything else through. */
877
+ mapError(err) {
878
+ if (isTransient(err)) {
879
+ return new TransientError(
880
+ `transient S3 fault: ${err?.name ?? "unknown"}`,
881
+ { cause: err }
882
+ );
883
+ }
884
+ return err;
885
+ }
886
+ };
887
+
888
+ exports.S3ColdDriver = S3ColdDriver;
889
+ exports.S3RegistryDriver = S3RegistryDriver;
890
+ //# sourceMappingURL=index.cjs.map
891
+ //# sourceMappingURL=index.cjs.map