partial-content 1.0.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +601 -0
- package/SECURITY.md +92 -0
- package/dist/azure.d.ts +79 -0
- package/dist/azure.d.ts.map +1 -0
- package/dist/azure.js +251 -0
- package/dist/azure.js.map +1 -0
- package/dist/content-disposition.d.ts +74 -0
- package/dist/content-disposition.d.ts.map +1 -0
- package/dist/content-disposition.js +253 -0
- package/dist/content-disposition.js.map +1 -0
- package/dist/fs.d.ts +86 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +375 -0
- package/dist/fs.js.map +1 -0
- package/dist/gcs.d.ts +72 -0
- package/dist/gcs.d.ts.map +1 -0
- package/dist/gcs.js +202 -0
- package/dist/gcs.js.map +1 -0
- package/dist/hono.d.ts +92 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +61 -0
- package/dist/hono.js.map +1 -0
- package/dist/http.d.ts +70 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +281 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel.d.ts +541 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1218 -0
- package/dist/kernel.js.map +1 -0
- package/dist/memory.d.ts +55 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +107 -0
- package/dist/memory.js.map +1 -0
- package/dist/mime.d.ts +49 -0
- package/dist/mime.d.ts.map +1 -0
- package/dist/mime.js +150 -0
- package/dist/mime.js.map +1 -0
- package/dist/node.d.ts +84 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +215 -0
- package/dist/node.js.map +1 -0
- package/dist/object-store.d.ts +472 -0
- package/dist/object-store.d.ts.map +1 -0
- package/dist/object-store.js +335 -0
- package/dist/object-store.js.map +1 -0
- package/dist/r2.d.ts +94 -0
- package/dist/r2.d.ts.map +1 -0
- package/dist/r2.js +150 -0
- package/dist/r2.js.map +1 -0
- package/dist/s3.d.ts +49 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +263 -0
- package/dist/s3.js.map +1 -0
- package/dist/web.d.ts +336 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +1094 -0
- package/dist/web.js.map +1 -0
- package/docs/DESIGN.md +426 -0
- package/package.json +182 -0
- package/src/azure.ts +329 -0
- package/src/content-disposition.ts +300 -0
- package/src/fs.ts +469 -0
- package/src/gcs.ts +290 -0
- package/src/hono.ts +123 -0
- package/src/http.ts +351 -0
- package/src/index.ts +85 -0
- package/src/kernel.ts +1498 -0
- package/src/memory.ts +148 -0
- package/src/mime.ts +160 -0
- package/src/node.ts +261 -0
- package/src/object-store.ts +665 -0
- package/src/r2.ts +232 -0
- package/src/s3.ts +324 -0
- package/src/web.ts +1603 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage backend contract for partial-content adapters.
|
|
3
|
+
*
|
|
4
|
+
* Implementations adapt a concrete store (S3, R2, Hetzner, GCS, fs) down to
|
|
5
|
+
* this surface. The kernel and framework adapters depend only on these types,
|
|
6
|
+
* never on a storage SDK.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import { parseContentRange } from "./kernel.js";
|
|
11
|
+
// ─── Error Types ────────────────────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when an object does not exist in a storage backend.
|
|
14
|
+
*
|
|
15
|
+
* All built-in adapters (S3, R2, GCS, Azure, fs) throw this error
|
|
16
|
+
* for missing objects. Framework adapters (e.g. `partial-content/web`)
|
|
17
|
+
* use the `status` property to distinguish "object not found" (404)
|
|
18
|
+
* from a transiently unavailable backend (503, {@link StoreUnavailableError})
|
|
19
|
+
* and other store failures (502).
|
|
20
|
+
*/
|
|
21
|
+
export class ObjectNotFoundError extends Error {
|
|
22
|
+
/** HTTP status hint for downstream handlers. */
|
|
23
|
+
status = 404;
|
|
24
|
+
/** The storage key that was not found. */
|
|
25
|
+
key;
|
|
26
|
+
constructor(key, cause) {
|
|
27
|
+
super(`Object not found: ${key}`);
|
|
28
|
+
this.name = "ObjectNotFoundError";
|
|
29
|
+
this.key = key;
|
|
30
|
+
if (cause !== undefined)
|
|
31
|
+
this.cause = cause;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Thrown when a pinned read ({@link GetObjectOptions.ifMatch}) finds the
|
|
36
|
+
* object changed since its validator was captured.
|
|
37
|
+
*
|
|
38
|
+
* Adapters map their backend's native rejection to this error (S3 412
|
|
39
|
+
* PreconditionFailed, R2 `onlyIf` body-less response, Azure 412
|
|
40
|
+
* ConditionNotMet, GCS etag mismatch). The web adapter treats it as a
|
|
41
|
+
* signal to re-validate the request against the object's new state.
|
|
42
|
+
*/
|
|
43
|
+
export class ObjectChangedError extends Error {
|
|
44
|
+
/** HTTP status hint: the pinned read's precondition failed. */
|
|
45
|
+
status = 412;
|
|
46
|
+
/** The storage key whose object changed. */
|
|
47
|
+
key;
|
|
48
|
+
constructor(key, cause) {
|
|
49
|
+
super(`Object changed since validation: ${key}`);
|
|
50
|
+
this.name = "ObjectChangedError";
|
|
51
|
+
this.key = key;
|
|
52
|
+
if (cause !== undefined)
|
|
53
|
+
this.cause = cause;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Thrown when a storage backend is transiently unavailable: overloaded,
|
|
58
|
+
* throttling, or timing out after the adapter's own retries are exhausted.
|
|
59
|
+
*
|
|
60
|
+
* A generic store failure (a malformed response, an unparseable
|
|
61
|
+
* `Content-Range`, an empty body) is reported by the framework adapter as
|
|
62
|
+
* `502 Bad Gateway` -- "the upstream returned something invalid." This error
|
|
63
|
+
* is the distinct RETRYABLE case: the upstream is healthy but momentarily
|
|
64
|
+
* cannot serve. The web adapter maps it to `503 Service Unavailable` and, when
|
|
65
|
+
* {@link retryAfterSeconds} is set, emits a `Retry-After` header so clients and
|
|
66
|
+
* shared caches back off instead of hammering an origin that is already shedding
|
|
67
|
+
* load. Adapters map their backend's throttle signals here (S3/R2/Hetzner
|
|
68
|
+
* `503 SlowDown`, `429 TooManyRequests`, request timeouts).
|
|
69
|
+
*/
|
|
70
|
+
export class StoreUnavailableError extends Error {
|
|
71
|
+
/** HTTP status hint: the backend is transiently unavailable. */
|
|
72
|
+
status = 503;
|
|
73
|
+
/** The storage key whose read failed. */
|
|
74
|
+
key;
|
|
75
|
+
/**
|
|
76
|
+
* Suggested back-off in whole seconds, echoed as `Retry-After`. Normalized at
|
|
77
|
+
* construction: a non-negative finite hint is floored to an integer (a
|
|
78
|
+
* fractional `2.9` -> `2`), and a NaN/negative/infinite/out-of-safe-range hint
|
|
79
|
+
* (from a hostile backend header or a buggy third-party classifier) is dropped
|
|
80
|
+
* entirely. So every consumer -- the web adapter AND anyone reading
|
|
81
|
+
* `.retryAfterSeconds` directly off the frozen contract -- sees a clean
|
|
82
|
+
* non-negative integer or `undefined`. Absent means the adapter emits `503`
|
|
83
|
+
* without a `Retry-After` (RFC 9110 Section 15.6.4 permits its absence).
|
|
84
|
+
*/
|
|
85
|
+
retryAfterSeconds;
|
|
86
|
+
constructor(key, opts) {
|
|
87
|
+
super(`Storage backend unavailable: ${key}`);
|
|
88
|
+
this.name = "StoreUnavailableError";
|
|
89
|
+
this.key = key;
|
|
90
|
+
const hint = parseRetryAfterSeconds(opts?.retryAfterSeconds);
|
|
91
|
+
if (hint !== undefined)
|
|
92
|
+
this.retryAfterSeconds = hint;
|
|
93
|
+
if (opts?.cause !== undefined)
|
|
94
|
+
this.cause = opts.cause;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Normalize a `Retry-After` value to whole non-negative seconds, or `undefined`.
|
|
99
|
+
*
|
|
100
|
+
* The single sanctioned parser shared by every adapter and the web layer.
|
|
101
|
+
* Accepts the delay-seconds form (a number, or a numeric string) and -- when
|
|
102
|
+
* `allowHttpDate` is set -- the HTTP-date form (RFC 9110 Section 10.2.3) as a
|
|
103
|
+
* non-negative delta from now. Everything else yields `undefined`: NaN,
|
|
104
|
+
* Infinity, negatives, and any value whose floor exceeds `Number.MAX_SAFE_INTEGER`
|
|
105
|
+
* (so a huge finite hint can never serialize as `1e+21`, which violates the
|
|
106
|
+
* `delay-seconds = DIGIT+` grammar), plus non-numeric text. A hostile header or
|
|
107
|
+
* a buggy third-party classifier therefore can never emit a malformed header.
|
|
108
|
+
*/
|
|
109
|
+
export function parseRetryAfterSeconds(raw, opts) {
|
|
110
|
+
if (typeof raw === "number") {
|
|
111
|
+
if (!Number.isFinite(raw) || raw < 0)
|
|
112
|
+
return undefined;
|
|
113
|
+
const floored = Math.floor(raw);
|
|
114
|
+
return Number.isSafeInteger(floored) ? floored : undefined;
|
|
115
|
+
}
|
|
116
|
+
if (typeof raw !== "string")
|
|
117
|
+
return undefined;
|
|
118
|
+
const trimmed = raw.trim();
|
|
119
|
+
if (/^\d+$/.test(trimmed)) {
|
|
120
|
+
const n = Number(trimmed);
|
|
121
|
+
return Number.isSafeInteger(n) ? n : undefined;
|
|
122
|
+
}
|
|
123
|
+
if (opts?.allowHttpDate) {
|
|
124
|
+
const when = Date.parse(trimmed);
|
|
125
|
+
if (Number.isNaN(when))
|
|
126
|
+
return undefined;
|
|
127
|
+
return Math.max(0, Math.round((when - Date.now()) / 1000));
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Run a backend read and normalize its failures to the contract's error types.
|
|
133
|
+
*
|
|
134
|
+
* This is the single ordered classification pipeline every SDK-backed adapter
|
|
135
|
+
* shares: `notFound` -> `changed` -> `throttled` -> rethrow. Centralizing it
|
|
136
|
+
* means the "which errors are handled, in what order" contract is structural
|
|
137
|
+
* rather than copy-pasted into each adapter's head/get catch blocks, so a
|
|
138
|
+
* classifier can never be present on one path and silently missing on another.
|
|
139
|
+
* The predicates MUST be mutually exclusive on a given backend (a 404, 412, and
|
|
140
|
+
* 429/503 are distinct); the order only decides precedence if they are not.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const meta = await classifyStoreRead(key, () => client.headObject(key), {
|
|
145
|
+
* notFound: isNotFound,
|
|
146
|
+
* changed: isPreconditionFailed,
|
|
147
|
+
* throttled: isThrottled,
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export async function classifyStoreRead(key, op, classifiers) {
|
|
152
|
+
try {
|
|
153
|
+
return await op();
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (classifiers.notFound(err))
|
|
157
|
+
throw new ObjectNotFoundError(key, err);
|
|
158
|
+
if (classifiers.changed?.(err))
|
|
159
|
+
throw new ObjectChangedError(key, err);
|
|
160
|
+
const throttled = classifiers.throttled(err);
|
|
161
|
+
if (throttled) {
|
|
162
|
+
throw new StoreUnavailableError(key, {
|
|
163
|
+
cause: err,
|
|
164
|
+
// `throttled` may return the backend's advised back-off; a bare `true`
|
|
165
|
+
// means "throttled, no hint" and emits a 503 without `Retry-After`.
|
|
166
|
+
retryAfterSeconds: typeof throttled === "object" ? throttled.retryAfterSeconds : undefined,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ─── Node Stream Conversion ─────────────────────────────────────────────────
|
|
173
|
+
/**
|
|
174
|
+
* Convert a Node.js readable stream (async iterable) to a web ReadableStream
|
|
175
|
+
* with proper backpressure, Buffer coercion, signal propagation, and cleanup.
|
|
176
|
+
*
|
|
177
|
+
* This is the single implementation shared by all Node.js-based adapters
|
|
178
|
+
* (fs, GCS, Azure, S3 fallback). It handles:
|
|
179
|
+
*
|
|
180
|
+
* - **Pull-based backpressure**: the web ReadableStream's `pull()` method
|
|
181
|
+
* drives the async iterator, so chunks are only read when the consumer
|
|
182
|
+
* is ready.
|
|
183
|
+
* - **Buffer to Uint8Array coercion**: Node.js streams yield `Buffer`
|
|
184
|
+
* instances. We coerce to `Uint8Array` for cross-runtime compatibility
|
|
185
|
+
* (Buffer extends Uint8Array in Node, but the `instanceof` check
|
|
186
|
+
* ensures correctness in edge cases).
|
|
187
|
+
* - **AbortSignal propagation**: when the client disconnects (signal aborts),
|
|
188
|
+
* the underlying node stream is destroyed immediately to stop I/O.
|
|
189
|
+
* - **Cancel cleanup**: when the web ReadableStream is cancelled (e.g. by
|
|
190
|
+
* `Response.body.cancel()`), the node stream is destroyed.
|
|
191
|
+
*
|
|
192
|
+
* @param iterable - The async iterable (Node.js Readable stream). Typed
|
|
193
|
+
* `AsyncIterable<Uint8Array>` (Node `Buffer` extends `Uint8Array`, so Buffer
|
|
194
|
+
* streams satisfy it) to keep the `Buffer` name -- and its `@types/node`
|
|
195
|
+
* coupling -- out of the package's public type surface.
|
|
196
|
+
* @param opts.destroy - Destroys the underlying stream on cancel/abort. When
|
|
197
|
+
* omitted, a `destroy()` method on the iterable is auto-detected and used.
|
|
198
|
+
* @param opts.signal - Optional AbortSignal for client-disconnect detection
|
|
199
|
+
* @param opts.expectedBytes - Exact byte count the source promised. When set,
|
|
200
|
+
* a graceful end that delivered a different total (a file truncated or grown
|
|
201
|
+
* in place mid-read) errors the web stream instead of closing it short, so a
|
|
202
|
+
* torn body can never masquerade as a complete response under the committed
|
|
203
|
+
* `Content-Length`. A client abort tears down via a thrown iterator error,
|
|
204
|
+
* not a graceful end, so it is unaffected by this check.
|
|
205
|
+
*/
|
|
206
|
+
export function nodeStreamToWeb(iterable, opts) {
|
|
207
|
+
const { signal, expectedBytes } = opts ?? {};
|
|
208
|
+
let seen = 0;
|
|
209
|
+
// Auto-detect the Node-stream destroy capability when the caller doesn't
|
|
210
|
+
// supply one: every adapter was hand-rolling the same
|
|
211
|
+
// `typeof stream.destroy === "function"` guard.
|
|
212
|
+
const nativeDestroy = iterable.destroy;
|
|
213
|
+
const destroy = opts?.destroy ?? (typeof nativeDestroy === "function" ? () => nativeDestroy.call(iterable) : undefined);
|
|
214
|
+
// Propagate abort signal to the underlying stream. The listener MUST be
|
|
215
|
+
// removed once the stream settles: a consumer that reuses one long-lived
|
|
216
|
+
// signal across many getObject calls would otherwise accumulate one
|
|
217
|
+
// listener (and one retained stream reference) per completed transfer
|
|
218
|
+
// until the signal is aborted or GC'd.
|
|
219
|
+
const abortListener = signal && destroy ? destroy : undefined;
|
|
220
|
+
if (signal && abortListener)
|
|
221
|
+
signal.addEventListener("abort", abortListener);
|
|
222
|
+
const detach = () => {
|
|
223
|
+
if (signal && abortListener)
|
|
224
|
+
signal.removeEventListener?.("abort", abortListener);
|
|
225
|
+
};
|
|
226
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
227
|
+
return new ReadableStream({
|
|
228
|
+
async pull(controller) {
|
|
229
|
+
let result;
|
|
230
|
+
try {
|
|
231
|
+
result = await iterator.next();
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
// Stop backend I/O when iteration fails, then error the web stream.
|
|
235
|
+
// Without this, a mid-transfer backend failure leaks the socket.
|
|
236
|
+
detach();
|
|
237
|
+
destroy?.();
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
if (result.done) {
|
|
241
|
+
detach();
|
|
242
|
+
// A graceful end that under- or over-ran the promised length means the
|
|
243
|
+
// source changed under an already-committed Content-Length: fail the
|
|
244
|
+
// body loudly (a reset the client sees as a failed transfer) rather
|
|
245
|
+
// than deliver a truncated stream that looks complete.
|
|
246
|
+
if (expectedBytes !== undefined && seen !== expectedBytes) {
|
|
247
|
+
destroy?.();
|
|
248
|
+
controller.error(new Error(`stream delivered ${seen} bytes, expected ${expectedBytes} (source changed mid-read)`));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
controller.close();
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Node.js streams yield Buffer; ensure Uint8Array for cross-runtime compat.
|
|
255
|
+
// Node Buffers and adapter chunks are ArrayBuffer-backed, so the narrow
|
|
256
|
+
// is runtime-safe and lets the web stream advertise a BodyInit-assignable
|
|
257
|
+
// Uint8Array<ArrayBuffer> (see F5 rationale in guardStreamLength).
|
|
258
|
+
const raw = result.value instanceof Uint8Array ? result.value : Buffer.from(result.value);
|
|
259
|
+
const chunk = raw;
|
|
260
|
+
seen += chunk.byteLength;
|
|
261
|
+
controller.enqueue(chunk);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
async cancel(reason) {
|
|
265
|
+
detach();
|
|
266
|
+
try {
|
|
267
|
+
await iterator.return?.(reason);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Iterator cleanup failed; socket will be GC'd
|
|
271
|
+
}
|
|
272
|
+
destroy?.();
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Wrap a web `ReadableStream` so a graceful end that delivered a byte count
|
|
278
|
+
* other than `expectedBytes` errors the stream instead of closing it short.
|
|
279
|
+
*
|
|
280
|
+
* The Node-stream path gets this guard inside {@link nodeStreamToWeb}. This is
|
|
281
|
+
* the equivalent for adapters whose SDK hands back a web `ReadableStream`
|
|
282
|
+
* directly (S3 in Bun/Deno or via `transformToWebStream`, R2's native binding,
|
|
283
|
+
* a browser `Blob` stream) rather than a Node `Readable`. Without it, an
|
|
284
|
+
* S3-compatible backend that ends a body cleanly but short of the committed
|
|
285
|
+
* `Content-Length` (some do in-flight body retries) would under-run the
|
|
286
|
+
* response undetected -- the exact truncation `expectedBytes` exists to catch.
|
|
287
|
+
* Passing `undefined` disables the check and returns the stream unwrapped.
|
|
288
|
+
*/
|
|
289
|
+
export function guardStreamLength(stream, expectedBytes) {
|
|
290
|
+
// Return position narrows to <ArrayBuffer> (backend byte chunks are always
|
|
291
|
+
// ArrayBuffer-backed) so the guarded body stays `new Response(...)`-assignable
|
|
292
|
+
// under DOM lib; the input stays wide for callers.
|
|
293
|
+
if (expectedBytes === undefined)
|
|
294
|
+
return stream;
|
|
295
|
+
let seen = 0;
|
|
296
|
+
return stream.pipeThrough(new TransformStream({
|
|
297
|
+
transform(chunk, controller) {
|
|
298
|
+
seen += chunk.byteLength;
|
|
299
|
+
controller.enqueue(chunk);
|
|
300
|
+
},
|
|
301
|
+
flush(controller) {
|
|
302
|
+
// A short (or long) graceful end means the source diverged from the
|
|
303
|
+
// committed length: fail the body loudly rather than deliver a
|
|
304
|
+
// truncated stream that looks complete. Mirrors nodeStreamToWeb.
|
|
305
|
+
if (seen !== expectedBytes) {
|
|
306
|
+
controller.error(new Error(`stream delivered ${seen} bytes, expected ${expectedBytes} (source changed mid-read)`));
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Parse a backend `Content-Range` response header into served bounds plus an
|
|
313
|
+
* honest total, applying the parser's `-1` unknown-total sentinel -> `undefined`
|
|
314
|
+
* mapping that every SDK adapter needs identically. Returns `null` ONLY when the
|
|
315
|
+
* header is present but unparseable -- the exact "byte accounting is
|
|
316
|
+
* untrustworthy, tear down and fail loudly" signal, which each adapter pairs
|
|
317
|
+
* with its own live-body cleanup (S3 `stream.cancel()`, Azure
|
|
318
|
+
* `destroyAzureDownload`, http `drain`) before throwing.
|
|
319
|
+
*
|
|
320
|
+
* Adapters that treat an ABSENT header as a full 200 (S3, Azure) check for the
|
|
321
|
+
* header before calling; callers that already know the response is partial
|
|
322
|
+
* (http's 206 branch) treat both an absent header and a `null` here as the same
|
|
323
|
+
* malformed-206 failure. Adapters that know their bounds natively (fs, memory,
|
|
324
|
+
* GCS, R2) construct {@link ServedRange} directly and never call this.
|
|
325
|
+
*/
|
|
326
|
+
export function resolveServedRange(contentRange) {
|
|
327
|
+
const parsed = parseContentRange(contentRange);
|
|
328
|
+
if (!parsed)
|
|
329
|
+
return null;
|
|
330
|
+
return {
|
|
331
|
+
served: { start: parsed.start, end: parsed.end },
|
|
332
|
+
totalSize: parsed.totalSize < 0 ? undefined : parsed.totalSize,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=object-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-store.js","sourceRoot":"","sources":["../src/object-store.ts"],"names":[],"mappings":"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"}
|
package/dist/r2.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare R2 native ObjectStore adapter for partial-content.
|
|
3
|
+
*
|
|
4
|
+
* Uses Cloudflare's native R2 bindings (`R2Bucket`) directly, without the
|
|
5
|
+
* AWS SDK. This eliminates the ~50-package `@aws-sdk/client-s3` dependency
|
|
6
|
+
* and its cold-start overhead in Workers.
|
|
7
|
+
*
|
|
8
|
+
* For S3-compatible access to R2 (e.g. from Node.js outside Workers),
|
|
9
|
+
* use `partial-content/s3` instead.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { r2Store } from "partial-content/r2";
|
|
14
|
+
* import { serveObject } from "partial-content/hono";
|
|
15
|
+
*
|
|
16
|
+
* // In a Cloudflare Worker with R2 binding:
|
|
17
|
+
* app.get("/files/:key", serveObject(
|
|
18
|
+
* r2Store({ bucket: env.MY_BUCKET }),
|
|
19
|
+
* { key: (c) => c.req.param("key") },
|
|
20
|
+
* ));
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
import { ObjectNotFoundError, ObjectChangedError, type ObjectStore } from "./index.js";
|
|
26
|
+
export { ObjectNotFoundError, ObjectChangedError };
|
|
27
|
+
/**
|
|
28
|
+
* Minimal R2Bucket interface from Cloudflare Workers types.
|
|
29
|
+
*
|
|
30
|
+
* Declared locally to avoid a dependency on `@cloudflare/workers-types`.
|
|
31
|
+
* The shapes match the runtime behavior; type-checking is the caller's
|
|
32
|
+
* responsibility (their Worker project imports the full types).
|
|
33
|
+
*/
|
|
34
|
+
interface R2Bucket {
|
|
35
|
+
head(key: string): Promise<R2Object | null>;
|
|
36
|
+
/**
|
|
37
|
+
* With `onlyIf`, a failed precondition resolves to a body-less `R2Object`
|
|
38
|
+
* (not null, not an error) -- callers must check for `body`.
|
|
39
|
+
*/
|
|
40
|
+
get(key: string, options?: R2GetOptions): Promise<R2Object | R2ObjectBody | null>;
|
|
41
|
+
}
|
|
42
|
+
interface R2Object {
|
|
43
|
+
key: string;
|
|
44
|
+
size: number;
|
|
45
|
+
etag: string;
|
|
46
|
+
uploaded: Date;
|
|
47
|
+
checksums: R2Checksums;
|
|
48
|
+
httpMetadata?: R2HttpMetadata;
|
|
49
|
+
}
|
|
50
|
+
interface R2ObjectBody extends R2Object {
|
|
51
|
+
body: ReadableStream<Uint8Array>;
|
|
52
|
+
range: R2Range;
|
|
53
|
+
}
|
|
54
|
+
interface R2Range {
|
|
55
|
+
offset: number;
|
|
56
|
+
length: number;
|
|
57
|
+
}
|
|
58
|
+
interface R2GetOptions {
|
|
59
|
+
range?: {
|
|
60
|
+
offset: number;
|
|
61
|
+
length?: number;
|
|
62
|
+
};
|
|
63
|
+
onlyIf?: {
|
|
64
|
+
etagMatches?: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
interface R2Checksums {
|
|
68
|
+
sha256?: ArrayBuffer;
|
|
69
|
+
}
|
|
70
|
+
interface R2HttpMetadata {
|
|
71
|
+
contentType?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface R2StoreOptions {
|
|
74
|
+
/** The R2 bucket binding from the Worker environment. */
|
|
75
|
+
bucket: R2Bucket;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create an {@link ObjectStore} backed by a Cloudflare R2 bucket.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* import { r2Store } from "partial-content/r2";
|
|
83
|
+
*
|
|
84
|
+
* export default {
|
|
85
|
+
* async fetch(request, env) {
|
|
86
|
+
* const store = r2Store({ bucket: env.MY_BUCKET });
|
|
87
|
+
* const meta = await store.headObject("reports/q4.pdf");
|
|
88
|
+
* // ...
|
|
89
|
+
* },
|
|
90
|
+
* };
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export declare function r2Store(opts: R2StoreOptions): ObjectStore;
|
|
94
|
+
//# sourceMappingURL=r2.d.ts.map
|
package/dist/r2.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2.d.ts","sourceRoot":"","sources":["../src/r2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAGL,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,WAAW,EAIjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;AAInD;;;;;;GAMG;AACH,UAAU,QAAQ;IAChB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IAC5C;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,GAAG,YAAY,GAAG,IAAI,CAAC,CAAC;CACnF;AAED,UAAU,QAAQ;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,WAAW,CAAC;IACvB,YAAY,CAAC,EAAE,cAAc,CAAC;CAC/B;AAED,UAAU,YAAa,SAAQ,QAAQ;IACrC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACjC,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,OAAO;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,YAAY;IAGpB,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,MAAM,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACnC;AAED,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,MAAM,EAAE,QAAQ,CAAC;CAClB;AAwBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,WAAW,CA+FzD"}
|
package/dist/r2.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare R2 native ObjectStore adapter for partial-content.
|
|
3
|
+
*
|
|
4
|
+
* Uses Cloudflare's native R2 bindings (`R2Bucket`) directly, without the
|
|
5
|
+
* AWS SDK. This eliminates the ~50-package `@aws-sdk/client-s3` dependency
|
|
6
|
+
* and its cold-start overhead in Workers.
|
|
7
|
+
*
|
|
8
|
+
* For S3-compatible access to R2 (e.g. from Node.js outside Workers),
|
|
9
|
+
* use `partial-content/s3` instead.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { r2Store } from "partial-content/r2";
|
|
14
|
+
* import { serveObject } from "partial-content/hono";
|
|
15
|
+
*
|
|
16
|
+
* // In a Cloudflare Worker with R2 binding:
|
|
17
|
+
* app.get("/files/:key", serveObject(
|
|
18
|
+
* r2Store({ bucket: env.MY_BUCKET }),
|
|
19
|
+
* { key: (c) => c.req.param("key") },
|
|
20
|
+
* ));
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
import { isOpenEndedRange, guardStreamLength, ObjectNotFoundError, ObjectChangedError, } from "./index.js";
|
|
26
|
+
// Re-export for convenience
|
|
27
|
+
export { ObjectNotFoundError, ObjectChangedError };
|
|
28
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Convert an ArrayBuffer SHA-256 checksum to base64 for RFC 9530 Repr-Digest.
|
|
31
|
+
*
|
|
32
|
+
* `checksums.sha256` is R2's whole-object SHA-256 as raw bytes, so unlike the
|
|
33
|
+
* S3 adapter there is no `<base64>-<partCount>` composite string form to
|
|
34
|
+
* reject: a 32-byte buffer always describes the full representation. If R2
|
|
35
|
+
* ever exposes a per-part checksum in this field, this digest would need the
|
|
36
|
+
* same whole-object guard S3 applies.
|
|
37
|
+
*/
|
|
38
|
+
function arrayBufferToBase64(buffer) {
|
|
39
|
+
const bytes = new Uint8Array(buffer);
|
|
40
|
+
let binary = "";
|
|
41
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
42
|
+
binary += String.fromCharCode(bytes[i]);
|
|
43
|
+
}
|
|
44
|
+
return btoa(binary);
|
|
45
|
+
}
|
|
46
|
+
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Create an {@link ObjectStore} backed by a Cloudflare R2 bucket.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* import { r2Store } from "partial-content/r2";
|
|
53
|
+
*
|
|
54
|
+
* export default {
|
|
55
|
+
* async fetch(request, env) {
|
|
56
|
+
* const store = r2Store({ bucket: env.MY_BUCKET });
|
|
57
|
+
* const meta = await store.headObject("reports/q4.pdf");
|
|
58
|
+
* // ...
|
|
59
|
+
* },
|
|
60
|
+
* };
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function r2Store(opts) {
|
|
64
|
+
const { bucket } = opts;
|
|
65
|
+
// No `classifyStoreRead` here (unlike s3/gcs/azure): the R2 Workers binding
|
|
66
|
+
// signals its recoverable outcomes structurally, not as classifiable errors.
|
|
67
|
+
// Not-found is a `null` return, a failed `onlyIf` pin is a body-less object,
|
|
68
|
+
// and both are handled inline below. R2 exposes NO documented throttle/503
|
|
69
|
+
// error shape on the native binding (rate limiting surfaces at the Workers
|
|
70
|
+
// platform layer, before the handler runs), so there is nothing to map to
|
|
71
|
+
// `StoreUnavailableError`. Inventing a classifier for an undocumented error
|
|
72
|
+
// shape would be guessing; the README correctly omits R2 from the throttle-
|
|
73
|
+
// classification list. Use `partial-content/s3` (R2's S3 endpoint) if you
|
|
74
|
+
// need SlowDown/429 -> 503 mapping.
|
|
75
|
+
return {
|
|
76
|
+
supportsRange: true,
|
|
77
|
+
// 206 bounds/total come from the R2ObjectBody's actual size/offset: the
|
|
78
|
+
// orchestrator may skip the validating HEAD for plain range requests.
|
|
79
|
+
authoritativeRange: true,
|
|
80
|
+
async headObject(key, opts) {
|
|
81
|
+
opts?.signal?.throwIfAborted();
|
|
82
|
+
const obj = await bucket.head(key);
|
|
83
|
+
if (!obj)
|
|
84
|
+
throw new ObjectNotFoundError(key);
|
|
85
|
+
return {
|
|
86
|
+
contentLength: obj.size,
|
|
87
|
+
etag: obj.etag,
|
|
88
|
+
lastModified: obj.uploaded.toUTCString(),
|
|
89
|
+
digest: obj.checksums.sha256
|
|
90
|
+
? arrayBufferToBase64(obj.checksums.sha256)
|
|
91
|
+
: undefined,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
async getObject(key, opts) {
|
|
95
|
+
const { range, ifMatch } = opts ?? {};
|
|
96
|
+
opts?.signal?.throwIfAborted();
|
|
97
|
+
// An open-ended fast-path range (OPEN_ENDED sentinel end) reads to the
|
|
98
|
+
// end of the object: pass offset alone so R2 streams the tail rather
|
|
99
|
+
// than requesting a ~9e15 length.
|
|
100
|
+
const r2Range = range
|
|
101
|
+
? (isOpenEndedRange(range)
|
|
102
|
+
? { offset: range.start }
|
|
103
|
+
: { offset: range.start, length: range.end - range.start + 1 })
|
|
104
|
+
: undefined;
|
|
105
|
+
const getOptions = r2Range || ifMatch
|
|
106
|
+
? {
|
|
107
|
+
...(r2Range ? { range: r2Range } : {}),
|
|
108
|
+
// Pin the read to the validated representation. On mismatch R2
|
|
109
|
+
// returns the object WITHOUT a body (checked below).
|
|
110
|
+
...(ifMatch ? { onlyIf: { etagMatches: ifMatch } } : {}),
|
|
111
|
+
}
|
|
112
|
+
: undefined;
|
|
113
|
+
const obj = await bucket.get(key, getOptions);
|
|
114
|
+
if (!obj)
|
|
115
|
+
throw new ObjectNotFoundError(key);
|
|
116
|
+
// onlyIf precondition failed: R2 resolves with metadata but no body.
|
|
117
|
+
if (!("body" in obj) || !obj.body) {
|
|
118
|
+
throw new ObjectChangedError(key);
|
|
119
|
+
}
|
|
120
|
+
const body = obj;
|
|
121
|
+
const totalSize = obj.size;
|
|
122
|
+
// Content-Range must reflect what R2 ACTUALLY returned, not what was
|
|
123
|
+
// requested. `body.range` is R2's authoritative offset/length for this
|
|
124
|
+
// response; if R2 clamped the range (object changed between HEAD and
|
|
125
|
+
// GET), fabricating from the request would corrupt the client's bytes.
|
|
126
|
+
const returned = range ? body.range : undefined;
|
|
127
|
+
const start = returned?.offset ?? range?.start;
|
|
128
|
+
const length = returned?.length
|
|
129
|
+
?? (range
|
|
130
|
+
? (isOpenEndedRange(range) ? totalSize - range.start : range.end - range.start + 1)
|
|
131
|
+
: undefined);
|
|
132
|
+
const isRanged = start !== undefined && length !== undefined;
|
|
133
|
+
const contentLength = isRanged ? length : totalSize;
|
|
134
|
+
return {
|
|
135
|
+
// Guard the committed length: if R2 ever ends the body short of the
|
|
136
|
+
// computed contentLength, error the stream rather than under-run it.
|
|
137
|
+
body: guardStreamLength(body.body, contentLength),
|
|
138
|
+
contentLength,
|
|
139
|
+
totalSize,
|
|
140
|
+
range: isRanged ? { start, end: start + length - 1 } : undefined,
|
|
141
|
+
etag: obj.etag,
|
|
142
|
+
lastModified: obj.uploaded.toUTCString(),
|
|
143
|
+
digest: obj.checksums.sha256
|
|
144
|
+
? arrayBufferToBase64(obj.checksums.sha256)
|
|
145
|
+
: undefined,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=r2.js.map
|
package/dist/r2.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2.js","sourceRoot":"","sources":["../src/r2.ts"],"names":[],"mappings":"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"}
|
package/dist/s3.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3-compatible ObjectStore adapter for partial-content.
|
|
3
|
+
*
|
|
4
|
+
* Implements the read-only {@link ObjectStore} interface from the kernel,
|
|
5
|
+
* wrapping `@aws-sdk/client-s3` HeadObject/GetObject commands. Covers:
|
|
6
|
+
* - AWS S3
|
|
7
|
+
* - Cloudflare R2 (S3-compatible mode)
|
|
8
|
+
* - Hetzner Object Storage
|
|
9
|
+
* - MinIO / Backblaze B2 / Wasabi
|
|
10
|
+
*
|
|
11
|
+
* For native R2 bindings (without the AWS SDK), use `partial-content/r2`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { S3Client } from "@aws-sdk/client-s3";
|
|
16
|
+
* import { s3Store } from "partial-content/s3";
|
|
17
|
+
*
|
|
18
|
+
* const client = new S3Client({ region: "eu-central-1" });
|
|
19
|
+
* const store = s3Store({ client, bucket: "documents" });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
25
|
+
import { ObjectNotFoundError, ObjectChangedError, StoreUnavailableError, type ObjectStore } from "./index.js";
|
|
26
|
+
export { ObjectNotFoundError, ObjectChangedError, StoreUnavailableError };
|
|
27
|
+
export interface S3StoreOptions {
|
|
28
|
+
/** Pre-configured S3Client instance (BYOC, no config coupling). */
|
|
29
|
+
client: S3Client;
|
|
30
|
+
/** The S3 bucket name. */
|
|
31
|
+
bucket: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create an {@link ObjectStore} backed by an S3-compatible bucket.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { S3Client } from "@aws-sdk/client-s3";
|
|
39
|
+
* import { s3Store } from "partial-content/s3";
|
|
40
|
+
*
|
|
41
|
+
* const client = new S3Client({ region: "eu-central-1" });
|
|
42
|
+
* const store = s3Store({ client, bucket: "documents" });
|
|
43
|
+
*
|
|
44
|
+
* // Use with partial-content's evaluateConditionalRequest:
|
|
45
|
+
* const meta = await store.headObject("reports/q4.pdf");
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function s3Store(opts: S3StoreOptions): ObjectStore;
|
|
49
|
+
//# sourceMappingURL=s3.d.ts.map
|
package/dist/s3.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,QAAQ,EAKT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EAOrB,KAAK,WAAW,EAKjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;AAiC1E,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,MAAM,EAAE,QAAQ,CAAC;IACjB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,WAAW,CA0IzD"}
|