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.
Files changed (81) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +601 -0
  4. package/SECURITY.md +92 -0
  5. package/dist/azure.d.ts +79 -0
  6. package/dist/azure.d.ts.map +1 -0
  7. package/dist/azure.js +251 -0
  8. package/dist/azure.js.map +1 -0
  9. package/dist/content-disposition.d.ts +74 -0
  10. package/dist/content-disposition.d.ts.map +1 -0
  11. package/dist/content-disposition.js +253 -0
  12. package/dist/content-disposition.js.map +1 -0
  13. package/dist/fs.d.ts +86 -0
  14. package/dist/fs.d.ts.map +1 -0
  15. package/dist/fs.js +375 -0
  16. package/dist/fs.js.map +1 -0
  17. package/dist/gcs.d.ts +72 -0
  18. package/dist/gcs.d.ts.map +1 -0
  19. package/dist/gcs.js +202 -0
  20. package/dist/gcs.js.map +1 -0
  21. package/dist/hono.d.ts +92 -0
  22. package/dist/hono.d.ts.map +1 -0
  23. package/dist/hono.js +61 -0
  24. package/dist/hono.js.map +1 -0
  25. package/dist/http.d.ts +70 -0
  26. package/dist/http.d.ts.map +1 -0
  27. package/dist/http.js +281 -0
  28. package/dist/http.js.map +1 -0
  29. package/dist/index.d.ts +21 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +27 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/kernel.d.ts +541 -0
  34. package/dist/kernel.d.ts.map +1 -0
  35. package/dist/kernel.js +1218 -0
  36. package/dist/kernel.js.map +1 -0
  37. package/dist/memory.d.ts +55 -0
  38. package/dist/memory.d.ts.map +1 -0
  39. package/dist/memory.js +107 -0
  40. package/dist/memory.js.map +1 -0
  41. package/dist/mime.d.ts +49 -0
  42. package/dist/mime.d.ts.map +1 -0
  43. package/dist/mime.js +150 -0
  44. package/dist/mime.js.map +1 -0
  45. package/dist/node.d.ts +84 -0
  46. package/dist/node.d.ts.map +1 -0
  47. package/dist/node.js +215 -0
  48. package/dist/node.js.map +1 -0
  49. package/dist/object-store.d.ts +472 -0
  50. package/dist/object-store.d.ts.map +1 -0
  51. package/dist/object-store.js +335 -0
  52. package/dist/object-store.js.map +1 -0
  53. package/dist/r2.d.ts +94 -0
  54. package/dist/r2.d.ts.map +1 -0
  55. package/dist/r2.js +150 -0
  56. package/dist/r2.js.map +1 -0
  57. package/dist/s3.d.ts +49 -0
  58. package/dist/s3.d.ts.map +1 -0
  59. package/dist/s3.js +263 -0
  60. package/dist/s3.js.map +1 -0
  61. package/dist/web.d.ts +336 -0
  62. package/dist/web.d.ts.map +1 -0
  63. package/dist/web.js +1094 -0
  64. package/dist/web.js.map +1 -0
  65. package/docs/DESIGN.md +426 -0
  66. package/package.json +182 -0
  67. package/src/azure.ts +329 -0
  68. package/src/content-disposition.ts +300 -0
  69. package/src/fs.ts +469 -0
  70. package/src/gcs.ts +290 -0
  71. package/src/hono.ts +123 -0
  72. package/src/http.ts +351 -0
  73. package/src/index.ts +85 -0
  74. package/src/kernel.ts +1498 -0
  75. package/src/memory.ts +148 -0
  76. package/src/mime.ts +160 -0
  77. package/src/node.ts +261 -0
  78. package/src/object-store.ts +665 -0
  79. package/src/r2.ts +232 -0
  80. package/src/s3.ts +324 -0
  81. package/src/web.ts +1603 -0
