@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,555 @@
1
+ import { mapWithConcurrency } from '../chunk-SNJVZ227.js';
2
+ import { errorName, httpStatus, isServerSide, isNetworkOrTimeout, isSdkRetryable } from '../chunk-7LMLYSVJ.js';
3
+ import { validateNewRegistryRecord, registryCounterOf, recordFromNew, validateRegistryPatch, applyRegistryPatch, parseRegistryEnvelope, serializeRegistryEnvelope } from '../chunk-2YDULGXS.js';
4
+ import { WriteConflictError, isValidationError, isWriteConflictError, isNotFoundError, ValidationError, NotFoundError, TransientError, IntegrityError, validateSegmentRef, namespacePart, DEFAULT_NAMESPACE } from '../chunk-NUIDEEFZ.js';
5
+ import { createHash } from 'crypto';
6
+ import { GetObjectCommand, HeadObjectCommand, DeleteObjectCommand, ListObjectsV2Command, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand } from '@aws-sdk/client-s3';
7
+
8
+ // src/drivers/s3/keys.ts
9
+ var SUFFIX = ".crbm";
10
+ var REGISTRY_SUFFIX = ".reg";
11
+ function prefixPart(prefix) {
12
+ if (prefix === void 0) return "";
13
+ const trimmed = prefix.replace(/^\/+|\/+$/g, "");
14
+ return trimmed === "" ? "" : `${trimmed}/`;
15
+ }
16
+ function normalizeS3Prefix(prefix) {
17
+ if (prefix === void 0) return void 0;
18
+ for (const ch of prefix) {
19
+ if (ch.charCodeAt(0) < 32) {
20
+ throw new ValidationError("prefix must not contain control characters");
21
+ }
22
+ }
23
+ for (const segment of prefix.split("/")) {
24
+ if (segment === "." || segment === "..") {
25
+ throw new ValidationError(
26
+ `prefix must not contain "." or ".." path segments: ${JSON.stringify(prefix)}`
27
+ );
28
+ }
29
+ }
30
+ return prefix;
31
+ }
32
+ function segmentObjectPrefix(prefix, ref) {
33
+ validateSegmentRef(ref);
34
+ return `${prefixPart(prefix)}${namespacePart(ref.namespace)}/segments/${ref.segment}.`;
35
+ }
36
+ function coldObjectKey(prefix, key) {
37
+ if (!Number.isInteger(key.generation) || key.generation < 0) {
38
+ throw new ValidationError(`generation must be a non-negative integer; got ${key.generation}`);
39
+ }
40
+ return `${segmentObjectPrefix(prefix, key)}${key.generation}${SUFFIX}`;
41
+ }
42
+ function registryPrefix(prefix) {
43
+ return `${prefixPart(prefix)}registry/`;
44
+ }
45
+ function registryObjectKey(prefix, ref) {
46
+ validateSegmentRef(ref);
47
+ return `${registryPrefix(prefix)}${namespacePart(ref.namespace)}/${ref.segment}${REGISTRY_SUFFIX}`;
48
+ }
49
+ function registryListPrefix(prefix, namespace) {
50
+ const base = registryPrefix(prefix);
51
+ return namespace === void 0 ? base : `${base}${namespacePart(namespace)}/`;
52
+ }
53
+ function parseRegistryKey(prefix, objectKey) {
54
+ const base = registryPrefix(prefix);
55
+ if (!objectKey.startsWith(base) || !objectKey.endsWith(REGISTRY_SUFFIX)) return null;
56
+ const rest = objectKey.slice(base.length, objectKey.length - REGISTRY_SUFFIX.length);
57
+ const slash = rest.indexOf("/");
58
+ if (slash < 0) return null;
59
+ const nsPart = rest.slice(0, slash);
60
+ const segment = rest.slice(slash + 1);
61
+ if (segment.length === 0 || segment.includes("/")) return null;
62
+ const namespace = nsPart === DEFAULT_NAMESPACE ? void 0 : nsPart;
63
+ const ref = { segment, namespace };
64
+ try {
65
+ validateSegmentRef(ref);
66
+ } catch {
67
+ return null;
68
+ }
69
+ return ref;
70
+ }
71
+ function parseGenerationFromKey(segmentPrefix, objectKey) {
72
+ if (!objectKey.startsWith(segmentPrefix) || !objectKey.endsWith(SUFFIX)) return null;
73
+ const middle = objectKey.slice(segmentPrefix.length, objectKey.length - SUFFIX.length);
74
+ if (!/^(0|[1-9]\d*)$/.test(middle)) return null;
75
+ const generation = Number(middle);
76
+ return Number.isSafeInteger(generation) ? generation : null;
77
+ }
78
+
79
+ // src/drivers/s3/s3-errors.ts
80
+ function isPreconditionFailed(err) {
81
+ return errorName(err) === "PreconditionFailed" || httpStatus(err) === 412;
82
+ }
83
+ function isConditionalConflict(err) {
84
+ return isPreconditionFailed(err) || errorName(err) === "ConditionalRequestConflict" || httpStatus(err) === 409;
85
+ }
86
+ function isNotFound(err) {
87
+ const name = errorName(err);
88
+ return name === "NoSuchKey" || name === "NotFound" || httpStatus(err) === 404;
89
+ }
90
+ function isInvalidRange(err) {
91
+ return errorName(err) === "InvalidRange" || httpStatus(err) === 416;
92
+ }
93
+ function isTransient(err) {
94
+ if (isConditionalConflict(err) || isNotFound(err) || isInvalidRange(err)) return false;
95
+ return errorName(err) === "SlowDown" || isServerSide(err) || isNetworkOrTimeout(err) || isSdkRetryable(err);
96
+ }
97
+ function totalFromContentRange(contentRange) {
98
+ if (contentRange === void 0) return void 0;
99
+ const match = /\/(\d+)\s*$/.exec(contentRange);
100
+ if (match === null) return void 0;
101
+ const total = Number(match[1]);
102
+ return Number.isSafeInteger(total) ? total : void 0;
103
+ }
104
+
105
+ // src/drivers/s3/cold.ts
106
+ var S3_PART_BYTES = 8 * 1024 * 1024;
107
+ var S3_MAX_PARTS = 1e4;
108
+ var S3ColdDriver = class {
109
+ client;
110
+ bucket;
111
+ prefix;
112
+ maxObjectBytes;
113
+ partBytes;
114
+ constructor(options) {
115
+ this.client = options.client;
116
+ this.bucket = options.bucket;
117
+ this.prefix = normalizeS3Prefix(options.prefix);
118
+ const requestedPart = Math.max(options.partBytes ?? S3_PART_BYTES, 5 * 1024 * 1024);
119
+ this.maxObjectBytes = options.maxObjectBytes ?? requestedPart * S3_MAX_PARTS;
120
+ this.partBytes = Math.max(requestedPart, Math.ceil(this.maxObjectBytes / S3_MAX_PARTS));
121
+ }
122
+ capabilities() {
123
+ return { rangeRead: true, maxObjectBytes: this.maxObjectBytes, conditionalPut: true };
124
+ }
125
+ async putImmutable(key, write) {
126
+ const objectKey = coldObjectKey(this.prefix, key);
127
+ const sink = new S3MultipartSink(
128
+ this.client,
129
+ this.bucket,
130
+ objectKey,
131
+ this.partBytes,
132
+ this.maxObjectBytes
133
+ );
134
+ try {
135
+ await write(sink);
136
+ return await sink.finish();
137
+ } catch (err) {
138
+ await sink.abort();
139
+ if (isConditionalConflict(err)) {
140
+ throw new WriteConflictError(
141
+ `generation already exists (write-once): ${key.segment}.${key.generation}`
142
+ );
143
+ }
144
+ if (isValidationError(err) || isWriteConflictError(err) || isNotFoundError(err)) {
145
+ throw err;
146
+ }
147
+ throw this.mapError(err);
148
+ }
149
+ }
150
+ async getRange(key, offset, length) {
151
+ if (!Number.isInteger(offset) || !Number.isInteger(length) || offset < 0 || length < 0) {
152
+ throw new ValidationError(`invalid range offset=${offset} length=${length}`);
153
+ }
154
+ const objectKey = coldObjectKey(this.prefix, key);
155
+ if (length === 0) return new Uint8Array(0);
156
+ try {
157
+ const res = await this.client.send(
158
+ new GetObjectCommand({
159
+ Bucket: this.bucket,
160
+ Key: objectKey,
161
+ Range: `bytes=${offset}-${offset + length - 1}`
162
+ })
163
+ );
164
+ const bytes = await collect(res.Body);
165
+ if (bytes.length !== length) {
166
+ throw new ValidationError(
167
+ `range [${offset}, ${offset + length}) out of bounds (got ${bytes.length}B)`
168
+ );
169
+ }
170
+ return bytes;
171
+ } catch (err) {
172
+ throw this.mapReadError(err, key);
173
+ }
174
+ }
175
+ async getTail(key, maxBytes) {
176
+ const objectKey = coldObjectKey(this.prefix, key);
177
+ if (maxBytes <= 0) {
178
+ try {
179
+ const head = await this.client.send(
180
+ new HeadObjectCommand({ Bucket: this.bucket, Key: objectKey })
181
+ );
182
+ return { bytes: new Uint8Array(0), size: head.ContentLength ?? 0 };
183
+ } catch (err) {
184
+ throw this.mapReadError(err, key);
185
+ }
186
+ }
187
+ try {
188
+ const res = await this.client.send(
189
+ new GetObjectCommand({ Bucket: this.bucket, Key: objectKey, Range: `bytes=-${maxBytes}` })
190
+ );
191
+ const bytes = await collect(res.Body);
192
+ let size = totalFromContentRange(res.ContentRange);
193
+ if (size === void 0) {
194
+ if (bytes.length === maxBytes) {
195
+ const head = await this.client.send(
196
+ new HeadObjectCommand({ Bucket: this.bucket, Key: objectKey })
197
+ );
198
+ size = head.ContentLength ?? bytes.length;
199
+ } else {
200
+ size = bytes.length;
201
+ }
202
+ }
203
+ return { bytes, size };
204
+ } catch (err) {
205
+ throw this.mapReadError(err, key);
206
+ }
207
+ }
208
+ async delete(key) {
209
+ try {
210
+ await this.client.send(
211
+ new DeleteObjectCommand({ Bucket: this.bucket, Key: coldObjectKey(this.prefix, key) })
212
+ );
213
+ } catch (err) {
214
+ throw this.mapError(err);
215
+ }
216
+ }
217
+ async *list(ref) {
218
+ const prefix = segmentObjectPrefix(this.prefix, ref);
219
+ let token;
220
+ do {
221
+ let res;
222
+ try {
223
+ res = await this.client.send(
224
+ new ListObjectsV2Command({
225
+ Bucket: this.bucket,
226
+ Prefix: prefix,
227
+ ContinuationToken: token
228
+ })
229
+ );
230
+ } catch (err) {
231
+ throw this.mapError(err);
232
+ }
233
+ for (const obj of res.Contents ?? []) {
234
+ if (obj.Key === void 0) continue;
235
+ const generation = parseGenerationFromKey(prefix, obj.Key);
236
+ if (generation !== null) {
237
+ yield { namespace: ref.namespace, segment: ref.segment, generation };
238
+ }
239
+ }
240
+ token = res.IsTruncated === true ? res.NextContinuationToken : void 0;
241
+ } while (token !== void 0);
242
+ }
243
+ /** Map S3 read errors to the driver vocabulary; pass everything else through {@link mapError}. */
244
+ mapReadError(err, key) {
245
+ if (isValidationError(err)) return err;
246
+ if (isNotFound(err)) {
247
+ return new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
248
+ }
249
+ if (isInvalidRange(err)) {
250
+ return new ValidationError(`range out of bounds for ${key.segment}.${key.generation}`);
251
+ }
252
+ return this.mapError(err);
253
+ }
254
+ /**
255
+ * Reclassify a transient S3 fault (throttle/5xx/dropped connection) as a retryable {@link TransientError}
256
+ * so the retry decorator can ride it out; everything else propagates unchanged. The final fallback at every
257
+ * `client.send` site, so callers and the decorator only ever see typed errors.
258
+ */
259
+ mapError(err) {
260
+ if (isTransient(err)) {
261
+ return new TransientError(
262
+ `transient S3 fault: ${err?.name ?? "unknown"}`,
263
+ { cause: err }
264
+ );
265
+ }
266
+ return err;
267
+ }
268
+ };
269
+ function concatBytes(parts, total) {
270
+ const out = new Uint8Array(total);
271
+ let offset = 0;
272
+ for (const p of parts) {
273
+ out.set(p, offset);
274
+ offset += p.length;
275
+ }
276
+ return out;
277
+ }
278
+ var S3MultipartSink = class {
279
+ constructor(client, bucket, objectKey, partBytes, maxObjectBytes) {
280
+ this.client = client;
281
+ this.bucket = bucket;
282
+ this.objectKey = objectKey;
283
+ this.partBytes = partBytes;
284
+ this.maxObjectBytes = maxObjectBytes;
285
+ }
286
+ client;
287
+ bucket;
288
+ objectKey;
289
+ partBytes;
290
+ maxObjectBytes;
291
+ hash = createHash("sha256");
292
+ pending = [];
293
+ pendingLen = 0;
294
+ total = 0;
295
+ uploadId;
296
+ partNumber = 0;
297
+ parts = [];
298
+ async write(bytes) {
299
+ if (bytes.length === 0) return;
300
+ this.total += bytes.length;
301
+ if (this.total > this.maxObjectBytes) {
302
+ throw new ValidationError(`object exceeds maxObjectBytes ${this.maxObjectBytes}`);
303
+ }
304
+ this.hash.update(bytes);
305
+ this.pending.push(bytes);
306
+ this.pendingLen += bytes.length;
307
+ if (this.pendingLen >= this.partBytes) await this.flushPart();
308
+ }
309
+ /** Upload the buffered bytes (≥ one part) as a single part, freeing them. Starts the upload on first call. */
310
+ async flushPart() {
311
+ if (this.uploadId === void 0) {
312
+ const res2 = await this.client.send(
313
+ new CreateMultipartUploadCommand({ Bucket: this.bucket, Key: this.objectKey })
314
+ );
315
+ if (res2.UploadId === void 0) {
316
+ throw new TransientError("S3 CreateMultipartUpload returned no UploadId");
317
+ }
318
+ this.uploadId = res2.UploadId;
319
+ }
320
+ const body = concatBytes(this.pending, this.pendingLen);
321
+ this.pending.length = 0;
322
+ this.pendingLen = 0;
323
+ this.partNumber += 1;
324
+ if (this.partNumber > S3_MAX_PARTS) {
325
+ throw new ValidationError(`multipart upload exceeded the S3 ${S3_MAX_PARTS}-part limit`);
326
+ }
327
+ const res = await this.client.send(
328
+ new UploadPartCommand({
329
+ Bucket: this.bucket,
330
+ Key: this.objectKey,
331
+ UploadId: this.uploadId,
332
+ PartNumber: this.partNumber,
333
+ Body: body
334
+ })
335
+ );
336
+ this.parts.push({ ETag: res.ETag, PartNumber: this.partNumber });
337
+ }
338
+ /** Commit the object: a single conditional PUT if it fit in one part, else complete the multipart upload. */
339
+ async finish() {
340
+ const sha256 = this.hash.digest("hex");
341
+ if (this.uploadId === void 0) {
342
+ await this.client.send(
343
+ new PutObjectCommand({
344
+ Bucket: this.bucket,
345
+ Key: this.objectKey,
346
+ Body: concatBytes(this.pending, this.pendingLen),
347
+ IfNoneMatch: "*"
348
+ // write-once
349
+ })
350
+ );
351
+ return { size: this.total, sha256 };
352
+ }
353
+ if (this.pendingLen > 0) await this.flushPart();
354
+ await this.client.send(
355
+ new CompleteMultipartUploadCommand({
356
+ Bucket: this.bucket,
357
+ Key: this.objectKey,
358
+ UploadId: this.uploadId,
359
+ MultipartUpload: { Parts: this.parts },
360
+ IfNoneMatch: "*"
361
+ // write-once: fail if the object already exists
362
+ })
363
+ );
364
+ this.uploadId = void 0;
365
+ return { size: this.total, sha256 };
366
+ }
367
+ /** Best-effort cleanup of an in-flight multipart upload after an error (a leaked MPU is reaped by a bucket
368
+ * lifecycle rule; never a correctness issue). No-op if nothing was started or it already completed. */
369
+ async abort() {
370
+ if (this.uploadId === void 0) return;
371
+ const id = this.uploadId;
372
+ this.uploadId = void 0;
373
+ try {
374
+ await this.client.send(
375
+ new AbortMultipartUploadCommand({ Bucket: this.bucket, Key: this.objectKey, UploadId: id })
376
+ );
377
+ } catch {
378
+ }
379
+ }
380
+ };
381
+ async function collect(body) {
382
+ if (body === void 0) {
383
+ throw new NotFoundError("S3 GetObject returned an empty body");
384
+ }
385
+ return body.transformToByteArray();
386
+ }
387
+ var MAX_ROW_BYTES = 1 * 1024 * 1024;
388
+ var MAX_DELETE_ATTEMPTS = 8;
389
+ var LIST_READ_CONCURRENCY = 16;
390
+ var S3RegistryDriver = class {
391
+ client;
392
+ bucket;
393
+ prefix;
394
+ now;
395
+ constructor(options) {
396
+ this.client = options.client;
397
+ this.bucket = options.bucket;
398
+ this.prefix = normalizeS3Prefix(options.prefix);
399
+ this.now = options.now ?? (() => Date.now());
400
+ }
401
+ capabilities() {
402
+ return { strongRead: true };
403
+ }
404
+ async get(ref) {
405
+ const current = await this.readRow(registryObjectKey(this.prefix, ref));
406
+ return current && !current.env.deleted ? current.env.record : null;
407
+ }
408
+ async create(ref, record) {
409
+ validateNewRegistryRecord(record);
410
+ const key = registryObjectKey(this.prefix, ref);
411
+ const current = await this.readRow(key);
412
+ if (current !== null && !current.env.deleted) {
413
+ throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);
414
+ }
415
+ const token = String(current ? registryCounterOf(current.env.record) + 1 : 0);
416
+ const env = {
417
+ deleted: false,
418
+ record: recordFromNew(ref, record, this.now(), token)
419
+ };
420
+ await this.putRow(key, env, current ? { ifMatch: current.etag } : { ifNoneMatch: "*" });
421
+ return { token };
422
+ }
423
+ async compareAndSwap(ref, expected, patch) {
424
+ validateRegistryPatch(patch);
425
+ const key = registryObjectKey(this.prefix, ref);
426
+ const current = await this.readRow(key);
427
+ if (current === null || current.env.deleted || current.env.record.token !== expected) {
428
+ throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
429
+ }
430
+ const token = String(registryCounterOf(current.env.record) + 1);
431
+ const env = {
432
+ deleted: false,
433
+ record: applyRegistryPatch(current.env.record, patch, this.now(), token)
434
+ };
435
+ await this.putRow(key, env, { ifMatch: current.etag });
436
+ return { token };
437
+ }
438
+ async *list(namespace) {
439
+ const prefix = registryListPrefix(this.prefix, namespace);
440
+ let token;
441
+ do {
442
+ let res;
443
+ try {
444
+ res = await this.client.send(
445
+ new ListObjectsV2Command({
446
+ Bucket: this.bucket,
447
+ Prefix: prefix,
448
+ ContinuationToken: token
449
+ })
450
+ );
451
+ } catch (err) {
452
+ throw this.mapError(err);
453
+ }
454
+ const keys = (res.Contents ?? []).map((obj) => obj.Key).filter((k) => k !== void 0 && parseRegistryKey(this.prefix, k) !== null);
455
+ const rows = await mapWithConcurrency(
456
+ keys,
457
+ LIST_READ_CONCURRENCY,
458
+ (key) => this.readRow(key)
459
+ );
460
+ for (const current of rows) {
461
+ if (current && !current.env.deleted) yield current.env.record;
462
+ }
463
+ token = res.IsTruncated === true ? res.NextContinuationToken : void 0;
464
+ } while (token !== void 0);
465
+ }
466
+ async delete(ref) {
467
+ const key = registryObjectKey(this.prefix, ref);
468
+ for (let attempt = 0; attempt < MAX_DELETE_ATTEMPTS; attempt++) {
469
+ const current = await this.readRow(key);
470
+ if (current === null || current.env.deleted) return;
471
+ const token = String(registryCounterOf(current.env.record) + 1);
472
+ const env = {
473
+ deleted: true,
474
+ record: { ...current.env.record, token, updatedAt: this.now() }
475
+ };
476
+ try {
477
+ await this.putRow(key, env, { ifMatch: current.etag });
478
+ return;
479
+ } catch (err) {
480
+ if (isWriteConflictError(err)) continue;
481
+ throw err;
482
+ }
483
+ }
484
+ throw new WriteConflictError(
485
+ `registry delete: contention tombstoning "${ref.segment}" \u2014 retry`
486
+ );
487
+ }
488
+ /** GET + parse a registry object, returning its envelope + ETag (for a later If-Match), or null if absent. */
489
+ async readRow(objectKey) {
490
+ let res;
491
+ try {
492
+ res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }));
493
+ } catch (err) {
494
+ if (isNotFound(err)) return null;
495
+ throw this.mapError(err);
496
+ }
497
+ if ((res.ContentLength ?? 0) > MAX_ROW_BYTES) {
498
+ throw new IntegrityError(
499
+ `registry object ${res.ContentLength}B exceeds cap ${MAX_ROW_BYTES}B`
500
+ );
501
+ }
502
+ if (res.Body === void 0) {
503
+ throw new IntegrityError(`registry object has an empty body: ${objectKey}`);
504
+ }
505
+ const bytes = await res.Body.transformToByteArray();
506
+ if (bytes.length > MAX_ROW_BYTES) {
507
+ throw new IntegrityError(`registry object ${bytes.length}B exceeds cap ${MAX_ROW_BYTES}B`);
508
+ }
509
+ const env = parseRegistryEnvelope(Buffer.from(bytes).toString("utf8"), objectKey);
510
+ if (res.ETag === void 0 || res.ETag.length === 0) {
511
+ throw new IntegrityError(
512
+ `registry object has no ETag (needed for compare-and-swap): ${objectKey}`
513
+ );
514
+ }
515
+ return { env, etag: res.ETag };
516
+ }
517
+ /** Conditional PUT of an envelope; maps a `412` precondition failure to {@link WriteConflictError}. */
518
+ async putRow(objectKey, env, cond) {
519
+ const Body = Buffer.from(serializeRegistryEnvelope(env), "utf8");
520
+ if (Body.length > MAX_ROW_BYTES) {
521
+ throw new ValidationError(`registry object ${Body.length}B exceeds cap ${MAX_ROW_BYTES}B`);
522
+ }
523
+ try {
524
+ await this.client.send(
525
+ new PutObjectCommand({
526
+ Bucket: this.bucket,
527
+ Key: objectKey,
528
+ Body,
529
+ ContentType: "application/json",
530
+ IfNoneMatch: "ifNoneMatch" in cond ? cond.ifNoneMatch : void 0,
531
+ IfMatch: "ifMatch" in cond ? cond.ifMatch : void 0
532
+ })
533
+ );
534
+ } catch (err) {
535
+ if (isConditionalConflict(err)) {
536
+ throw new WriteConflictError(`registry OCC conflict for ${objectKey}`);
537
+ }
538
+ throw this.mapError(err);
539
+ }
540
+ }
541
+ /** Reclassify a transient S3 fault as a retryable {@link TransientError}; pass everything else through. */
542
+ mapError(err) {
543
+ if (isTransient(err)) {
544
+ return new TransientError(
545
+ `transient S3 fault: ${err?.name ?? "unknown"}`,
546
+ { cause: err }
547
+ );
548
+ }
549
+ return err;
550
+ }
551
+ };
552
+
553
+ export { S3ColdDriver, S3RegistryDriver };
554
+ //# sourceMappingURL=index.js.map
555
+ //# sourceMappingURL=index.js.map