@cloudbitmaps/s3 0.11.1 → 0.12.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
@@ -16,7 +16,7 @@ pnpm add @cloudbitmaps/roaring @cloudbitmaps/s3
16
16
 
17
17
  On npm 12 and pnpm 10 and later, allow `roaring`'s one install script first, or the install exits 0 and the package throws at `import`. Put this in your `package.json`: `{ "allowScripts": { "roaring": true }, "pnpm": { "onlyBuiltDependencies": ["roaring"] } }`. npm 11 runs the script but warns until you allow it the same way; pnpm 9 needs nothing extra. [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
18
18
 
19
- `@aws-sdk/client-s3` (`>=3.645.0 <4`) is a real dependency of this package, so installing it is the whole step.
19
+ `@aws-sdk/client-s3` (`>=3.700.0 <4`) is a real dependency of this package, so installing it is the whole step.
20
20
  `@cloudbitmaps/core`, the engine underneath, arrives with it and you never name it.
21
21
 
22
22
  ## Use
@@ -42,26 +42,46 @@ It builds its own client from your usual AWS credentials. Any other key is refus
42
42
  | `client` | your own `S3Client`; it carries its own region, endpoint and credentials |
43
43
  | `region`, `endpoint`, `pathStyle`, `credentials` | build a client for you, such as one for MinIO; refused beside `client` |
44
44
  | `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) |
45
+ | `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 |
46
+ | `conditionalDelete` | whether the registry removes a deleted row with a `DeleteObject` under `If-Match` rather than leaving a tombstone. 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) |
45
47
 
46
48
  ## Before production
47
49
 
48
50
  - **The bucket must honor conditional writes**: `If-None-Match: *` for a write-once generation and `If-Match` for the
49
51
  pointer. AWS S3 does, and the test suite runs against MinIO. Another S3-compatible service that ignores the headers
50
52
  turns write-once into overwrite, so check yours before you depend on it.
51
- - **Use `@aws-sdk/client-s3` 3.645.0 or later** if you pin the SDK or pass your own `client`. This floor is a
52
- correctness floor, not a preference. The SDK models the conditional write this library's write-once guarantee is
53
- built on from 3.641.0: measured against MinIO, 3.640.0 silently overwrites an existing object, which loses a
54
- published generation without an error, and 3.641.0 rejects correctly. The floor sits a small margin above that.
55
- This package's own range never resolves below it.
53
+ - **Use `@aws-sdk/client-s3` 3.700.0 or later** if you pin the SDK or pass your own `client`. This floor is a
54
+ correctness floor, not a preference. An SDK sends only the conditional headers it models, and drops one it does
55
+ not, without an error. `If-None-Match`, which write-once is built on, is modelled from 3.641.0: measured against
56
+ MinIO, 3.640.0 silently overwrites an existing object, which loses a published generation. `If-Match` on
57
+ `PutObject`, which the registry's compare-and-swap is built on, is modelled from 3.700.0: 3.699.0's serializer
58
+ omits it, so a fenced row write goes out unconditionally and can land over a concurrent writer's. This package's
59
+ own range never resolves below 3.700.0. The registry also checks, before its first request, that the client it is given sends `If-Match` on
60
+ `PutObject` and `DeleteObject`, and refuses a write it would send without its precondition.
56
61
  - **Grant `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject`, `s3:AbortMultipartUpload` and `s3:ListBucket`.** Without
57
62
  `s3:ListBucket`, S3 answers a missing key with `403` instead of `404`.
58
63
  - **Add a lifecycle rule that aborts incomplete multipart uploads**, and never one that expires current objects or the
59
64
  `registry/` prefix.
60
65
  - **A transient failure of a write throws `TransientError`, and the write may or may not have landed.** Re-run the
61
66
  call, or check `store.generations(ref)`. The SDK's own retry is off for conditional writes, so a write that landed
