@fedify/express 2.4.0-pr.934.40 → 2.4.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/dist/index.cjs CHANGED
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let node_buffer = require("node:buffer");
3
- let node_stream = require("node:stream");
4
3
  //#region src/index.ts
5
4
  function integrateFederation(federation, contextDataFactory) {
6
5
  return (req, res, next) => {
7
- const request = fromERequest(req);
6
+ const body = req.method === "GET" || req.method === "HEAD" ? void 0 : createRequestBody(req);
7
+ const request = fromERequest(req, body?.stream);
8
8
  const contextData = contextDataFactory(req);
9
9
  (contextData instanceof Promise ? contextData : Promise.resolve(contextData)).then(async (contextData) => {
10
10
  let notFound = false;
@@ -13,11 +13,13 @@ function integrateFederation(federation, contextDataFactory) {
13
13
  contextData,
14
14
  onNotFound: () => {
15
15
  notFound = true;
16
+ body?.restore();
16
17
  next();
17
18
  return new Response("Not found", { status: 404 });
18
19
  },
19
20
  onNotAcceptable: () => {
20
21
  notAcceptable = true;
22
+ body?.restore();
21
23
  next();
22
24
  return new Response("Not acceptable", {
23
25
  status: 406,
@@ -40,7 +42,7 @@ function integrateFederation(federation, contextDataFactory) {
40
42
  });
41
43
  };
42
44
  }
43
- function fromERequest(req) {
45
+ function fromERequest(req, body) {
44
46
  const url = `${req.protocol}://${req.host}${req.url}`;
45
47
  const headers = new Headers();
46
48
  for (const [key, value] of Object.entries(req.headers)) if (Array.isArray(value)) for (const v of value) headers.append(key, v);
@@ -49,7 +51,88 @@ function fromERequest(req) {
49
51
  method: req.method,
50
52
  headers,
51
53
  duplex: "half",
52
- body: req.method === "GET" || req.method === "HEAD" ? void 0 : node_stream.Readable.toWeb(req)
54
+ body
55
+ });
56
+ }
57
+ /**
58
+ * Wraps the raw `IncomingMessage` in a `ReadableStream` that stays inert until
59
+ * first read, and that keeps the chunks it reads so that they can be put back.
60
+ *
61
+ * The `Request` is built before Fedify decides whether it handles the request.
62
+ * Laziness alone is not enough: `Request.clone()` tees the body, and a tee pulls
63
+ * a chunk right away, so a dispatcher calling `ctx.getSignedKey()` starts
64
+ * reading the body even if Fedify then declines the request. Reading in paused
65
+ * mode, rather than through `Readable.toWeb()`, lets `restore()` unshift those
66
+ * chunks back.
67
+ */
68
+ function createRequestBody(message) {
69
+ const consumed = [];
70
+ const restoring = new AbortController();
71
+ let started = false;
72
+ let restored = false;
73
+ return {
74
+ stream: new ReadableStream({
75
+ async pull(controller) {
76
+ started = true;
77
+ while (!restored) {
78
+ const size = message.readableLength;
79
+ const chunk = size > 0 ? message.read(size) : null;
80
+ if (chunk != null) {
81
+ const bytes = typeof chunk === "string" ? node_buffer.Buffer.from(chunk) : chunk;
82
+ consumed.push(bytes);
83
+ controller.enqueue(bytes);
84
+ return;
85
+ }
86
+ if (isEnded(message)) break;
87
+ if (message.destroyed) throw new Error("The request was closed before its body ended.");
88
+ await waitForReadable(message, restoring.signal);
89
+ }
90
+ controller.close();
91
+ },
92
+ cancel(reason) {
93
+ if (started && !restored) message.destroy(reason instanceof Error ? reason : void 0);
94
+ }
95
+ }, { highWaterMark: 0 }),
96
+ restore() {
97
+ restored = true;
98
+ restoring.abort();
99
+ for (let i = consumed.length - 1; i >= 0; i--) message.unshift(consumed[i]);
100
+ consumed.length = 0;
101
+ }
102
+ };
103
+ }
104
+ /**
105
+ * Tells whether the stream has received all of its data, even if it has not
106
+ * emitted `"end"` yet. No public API exposes this, so it reads the internal
107
+ * state, which Node.js, Deno, and Bun all provide, and falls back to
108
+ * `readableEnded` elsewhere.
109
+ */
110
+ function isEnded(message) {
111
+ return message._readableState?.ended ?? message.readableEnded;
112
+ }
113
+ function waitForReadable(message, signal) {
114
+ return new Promise((resolve, reject) => {
115
+ if (signal.aborted) return resolve();
116
+ const cleanup = () => {
117
+ message.off("readable", onReady);
118
+ message.off("end", onReady);
119
+ message.off("close", onReady);
120
+ message.off("error", onError);
121
+ signal.removeEventListener("abort", onReady);
122
+ };
123
+ const onReady = () => {
124
+ cleanup();
125
+ resolve();
126
+ };
127
+ const onError = (error) => {
128
+ cleanup();
129
+ reject(error);
130
+ };
131
+ message.on("readable", onReady);
132
+ message.on("end", onReady);
133
+ message.on("close", onReady);
134
+ message.on("error", onError);
135
+ signal.addEventListener("abort", onReady);
53
136
  });
54
137
  }
55
138
  function setEResponse(res, response) {
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { Readable } from "node:stream";
3
2
  //#region src/index.ts
4
3
  function integrateFederation(federation, contextDataFactory) {
5
4
  return (req, res, next) => {
6
- const request = fromERequest(req);
5
+ const body = req.method === "GET" || req.method === "HEAD" ? void 0 : createRequestBody(req);
6
+ const request = fromERequest(req, body?.stream);
7
7
  const contextData = contextDataFactory(req);
8
8
  (contextData instanceof Promise ? contextData : Promise.resolve(contextData)).then(async (contextData) => {
9
9
  let notFound = false;
@@ -12,11 +12,13 @@ function integrateFederation(federation, contextDataFactory) {
12
12
  contextData,
13
13
  onNotFound: () => {
14
14
  notFound = true;
15
+ body?.restore();
15
16
  next();
16
17
  return new Response("Not found", { status: 404 });
17
18
  },
18
19
  onNotAcceptable: () => {
19
20
  notAcceptable = true;
21
+ body?.restore();
20
22
  next();
21
23
  return new Response("Not acceptable", {
22
24
  status: 406,
@@ -39,7 +41,7 @@ function integrateFederation(federation, contextDataFactory) {
39
41
  });
40
42
  };
41
43
  }
42
- function fromERequest(req) {
44
+ function fromERequest(req, body) {
43
45
  const url = `${req.protocol}://${req.host}${req.url}`;
44
46
  const headers = new Headers();
45
47
  for (const [key, value] of Object.entries(req.headers)) if (Array.isArray(value)) for (const v of value) headers.append(key, v);
@@ -48,7 +50,88 @@ function fromERequest(req) {
48
50
  method: req.method,
49
51
  headers,
50
52
  duplex: "half",
51
- body: req.method === "GET" || req.method === "HEAD" ? void 0 : Readable.toWeb(req)
53
+ body
54
+ });
55
+ }
56
+ /**
57
+ * Wraps the raw `IncomingMessage` in a `ReadableStream` that stays inert until
58
+ * first read, and that keeps the chunks it reads so that they can be put back.
59
+ *
60
+ * The `Request` is built before Fedify decides whether it handles the request.
61
+ * Laziness alone is not enough: `Request.clone()` tees the body, and a tee pulls
62
+ * a chunk right away, so a dispatcher calling `ctx.getSignedKey()` starts
63
+ * reading the body even if Fedify then declines the request. Reading in paused
64
+ * mode, rather than through `Readable.toWeb()`, lets `restore()` unshift those
65
+ * chunks back.
66
+ */
67
+ function createRequestBody(message) {
68
+ const consumed = [];
69
+ const restoring = new AbortController();
70
+ let started = false;
71
+ let restored = false;
72
+ return {
73
+ stream: new ReadableStream({
74
+ async pull(controller) {
75
+ started = true;
76
+ while (!restored) {
77
+ const size = message.readableLength;
78
+ const chunk = size > 0 ? message.read(size) : null;
79
+ if (chunk != null) {
80
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
81
+ consumed.push(bytes);
82
+ controller.enqueue(bytes);
83
+ return;
84
+ }
85
+ if (isEnded(message)) break;
86
+ if (message.destroyed) throw new Error("The request was closed before its body ended.");
87
+ await waitForReadable(message, restoring.signal);
88
+ }
89
+ controller.close();
90
+ },
91
+ cancel(reason) {
92
+ if (started && !restored) message.destroy(reason instanceof Error ? reason : void 0);
93
+ }
94
+ }, { highWaterMark: 0 }),
95
+ restore() {
96
+ restored = true;
97
+ restoring.abort();
98
+ for (let i = consumed.length - 1; i >= 0; i--) message.unshift(consumed[i]);
99
+ consumed.length = 0;
100
+ }
101
+ };
102
+ }
103
+ /**
104
+ * Tells whether the stream has received all of its data, even if it has not
105
+ * emitted `"end"` yet. No public API exposes this, so it reads the internal
106
+ * state, which Node.js, Deno, and Bun all provide, and falls back to
107
+ * `readableEnded` elsewhere.
108
+ */
109
+ function isEnded(message) {
110
+ return message._readableState?.ended ?? message.readableEnded;
111
+ }
112
+ function waitForReadable(message, signal) {
113
+ return new Promise((resolve, reject) => {
114
+ if (signal.aborted) return resolve();
115
+ const cleanup = () => {
116
+ message.off("readable", onReady);
117
+ message.off("end", onReady);
118
+ message.off("close", onReady);
119
+ message.off("error", onError);
120
+ signal.removeEventListener("abort", onReady);
121
+ };
122
+ const onReady = () => {
123
+ cleanup();
124
+ resolve();
125
+ };
126
+ const onError = (error) => {
127
+ cleanup();
128
+ reject(error);
129
+ };
130
+ message.on("readable", onReady);
131
+ message.on("end", onReady);
132
+ message.on("close", onReady);
133
+ message.on("error", onError);
134
+ signal.addEventListener("abort", onReady);
52
135
  });
53
136
  }
54
137
  function setEResponse(res, response) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/express",
3
- "version": "2.4.0-pr.934.40+3331d302",
3
+ "version": "2.4.0",
4
4
  "description": "Integrate Fedify with Express",
5
5
  "keywords": [
6
6
  "Fedify",
@@ -46,17 +46,19 @@
46
46
  ],
47
47
  "peerDependencies": {
48
48
  "express": ">=4.0.0",
49
- "@fedify/fedify": "^2.4.0-pr.934.40+3331d302"
49
+ "@fedify/fedify": "^2.4.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/express": "^4.0.0",
53
- "@types/node": "^22.17.0",
53
+ "@types/node": "^24.12.2",
54
54
  "tsdown": "^0.22.0",
55
55
  "typescript": "^6.0.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build:self": "tsdown",
59
59
  "build": "pnpm --filter @fedify/express... run build:self",
60
- "prepublish": "pnpm build"
60
+ "prepublish": "pnpm build",
61
+ "test": "node --experimental-transform-types --test",
62
+ "test:bun": "bun test src/"
61
63
  }
62
64
  }