@cloudbitmaps/gcs 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/backend.d.ts +4 -1
- package/dist/download-retry.d.ts +2 -0
- package/dist/gcs-errors.d.ts +10 -1
- package/dist/index.js +236 -38
- package/dist/index.js.map +3 -3
- package/dist/read-once.d.ts +37 -0
- package/dist/registry.d.ts +5 -4
- package/dist/storage.d.ts +20 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ second service to run.
|
|
|
14
14
|
pnpm add @cloudbitmaps/roaring @cloudbitmaps/gcs
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
On pnpm 10 and later, allow
|
|
17
|
+
On npm 12 and pnpm 10 and later, allow `roaring`'s one install script first, or the install exits 0 and the package throws at `import`. Put this in your `package.json`: `{ "allowScripts": { "roaring": true }, "pnpm": { "onlyBuiltDependencies": ["roaring"] } }`. npm 11 runs the script but warns until you allow it the same way; pnpm 9 needs nothing extra. [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
|
|
18
18
|
|
|
19
19
|
`@google-cloud/storage` (`^7 || ^8`) is a real dependency of this package, so installing it is the whole step.
|
|
20
20
|
`@cloudbitmaps/core`, the engine underneath, arrives with it and you never name it.
|
|
@@ -39,7 +39,7 @@ It builds its own client from Application Default Credentials. Any other key is
|
|
|
39
39
|
|---|---|
|
|
40
40
|
| `bucket` (required) | the bucket |
|
|
41
41
|
| `prefix` | a key prefix for everything this store writes |
|
|
42
|
-
| `client` | your own `Storage` client. Pass it as `client`; `storage` is refused, since in `CloudRoaring` that word means the backend |
|
|
42
|
+
| `client` | your own `Storage` client. Pass it as `client`; `storage` is refused, since in `CloudRoaring` that word means the backend. Build it with `retryOptions: { autoRetry: false }` (see below) |
|
|
43
43
|
| `projectId`, `apiEndpoint` | build a client for you, such as one for fake-gcs-server; refused beside `client` |
|
|
44
44
|
| `simpleUploadThresholdBytes`, `maxObjectBytes` | the size up to which an object is one simple request (default 8 MiB) and the largest object (default 5 TiB, GCS's maximum) |
|
|
45
45
|
|
|
@@ -51,10 +51,21 @@ It builds its own client from Application Default Credentials. Any other key is
|
|
|
51
51
|
`roles/storage.objectAdmin` covers all four.
|
|
52
52
|
- **The write-once guarantee rides GCS preconditions** (`ifGenerationMatch`), so nothing else needs enabling.
|
|
53
53
|
- **Never add a lifecycle rule that expires current objects or the `registry/` prefix.**
|
|
54
|
+
- **The SDK does not retry downloads; the driver does.** In `@google-cloud/storage` 7.x and 8.x (checked on 7.22.0 and
|
|
55
|
+
8.1.0), a download the SDK retries after any status it retries (408, 429, 500, 502, 503 or 504) can crash the process with
|
|
56
|
+
`ERR_STREAM_UNABLE_TO_PIPE`, thrown outside any promise, even though the retried request succeeded. The client
|
|
57
|
+
`GcsStorage` builds therefore sends each download once, and the driver runs a download again itself, up to three
|
|
58
|
+
more times with backoff, after a connection fault (refused, reset, timed out, a DNS failure, a body cut off) or a 408, 429, 500, 502, 503 or 504, and after nothing else (not a missing credentials file or a TLS failure). That holds for every
|
|
59
|
+
caller, including a store built with `retry: false`. The client's other requests (uploads, listings, metadata reads)
|
|
60
|
+
keep the SDK's retries. **A client you pass as `client` is used as it is, so build it with
|
|
61
|
+
`retryOptions: { autoRetry: false }`.** That also turns off the SDK's retries of listings, metadata reads and
|
|
62
|
+
resumable uploads on that client, which the library does not retry; the client `GcsStorage` builds keeps them.
|
|
63
|
+
- **A client `timeout` does not bound a download on `@google-cloud/storage` 8.x**, so a read whose server stalls waits
|
|
64
|
+
for it. Measured against a local server that never answers: still pending after 12 s with `timeout: 2000`.
|
|
54
65
|
- **A transient failure of a write throws `TransientError`, and the write may or may not have landed.** Re-run the
|
|
55
66
|
call, or check `store.generations(ref)`.
|
|
56
67
|
|
|
57
|
-
The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item, and the ones every backend shares:
|
|
68
|
+
The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item, and the ones every backend shares: backups of the data and the registry, and a schedule for your loads.
|
|
58
69
|
|
|
59
70
|
## Documentation
|
|
60
71
|
|
package/dist/backend.d.ts
CHANGED
|
@@ -19,7 +19,10 @@ export interface GcsStorageOptions {
|
|
|
19
19
|
/**
|
|
20
20
|
* A constructed `@google-cloud/storage` client. One is built from the ambient credentials when absent. Its retry
|
|
21
21
|
* options apply to every request except the single-request conditional writes, which are sent once whatever they
|
|
22
|
-
* say.
|
|
22
|
+
* say. In `@google-cloud/storage` 7.x and 8.x a download the SDK retries after a 408, 429, 500, 502, 503 or 504 can crash the process
|
|
23
|
+
* with `ERR_STREAM_UNABLE_TO_PIPE`, so build it with `retryOptions: { autoRetry: false }`; the driver retries
|
|
24
|
+
* downloads itself. That also turns off the SDK's retries of listings, metadata reads and resumable uploads on
|
|
25
|
+
* this client, which the store does not retry. The client built here needs none of this: only its downloads are sent once.
|
|
23
26
|
*/
|
|
24
27
|
readonly client?: GcsClient;
|
|
25
28
|
/** Project id for the client built when `client` is absent (refused beside `client`). Falls back to the SDK's own resolution. */
|
package/dist/gcs-errors.d.ts
CHANGED
|
@@ -16,8 +16,17 @@ export declare function isPreconditionFailed(err: unknown): boolean;
|
|
|
16
16
|
export declare function isNotFound(err: unknown): boolean;
|
|
17
17
|
/** A range request started past EOF (HTTP 416 Requested Range Not Satisfiable). */
|
|
18
18
|
export declare function isInvalidRange(err: unknown): boolean;
|
|
19
|
+
/** A download that failed in transit rather than with an HTTP status: one of {@link TRANSPORT_CODES}. */
|
|
20
|
+
export declare function isTransportFault(err: unknown): boolean;
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
22
|
+
* Whether the driver retries a failed download, which it does in the SDK's place: the statuses the SDK's predicate
|
|
23
|
+
* retries (408, 429, 500, 502, 503 and 504, as a number or a string `code`), the connection faults it names in any
|
|
24
|
+
* `errors[].reason`, and every {@link isTransportFault}, which the SDK retried before any response came. A 404, 412,
|
|
25
|
+
* 416 or 403, a 501 or 505, a credentials or TLS failure, and every other answer reach the caller on the first attempt.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isDownloadRetryable(err: unknown): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* A transient GCS fault that is safe to retry: throttling (429), a request timeout (408), any 5xx, or a dropped/timed-out socket.
|
|
21
30
|
* Excludes the deterministic, caller-meaningful outcomes (412/404/416) — those must never be reclassified as
|
|
22
31
|
* a blind transient (a retried doomed conditional write would just fail again, and mask an OCC conflict).
|
|
23
32
|
*/
|
package/dist/index.js
CHANGED
|
@@ -78,14 +78,69 @@ function isNotFound(err) {
|
|
|
78
78
|
function isInvalidRange(err) {
|
|
79
79
|
return httpStatus(err) === 416;
|
|
80
80
|
}
|
|
81
|
+
var SDK_RETRIED_STATUSES = [408, 429, 500, 502, 503, 504];
|
|
82
|
+
function isSdkConnectionProblem(reason) {
|
|
83
|
+
return reason.includes("eai_again") || reason === "econnreset" || reason === "unexpected connection closure" || reason === "epipe" || reason === "socket connection timeout";
|
|
84
|
+
}
|
|
85
|
+
var TRANSPORT_CODES = /* @__PURE__ */ new Set([
|
|
86
|
+
"ECONNRESET",
|
|
87
|
+
"ECONNREFUSED",
|
|
88
|
+
"ECONNABORTED",
|
|
89
|
+
"ETIMEDOUT",
|
|
90
|
+
"ENOTFOUND",
|
|
91
|
+
"EAI_AGAIN",
|
|
92
|
+
"EHOSTUNREACH",
|
|
93
|
+
"ENETUNREACH",
|
|
94
|
+
"EPIPE",
|
|
95
|
+
"ERR_STREAM_PREMATURE_CLOSE"
|
|
96
|
+
]);
|
|
97
|
+
function isTransportFault(err) {
|
|
98
|
+
if (httpStatus(err) !== void 0) return false;
|
|
99
|
+
const code = networkCode(err);
|
|
100
|
+
return code !== void 0 && TRANSPORT_CODES.has(code);
|
|
101
|
+
}
|
|
102
|
+
function isDownloadRetryable(err) {
|
|
103
|
+
if (err === null || typeof err !== "object") return false;
|
|
104
|
+
if (isTransportFault(err)) return true;
|
|
105
|
+
const e = err;
|
|
106
|
+
if (typeof e.code === "number" && SDK_RETRIED_STATUSES.includes(e.code)) return true;
|
|
107
|
+
if (typeof e.code === "string") {
|
|
108
|
+
if (SDK_RETRIED_STATUSES.map(String).includes(e.code)) return true;
|
|
109
|
+
if (isSdkConnectionProblem(e.code.toLowerCase())) return true;
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(e.errors)) {
|
|
112
|
+
for (const inner of e.errors) {
|
|
113
|
+
const reason = inner?.reason?.toString().toLowerCase();
|
|
114
|
+
if (reason !== void 0 && isSdkConnectionProblem(reason)) return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
81
119
|
function isTransient(err) {
|
|
82
120
|
if (isPreconditionFailed(err) || isNotFound(err) || isInvalidRange(err)) return false;
|
|
83
121
|
const status = httpStatus(err);
|
|
84
|
-
if (status === 429 || status !== void 0 && status >= 500 && status < 600)
|
|
122
|
+
if (status === 408 || status === 429 || status !== void 0 && status >= 500 && status < 600)
|
|
123
|
+
return true;
|
|
85
124
|
const net = networkCode(err);
|
|
86
125
|
return net === "ECONNRESET" || net === "ETIMEDOUT" || net === "ECONNREFUSED" || net === "EPIPE" || net === "EAI_AGAIN" || net === "ENOTFOUND";
|
|
87
126
|
}
|
|
88
127
|
|
|
128
|
+
// src/download-retry.ts
|
|
129
|
+
var RETRIES = 3;
|
|
130
|
+
var BASE_DELAY_MS = 100;
|
|
131
|
+
var MAX_DELAY_MS = 1e3;
|
|
132
|
+
async function retryDownload(download) {
|
|
133
|
+
for (let attempt = 0; ; attempt++) {
|
|
134
|
+
try {
|
|
135
|
+
return await download();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (attempt >= RETRIES || !isDownloadRetryable(err)) throw err;
|
|
138
|
+
const ceiling = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * ceiling));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
89
144
|
// src/send-once.ts
|
|
90
145
|
import { once } from "node:events";
|
|
91
146
|
async function saveOnce(file, body, options) {
|
|
@@ -95,18 +150,71 @@ async function saveOnce(file, body, options) {
|
|
|
95
150
|
await stored;
|
|
96
151
|
}
|
|
97
152
|
|
|
153
|
+
// src/read-once.ts
|
|
154
|
+
function singleHeader(headers, name) {
|
|
155
|
+
const value = headers[name];
|
|
156
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
157
|
+
}
|
|
158
|
+
function readOnce(file, options, maxBytes, oversize) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const stream = file.createReadStream(options);
|
|
161
|
+
const chunks = [];
|
|
162
|
+
let total = 0;
|
|
163
|
+
let status = 0;
|
|
164
|
+
let headers = {};
|
|
165
|
+
let settled = false;
|
|
166
|
+
const fail = (err) => {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
setImmediate(() => stream.destroy());
|
|
170
|
+
reject(err);
|
|
171
|
+
};
|
|
172
|
+
stream.on("response", (res) => {
|
|
173
|
+
status = typeof res.statusCode === "number" ? res.statusCode : 0;
|
|
174
|
+
headers = res.headers ?? {};
|
|
175
|
+
if (status < 200 || status > 299) return;
|
|
176
|
+
const advertised = singleHeader(headers, "content-length");
|
|
177
|
+
if (advertised !== void 0 && /^\d+$/.test(advertised) && Number(advertised) > maxBytes) {
|
|
178
|
+
fail(oversize(Number(advertised)));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
stream.on("data", (chunk) => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
total += chunk.length;
|
|
184
|
+
if (total > maxBytes) return fail(oversize(void 0));
|
|
185
|
+
chunks.push(chunk);
|
|
186
|
+
});
|
|
187
|
+
stream.on("error", fail);
|
|
188
|
+
stream.on("end", () => {
|
|
189
|
+
if (settled) return;
|
|
190
|
+
settled = true;
|
|
191
|
+
resolve({ status, headers, bytes: new Uint8Array(Buffer.concat(chunks, total)) });
|
|
192
|
+
});
|
|
193
|
+
stream.on(
|
|
194
|
+
"close",
|
|
195
|
+
() => fail(
|
|
196
|
+
Object.assign(new Error("GCS read ended before the response completed"), {
|
|
197
|
+
code: "ECONNRESET"
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
98
204
|
// src/storage.ts
|
|
99
205
|
var DEFAULT_MAX_OBJECT_BYTES = 5 * 1024 * 1024 * 1024 * 1024;
|
|
100
206
|
var WRITE_ID_KEY = "cbwid";
|
|
101
207
|
var DEFAULT_UPLOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
|
|
102
208
|
var GcsStorageDriver = class {
|
|
103
209
|
storage;
|
|
210
|
+
readStorage;
|
|
104
211
|
bucket;
|
|
105
212
|
prefix;
|
|
106
213
|
maxObjectBytes;
|
|
107
214
|
threshold;
|
|
108
215
|
constructor(options) {
|
|
109
216
|
this.storage = options.storage;
|
|
217
|
+
this.readStorage = options.readStorage ?? options.storage;
|
|
110
218
|
this.bucket = options.bucket;
|
|
111
219
|
this.prefix = normalizeGcsPrefix(options.prefix);
|
|
112
220
|
for (const [name, value] of [
|
|
@@ -126,6 +234,10 @@ var GcsStorageDriver = class {
|
|
|
126
234
|
file(name) {
|
|
127
235
|
return this.storage.bucket(this.bucket).file(name);
|
|
128
236
|
}
|
|
237
|
+
/** The same object on the download client. */
|
|
238
|
+
downloadable(name) {
|
|
239
|
+
return this.readStorage.bucket(this.bucket).file(name);
|
|
240
|
+
}
|
|
129
241
|
async putImmutable(key, write) {
|
|
130
242
|
const objectName = storageObjectName(this.prefix, key);
|
|
131
243
|
const sink = new GcsUploadSink(this.file(objectName), this.maxObjectBytes, this.threshold);
|
|
@@ -150,10 +262,9 @@ var GcsStorageDriver = class {
|
|
|
150
262
|
if (length === 0) return new Uint8Array(0);
|
|
151
263
|
const objectName = storageObjectName(this.prefix, key);
|
|
152
264
|
try {
|
|
153
|
-
const [buf] = await
|
|
154
|
-
start: offset,
|
|
155
|
-
|
|
156
|
-
});
|
|
265
|
+
const [buf] = await retryDownload(
|
|
266
|
+
() => this.downloadable(objectName).download({ start: offset, end: offset + length - 1 })
|
|
267
|
+
);
|
|
157
268
|
if (buf.length !== length) {
|
|
158
269
|
throw new ValidationError2(
|
|
159
270
|
`range [${offset}, ${offset + length}) out of bounds (got ${buf.length}B)`
|
|
@@ -167,19 +278,76 @@ var GcsStorageDriver = class {
|
|
|
167
278
|
async getTail(key, maxBytes) {
|
|
168
279
|
const objectName = storageObjectName(this.prefix, key);
|
|
169
280
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
281
|
+
if (maxBytes <= 0) return { bytes: new Uint8Array(0), size: await this.sizeOf(objectName) };
|
|
282
|
+
if (!Number.isSafeInteger(maxBytes)) {
|
|
283
|
+
throw new ValidationError2(`invalid tail length ${maxBytes}`);
|
|
284
|
+
}
|
|
285
|
+
let res;
|
|
286
|
+
try {
|
|
287
|
+
res = await retryDownload(
|
|
288
|
+
() => readOnce(
|
|
289
|
+
this.downloadable(objectName),
|
|
290
|
+
{ end: -maxBytes, decompress: false },
|
|
291
|
+
maxBytes,
|
|
292
|
+
() => this.badTail(key, `the response is longer than the ${maxBytes}B requested`)
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (!isInvalidRange(err)) throw err;
|
|
297
|
+
const size = await this.sizeOf(objectName);
|
|
298
|
+
if (size !== 0) throw err;
|
|
299
|
+
return { bytes: new Uint8Array(0), size };
|
|
300
|
+
}
|
|
301
|
+
if (res.bytes.length === 0) {
|
|
302
|
+
const size = await this.sizeOf(objectName);
|
|
303
|
+
if (size !== 0) throw this.badTail(key, `no bytes returned for an object of ${size}B`);
|
|
304
|
+
return { bytes: res.bytes, size };
|
|
174
305
|
}
|
|
175
|
-
|
|
176
|
-
const start = Math.max(0, size - maxBytes);
|
|
177
|
-
const [buf] = await this.file(objectName).download({ start, end: size - 1 });
|
|
178
|
-
return { bytes: new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), size };
|
|
306
|
+
return { bytes: res.bytes, size: this.tailSize(res, maxBytes, key) };
|
|
179
307
|
} catch (err) {
|
|
180
308
|
throw this.mapReadError(err, key);
|
|
181
309
|
}
|
|
182
310
|
}
|
|
311
|
+
/** The object's size from its metadata, validated. */
|
|
312
|
+
async sizeOf(objectName) {
|
|
313
|
+
const [meta] = await this.file(objectName).getMetadata();
|
|
314
|
+
const size = Number(meta.size ?? 0);
|
|
315
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
316
|
+
throw new ValidationError2(`GCS returned an invalid object size: ${String(meta.size)}`);
|
|
317
|
+
}
|
|
318
|
+
return size;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The object's total size from a tail response, refused unless every header agrees with the bytes received. A 206
|
|
322
|
+
* must carry `Content-Range: bytes a-b/total` that names exactly the suffix asked for and exactly the bytes held;
|
|
323
|
+
* a 200 (a server that ignored the range) is the whole object, and is accepted only when it fits in the request.
|
|
324
|
+
*/
|
|
325
|
+
tailSize(res, maxBytes, key) {
|
|
326
|
+
const received = res.bytes.length;
|
|
327
|
+
if (res.status === 200) {
|
|
328
|
+
if (received > maxBytes)
|
|
329
|
+
throw this.badTail(key, "whole object returned past the requested length");
|
|
330
|
+
return received;
|
|
331
|
+
}
|
|
332
|
+
if (res.status !== 206) throw this.badTail(key, `unexpected HTTP ${res.status}`);
|
|
333
|
+
const header = singleHeader(res.headers, "content-range");
|
|
334
|
+
const m = header === void 0 ? null : /^bytes (\d+)-(\d+)\/(\d+)$/.exec(header);
|
|
335
|
+
if (m === null) throw this.badTail(key, `Content-Range is missing or malformed`);
|
|
336
|
+
const [first, last, total] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
337
|
+
if (![first, last, total].every(Number.isSafeInteger)) {
|
|
338
|
+
throw this.badTail(key, "Content-Range holds a number that is not a safe integer");
|
|
339
|
+
}
|
|
340
|
+
if (last !== total - 1 || first !== Math.max(0, total - maxBytes) || last - first + 1 !== received) {
|
|
341
|
+
throw this.badTail(
|
|
342
|
+
key,
|
|
343
|
+
`Content-Range ${first}-${last}/${total} disagrees with ${received}B received for a ${maxBytes}B tail`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return total;
|
|
347
|
+
}
|
|
348
|
+
badTail(key, why) {
|
|
349
|
+
return new ValidationError2(`GCS tail read of ${key.segment}.${key.generation} refused: ${why}`);
|
|
350
|
+
}
|
|
183
351
|
async delete(key) {
|
|
184
352
|
try {
|
|
185
353
|
await this.file(storageObjectName(this.prefix, key)).delete({ ignoreNotFound: true });
|
|
@@ -210,13 +378,21 @@ var GcsStorageDriver = class {
|
|
|
210
378
|
if (isInvalidRange(err)) {
|
|
211
379
|
return new ValidationError2(`range out of bounds for ${key.segment}.${key.generation}`);
|
|
212
380
|
}
|
|
381
|
+
if (isTransportFault(err)) {
|
|
382
|
+
return new TransientError(
|
|
383
|
+
`transient GCS fault: ${String(err.code)}`,
|
|
384
|
+
{
|
|
385
|
+
cause: err
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
}
|
|
213
389
|
return this.mapError(err);
|
|
214
390
|
}
|
|
215
391
|
/**
|
|
216
392
|
* Reclassify a transient GCS fault (throttle/5xx/dropped connection) as a retryable {@link TransientError},
|
|
217
393
|
* so the store's read retry can ride it out and a write's caller can tell it from a deterministic failure;
|
|
218
|
-
* everything else propagates unchanged. The final fallback at every client-call site
|
|
219
|
-
*
|
|
394
|
+
* everything else propagates unchanged. The final fallback at every client-call site: an HTTP status and a fault
|
|
395
|
+
* in transit arrive typed, and anything else (a checksum mismatch, a credentials failure) as the SDK raised it.
|
|
220
396
|
*/
|
|
221
397
|
mapError(err) {
|
|
222
398
|
if (isTransient(err)) {
|
|
@@ -340,44 +516,53 @@ import {
|
|
|
340
516
|
IntegrityError,
|
|
341
517
|
MAX_ROW_BYTES,
|
|
342
518
|
ObjectStoreRegistry,
|
|
343
|
-
ObjectVersionRaced,
|
|
344
519
|
TransientError as TransientError2,
|
|
345
520
|
WriteConflictError as WriteConflictError2,
|
|
346
521
|
normalizeObjectPrefix
|
|
347
522
|
} from "@cloudbitmaps/core/driver-kit";
|
|
348
523
|
var GcsStore = class {
|
|
349
|
-
constructor(storage, bucket) {
|
|
524
|
+
constructor(storage, readStorage, bucket) {
|
|
350
525
|
this.storage = storage;
|
|
526
|
+
this.readStorage = readStorage;
|
|
351
527
|
this.bucket = bucket;
|
|
352
528
|
}
|
|
353
529
|
storage;
|
|
530
|
+
readStorage;
|
|
354
531
|
bucket;
|
|
355
532
|
label = "GCS";
|
|
356
|
-
/** A
|
|
357
|
-
file(name
|
|
358
|
-
return
|
|
533
|
+
/** A handle on one registry object. */
|
|
534
|
+
file(name) {
|
|
535
|
+
return this.storage.bucket(this.bucket).file(name);
|
|
536
|
+
}
|
|
537
|
+
/** The same file handle on the download client. */
|
|
538
|
+
downloadable(name) {
|
|
539
|
+
return this.readStorage.bucket(this.bucket).file(name);
|
|
359
540
|
}
|
|
360
541
|
async read(key) {
|
|
361
|
-
let
|
|
362
|
-
let size;
|
|
542
|
+
let res;
|
|
363
543
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
544
|
+
res = await retryDownload(
|
|
545
|
+
() => readOnce(
|
|
546
|
+
this.downloadable(key),
|
|
547
|
+
{},
|
|
548
|
+
MAX_ROW_BYTES,
|
|
549
|
+
(size) => new IntegrityError(
|
|
550
|
+
size === void 0 ? `registry object exceeds cap ${MAX_ROW_BYTES}B` : `registry object ${size}B exceeds cap ${MAX_ROW_BYTES}B`
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
);
|
|
367
554
|
} catch (err) {
|
|
368
555
|
if (isNotFound(err)) return null;
|
|
369
|
-
throw
|
|
556
|
+
throw mapReadError(err);
|
|
370
557
|
}
|
|
371
|
-
if (
|
|
372
|
-
throw new IntegrityError(`registry
|
|
558
|
+
if (res.status !== 200) {
|
|
559
|
+
throw new IntegrityError(`registry read answered HTTP ${res.status}, not 200: ${key}`);
|
|
373
560
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
} catch (err) {
|
|
378
|
-
if (isNotFound(err)) throw new ObjectVersionRaced(key);
|
|
379
|
-
throw mapError(err);
|
|
561
|
+
const generation = singleHeader(res.headers, "x-goog-generation");
|
|
562
|
+
if (generation === void 0 || !/^[1-9]\d*$/.test(generation) || !Number.isSafeInteger(Number(generation))) {
|
|
563
|
+
throw new IntegrityError(`registry read carries no usable x-goog-generation: ${key}`);
|
|
380
564
|
}
|
|
565
|
+
return { bytes: res.bytes, version: generation };
|
|
381
566
|
}
|
|
382
567
|
async write(key, body, expect) {
|
|
383
568
|
try {
|
|
@@ -416,10 +601,18 @@ function generationFence(version, key) {
|
|
|
416
601
|
}
|
|
417
602
|
return generation;
|
|
418
603
|
}
|
|
604
|
+
function mapReadError(err) {
|
|
605
|
+
if (isTransportFault(err)) {
|
|
606
|
+
return new TransientError2(`transient GCS fault: ${String(err.code)}`, {
|
|
607
|
+
cause: err
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return mapError(err);
|
|
611
|
+
}
|
|
419
612
|
function mapError(err) {
|
|
420
613
|
if (isTransient(err)) {
|
|
421
614
|
return new TransientError2(
|
|
422
|
-
`transient GCS fault: ${err?.name ?? "unknown"}`,
|
|
615
|
+
`transient GCS fault: ${err?.code ?? err?.name ?? "unknown"}`,
|
|
423
616
|
{ cause: err }
|
|
424
617
|
);
|
|
425
618
|
}
|
|
@@ -428,7 +621,7 @@ function mapError(err) {
|
|
|
428
621
|
var GcsRegistryDriver = class extends ObjectStoreRegistry {
|
|
429
622
|
constructor(options) {
|
|
430
623
|
super(
|
|
431
|
-
new GcsStore(options.storage, options.bucket),
|
|
624
|
+
new GcsStore(options.storage, options.readStorage ?? options.storage, options.bucket),
|
|
432
625
|
normalizeObjectPrefix(options.prefix),
|
|
433
626
|
options.now ?? (() => Date.now())
|
|
434
627
|
);
|
|
@@ -467,6 +660,7 @@ var GcsStorage = class {
|
|
|
467
660
|
/** The client both halves share — built here unless one was supplied. */
|
|
468
661
|
client;
|
|
469
662
|
constructor(options) {
|
|
663
|
+
let readClient;
|
|
470
664
|
refuseUnknown(
|
|
471
665
|
"GcsStorage",
|
|
472
666
|
options,
|
|
@@ -481,14 +675,18 @@ var GcsStorage = class {
|
|
|
481
675
|
);
|
|
482
676
|
}
|
|
483
677
|
this.client = options.client;
|
|
678
|
+
readClient = options.client;
|
|
484
679
|
} else {
|
|
485
|
-
|
|
680
|
+
const settings = {
|
|
486
681
|
...options.projectId === void 0 ? {} : { projectId: options.projectId },
|
|
487
682
|
...options.apiEndpoint === void 0 ? {} : { apiEndpoint: options.apiEndpoint }
|
|
488
|
-
}
|
|
683
|
+
};
|
|
684
|
+
this.client = new GcsClient(settings);
|
|
685
|
+
readClient = new GcsClient({ ...settings, retryOptions: { autoRetry: false } });
|
|
489
686
|
}
|
|
490
687
|
const shared = {
|
|
491
688
|
storage: this.client,
|
|
689
|
+
readStorage: readClient,
|
|
492
690
|
bucket: options.bucket,
|
|
493
691
|
...options.prefix === void 0 ? {} : { prefix: options.prefix }
|
|
494
692
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/backend.ts", "../src/storage.ts", "../src/keys.ts", "../src/gcs-errors.ts", "../src/send-once.ts", "../src/registry.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `GcsStorage` \u2014 the Google Cloud Storage backend as one object: generations and pointer, one bucket, stated once.\n *\n * See {@link StorageBackend} for why the two halves are configured together rather than separately.\n *\n * **A note on the word.** `@google-cloud/storage` names its own client class `Storage`, so the driver option\n * that takes it is also called `storage` \u2014 which, since this library calls its durable tier storage too, reads\n * as the same word meaning two things. That is why this class builds the client itself: `new GcsStorage({ bucket })`\n * never makes you write `{ storage: storage }`. Supply `client` when you need your own.\n */\nimport { STORAGE_BACKEND, ValidationError, brandAsBackend } from '@cloudbitmaps/core/driver-kit';\nimport type {\n IRegistryDriver,\n IStorageDriver,\n StorageBackend,\n} from '@cloudbitmaps/core/driver-kit';\nimport { Storage as GcsClient } from '@google-cloud/storage';\nimport { GcsStorageDriver } from './storage';\nimport { GcsRegistryDriver } from './registry';\n\nexport interface GcsStorageOptions {\n /** Target bucket (must already exist). */\n readonly bucket: string;\n /** Optional object-name prefix under which everything lives \u2014 generations and the registry alike. */\n readonly prefix?: string;\n /**\n * A constructed `@google-cloud/storage` client. One is built from the ambient credentials when absent. Its retry\n * options apply to every request except the single-request conditional writes, which are sent once whatever they\n * say.\n */\n readonly client?: GcsClient;\n /** Project id for the client built when `client` is absent (refused beside `client`). Falls back to the SDK's own resolution. */\n readonly projectId?: string;\n /** Endpoint override \u2014 point it at fake-gcs-server locally. Refused beside `client`, which carries its own. */\n readonly apiEndpoint?: string;\n /** Largest object the backend will write and advertise (default = GCS's 5 TiB max). Must be a positive safe integer. */\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 * Must be a positive safe integer. */\n readonly simpleUploadThresholdBytes?: number;\n /** Injected clock for the registry's `createdAt`/`updatedAt`; defaults to `Date.now`. */\n readonly now?: () => number;\n}\n\n/**\n * The keys `new GcsStorage(options)` takes. Any other is refused by name rather than ignored: an ignored client key\n * \u2014 `storage`, as the lower-level storage driver calls it \u2014 falls back to ambient credentials and the **public** endpoint, and\n * for a client pointed at an emulator that is production traffic from a wiring typo.\n */\nexport const GCS_STORAGE_OPTION_KEYS = [\n 'bucket',\n 'prefix',\n 'client',\n 'projectId',\n 'apiEndpoint',\n 'maxObjectBytes',\n 'simpleUploadThresholdBytes',\n 'now',\n] as const;\n\n/** The settings that build a client, which a supplied `client` already carries and so cannot be given beside. */\nconst CLIENT_SETTINGS = ['projectId', 'apiEndpoint'] as const;\n\n/** Refuse an options bag that is not an object, or that holds a key not in `keys`, naming each such key. */\nfunction refuseUnknown(\n name: string,\n options: unknown,\n keys: readonly string[],\n hint: string,\n): void {\n if (options === null || typeof options !== 'object') {\n throw new ValidationError(\n `${name} needs an options object \u2014 got ${options === null ? 'null' : typeof options}`,\n );\n }\n const unknown = Object.keys(options).filter((k) => !keys.includes(k));\n if (unknown.length > 0) {\n const list = (ks: readonly string[]): string => ks.map((k) => `\\`${k}\\``).join(', ');\n throw new ValidationError(\n `${name} does not take ${list(unknown)}. It takes ${list(keys)}; ${hint}.`,\n );\n }\n}\n\nexport class GcsStorage implements StorageBackend {\n /** Cross-bundle brand, stamped non-enumerably in the constructor so a spread cannot carry it. */\n declare readonly [STORAGE_BACKEND]: true;\n readonly storage: IStorageDriver;\n readonly registry: IRegistryDriver;\n /** The client both halves share \u2014 built here unless one was supplied. */\n readonly client: GcsClient;\n\n constructor(options: GcsStorageOptions) {\n refuseUnknown(\n 'GcsStorage',\n options,\n GCS_STORAGE_OPTION_KEYS,\n 'a @google-cloud/storage client goes in `client`',\n );\n if (options.client !== undefined && options.client !== null) {\n // A supplied client already carries its project and endpoint, so a setting beside it is ignored, and\n // ignoring it leaves the store talking to somewhere the caller did not mean: an `apiEndpoint` meant for an\n // emulator, silently dropped, is production traffic from a client that was built for the public endpoint.\n // Refuse instead of picking one.\n const ignored = CLIENT_SETTINGS.filter((k) => options[k] !== undefined);\n if (ignored.length > 0) {\n throw new ValidationError(\n `GcsStorage takes \\`client\\` OR ${CLIENT_SETTINGS.map((k) => `\\`${k}\\``).join(' / ')}, not both \u2014 ` +\n `got \\`client\\` with ${ignored.map((k) => `\\`${k}\\``).join(', ')}; ` +\n 'the `client` already carries them, so configure them on the client, or drop `client`',\n );\n }\n this.client = options.client;\n } else {\n this.client = new GcsClient({\n ...(options.projectId === undefined ? {} : { projectId: options.projectId }),\n ...(options.apiEndpoint === undefined ? {} : { apiEndpoint: options.apiEndpoint }),\n });\n }\n const shared = {\n storage: this.client,\n bucket: options.bucket,\n ...(options.prefix === undefined ? {} : { prefix: options.prefix }),\n };\n this.storage = new GcsStorageDriver({\n ...shared,\n ...(options.maxObjectBytes === undefined ? {} : { maxObjectBytes: options.maxObjectBytes }),\n ...(options.simpleUploadThresholdBytes === undefined\n ? {}\n : { simpleUploadThresholdBytes: options.simpleUploadThresholdBytes }),\n });\n this.registry = new GcsRegistryDriver({\n ...shared,\n ...(options.now === undefined ? {} : { now: options.now }),\n });\n brandAsBackend(this);\n }\n}\n", "/**\n * `GcsStorageDriver` \u2014 an {@link IStorageDriver} over Google Cloud Storage.\n *\n * Uses the official `@google-cloud/storage`, a real dependency of this package \u2014 installing\n * `@cloudbitmaps/gcs` is what installs 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 \u2014 a second write to the same\n * object fails with 412 \u2192 {@link WriteConflictError}, never a silent overwrite (hard invariant 2: storage\n * objects are immutable and never overwritten in place), the GCS analogue of S3's `If-None-Match: *` and\n * 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 a load'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 {\n NotFoundError,\n TransientError,\n ValidationError,\n WriteConflictError,\n isNotFoundError,\n isValidationError,\n isWriteConflictError,\n} from '@cloudbitmaps/core/driver-kit';\nimport type {\n BlobSink,\n GenKey,\n IStorageDriver,\n SegmentRef,\n StorageCaps,\n} from '@cloudbitmaps/core/driver-kit';\nimport { createHash, randomBytes, type Hash } from 'node:crypto';\nimport type { Writable } from 'node:stream';\nimport { once } from 'node:events';\nimport type { Storage } from '@google-cloud/storage';\nimport {\n storageObjectName,\n normalizeGcsPrefix,\n parseGenerationFromName,\n segmentObjectPrefix,\n} from './keys';\nimport { isInvalidRange, isNotFound, isPreconditionFailed, isTransient } from './gcs-errors';\nimport { saveOnce } from './send-once';\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/** The custom-metadata name a resumable upload stores its write id under. Short: it travels with every such write. */\nconst WRITE_ID_KEY = 'cbwid';\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 \u2014 the simple path buffers\n * only \u2264 this many bytes, the resumable path streams. Default 8 MiB (\u2248 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 GcsStorageDriverOptions {\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. `cloudbitmaps/`). */\n readonly prefix?: string;\n /** Largest object this driver will write/advertise (default = GCS's 5 TiB max). Must be a positive safe integer. */\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 * Must be a positive safe integer. */\n readonly simpleUploadThresholdBytes?: number;\n}\n\nexport class GcsStorageDriver implements IStorageDriver {\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: GcsStorageDriverOptions) {\n this.storage = options.storage;\n this.bucket = options.bucket;\n this.prefix = normalizeGcsPrefix(options.prefix);\n // Fail fast at the boundary: `??` only guards `undefined`, so NaN, 0, a negative or a fraction would otherwise\n // reject every write (cap) or corrupt the upload-path choice (threshold).\n for (const [name, value] of [\n ['maxObjectBytes', options.maxObjectBytes],\n ['simpleUploadThresholdBytes', options.simpleUploadThresholdBytes],\n ] as const) {\n if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {\n throw new ValidationError(`${name} must be a positive safe integer; got ${value}`);\n }\n }\n this.maxObjectBytes = options.maxObjectBytes ?? DEFAULT_MAX_OBJECT_BYTES;\n this.threshold = options.simpleUploadThresholdBytes ?? DEFAULT_UPLOAD_THRESHOLD_BYTES;\n }\n\n capabilities(): StorageCaps {\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 = storageObjectName(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 \u2014 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 = storageObjectName(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 \u2014 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 = storageObjectName(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 \u2014 NOT the per-op cache\n // path (has/count/intersect) \u2014 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(storageObjectName(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) \u2014 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 store's read retry can ride it out and a write's caller can tell it from a deterministic failure;\n * everything else propagates unchanged. The final fallback at every client-call site, so callers and the read\n * retry 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** ({@link saveOnce}) \u2014 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 \u2248 one threshold). SHA-256 is hashed incrementally. Both\n * paths carry `ifGenerationMatch: 0` (create-only-if-absent) \u2192 a conflict is a 412, mapped by the driver to\n * {@link WriteConflictError}. On error the caller invokes {@link abort}.\n *\n * **The simple upload is sent once** ({@link saveOnce}): a replay of one that landed and lost its response would find\n * its own object and read as a lost race, so a transient failure throws {@link TransientError} instead. The resumable\n * upload is a session of requests, which the SDK retries within by asking the session how much it holds; the SDK takes\n * that retry from the client's options, not the call's, so it stays on. Instead, each resumable upload tags its\n * object with a random id in custom metadata, and a `412` on its commit is read back: an object that carries this\n * upload's id is a success, and any other is the lost race. The read is paid only on that `412`.\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 \u2192 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 /**\n * This write's own id, which the resumable upload stores in the object's custom metadata (outside the `.crbm`\n * bytes) so a conflict can be told from a replay of this same write. The simple upload is sent once and needs none.\n */\n private readonly writeId = randomBytes(16).toString('hex');\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 \u2014 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({\n resumable: true,\n ...GcsUploadSink.WRITE_OPTS,\n metadata: {\n ...GcsUploadSink.WRITE_OPTS.metadata,\n metadata: { [WRITE_ID_KEY]: this.writeId },\n },\n });\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); // \u2264 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 const sha256 = this.hash.digest('hex');\n if (this.stream === undefined) {\n // Small object: one simple (non-resumable) upload, sent once \u2014 write-once enforced everywhere, the emulator too.\n await saveOnce(\n this.file,\n concatBytes(this.buffered, this.bufferedLen),\n GcsUploadSink.WRITE_OPTS,\n );\n return { size: this.total, sha256 };\n }\n try {\n if (this.failure !== undefined) throw this.failure;\n this.stream.end();\n await this.done; // rejects if the commit fails (e.g. 412 write-once conflict)\n } catch (err) {\n // The SDK retries a resumable session, so a commit that landed and lost its response can be answered `412` by\n // its own replay. Nothing overwrites a write-once object, so the id it holds says who wrote it. A failed\n // read-back throws, and reaches the caller as `TransientError` when the fault is transient: neither a success\n // nor a conflict.\n if (!isPreconditionFailed(err) || (await this.storedWriteId()) !== this.writeId) throw err;\n }\n return { size: this.total, sha256 };\n }\n\n /** The id the stored object carries in its custom metadata, or `undefined` when it carries none or is gone. */\n private async storedWriteId(): Promise<string | undefined> {\n try {\n const [meta] = await this.file.getMetadata();\n return meta.metadata?.[WRITE_ID_KEY] as string | undefined;\n } catch (err) {\n if (isNotFound(err)) return undefined;\n throw err;\n }\n }\n\n async abort(): Promise<void> {\n if (this.stream !== undefined && !this.stream.destroyed) this.stream.destroy();\n }\n}\n", "/**\n * Logical-ref \u2192 GCS object-name mapping for {@link GcsStorageDriver}.\n *\n * Pure string logic, no SDK dependency \u2014 unit-testable without GCS or an emulator. Uses the **same\n * backend-agnostic `.crbm` object-name scheme** as the S3 + LocalFs storage drivers\n * (`<prefix><ns>/segments/<segment>.<gen>.crbm`), so a segment reads identically whichever storage backend holds\n * it. The default (absent) namespace maps to `_default`, which cannot collide with a real namespace because a\n * caller's `_default` encodes to `%5Fdefault` while the sentinel is emitted literally.\n *\n * The storage-key layout is shared, not copied: `prefixPart` and the name encoders come from\n * `@cloudbitmaps/core/driver-kit`, so a segment written by one driver reads identically under another. Two\n * drivers disagreeing about how a name becomes a key would be a silent cross-driver incompatibility on the\n * same bucket, which is exactly the kind of thing a local copy drifts into.\n */\n\nimport {\n ValidationError,\n encodeNameForKey,\n namespaceKeyPart,\n prefixPart,\n validateSegmentRef,\n} from '@cloudbitmaps/core/driver-kit';\nimport type { GenKey, SegmentRef } from '@cloudbitmaps/core/driver-kit';\n\nconst SUFFIX = '.crbm';\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)}${namespaceKeyPart(ref.namespace)}/segments/${encodeNameForKey(ref.segment)}.`;\n}\n\n/** The full GCS object name of one `.crbm` generation: `<segmentPrefix><gen>.crbm`. */\nexport function storageObjectName(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 \u2014 no leading zeros (so `\u2026s.07.crbm` can't alias `\u2026s.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 (the transient class mirrors S3's).\n *\n * SDK-free + side-effect-free \u2014 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`, \u2026).\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 \u2014 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) \u2014 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 * `saveOnce` \u2014 upload one object in a single request, sent once, with no SDK retry around it.\n *\n * `file.save()` runs a single-request upload inside a retry loop, and it counts an upload that carries\n * `ifGenerationMatch` as safe to repeat, so after a lost response \u2014 a timeout, a reset connection, a 5xx \u2014 it sends\n * the upload again. When the first had landed, the second meets it: `ifGenerationMatch: 0` finds the object it\n * created, a generation fence finds the generation it replaced, and GCS answers `412`, which the driver can only report\n * as a lost race. The caller is told its write lost when it won. Only the caller can find out which happened, by\n * reading the pointer or listing the generations, so the write is sent once and a transient failure reaches it as\n * `TransientError`.\n *\n * `createWriteStream({ resumable: false })` is that same single multipart request without the loop: the SDK sends a\n * streamed upload with no retries of its own, since a stream it has read cannot be read again. The client is not\n * touched, so a caller's own client keeps its retry options for every other call.\n *\n * **`resumable: false` is load-bearing twice.** It is what makes this one request, and it is the path on which\n * fake-gcs-server enforces `ifGenerationMatch`, so the integration lane tests the precondition rather than passing over\n * a write with none.\n */\nimport { once } from 'node:events';\nimport type { CreateWriteStreamOptions, Storage } from '@google-cloud/storage';\n\ntype GcsFile = ReturnType<ReturnType<Storage['bucket']>['file']>;\n\n/** Upload `body` to `file` in one request, sent once; resolves once GCS has stored it and its checksum matched. */\nexport async function saveOnce(\n file: GcsFile,\n body: Uint8Array,\n options: CreateWriteStreamOptions,\n): Promise<void> {\n const stream = file.createWriteStream({ ...options, resumable: false });\n const stored = once(stream, 'finish'); // rejects on 'error', which is how a failed upload surfaces\n stream.end(body);\n await stored;\n}\n", "/**\n * `GcsRegistryDriver` \u2014 an {@link IRegistryDriver} over Google Cloud Storage.\n *\n * Lets a **GCS deployment run on one bucket alone** \u2014 storage `.crbm` generations and the registry in the same\n * place, with no second cloud involved.\n *\n * The protocol (an ABA-safe OCC counter, tombstoning delete, the bounded retry, the key layout) lives once\n * in {@link ObjectStoreRegistry}; this file is only the three I/O calls GCS makes.\n *\n * **The atomic swap is offloaded to GCS's object preconditions.** `ifGenerationMatch: 0` is create-only\n * (\"only if it does not exist\") and `ifGenerationMatch: <generation>` is compare-and-swap \u2014 the same pair S3\n * spells `If-None-Match: *` / `If-Match: <etag>`. A lost race returns `412` \u2192 {@link WriteConflictError}. Each write\n * is one request sent once ({@link saveOnce}), so a `412` means another write got there first, never this one meeting\n * itself after a lost response. A transient failure reaches the caller as {@link TransientError}: the write may or may\n * not have landed, and the caller re-reads the row to learn where it stands.\n *\n * **The version fence is the object's `generation`, not its ETag**: GCS mutates the generation on every\n * overwrite and it is the value `ifGenerationMatch` compares, so it is the only correct fence here. (Note\n * the unrelated collision of vocabulary \u2014 a GCS *object generation* has nothing to do with a CloudBitmaps\n * `.crbm` *generation*.) Reads are strongly consistent, satisfying the registry's `strongRead` contract.\n *\n * **Deployment requirements** (a policy that violates these silently corrupts the registry):\n * - The principal needs `storage.objects.get`, `create`, `update` and `list` on the bucket. Without `list`\n * the registry cannot enumerate, and a missing-object read may surface as `403` rather than `404`.\n * - **Do not apply an Object Lifecycle rule to the `registry/` prefix that deletes a live object** (one on\n * noncurrent versions only is safe), and do not enable a retention\n * policy that blocks overwrite. `delete` tombstones rather than removing, for ABA-safety \u2014 see\n * {@link ObjectStoreRegistry}.\n * - Object versioning is neither required nor used; the driver always reads the live generation. Because a\n * non-versioned bucket retires a superseded generation immediately, a read whose pinned generation is\n * overwritten mid-flight reports {@link ObjectVersionRaced} and is retried by {@link ObjectStoreRegistry},\n * never mistaken for an absent row.\n */\nimport {\n IntegrityError,\n MAX_ROW_BYTES,\n ObjectStoreRegistry,\n ObjectVersionRaced,\n TransientError,\n WriteConflictError,\n normalizeObjectPrefix,\n} from '@cloudbitmaps/core/driver-kit';\nimport type { ObjectRegistryStore, ObjectRow } from '@cloudbitmaps/core/driver-kit';\nimport type { Storage } from '@google-cloud/storage';\nimport { isNotFound, isPreconditionFailed, isTransient } from './gcs-errors';\nimport { saveOnce } from './send-once';\n\nexport interface GcsRegistryDriverOptions {\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 registry objects live (e.g. `cloudbitmaps/`). */\n readonly prefix?: string;\n /** Injected clock for `createdAt`/`updatedAt`; defaults to `Date.now`. */\n readonly now?: () => number;\n}\n\n/** The three calls {@link ObjectStoreRegistry} needs, in GCS's dialect. */\nclass GcsStore implements ObjectRegistryStore {\n readonly label = 'GCS';\n\n constructor(\n private readonly storage: Storage,\n private readonly bucket: string,\n ) {}\n\n /** A file handle, optionally pinned to one object generation (GCS's version fence). */\n private file(name: string, generation?: string) {\n return generation === undefined\n ? this.storage.bucket(this.bucket).file(name)\n : this.storage.bucket(this.bucket).file(name, { generation });\n }\n\n async read(key: string): Promise<ObjectRow | null> {\n // Metadata first: it carries the `generation` fence AND the size, so a hostile object is rejected on its\n // advertised length before any of it is buffered.\n let generation: string;\n let size: number;\n try {\n const [meta] = await this.file(key).getMetadata();\n generation = String(meta.generation ?? '');\n size = Number(meta.size ?? 0);\n } catch (err) {\n if (isNotFound(err)) return null;\n throw mapError(err);\n }\n if (size > MAX_ROW_BYTES) {\n throw new IntegrityError(`registry object ${size}B exceeds cap ${MAX_ROW_BYTES}B`);\n }\n try {\n // Pin the download to the generation we just measured, so a concurrent overwrite between the two calls\n // cannot hand us bytes that do not match the fence we are about to compare-and-swap against.\n const [buf] = await this.file(key, generation).download();\n return { bytes: new Uint8Array(buf), version: generation };\n } catch (err) {\n // A 404 on the PINNED download does not mean the object is gone \u2014 it means the generation we pinned\n // is. Buckets here run without Object Versioning (the registry neither needs nor uses it), so an\n // overwrite retires the superseded generation immediately and this is simply what losing the pin looks\n // like. Reporting it as absence would make a live row disappear from `get`, hand `delete` a false\n // success, and silently drop rows from `list`; the caller re-reads instead.\n if (isNotFound(err)) throw new ObjectVersionRaced(key);\n throw mapError(err);\n }\n }\n\n async write(\n key: string,\n body: Uint8Array,\n expect: 'absent' | { version: string },\n ): Promise<void> {\n try {\n // One request, sent once: a replay of a write that landed would fail its own precondition, and read as a lost\n // race. A registry row is a few hundred bytes, so a resumable session would also carry one round trip's worth\n // of data over two.\n await saveOnce(this.file(key), body, {\n contentType: 'application/json',\n preconditionOpts: {\n ifGenerationMatch: expect === 'absent' ? 0 : generationFence(expect.version, key),\n },\n });\n } catch (err) {\n if (isPreconditionFailed(err)) {\n throw new WriteConflictError(`registry OCC conflict for ${key}`);\n }\n throw mapError(err);\n }\n }\n\n async *listKeys(prefix: string): AsyncIterable<string> {\n let pageToken: string | undefined;\n do {\n let files;\n let next: { pageToken?: string } | null | undefined;\n try {\n // Explicit paging rather than autoPaginate: a registry can hold far more rows than a segment holds\n // generations, and draining every page into memory first is what `list()` exists to avoid.\n [files, next] = await this.storage\n .bucket(this.bucket)\n .getFiles({ prefix, autoPaginate: false, maxResults: 1000, pageToken });\n } catch (err) {\n throw mapError(err);\n }\n for (const f of files ?? []) yield f.name;\n pageToken = next?.pageToken;\n } while (pageToken !== undefined);\n }\n}\n\n/**\n * Narrow a version fence back to the number `ifGenerationMatch` takes. The fence always originates as\n * `meta.generation` in {@link GcsStore.read}, so this cannot fire in practice \u2014 but `Number()` answers `NaN`\n * for anything non-numeric, and `ifGenerationMatch: NaN` serializes to a precondition GCS ignores. That\n * failure is invisible (writes simply stop being fenced), so it is checked rather than assumed.\n */\nfunction generationFence(version: string, key: string): number {\n const generation = Number(version);\n if (!Number.isSafeInteger(generation) || generation <= 0) {\n throw new IntegrityError(`registry version fence is not a GCS generation: ${key}`);\n }\n return generation;\n}\n\n/** Reclassify a transient GCS fault as a retryable {@link TransientError}; pass everything else through. */\nfunction mapError(err: unknown): unknown {\n if (isTransient(err)) {\n return new TransientError(\n `transient GCS fault: ${(err as { name?: string } | null)?.name ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n}\n\nexport class GcsRegistryDriver extends ObjectStoreRegistry {\n constructor(options: GcsRegistryDriverOptions) {\n super(\n new GcsStore(options.storage, options.bucket),\n normalizeObjectPrefix(options.prefix),\n options.now ?? ((): number => Date.now()),\n );\n }\n}\n"],
|
|
5
|
-
"mappings": ";AAUA,SAA0B,mBAAAA,kBAAiB,sBAAsB;AAMjE,SAAS,WAAW,iBAAiB;;;ACCrC;AAAA,EACE;AAAA,EACA;AAAA,EACA,mBAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,YAAY,mBAA8B;AAEnD,SAAS,QAAAC,aAAY;;;ACpBrB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,IAAM,SAAS;AAOR,SAAS,mBAAmB,QAAgD;AACjF,MAAI,WAAW,OAAW,QAAO;AACjC,aAAW,MAAM,QAAQ;AACvB,QAAI,GAAG,WAAW,CAAC,IAAI,IAAM;AAC3B,YAAM,IAAI,gBAAgB,4CAA4C;AAAA,IACxE;AAAA,EACF;AACA,aAAW,WAAW,OAAO,MAAM,GAAG,GAAG;AACvC,QAAI,YAAY,OAAO,YAAY,MAAM;AACvC,YAAM,IAAI;AAAA,QACR,sDAAsD,KAAK,UAAU,MAAM,CAAC;AAAA,MAC9E;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAMO,SAAS,oBAAoB,QAA4B,KAAyB;AACvF,qBAAmB,GAAG;AACtB,SAAO,GAAG,WAAW,MAAM,CAAC,GAAG,iBAAiB,IAAI,SAAS,CAAC,aAAa,iBAAiB,IAAI,OAAO,CAAC;AAC1G;AAGO,SAAS,kBAAkB,QAA4B,KAAqB;AACjF,MAAI,CAAC,OAAO,UAAU,IAAI,UAAU,KAAK,IAAI,aAAa,GAAG;AAC3D,UAAM,IAAI,gBAAgB,kDAAkD,IAAI,UAAU,EAAE;AAAA,EAC9F;AACA,SAAO,GAAG,oBAAoB,QAAQ,GAAG,CAAC,GAAG,IAAI,UAAU,GAAG,MAAM;AACtE;AAQO,SAAS,wBAAwB,eAAuB,YAAmC;AAChG,MAAI,CAAC,WAAW,WAAW,aAAa,KAAK,CAAC,WAAW,SAAS,MAAM,EAAG,QAAO;AAClF,QAAM,SAAS,WAAW,MAAM,cAAc,QAAQ,WAAW,SAAS,OAAO,MAAM;AACvF,MAAI,CAAC,iBAAiB,KAAK,MAAM,EAAG,QAAO;AAC3C,QAAM,aAAa,OAAO,MAAM;AAChC,SAAO,OAAO,cAAc,UAAU,IAAI,aAAa;AACzD;;;AClEA,SAAS,WAAW,KAAkC;AACpD,QAAM,IAAI;AACV,MAAI,OAAO,GAAG,SAAS,SAAU,QAAO,EAAE;AAC1C,MAAI,OAAO,GAAG,UAAU,WAAW,SAAU,QAAO,EAAE,SAAS;AAC/D,SAAO;AACT;AAGA,SAAS,YAAY,KAAkC;AACrD,QAAM,OAAQ,KAAmC;AACjD,SAAO,OAAO,SAAS,WAAW,OAAO;AAC3C;AAMO,SAAS,qBAAqB,KAAuB;AAC1D,SAAO,WAAW,GAAG,MAAM;AAC7B;AAGO,SAAS,WAAW,KAAuB;AAChD,SAAO,WAAW,GAAG,MAAM;AAC7B;AAGO,SAAS,eAAe,KAAuB;AACpD,SAAO,WAAW,GAAG,MAAM;AAC7B;AAOO,SAAS,YAAY,KAAuB;AACjD,MAAI,qBAAqB,GAAG,KAAK,WAAW,GAAG,KAAK,eAAe,GAAG,EAAG,QAAO;AAChF,QAAM,SAAS,WAAW,GAAG;AAC7B,MAAI,WAAW,OAAQ,WAAW,UAAa,UAAU,OAAO,SAAS,IAAM,QAAO;AACtF,QAAM,MAAM,YAAY,GAAG;AAC3B,SACE,QAAQ,gBACR,QAAQ,eACR,QAAQ,kBACR,QAAQ,WACR,QAAQ,eACR,QAAQ;AAEZ;;;ACzCA,SAAS,YAAY;AAMrB,eAAsB,SACpB,MACA,MACA,SACe;AACf,QAAM,SAAS,KAAK,kBAAkB,EAAE,GAAG,SAAS,WAAW,MAAM,CAAC;AACtE,QAAM,SAAS,KAAK,QAAQ,QAAQ;AACpC,SAAO,IAAI,IAAI;AACf,QAAM;AACR;;;AHaA,IAAM,2BAA2B,IAAI,OAAO,OAAO,OAAO;AAG1D,IAAM,eAAe;AAQrB,IAAM,iCAAiC,IAAI,OAAO;AAgB3C,IAAM,mBAAN,MAAiD;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,SAAkC;AAC5C,SAAK,UAAU,QAAQ;AACvB,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,mBAAmB,QAAQ,MAAM;AAG/C,eAAW,CAAC,MAAM,KAAK,KAAK;AAAA,MAC1B,CAAC,kBAAkB,QAAQ,cAAc;AAAA,MACzC,CAAC,8BAA8B,QAAQ,0BAA0B;AAAA,IACnE,GAAY;AACV,UAAI,UAAU,WAAc,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,IAAI;AACtE,cAAM,IAAIC,iBAAgB,GAAG,IAAI,yCAAyC,KAAK,EAAE;AAAA,MACnF;AAAA,IACF;AACA,SAAK,iBAAiB,QAAQ,kBAAkB;AAChD,SAAK,YAAY,QAAQ,8BAA8B;AAAA,EACzD;AAAA,EAEA,eAA4B;AAC1B,WAAO,EAAE,WAAW,MAAM,gBAAgB,KAAK,gBAAgB,gBAAgB,KAAK;AAAA,EACtF;AAAA,EAEQ,KAAK,MAAc;AACzB,WAAO,KAAK,QAAQ,OAAO,KAAK,MAAM,EAAE,KAAK,IAAI;AAAA,EACnD;AAAA,EAEA,MAAM,aACJ,KACA,OAC2C;AAC3C,UAAM,aAAa,kBAAkB,KAAK,QAAQ,GAAG;AACrD,UAAM,OAAO,IAAI,cAAc,KAAK,KAAK,UAAU,GAAG,KAAK,gBAAgB,KAAK,SAAS;AACzF,QAAI;AACF,YAAM,MAAM,IAAI;AAChB,aAAO,MAAM,KAAK,OAAO;AAAA,IAC3B,SAAS,KAAK;AACZ,YAAM,KAAK,MAAM;AAEjB,UAAI,qBAAqB,GAAG,GAAG;AAC7B,cAAM,IAAI;AAAA,UACR,2CAA2C,IAAI,OAAO,IAAI,IAAI,UAAU;AAAA,QAC1E;AAAA,MACF;AACA,UAAI,kBAAkB,GAAG,KAAK,qBAAqB,GAAG,KAAK,gBAAgB,GAAG,EAAG,OAAM;AACvF,YAAM,KAAK,SAAS,GAAG;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAAa,QAAgB,QAAqC;AAC/E,QAAI,CAAC,OAAO,UAAU,MAAM,KAAK,CAAC,OAAO,UAAU,MAAM,KAAK,SAAS,KAAK,SAAS,GAAG;AACtF,YAAM,IAAIA,iBAAgB,wBAAwB,MAAM,WAAW,MAAM,EAAE;AAAA,IAC7E;AACA,QAAI,WAAW,EAAG,QAAO,IAAI,WAAW,CAAC;AACzC,UAAM,aAAa,kBAAkB,KAAK,QAAQ,GAAG;AACrD,QAAI;AAEF,YAAM,CAAC,GAAG,IAAI,MAAM,KAAK,KAAK,UAAU,EAAE,SAAS;AAAA,QACjD,OAAO;AAAA,QACP,KAAK,SAAS,SAAS;AAAA,MACzB,CAAC;AAED,UAAI,IAAI,WAAW,QAAQ;AACzB,cAAM,IAAIA;AAAA,UACR,UAAU,MAAM,KAAK,SAAS,MAAM,wBAAwB,IAAI,MAAM;AAAA,QACxE;AAAA,MACF;AACA,aAAO,IAAI,WAAW,IAAI,QAAQ,IAAI,YAAY,IAAI,UAAU;AAAA,IAClE,SAAS,KAAK;AACZ,YAAM,KAAK,aAAa,KAAK,GAAG;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,KAAa,UAAgE;AACzF,UAAM,aAAa,kBAAkB,KAAK,QAAQ,GAAG;AACrD,QAAI;AAIF,YAAM,CAAC,IAAI,IAAI,MAAM,KAAK,KAAK,UAAU,EAAE,YAAY;AACvD,YAAM,OAAO,OAAO,KAAK,QAAQ,CAAC;AAClC,UAAI,CAAC,OAAO,cAAc,IAAI,KAAK,OAAO,GAAG;AAC3C,cAAM,IAAIA,iBAAgB,wCAAwC,OAAO,KAAK,IAAI,CAAC,EAAE;AAAA,MACvF;AACA,UAAI,YAAY,KAAK,SAAS,EAAG,QAAO,EAAE,OAAO,IAAI,WAAW,CAAC,GAAG,KAAK;AACzE,YAAM,QAAQ,KAAK,IAAI,GAAG,OAAO,QAAQ;AACzC,YAAM,CAAC,GAAG,IAAI,MAAM,KAAK,KAAK,UAAU,EAAE,SAAS,EAAE,OAAO,KAAK,OAAO,EAAE,CAAC;AAC3E,aAAO,EAAE,OAAO,IAAI,WAAW,IAAI,QAAQ,IAAI,YAAY,IAAI,UAAU,GAAG,KAAK;AAAA,IACnF,SAAS,KAAK;AACZ,YAAM,KAAK,aAAa,KAAK,GAAG;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,KAA4B;AAEvC,QAAI;AACF,YAAM,KAAK,KAAK,kBAAkB,KAAK,QAAQ,GAAG,CAAC,EAAE,OAAO,EAAE,gBAAgB,KAAK,CAAC;AAAA,IACtF,SAAS,KAAK;AACZ,YAAM,KAAK,SAAS,GAAG;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,OAAO,KAAK,KAAwC;AAClD,UAAM,SAAS,oBAAoB,KAAK,QAAQ,GAAG;AACnD,QAAI;AACJ,QAAI;AAEF,OAAC,KAAK,IAAI,MAAM,KAAK,QAAQ,OAAO,KAAK,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC;AAAA,IACtE,SAAS,KAAK;AACZ,YAAM,KAAK,SAAS,GAAG;AAAA,IACzB;AACA,eAAW,KAAK,OAAO;AACrB,YAAM,aAAa,wBAAwB,QAAQ,EAAE,IAAI;AACzD,UAAI,eAAe,MAAM;AACvB,cAAM,EAAE,WAAW,IAAI,WAAW,SAAS,IAAI,SAAS,WAAW;AAAA,MACrE;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGQ,aAAa,KAAc,KAAsB;AACvD,QAAI,kBAAkB,GAAG,EAAG,QAAO;AACnC,QAAI,WAAW,GAAG;AAChB,aAAO,IAAI,cAAc,uBAAuB,IAAI,OAAO,IAAI,IAAI,UAAU,EAAE;AAGjF,QAAI,eAAe,GAAG,GAAG;AACvB,aAAO,IAAIA,iBAAgB,2BAA2B,IAAI,OAAO,IAAI,IAAI,UAAU,EAAE;AAAA,IACvF;AACA,WAAO,KAAK,SAAS,GAAG;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,SAAS,KAAuB;AACtC,QAAI,YAAY,GAAG,GAAG;AACpB,aAAO,IAAI;AAAA,QACT,wBAAyB,KAAmC,QAAQ,SAAS;AAAA,QAC7E,EAAE,OAAO,IAAI;AAAA,MACf;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;AAKA,SAAS,YAAY,OAA8B,OAA2B;AAC5E,QAAM,MAAM,IAAI,WAAW,KAAK;AAChC,MAAI,SAAS;AACb,aAAW,KAAK,OAAO;AACrB,QAAI,IAAI,GAAG,MAAM;AACjB,cAAU,EAAE;AAAA,EACd;AACA,SAAO;AACT;AAiBA,IAAM,gBAAN,MAAM,eAAkC;AAAA,EAStC,YACmB,MACA,gBACA,WACjB;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EAHgB;AAAA,EACA;AAAA,EACA;AAAA,EAXF,OAAa,WAAW,QAAQ;AAAA,EACzC,QAAQ;AAAA,EACC,WAAyB,CAAC;AAAA,EACnC,cAAc;AAAA,EACd;AAAA;AAAA,EACA;AAAA,EACA;AAAA,EAQR,OAAwB,aAAa;AAAA,IACnC,kBAAkB,EAAE,mBAAmB,EAAW;AAAA;AAAA,IAClD,UAAU,EAAE,aAAa,2BAA2B;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMiB,UAAU,YAAY,EAAE,EAAE,SAAS,KAAK;AAAA,EAEzD,MAAM,MAAM,OAAkC;AAC5C,QAAI,KAAK,YAAY,OAAW,OAAM,KAAK;AAC3C,QAAI,MAAM,WAAW,EAAG;AACxB,SAAK,SAAS,MAAM;AACpB,QAAI,KAAK,QAAQ,KAAK,gBAAgB;AACpC,YAAM,IAAIA,iBAAgB,iCAAiC,KAAK,cAAc,EAAE;AAAA,IAClF;AACA,SAAK,KAAK,OAAO,KAAK;AACtB,QAAI,KAAK,WAAW,QAAW;AAI7B,UAAI,CAAC,KAAK,OAAO,MAAM,KAAK,EAAG,OAAMC,MAAK,KAAK,QAAQ,OAAO;AAC9D;AAAA,IACF;AACA,SAAK,SAAS,KAAK,KAAK;AACxB,SAAK,eAAe,MAAM;AAC1B,QAAI,KAAK,cAAc,KAAK,UAAW,MAAK,eAAe;AAAA,EAC7D;AAAA;AAAA,EAGQ,iBAAuB;AAC7B,UAAM,SAAS,KAAK,KAAK,kBAAkB;AAAA,MACzC,WAAW;AAAA,MACX,GAAG,eAAc;AAAA,MACjB,UAAU;AAAA,QACR,GAAG,eAAc,WAAW;AAAA,QAC5B,UAAU,EAAE,CAAC,YAAY,GAAG,KAAK,QAAQ;AAAA,MAC3C;AAAA,IACF,CAAC;AACD,SAAK,SAAS;AACd,SAAK,OAAOA,MAAK,QAAQ,QAAQ;AACjC,WAAO,GAAG,SAAS,CAAC,MAAe;AACjC,WAAK,YAAY;AAAA,IACnB,CAAC;AACD,SAAK,KAAK,MAAM,MAAM,MAAS;AAC/B,UAAM,MAAM,YAAY,KAAK,UAAU,KAAK,WAAW;AACvD,SAAK,SAAS,SAAS;AACvB,SAAK,cAAc;AACnB,WAAO,MAAM,GAAG;AAAA,EAClB;AAAA,EAEA,MAAM,SAAoD;AACxD,UAAM,SAAS,KAAK,KAAK,OAAO,KAAK;AACrC,QAAI,KAAK,WAAW,QAAW;AAE7B,YAAM;AAAA,QACJ,KAAK;AAAA,QACL,YAAY,KAAK,UAAU,KAAK,WAAW;AAAA,QAC3C,eAAc;AAAA,MAChB;AACA,aAAO,EAAE,MAAM,KAAK,OAAO,OAAO;AAAA,IACpC;AACA,QAAI;AACF,UAAI,KAAK,YAAY,OAAW,OAAM,KAAK;AAC3C,WAAK,OAAO,IAAI;AAChB,YAAM,KAAK;AAAA,IACb,SAAS,KAAK;AAKZ,UAAI,CAAC,qBAAqB,GAAG,KAAM,MAAM,KAAK,cAAc,MAAO,KAAK,QAAS,OAAM;AAAA,IACzF;AACA,WAAO,EAAE,MAAM,KAAK,OAAO,OAAO;AAAA,EACpC;AAAA;AAAA,EAGA,MAAc,gBAA6C;AACzD,QAAI;AACF,YAAM,CAAC,IAAI,IAAI,MAAM,KAAK,KAAK,YAAY;AAC3C,aAAO,KAAK,WAAW,YAAY;AAAA,IACrC,SAAS,KAAK;AACZ,UAAI,WAAW,GAAG,EAAG,QAAO;AAC5B,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI,KAAK,WAAW,UAAa,CAAC,KAAK,OAAO,UAAW,MAAK,OAAO,QAAQ;AAAA,EAC/E;AACF;;;AI3UA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAAC;AAAA,EACA,sBAAAC;AAAA,EACA;AAAA,OACK;AAkBP,IAAM,WAAN,MAA8C;AAAA,EAG5C,YACmB,SACA,QACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA,EAJV,QAAQ;AAAA;AAAA,EAQT,KAAK,MAAc,YAAqB;AAC9C,WAAO,eAAe,SAClB,KAAK,QAAQ,OAAO,KAAK,MAAM,EAAE,KAAK,IAAI,IAC1C,KAAK,QAAQ,OAAO,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,WAAW,CAAC;AAAA,EAChE;AAAA,EAEA,MAAM,KAAK,KAAwC;AAGjD,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,YAAM,CAAC,IAAI,IAAI,MAAM,KAAK,KAAK,GAAG,EAAE,YAAY;AAChD,mBAAa,OAAO,KAAK,cAAc,EAAE;AACzC,aAAO,OAAO,KAAK,QAAQ,CAAC;AAAA,IAC9B,SAAS,KAAK;AACZ,UAAI,WAAW,GAAG,EAAG,QAAO;AAC5B,YAAM,SAAS,GAAG;AAAA,IACpB;AACA,QAAI,OAAO,eAAe;AACxB,YAAM,IAAI,eAAe,mBAAmB,IAAI,iBAAiB,aAAa,GAAG;AAAA,IACnF;AACA,QAAI;AAGF,YAAM,CAAC,GAAG,IAAI,MAAM,KAAK,KAAK,KAAK,UAAU,EAAE,SAAS;AACxD,aAAO,EAAE,OAAO,IAAI,WAAW,GAAG,GAAG,SAAS,WAAW;AAAA,IAC3D,SAAS,KAAK;AAMZ,UAAI,WAAW,GAAG,EAAG,OAAM,IAAI,mBAAmB,GAAG;AACrD,YAAM,SAAS,GAAG;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,MAAM,MACJ,KACA,MACA,QACe;AACf,QAAI;AAIF,YAAM,SAAS,KAAK,KAAK,GAAG,GAAG,MAAM;AAAA,QACnC,aAAa;AAAA,QACb,kBAAkB;AAAA,UAChB,mBAAmB,WAAW,WAAW,IAAI,gBAAgB,OAAO,SAAS,GAAG;AAAA,QAClF;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,qBAAqB,GAAG,GAAG;AAC7B,cAAM,IAAIC,oBAAmB,6BAA6B,GAAG,EAAE;AAAA,MACjE;AACA,YAAM,SAAS,GAAG;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,OAAO,SAAS,QAAuC;AACrD,QAAI;AACJ,OAAG;AACD,UAAI;AACJ,UAAI;AACJ,UAAI;AAGF,SAAC,OAAO,IAAI,IAAI,MAAM,KAAK,QACxB,OAAO,KAAK,MAAM,EAClB,SAAS,EAAE,QAAQ,cAAc,OAAO,YAAY,KAAM,UAAU,CAAC;AAAA,MAC1E,SAAS,KAAK;AACZ,cAAM,SAAS,GAAG;AAAA,MACpB;AACA,iBAAW,KAAK,SAAS,CAAC,EAAG,OAAM,EAAE;AACrC,kBAAY,MAAM;AAAA,IACpB,SAAS,cAAc;AAAA,EACzB;AACF;AAQA,SAAS,gBAAgB,SAAiB,KAAqB;AAC7D,QAAM,aAAa,OAAO,OAAO;AACjC,MAAI,CAAC,OAAO,cAAc,UAAU,KAAK,cAAc,GAAG;AACxD,UAAM,IAAI,eAAe,mDAAmD,GAAG,EAAE;AAAA,EACnF;AACA,SAAO;AACT;AAGA,SAAS,SAAS,KAAuB;AACvC,MAAI,YAAY,GAAG,GAAG;AACpB,WAAO,IAAIC;AAAA,MACT,wBAAyB,KAAkC,QAAQ,SAAS;AAAA,MAC5E,EAAE,OAAO,IAAI;AAAA,IACf;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,oBAAN,cAAgC,oBAAoB;AAAA,EACzD,YAAY,SAAmC;AAC7C;AAAA,MACE,IAAI,SAAS,QAAQ,SAAS,QAAQ,MAAM;AAAA,MAC5C,sBAAsB,QAAQ,MAAM;AAAA,MACpC,QAAQ,QAAQ,MAAc,KAAK,IAAI;AAAA,IACzC;AAAA,EACF;AACF;;;ALrIO,IAAM,0BAA0B;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,kBAAkB,CAAC,aAAa,aAAa;AAGnD,SAAS,cACP,MACA,SACA,MACA,MACM;AACN,MAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;AACnD,UAAM,IAAIC;AAAA,MACR,GAAG,IAAI,uCAAkC,YAAY,OAAO,SAAS,OAAO,OAAO;AAAA,IACrF;AAAA,EACF;AACA,QAAM,UAAU,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC;AACpE,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,OAAO,CAAC,OAAkC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI;AACnF,UAAM,IAAIA;AAAA,MACR,GAAG,IAAI,kBAAkB,KAAK,OAAO,CAAC,cAAc,KAAK,IAAI,CAAC,KAAK,IAAI;AAAA,IACzE;AAAA,EACF;AACF;AAEO,IAAM,aAAN,MAA2C;AAAA,EAGvC;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EAET,YAAY,SAA4B;AACtC;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,QAAI,QAAQ,WAAW,UAAa,QAAQ,WAAW,MAAM;AAK3D,YAAM,UAAU,gBAAgB,OAAO,CAAC,MAAM,QAAQ,CAAC,MAAM,MAAS;AACtE,UAAI,QAAQ,SAAS,GAAG;AACtB,cAAM,IAAIA;AAAA,UACR,kCAAkC,gBAAgB,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,KAAK,CAAC,yCAC3D,QAAQ,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC;AAAA,QAEpE;AAAA,MACF;AACA,WAAK,SAAS,QAAQ;AAAA,IACxB,OAAO;AACL,WAAK,SAAS,IAAI,UAAU;AAAA,QAC1B,GAAI,QAAQ,cAAc,SAAY,CAAC,IAAI,EAAE,WAAW,QAAQ,UAAU;AAAA,QAC1E,GAAI,QAAQ,gBAAgB,SAAY,CAAC,IAAI,EAAE,aAAa,QAAQ,YAAY;AAAA,MAClF,CAAC;AAAA,IACH;AACA,UAAM,SAAS;AAAA,MACb,SAAS,KAAK;AAAA,MACd,QAAQ,QAAQ;AAAA,MAChB,GAAI,QAAQ,WAAW,SAAY,CAAC,IAAI,EAAE,QAAQ,QAAQ,OAAO;AAAA,IACnE;AACA,SAAK,UAAU,IAAI,iBAAiB;AAAA,MAClC,GAAG;AAAA,MACH,GAAI,QAAQ,mBAAmB,SAAY,CAAC,IAAI,EAAE,gBAAgB,QAAQ,eAAe;AAAA,MACzF,GAAI,QAAQ,+BAA+B,SACvC,CAAC,IACD,EAAE,4BAA4B,QAAQ,2BAA2B;AAAA,IACvE,CAAC;AACD,SAAK,WAAW,IAAI,kBAAkB;AAAA,MACpC,GAAG;AAAA,MACH,GAAI,QAAQ,QAAQ,SAAY,CAAC,IAAI,EAAE,KAAK,QAAQ,IAAI;AAAA,IAC1D,CAAC;AACD,mBAAe,IAAI;AAAA,EACrB;AACF;",
|
|
3
|
+
"sources": ["../src/backend.ts", "../src/storage.ts", "../src/keys.ts", "../src/gcs-errors.ts", "../src/download-retry.ts", "../src/send-once.ts", "../src/read-once.ts", "../src/registry.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * `GcsStorage` \u2014 the Google Cloud Storage backend as one object: generations and pointer, one bucket, stated once.\n *\n * See {@link StorageBackend} for why the two halves are configured together rather than separately.\n *\n * **A note on the word.** `@google-cloud/storage` names its own client class `Storage`, so the driver option\n * that takes it is also called `storage` \u2014 which, since this library calls its durable tier storage too, reads\n * as the same word meaning two things. That is why this class builds the client itself: `new GcsStorage({ bucket })`\n * never makes you write `{ storage: storage }`. Supply `client` when you need your own.\n */\nimport { STORAGE_BACKEND, ValidationError, brandAsBackend } from '@cloudbitmaps/core/driver-kit';\nimport type {\n IRegistryDriver,\n IStorageDriver,\n StorageBackend,\n} from '@cloudbitmaps/core/driver-kit';\nimport { Storage as GcsClient } from '@google-cloud/storage';\nimport { GcsStorageDriver } from './storage';\nimport { GcsRegistryDriver } from './registry';\n\nexport interface GcsStorageOptions {\n /** Target bucket (must already exist). */\n readonly bucket: string;\n /** Optional object-name prefix under which everything lives \u2014 generations and the registry alike. */\n readonly prefix?: string;\n /**\n * A constructed `@google-cloud/storage` client. One is built from the ambient credentials when absent. Its retry\n * options apply to every request except the single-request conditional writes, which are sent once whatever they\n * say. In `@google-cloud/storage` 7.x and 8.x a download the SDK retries after a 408, 429, 500, 502, 503 or 504 can crash the process\n * with `ERR_STREAM_UNABLE_TO_PIPE`, so build it with `retryOptions: { autoRetry: false }`; the driver retries\n * downloads itself. That also turns off the SDK's retries of listings, metadata reads and resumable uploads on\n * this client, which the store does not retry. The client built here needs none of this: only its downloads are sent once.\n */\n readonly client?: GcsClient;\n /** Project id for the client built when `client` is absent (refused beside `client`). Falls back to the SDK's own resolution. */\n readonly projectId?: string;\n /** Endpoint override \u2014 point it at fake-gcs-server locally. Refused beside `client`, which carries its own. */\n readonly apiEndpoint?: string;\n /** Largest object the backend will write and advertise (default = GCS's 5 TiB max). Must be a positive safe integer. */\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 * Must be a positive safe integer. */\n readonly simpleUploadThresholdBytes?: number;\n /** Injected clock for the registry's `createdAt`/`updatedAt`; defaults to `Date.now`. */\n readonly now?: () => number;\n}\n\n/**\n * The keys `new GcsStorage(options)` takes. Any other is refused by name rather than ignored: an ignored client key\n * \u2014 `storage`, as the lower-level storage driver calls it \u2014 falls back to ambient credentials and the **public** endpoint, and\n * for a client pointed at an emulator that is production traffic from a wiring typo.\n */\nexport const GCS_STORAGE_OPTION_KEYS = [\n 'bucket',\n 'prefix',\n 'client',\n 'projectId',\n 'apiEndpoint',\n 'maxObjectBytes',\n 'simpleUploadThresholdBytes',\n 'now',\n] as const;\n\n/** The settings that build a client, which a supplied `client` already carries and so cannot be given beside. */\nconst CLIENT_SETTINGS = ['projectId', 'apiEndpoint'] as const;\n\n/** Refuse an options bag that is not an object, or that holds a key not in `keys`, naming each such key. */\nfunction refuseUnknown(\n name: string,\n options: unknown,\n keys: readonly string[],\n hint: string,\n): void {\n if (options === null || typeof options !== 'object') {\n throw new ValidationError(\n `${name} needs an options object \u2014 got ${options === null ? 'null' : typeof options}`,\n );\n }\n const unknown = Object.keys(options).filter((k) => !keys.includes(k));\n if (unknown.length > 0) {\n const list = (ks: readonly string[]): string => ks.map((k) => `\\`${k}\\``).join(', ');\n throw new ValidationError(\n `${name} does not take ${list(unknown)}. It takes ${list(keys)}; ${hint}.`,\n );\n }\n}\n\nexport class GcsStorage implements StorageBackend {\n /** Cross-bundle brand, stamped non-enumerably in the constructor so a spread cannot carry it. */\n declare readonly [STORAGE_BACKEND]: true;\n readonly storage: IStorageDriver;\n readonly registry: IRegistryDriver;\n /** The client both halves share \u2014 built here unless one was supplied. */\n readonly client: GcsClient;\n\n constructor(options: GcsStorageOptions) {\n let readClient: GcsClient;\n refuseUnknown(\n 'GcsStorage',\n options,\n GCS_STORAGE_OPTION_KEYS,\n 'a @google-cloud/storage client goes in `client`',\n );\n if (options.client !== undefined && options.client !== null) {\n // A supplied client already carries its project and endpoint, so a setting beside it is ignored, and\n // ignoring it leaves the store talking to somewhere the caller did not mean: an `apiEndpoint` meant for an\n // emulator, silently dropped, is production traffic from a client that was built for the public endpoint.\n // Refuse instead of picking one.\n const ignored = CLIENT_SETTINGS.filter((k) => options[k] !== undefined);\n if (ignored.length > 0) {\n throw new ValidationError(\n `GcsStorage takes \\`client\\` OR ${CLIENT_SETTINGS.map((k) => `\\`${k}\\``).join(' / ')}, not both \u2014 ` +\n `got \\`client\\` with ${ignored.map((k) => `\\`${k}\\``).join(', ')}; ` +\n 'the `client` already carries them, so configure them on the client, or drop `client`',\n );\n }\n this.client = options.client;\n readClient = options.client;\n } else {\n const settings = {\n ...(options.projectId === undefined ? {} : { projectId: options.projectId }),\n ...(options.apiEndpoint === undefined ? {} : { apiEndpoint: options.apiEndpoint }),\n };\n this.client = new GcsClient(settings);\n // A second client, for downloads only. `@google-cloud/storage` 7.x and 8.x retry a failed download by default, and\n // when the retried request succeeds they throw `ERR_STREAM_UNABLE_TO_PIPE` outside any promise, which ends the\n // process whatever the caller wrote around the call. A download is sent once instead, and the driver retries it\n // itself (`download-retry.ts`). Everything else keeps the default retries: a resumable upload's session, a\n // listing, a metadata read, none of which the store retries on its own. Two clients mean two `GoogleAuth`\n // instances: lazy, so the first download makes a second token fetch, and a second hourly refresh follows.\n readClient = new GcsClient({ ...settings, retryOptions: { autoRetry: false } });\n }\n const shared = {\n storage: this.client,\n readStorage: readClient,\n bucket: options.bucket,\n ...(options.prefix === undefined ? {} : { prefix: options.prefix }),\n };\n this.storage = new GcsStorageDriver({\n ...shared,\n ...(options.maxObjectBytes === undefined ? {} : { maxObjectBytes: options.maxObjectBytes }),\n ...(options.simpleUploadThresholdBytes === undefined\n ? {}\n : { simpleUploadThresholdBytes: options.simpleUploadThresholdBytes }),\n });\n this.registry = new GcsRegistryDriver({\n ...shared,\n ...(options.now === undefined ? {} : { now: options.now }),\n });\n brandAsBackend(this);\n }\n}\n", "/**\n * `GcsStorageDriver` \u2014 an {@link IStorageDriver} over Google Cloud Storage.\n *\n * Uses the official `@google-cloud/storage`, a real dependency of this package \u2014 installing\n * `@cloudbitmaps/gcs` is what installs 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 \u2014 a second write to the same\n * object fails with 412 \u2192 {@link WriteConflictError}, never a silent overwrite (hard invariant 2: storage\n * objects are immutable and never overwritten in place), the GCS analogue of S3's `If-None-Match: *` and\n * 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 a load'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 {\n NotFoundError,\n TransientError,\n ValidationError,\n WriteConflictError,\n isNotFoundError,\n isValidationError,\n isWriteConflictError,\n} from '@cloudbitmaps/core/driver-kit';\nimport type {\n BlobSink,\n GenKey,\n IStorageDriver,\n SegmentRef,\n StorageCaps,\n} from '@cloudbitmaps/core/driver-kit';\nimport { createHash, randomBytes, type Hash } from 'node:crypto';\nimport type { Writable } from 'node:stream';\nimport { once } from 'node:events';\nimport type { Storage } from '@google-cloud/storage';\nimport {\n storageObjectName,\n normalizeGcsPrefix,\n parseGenerationFromName,\n segmentObjectPrefix,\n} from './keys';\nimport {\n isInvalidRange,\n isNotFound,\n isPreconditionFailed,\n isTransient,\n isTransportFault,\n} from './gcs-errors';\nimport { retryDownload } from './download-retry';\nimport { saveOnce } from './send-once';\nimport { readOnce, singleHeader, type ObjectRead } from './read-once';\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/** The custom-metadata name a resumable upload stores its write id under. Short: it travels with every such write. */\nconst WRITE_ID_KEY = 'cbwid';\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 \u2014 the simple path buffers\n * only \u2264 this many bytes, the resumable path streams. Default 8 MiB (\u2248 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 GcsStorageDriverOptions {\n /** A constructed `@google-cloud/storage` `Storage` client (point `apiEndpoint` at fake-gcs-server locally). */\n readonly storage: Storage;\n /**\n * The client the downloads go through; `storage` when absent. `GcsStorage` passes one built with the SDK's request\n * retries off (the read client built in its constructor), because a download the SDK retries can crash the process;\n * the driver retries a download itself instead.\n */\n readonly readStorage?: Storage;\n /** Target bucket (must already exist). */\n readonly bucket: string;\n /** Optional object-name prefix under which all objects live (e.g. `cloudbitmaps/`). */\n readonly prefix?: string;\n /** Largest object this driver will write/advertise (default = GCS's 5 TiB max). Must be a positive safe integer. */\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 * Must be a positive safe integer. */\n readonly simpleUploadThresholdBytes?: number;\n}\n\nexport class GcsStorageDriver implements IStorageDriver {\n private readonly storage: Storage;\n private readonly readStorage: 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: GcsStorageDriverOptions) {\n this.storage = options.storage;\n this.readStorage = options.readStorage ?? options.storage;\n this.bucket = options.bucket;\n this.prefix = normalizeGcsPrefix(options.prefix);\n // Fail fast at the boundary: `??` only guards `undefined`, so NaN, 0, a negative or a fraction would otherwise\n // reject every write (cap) or corrupt the upload-path choice (threshold).\n for (const [name, value] of [\n ['maxObjectBytes', options.maxObjectBytes],\n ['simpleUploadThresholdBytes', options.simpleUploadThresholdBytes],\n ] as const) {\n if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {\n throw new ValidationError(`${name} must be a positive safe integer; got ${value}`);\n }\n }\n this.maxObjectBytes = options.maxObjectBytes ?? DEFAULT_MAX_OBJECT_BYTES;\n this.threshold = options.simpleUploadThresholdBytes ?? DEFAULT_UPLOAD_THRESHOLD_BYTES;\n }\n\n capabilities(): StorageCaps {\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 /** The same object on the download client. */\n private downloadable(name: string) {\n return this.readStorage.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 = storageObjectName(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 \u2014 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 = storageObjectName(this.prefix, key);\n try {\n // GCS `end` is inclusive.\n const [buf] = await retryDownload(() =>\n this.downloadable(objectName).download({ start: offset, end: offset + length - 1 }),\n );\n // A short read means the range ran past EOF \u2014 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 = storageObjectName(this.prefix, key);\n try {\n // Nothing to read: only the size is wanted, which the metadata answers in one request.\n if (maxBytes <= 0) return { bytes: new Uint8Array(0), size: await this.sizeOf(objectName) };\n if (!Number.isSafeInteger(maxBytes)) {\n throw new ValidationError(`invalid tail length ${maxBytes}`);\n }\n // One request: a suffix range (`Range: bytes=-N`, \"the last N bytes\") answers with the tail and, in\n // `Content-Range: bytes a-b/total`, the object's size. An object shorter than N comes back whole.\n let res;\n try {\n res = await retryDownload(() =>\n readOnce(\n this.downloadable(objectName),\n { end: -maxBytes, decompress: false },\n maxBytes,\n () => this.badTail(key, `the response is longer than the ${maxBytes}B requested`),\n ),\n );\n } catch (err) {\n // A zero-byte object has no suffix to satisfy, and a server may refuse the range with a 416. The metadata\n // settles whether that is an empty object (a valid, empty tail) or a real range fault.\n if (!isInvalidRange(err)) throw err;\n const size = await this.sizeOf(objectName);\n if (size !== 0) throw err;\n return { bytes: new Uint8Array(0), size };\n }\n if (res.bytes.length === 0) {\n // No bytes came back for a positive request: only an empty object may do that.\n const size = await this.sizeOf(objectName);\n if (size !== 0) throw this.badTail(key, `no bytes returned for an object of ${size}B`);\n return { bytes: res.bytes, size };\n }\n return { bytes: res.bytes, size: this.tailSize(res, maxBytes, key) };\n } catch (err) {\n throw this.mapReadError(err, key);\n }\n }\n\n /** The object's size from its metadata, validated. */\n private async sizeOf(objectName: string): Promise<number> {\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 return size;\n }\n\n /**\n * The object's total size from a tail response, refused unless every header agrees with the bytes received. A 206\n * must carry `Content-Range: bytes a-b/total` that names exactly the suffix asked for and exactly the bytes held;\n * a 200 (a server that ignored the range) is the whole object, and is accepted only when it fits in the request.\n */\n private tailSize(res: ObjectRead, maxBytes: number, key: GenKey): number {\n const received = res.bytes.length;\n if (res.status === 200) {\n if (received > maxBytes)\n throw this.badTail(key, 'whole object returned past the requested length');\n return received;\n }\n if (res.status !== 206) throw this.badTail(key, `unexpected HTTP ${res.status}`);\n const header = singleHeader(res.headers, 'content-range');\n const m = header === undefined ? null : /^bytes (\\d+)-(\\d+)\\/(\\d+)$/.exec(header);\n if (m === null) throw this.badTail(key, `Content-Range is missing or malformed`);\n const [first, last, total] = [Number(m[1]), Number(m[2]), Number(m[3])];\n if (![first, last, total].every(Number.isSafeInteger)) {\n throw this.badTail(key, 'Content-Range holds a number that is not a safe integer');\n }\n if (\n last !== total - 1 ||\n first !== Math.max(0, total - maxBytes) ||\n last - first + 1 !== received\n ) {\n throw this.badTail(\n key,\n `Content-Range ${first}-${last}/${total} disagrees with ${received}B received for a ${maxBytes}B tail`,\n );\n }\n return total;\n }\n\n private badTail(key: GenKey, why: string): ValidationError {\n return new ValidationError(`GCS tail read of ${key.segment}.${key.generation} refused: ${why}`);\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(storageObjectName(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) \u2014 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 // A connection that failed or was cut off, after the driver's retries: transient, whichever code Node gave it.\n if (isTransportFault(err)) {\n return new TransientError(\n `transient GCS fault: ${String((err as { code?: unknown }).code)}`,\n {\n cause: err,\n },\n );\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 store's read retry can ride it out and a write's caller can tell it from a deterministic failure;\n * everything else propagates unchanged. The final fallback at every client-call site: an HTTP status and a fault\n * in transit arrive typed, and anything else (a checksum mismatch, a credentials failure) as the SDK raised it.\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** ({@link saveOnce}) \u2014 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 \u2248 one threshold). SHA-256 is hashed incrementally. Both\n * paths carry `ifGenerationMatch: 0` (create-only-if-absent) \u2192 a conflict is a 412, mapped by the driver to\n * {@link WriteConflictError}. On error the caller invokes {@link abort}.\n *\n * **The simple upload is sent once** ({@link saveOnce}): a replay of one that landed and lost its response would find\n * its own object and read as a lost race, so a transient failure throws {@link TransientError} instead. The resumable\n * upload is a session of requests, which the SDK retries within by asking the session how much it holds; the SDK takes\n * that retry from the client's options, not the call's, so it stays on. Instead, each resumable upload tags its\n * object with a random id in custom metadata, and a `412` on its commit is read back: an object that carries this\n * upload's id is a success, and any other is the lost race. The read is paid only on that `412`.\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 \u2192 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 /**\n * This write's own id, which the resumable upload stores in the object's custom metadata (outside the `.crbm`\n * bytes) so a conflict can be told from a replay of this same write. The simple upload is sent once and needs none.\n */\n private readonly writeId = randomBytes(16).toString('hex');\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 \u2014 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({\n resumable: true,\n ...GcsUploadSink.WRITE_OPTS,\n metadata: {\n ...GcsUploadSink.WRITE_OPTS.metadata,\n metadata: { [WRITE_ID_KEY]: this.writeId },\n },\n });\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); // \u2264 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 const sha256 = this.hash.digest('hex');\n if (this.stream === undefined) {\n // Small object: one simple (non-resumable) upload, sent once \u2014 write-once enforced everywhere, the emulator too.\n await saveOnce(\n this.file,\n concatBytes(this.buffered, this.bufferedLen),\n GcsUploadSink.WRITE_OPTS,\n );\n return { size: this.total, sha256 };\n }\n try {\n if (this.failure !== undefined) throw this.failure;\n this.stream.end();\n await this.done; // rejects if the commit fails (e.g. 412 write-once conflict)\n } catch (err) {\n // The SDK retries a resumable session, so a commit that landed and lost its response can be answered `412` by\n // its own replay. Nothing overwrites a write-once object, so the id it holds says who wrote it. A failed\n // read-back throws, and reaches the caller as `TransientError` when the fault is transient: neither a success\n // nor a conflict.\n if (!isPreconditionFailed(err) || (await this.storedWriteId()) !== this.writeId) throw err;\n }\n return { size: this.total, sha256 };\n }\n\n /** The id the stored object carries in its custom metadata, or `undefined` when it carries none or is gone. */\n private async storedWriteId(): Promise<string | undefined> {\n try {\n const [meta] = await this.file.getMetadata();\n return meta.metadata?.[WRITE_ID_KEY] as string | undefined;\n } catch (err) {\n if (isNotFound(err)) return undefined;\n throw err;\n }\n }\n\n async abort(): Promise<void> {\n if (this.stream !== undefined && !this.stream.destroyed) this.stream.destroy();\n }\n}\n", "/**\n * Logical-ref \u2192 GCS object-name mapping for {@link GcsStorageDriver}.\n *\n * Pure string logic, no SDK dependency \u2014 unit-testable without GCS or an emulator. Uses the **same\n * backend-agnostic `.crbm` object-name scheme** as the S3 + LocalFs storage drivers\n * (`<prefix><ns>/segments/<segment>.<gen>.crbm`), so a segment reads identically whichever storage backend holds\n * it. The default (absent) namespace maps to `_default`, which cannot collide with a real namespace because a\n * caller's `_default` encodes to `%5Fdefault` while the sentinel is emitted literally.\n *\n * The storage-key layout is shared, not copied: `prefixPart` and the name encoders come from\n * `@cloudbitmaps/core/driver-kit`, so a segment written by one driver reads identically under another. Two\n * drivers disagreeing about how a name becomes a key would be a silent cross-driver incompatibility on the\n * same bucket, which is exactly the kind of thing a local copy drifts into.\n */\n\nimport {\n ValidationError,\n encodeNameForKey,\n namespaceKeyPart,\n prefixPart,\n validateSegmentRef,\n} from '@cloudbitmaps/core/driver-kit';\nimport type { GenKey, SegmentRef } from '@cloudbitmaps/core/driver-kit';\n\nconst SUFFIX = '.crbm';\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)}${namespaceKeyPart(ref.namespace)}/segments/${encodeNameForKey(ref.segment)}.`;\n}\n\n/** The full GCS object name of one `.crbm` generation: `<segmentPrefix><gen>.crbm`. */\nexport function storageObjectName(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 \u2014 no leading zeros (so `\u2026s.07.crbm` can't alias `\u2026s.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 (the transient class mirrors S3's).\n *\n * SDK-free + side-effect-free \u2014 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`, \u2026).\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 \u2014 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/** The statuses the SDK's own retry predicate retries (`RETRYABLE_ERR_FN_DEFAULT` in `@google-cloud/storage`). */\nconst SDK_RETRIED_STATUSES = [408, 429, 500, 502, 503, 504];\n\n/** The connection faults the same predicate names, matched on a lower-cased code or reason as it does. */\nfunction isSdkConnectionProblem(reason: string): boolean {\n return (\n reason.includes('eai_again') ||\n reason === 'econnreset' ||\n reason === 'unexpected connection closure' ||\n reason === 'epipe' ||\n reason === 'socket connection timeout'\n );\n}\n\n/**\n * The codes Node gives a connection that failed or was cut off: refused, reset, aborted, timed out, a DNS failure, an\n * unreachable host or network, a broken pipe, and a chunked body cut off mid-stream (`ERR_STREAM_PREMATURE_CLOSE`).\n * Only these: a file the credentials name that is missing or unreadable (`ENOENT`, `EACCES`) and a TLS failure\n * (`EPROTO`, a certificate error) carry codes of the same shape, and would fail the same way again.\n */\nconst TRANSPORT_CODES = new Set([\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'ETIMEDOUT',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'EHOSTUNREACH',\n 'ENETUNREACH',\n 'EPIPE',\n 'ERR_STREAM_PREMATURE_CLOSE',\n]);\n\n/** A download that failed in transit rather than with an HTTP status: one of {@link TRANSPORT_CODES}. */\nexport function isTransportFault(err: unknown): boolean {\n if (httpStatus(err) !== undefined) return false;\n const code = networkCode(err);\n return code !== undefined && TRANSPORT_CODES.has(code);\n}\n\n/**\n * Whether the driver retries a failed download, which it does in the SDK's place: the statuses the SDK's predicate\n * retries (408, 429, 500, 502, 503 and 504, as a number or a string `code`), the connection faults it names in any\n * `errors[].reason`, and every {@link isTransportFault}, which the SDK retried before any response came. A 404, 412,\n * 416 or 403, a 501 or 505, a credentials or TLS failure, and every other answer reach the caller on the first attempt.\n */\nexport function isDownloadRetryable(err: unknown): boolean {\n if (err === null || typeof err !== 'object') return false;\n if (isTransportFault(err)) return true;\n const e = err as { code?: unknown; errors?: unknown };\n if (typeof e.code === 'number' && SDK_RETRIED_STATUSES.includes(e.code)) return true;\n if (typeof e.code === 'string') {\n if (SDK_RETRIED_STATUSES.map(String).includes(e.code)) return true;\n if (isSdkConnectionProblem(e.code.toLowerCase())) return true;\n }\n if (Array.isArray(e.errors)) {\n for (const inner of e.errors as Array<{ reason?: unknown } | null>) {\n const reason = inner?.reason?.toString().toLowerCase();\n if (reason !== undefined && isSdkConnectionProblem(reason)) return true;\n }\n }\n return false;\n}\n\n/**\n * A transient GCS fault that is safe to retry: throttling (429), a request timeout (408), any 5xx, or a dropped/timed-out socket.\n * Excludes the deterministic, caller-meaningful outcomes (412/404/416) \u2014 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 === 408 || status === 429 || (status !== undefined && status >= 500 && status < 600))\n 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 * `retryDownload` \u2014 run one download again after the faults the SDK itself would have retried.\n *\n * The client `GcsStorage` builds sends a download once (see `backend.ts`: a download the SDK retries can crash the\n * process), which also took away the SDK's quiet retry of a connection fault and of a 408, 429, 500, 502, 503 or 504.\n * This puts that retry back inside the driver, so every caller keeps it: the store's admin and write-path registry\n * reads, and a store built with `retry: false`, included. Which faults: `isDownloadRetryable`. How many: three\n * retries, the SDK's default, with full-jitter backoff of at most about 0.7 s a call. A chunk read that the store's\n * own retry also repeats makes at most 16 requests, as it did with the SDK's retry. A 404, 412, 403 and every other\n * answer reach the caller on the first attempt. What the last attempt raised is rethrown as it is, for the driver to\n * classify.\n */\nimport { isDownloadRetryable } from './gcs-errors';\n\nconst RETRIES = 3;\nconst BASE_DELAY_MS = 100;\nconst MAX_DELAY_MS = 1_000;\n\n/** Call `download`, and again after each retryable fault, up to {@link RETRIES} more times; rethrow the last error. */\nexport async function retryDownload<T>(download: () => Promise<T>): Promise<T> {\n for (let attempt = 0; ; attempt++) {\n try {\n return await download();\n } catch (err) {\n if (attempt >= RETRIES || !isDownloadRetryable(err)) throw err;\n const ceiling = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);\n await new Promise((resolve) => setTimeout(resolve, Math.random() * ceiling));\n }\n }\n}\n", "/**\n * `saveOnce` \u2014 upload one object in a single request, sent once, with no SDK retry around it.\n *\n * `file.save()` runs a single-request upload inside a retry loop, and it counts an upload that carries\n * `ifGenerationMatch` as safe to repeat, so after a lost response \u2014 a timeout, a reset connection, a 5xx \u2014 it sends\n * the upload again. When the first had landed, the second meets it: `ifGenerationMatch: 0` finds the object it\n * created, a generation fence finds the generation it replaced, and GCS answers `412`, which the driver can only report\n * as a lost race. The caller is told its write lost when it won. Only the caller can find out which happened, by\n * reading the pointer or listing the generations, so the write is sent once and a transient failure reaches it as\n * `TransientError`.\n *\n * `createWriteStream({ resumable: false })` is that same single multipart request without the loop: the SDK sends a\n * streamed upload with no retries of its own, since a stream it has read cannot be read again. The client is not\n * touched, so a caller's own client keeps its retry options for every other call.\n *\n * **`resumable: false` is load-bearing twice.** It is what makes this one request, and it is the path on which\n * fake-gcs-server enforces `ifGenerationMatch`, so the integration lane tests the precondition rather than passing over\n * a write with none.\n */\nimport { once } from 'node:events';\nimport type { CreateWriteStreamOptions, Storage } from '@google-cloud/storage';\n\ntype GcsFile = ReturnType<ReturnType<Storage['bucket']>['file']>;\n\n/** Upload `body` to `file` in one request, sent once; resolves once GCS has stored it and its checksum matched. */\nexport async function saveOnce(\n file: GcsFile,\n body: Uint8Array,\n options: CreateWriteStreamOptions,\n): Promise<void> {\n const stream = file.createWriteStream({ ...options, resumable: false });\n const stored = once(stream, 'finish'); // rejects on 'error', which is how a failed upload surfaces\n stream.end(body);\n await stored;\n}\n", "/**\n * `readOnce` \u2014 read one object, or one range of it, in a single GET, with the response headers in hand.\n *\n * `file.download()` returns only the bytes. The headers carry what a caller needs alongside them \u2014 the object's\n * `generation` (the registry's version fence) and, on a ranged read, the object's total size in `Content-Range` \u2014 so\n * this reads through `createReadStream`, which announces the raw response before any data flows. Both come from the\n * same response as the bytes, so they describe one observation of the object.\n *\n * **Everything on the wire is untrusted.** The advertised `Content-Length` is checked against `maxBytes` before the\n * first byte is buffered, and the bytes are counted as they arrive, so a response that understates its length (or\n * omits it) still cannot make the caller hold more than `maxBytes`. The headers are returned as received; each caller\n * parses the ones it relies on strictly.\n */\nimport type { Storage } from '@google-cloud/storage';\n\ntype GcsFile = ReturnType<ReturnType<Storage['bucket']>['file']>;\n\nexport interface ObjectRead {\n /** The HTTP status of the response (200 for a whole object, 206 for a range). */\n readonly status: number;\n /** The response headers, lower-cased as Node delivers them. */\n readonly headers: Readonly<Record<string, string | string[] | undefined>>;\n /** The body, at most `maxBytes` long. */\n readonly bytes: Uint8Array;\n}\n\ninterface RawResponse {\n statusCode?: unknown;\n headers?: Record<string, string | string[] | undefined>;\n}\n\n/** The value of a header that must appear once, or `undefined` when it is absent, repeated or empty. */\nexport function singleHeader(headers: ObjectRead['headers'], name: string): string | undefined {\n const value = headers[name];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * GET `file` (with the SDK's `createReadStream` options), buffering at most `maxBytes`. `oversize` builds the error\n * thrown when the advertised or actual length passes the cap, so each caller reports it in its own vocabulary.\n * Transport and HTTP errors reject as the SDK raises them (a 404 carries `code: 404`).\n */\nexport function readOnce(\n file: GcsFile,\n options: { start?: number; end?: number; validation?: false; decompress?: false },\n maxBytes: number,\n oversize: (size: number | undefined) => Error,\n): Promise<ObjectRead> {\n return new Promise<ObjectRead>((resolve, reject) => {\n const stream = file.createReadStream(options);\n const chunks: Buffer[] = [];\n let total = 0;\n let status = 0;\n let headers: ObjectRead['headers'] = {};\n let settled = false;\n\n const fail = (err: unknown): void => {\n if (settled) return;\n settled = true;\n // Deferred: the SDK builds its response pipeline right after announcing the response, and destroying the\n // stream before that makes the pipeline throw out of an event handler, where nothing can catch it, and\n // leaves the socket open. One turn later the same destroy ends the read without a throw. The SDK then\n // destroys its keep-alive agent, which the client's other requests share, so a refused response also resets\n // whatever else that client has in flight; those surface as connection faults, which a read retries.\n setImmediate(() => stream.destroy());\n reject(err);\n };\n\n stream.on('response', (res: RawResponse) => {\n status = typeof res.statusCode === 'number' ? res.statusCode : 0;\n headers = res.headers ?? {};\n if (status < 200 || status > 299) return; // the SDK raises the error itself, with its own status\n const advertised = singleHeader(headers, 'content-length');\n if (advertised !== undefined && /^\\d+$/.test(advertised) && Number(advertised) > maxBytes) {\n fail(oversize(Number(advertised)));\n }\n });\n stream.on('data', (chunk: Buffer) => {\n if (settled) return;\n total += chunk.length;\n if (total > maxBytes) return fail(oversize(undefined));\n chunks.push(chunk);\n });\n stream.on('error', fail);\n stream.on('end', () => {\n if (settled) return;\n settled = true;\n resolve({ status, headers, bytes: new Uint8Array(Buffer.concat(chunks, total)) });\n });\n // A net under the SDK: it reports a cut-off body as an 'error' itself, but a stream that closes without ending\n // or erring was cut off all the same, so it is a dropped connection, and retryable.\n stream.on('close', () =>\n fail(\n Object.assign(new Error('GCS read ended before the response completed'), {\n code: 'ECONNRESET',\n }),\n ),\n );\n });\n}\n", "/**\n * `GcsRegistryDriver` \u2014 an {@link IRegistryDriver} over Google Cloud Storage.\n *\n * Lets a **GCS deployment run on one bucket alone** \u2014 storage `.crbm` generations and the registry in the same\n * place, with no second cloud involved.\n *\n * The protocol (an ABA-safe OCC counter, tombstoning delete, the bounded retry, the key layout) lives once\n * in {@link ObjectStoreRegistry}; this file is only the three I/O calls GCS makes.\n *\n * **The atomic swap is offloaded to GCS's object preconditions.** `ifGenerationMatch: 0` is create-only\n * (\"only if it does not exist\") and `ifGenerationMatch: <generation>` is compare-and-swap \u2014 the same pair S3\n * spells `If-None-Match: *` / `If-Match: <etag>`. A lost race returns `412` \u2192 {@link WriteConflictError}. Each write\n * is one request sent once ({@link saveOnce}), so a `412` means another write got there first, never this one meeting\n * itself after a lost response. A transient failure reaches the caller as {@link TransientError}: the write may or may\n * not have landed, and the caller re-reads the row to learn where it stands.\n *\n * **The version fence is the object's `generation`, not its ETag**: GCS mutates the generation on every\n * overwrite and it is the value `ifGenerationMatch` compares, so it is the only correct fence here. (Note\n * the unrelated collision of vocabulary \u2014 a GCS *object generation* has nothing to do with a CloudBitmaps\n * `.crbm` *generation*.) Reads are strongly consistent, satisfying the registry's `strongRead` contract.\n *\n * **Deployment requirements** (a policy that violates these silently corrupts the registry):\n * - The principal needs `storage.objects.get`, `create`, `update` and `list` on the bucket. Without `list`\n * the registry cannot enumerate, and a missing-object read may surface as `403` rather than `404`.\n * - **Do not apply an Object Lifecycle rule to the `registry/` prefix that deletes a live object** (one on\n * noncurrent versions only is safe), and do not enable a retention\n * policy that blocks overwrite. `delete` tombstones rather than removing, for ABA-safety \u2014 see\n * {@link ObjectStoreRegistry}.\n * - Object versioning is neither required nor used; the driver always reads the live generation. A read is one\n * GET whose `x-goog-generation` header and body describe the same object, so a concurrent overwrite is simply\n * observed as the older or the newer row, never as a torn pair or a missing one.\n */\nimport {\n IntegrityError,\n MAX_ROW_BYTES,\n ObjectStoreRegistry,\n TransientError,\n WriteConflictError,\n normalizeObjectPrefix,\n} from '@cloudbitmaps/core/driver-kit';\nimport type { ObjectRegistryStore, ObjectRow } from '@cloudbitmaps/core/driver-kit';\nimport type { Storage } from '@google-cloud/storage';\nimport { isNotFound, isPreconditionFailed, isTransient, isTransportFault } from './gcs-errors';\nimport { retryDownload } from './download-retry';\nimport { readOnce, singleHeader } from './read-once';\nimport { saveOnce } from './send-once';\n\nexport interface GcsRegistryDriverOptions {\n /** A constructed `@google-cloud/storage` `Storage` client (point `apiEndpoint` at fake-gcs-server locally). */\n readonly storage: Storage;\n /** The client the downloads go through; `storage` when absent (see `GcsStorageDriverOptions.readStorage`). */\n readonly readStorage?: Storage;\n /** Target bucket (must already exist). */\n readonly bucket: string;\n /** Optional object-name prefix under which all registry objects live (e.g. `cloudbitmaps/`). */\n readonly prefix?: string;\n /** Injected clock for `createdAt`/`updatedAt`; defaults to `Date.now`. */\n readonly now?: () => number;\n}\n\n/** The three calls {@link ObjectStoreRegistry} needs, in GCS's dialect. */\nclass GcsStore implements ObjectRegistryStore {\n readonly label = 'GCS';\n\n constructor(\n private readonly storage: Storage,\n private readonly readStorage: Storage,\n private readonly bucket: string,\n ) {}\n\n /** A handle on one registry object. */\n private file(name: string) {\n return this.storage.bucket(this.bucket).file(name);\n }\n\n /** The same file handle on the download client. */\n private downloadable(name: string) {\n return this.readStorage.bucket(this.bucket).file(name);\n }\n\n async read(key: string): Promise<ObjectRow | null> {\n // One GET: the response headers carry the `generation` fence and the length, and the body is the same\n // observation of the object, so the pair cannot straddle a concurrent overwrite.\n let res;\n try {\n res = await retryDownload(() =>\n readOnce(\n this.downloadable(key),\n {},\n MAX_ROW_BYTES,\n (size) =>\n new IntegrityError(\n size === undefined\n ? `registry object exceeds cap ${MAX_ROW_BYTES}B`\n : `registry object ${size}B exceeds cap ${MAX_ROW_BYTES}B`,\n ),\n ),\n );\n } catch (err) {\n if (isNotFound(err)) return null;\n throw mapReadError(err);\n }\n if (res.status !== 200) {\n throw new IntegrityError(`registry read answered HTTP ${res.status}, not 200: ${key}`);\n }\n // The fence is compared by `ifGenerationMatch` on the next write, so it is parsed here, where a header that\n // is missing or malformed can be refused before it is mistaken for a version.\n const generation = singleHeader(res.headers, 'x-goog-generation');\n if (\n generation === undefined ||\n !/^[1-9]\\d*$/.test(generation) ||\n !Number.isSafeInteger(Number(generation))\n ) {\n throw new IntegrityError(`registry read carries no usable x-goog-generation: ${key}`);\n }\n return { bytes: res.bytes, version: generation };\n }\n\n async write(\n key: string,\n body: Uint8Array,\n expect: 'absent' | { version: string },\n ): Promise<void> {\n try {\n // One request, sent once: a replay of a write that landed would fail its own precondition, and read as a lost\n // race. A registry row is a few hundred bytes, so a resumable session would also carry one round trip's worth\n // of data over two.\n await saveOnce(this.file(key), body, {\n contentType: 'application/json',\n preconditionOpts: {\n ifGenerationMatch: expect === 'absent' ? 0 : generationFence(expect.version, key),\n },\n });\n } catch (err) {\n if (isPreconditionFailed(err)) {\n throw new WriteConflictError(`registry OCC conflict for ${key}`);\n }\n throw mapError(err);\n }\n }\n\n async *listKeys(prefix: string): AsyncIterable<string> {\n let pageToken: string | undefined;\n do {\n let files;\n let next: { pageToken?: string } | null | undefined;\n try {\n // Explicit paging rather than autoPaginate: a registry can hold far more rows than a segment holds\n // generations, and draining every page into memory first is what `list()` exists to avoid.\n [files, next] = await this.storage\n .bucket(this.bucket)\n .getFiles({ prefix, autoPaginate: false, maxResults: 1000, pageToken });\n } catch (err) {\n throw mapError(err);\n }\n for (const f of files ?? []) yield f.name;\n pageToken = next?.pageToken;\n } while (pageToken !== undefined);\n }\n}\n\n/**\n * Narrow a version fence back to the number `ifGenerationMatch` takes. The fence always originates as\n * the `x-goog-generation` header in {@link GcsStore.read}, so this cannot fire in practice \u2014 but `Number()` answers `NaN`\n * for anything non-numeric, and `ifGenerationMatch: NaN` serializes to a precondition GCS ignores. That\n * failure is invisible (writes simply stop being fenced), so it is checked rather than assumed.\n */\nfunction generationFence(version: string, key: string): number {\n const generation = Number(version);\n if (!Number.isSafeInteger(generation) || generation <= 0) {\n throw new IntegrityError(`registry version fence is not a GCS generation: ${key}`);\n }\n return generation;\n}\n\n/** A read's error: a connection that failed or was cut off is transient too, after the driver's retries. */\nfunction mapReadError(err: unknown): unknown {\n if (isTransportFault(err)) {\n return new TransientError(`transient GCS fault: ${String((err as { code?: unknown }).code)}`, {\n cause: err,\n });\n }\n return mapError(err);\n}\n\n/** Reclassify a transient GCS fault as a retryable {@link TransientError}; pass everything else through. */\nfunction mapError(err: unknown): unknown {\n if (isTransient(err)) {\n return new TransientError(\n `transient GCS fault: ${(err as { code?: unknown } | null)?.code ?? (err as { name?: string } | null)?.name ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n}\n\nexport class GcsRegistryDriver extends ObjectStoreRegistry {\n constructor(options: GcsRegistryDriverOptions) {\n super(\n new GcsStore(options.storage, options.readStorage ?? options.storage, options.bucket),\n normalizeObjectPrefix(options.prefix),\n options.now ?? ((): number => Date.now()),\n );\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["ValidationError", "ValidationError", "once", "ValidationError", "once", "TransientError", "WriteConflictError", "WriteConflictError", "TransientError", "ValidationError"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `readOnce` — read one object, or one range of it, in a single GET, with the response headers in hand.
|
|
3
|
+
*
|
|
4
|
+
* `file.download()` returns only the bytes. The headers carry what a caller needs alongside them — the object's
|
|
5
|
+
* `generation` (the registry's version fence) and, on a ranged read, the object's total size in `Content-Range` — so
|
|
6
|
+
* this reads through `createReadStream`, which announces the raw response before any data flows. Both come from the
|
|
7
|
+
* same response as the bytes, so they describe one observation of the object.
|
|
8
|
+
*
|
|
9
|
+
* **Everything on the wire is untrusted.** The advertised `Content-Length` is checked against `maxBytes` before the
|
|
10
|
+
* first byte is buffered, and the bytes are counted as they arrive, so a response that understates its length (or
|
|
11
|
+
* omits it) still cannot make the caller hold more than `maxBytes`. The headers are returned as received; each caller
|
|
12
|
+
* parses the ones it relies on strictly.
|
|
13
|
+
*/
|
|
14
|
+
import type { Storage } from '@google-cloud/storage';
|
|
15
|
+
type GcsFile = ReturnType<ReturnType<Storage['bucket']>['file']>;
|
|
16
|
+
export interface ObjectRead {
|
|
17
|
+
/** The HTTP status of the response (200 for a whole object, 206 for a range). */
|
|
18
|
+
readonly status: number;
|
|
19
|
+
/** The response headers, lower-cased as Node delivers them. */
|
|
20
|
+
readonly headers: Readonly<Record<string, string | string[] | undefined>>;
|
|
21
|
+
/** The body, at most `maxBytes` long. */
|
|
22
|
+
readonly bytes: Uint8Array;
|
|
23
|
+
}
|
|
24
|
+
/** The value of a header that must appear once, or `undefined` when it is absent, repeated or empty. */
|
|
25
|
+
export declare function singleHeader(headers: ObjectRead['headers'], name: string): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* GET `file` (with the SDK's `createReadStream` options), buffering at most `maxBytes`. `oversize` builds the error
|
|
28
|
+
* thrown when the advertised or actual length passes the cap, so each caller reports it in its own vocabulary.
|
|
29
|
+
* Transport and HTTP errors reject as the SDK raises them (a 404 carries `code: 404`).
|
|
30
|
+
*/
|
|
31
|
+
export declare function readOnce(file: GcsFile, options: {
|
|
32
|
+
start?: number;
|
|
33
|
+
end?: number;
|
|
34
|
+
validation?: false;
|
|
35
|
+
decompress?: false;
|
|
36
|
+
}, maxBytes: number, oversize: (size: number | undefined) => Error): Promise<ObjectRead>;
|
|
37
|
+
export {};
|
package/dist/registry.d.ts
CHANGED
|
@@ -26,16 +26,17 @@
|
|
|
26
26
|
* noncurrent versions only is safe), and do not enable a retention
|
|
27
27
|
* policy that blocks overwrite. `delete` tombstones rather than removing, for ABA-safety — see
|
|
28
28
|
* {@link ObjectStoreRegistry}.
|
|
29
|
-
* - Object versioning is neither required nor used; the driver always reads the live generation.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* never mistaken for an absent row.
|
|
29
|
+
* - Object versioning is neither required nor used; the driver always reads the live generation. A read is one
|
|
30
|
+
* GET whose `x-goog-generation` header and body describe the same object, so a concurrent overwrite is simply
|
|
31
|
+
* observed as the older or the newer row, never as a torn pair or a missing one.
|
|
33
32
|
*/
|
|
34
33
|
import { ObjectStoreRegistry } from '@cloudbitmaps/core/driver-kit';
|
|
35
34
|
import type { Storage } from '@google-cloud/storage';
|
|
36
35
|
export interface GcsRegistryDriverOptions {
|
|
37
36
|
/** A constructed `@google-cloud/storage` `Storage` client (point `apiEndpoint` at fake-gcs-server locally). */
|
|
38
37
|
readonly storage: Storage;
|
|
38
|
+
/** The client the downloads go through; `storage` when absent (see `GcsStorageDriverOptions.readStorage`). */
|
|
39
|
+
readonly readStorage?: Storage;
|
|
39
40
|
/** Target bucket (must already exist). */
|
|
40
41
|
readonly bucket: string;
|
|
41
42
|
/** Optional object-name prefix under which all registry objects live (e.g. `cloudbitmaps/`). */
|
package/dist/storage.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import type { Storage } from '@google-cloud/storage';
|
|
|
3
3
|
export interface GcsStorageDriverOptions {
|
|
4
4
|
/** A constructed `@google-cloud/storage` `Storage` client (point `apiEndpoint` at fake-gcs-server locally). */
|
|
5
5
|
readonly storage: Storage;
|
|
6
|
+
/**
|
|
7
|
+
* The client the downloads go through; `storage` when absent. `GcsStorage` passes one built with the SDK's request
|
|
8
|
+
* retries off (the read client built in its constructor), because a download the SDK retries can crash the process;
|
|
9
|
+
* the driver retries a download itself instead.
|
|
10
|
+
*/
|
|
11
|
+
readonly readStorage?: Storage;
|
|
6
12
|
/** Target bucket (must already exist). */
|
|
7
13
|
readonly bucket: string;
|
|
8
14
|
/** Optional object-name prefix under which all objects live (e.g. `cloudbitmaps/`). */
|
|
@@ -15,6 +21,7 @@ export interface GcsStorageDriverOptions {
|
|
|
15
21
|
}
|
|
16
22
|
export declare class GcsStorageDriver implements IStorageDriver {
|
|
17
23
|
private readonly storage;
|
|
24
|
+
private readonly readStorage;
|
|
18
25
|
private readonly bucket;
|
|
19
26
|
private readonly prefix;
|
|
20
27
|
private readonly maxObjectBytes;
|
|
@@ -22,6 +29,8 @@ export declare class GcsStorageDriver implements IStorageDriver {
|
|
|
22
29
|
constructor(options: GcsStorageDriverOptions);
|
|
23
30
|
capabilities(): StorageCaps;
|
|
24
31
|
private file;
|
|
32
|
+
/** The same object on the download client. */
|
|
33
|
+
private downloadable;
|
|
25
34
|
putImmutable(key: GenKey, write: (sink: BlobSink) => Promise<void>): Promise<{
|
|
26
35
|
size: number;
|
|
27
36
|
sha256: string;
|
|
@@ -31,6 +40,15 @@ export declare class GcsStorageDriver implements IStorageDriver {
|
|
|
31
40
|
bytes: Uint8Array;
|
|
32
41
|
size: number;
|
|
33
42
|
}>;
|
|
43
|
+
/** The object's size from its metadata, validated. */
|
|
44
|
+
private sizeOf;
|
|
45
|
+
/**
|
|
46
|
+
* The object's total size from a tail response, refused unless every header agrees with the bytes received. A 206
|
|
47
|
+
* must carry `Content-Range: bytes a-b/total` that names exactly the suffix asked for and exactly the bytes held;
|
|
48
|
+
* a 200 (a server that ignored the range) is the whole object, and is accepted only when it fits in the request.
|
|
49
|
+
*/
|
|
50
|
+
private tailSize;
|
|
51
|
+
private badTail;
|
|
34
52
|
delete(key: GenKey): Promise<void>;
|
|
35
53
|
list(ref: SegmentRef): AsyncIterable<GenKey>;
|
|
36
54
|
/** Map GCS read errors to the driver vocabulary; pass everything else through {@link mapError}. */
|
|
@@ -38,8 +56,8 @@ export declare class GcsStorageDriver implements IStorageDriver {
|
|
|
38
56
|
/**
|
|
39
57
|
* Reclassify a transient GCS fault (throttle/5xx/dropped connection) as a retryable {@link TransientError},
|
|
40
58
|
* so the store's read retry can ride it out and a write's caller can tell it from a deterministic failure;
|
|
41
|
-
* everything else propagates unchanged. The final fallback at every client-call site
|
|
42
|
-
*
|
|
59
|
+
* everything else propagates unchanged. The final fallback at every client-call site: an HTTP status and a fault
|
|
60
|
+
* in transit arrive typed, and anything else (a checksum mismatch, a credentials failure) as the SDK raised it.
|
|
43
61
|
*/
|
|
44
62
|
private mapError;
|
|
45
63
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudbitmaps/gcs",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Google Cloud Storage for CloudBitmaps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudbitmaps",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@google-cloud/storage": "^7 || ^8",
|
|
52
|
-
"@cloudbitmaps/core": "^0.11.
|
|
52
|
+
"@cloudbitmaps/core": "^0.11.2"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^22.0.0",
|