62
- is not reported as a conflict.
63
-
64
- The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item, with a sample IAM policy, 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.
67
+ is not reported as a conflict. The driver sends each registry write once; a load that gets no answer reads the row and
68
+ sends a fresh compare-and-swap from it, at most three times. A generation's object is sent again, up to three more
69
+ times, only after a `503` (`SlowDown` is the usual one); it carries a random id in its user metadata
70
+ (`x-amz-meta-cbwid`), so a first send that landed is told from another writer's object. A load whose row writes all go
71
+ unanswered throws `TransientError` and keeps its object. The waits before an object's re-sends use real timers: the
72
+ store's injected clock does not reach them, and `S3Storage` takes no clock, so a test that throttles one object
73
+ write three times waits about 3.5 s at most (the publish's waits before a fresh row write do use the store's
74
+ clock). **A bare `429`** (AWS S3 does not send one; some
75
+ S3-compatible services do) **is not retried and is not classified transient**: it surfaces as the SDK's own error, so a
76
+ layer of yours that keys on `TransientError` will not retry it.
77
+ - **Reads can be timed, and writes are not.** Nothing is timed unless you set `readTimeoutMs`. Set, a read that has
78
+ not finished in that many ms throws `TransientError`, and the store runs it again; AWS's guidance is to retry a GET
79
+ of under 512 KB after about 2 seconds. The timer covers the body as well, so a connection that sends its headers and
80
+ then stalls is cut off too. It starts when the read is handed to the SDK, so a burst of reads larger than the
81
+ client's socket pool (50 by default) can time out while queued: set it above that queueing, or raise `maxSockets`.
82
+ For the writes and listings, give your client a timeout of its own.
83
+
84
+ The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item, with a sample IAM policy, and the ones every backend shares: a request timeout on your client for the requests the library does not time, backups of the data and the registry, and a schedule for your loads.
65
85
 
66
86
  ## Documentation
67
87
 
package/dist/backend.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * credential chain the SDK cannot infer (SSO, an assumed role, a custom retry strategy); pass `endpoint` +
12
12
  * `pathStyle` + `credentials` for an S3-compatible store (MinIO, Ceph, R2). Both halves stay reachable as `.storage` and
13
13
  * `.registry` for anyone wiring something the facade does not cover. `maxObjectBytes` and `partBytes` size the
14
- * multipart upload.
14
+ * multipart upload, and `readTimeoutMs`, when set, bounds each read both halves make.
15
15
  */
16
16
  import { STORAGE_BACKEND } from '@cloudbitmaps/core/driver-kit';
17
17
  import type { IRegistryDriver, IStorageDriver, StorageBackend } from '@cloudbitmaps/core/driver-kit';
