diff --git a/.changeset/whole-planes-mate.md b/.changeset/whole-planes-mate.md new file mode 100644 index 00000000000..e92ff5d60a3 --- /dev/null +++ b/.changeset/whole-planes-mate.md @@ -0,0 +1,17 @@ +--- +'@whatwg-node/node-fetch': patch +'@whatwg-node/fetch': patch +'@whatwg-node/server': patch +--- + +Some implementations like `compression` npm package do not implement `response.write(data, callback)` signature, but whatwg-node/server waits for it to finish the response stream. +Then it causes the response stream hangs when the compression package takes the stream over when the response data is larger than its threshold. + +It is actually a bug in `compression` package; +[expressjs/compression#46](https://github.com/expressjs/compression/issues/46) +But since it is a common mistake, we prefer to workaround this on our end. + +Now after calling `response.write`, it no longer uses callback but first it checks the result; + +if it is `true`, it means stream is drained and we can call `response.end` immediately. +else if it is `false`, it means the stream is not drained yet, so we can wait for the `drain` event to call `response.end`. diff --git a/packages/node-fetch/src/FormData.ts b/packages/node-fetch/src/FormData.ts index 8d92decb94a..10dbd86afb9 100644 --- a/packages/node-fetch/src/FormData.ts +++ b/packages/node-fetch/src/FormData.ts @@ -94,55 +94,68 @@ export function getStreamFromFormData( formData: FormData, boundary = '---', ): PonyfillReadableStream { - const entries: [string, string | PonyfillFile][] = []; + let entriesIterator: FormDataIterator<[string, FormDataEntryValue]>; let sentInitialHeader = false; - return new PonyfillReadableStream({ - start: controller => { - formData.forEach((value, key) => { - if (!sentInitialHeader) { - controller.enqueue(Buffer.from(`--${boundary}\r\n`)); - sentInitialHeader = true; + let currentAsyncIterator: AsyncIterator | undefined; + let hasBefore = false; + function handleNextEntry(controller: ReadableStreamController) { + const { done, value } = entriesIterator.next(); + if (done) { + controller.enqueue(Buffer.from(`\r\n--${boundary}--\r\n`)); + return controller.close(); + } + if (hasBefore) { + controller.enqueue(Buffer.from(`\r\n--${boundary}\r\n`)); + } + if (value) { + const [key, blobOrString] = value; + if (typeof blobOrString === 'string') { + controller.enqueue(Buffer.from(`Content-Disposition: form-data; name="${key}"\r\n\r\n`)); + controller.enqueue(Buffer.from(blobOrString)); + } else { + let filenamePart = ''; + if (blobOrString.name) { + filenamePart = `; filename="${blobOrString.name}"`; } - entries.push([key, value as any]); - }); - if (!sentInitialHeader) { - controller.enqueue(Buffer.from(`--${boundary}--\r\n`)); - controller.close(); + controller.enqueue( + Buffer.from(`Content-Disposition: form-data; name="${key}"${filenamePart}\r\n`), + ); + controller.enqueue( + Buffer.from(`Content-Type: ${blobOrString.type || 'application/octet-stream'}\r\n\r\n`), + ); + const entryStream = blobOrString.stream(); + // @ts-expect-error - ReadableStream is async iterable + currentAsyncIterator = entryStream[Symbol.asyncIterator](); } + hasBefore = true; + } + } + return new PonyfillReadableStream({ + start: () => { + entriesIterator = formData.entries(); }, - pull: async controller => { - const entry = entries.shift(); - if (entry) { - const [key, value] = entry; - if (typeof value === 'string') { - controller.enqueue(Buffer.from(`Content-Disposition: form-data; name="${key}"\r\n\r\n`)); - controller.enqueue(Buffer.from(value)); - } else { - let filenamePart = ''; - if (value.name) { - filenamePart = `; filename="${value.name}"`; + pull: controller => { + if (!sentInitialHeader) { + sentInitialHeader = true; + return controller.enqueue(Buffer.from(`--${boundary}\r\n`)); + } + if (currentAsyncIterator) { + return currentAsyncIterator.next().then(({ done, value }) => { + if (done) { + currentAsyncIterator = undefined; } - controller.enqueue( - Buffer.from(`Content-Disposition: form-data; name="${key}"${filenamePart}\r\n`), - ); - controller.enqueue( - Buffer.from(`Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`), - ); - const entryStream = value.stream(); - for await (const chunk of entryStream) { - controller.enqueue(chunk); + if (value) { + return controller.enqueue(value); + } else { + return handleNextEntry(controller); } - } - if (entries.length === 0) { - controller.enqueue(Buffer.from(`\r\n--${boundary}--\r\n`)); - controller.close(); - } else { - controller.enqueue(Buffer.from(`\r\n--${boundary}\r\n`)); - } - } else { - controller.enqueue(Buffer.from(`\r\n--${boundary}--\r\n`)); - controller.close(); + }); } + return handleNextEntry(controller); + }, + cancel: err => { + entriesIterator?.return?.(err); + currentAsyncIterator?.return?.(err); }, }); } diff --git a/packages/node-fetch/src/TransformStream.ts b/packages/node-fetch/src/TransformStream.ts index 2e54c8ef6de..cc12903e2f2 100644 --- a/packages/node-fetch/src/TransformStream.ts +++ b/packages/node-fetch/src/TransformStream.ts @@ -1,5 +1,6 @@ import { Transform } from 'node:stream'; import { PonyfillReadableStream } from './ReadableStream.js'; +import { endStream } from './utils.js'; import { PonyfillWritableStream } from './WritableStream.js'; export class PonyfillTransformStream implements TransformStream { @@ -19,7 +20,7 @@ export class PonyfillTransformStream implements TransformStrea transform.destroy(reason); }, terminate() { - transform.end(); + endStream(transform); }, get desiredSize() { return transform.writableLength; diff --git a/packages/node-fetch/src/WritableStream.ts b/packages/node-fetch/src/WritableStream.ts index 94ffc64c1d9..ceee2f8c5b4 100644 --- a/packages/node-fetch/src/WritableStream.ts +++ b/packages/node-fetch/src/WritableStream.ts @@ -1,5 +1,6 @@ import { Writable } from 'node:stream'; -import { fakePromise } from './utils.js'; +import { fakeRejectPromise } from '@whatwg-node/promise-helpers'; +import { endStream, fakePromise, safeWrite } from './utils.js'; export class PonyfillWritableStream implements WritableStream { writable: Writable; @@ -78,36 +79,20 @@ export class PonyfillWritableStream implements WritableStream { // no-op }, write(chunk: W) { + const promise = fakePromise(); if (chunk == null) { - return fakePromise(); + return promise; } - return new Promise((resolve, reject) => { - writable.write(chunk, (err: Error | null | undefined) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return promise.then(() => safeWrite(chunk, writable)); }, close() { if (!writable.errored && writable.closed) { return fakePromise(); } - return new Promise((resolve, reject) => { - if (writable.errored) { - reject(writable.errored); - } else { - writable.end((err: Error | null) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - } - }); + if (writable.errored) { + return fakeRejectPromise(writable.errored); + } + return fakePromise().then(() => endStream(writable)); }, abort(reason) { return new Promise(resolve => { @@ -122,19 +107,10 @@ export class PonyfillWritableStream implements WritableStream { if (!this.writable.errored && this.writable.closed) { return fakePromise(); } - return new Promise((resolve, reject) => { - if (this.writable.errored) { - reject(this.writable.errored); - } else { - this.writable.end((err: Error | null) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - } - }); + if (this.writable.errored) { + return fakeRejectPromise(this.writable.errored); + } + return fakePromise().then(() => endStream(this.writable)); } abort(reason: any): Promise { diff --git a/packages/node-fetch/src/fetchNodeHttp.ts b/packages/node-fetch/src/fetchNodeHttp.ts index 120e3d09884..169f464c2a4 100644 --- a/packages/node-fetch/src/fetchNodeHttp.ts +++ b/packages/node-fetch/src/fetchNodeHttp.ts @@ -2,12 +2,15 @@ import { request as httpRequest, STATUS_CODES } from 'node:http'; import { request as httpsRequest } from 'node:https'; import { PassThrough, Readable } from 'node:stream'; import { createBrotliDecompress, createGunzip, createInflate, createInflateRaw } from 'node:zlib'; +import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import { PonyfillRequest } from './Request.js'; import { PonyfillResponse } from './Response.js'; import { PonyfillURL } from './URL.js'; import { + endStream, getHeadersObj, isNodeReadable, + safeWrite, shouldRedirect, wrapIncomingMessageWithPassthrough, } from './utils.js'; @@ -56,6 +59,7 @@ export function fetchNodeHttp( }); } + nodeRequest.once('error', reject); nodeRequest.once('response', nodeResponse => { let outputStream: PassThrough | undefined; const contentEncoding = nodeResponse.headers['content-encoding']; @@ -125,12 +129,13 @@ export function fetchNodeHttp( }); resolve(ponyfillResponse); }); - nodeRequest.once('error', reject); if (fetchRequest['_buffer'] != null) { - nodeRequest.write(fetchRequest['_buffer']); - // @ts-expect-error Avoid arguments adaptor trampoline https://v8.dev/blog/adaptor-frame - nodeRequest.end(null, null, null); + handleMaybePromise( + () => safeWrite(fetchRequest['_buffer'], nodeRequest), + () => endStream(nodeRequest), + reject, + ); } else { const nodeReadable = ( fetchRequest.body != null @@ -142,8 +147,7 @@ export function fetchNodeHttp( if (nodeReadable) { nodeReadable.pipe(nodeRequest); } else { - // @ts-expect-error Avoid arguments adaptor trampoline https://v8.dev/blog/adaptor-frame - nodeRequest.end(null, null, null); + endStream(nodeRequest); } } } catch (e) { diff --git a/packages/node-fetch/src/utils.ts b/packages/node-fetch/src/utils.ts index 2660e51ef98..7e183e2e00a 100644 --- a/packages/node-fetch/src/utils.ts +++ b/packages/node-fetch/src/utils.ts @@ -70,3 +70,18 @@ export function wrapIncomingMessageWithPassthrough({ .catch(onError); return passThrough; } + +export function endStream(stream: { end: () => void }) { + // @ts-expect-error Avoid arguments adaptor trampoline https://v8.dev/blog/adaptor-frame + return stream.end(null, null, null); +} + +export function safeWrite( + chunk: any, + stream: { write: (chunk: any) => boolean; once: (event: string, listener: () => void) => void }, +) { + const result = stream.write(chunk); + if (!result) { + return new Promise(resolve => stream.once('drain', resolve)); + } +} diff --git a/packages/promise-helpers/tests/handleMaybePromise.spec.ts b/packages/promise-helpers/tests/handleMaybePromise.spec.ts index a75cee3a40f..0c86ca4e58e 100644 --- a/packages/promise-helpers/tests/handleMaybePromise.spec.ts +++ b/packages/promise-helpers/tests/handleMaybePromise.spec.ts @@ -64,42 +64,45 @@ describe('promise-helpers', () => { }, ); - it.each(cases)('when fake value is falsy', ({ input, output }) => { - expect( - handleMaybePromise( - () => (input === 'fake' ? fakePromise(undefined) : undefined), - res => (output === 'fake' ? fakePromise(undefined) : res), - ), - ).toBe(undefined); + it.each(cases)( + 'when fake value is falsy; input: $input output: $output', + ({ input, output }) => { + expect( + handleMaybePromise( + () => (input === 'fake' ? fakePromise(undefined) : undefined), + res => (output === 'fake' ? fakePromise(undefined) : res), + ), + ).toBe(undefined); - expect( - handleMaybePromise( - () => (input === 'fake' ? fakePromise(null) : null), - res => (output === 'fake' ? fakePromise(null) : res), - ), - ).toBe(null); + expect( + handleMaybePromise( + () => (input === 'fake' ? fakePromise(null) : null), + res => (output === 'fake' ? fakePromise(null) : res), + ), + ).toBe(null); - expect( - handleMaybePromise( - () => (input === 'fake' ? fakePromise('') : ''), - res => (output === 'fake' ? fakePromise('') : res), - ), - ).toBe(''); + expect( + handleMaybePromise( + () => (input === 'fake' ? fakePromise('') : ''), + res => (output === 'fake' ? fakePromise('') : res), + ), + ).toBe(''); - expect( - handleMaybePromise( - () => (input === 'fake' ? fakePromise(false) : false), - res => (output === 'fake' ? fakePromise(false) : res), - ), - ).toBe(false); + expect( + handleMaybePromise( + () => (input === 'fake' ? fakePromise(false) : false), + res => (output === 'fake' ? fakePromise(false) : res), + ), + ).toBe(false); - expect( - handleMaybePromise( - () => (input === 'fake' ? fakePromise(0) : 0), - res => (output === 'fake' ? fakePromise(0) : res), - ), - ).toBe(0); - }); + expect( + handleMaybePromise( + () => (input === 'fake' ? fakePromise(0) : 0), + res => (output === 'fake' ? fakePromise(0) : res), + ), + ).toBe(0); + }, + ); }); describe('finally', () => { describe('with promises', () => { diff --git a/packages/server/package.json b/packages/server/package.json index 5c97e4e12f4..f14d304270d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -41,14 +41,16 @@ "tslib": "^2.6.3" }, "devDependencies": { - "@hapi/hapi": "^21.3.12", + "@hapi/hapi": "21.4.0", + "@types/compression": "1.7.5", "@types/express": "5.0.1", - "@types/koa": "^2.15.0", + "@types/koa": "2.15.0", "@types/node": "22.15.17", + "compression": "1.8.0", "express": "5.1.0", "fastify": "5.3.2", - "form-data": "^4.0.2", - "koa": "^3.0.0", + "form-data": "4.0.2", + "koa": "3.0.0", "react": "19.1.0", "react-dom": "19.1.0" }, diff --git a/packages/server/src/createServerAdapter.ts b/packages/server/src/createServerAdapter.ts index 59555de69eb..39c6e6ffba4 100644 --- a/packages/server/src/createServerAdapter.ts +++ b/packages/server/src/createServerAdapter.ts @@ -30,7 +30,6 @@ import { isServerResponse, iterateAsyncVoid, NodeRequest, - nodeRequestResponseMap, NodeResponse, normalizeNodeRequest, sendNodeResponse, @@ -278,8 +277,13 @@ function createServerAdapter< ) { const nodeResponse: NodeResponse = (nodeResponseOrContainer as any).raw || nodeResponseOrContainer; - nodeRequestResponseMap.set(nodeRequest, nodeResponse); - return handleNodeRequest(nodeRequest, ...ctx); + const serverContext = ctx.length > 1 ? completeAssign(...ctx) : ctx[0] || {}; + // Ensure `waitUntil` is available in the server context + if (!serverContext.waitUntil) { + serverContext.waitUntil = waitUntil; + } + const request = normalizeNodeRequest(nodeRequest, fetchAPI, nodeResponse); + return handleRequest(request, serverContext); } function requestListener( diff --git a/packages/server/src/utils.ts b/packages/server/src/utils.ts index 10664a9abf9..14433de2568 100644 --- a/packages/server/src/utils.ts +++ b/packages/server/src/utils.ts @@ -5,6 +5,7 @@ import type { Readable } from 'node:stream'; import { createDeferredPromise, fakePromise, + handleMaybePromise, isPromise, MaybePromise, } from '@whatwg-node/promise-helpers'; @@ -90,9 +91,11 @@ function isRequestBody(body: any): body is BodyInit { return false; } -export const nodeRequestResponseMap = new WeakMap(); - -export function normalizeNodeRequest(nodeRequest: NodeRequest, fetchAPI: FetchAPI): Request { +export function normalizeNodeRequest( + nodeRequest: NodeRequest, + fetchAPI: FetchAPI, + nodeResponse?: NodeResponse, +): Request { const rawRequest = nodeRequest.raw || nodeRequest.req || nodeRequest; let fullUrl = buildFullUrl(rawRequest); if (nodeRequest.query) { @@ -103,8 +106,6 @@ export function normalizeNodeRequest(nodeRequest: NodeRequest, fetchAPI: FetchAP fullUrl = url.toString(); } - const nodeResponse = nodeRequestResponseMap.get(nodeRequest); - nodeRequestResponseMap.delete(nodeRequest); let normalizedHeaders: Record = nodeRequest.headers; if (nodeRequest.headers?.[':method']) { normalizedHeaders = {}; @@ -285,38 +286,22 @@ function sendAsyncIterable(serverResponse: NodeResponse, asyncIterable: AsyncIte if (closed || done) { return; } - return new Promise(resolve => { - if ( - !serverResponse - // @ts-expect-error http and http2 writes are actually compatible - .write(value, err => { - if (err) { - resolve(true); - } - }) - ) { - if (closed) { - resolve(true); - return; - } - serverResponse.once('drain', () => { - resolve(false); - }); - } - }) - .then(shouldBreak => { - if (shouldBreak) { - return; - } - return pump(); - }) - .then(() => { - endResponse(serverResponse); - }); + return handleMaybePromise( + () => safeWrite(value, serverResponse), + () => (closed ? endResponse(serverResponse) : pump()), + ); }); return pump(); } +function safeWrite(chunk: any, serverResponse: NodeResponse) { + // @ts-expect-error http and http2 writes are actually compatible + const result = serverResponse.write(chunk); + if (!result) { + return new Promise(resolve => serverResponse.once('drain', resolve)); + } +} + export function sendNodeResponse( fetchResponse: Response, serverResponse: NodeResponse, @@ -354,9 +339,10 @@ export function sendNodeResponse( // @ts-expect-error - _buffer is a private property fetchResponse._buffer; if (bufOfRes) { - // @ts-expect-error http and http2 writes are actually compatible - serverResponse.write(bufOfRes, () => endResponse(serverResponse)); - return; + return handleMaybePromise( + () => safeWrite(bufOfRes, serverResponse), + () => endResponse(serverResponse), + ); } // Other fetch implementations @@ -370,11 +356,10 @@ export function sendNodeResponse( // @ts-expect-error - Uint8Array is a valid body type fetchBody[Symbol.toStringTag] === 'Uint8Array' ) { - serverResponse - // @ts-expect-error http and http2 writes are actually compatible - .write(fetchBody); - endResponse(serverResponse); - return; + return handleMaybePromise( + () => safeWrite(fetchBody, serverResponse), + () => endResponse(serverResponse), + ); } configureSocket(nodeRequest); @@ -398,7 +383,7 @@ export function sendNodeResponse( } } -async function sendReadableStream( +function sendReadableStream( nodeRequest: NodeRequest, serverResponse: NodeResponse, readableStream: ReadableStream, @@ -407,20 +392,16 @@ async function sendReadableStream( nodeRequest?.once?.('error', err => { reader.cancel(err); }); - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - if ( - !serverResponse - // @ts-expect-error http and http2 writes are actually compatible - .write(value) - ) { - await new Promise(resolve => serverResponse.once('drain', resolve)); - } + function pump(): Promise { + return reader + .read() + .then(({ done, value }) => + done + ? endResponse(serverResponse) + : handleMaybePromise(() => safeWrite(value, serverResponse), pump), + ); } - endResponse(serverResponse); + return pump(); } export function isRequestInit(val: unknown): val is RequestInit { diff --git a/packages/server/test/reproductions.spec.ts b/packages/server/test/reproductions.spec.ts index d47418b18de..aa32dab8f72 100644 --- a/packages/server/test/reproductions.spec.ts +++ b/packages/server/test/reproductions.spec.ts @@ -1,5 +1,6 @@ import { createServer, Server } from 'node:http'; import { AddressInfo } from 'node:net'; +import compression from 'compression'; import express from 'express'; import { afterEach, expect, it } from '@jest/globals'; import { fetch } from '@whatwg-node/fetch'; @@ -79,3 +80,48 @@ if (!globalThis.Bun && !globalThis.Deno) { expect(await req!.text()).toEqual('hello world'); }); } + +const bodies = [ + 'hello world', // 11 bytes + 'hello world'.repeat(1024 * 1024), // 1MB + 'hello world'.repeat(1024 * 1024 * 5), // 5MB +]; + +for (const largeBody of bodies) { + it(`express + compression (${largeBody.length} bytes)`, async () => { + const app = express(); + + app.use(compression()); + + const echoAdapter = createServerAdapter(req => + req.json().then(body => + Response.json({ + body, + url: req.url, + }), + ), + ); + + app.use('/my-path', echoAdapter); + + server = await new Promise((resolve, reject) => { + const server = app.listen(0, err => (err ? reject(err) : resolve(server))); + }); + + const port = (server.address() as AddressInfo).port; + + const response = await fetch(`http://localhost:${port}/my-path`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ largeBody }), + }); + + const bodyJson = await response.json(); + expect(bodyJson).toEqual({ + body: { largeBody }, + url: `http://localhost:${port}/my-path`, + }); + }); +} diff --git a/packages/server/test/test-server.ts b/packages/server/test/test-server.ts index 80d60f52533..4a757a55852 100644 --- a/packages/server/test/test-server.ts +++ b/packages/server/test/test-server.ts @@ -209,9 +209,8 @@ if (!globalThis.Deno) { fastifyApp.addContentTypeParser(/(.*)/, {}, (_req, _payload, done) => done(null)); - let url = await fastifyApp.listen({ port: 0, host: '::1' }); - url = url.replace('127.0.0.1', 'localhost'); - url = url.replace('[::1]', 'localhost'); + await fastifyApp.listen({ port: 0, host: '::' }); + const url = `http://localhost:${(fastifyApp.server.address() as AddressInfo).port}`; return { name: 'fastify', url, diff --git a/test.mjs b/test.mjs new file mode 100644 index 00000000000..da55047efbd --- /dev/null +++ b/test.mjs @@ -0,0 +1,8 @@ +import { createServer } from 'node:http'; +import { Response } from '@whatwg-node/fetch'; +import { createServerAdapter } from '@whatwg-node/server'; + +createServer(createServerAdapter(() => Response.json({ hello: 'world' }))).listen(3000, () => { + console.log('Server is running at http://localhost:3000'); + console.log('Press Ctrl+C to stop the server.'); +}); diff --git a/yarn.lock b/yarn.lock index 050dbeba573..a7d3f2f715d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1608,7 +1608,7 @@ resolved "https://registry.yarnpkg.com/@hapi/file/-/file-3.0.0.tgz#f1fd824493ac89a6fceaf89c824afc5ae2121c09" integrity sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q== -"@hapi/hapi@^21.3.12": +"@hapi/hapi@21.4.0": version "21.4.0" resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-21.4.0.tgz#6f1493ffd6d83ef2d06a95b6e855f554545e3703" integrity sha512-kqiRWbYYLSSt2rYbxyNj8svPsXP715p4W/K3OXpXeiiVLNSdBX4f+zfmC+dY6eyb6rqTqTAbx6x8b5HpJTkviQ== @@ -3136,6 +3136,13 @@ "@types/node" "*" "@types/responselike" "^1.0.0" +"@types/compression@1.7.5": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@types/compression/-/compression-1.7.5.tgz#0f80efef6eb031be57b12221c4ba6bc3577808f7" + integrity sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg== + dependencies: + "@types/express" "*" + "@types/connect@*": version "3.4.38" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" @@ -3267,7 +3274,7 @@ dependencies: "@types/koa" "*" -"@types/koa@*", "@types/koa@^2.15.0": +"@types/koa@*", "@types/koa@2.15.0": version "2.15.0" resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.15.0.tgz#eca43d76f527c803b491731f95df575636e7b6f2" integrity sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g== @@ -4443,6 +4450,26 @@ common-ancestor-path@^1.0.1: resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w== +compressible@~2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.0.tgz#09420efc96e11a0f44f3a558de59e321364180f7" + integrity sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + negotiator "~0.6.4" + on-headers "~1.0.2" + safe-buffer "5.2.1" + vary "~1.1.2" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -4599,6 +4626,13 @@ dataloader@^1.4.0: resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@4, debug@4.4.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" @@ -5701,7 +5735,7 @@ foreground-child@^3.1.0: cross-spawn "^7.0.6" signal-exit "^4.0.1" -form-data@^4.0.0, form-data@^4.0.2: +form-data@4.0.2, form-data@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c" integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== @@ -7270,7 +7304,7 @@ koa-compose@^4.1.0: resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== -koa@^3.0.0: +koa@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/koa/-/koa-3.0.0.tgz#42b74a8404bbeab1cfc40b2431f488112f5a4d7f" integrity sha512-Usyqf1o+XN618R3Jzq4S4YWbKsRtPcGpgyHXD4APdGYQQyqQ59X+Oyc7fXHS2429stzLsBiDjj6zqqYe8kknfw== @@ -7574,7 +7608,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-db@^1.52.0, mime-db@^1.54.0: +"mime-db@>= 1.43.0 < 2", mime-db@^1.52.0, mime-db@^1.54.0: version "1.54.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== @@ -7757,6 +7791,11 @@ mri@^1.2.0: resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -7774,7 +7813,6 @@ mvdan-sh@^0.10.1: "nan@github:JCMais/nan#fix/electron-failures": version "2.22.0" - uid "0ec2eca8b2fd7518affb3945d087e393ad839b7e" resolved "https://codeload.github.com/JCMais/nan/tar.gz/0ec2eca8b2fd7518affb3945d087e393ad839b7e" nanoid@^3.3.6: @@ -7802,6 +7840,11 @@ negotiator@^1.0.0: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + next@15.3.2: version "15.3.2" resolved "https://registry.yarnpkg.com/next/-/next-15.3.2.tgz#97510629e38a058dd154782a5c2ec9c9ab94d0d8" @@ -8081,6 +8124,11 @@ on-finished@^2.3.0, on-finished@^2.4.1: dependencies: ee-first "1.1.1" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -9957,7 +10005,6 @@ typescript@5.8.3: uWebSockets.js@uNetworking/uWebSockets.js#v20.51.0: version "20.51.0" - uid "6609a88ffa9a16ac5158046761356ce03250a0df" resolved "https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/6609a88ffa9a16ac5158046761356ce03250a0df" ufo@^1.5.4: @@ -10156,7 +10203,7 @@ validate-npm-package-name@^5.0.0: resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz#a316573e9b49f3ccd90dbb6eb52b3f06c6d604e8" integrity sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ== -vary@^1, vary@^1.1.2: +vary@^1, vary@^1.1.2, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==