@cloudbitmaps/s3 0.22.0 → 0.23.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
@@ -14,6 +14,8 @@ service to run.
14
14
  pnpm add @cloudbitmaps/roaring @cloudbitmaps/s3
15
15
  ```
16
16
 
17
+ Install the same version of every `@cloudbitmaps/*` package: they release together, and each pins the exact version of `core` it was built with.
18
+
17
19
  `roaring` needs its one install script to fetch its native binary, and newer clients run a dependency's script only if you allow it. Allow it before you install:
18
20
 
19
21
  - **npm 12:** add `{ "allowScripts": { "roaring": true } }` to your `package.json`. Without it the install exits 0 and the package throws at `import`. npm 11 runs the script but warns until you allow it the same way.
@@ -28,6 +30,7 @@ pnpm add @cloudbitmaps/roaring @cloudbitmaps/s3
28
30
 
29
31
  ## Use
30
32
 
33
+ <!-- typecheck -->
31
34
  ```ts
32
35
  import { CloudRoaring } from '@cloudbitmaps/roaring';
33
36
  import { S3Storage } from '@cloudbitmaps/s3';
@@ -49,12 +52,17 @@ It builds its own client from your usual AWS credentials. Any other key is refus
49
52
  | `client` | your own `S3Client`; it carries its own region, endpoint and credentials |
50
53
  | `region`, `endpoint`, `pathStyle`, `credentials` | build a client for you, such as one for MinIO; refused beside `client` |
51
54
  | `maxSockets` | most sockets the client you do not pass opens at once, for `https` and plain `http` alike; a positive integer, default 128 (the SDK's own is 50; if a future SDK changes its handler, the built client keeps the SDK's own limit); refused beside `client` |
52
- | `partBytes`, `maxObjectBytes` | multipart sizing: part size (default 8 MiB; the upload buffers one part at a time) and the largest object (about 80 GiB by default, up to S3's 5 TiB). A cap above 5 TiB, or a part above S3's 5 GiB part limit, is a `ValidationError` |
55
+ | `partBytes`, `maxObjectBytes` | multipart sizing: part size (default 8 MiB; the upload buffers one part at a time and copies it into one body to send it, so it holds up to about twice the part size at a flush, 16 MiB at the default, derived from the driver's code) and the largest object (about 78 GiB by default, up to S3's 5 TiB). A cap above 5 TiB, or a part above S3's 5 GiB part limit, is a `ValidationError` |
53
56
  | `readTimeoutMs` | how long one read (a `GetObject` or `HeadObject`, its body included) may take before it throws `TransientError` and the store retries it; `0`, the default, sets no timeout. Writes are not timed |
54
57
  | `conditionalDelete` | whether the store applies `If-Match` on a `DeleteObject`: the registry then removes a deleted row rather than leaving a tombstone, and the storage half deletes an erasure's holder only while it is the object the erasure read, by its ETag. On by default when the host the client resolves is an AWS S3 host, however its endpoint was set (`endpoint`, `AWS_ENDPOINT_URL_S3`, `AWS_ENDPOINT_URL`, the shared config file); off for any other host: set it for an S3-compatible store only once you know it applies the header (MinIO ignores it) |
55
58
 
56
59
  ## Before production
57
60
 
61
+ - **Run `await store.preflight()` once before the first load.** It reads the registry, writes one probe object, writes the
62
+ same key again (which must be refused), reads it back, lists it, deletes it and checks the lifecycle rules, with
63
+ `s3:ListBucket`, `s3:GetObject`, `s3:PutObject` and `s3:DeleteObject`, and `s3:GetLifecycleConfiguration` for the last. It does
64
+ not check `s3:AbortMultipartUpload`, the registry's `If-Match`, or a read after a delete, among others: [the full list](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md#what-preflight-does-not-check). It
65
+ shows an SDK of 3.640.0 or older as a `write-once` problem, and cannot see one from 3.641.0 to 3.699.0, so the floor below stays yours to check. The probe sits at `<prefix>cbm.due.preflight/`, outside your application's namespaces: a grant scoped per namespace shows as a `write` problem, and as a `registry` problem too when the registry grant is scoped per namespace.
58
66
  - **The bucket must honor conditional writes**: `If-None-Match: *` for a write-once generation and `If-Match` for the
59
67
  pointer. AWS S3 does, and the test suite runs against MinIO. Another S3-compatible service that ignores the headers
60
68
  turns write-once into overwrite, so check yours before you depend on it. It must also be strongly consistent: a read
@@ -74,18 +82,26 @@ It builds its own client from your usual AWS credentials. Any other key is refus
74
82
  `registry/` prefix. An expired registry row takes the segment's pointer with it, and the token every publish is fenced
75
83
  on, so its generations look unreferenced; an expired generation makes a live segment unreadable
76
84
  ([bucket lifecycle](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md#bucket-lifecycle)).
77
- - **A transient failure of a write throws `TransientError`, and the write may or may not have landed.** Re-run the
78
- call, or check `store.generations(ref)`. The SDK's own retry is off for conditional writes, so a write that landed
79
- is not reported as a conflict. The driver sends each registry write once; a load that gets no answer reads the row and
80
- sends a fresh compare-and-swap from it, at most three times. A generation's object is sent again, up to three more
81
- times, only after a `503` (`SlowDown` is the usual one); it carries a random id in its user metadata
82
- (`x-amz-meta-cbwid`), so a first send that landed is told from another writer's object. A load whose row writes all go
83
- unanswered throws `TransientError` and keeps its object. The waits before an object's re-sends use real timers: the
84
- store's injected clock does not reach them, and `S3Storage` takes no clock, so a test that throttles one object
85
- write three times waits about 3.5 s at most (the publish's waits before a fresh row write do use the store's
86
- clock). **A bare `429`** (AWS S3 does not send one; some
87
- S3-compatible services do) **is not retried and is not classified transient**: it surfaces as the SDK's own error, so a
88
- layer of yours that keys on `TransientError` will not retry it.
85
+ - **Check the bucket's lifecycle rules with `await backend.checkLifecycle()`.** It reads the configuration with one
86
+ request (bounded by `readTimeoutMs`, when set) and answers `ok`, `problem` (an enabled, untagged rule over your prefix that expires current objects or moves
87
+ them to `GLACIER`, `DEEP_ARCHIVE` or a class it does not know) or `unchecked`. It needs `s3:GetLifecycleConfiguration`, which a load never uses and
88
+ `store.preflight()`'s lifecycle step does: without it the answer is `unchecked`, naming the permission. A missing abort-incomplete-upload rule is
89
+ advice in `detail`, never a `problem`
90
+ ([check the lifecycle rules from code](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md#check-the-lifecycle-rules-from-code)).
91
+ - **A transient failure of a write throws `TransientError`, and the write may or may not have landed.** Re-run the call,
92
+ or check `store.generations(ref)`. The SDK's own retry is off for conditional writes, so a write that landed is not
93
+ reported as a conflict. The driver [sends each registry write
94
+ once](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#sdk-retry); a load that gets no
95
+ answer [reads the row and sends a fresh compare-and-swap from
96
+ it](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#row-write-settled), at most three
97
+ times. A generation's object is sent again, up to three more times, only after a `503` (`SlowDown` is the usual one) or a bare `429` ([why that is
98
+ safe](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#object-write-resent));
99
+ it carries a random id in its user metadata (`x-amz-meta-cbwid`), so a first send that landed is told from another
100
+ writer's object. A load whose row writes all go unanswered throws `TransientError` and keeps its object. The waits
101
+ before an object's re-sends use real timers: the store's injected clock does not reach them, and `S3Storage` takes no
102
+ clock, so a test that throttles one object write three times waits about 3.5 s at most (the publish's waits before a
103
+ fresh row write do use the store's clock). **A bare `429`** (AWS S3 does not send one; some S3-compatible services do)
104
+ **is a throttle, handled exactly as a `503` is**, and one that ends a call is a `TransientError`.
89
105
  - **Reads can be timed, and writes are not.** Nothing is timed unless you set `readTimeoutMs`. Set, a read that has
90
106
  not finished in that many ms throws `TransientError`, and the store runs it again; AWS's guidance is to retry a GET
91
107
  of under 512 KB after about 2 seconds. The timer covers the body as well, so a connection that sends its headers and
package/dist/backend.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { STORAGE_BACKEND } from '@cloudbitmaps/core/driver-kit';
2
- import type { IRegistryDriver, IStorageDriver, StorageBackend } from '@cloudbitmaps/core/driver-kit';
2
+ import type { IRegistryDriver, IStorageDriver, LifecycleVerdict, StorageBackend } from '@cloudbitmaps/core/driver-kit';
3
3
  import { S3Client } from '@aws-sdk/client-s3';
4
4
  import type { IMetricsSink } from '@cloudbitmaps/core/driver-kit';
5
5
  /**
@@ -9,7 +9,7 @@ import type { IMetricsSink } from '@cloudbitmaps/core/driver-kit';
9
9
  export interface S3StorageOptions {
10
10
  /** Target bucket (must already exist). */
