From 78f98dab30d05dfed105e9fa47cd449ca275e2dc Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 26 Mar 2025 07:10:48 +0100 Subject: [PATCH 1/2] fix: replace @perseveranza-pets/milo with @achingbrain/http-parser-js `@achingbrain/http-parser-js` is a ESM port of `http-parser-js` that uses `Uint8Array`s instead of node `Buffer`s and is 6.6kb. `@perseveranza-pets/milo` is 84.7kb. --- .aegir.js | 7 ++ package.json | 4 +- src/fetch/index.ts | 160 +++++++++++++++------------------------------ 3 files changed, 64 insertions(+), 107 deletions(-) create mode 100644 .aegir.js diff --git a/.aegir.js b/.aegir.js new file mode 100644 index 0000000..81d2431 --- /dev/null +++ b/.aegir.js @@ -0,0 +1,7 @@ + +/** @type {import('aegir/types').PartialOptions} */ +export default { + build: { + bundlesizeMax: '18kB' + } +} diff --git a/package.json b/package.json index ae3e2e4..002e229 100644 --- a/package.json +++ b/package.json @@ -167,13 +167,15 @@ "release": "aegir release" }, "dependencies": { + "@achingbrain/http-parser-js": "^0.5.8", "@libp2p/crypto": "^5.0.6", "@libp2p/interface": "^2.2.0", "@libp2p/interface-internal": "^2.0.10", "@libp2p/peer-id": "^5.0.7", "@multiformats/multiaddr": "^12.3.0", "@multiformats/multiaddr-to-uri": "^11.0.0", - "@perseveranza-pets/milo": "^0.2.1", + "buffer": "^6.0.3", + "http-parser-js": "^0.5.9", "p-defer": "^4.0.1", "uint8-varint": "^2.0.4", "uint8arraylist": "^2.4.8", diff --git a/src/fetch/index.ts b/src/fetch/index.ts index cbad0e7..4f89c38 100644 --- a/src/fetch/index.ts +++ b/src/fetch/index.ts @@ -1,14 +1,24 @@ /* eslint-disable max-depth */ /* eslint-disable complexity */ +import { HTTPParser } from '@achingbrain/http-parser-js' import { multiaddr, protocols } from '@multiformats/multiaddr' import { multiaddrToUri } from '@multiformats/multiaddr-to-uri' -// @ts-expect-error missing types -import { milo } from '@perseveranza-pets/milo/index-with-wasm.js' import defer from 'p-defer' -import { Uint8ArrayList, isUint8ArrayList } from 'uint8arraylist' +import { type Uint8ArrayList } from 'uint8arraylist' + interface Fetch { (req: Request): Promise } +const METHOD_GET = 1 + +function getStringMethod (method: number): string { + if (method === 1) { + return 'GET' + } + + return 'UNKNOWN' +} + interface Duplex> { source: AsyncIterable | Iterable sink(source: AsyncIterable | Iterable): RSink @@ -85,8 +95,6 @@ export async function handleRequestViaDuplex (s: Duplex { - const unconsumedChunks = new Uint8ArrayList() - - const textDecoder = new TextDecoder() - const ptr = milo.alloc(BUFFER_SIZE) - - const parser = milo.create() - // Simplifies implementation at the cost of storing data twice - milo.setManageUnconsumed(parser, true) - - const bodyStreamControllerPromise = defer >() - const body = new ReadableStream({ - async start (controller) { - bodyStreamControllerPromise.resolve(controller) - } - }) - const bodyStreamController = await bodyStreamControllerPromise.promise - - // Response - let status = '' - let reason = '' - - // Requests - let url = '' - let method = '' - + const body = new TransformStream() + const writer = body.writable.getWriter() + let messageComplete = false let fulfilledMsgPromise = false - milo.setOnStatus(parser, (_: unknown, from: number, size: number) => { - status = textDecoder.decode(unconsumedChunks.subarray(from, from + size)) - }) - milo.setOnReason(parser, (_: unknown, from: number, size: number) => { - reason = textDecoder.decode(unconsumedChunks.subarray(from, from + size)) - }) - milo.setOnUrl(parser, (_: unknown, from: number, size: number) => { - url = textDecoder.decode(unconsumedChunks.subarray(from, from + size)) - }) - milo.setOnMethod(parser, (_: unknown, from: number, size: number) => { - method = textDecoder.decode(unconsumedChunks.subarray(from, from + size)) - }) + const parser = new HTTPParser(expectRequest ? 'REQUEST' : 'RESPONSE') + parser[HTTPParser.kOnHeadersComplete] = (info) => { + fulfilledMsgPromise = true - milo.setOnRequest(parser, () => { - if (!expectRequest) { - msgPromise.reject(new Error('Received request instead of response')) - fulfilledMsgPromise = true - } - }) - milo.setOnResponse(parser, () => { - if (expectRequest) { - msgPromise.reject(new Error('Received response instead of request')) - fulfilledMsgPromise = true + // Handle the headers + const headers = new Headers() + + for (let i = 0; i < info.headers.length; i += 2) { + headers.set(info.headers[i], info.headers[i + 1]) } - }) - // Handle the headers - const headers = new Headers() - let lastHeaderName: string = '' + let reqBody: ReadableStream | null = body.readable - milo.setOnHeaderName(parser, (_: unknown, from: number, size: number) => { - lastHeaderName = textDecoder.decode(unconsumedChunks.subarray(from, from + size)) - }) - milo.setOnHeaderValue(parser, (_: unknown, from: number, size: number) => { - const headerVal = textDecoder.decode(unconsumedChunks.subarray(from, from + size)) - headers.set(lastHeaderName, headerVal) - }) - milo.setOnHeaders(parser, (_: unknown, from: number, size: number) => { // Headers are parsed. We can return the response try { if (expectRequest) { - let reqBody: ReadableStream | null = body - if (method === 'GET') { + if (info.method === METHOD_GET) { reqBody = null } - const urlWithHost = `https://${headers.get('Host') ?? 'unknown_host._libp2p'}${url}` + const urlWithHost = `https://${headers.get('Host') ?? 'unknown_host._libp2p'}${info.url}` detectBrokenRequestBody().then(async (broken) => { let req: Request if (!broken) { req = new Request(urlWithHost, { - method, + method: getStringMethod(info.method), body: reqBody, headers, // @ts-expect-error this is required by NodeJS despite being the only reasonable option https://fetch.spec.whatwg.org/#requestinit @@ -187,7 +147,7 @@ export function readHTTPMsg (expectRequest: boolean, r: Duplex | null = body - if (status === '204') { + let respBody: ReadableStream | null = body.readable + if (info.statusCode === 204) { respBody = null } const resp = new Response(respBody, { headers, - status: parseInt(status), - statusText: reason + status: info.statusCode, + statusText: info.statusMessage }) msgPromise.resolve(resp) fulfilledMsgPromise = true @@ -238,48 +198,36 @@ export function readHTTPMsg (expectRequest: boolean, r: Duplex { - const c: Uint8Array = unconsumedChunks.subarray(from, from + size) - // @ts-expect-error Unclear why this fails typecheck. TODO debug - bodyStreamController.enqueue(c) - }) - milo.setOnError(parser, () => { - bodyStreamController.error(new Error('Error parsing HTTP message')) - }) - - let messageComplete = false - milo.setOnMessageComplete(parser, () => { - bodyStreamController.close() + } + parser[HTTPParser.kOnBody] = (buf) => { + writer.write(buf) + .catch((err: Error) => { + msgPromise.reject(err) + }) + } + parser[HTTPParser.kOnMessageComplete] = () => { messageComplete = true - }) + writer.close() + .catch((err: Error) => { + msgPromise.reject(err) + }) + } // Consume data - for await (let chunks of r.source) { - if (!isUint8ArrayList(chunks)) { - chunks = new Uint8ArrayList(chunks) - } - for (const chunk of chunks) { - unconsumedChunks.append(chunk) - const buffer = new Uint8Array(milo.memory.buffer, ptr, BUFFER_SIZE) - buffer.set(chunk, 0) - const consumed = milo.parse(parser, ptr, chunk.length) - unconsumedChunks.consume(consumed) - } + for await (const chunks of r.source) { + const chunk = chunks.subarray() + parser.execute(chunk) } - milo.finish(parser) + + parser.finish() if (!messageComplete) { - bodyStreamController.error(new Error('Incomplete HTTP message')) + await writer.abort(new Error('Incomplete HTTP message')) + if (!fulfilledMsgPromise) { msgPromise.reject(new Error('Incomplete HTTP message')) } } - - milo.destroy(parser) - milo.dealloc(ptr, BUFFER_SIZE) })() ] } From 84bc15eb8584d3c11bee98f23511470156140b0b Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 26 Mar 2025 08:07:19 +0100 Subject: [PATCH 2/2] chore: update deps --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 002e229..3c073c6 100644 --- a/package.json +++ b/package.json @@ -174,8 +174,6 @@ "@libp2p/peer-id": "^5.0.7", "@multiformats/multiaddr": "^12.3.0", "@multiformats/multiaddr-to-uri": "^11.0.0", - "buffer": "^6.0.3", - "http-parser-js": "^0.5.9", "p-defer": "^4.0.1", "uint8-varint": "^2.0.4", "uint8arraylist": "^2.4.8",