@@ -23,7 +23,8 @@ export interface S3StorageOptions {
23
23
  readonly prefix?: string;
24
24
  /**
25
25
  * A constructed client. Supply one for a credential chain the SDK cannot infer; otherwise one is built. Its retry
26
- * applies to every request except the conditional writes, which are sent once whatever it is configured to do.
26
+ * applies to every request except the conditional writes, which the driver sends with that retry off whatever it is
27
+ * configured to do: a registry row once, and a generation's object again only after a throttle, under its own backoff.
27
28
  */
28
29
  readonly client?: S3Client;
29
30
  /** Region for the client built when `client` is absent (refused beside `client`). Falls back to the SDK's own resolution. */
@@ -55,15 +56,40 @@ export interface S3StorageOptions {
55
56
  /** Multipart part size in bytes (default 8 MiB; a smaller value is raised to the S3 5 MiB minimum). Must be a
56
57
  * positive safe integer. Tunes peak write memory. */
57
58
  readonly partBytes?: number;
59
+ /**
60
+ * How long one read may take before it is abandoned, in ms. `0`, the default, sets no timeout. When set, it bounds
61
+ * each `GetObject` and `HeadObject` either half sends, the response body included, so a connection that stops
62
+ * answering part-way through a body is cut off too. A read that runs out of time throws `TransientError`, which the
63
+ * store's read retry runs again. AWS's S3 guidance is to retry a GET of under 512 KB that has not answered in about
64
+ * 2 seconds. Must be a non-negative safe integer no larger than 2,147,483,647.
65
+ *
66
+ * The clock starts when the read is handed to the SDK, so it also counts the time the read waits for one of the
67
+ * client's sockets (50 by default) and the time spent fetching credentials, and under `retryMode: 'adaptive'` the
68
+ * SDK's rate-limiter wait. A burst of concurrent reads larger than the socket pool can therefore time out with
69
+ * nothing slow on the wire: size the timeout above the worst queueing your concurrency implies, or raise the client's
70
+ * `maxSockets`. On a client built with `cacheMiddleware: true`, a timed read resolves its middleware each time.
71
+ *
72
+ * Writes and listings are never timed: a write that hangs needs a timeout on the client (its `requestHandler`). The
73
+ * timeout is applied per request, so a `client` you pass gets it without being changed.
74
+ */
75
+ readonly readTimeoutMs?: number;
58
76
  /** Injected clock for the registry's `createdAt`/`updatedAt`; defaults to `Date.now`. */
59
77
  readonly now?: () => number;
78
+ /**
79
+ * Whether the registry removes a deleted row for good, by a `DeleteObject` sent with `If-Match`, rather than leaving
80
+ * a tombstone a full listing reads forever. Defaults to `true` when the host the client resolves is an AWS S3 host,
81
+ * whichever way its endpoint was set (`endpoint`, `AWS_ENDPOINT_URL_S3`, `AWS_ENDPOINT_URL`, the shared config file),
82
+ * and to `false` for any other host: set it for an S3-compatible store only once you know the store applies `If-Match`
83
+ * on a delete. MinIO, for one, ignores it. It is never `true` for an SDK that does not send the header.
84
+ */
85
+ readonly conditionalDelete?: boolean;
60
86
  }
61
87
  /**
62
88
  * The keys `new S3Storage(options)` takes. Any other is refused by name rather than ignored: an ignored client or
63
89
  * endpoint key builds a client from ambient credentials against the **public** endpoint, and for a store pointed at
64
90
  * MinIO that is production traffic from a wiring typo.
65
91
  */
66
- export declare const S3_STORAGE_OPTION_KEYS: readonly ["bucket", "prefix", "client", "region", "endpoint", "pathStyle", "credentials", "maxObjectBytes", "partBytes", "now"];
92
+ export declare const S3_STORAGE_OPTION_KEYS: readonly ["bucket", "prefix", "client", "region", "endpoint", "pathStyle", "credentials", "maxObjectBytes", "partBytes", "readTimeoutMs", "now", "conditionalDelete"];
67
93
  export declare class S3Storage implements StorageBackend {
68
94
  /** Cross-bundle brand, stamped non-enumerably in the constructor so a spread cannot carry it. */
69
95
  readonly [STORAGE_BACKEND]: true;
@@ -0,0 +1,26 @@
1
+ import type { S3Client } from '@aws-sdk/client-s3';
2
+ /** What a client does with the requests the registry sends. */
3
+ export interface ClientFacts {
4
+ /** The host a request to the bucket is addressed to, as the client resolves it now. */
5
+ readonly host: string;
6
+ /** Whether a `DeleteObject` given `IfMatch` goes out with an `If-Match` header. */
7
+ readonly sendsDeleteIfMatch: boolean;
8
+ /** Whether a `PutObject` given `IfMatch` goes out with an `If-Match` header: the registry's compare-and-swap. */
9
+ readonly sendsPutIfMatch: boolean;
10
+ /** Whether a `PutObject` given `IfNoneMatch` goes out with an `If-None-Match` header: the registry's create. */
11
+ readonly sendsPutIfNoneMatch: boolean;
12
+ }
13
+ /** A key no registry row or generation can hold; the probe's requests are never sent, so it is never used. */
14
+ export declare const PROBE_KEY = "cloudbitmaps-probe";
15
+ /**
16
+ * What `client` does with a conditional `DeleteObject` and `PutObject` to `bucket`, or `undefined` when it cannot be
17
+ * found out. Never throws, never sends a request, and never runs anything the caller added to `client`.
18
+ */
19
+ export declare function probeClient(client: S3Client, bucket: string): Promise<ClientFacts | undefined>;
20
+ /**
21
+ * Whether `hostname` is an AWS S3 host: under an AWS domain and naming S3 in one of its labels (`s3`, `s3-fips`,
22
+ * `s3-accesspoint`, `s3express-…`), so the FIPS, dual-stack, access-point and VPC interface forms all count, and another
23
+ * AWS service's host does not. A host of any other domain is not, whatever it speaks: a store behind one has to be
24
+ * vouched for by the caller.
25
+ */
26
+ export declare function isAwsS3Host(hostname: string): boolean;