11
11
  readonly bucket: string;
12
- /** Optional key prefix under which everything lives — generations and the registry alike. */
12
+ /** Optional key prefix under which everything lives — generations and the registry alike. A string, else `ValidationError`. */
13
13
  readonly prefix?: string;
14
14
  /**
15
15
  * A constructed client. Supply one for a credential chain the SDK cannot infer; otherwise one is built. Its retry
@@ -17,14 +17,27 @@ export interface S3StorageOptions {
17
17
  * configured to do: a registry row once, and a generation's object again only after a throttle, under its own backoff.
18
18
  */
19
19
  readonly client?: S3Client;
20
- /** Region for the client built when `client` is absent (refused beside `client`). Falls back to the SDK's own resolution. */
20
+ /**
21
+ * Region for the client built when `client` is absent (refused beside `client`): a non-empty string, else
22
+ * `ValidationError`. Falls back to the SDK's own resolution.
23
+ */
21
24
  readonly region?: string;
22
- /** Endpoint for an S3-compatible store (MinIO, Ceph, R2). Refused beside `client`, which carries its own. */
25
+ /**
26
+ * Endpoint for an S3-compatible store (MinIO, Ceph, R2): an http or https URL, since an empty one would send
27
+ * requests to AWS, else `ValidationError`. Refused beside `client`, which carries its own.
28
+ */
23
29
  readonly endpoint?: string;