package/dist/node.js ADDED
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Node.js HTTP adapter for partial-content.
3
+ *
4
+ * Bridges the gap between Node.js `IncomingMessage`/`ServerResponse` and the
5
+ * Fetch-based `partial-content/web` handler. Works with Express, Fastify
6
+ * (in compatibility mode), Koa, raw `http.createServer`, and any Node.js
7
+ * HTTP framework that exposes the standard request/response objects.
8
+ *
9
+ * @example Express
10
+ * ```typescript
11
+ * import express from "express";
12
+ * import { serveObject } from "partial-content/node";
13
+ * import { fsStore } from "partial-content/fs";
14
+ *
15
+ * const store = fsStore({ root: "./uploads" });
16
+ * const app = express();
17
+ *
18
+ * app.get("/files/:key", serveObject(store, {
19
+ * key: (req) => req.params.key,
20
+ * disposition: "inline",
21
+ * }));
22
+ * ```
23
+ *
24
+ * @example raw http.createServer
25
+ * ```typescript
26
+ * import { createServer } from "node:http";
27
+ * import { serveObject } from "partial-content/node";
28
+ * import { s3Store } from "partial-content/s3";
29
+ *
30
+ * const store = s3Store({ client, bucket: "docs" });
31
+ * const handler = serveObject(store, {
32
+ * key: (req) => new URL(req.url!, `http://${req.headers.host}`).pathname.slice(1),
33
+ * });
34
+ *
35
+ * createServer((req, res) => handler(req, res)).listen(3000);
36
+ * ```
37
+ *
38
+ * @packageDocumentation
39
+ */
40
+ import { serveObjectRaw } from "./web.js";
41
+ /** Default backpressure stall bound: 60s of zero drain is a stalled reader. */
42
+ const DEFAULT_WRITE_STALL_MS = 60_000;
43
+ // ─── Factory ────────────────────────────────────────────────────────────────
44
+ /**
45
+ * Create a Node.js request handler that serves files from an {@link ObjectStore}.
46
+ *
47
+ * Converts Node.js `IncomingMessage` to a Fetch `Request`, delegates to the
48
+ * web adapter's `serveObject`, then writes the Fetch `Response` back to the
49
+ * Node.js `ServerResponse`.
50
+ */
51
+ export function serveObject(store, opts) {
52
+ const { key: keyFn, mime: mimeFn, filename: filenameFn, writeStallTimeoutMs, ...serveOpts } = opts;
53
+ const handler = serveObjectRaw(store, serveOpts);
54
+ const stallMs = writeStallTimeoutMs ?? DEFAULT_WRITE_STALL_MS;
55
+ return async function handleNodeServe(req, res) {
56
+ // Wire up client-disconnect detection: when the Node.js request closes
57
+ // (client navigates away, connection drops), signal the web adapter to
58
+ // cancel the storage stream and stop transferring bytes.
59
+ //
60
+ // The listener MUST be detached once the response is fully written:
61
+ // IncomingMessage emits "close" after every NORMAL completion too, and
62
+ // an abort() there constructs a DOMException and notifies the store's
63
+ // abort listener on every request -- measured at ~8% of serve CPU.
64
+ const ac = new AbortController();
65
+ const onClientGone = () => ac.abort();
66
+ req.once("close", onClientGone);
67
+ // Lightweight ServableRequest view: the orchestrator only reads method,
68
+ // headers.get, and signal, so constructing real fetch primitives
69
+ // (undici Request + Headers) per request would be pure overhead. Node
70
+ // already lower-cases incoming header names, so reads go straight to
71
+ // req.headers with no per-request copy; join() matches how Node folds
72
+ // repeated headers (request headers are never Set-Cookie).
73
+ const nodeHeaders = req.headers;
74
+ const fetchRequest = {
75
+ method: req.method ?? "GET",
76
+ headers: {
77
+ get(name) {
78
+ const value = nodeHeaders[name.toLowerCase()];
79
+ if (value === undefined)
80
+ return null;
81
+ return Array.isArray(value) ? value.join(", ") : value;
82
+ },
83
+ },
84
+ signal: ac.signal,
85
+ };
86
+ // Resolve context and delegate to the web adapter. Guarded: a throwing
87
+ // key/mime/filename extractor (caller code) must become a 500 response,
88
+ // not a rejected handler -- in Express 4 an async handler rejection is
89
+ // an unhandled promise rejection, which kills the process.
90
+ let parts;
91
+ try {
92
+ const key = await keyFn(req);
93
+ const ctx = {
94
+ key,
95
+ mime: mimeFn?.(req),
96
+ filename: filenameFn?.(req),
97
+ };
98
+ parts = await handler(fetchRequest, ctx);
99
+ }
100
+ catch {
101
+ if (!res.headersSent) {
102
+ const body = "Internal Server Error";
103
+ res.writeHead(500, {
104
+ "Content-Type": "text/plain; charset=utf-8",
105
+ "Content-Length": String(body.length),
106
+ "X-Content-Type-Options": "nosniff",
107
+ });
108
+ res.end(body);
109
+ }
110
+ else if (!res.destroyed) {
111
+ res.destroy();
112
+ }
113
+ req.off("close", onClientGone);
114
+ return;
115
+ }
116
+ // Write the raw parts straight to the ServerResponse: no fetch
117
+ // primitives were constructed anywhere on this path. Preserves custom
118
+ // reason phrases (e.g. 499 Client Closed Request). The outer try/finally
119
+ // is load-bearing: writeHead and end can throw SYNCHRONOUSLY (socket
120
+ // destroyed in the await window, a header value the runtime rejects),
121
+ // and that throw must not leak the storage stream, leave the disconnect
122
+ // listener attached, or escape as an unhandled rejection (which kills
123
+ // Express 4 processes).
124
+ try {
125
+ res.writeHead(parts.status, parts.statusText || undefined, parts.headers);
126
+ if (!parts.body) {
127
+ res.end();
128
+ return;
129
+ }
130
+ // Byte bodies (in-memory stores, fs small-transfer fast path, error
131
+ // pages): a single write, no reader machinery.
132
+ if (parts.body instanceof Uint8Array) {
133
+ res.end(parts.body);
134
+ return;
135
+ }
136
+ // Stream the response body with backpressure. Client disconnects abort
137
+ // the storage stream (via the signal above), which rejects reader.read();
138
+ // that rejection must be contained here, not escape the handler.
139
+ const reader = parts.body.getReader();
140
+ try {
141
+ while (true) {
142
+ const { done, value } = await reader.read();
143
+ if (done)
144
+ break;
145
+ if (res.destroyed) {
146
+ // Client is gone; stop pulling and release the storage stream.
147
+ await reader.cancel();
148
+ break;
149
+ }
150
+ const canContinue = res.write(value);
151
+ if (!canContinue) {
152
+ // Backpressure: wait for drain, or for the response to close
153
+ // (client disconnect) so a stalled socket cannot hang this loop.
154
+ // A stall timeout bounds a reader that stops consuming but holds
155
+ // the socket open: it rejects into the catch below, which cancels
156
+ // the storage reader and destroys the response.
157
+ await new Promise((resolve, reject) => {
158
+ let timer;
159
+ const cleanup = () => {
160
+ if (timer !== undefined)
161
+ clearTimeout(timer);
162
+ res.off("drain", onDone);
163
+ res.off("close", onDone);
164
+ };
165
+ const onDone = () => {
166
+ cleanup();
167
+ resolve();
168
+ };
169
+ if (stallMs > 0) {
170
+ timer = setTimeout(() => {
171
+ cleanup();
172
+ reject(new Error("partial-content: response write stalled"));
173
+ }, stallMs);
174
+ // Do not keep the event loop alive solely for this timer.
175
+ timer.unref?.();
176
+ }
177
+ res.once("drain", onDone);
178
+ res.once("close", onDone);
179
+ });
180
+ }
181
+ }
182
+ res.end();
183
+ }
184
+ catch {
185
+ // Transfer failed mid-stream (disconnect or storage error). Headers are
186
+ // already sent, so no error response is possible; tear the socket down
187
+ // so the client sees a truncated transfer instead of a hang.
188
+ await reader.cancel().catch(() => {
189
+ // Stream may already be errored; cancel is best-effort
190
+ });
191
+ if (!res.destroyed)
192
+ res.destroy();
193
+ }
194
+ finally {
195
+ reader.releaseLock();
196
+ }
197
+ }
198
+ catch {
199
+ // writeHead/end threw synchronously. The pump above never rethrows, so
200
+ // this only catches pre-stream failures: release an unconsumed stream
201
+ // body (fs handle / backend socket) and tear the connection down.
202
+ if (parts.body instanceof ReadableStream) {
203
+ await parts.body.cancel().catch(() => {
204
+ // Already locked or errored; best-effort release
205
+ });
206
+ }
207
+ if (!res.destroyed)
208
+ res.destroy();
209
+ }
210
+ finally {
211
+ req.off("close", onClientGone);
212
+ }
213
+ };
214
+ }
215
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}