@fedify/koa 2.4.0-pr.936.41 → 2.4.1

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