24
- /** Path-style addressing, which most S3-compatible stores require. Refused beside `client`, which carries its own. */
30
+ /**
31
+ * Path-style addressing, which most S3-compatible stores require: a boolean, else `ValidationError`. Refused beside
32
+ * `client`, which carries its own.
33
+ */
25
34
  readonly pathStyle?: boolean;
26
35
  /**
27
- * Static credentials, for the S3-compatible stores that issue them (MinIO, Ceph, R2).
36
+ * Static credentials, for the S3-compatible stores that issue them (MinIO, Ceph, R2): non-empty `accessKeyId` and
37
+ * `secretAccessKey` strings and an optional `sessionToken`, else `ValidationError`, which never prints a value. No other
38
+ * key is taken except `$source`, `credentialScope`, `accountId` and `expiration`, which are tolerated and not read:
39
+ * the SDK stamps `$source` on a credentials object it is given, so one used for two stores carries it, and a credential
40
+ * provider's result carries the others. A provider function is not taken here; put it on an `S3Client` given as `client`.
28
41
  *
29
42
  * On AWS itself, leave this unset — the SDK's own chain (instance role, SSO, environment, profile) is what
30
43
  * you want, and hard-coding keys to reach it would be a downgrade. It exists because the alternative for a
@@ -48,7 +61,7 @@ export interface S3StorageOptions {
48
61
  */
49
62
  readonly maxSockets?: number;
50
63
  /**
51
- * Largest object the backend will write and advertise. Default = `partBytes × 10,000` (≈ 80 GiB at the default
64
+ * Largest object the backend will write and advertise. Default = `partBytes × 10,000` (≈ 78 GiB at the default
52
65
  * 8 MiB part) — the honest ceiling reachable within S3's 10,000-part limit. Set it higher and `partBytes`
53
66
  * auto-grows so 10,000 parts still cover it (raising peak write memory to ~one part); up to the 5 TiB S3 max, and a
54
67
  * larger cap, or a `partBytes` above S3's 5 GiB part limit, is refused with `ValidationError`.
@@ -128,10 +141,32 @@ export declare class S3Storage implements StorageBackend {
128
141
  /** The client both halves share — built here unless one was supplied. */
129
142
  readonly client: S3Client;
130
143
  private readonly sockets;
144
+ private readonly bucket;
145
+ /** The key prefix every object sits under: `''`, or `trimmed/`. */
146
+ private readonly keyPrefix;
147
+ private readonly readTimeoutMs;
148
+ /**
149
+ * Builds the S3 backend over one bucket, which `bucket` names. Pass `client`, or `region`, `endpoint`, `pathStyle`,
150
+ * `credentials` and `maxSockets` to have one built, not both. `prefix` puts every object under a key prefix. Throws
151
+ * `ValidationError` for an unknown option, no bucket, a `now` that is not a function, a client setting beside
152
+ * `client`, a `maxSockets` (with no `client`) that is not a positive integer, a `partBytes` or `maxObjectBytes` that
153
+ * is not a positive integer, a `conditionalDelete` that is not a boolean, a `region` or `endpoint` that is empty or not a string, a `pathStyle` that
154
+ * is not a boolean, `credentials` that are not complete, or an invalid `prefix` or `readTimeoutMs`.
155
+ */
131
156
  constructor(options: S3StorageOptions);
132
157
  /**
133
158
  * Called by a store given a metrics sink. After the first read, a client whose socket pool is smaller than twice the
134
159
  * default window (64) sends the sink one `advisory` event, once; see the production guide's socket sizing.
135
160
  */
136
161
  attachMetrics(sink: IMetricsSink): void;
162
+ /**
163
+ * Reads the bucket's lifecycle configuration (one `GetBucketLifecycleConfiguration` request, plus the SDK's own
164
+ * retries, bounded by `readTimeoutMs` when it is set) and reports a rule that would expire, or move to GLACIER,
165
+ * DEEP_ARCHIVE or a class it does not know, the current objects under this store's prefix, generations and registry
166
+ * rows alike. Needs `s3:GetLifecycleConfiguration`, which a load never uses (`store.preflight()`'s
167
+ * lifecycle step does): without it the
168
+ * answer is `unchecked`, naming the permission. A missing rule that aborts incomplete multipart uploads is advice in
169
+ * `detail`, not a problem. Never rejects; a `detail` carries no credential. Rule reading: see the production guide.
170
+ */
171
+ checkLifecycle(): Promise<LifecycleVerdict>;
137
172
  }