@cloudbitmaps/gcs 0.10.0-rc.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,35 +1,23 @@
1
1
  # @cloudbitmaps/gcs
2
2
 
3
- **Google Cloud Storage for [CloudBitmaps](https://github.com/cloudbitmaps/cloudbitmaps).**
3
+ **Google Cloud Storage for [CloudBitmaps](https://github.com/cloudbitmaps/cloudbitmaps).** One bucket holds your generations and their pointers, so there is no
4
+ second service to run.
4
5
 
5
- > **ESM-only, Node ≥ 22.12.** This package ships as ES modules; there is no CommonJS bundle.
6
- > `require()` works on Node 22.12+ through `require(esm)`, but a runner with its own CommonJS loader
7
- > (notably Jest in its default configuration) does not get that and needs `import` instead. On TypeScript,
8
- > a CommonJS project needs `"module": "nodenext"` or `"node20"`. See the
9
- > [repository README](https://github.com/cloudbitmaps/cloudbitmaps#install--entry-points) for the details.
6
+ > **ESM-only, Node ≥ 22.12.** Use `import`; for `require()`, Jest and TypeScript, see
7
+ > [CommonJS, Jest and TypeScript](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#commonjs-jest-and-typescript).
10
8
 
11
- Google Cloud Storage. Compare-and-swap rides GCS object preconditions, so a deployment runs on **one bucket alone** — no second service holds the pointer.
9
+ > Pre-1.0: the API and the on-disk format can still change. These docs describe `main`, ahead of the npm release.
12
10
 
13
11
  ## Install
14
12
 
15
13
  ```bash
16
14
  pnpm add @cloudbitmaps/roaring @cloudbitmaps/gcs
17
- # npm i @cloudbitmaps/roaring @cloudbitmaps/gcs # the same, with npm
18
15
  ```
19
16
 
20
- > **On pnpm 10+, allow the one build script.** pnpm 10 skips dependency build scripts by default, so
21
- > the `roaring` native addon never downloads and the package throws at `import` — while the install
22
- > itself prints a warning and **exits 0**. Add this to your `package.json`, then install:
23
- >
24
- > ```json
25
- > { "pnpm": { "onlyBuiltDependencies": ["roaring"] } }
26
- > ```
27
- >
28
- > pnpm 9 and npm run it already. [Full symptoms and fixes](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
17
+ On pnpm 10 and later, allow the one build script first, or the package throws at `import` while the install exits 0. Put this in your `package.json`: `{ "pnpm": { "onlyBuiltDependencies": ["roaring"] } }`. npm and pnpm 9 need nothing extra. [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
29
18
 
30
- Two packages: the **codec** you want and the **storage** you have. `@google-cloud/storage` (`^7 || ^8`) is a real dependency of
31
- this package, so installing it is the whole step — there is no optional peer to remember. `@cloudbitmaps/core` is one
32
- too, so the engine lands in your tree without you installing it — you never name it yourself.
19
+ `@google-cloud/storage` (`^7 || ^8`) is a real dependency of this package, so installing it is the whole step.
20
+ `@cloudbitmaps/core`, the engine underneath, arrives with it and you never name it.
33
21
 
34
22
  ## Use
35
23
 
@@ -42,26 +30,36 @@ const store = new CloudRoaring({
42
30
  });
43
31
 
44
32
  await store.load({ segment: 'active-users' }, [1, 2, 3]);
45
- const seg = store.segment('active-users');
46
- await seg.has(2); // true
33
+ await store.segment('active-users').has(2); // true
47
34
  ```
48
35
 
49
- `GcsStorage` configures both halves — the immutable generation objects and the registry pointer row — from one set
50
- of values. Need them apart? This package also exports the two drivers and their option types; see the
51
- [API reference](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/api-reference.md).
36
+ It builds its own client from Application Default Credentials. Any other key is refused by name.
52
37
 
53
- ## What this package is
38
+ | Option | What it does |
39
+ |---|---|
40
+ | `bucket` (required) | the bucket |
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 |
43
+ | `projectId`, `apiEndpoint` | build a client for you, such as one for fake-gcs-server; refused beside `client` |
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) |
54
45
 
55
- These drivers move opaque payload bytes, so they are codec-agnostic: the same package serves every codec
56
- flavor. That is why storage is a package rather than a subpath of one — as a subpath, each flavor needed a
57
- re-export barrel per service, and the count multiplied with every codec added.
46
+ ## Before production
58
47
 
59
- Built against `@cloudbitmaps/core/driver-kit`, the declared contract for a driver — the same surface a
60
- third-party driver would use.
48
+ - **This backend is outside the validated envelope.** It passes the same conformance suites as S3, but the proven
49
+ scale and the calibration were done on S3. You are an early user.
50
+ - **Grant the bucket's object permissions.** The library creates, reads, lists and deletes objects:
51
+ `roles/storage.objectAdmin` covers all four.
52
+ - **The write-once guarantee rides GCS preconditions** (`ifGenerationMatch`), so nothing else needs enabling.
53
+ - **Never add a lifecycle rule that expires current objects or the `registry/` prefix.**
54
+ - **A transient failure of a write throws `TransientError`, and the write may or may not have landed.** Re-run the
55
+ call, or check `store.generations(ref)`.
56
+
57
+ The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item, and the ones every backend shares: a request timeout on your client, backups of the data and the registry, and a schedule for your loads.
61
58
 
62
59
  ## Documentation
63
60
 
64
61
  - [Getting started](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md)
62
+ - [Before production](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md)
65
63
  - [API reference](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/api-reference.md)
66
64
  - [Changelog](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/CHANGELOG.md)
67
65
 
package/dist/backend.d.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * See {@link StorageBackend} for why the two halves are configured together rather than separately.
5
5
  *
6
6
  * **A note on the word.** `@google-cloud/storage` names its own client class `Storage`, so the driver option
7
- * that takes it is also called `storage` — which, since our durable tier is now called storage too, reads as
8
- * the same word meaning two things. That is why this class builds the client itself: `new GcsStorage({ bucket })`
7
+ * that takes it is also called `storage` — which, since this library calls its durable tier storage too, reads
8
+ * as the same word meaning two things. That is why this class builds the client itself: `new GcsStorage({ bucket })`
9
9
  * never makes you write `{ storage: storage }`. Supply `client` when you need your own.
10
10
  */
11
11
  import { STORAGE_BACKEND } from '@cloudbitmaps/core/driver-kit';
@@ -16,22 +16,30 @@ export interface GcsStorageOptions {
16
16
  readonly bucket: string;
17
17
  /** Optional object-name prefix under which everything lives — generations and the registry alike. */
18
18
  readonly prefix?: string;
19
- /** A constructed `@google-cloud/storage` client. One is built from the ambient credentials when absent. */
20
- readonly client?: GcsClient;
21
19
  /**
22
- * Not an option — the old `GcsStorageDriver` took the client as `storage`, and a migrating caller keeps the
23
- * name. Typed `never` so the object literal is a compile error, and rejected at runtime for JavaScript
24
- * callers, because silently ignoring it falls back to ambient credentials and the **public** endpoint: for
25
- * anyone whose client pointed at an emulator, that is production traffic from a wiring typo.
20
+ * A constructed `@google-cloud/storage` client. One is built from the ambient credentials when absent. Its retry
21
+ * options apply to every request except the single-request conditional writes, which are sent once whatever they
22
+ * say.
26
23
  */
27
- readonly storage?: never;
28
- /** Project id for the client built when `client` is absent. Falls back to the SDK's own resolution. */
24
+ readonly client?: GcsClient;
25
+ /** Project id for the client built when `client` is absent (refused beside `client`). Falls back to the SDK's own resolution. */
29
26
  readonly projectId?: string;
30
- /** Endpoint override — point it at fake-gcs-server locally. Ignored when `client` is supplied. */
27
+ /** Endpoint override — point it at fake-gcs-server locally. Refused beside `client`, which carries its own. */
31
28
  readonly apiEndpoint?: string;
29
+ /** Largest object the backend will write and advertise (default = GCS's 5 TiB max). Must be a positive safe integer. */
30
+ readonly maxObjectBytes?: number;
31
+ /** Bytes at/under which a single non-resumable upload is used instead of a resumable stream (default 8 MiB).
32
+ * Must be a positive safe integer. */
33
+ readonly simpleUploadThresholdBytes?: number;
32
34
  /** Injected clock for the registry's `createdAt`/`updatedAt`; defaults to `Date.now`. */
33
35
  readonly now?: () => number;
34
36
  }
37
+ /**
38
+ * The keys `new GcsStorage(options)` takes. Any other is refused by name rather than ignored: an ignored client key
39
+ * — `storage`, as the lower-level storage driver calls it — falls back to ambient credentials and the **public** endpoint, and
40
+ * for a client pointed at an emulator that is production traffic from a wiring typo.
41
+ */
42
+ export declare const GCS_STORAGE_OPTION_KEYS: readonly ["bucket", "prefix", "client", "projectId", "apiEndpoint", "maxObjectBytes", "simpleUploadThresholdBytes", "now"];
35
43
  export declare class GcsStorage implements StorageBackend {
36
44
  /** Cross-bundle brand, stamped non-enumerably in the constructor so a spread cannot carry it. */
37
45
  readonly [STORAGE_BACKEND]: true;
package/dist/index.d.ts CHANGED
@@ -12,17 +12,13 @@
12
12
  * ```
13
13
  *
14
14
  * These drivers move opaque payload bytes, so they are codec-agnostic: the same package serves every
15
- * flavor. That is why they are a package rather than a subpath of one — as a subpath, each flavor needed a
16
- * re-export barrel per service, and the count multiplied with every new codec.
15
+ * flavor. That is why they are a package rather than a subpath of one — as a subpath, each flavor would need a
16
+ * re-export barrel per service, and the count would multiply with every new codec.
17
17
  *
18
18
  * The engine, the `.crbm` format and the ports live in `@cloudbitmaps/core`, which is a real dependency of
19
19
  * THIS package: it lands in your tree without you installing it, and you never name it yourself. What this
20
20
  * package builds against is `@cloudbitmaps/core/driver-kit`, the declared contract for a driver — the same
21
21
  * surface a third-party driver would use.
22
22
  */
23
- export { GcsStorageDriver } from './storage.js';
24
- export type { GcsStorageDriverOptions } from './storage.js';
25
- export { GcsRegistryDriver } from './registry.js';
26
- export type { GcsRegistryDriverOptions } from './registry.js';
27
23
  export { GcsStorage } from './backend.js';
28
24
  export type { GcsStorageOptions } from './backend.js';
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ // src/backend.ts
2
+ import { ValidationError as ValidationError3, brandAsBackend } from "@cloudbitmaps/core/driver-kit";
3
+ import { Storage as GcsClient } from "@google-cloud/storage";
4
+
1
5
  // src/storage.ts
2
6
  import {
3
7
  NotFoundError,
@@ -8,8 +12,8 @@ import {
8
12
  isValidationError,
9
13
  isWriteConflictError
10
14
  } from "@cloudbitmaps/core/driver-kit";
11
- import { createHash } from "node:crypto";
12
- import { once } from "node:events";
15
+ import { createHash, randomBytes } from "node:crypto";
16
+ import { once as once2 } from "node:events";
13
17
 
14
18
  // src/keys.ts
15
19
  import {
@@ -82,8 +86,18 @@ function isTransient(err) {
82
86
  return net === "ECONNRESET" || net === "ETIMEDOUT" || net === "ECONNREFUSED" || net === "EPIPE" || net === "EAI_AGAIN" || net === "ENOTFOUND";
83
87
  }
84
88
 
89
+ // src/send-once.ts
90
+ import { once } from "node:events";
91
+ async function saveOnce(file, body, options) {
92
+ const stream = file.createWriteStream({ ...options, resumable: false });
93
+ const stored = once(stream, "finish");
94
+ stream.end(body);
95
+ await stored;
96
+ }
97
+
85
98
  // src/storage.ts
86
99
  var DEFAULT_MAX_OBJECT_BYTES = 5 * 1024 * 1024 * 1024 * 1024;
100
+ var WRITE_ID_KEY = "cbwid";
87
101
  var DEFAULT_UPLOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
88
102
  var GcsStorageDriver = class {
89
103
  storage;
@@ -95,6 +109,14 @@ var GcsStorageDriver = class {
95
109
  this.storage = options.storage;
96
110
  this.bucket = options.bucket;
97
111
  this.prefix = normalizeGcsPrefix(options.prefix);
112
+ for (const [name, value] of [
113
+ ["maxObjectBytes", options.maxObjectBytes],
114
+ ["simpleUploadThresholdBytes", options.simpleUploadThresholdBytes]
115
+ ]) {
116
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) {
117
+ throw new ValidationError2(`${name} must be a positive safe integer; got ${value}`);
118
+ }
119
+ }
98
120
  this.maxObjectBytes = options.maxObjectBytes ?? DEFAULT_MAX_OBJECT_BYTES;
99
121
  this.threshold = options.simpleUploadThresholdBytes ?? DEFAULT_UPLOAD_THRESHOLD_BYTES;
100
122
  }
@@ -191,9 +213,10 @@ var GcsStorageDriver = class {
191
213
  return this.mapError(err);
192
214
  }
193
215
  /**
194
- * Reclassify a transient GCS fault (throttle/5xx/dropped connection) as a retryable {@link TransientError}
195
- * so the retry decorator can ride it out; everything else propagates unchanged. The final fallback at every
196
- * client-call site, so callers + the decorator only ever see typed errors.
216
+ * Reclassify a transient GCS fault (throttle/5xx/dropped connection) as a retryable {@link TransientError},
217
+ * 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, so callers and the read
219
+ * retry only ever see typed errors.
197
220
  */
198
221
  mapError(err) {
199
222
  if (isTransient(err)) {
@@ -236,6 +259,11 @@ var GcsUploadSink = class _GcsUploadSink {
236
259
  // write-once: create only if absent
237
260
  metadata: { contentType: "application/octet-stream" }
238
261
  };
262
+ /**
263
+ * This write's own id, which the resumable upload stores in the object's custom metadata (outside the `.crbm`
264
+ * bytes) so a conflict can be told from a replay of this same write. The simple upload is sent once and needs none.
265
+ */
266
+ writeId = randomBytes(16).toString("hex");
239
267
  async write(bytes) {
240
268
  if (this.failure !== void 0) throw this.failure;
241
269
  if (bytes.length === 0) return;
@@ -245,7 +273,7 @@ var GcsUploadSink = class _GcsUploadSink {
245
273
  }
246
274
  this.hash.update(bytes);
247
275
  if (this.stream !== void 0) {
248
- if (!this.stream.write(bytes)) await once(this.stream, "drain");
276
+ if (!this.stream.write(bytes)) await once2(this.stream, "drain");
249
277
  return;
250
278
  }
251
279
  this.buffered.push(bytes);
@@ -254,9 +282,16 @@ var GcsUploadSink = class _GcsUploadSink {
254
282
  }
255
283
  /** Cross into resumable streaming: open the stream, flush the buffered bytes, keep only ~one threshold resident. */
256
284
  startResumable() {
257
- const stream = this.file.createWriteStream({ resumable: true, ..._GcsUploadSink.WRITE_OPTS });
285
+ const stream = this.file.createWriteStream({
286
+ resumable: true,
287
+ ..._GcsUploadSink.WRITE_OPTS,
288
+ metadata: {
289
+ ..._GcsUploadSink.WRITE_OPTS.metadata,
290
+ metadata: { [WRITE_ID_KEY]: this.writeId }
291
+ }
292
+ });
258
293
  this.stream = stream;
259
- this.done = once(stream, "finish");
294
+ this.done = once2(stream, "finish");
260
295
  stream.on("error", (e) => {
261
296
  this.failure ??= e;
262
297
  });
@@ -267,19 +302,34 @@ var GcsUploadSink = class _GcsUploadSink {
267
302
  stream.write(buf);
268
303
  }
269
304
  async finish() {
270
- if (this.failure !== void 0) throw this.failure;
271
305
  const sha256 = this.hash.digest("hex");
272
306
  if (this.stream === void 0) {
273
- await this.file.save(concatBytes(this.buffered, this.bufferedLen), {
274
- resumable: false,
275
- ..._GcsUploadSink.WRITE_OPTS
276
- });
307
+ await saveOnce(
308
+ this.file,
309
+ concatBytes(this.buffered, this.bufferedLen),
310
+ _GcsUploadSink.WRITE_OPTS
311
+ );
277
312
  return { size: this.total, sha256 };
278
313
  }
279
- this.stream.end();
280
- await this.done;
314
+ try {
315
+ if (this.failure !== void 0) throw this.failure;
316
+ this.stream.end();
317
+ await this.done;
318
+ } catch (err) {
319
+ if (!isPreconditionFailed(err) || await this.storedWriteId() !== this.writeId) throw err;
320
+ }
281
321
  return { size: this.total, sha256 };
282
322
  }
323
+ /** The id the stored object carries in its custom metadata, or `undefined` when it carries none or is gone. */
324
+ async storedWriteId() {
325
+ try {
326
+ const [meta] = await this.file.getMetadata();
327
+ return meta.metadata?.[WRITE_ID_KEY];
328
+ } catch (err) {
329
+ if (isNotFound(err)) return void 0;
330
+ throw err;
331
+ }
332
+ }
283
333
  async abort() {
284
334
  if (this.stream !== void 0 && !this.stream.destroyed) this.stream.destroy();
285
335
  }
@@ -331,14 +381,8 @@ var GcsStore = class {
331
381
  }
332
382
  async write(key, body, expect) {
333
383
  try {
334
- await this.file(key).save(Buffer.from(body), {
384
+ await saveOnce(this.file(key), body, {
335
385
  contentType: "application/json",
336
- // `resumable: false` is load-bearing, not a tuning knob. `save()` otherwise opens a resumable
337
- // session, and a registry row is a few hundred bytes — one round trip's worth of data carried over
338
- // two. It also costs the fence: fake-gcs-server does not enforce `ifGenerationMatch` on the
339
- // resumable path, so the whole integration lane would pass over a registry with no
340
- // compare-and-swap at all. `GcsStorageDriver` pins the same flag for the same reason.
341
- resumable: false,
342
386
  preconditionOpts: {
343
387
  ifGenerationMatch: expect === "absent" ? 0 : generationFence(expect.version, key)
344
388
  }
@@ -392,29 +436,67 @@ var GcsRegistryDriver = class extends ObjectStoreRegistry {
392
436
  };
393
437
 
394
438
  // src/backend.ts
395
- import { ValidationError as ValidationError3, brandAsBackend } from "@cloudbitmaps/core/driver-kit";
396
- import { Storage as GcsClient } from "@google-cloud/storage";
439
+ var GCS_STORAGE_OPTION_KEYS = [
440
+ "bucket",
441
+ "prefix",
442
+ "client",
443
+ "projectId",
444
+ "apiEndpoint",
445
+ "maxObjectBytes",
446
+ "simpleUploadThresholdBytes",
447
+ "now"
448
+ ];
449
+ var CLIENT_SETTINGS = ["projectId", "apiEndpoint"];
450
+ function refuseUnknown(name, options, keys, hint) {
451
+ if (options === null || typeof options !== "object") {
452
+ throw new ValidationError3(
453
+ `${name} needs an options object \u2014 got ${options === null ? "null" : typeof options}`
454
+ );
455
+ }
456
+ const unknown = Object.keys(options).filter((k) => !keys.includes(k));
457
+ if (unknown.length > 0) {
458
+ const list = (ks) => ks.map((k) => `\`${k}\``).join(", ");
459
+ throw new ValidationError3(
460
+ `${name} does not take ${list(unknown)}. It takes ${list(keys)}; ${hint}.`
461
+ );
462
+ }
463
+ }
397
464
  var GcsStorage = class {
398
465
  storage;
399
466
  registry;
400
467
  /** The client both halves share — built here unless one was supplied. */
401
468
  client;
402
469
  constructor(options) {
403
- if (options.storage !== void 0) {
404
- throw new ValidationError3(
405
- "GcsStorage takes the @google-cloud/storage client as `client`, not `storage` (which was the old GcsStorageDriver option). Rename it, or omit it and let the backend build one."
406
- );
470
+ refuseUnknown(
471
+ "GcsStorage",
472
+ options,
473
+ GCS_STORAGE_OPTION_KEYS,
474
+ "a @google-cloud/storage client goes in `client`"
475
+ );
476
+ if (options.client !== void 0 && options.client !== null) {
477
+ const ignored = CLIENT_SETTINGS.filter((k) => options[k] !== void 0);
478
+ if (ignored.length > 0) {
479
+ throw new ValidationError3(
480
+ `GcsStorage takes \`client\` OR ${CLIENT_SETTINGS.map((k) => `\`${k}\``).join(" / ")}, not both \u2014 got \`client\` with ${ignored.map((k) => `\`${k}\``).join(", ")}; the \`client\` already carries them, so configure them on the client, or drop \`client\``
481
+ );
482
+ }
483
+ this.client = options.client;
484
+ } else {
485
+ this.client = new GcsClient({
486
+ ...options.projectId === void 0 ? {} : { projectId: options.projectId },
487
+ ...options.apiEndpoint === void 0 ? {} : { apiEndpoint: options.apiEndpoint }
488
+ });
407
489
  }
408
- this.client = options.client ?? new GcsClient({
409
- ...options.projectId === void 0 ? {} : { projectId: options.projectId },
410
- ...options.apiEndpoint === void 0 ? {} : { apiEndpoint: options.apiEndpoint }
411
- });
412
490
  const shared = {
413
491
  storage: this.client,
414
492
  bucket: options.bucket,
415
493
  ...options.prefix === void 0 ? {} : { prefix: options.prefix }
416
494
  };
417
- this.storage = new GcsStorageDriver(shared);
495
+ this.storage = new GcsStorageDriver({
496
+ ...shared,
497
+ ...options.maxObjectBytes === void 0 ? {} : { maxObjectBytes: options.maxObjectBytes },
498
+ ...options.simpleUploadThresholdBytes === void 0 ? {} : { simpleUploadThresholdBytes: options.simpleUploadThresholdBytes }
499
+ });
418
500
  this.registry = new GcsRegistryDriver({
419
501
  ...shared,
420
502
  ...options.now === void 0 ? {} : { now: options.now }
@@ -423,8 +505,6 @@ var GcsStorage = class {
423
505
  }
424
506
  };
425
507
  export {
426
- GcsRegistryDriver,
427
- GcsStorage,
428
- GcsStorageDriver
508
+ GcsStorage
429
509
  };
430
510
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/storage.ts", "../src/keys.ts", "../src/gcs-errors.ts", "../src/registry.ts", "../src/backend.ts"],
4
- "sourcesContent": ["/**\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, 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';\n\n/** Default object ceiling: GCS's 5 TiB per-object hard max. Set lower to fail fast on a runaway write. */\nconst DEFAULT_MAX_OBJECT_BYTES = 5 * 1024 * 1024 * 1024 * 1024;\n\n/**\n * Objects at/under this size are uploaded in a **single simple (non-resumable) request**; larger ones switch to\n * a **resumable stream** (constant memory). Mirrors S3's single-PUT-vs-multipart split \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. `cloudroaring/`). */\n readonly prefix?: string;\n /** Largest object this driver will write/advertise (default = GCS's 5 TiB max). */\n readonly maxObjectBytes?: number;\n /** Bytes at/under which a single non-resumable upload is used instead of a resumable stream (default 8 MiB). */\n readonly simpleUploadThresholdBytes?: number;\n}\n\nexport class 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 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 (add/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 retry decorator can ride it out; everything else propagates unchanged. The final fallback at every\n * client-call site, so callers + the decorator only ever see typed errors.\n */\n private mapError(err: unknown): unknown {\n if (isTransient(err)) {\n return new TransientError(\n `transient GCS fault: ${(err as { code?: unknown } | null)?.code ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n }\n}\n\ntype GcsFile = ReturnType<ReturnType<Storage['bucket']>['file']>;\n\n/** Concatenate byte chunks of known total length into one buffer. */\nfunction concatBytes(parts: readonly Uint8Array[], total: number): Uint8Array {\n const out = new Uint8Array(total);\n let offset = 0;\n for (const p of parts) {\n out.set(p, offset);\n offset += p.length;\n }\n return out;\n}\n\n/**\n * {@link BlobSink} that uploads one GCS object write-once. It **buffers up to a threshold**: an object that\n * finishes at/under it is committed in a **single simple (non-resumable) request** (`file.save`) \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 */\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 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({ resumable: true, ...GcsUploadSink.WRITE_OPTS });\n this.stream = stream;\n this.done = once(stream, 'finish'); // resolves on a clean commit; rejects on 'error' (e.g. 412)\n stream.on('error', (e: unknown) => {\n this.failure ??= e; // a persistent listener so a stray 'error' is never an unhandled 'error' event\n });\n this.done.catch(() => undefined); // finish() awaits this; guard against unhandled rejection until then\n const buf = concatBytes(this.buffered, this.bufferedLen);\n this.buffered.length = 0;\n this.bufferedLen = 0;\n stream.write(buf); // \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 if (this.failure !== undefined) throw this.failure;\n const sha256 = this.hash.digest('hex');\n if (this.stream === undefined) {\n // Small object: a single simple (non-resumable) upload \u2014 write-once enforced everywhere (incl. the emulator).\n await this.file.save(concatBytes(this.buffered, this.bufferedLen), {\n resumable: false,\n ...GcsUploadSink.WRITE_OPTS,\n });\n return { size: this.total, sha256 };\n }\n this.stream.end();\n await this.done; // rejects if the commit fails (e.g. 412 write-once conflict)\n return { size: this.total, sha256 };\n }\n\n async abort(): Promise<void> {\n if (this.stream !== undefined && !this.stream.destroyed) this.stream.destroy();\n }\n}\n", "/**\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 * `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. Before this existed, a GCS user had to point the registry at a\n * separate AWS-hosted table, which meant holding an AWS account purely to store the pointer that says which\n * generation is current.\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}.\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**, 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';\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. `cloudroaring/`). */\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 await this.file(key).save(Buffer.from(body), {\n contentType: 'application/json',\n // `resumable: false` is load-bearing, not a tuning knob. `save()` otherwise opens a resumable\n // session, and a registry row is a few hundred bytes \u2014 one round trip's worth of data carried over\n // two. It also costs the fence: fake-gcs-server does not enforce `ifGenerationMatch` on the\n // resumable path, so the whole integration lane would pass over a registry with no\n // compare-and-swap at all. `GcsStorageDriver` pins the same flag for the same reason.\n resumable: false,\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", "/**\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 our durable tier is now called storage too, reads as\n * 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 /** A constructed `@google-cloud/storage` client. One is built from the ambient credentials when absent. */\n readonly client?: GcsClient;\n /**\n * Not an option \u2014 the old `GcsStorageDriver` took the client as `storage`, and a migrating caller keeps the\n * name. Typed `never` so the object literal is a compile error, and rejected at runtime for JavaScript\n * callers, because silently ignoring it falls back to ambient credentials and the **public** endpoint: for\n * anyone whose client pointed at an emulator, that is production traffic from a wiring typo.\n */\n readonly storage?: never;\n /** Project id for the client built when `client` is absent. Falls back to the SDK's own resolution. */\n readonly projectId?: string;\n /** Endpoint override \u2014 point it at fake-gcs-server locally. Ignored when `client` is supplied. */\n readonly apiEndpoint?: string;\n /** Injected clock for the registry's `createdAt`/`updatedAt`; defaults to `Date.now`. */\n readonly now?: () => number;\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 if ((options as { storage?: unknown }).storage !== undefined) {\n throw new ValidationError(\n 'GcsStorage takes the @google-cloud/storage client as `client`, not `storage` (which was the old ' +\n 'GcsStorageDriver option). Rename it, or omit it and let the backend build one.',\n );\n }\n this.client =\n options.client ??\n new GcsClient({\n ...(options.projectId === undefined ? {} : { projectId: options.projectId }),\n ...(options.apiEndpoint === undefined ? {} : { apiEndpoint: options.apiEndpoint }),\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(shared);\n this.registry = new GcsRegistryDriver({\n ...shared,\n ...(options.now === undefined ? {} : { now: options.now }),\n });\n brandAsBackend(this);\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["ValidationError", "ValidationError", "TransientError", "WriteConflictError", "WriteConflictError", "TransientError", "ValidationError", "ValidationError"]
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": "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",
6
+ "names": ["ValidationError", "ValidationError", "once", "ValidationError", "once", "TransientError", "WriteConflictError", "WriteConflictError", "TransientError", "ValidationError"]
7
7
  }
@@ -2,16 +2,18 @@
2
2
  * `GcsRegistryDriver` — an {@link IRegistryDriver} over Google Cloud Storage.
3
3
  *
4
4
  * Lets a **GCS deployment run on one bucket alone** — storage `.crbm` generations and the registry in the same
5
- * place, with no second cloud involved. Before this existed, a GCS user had to point the registry at a
6
- * separate AWS-hosted table, which meant holding an AWS account purely to store the pointer that says which
7
- * generation is current.
5
+ * place, with no second cloud involved.
8
6
  *
9
7
  * The protocol (an ABA-safe OCC counter, tombstoning delete, the bounded retry, the key layout) lives once
10
8
  * in {@link ObjectStoreRegistry}; this file is only the three I/O calls GCS makes.
11
9
  *
12
10
  * **The atomic swap is offloaded to GCS's object preconditions.** `ifGenerationMatch: 0` is create-only
13
11
  * ("only if it does not exist") and `ifGenerationMatch: <generation>` is compare-and-swap — the same pair S3
14
- * spells `If-None-Match: *` / `If-Match: <etag>`. A lost race returns `412` → {@link WriteConflictError}.
12
+ * spells `If-None-Match: *` / `If-Match: <etag>`. A lost race returns `412` → {@link WriteConflictError}. Each write
13
+ * is one request sent once ({@link saveOnce}), so a `412` means another write got there first, never this one meeting
14
+ * itself after a lost response. A transient failure reaches the caller as {@link TransientError}: the write may or may
15
+ * not have landed, and the caller re-reads the row to learn where it stands.
16
+ *
15
17
  * **The version fence is the object's `generation`, not its ETag**: GCS mutates the generation on every
16
18
  * overwrite and it is the value `ifGenerationMatch` compares, so it is the only correct fence here. (Note
17
19
  * the unrelated collision of vocabulary — a GCS *object generation* has nothing to do with a CloudBitmaps
@@ -20,7 +22,8 @@
20
22
  * **Deployment requirements** (a policy that violates these silently corrupts the registry):
21
23
  * - The principal needs `storage.objects.get`, `create`, `update` and `list` on the bucket. Without `list`
22
24
  * the registry cannot enumerate, and a missing-object read may surface as `403` rather than `404`.
23
- * - **Do not apply an Object Lifecycle rule to the `registry/` prefix**, and do not enable a retention
25
+ * - **Do not apply an Object Lifecycle rule to the `registry/` prefix that deletes a live object** (one on
26
+ * noncurrent versions only is safe), and do not enable a retention
24
27
  * policy that blocks overwrite. `delete` tombstones rather than removing, for ABA-safety — see
25
28
  * {@link ObjectStoreRegistry}.
26
29
  * - Object versioning is neither required nor used; the driver always reads the live generation. Because a
@@ -35,7 +38,7 @@ export interface GcsRegistryDriverOptions {
35
38
  readonly storage: Storage;
36
39
  /** Target bucket (must already exist). */
37
40
  readonly bucket: string;
38
- /** Optional object-name prefix under which all registry objects live (e.g. `cloudroaring/`). */
41
+ /** Optional object-name prefix under which all registry objects live (e.g. `cloudbitmaps/`). */
39
42
  readonly prefix?: string;
40
43
  /** Injected clock for `createdAt`/`updatedAt`; defaults to `Date.now`. */
41
44
  readonly now?: () => number;
@@ -0,0 +1,5 @@
1
+ import type { CreateWriteStreamOptions, Storage } from '@google-cloud/storage';
2
+ type GcsFile = ReturnType<ReturnType<Storage['bucket']>['file']>;
3
+ /** Upload `body` to `file` in one request, sent once; resolves once GCS has stored it and its checksum matched. */
4
+ export declare function saveOnce(file: GcsFile, body: Uint8Array, options: CreateWriteStreamOptions): Promise<void>;
5
+ export {};
package/dist/storage.d.ts CHANGED
@@ -5,11 +5,12 @@ export interface GcsStorageDriverOptions {
5
5
  readonly storage: Storage;
6
6
  /** Target bucket (must already exist). */
7
7
  readonly bucket: string;
8
- /** Optional object-name prefix under which all objects live (e.g. `cloudroaring/`). */
8
+ /** Optional object-name prefix under which all objects live (e.g. `cloudbitmaps/`). */
9
9
  readonly prefix?: string;
10
- /** Largest object this driver will write/advertise (default = GCS's 5 TiB max). */
10
+ /** Largest object this driver will write/advertise (default = GCS's 5 TiB max). Must be a positive safe integer. */
11
11
  readonly maxObjectBytes?: number;
12
- /** Bytes at/under which a single non-resumable upload is used instead of a resumable stream (default 8 MiB). */
12
+ /** Bytes at/under which a single non-resumable upload is used instead of a resumable stream (default 8 MiB).
13
+ * Must be a positive safe integer. */
13
14
  readonly simpleUploadThresholdBytes?: number;
14
15
  }
15
16
  export declare class GcsStorageDriver implements IStorageDriver {
@@ -35,9 +36,10 @@ export declare class GcsStorageDriver implements IStorageDriver {
35
36
  /** Map GCS read errors to the driver vocabulary; pass everything else through {@link mapError}. */
36
37
  private mapReadError;
37
38
  /**
38
- * Reclassify a transient GCS fault (throttle/5xx/dropped connection) as a retryable {@link TransientError}
39
- * so the retry decorator can ride it out; everything else propagates unchanged. The final fallback at every
40
- * client-call site, so callers + the decorator only ever see typed errors.
39
+ * Reclassify a transient GCS fault (throttle/5xx/dropped connection) as a retryable {@link TransientError},
40
+ * 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, so callers and the read
42
+ * retry only ever see typed errors.
41
43
  */
42
44
  private mapError;
43
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbitmaps/gcs",
3
- "version": "0.10.0-rc.0",
3
+ "version": "0.11.0",
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.10.0"
52
+ "@cloudbitmaps/core": "^0.11.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^22.0.0",