@fedify/express 2.4.0-pr.936.41 → 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 +87 -4
- package/dist/index.js +87 -4
- package/package.json +6 -4
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
49
|
+
"@fedify/fedify": "^2.4.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/express": "^4.0.0",
|
|
53
|
-
"@types/node": "^
|
|
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
|
}
|