@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,279 @@
1
+ import { WriteConflictError, isValidationError, isWriteConflictError, isNotFoundError, ValidationError, NotFoundError, TransientError, validateSegmentRef, namespacePart } from '../chunk-NUIDEEFZ.js';
2
+ import { createHash } from 'crypto';
3
+ import { once } from 'events';
4
+
5
+ // src/drivers/gcs/keys.ts
6
+ var SUFFIX = ".crbm";
7
+ function prefixPart(prefix) {
8
+ if (prefix === void 0) return "";
9
+ const trimmed = prefix.replace(/^\/+|\/+$/g, "");
10
+ return trimmed === "" ? "" : `${trimmed}/`;
11
+ }
12
+ function normalizeGcsPrefix(prefix) {
13
+ if (prefix === void 0) return void 0;
14
+ for (const ch of prefix) {
15
+ if (ch.charCodeAt(0) < 32) {
16
+ throw new ValidationError("prefix must not contain control characters");
17
+ }
18
+ }
19
+ for (const segment of prefix.split("/")) {
20
+ if (segment === "." || segment === "..") {
21
+ throw new ValidationError(
22
+ `prefix must not contain "." or ".." path segments: ${JSON.stringify(prefix)}`
23
+ );
24
+ }
25
+ }
26
+ return prefix;
27
+ }
28
+ function segmentObjectPrefix(prefix, ref) {
29
+ validateSegmentRef(ref);
30
+ return `${prefixPart(prefix)}${namespacePart(ref.namespace)}/segments/${ref.segment}.`;
31
+ }
32
+ function coldObjectName(prefix, key) {
33
+ if (!Number.isInteger(key.generation) || key.generation < 0) {
34
+ throw new ValidationError(`generation must be a non-negative integer; got ${key.generation}`);
35
+ }
36
+ return `${segmentObjectPrefix(prefix, key)}${key.generation}${SUFFIX}`;
37
+ }
38
+ function parseGenerationFromName(segmentPrefix, objectName) {
39
+ if (!objectName.startsWith(segmentPrefix) || !objectName.endsWith(SUFFIX)) return null;
40
+ const middle = objectName.slice(segmentPrefix.length, objectName.length - SUFFIX.length);
41
+ if (!/^(0|[1-9]\d*)$/.test(middle)) return null;
42
+ const generation = Number(middle);
43
+ return Number.isSafeInteger(generation) ? generation : null;
44
+ }
45
+
46
+ // src/drivers/gcs/gcs-errors.ts
47
+ function httpStatus(err) {
48
+ const e = err;
49
+ if (typeof e?.code === "number") return e.code;
50
+ if (typeof e?.response?.status === "number") return e.response.status;
51
+ return void 0;
52
+ }
53
+ function networkCode(err) {
54
+ const code = err?.code;
55
+ return typeof code === "string" ? code : void 0;
56
+ }
57
+ function isPreconditionFailed(err) {
58
+ return httpStatus(err) === 412;
59
+ }
60
+ function isNotFound(err) {
61
+ return httpStatus(err) === 404;
62
+ }
63
+ function isInvalidRange(err) {
64
+ return httpStatus(err) === 416;
65
+ }
66
+ function isTransient(err) {
67
+ if (isPreconditionFailed(err) || isNotFound(err) || isInvalidRange(err)) return false;
68
+ const status = httpStatus(err);
69
+ if (status === 429 || status !== void 0 && status >= 500 && status < 600) return true;
70
+ const net = networkCode(err);
71
+ return net === "ECONNRESET" || net === "ETIMEDOUT" || net === "ECONNREFUSED" || net === "EPIPE" || net === "EAI_AGAIN" || net === "ENOTFOUND";
72
+ }
73
+
74
+ // src/drivers/gcs/cold.ts
75
+ var DEFAULT_MAX_OBJECT_BYTES = 5 * 1024 * 1024 * 1024 * 1024;
76
+ var DEFAULT_UPLOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
77
+ var GcsColdDriver = class {
78
+ storage;
79
+ bucket;
80
+ prefix;
81
+ maxObjectBytes;
82
+ threshold;
83
+ constructor(options) {
84
+ this.storage = options.storage;
85
+ this.bucket = options.bucket;
86
+ this.prefix = normalizeGcsPrefix(options.prefix);
87
+ this.maxObjectBytes = options.maxObjectBytes ?? DEFAULT_MAX_OBJECT_BYTES;
88
+ this.threshold = options.simpleUploadThresholdBytes ?? DEFAULT_UPLOAD_THRESHOLD_BYTES;
89
+ }
90
+ capabilities() {
91
+ return { rangeRead: true, maxObjectBytes: this.maxObjectBytes, conditionalPut: true };
92
+ }
93
+ file(name) {
94
+ return this.storage.bucket(this.bucket).file(name);
95
+ }
96
+ async putImmutable(key, write) {
97
+ const objectName = coldObjectName(this.prefix, key);
98
+ const sink = new GcsUploadSink(this.file(objectName), this.maxObjectBytes, this.threshold);
99
+ try {
100
+ await write(sink);
101
+ return await sink.finish();
102
+ } catch (err) {
103
+ await sink.abort();
104
+ if (isPreconditionFailed(err)) {
105
+ throw new WriteConflictError(
106
+ `generation already exists (write-once): ${key.segment}.${key.generation}`
107
+ );
108
+ }
109
+ if (isValidationError(err) || isWriteConflictError(err) || isNotFoundError(err)) throw err;
110
+ throw this.mapError(err);
111
+ }
112
+ }
113
+ async getRange(key, offset, length) {
114
+ if (!Number.isInteger(offset) || !Number.isInteger(length) || offset < 0 || length < 0) {
115
+ throw new ValidationError(`invalid range offset=${offset} length=${length}`);
116
+ }
117
+ if (length === 0) return new Uint8Array(0);
118
+ const objectName = coldObjectName(this.prefix, key);
119
+ try {
120
+ const [buf] = await this.file(objectName).download({
121
+ start: offset,
122
+ end: offset + length - 1
123
+ });
124
+ if (buf.length !== length) {
125
+ throw new ValidationError(
126
+ `range [${offset}, ${offset + length}) out of bounds (got ${buf.length}B)`
127
+ );
128
+ }
129
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
130
+ } catch (err) {
131
+ throw this.mapReadError(err, key);
132
+ }
133
+ }
134
+ async getTail(key, maxBytes) {
135
+ const objectName = coldObjectName(this.prefix, key);
136
+ try {
137
+ const [meta] = await this.file(objectName).getMetadata();
138
+ const size = Number(meta.size ?? 0);
139
+ if (!Number.isSafeInteger(size) || size < 0) {
140
+ throw new ValidationError(`GCS returned an invalid object size: ${String(meta.size)}`);
141
+ }
142
+ if (maxBytes <= 0 || size === 0) return { bytes: new Uint8Array(0), size };
143
+ const start = Math.max(0, size - maxBytes);
144
+ const [buf] = await this.file(objectName).download({ start, end: size - 1 });
145
+ return { bytes: new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), size };
146
+ } catch (err) {
147
+ throw this.mapReadError(err, key);
148
+ }
149
+ }
150
+ async delete(key) {
151
+ try {
152
+ await this.file(coldObjectName(this.prefix, key)).delete({ ignoreNotFound: true });
153
+ } catch (err) {
154
+ throw this.mapError(err);
155
+ }
156
+ }
157
+ async *list(ref) {
158
+ const prefix = segmentObjectPrefix(this.prefix, ref);
159
+ let files;
160
+ try {
161
+ [files] = await this.storage.bucket(this.bucket).getFiles({ prefix });
162
+ } catch (err) {
163
+ throw this.mapError(err);
164
+ }
165
+ for (const f of files) {
166
+ const generation = parseGenerationFromName(prefix, f.name);
167
+ if (generation !== null) {
168
+ yield { namespace: ref.namespace, segment: ref.segment, generation };
169
+ }
170
+ }
171
+ }
172
+ /** Map GCS read errors to the driver vocabulary; pass everything else through {@link mapError}. */
173
+ mapReadError(err, key) {
174
+ if (isValidationError(err)) return err;
175
+ if (isNotFound(err))
176
+ return new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
177
+ if (isInvalidRange(err)) {
178
+ return new ValidationError(`range out of bounds for ${key.segment}.${key.generation}`);
179
+ }
180
+ return this.mapError(err);
181
+ }
182
+ /**
183
+ * Reclassify a transient GCS fault (throttle/5xx/dropped connection) as a retryable {@link TransientError}
184
+ * so the retry decorator can ride it out; everything else propagates unchanged. The final fallback at every
185
+ * client-call site, so callers + the decorator only ever see typed errors.
186
+ */
187
+ mapError(err) {
188
+ if (isTransient(err)) {
189
+ return new TransientError(
190
+ `transient GCS fault: ${err?.code ?? "unknown"}`,
191
+ { cause: err }
192
+ );
193
+ }
194
+ return err;
195
+ }
196
+ };
197
+ function concatBytes(parts, total) {
198
+ const out = new Uint8Array(total);
199
+ let offset = 0;
200
+ for (const p of parts) {
201
+ out.set(p, offset);
202
+ offset += p.length;
203
+ }
204
+ return out;
205
+ }
206
+ var GcsUploadSink = class _GcsUploadSink {
207
+ constructor(file, maxObjectBytes, threshold) {
208
+ this.file = file;
209
+ this.maxObjectBytes = maxObjectBytes;
210
+ this.threshold = threshold;
211
+ }
212
+ file;
213
+ maxObjectBytes;
214
+ threshold;
215
+ hash = createHash("sha256");
216
+ total = 0;
217
+ buffered = [];
218
+ bufferedLen = 0;
219
+ stream;
220
+ // set once we cross the threshold → resumable mode
221
+ done;
222
+ failure;
223
+ static WRITE_OPTS = {
224
+ preconditionOpts: { ifGenerationMatch: 0 },
225
+ // write-once: create only if absent
226
+ metadata: { contentType: "application/octet-stream" }
227
+ };
228
+ async write(bytes) {
229
+ if (this.failure !== void 0) throw this.failure;
230
+ if (bytes.length === 0) return;
231
+ this.total += bytes.length;
232
+ if (this.total > this.maxObjectBytes) {
233
+ throw new ValidationError(`object exceeds maxObjectBytes ${this.maxObjectBytes}`);
234
+ }
235
+ this.hash.update(bytes);
236
+ if (this.stream !== void 0) {
237
+ if (!this.stream.write(bytes)) await once(this.stream, "drain");
238
+ return;
239
+ }
240
+ this.buffered.push(bytes);
241
+ this.bufferedLen += bytes.length;
242
+ if (this.bufferedLen > this.threshold) this.startResumable();
243
+ }
244
+ /** Cross into resumable streaming: open the stream, flush the buffered bytes, keep only ~one threshold resident. */
245
+ startResumable() {
246
+ const stream = this.file.createWriteStream({ resumable: true, ..._GcsUploadSink.WRITE_OPTS });
247
+ this.stream = stream;
248
+ this.done = once(stream, "finish");
249
+ stream.on("error", (e) => {
250
+ this.failure ??= e;
251
+ });
252
+ this.done.catch(() => void 0);
253
+ const buf = concatBytes(this.buffered, this.bufferedLen);
254
+ this.buffered.length = 0;
255
+ this.bufferedLen = 0;
256
+ stream.write(buf);
257
+ }
258
+ async finish() {
259
+ if (this.failure !== void 0) throw this.failure;
260
+ const sha256 = this.hash.digest("hex");
261
+ if (this.stream === void 0) {
262
+ await this.file.save(concatBytes(this.buffered, this.bufferedLen), {
263
+ resumable: false,
264
+ ..._GcsUploadSink.WRITE_OPTS
265
+ });
266
+ return { size: this.total, sha256 };
267
+ }
268
+ this.stream.end();
269
+ await this.done;
270
+ return { size: this.total, sha256 };
271
+ }
272
+ async abort() {
273
+ if (this.stream !== void 0 && !this.stream.destroyed) this.stream.destroy();
274
+ }
275
+ };
276
+
277
+ export { GcsColdDriver };
278
+ //# sourceMappingURL=index.js.map
279
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/drivers/gcs/keys.ts","../../src/drivers/gcs/gcs-errors.ts","../../src/drivers/gcs/cold.ts"],"names":[],"mappings":";;;;;AAoBA,IAAM,MAAA,GAAS,OAAA;AAGf,SAAS,WAAW,MAAA,EAAoC;AACtD,EAAA,IAAI,MAAA,KAAW,QAAW,OAAO,EAAA;AACjC,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,YAAA,EAAc,EAAE,CAAA;AAC/C,EAAA,OAAO,OAAA,KAAY,EAAA,GAAK,EAAA,GAAK,CAAA,EAAG,OAAO,CAAA,CAAA,CAAA;AACzC;AAOO,SAAS,mBAAmB,MAAA,EAAgD;AACjF,EAAA,IAAI,MAAA,KAAW,QAAW,OAAO,MAAA;AACjC,EAAA,KAAA,MAAW,MAAM,MAAA,EAAQ;AACvB,IAAA,IAAI,EAAA,CAAG,UAAA,CAAW,CAAC,CAAA,GAAI,EAAA,EAAM;AAC3B,MAAA,MAAM,IAAI,gBAAgB,4CAA4C,CAAA;AAAA,IACxE;AAAA,EACF;AACA,EAAA,KAAA,MAAW,OAAA,IAAW,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA,EAAG;AACvC,IAAA,IAAI,OAAA,KAAY,GAAA,IAAO,OAAA,KAAY,IAAA,EAAM;AACvC,MAAA,MAAM,IAAI,eAAA;AAAA,QACR,CAAA,mDAAA,EAAsD,IAAA,CAAK,SAAA,CAAU,MAAM,CAAC,CAAA;AAAA,OAC9E;AAAA,IACF;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAMO,SAAS,mBAAA,CAAoB,QAA4B,GAAA,EAAyB;AACvF,EAAA,kBAAA,CAAmB,GAAG,CAAA;AACtB,EAAA,OAAO,CAAA,EAAG,UAAA,CAAW,MAAM,CAAC,CAAA,EAAG,aAAA,CAAc,GAAA,CAAI,SAAS,CAAC,CAAA,UAAA,EAAa,GAAA,CAAI,OAAO,CAAA,CAAA,CAAA;AACrF;AAGO,SAAS,cAAA,CAAe,QAA4B,GAAA,EAAqB;AAC9E,EAAA,IAAI,CAAC,OAAO,SAAA,CAAU,GAAA,CAAI,UAAU,CAAA,IAAK,GAAA,CAAI,aAAa,CAAA,EAAG;AAC3D,IAAA,MAAM,IAAI,eAAA,CAAgB,CAAA,+CAAA,EAAkD,GAAA,CAAI,UAAU,CAAA,CAAE,CAAA;AAAA,EAC9F;AACA,EAAA,OAAO,CAAA,EAAG,oBAAoB,MAAA,EAAQ,GAAG,CAAC,CAAA,EAAG,GAAA,CAAI,UAAU,CAAA,EAAG,MAAM,CAAA,CAAA;AACtE;AAQO,SAAS,uBAAA,CAAwB,eAAuB,UAAA,EAAmC;AAChG,EAAA,IAAI,CAAC,UAAA,CAAW,UAAA,CAAW,aAAa,CAAA,IAAK,CAAC,UAAA,CAAW,QAAA,CAAS,MAAM,CAAA,EAAG,OAAO,IAAA;AAClF,EAAA,MAAM,MAAA,GAAS,WAAW,KAAA,CAAM,aAAA,CAAc,QAAQ,UAAA,CAAW,MAAA,GAAS,OAAO,MAAM,CAAA;AACvF,EAAA,IAAI,CAAC,gBAAA,CAAiB,IAAA,CAAK,MAAM,GAAG,OAAO,IAAA;AAC3C,EAAA,MAAM,UAAA,GAAa,OAAO,MAAM,CAAA;AAChC,EAAA,OAAO,MAAA,CAAO,aAAA,CAAc,UAAU,CAAA,GAAI,UAAA,GAAa,IAAA;AACzD;;;ACrEA,SAAS,WAAW,GAAA,EAAkC;AACpD,EAAA,MAAM,CAAA,GAAI,GAAA;AACV,EAAA,IAAI,OAAO,CAAA,EAAG,IAAA,KAAS,QAAA,SAAiB,CAAA,CAAE,IAAA;AAC1C,EAAA,IAAI,OAAO,CAAA,EAAG,QAAA,EAAU,WAAW,QAAA,EAAU,OAAO,EAAE,QAAA,CAAS,MAAA;AAC/D,EAAA,OAAO,MAAA;AACT;AAGA,SAAS,YAAY,GAAA,EAAkC;AACrD,EAAA,MAAM,OAAQ,GAAA,EAAmC,IAAA;AACjD,EAAA,OAAO,OAAO,IAAA,KAAS,QAAA,GAAW,IAAA,GAAO,MAAA;AAC3C;AAMO,SAAS,qBAAqB,GAAA,EAAuB;AAC1D,EAAA,OAAO,UAAA,CAAW,GAAG,CAAA,KAAM,GAAA;AAC7B;AAGO,SAAS,WAAW,GAAA,EAAuB;AAChD,EAAA,OAAO,UAAA,CAAW,GAAG,CAAA,KAAM,GAAA;AAC7B;AAGO,SAAS,eAAe,GAAA,EAAuB;AACpD,EAAA,OAAO,UAAA,CAAW,GAAG,CAAA,KAAM,GAAA;AAC7B;AAOO,SAAS,YAAY,GAAA,EAAuB;AACjD,EAAA,IAAI,oBAAA,CAAqB,GAAG,CAAA,IAAK,UAAA,CAAW,GAAG,CAAA,IAAK,cAAA,CAAe,GAAG,CAAA,EAAG,OAAO,KAAA;AAChF,EAAA,MAAM,MAAA,GAAS,WAAW,GAAG,CAAA;AAC7B,EAAA,IAAI,MAAA,KAAW,OAAQ,MAAA,KAAW,MAAA,IAAa,UAAU,GAAA,IAAO,MAAA,GAAS,KAAM,OAAO,IAAA;AACtF,EAAA,MAAM,GAAA,GAAM,YAAY,GAAG,CAAA;AAC3B,EAAA,OACE,GAAA,KAAQ,YAAA,IACR,GAAA,KAAQ,WAAA,IACR,GAAA,KAAQ,kBACR,GAAA,KAAQ,OAAA,IACR,GAAA,KAAQ,WAAA,IACR,GAAA,KAAQ,WAAA;AAEZ;;;ACpBA,IAAM,wBAAA,GAA2B,CAAA,GAAI,IAAA,GAAO,IAAA,GAAO,IAAA,GAAO,IAAA;AAQ1D,IAAM,8BAAA,GAAiC,IAAI,IAAA,GAAO,IAAA;AAe3C,IAAM,gBAAN,MAA2C;AAAA,EAC/B,OAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,cAAA;AAAA,EACA,SAAA;AAAA,EAEjB,YAAY,OAAA,EAA+B;AACzC,IAAA,IAAA,CAAK,UAAU,OAAA,CAAQ,OAAA;AACvB,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,MAAA,GAAS,kBAAA,CAAmB,OAAA,CAAQ,MAAM,CAAA;AAC/C,IAAA,IAAA,CAAK,cAAA,GAAiB,QAAQ,cAAA,IAAkB,wBAAA;AAChD,IAAA,IAAA,CAAK,SAAA,GAAY,QAAQ,0BAAA,IAA8B,8BAAA;AAAA,EACzD;AAAA,EAEA,YAAA,GAAyB;AACvB,IAAA,OAAO,EAAE,SAAA,EAAW,IAAA,EAAM,gBAAgB,IAAA,CAAK,cAAA,EAAgB,gBAAgB,IAAA,EAAK;AAAA,EACtF;AAAA,EAEQ,KAAK,IAAA,EAAc;AACzB,IAAA,OAAO,KAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,MAAM,CAAA,CAAE,KAAK,IAAI,CAAA;AAAA,EACnD;AAAA,EAEA,MAAM,YAAA,CACJ,GAAA,EACA,KAAA,EAC2C;AAC3C,IAAA,MAAM,UAAA,GAAa,cAAA,CAAe,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAA;AAClD,IAAA,MAAM,IAAA,GAAO,IAAI,aAAA,CAAc,IAAA,CAAK,IAAA,CAAK,UAAU,CAAA,EAAG,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,SAAS,CAAA;AACzF,IAAA,IAAI;AACF,MAAA,MAAM,MAAM,IAAI,CAAA;AAChB,MAAA,OAAO,MAAM,KAAK,MAAA,EAAO;AAAA,IAC3B,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,KAAK,KAAA,EAAM;AAEjB,MAAA,IAAI,oBAAA,CAAqB,GAAG,CAAA,EAAG;AAC7B,QAAA,MAAM,IAAI,kBAAA;AAAA,UACR,CAAA,wCAAA,EAA2C,GAAA,CAAI,OAAO,CAAA,CAAA,EAAI,IAAI,UAAU,CAAA;AAAA,SAC1E;AAAA,MACF;AACA,MAAA,IAAI,iBAAA,CAAkB,GAAG,CAAA,IAAK,oBAAA,CAAqB,GAAG,CAAA,IAAK,eAAA,CAAgB,GAAG,CAAA,EAAG,MAAM,GAAA;AACvF,MAAA,MAAM,IAAA,CAAK,SAAS,GAAG,CAAA;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,MAAM,QAAA,CAAS,GAAA,EAAa,MAAA,EAAgB,MAAA,EAAqC;AAC/E,IAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,MAAM,CAAA,IAAK,CAAC,MAAA,CAAO,SAAA,CAAU,MAAM,CAAA,IAAK,MAAA,GAAS,CAAA,IAAK,SAAS,CAAA,EAAG;AACtF,MAAA,MAAM,IAAI,eAAA,CAAgB,CAAA,qBAAA,EAAwB,MAAM,CAAA,QAAA,EAAW,MAAM,CAAA,CAAE,CAAA;AAAA,IAC7E;AACA,IAAA,IAAI,MAAA,KAAW,CAAA,EAAG,OAAO,IAAI,WAAW,CAAC,CAAA;AACzC,IAAA,MAAM,UAAA,GAAa,cAAA,CAAe,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAA;AAClD,IAAA,IAAI;AAEF,MAAA,MAAM,CAAC,GAAG,CAAA,GAAI,MAAM,KAAK,IAAA,CAAK,UAAU,EAAE,QAAA,CAAS;AAAA,QACjD,KAAA,EAAO,MAAA;AAAA,QACP,GAAA,EAAK,SAAS,MAAA,GAAS;AAAA,OACxB,CAAA;AAED,MAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,QAAA,MAAM,IAAI,eAAA;AAAA,UACR,UAAU,MAAM,CAAA,EAAA,EAAK,SAAS,MAAM,CAAA,qBAAA,EAAwB,IAAI,MAAM,CAAA,EAAA;AAAA,SACxE;AAAA,MACF;AACA,MAAA,OAAO,IAAI,UAAA,CAAW,GAAA,CAAI,QAAQ,GAAA,CAAI,UAAA,EAAY,IAAI,UAAU,CAAA;AAAA,IAClE,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAA,CAAK,YAAA,CAAa,GAAA,EAAK,GAAG,CAAA;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAM,OAAA,CAAQ,GAAA,EAAa,QAAA,EAAgE;AACzF,IAAA,MAAM,UAAA,GAAa,cAAA,CAAe,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAA;AAClD,IAAA,IAAI;AAIF,MAAA,MAAM,CAAC,IAAI,CAAA,GAAI,MAAM,KAAK,IAAA,CAAK,UAAU,EAAE,WAAA,EAAY;AACvD,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,IAAA,IAAQ,CAAC,CAAA;AAClC,MAAA,IAAI,CAAC,MAAA,CAAO,aAAA,CAAc,IAAI,CAAA,IAAK,OAAO,CAAA,EAAG;AAC3C,QAAA,MAAM,IAAI,eAAA,CAAgB,CAAA,qCAAA,EAAwC,OAAO,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,MACvF;AACA,MAAA,IAAI,QAAA,IAAY,CAAA,IAAK,IAAA,KAAS,CAAA,EAAG,OAAO,EAAE,KAAA,EAAO,IAAI,UAAA,CAAW,CAAC,CAAA,EAAG,IAAA,EAAK;AACzE,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,OAAO,QAAQ,CAAA;AACzC,MAAA,MAAM,CAAC,GAAG,CAAA,GAAI,MAAM,KAAK,IAAA,CAAK,UAAU,CAAA,CAAE,QAAA,CAAS,EAAE,KAAA,EAAO,GAAA,EAAK,IAAA,GAAO,GAAG,CAAA;AAC3E,MAAA,OAAO,EAAE,KAAA,EAAO,IAAI,UAAA,CAAW,GAAA,CAAI,MAAA,EAAQ,GAAA,CAAI,UAAA,EAAY,GAAA,CAAI,UAAU,CAAA,EAAG,IAAA,EAAK;AAAA,IACnF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAA,CAAK,YAAA,CAAa,GAAA,EAAK,GAAG,CAAA;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,GAAA,EAA4B;AAEvC,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,IAAA,CAAK,cAAA,CAAe,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAC,CAAA,CAAE,MAAA,CAAO,EAAE,cAAA,EAAgB,IAAA,EAAM,CAAA;AAAA,IACnF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAA,CAAK,SAAS,GAAG,CAAA;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,OAAO,KAAK,GAAA,EAAwC;AAClD,IAAA,MAAM,MAAA,GAAS,mBAAA,CAAoB,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAA;AACnD,IAAA,IAAI,KAAA;AACJ,IAAA,IAAI;AAEF,MAAA,CAAC,KAAK,CAAA,GAAI,MAAM,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA,CAAE,QAAA,CAAS,EAAE,MAAA,EAAQ,CAAA;AAAA,IACtE,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAA,CAAK,SAAS,GAAG,CAAA;AAAA,IACzB;AACA,IAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,MAAA,MAAM,UAAA,GAAa,uBAAA,CAAwB,MAAA,EAAQ,CAAA,CAAE,IAAI,CAAA;AACzD,MAAA,IAAI,eAAe,IAAA,EAAM;AACvB,QAAA,MAAM,EAAE,SAAA,EAAW,GAAA,CAAI,WAAW,OAAA,EAAS,GAAA,CAAI,SAAS,UAAA,EAAW;AAAA,MACrE;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGQ,YAAA,CAAa,KAAc,GAAA,EAAsB;AACvD,IAAA,IAAI,iBAAA,CAAkB,GAAG,CAAA,EAAG,OAAO,GAAA;AACnC,IAAA,IAAI,WAAW,GAAG,CAAA;AAChB,MAAA,OAAO,IAAI,cAAc,CAAA,oBAAA,EAAuB,GAAA,CAAI,OAAO,CAAA,CAAA,EAAI,GAAA,CAAI,UAAU,CAAA,CAAE,CAAA;AAGjF,IAAA,IAAI,cAAA,CAAe,GAAG,CAAA,EAAG;AACvB,MAAA,OAAO,IAAI,gBAAgB,CAAA,wBAAA,EAA2B,GAAA,CAAI,OAAO,CAAA,CAAA,EAAI,GAAA,CAAI,UAAU,CAAA,CAAE,CAAA;AAAA,IACvF;AACA,IAAA,OAAO,IAAA,CAAK,SAAS,GAAG,CAAA;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,SAAS,GAAA,EAAuB;AACtC,IAAA,IAAI,WAAA,CAAY,GAAG,CAAA,EAAG;AACpB,MAAA,OAAO,IAAI,cAAA;AAAA,QACT,CAAA,qBAAA,EAAyB,GAAA,EAAmC,IAAA,IAAQ,SAAS,CAAA,CAAA;AAAA,QAC7E,EAAE,OAAO,GAAA;AAAI,OACf;AAAA,IACF;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AACF;AAKA,SAAS,WAAA,CAAY,OAA8B,KAAA,EAA2B;AAC5E,EAAA,MAAM,GAAA,GAAM,IAAI,UAAA,CAAW,KAAK,CAAA;AAChC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,GAAA,CAAI,GAAA,CAAI,GAAG,MAAM,CAAA;AACjB,IAAA,MAAA,IAAU,CAAA,CAAE,MAAA;AAAA,EACd;AACA,EAAA,OAAO,GAAA;AACT;AAUA,IAAM,aAAA,GAAN,MAAM,cAAA,CAAkC;AAAA,EAStC,WAAA,CACmB,IAAA,EACA,cAAA,EACA,SAAA,EACjB;AAHiB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AACA,IAAA,IAAA,CAAA,cAAA,GAAA,cAAA;AACA,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AAAA,EAChB;AAAA,EAHgB,IAAA;AAAA,EACA,cAAA;AAAA,EACA,SAAA;AAAA,EAXF,IAAA,GAAa,WAAW,QAAQ,CAAA;AAAA,EACzC,KAAA,GAAQ,CAAA;AAAA,EACC,WAAyB,EAAC;AAAA,EACnC,WAAA,GAAc,CAAA;AAAA,EACd,MAAA;AAAA;AAAA,EACA,IAAA;AAAA,EACA,OAAA;AAAA,EAQR,OAAwB,UAAA,GAAa;AAAA,IACnC,gBAAA,EAAkB,EAAE,iBAAA,EAAmB,CAAA,EAAW;AAAA;AAAA,IAClD,QAAA,EAAU,EAAE,WAAA,EAAa,0BAAA;AAA2B,GACtD;AAAA,EAEA,MAAM,MAAM,KAAA,EAAkC;AAC5C,IAAA,IAAI,IAAA,CAAK,OAAA,KAAY,MAAA,EAAW,MAAM,IAAA,CAAK,OAAA;AAC3C,IAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACxB,IAAA,IAAA,CAAK,SAAS,KAAA,CAAM,MAAA;AACpB,IAAA,IAAI,IAAA,CAAK,KAAA,GAAQ,IAAA,CAAK,cAAA,EAAgB;AACpC,MAAA,MAAM,IAAI,eAAA,CAAgB,CAAA,8BAAA,EAAiC,IAAA,CAAK,cAAc,CAAA,CAAE,CAAA;AAAA,IAClF;AACA,IAAA,IAAA,CAAK,IAAA,CAAK,OAAO,KAAK,CAAA;AACtB,IAAA,IAAI,IAAA,CAAK,WAAW,MAAA,EAAW;AAI7B,MAAA,IAAI,CAAC,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,KAAK,GAAG,MAAM,IAAA,CAAK,IAAA,CAAK,MAAA,EAAQ,OAAO,CAAA;AAC9D,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,QAAA,CAAS,KAAK,KAAK,CAAA;AACxB,IAAA,IAAA,CAAK,eAAe,KAAA,CAAM,MAAA;AAC1B,IAAA,IAAI,IAAA,CAAK,WAAA,GAAc,IAAA,CAAK,SAAA,OAAgB,cAAA,EAAe;AAAA,EAC7D;AAAA;AAAA,EAGQ,cAAA,GAAuB;AAC7B,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,iBAAA,CAAkB,EAAE,WAAW,IAAA,EAAM,GAAG,cAAA,CAAc,UAAA,EAAY,CAAA;AAC3F,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,MAAA,EAAQ,QAAQ,CAAA;AACjC,IAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,CAAC,CAAA,KAAe;AACjC,MAAA,IAAA,CAAK,OAAA,KAAY,CAAA;AAAA,IACnB,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,MAAM,MAAS,CAAA;AAC/B,IAAA,MAAM,GAAA,GAAM,WAAA,CAAY,IAAA,CAAK,QAAA,EAAU,KAAK,WAAW,CAAA;AACvD,IAAA,IAAA,CAAK,SAAS,MAAA,GAAS,CAAA;AACvB,IAAA,IAAA,CAAK,WAAA,GAAc,CAAA;AACnB,IAAA,MAAA,CAAO,MAAM,GAAG,CAAA;AAAA,EAClB;AAAA,EAEA,MAAM,MAAA,GAAoD;AACxD,IAAA,IAAI,IAAA,CAAK,OAAA,KAAY,MAAA,EAAW,MAAM,IAAA,CAAK,OAAA;AAC3C,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,MAAA,CAAO,KAAK,CAAA;AACrC,IAAA,IAAI,IAAA,CAAK,WAAW,MAAA,EAAW;AAE7B,MAAA,MAAM,IAAA,CAAK,KAAK,IAAA,CAAK,WAAA,CAAY,KAAK,QAAA,EAAU,IAAA,CAAK,WAAW,CAAA,EAAG;AAAA,QACjE,SAAA,EAAW,KAAA;AAAA,QACX,GAAG,cAAA,CAAc;AAAA,OAClB,CAAA;AACD,MAAA,OAAO,EAAE,IAAA,EAAM,IAAA,CAAK,KAAA,EAAO,MAAA,EAAO;AAAA,IACpC;AACA,IAAA,IAAA,CAAK,OAAO,GAAA,EAAI;AAChB,IAAA,MAAM,IAAA,CAAK,IAAA;AACX,IAAA,OAAO,EAAE,IAAA,EAAM,IAAA,CAAK,KAAA,EAAO,MAAA,EAAO;AAAA,EACpC;AAAA,EAEA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAI,IAAA,CAAK,WAAW,MAAA,IAAa,CAAC,KAAK,MAAA,CAAO,SAAA,EAAW,IAAA,CAAK,MAAA,CAAO,OAAA,EAAQ;AAAA,EAC/E;AACF,CAAA","file":"index.js","sourcesContent":["/**\n * Logical-ref → GCS object-name mapping for {@link GcsColdDriver} (Phase 7).\n *\n * Pure string logic, no SDK dependency — unit-testable without GCS or an emulator. Uses the **same\n * backend-agnostic `.crbm` object-name scheme** as the S3 + LocalFs cold drivers\n * (`<prefix><ns>/segments/<segment>.<gen>.crbm`), so a segment reads identically whichever cold backend holds\n * it. The default (absent) namespace maps to `_default` (the grammar forbids a leading underscore, so it can't\n * collide with a real namespace).\n *\n * NOTE (DRY): this mirrors the pure cold-key builders in `drivers/s3/keys.ts`. They are deliberately **not**\n * shared across driver folders today — a driver must stay self-contained so it lifts cleanly into its own\n * package at the [Phase 9 split]. At that split the shared cold-key\n * scheme is promoted into `@cloudbitmaps/core`'s driver-kit (imported by every driver package), which is the\n * right home for it; until then a self-contained copy beats a cross-driver import.\n */\nimport { ValidationError } from '@/core/errors';\nimport { validateSegmentRef } from '@/core/validate';\nimport type { GenKey, SegmentRef } from '@/core/ports';\nimport { namespacePart } from '../_shared/keys';\n\nconst SUFFIX = '.crbm';\n\n/** Normalize an optional caller prefix to either `''` or `trimmed/` (no leading/trailing slashes). */\nfunction prefixPart(prefix: string | undefined): string {\n if (prefix === undefined) return '';\n const trimmed = prefix.replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `${trimmed}/`;\n}\n\n/**\n * Validate the caller-supplied key prefix (trusted config, but a real containment boundary): reject control\n * characters and `.`/`..` path segments so a prefix can't traverse out of its intended space. Returns it\n * unchanged (the key builders normalize slashes).\n */\nexport function normalizeGcsPrefix(prefix: string | undefined): string | undefined {\n if (prefix === undefined) return undefined;\n for (const ch of prefix) {\n if (ch.charCodeAt(0) < 0x20) {\n throw new ValidationError('prefix must not contain control characters');\n }\n }\n for (const segment of prefix.split('/')) {\n if (segment === '.' || segment === '..') {\n throw new ValidationError(\n `prefix must not contain \".\" or \"..\" path segments: ${JSON.stringify(prefix)}`,\n );\n }\n }\n return prefix;\n}\n\n/**\n * The GCS object-name prefix shared by all of a segment's generations: `<prefix><ns>/segments/<segment>.`.\n * Used both as the `getFiles` prefix and as the string stripped by {@link parseGenerationFromName}.\n */\nexport function segmentObjectPrefix(prefix: string | undefined, ref: SegmentRef): string {\n validateSegmentRef(ref);\n return `${prefixPart(prefix)}${namespacePart(ref.namespace)}/segments/${ref.segment}.`;\n}\n\n/** The full GCS object name of one `.crbm` generation: `<segmentPrefix><gen>.crbm`. */\nexport function coldObjectName(prefix: string | undefined, key: GenKey): string {\n if (!Number.isInteger(key.generation) || key.generation < 0) {\n throw new ValidationError(`generation must be a non-negative integer; got ${key.generation}`);\n }\n return `${segmentObjectPrefix(prefix, key)}${key.generation}${SUFFIX}`;\n}\n\n/**\n * Parse a generation number out of a full object name, given its segment prefix, or `null` if it doesn't\n * match. Canonical decimal only — no leading zeros (so `…s.07.crbm` can't alias `…s.7.crbm`) and within\n * safe-integer range. Also rejects a *different* segment whose name merely shares the prefix (its middle\n * isn't all digits).\n */\nexport function parseGenerationFromName(segmentPrefix: string, objectName: string): number | null {\n if (!objectName.startsWith(segmentPrefix) || !objectName.endsWith(SUFFIX)) return null;\n const middle = objectName.slice(segmentPrefix.length, objectName.length - SUFFIX.length);\n if (!/^(0|[1-9]\\d*)$/.test(middle)) return null;\n const generation = Number(middle);\n return Number.isSafeInteger(generation) ? generation : null;\n}\n","/**\n * Pure helpers for classifying Google Cloud Storage SDK errors (Phase 7; transient class mirrors S3's).\n *\n * SDK-free + side-effect-free — they only read structural shapes off the thrown value (`err.code`,\n * `err.response.status`, `err.name`), so the GCS-specific translation is unit-testable without a live GCS or\n * an emulator, and without importing `@google-cloud/storage`. GCS's `ApiError` carries the HTTP status on\n * `.code` (a number); dropped/timed-out sockets surface as a Node error with a string `.code` (`ECONNRESET`,\n * `ETIMEDOUT`, …).\n */\n\n/** The HTTP status of a GCS `ApiError`, if present (`err.code` as a number, or `err.response.status`). */\nfunction httpStatus(err: unknown): number | undefined {\n const e = err as { code?: unknown; response?: { status?: unknown } } | null;\n if (typeof e?.code === 'number') return e.code;\n if (typeof e?.response?.status === 'number') return e.response.status;\n return undefined;\n}\n\n/** A network-level error `code` string (e.g. `ECONNRESET`, `ETIMEDOUT`, `EAI_AGAIN`, `EPIPE`). */\nfunction networkCode(err: unknown): string | undefined {\n const code = (err as { code?: unknown } | null)?.code;\n return typeof code === 'string' ? code : undefined;\n}\n\n/**\n * A conditional `ifGenerationMatch: 0` write lost the write-once race — the object already existed, so GCS\n * returns **412 Precondition Failed**. Maps to `WriteConflictError` (caller OCC), never a blind retry.\n */\nexport function isPreconditionFailed(err: unknown): boolean {\n return httpStatus(err) === 412;\n}\n\n/** The object / generation does not exist (GCS returns 404). */\nexport function isNotFound(err: unknown): boolean {\n return httpStatus(err) === 404;\n}\n\n/** A range request started past EOF (HTTP 416 Requested Range Not Satisfiable). */\nexport function isInvalidRange(err: unknown): boolean {\n return httpStatus(err) === 416;\n}\n\n/**\n * A transient GCS fault that is safe to retry: throttling (429), any 5xx, or a dropped/timed-out socket.\n * Excludes the deterministic, caller-meaningful outcomes (412/404/416) — those must never be reclassified as\n * a blind transient (a retried doomed conditional write would just fail again, and mask an OCC conflict).\n */\nexport function isTransient(err: unknown): boolean {\n if (isPreconditionFailed(err) || isNotFound(err) || isInvalidRange(err)) return false;\n const status = httpStatus(err);\n if (status === 429 || (status !== undefined && status >= 500 && status < 600)) return true;\n const net = networkCode(err);\n return (\n net === 'ECONNRESET' ||\n net === 'ETIMEDOUT' ||\n net === 'ECONNREFUSED' ||\n net === 'EPIPE' ||\n net === 'EAI_AGAIN' ||\n net === 'ENOTFOUND'\n );\n}\n","/**\n * `GcsColdDriver` — an {@link IColdDriver} over Google Cloud Storage (Phase 7).\n *\n * Uses the official `@google-cloud/storage`, an **optional peer dependency** — only consumers of\n * `cloud-roaring/gcs` install it. The `Storage` client is **injected** (dependency injection): the driver owns\n * no credential/project/endpoint logic, so it's thin, reuses the caller's client, and is testable against the\n * `fake-gcs-server` emulator (point a `Storage` at its `apiEndpoint`).\n *\n * Generations are write-once immutable objects: a resumable upload with the **`ifGenerationMatch: 0`**\n * precondition (\"create only if it does not exist\") makes the publish atomic — a second write to the same\n * object fails with 412 → {@link WriteConflictError}, never a silent overwrite (C13), the GCS analogue of S3's\n * `If-None-Match: *` and LocalFs's atomic `link`. **Writes stream in constant memory:** the codec's bytes are\n * piped into a GCS resumable-upload `Writable` (chunked/freed by the SDK as they go), so the daemon's write\n * footprint stays bounded regardless of segment size, up to the advertised `maxObjectBytes`. Drivers may use\n * `node:crypto`; only `core/` is bound by the determinism lint.\n */\nimport { createHash, type Hash } from 'node:crypto';\nimport type { Writable } from 'node:stream';\nimport { once } from 'node:events';\nimport type { Storage } from '@google-cloud/storage';\nimport type { BlobSink } from '@/core/blob';\nimport {\n NotFoundError,\n TransientError,\n ValidationError,\n WriteConflictError,\n isNotFoundError,\n isValidationError,\n isWriteConflictError,\n} from '@/core/errors';\nimport type { ColdCaps, GenKey, IColdDriver, SegmentRef } from '@/core/ports';\nimport {\n coldObjectName,\n normalizeGcsPrefix,\n parseGenerationFromName,\n segmentObjectPrefix,\n} from './keys';\nimport { isInvalidRange, isNotFound, isPreconditionFailed, isTransient } from './gcs-errors';\n\n/** Default object ceiling: GCS's 5 TiB per-object hard max. Set lower to fail fast on a runaway write. */\nconst DEFAULT_MAX_OBJECT_BYTES = 5 * 1024 * 1024 * 1024 * 1024;\n\n/**\n * Objects at/under this size are uploaded in a **single simple (non-resumable) request**; larger ones switch to\n * a **resumable stream** (constant memory). Mirrors S3's single-PUT-vs-multipart split — the simple path buffers\n * only ≤ this many bytes, the resumable path streams. Default 8 MiB (≈ S3's part size), so peak write memory is\n * ~one threshold's worth regardless of object size.\n */\nconst DEFAULT_UPLOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;\n\nexport interface GcsColdDriverOptions {\n /** A constructed `@google-cloud/storage` `Storage` client (point `apiEndpoint` at fake-gcs-server locally). */\n readonly storage: Storage;\n /** Target bucket (must already exist). */\n readonly bucket: string;\n /** Optional object-name prefix under which all objects live (e.g. `cloudroaring/`). */\n readonly prefix?: string;\n /** Largest object this driver will write/advertise (default = GCS's 5 TiB max). */\n readonly maxObjectBytes?: number;\n /** Bytes at/under which a single non-resumable upload is used instead of a resumable stream (default 8 MiB). */\n readonly simpleUploadThresholdBytes?: number;\n}\n\nexport class GcsColdDriver implements IColdDriver {\n private readonly storage: Storage;\n private readonly bucket: string;\n private readonly prefix: string | undefined;\n private readonly maxObjectBytes: number;\n private readonly threshold: number;\n\n constructor(options: GcsColdDriverOptions) {\n this.storage = options.storage;\n this.bucket = options.bucket;\n this.prefix = normalizeGcsPrefix(options.prefix);\n this.maxObjectBytes = options.maxObjectBytes ?? DEFAULT_MAX_OBJECT_BYTES;\n this.threshold = options.simpleUploadThresholdBytes ?? DEFAULT_UPLOAD_THRESHOLD_BYTES;\n }\n\n capabilities(): ColdCaps {\n return { rangeRead: true, maxObjectBytes: this.maxObjectBytes, conditionalPut: true };\n }\n\n private file(name: string) {\n return this.storage.bucket(this.bucket).file(name);\n }\n\n async putImmutable(\n key: GenKey,\n write: (sink: BlobSink) => Promise<void>,\n ): Promise<{ size: number; sha256: string }> {\n const objectName = coldObjectName(this.prefix, key); // validates ref + generation\n const sink = new GcsUploadSink(this.file(objectName), this.maxObjectBytes, this.threshold);\n try {\n await write(sink);\n return await sink.finish();\n } catch (err) {\n await sink.abort(); // best-effort teardown of the in-flight upload\n // A lost write-once race — the object already existed, so `ifGenerationMatch: 0` failed with 412.\n if (isPreconditionFailed(err)) {\n throw new WriteConflictError(\n `generation already exists (write-once): ${key.segment}.${key.generation}`,\n );\n }\n if (isValidationError(err) || isWriteConflictError(err) || isNotFoundError(err)) throw err;\n throw this.mapError(err);\n }\n }\n\n async getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array> {\n if (!Number.isInteger(offset) || !Number.isInteger(length) || offset < 0 || length < 0) {\n throw new ValidationError(`invalid range offset=${offset} length=${length}`);\n }\n if (length === 0) return new Uint8Array(0);\n const objectName = coldObjectName(this.prefix, key);\n try {\n // GCS `end` is inclusive.\n const [buf] = await this.file(objectName).download({\n start: offset,\n end: offset + length - 1,\n });\n // A short read means the range ran past EOF — treat as out-of-bounds, never a partial result.\n if (buf.length !== length) {\n throw new ValidationError(\n `range [${offset}, ${offset + length}) out of bounds (got ${buf.length}B)`,\n );\n }\n return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);\n } catch (err) {\n throw this.mapReadError(err, key);\n }\n }\n\n async getTail(key: GenKey, maxBytes: number): Promise<{ bytes: Uint8Array; size: number }> {\n const objectName = coldObjectName(this.prefix, key);\n try {\n // Two round-trips (metadata for the size, then a ranged download) vs S3's one (suffix-range +\n // Content-Range). This is on the per-*generation* open path, which the reader caches — NOT the per-op hot\n // path (add/has/count/intersect) — so it's amortized; collapsing to one RT is a tracked follow-up.\n const [meta] = await this.file(objectName).getMetadata();\n const size = Number(meta.size ?? 0);\n if (!Number.isSafeInteger(size) || size < 0) {\n throw new ValidationError(`GCS returned an invalid object size: ${String(meta.size)}`);\n }\n if (maxBytes <= 0 || size === 0) return { bytes: new Uint8Array(0), size };\n const start = Math.max(0, size - maxBytes);\n const [buf] = await this.file(objectName).download({ start, end: size - 1 });\n return { bytes: new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), size };\n } catch (err) {\n throw this.mapReadError(err, key);\n }\n }\n\n async delete(key: GenKey): Promise<void> {\n // Idempotent: `ignoreNotFound` so a racing/retried GC sweep of an absent object is a no-op.\n try {\n await this.file(coldObjectName(this.prefix, key)).delete({ ignoreNotFound: true });\n } catch (err) {\n throw this.mapError(err);\n }\n }\n\n async *list(ref: SegmentRef): AsyncIterable<GenKey> {\n const prefix = segmentObjectPrefix(this.prefix, ref); // validates ref\n let files;\n try {\n // autoPaginate (default) drains every page; a segment has few generations, so the set is small.\n [files] = await this.storage.bucket(this.bucket).getFiles({ prefix });\n } catch (err) {\n throw this.mapError(err);\n }\n for (const f of files) {\n const generation = parseGenerationFromName(prefix, f.name);\n if (generation !== null) {\n yield { namespace: ref.namespace, segment: ref.segment, generation };\n }\n }\n }\n\n /** Map GCS read errors to the driver vocabulary; pass everything else through {@link mapError}. */\n private mapReadError(err: unknown, key: GenKey): unknown {\n if (isValidationError(err)) return err;\n if (isNotFound(err))\n return new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);\n // A fully out-of-range request (start past EOF) — the BlobReader contract treats range errors as\n // ValidationError, never a short/empty read.\n if (isInvalidRange(err)) {\n return new ValidationError(`range out of bounds for ${key.segment}.${key.generation}`);\n }\n return this.mapError(err);\n }\n\n /**\n * Reclassify a transient GCS fault (throttle/5xx/dropped connection) as a retryable {@link TransientError}\n * so the retry decorator can ride it out; everything else propagates unchanged. The final fallback at every\n * client-call site, so callers + the decorator only ever see typed errors.\n */\n private mapError(err: unknown): unknown {\n if (isTransient(err)) {\n return new TransientError(\n `transient GCS fault: ${(err as { code?: unknown } | null)?.code ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n }\n}\n\ntype GcsFile = ReturnType<ReturnType<Storage['bucket']>['file']>;\n\n/** Concatenate byte chunks of known total length into one buffer. */\nfunction concatBytes(parts: readonly Uint8Array[], total: number): Uint8Array {\n const out = new Uint8Array(total);\n let offset = 0;\n for (const p of parts) {\n out.set(p, offset);\n offset += p.length;\n }\n return out;\n}\n\n/**\n * {@link BlobSink} that uploads one GCS object write-once. It **buffers up to a threshold**: an object that\n * finishes at/under it is committed in a **single simple (non-resumable) request** (`file.save`) — the write-once\n * path both real GCS and `fake-gcs-server` enforce; a larger one switches to a **resumable stream**, flushing the\n * buffer then piping the rest in constant memory (peak ≈ one threshold). SHA-256 is hashed incrementally. Both\n * paths carry `ifGenerationMatch: 0` (create-only-if-absent) → a conflict is a 412, mapped by the driver to\n * {@link WriteConflictError}. On error the caller invokes {@link abort}.\n */\nclass GcsUploadSink implements BlobSink {\n private readonly hash: Hash = createHash('sha256');\n private total = 0;\n private readonly buffered: Uint8Array[] = [];\n private bufferedLen = 0;\n private stream: Writable | undefined; // set once we cross the threshold → resumable mode\n private done: Promise<unknown[]> | undefined;\n private failure: unknown;\n\n constructor(\n private readonly file: GcsFile,\n private readonly maxObjectBytes: number,\n private readonly threshold: number,\n ) {}\n\n private static readonly WRITE_OPTS = {\n preconditionOpts: { ifGenerationMatch: 0 as const }, // write-once: create only if absent\n metadata: { contentType: 'application/octet-stream' },\n };\n\n async write(bytes: Uint8Array): Promise<void> {\n if (this.failure !== undefined) throw this.failure;\n if (bytes.length === 0) return;\n this.total += bytes.length;\n if (this.total > this.maxObjectBytes) {\n throw new ValidationError(`object exceeds maxObjectBytes ${this.maxObjectBytes}`);\n }\n this.hash.update(bytes);\n if (this.stream !== undefined) {\n // Resumable mode: honor backpressure. `events.once(stream,'drain')` REJECTS if the stream emits 'error'\n // while we wait (that's its documented behavior for any awaited event except 'error'), and auto-removes\n // both listeners on settle — so there's no listener accumulation across drain cycles.\n if (!this.stream.write(bytes)) await once(this.stream, 'drain');\n return;\n }\n this.buffered.push(bytes);\n this.bufferedLen += bytes.length;\n if (this.bufferedLen > this.threshold) this.startResumable();\n }\n\n /** Cross into resumable streaming: open the stream, flush the buffered bytes, keep only ~one threshold resident. */\n private startResumable(): void {\n const stream = this.file.createWriteStream({ resumable: true, ...GcsUploadSink.WRITE_OPTS });\n this.stream = stream;\n this.done = once(stream, 'finish'); // resolves on a clean commit; rejects on 'error' (e.g. 412)\n stream.on('error', (e: unknown) => {\n this.failure ??= e; // a persistent listener so a stray 'error' is never an unhandled 'error' event\n });\n this.done.catch(() => undefined); // finish() awaits this; guard against unhandled rejection until then\n const buf = concatBytes(this.buffered, this.bufferedLen);\n this.buffered.length = 0;\n this.bufferedLen = 0;\n stream.write(buf); // ≤ threshold+one-chunk; backpressure is absorbed by the next write()'s drain-await / end()\n }\n\n async finish(): Promise<{ size: number; sha256: string }> {\n if (this.failure !== undefined) throw this.failure;\n const sha256 = this.hash.digest('hex');\n if (this.stream === undefined) {\n // Small object: a single simple (non-resumable) upload — write-once enforced everywhere (incl. the emulator).\n await this.file.save(concatBytes(this.buffered, this.bufferedLen), {\n resumable: false,\n ...GcsUploadSink.WRITE_OPTS,\n });\n return { size: this.total, sha256 };\n }\n this.stream.end();\n await this.done; // rejects if the commit fails (e.g. 412 write-once conflict)\n return { size: this.total, sha256 };\n }\n\n async abort(): Promise<void> {\n if (this.stream !== undefined && !this.stream.destroyed) this.stream.destroy();\n }\n}\n"]}