From f98fa8ead8c39d20bf41436de82dafe5409d00e5 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 2 Oct 2025 13:21:13 +0300 Subject: [PATCH 1/5] feat!: update to libp2p v3 API Updates implementation to use the v3 API. BREAKING CHANGE: must be used with `libp2p@3.x.x` or later --- .../express-server-over-libp2p/package.json | 12 +- examples/hono-server-over-libp2p/package.json | 14 +- .../libp2p-as-http-transport/package.json | 12 +- examples/libp2p-over-http-server/package.json | 6 +- examples/peer-id-auth/package.json | 4 +- .../package.json | 26 +-- .../src/proxy.js | 2 +- examples/websockets-over-libp2p/package.json | 12 +- package.json | 2 +- packages/http-fetch/package.json | 10 +- packages/http-fetch/src/index.ts | 10 +- packages/http-fetch/src/read-response.ts | 7 +- packages/http-fetch/src/send-request.ts | 2 +- packages/http-fetch/test/index.spec.ts | 80 +++----- packages/http-peer-id-auth/package.json | 9 +- packages/http-peer-id-auth/src/client.ts | 3 +- packages/http-ping/package.json | 10 +- packages/http-server/package.json | 8 +- packages/http-server/src/node-server.ts | 6 +- packages/http-utils/package.json | 20 +- packages/http-utils/src/index.ts | 144 +++++++------- packages/http-utils/src/stream-to-socket.ts | 178 ++++++++++-------- packages/http-websocket/package.json | 14 +- packages/http-websocket/src/index.ts | 17 +- packages/http-websocket/src/utils.ts | 26 ++- packages/http-websocket/src/websocket.ts | 57 ++++-- packages/http/package.json | 10 +- packages/http/src/http.browser.ts | 9 +- packages/http/src/http.ts | 24 +-- packages/http/src/middleware/peer-id-auth.ts | 3 +- packages/http/src/registrar.ts | 14 +- packages/interop/package.json | 42 ++--- packages/interop/test/fixtures/get-libp2p.ts | 6 + packages/interop/test/ping.spec.ts | 17 +- 34 files changed, 424 insertions(+), 392 deletions(-) diff --git a/examples/express-server-over-libp2p/package.json b/examples/express-server-over-libp2p/package.json index 5ecee72..299ef93 100644 --- a/examples/express-server-over-libp2p/package.json +++ b/examples/express-server-over-libp2p/package.json @@ -16,17 +16,17 @@ "test": "test-node-example test/*" }, "dependencies": { - "@chainsafe/libp2p-noise": "^16.1.3", - "@chainsafe/libp2p-yamux": "^7.0.1", + "@chainsafe/libp2p-noise": "^17.0.0", + "@chainsafe/libp2p-yamux": "^8.0.0", "@libp2p/http": "^1.0.0", "@libp2p/http-server": "^1.0.0", - "@libp2p/tcp": "^10.1.13", - "@multiformats/multiaddr": "^12.4.0", + "@libp2p/tcp": "^11.0.4", + "@multiformats/multiaddr": "^13.0.1", "express": "^5.1.0", - "libp2p": "^2.8.8" + "libp2p": "^3.0.5" }, "devDependencies": { - "aegir": "^47.0.16", + "aegir": "^47.0.22", "test-ipfs-example": "^1.3.3" }, "private": true, diff --git a/examples/hono-server-over-libp2p/package.json b/examples/hono-server-over-libp2p/package.json index ac1629c..0d61813 100644 --- a/examples/hono-server-over-libp2p/package.json +++ b/examples/hono-server-over-libp2p/package.json @@ -15,17 +15,17 @@ "test": "test-node-example test/*" }, "dependencies": { - "@chainsafe/libp2p-noise": "^16.1.3", - "@chainsafe/libp2p-yamux": "^7.0.1", + "@chainsafe/libp2p-noise": "^17.0.0", + "@chainsafe/libp2p-yamux": "^8.0.0", "@libp2p/http": "^1.0.0", "@libp2p/http-server": "^1.0.0", - "@libp2p/tcp": "^10.1.13", - "@multiformats/multiaddr": "^12.4.0", - "hono": "^4.7.10", - "libp2p": "^2.8.8" + "@libp2p/tcp": "^11.0.4", + "@multiformats/multiaddr": "^13.0.1", + "hono": "^4.9.9", + "libp2p": "^3.0.5" }, "devDependencies": { - "aegir": "^47.0.16", + "aegir": "^47.0.22", "test-ipfs-example": "^1.3.3" }, "private": true, diff --git a/examples/libp2p-as-http-transport/package.json b/examples/libp2p-as-http-transport/package.json index f5d0f6f..12c653f 100644 --- a/examples/libp2p-as-http-transport/package.json +++ b/examples/libp2p-as-http-transport/package.json @@ -15,17 +15,17 @@ "test": "test-node-example test/*" }, "dependencies": { - "@chainsafe/libp2p-noise": "^16.1.3", - "@chainsafe/libp2p-yamux": "^7.0.1", + "@chainsafe/libp2p-noise": "^17.0.0", + "@chainsafe/libp2p-yamux": "^8.0.0", "@libp2p/http": "^1.0.0", "@libp2p/http-server": "^1.0.0", - "@libp2p/tcp": "^10.1.13", - "@multiformats/multiaddr": "^12.4.0", + "@libp2p/tcp": "^11.0.4", + "@multiformats/multiaddr": "^13.0.1", "express": "^5.1.0", - "libp2p": "^2.8.8" + "libp2p": "^3.0.5" }, "devDependencies": { - "aegir": "^47.0.16", + "aegir": "^47.0.22", "test-ipfs-example": "^1.3.3" }, "private": true, diff --git a/examples/libp2p-over-http-server/package.json b/examples/libp2p-over-http-server/package.json index aac5e89..5368ecd 100644 --- a/examples/libp2p-over-http-server/package.json +++ b/examples/libp2p-over-http-server/package.json @@ -16,12 +16,12 @@ }, "dependencies": { "@libp2p/http": "^1.0.0", - "@libp2p/http-server": "^1.0.0", "@libp2p/http-ping": "^1.0.0", - "libp2p": "^2.8.8" + "@libp2p/http-server": "^1.0.0", + "libp2p": "^3.0.5" }, "devDependencies": { - "aegir": "^47.0.16", + "aegir": "^47.0.22", "test-ipfs-example": "^1.3.3" }, "private": true, diff --git a/examples/peer-id-auth/package.json b/examples/peer-id-auth/package.json index ad2db10..5c1aa6e 100644 --- a/examples/peer-id-auth/package.json +++ b/examples/peer-id-auth/package.json @@ -19,10 +19,10 @@ "@libp2p/http": "^1.0.0", "@libp2p/http-peer-id-auth": "^1.0.0", "@libp2p/http-server": "^1.0.0", - "libp2p": "^2.8.8" + "libp2p": "^3.0.5" }, "devDependencies": { - "aegir": "^47.0.16", + "aegir": "^47.0.22", "test-ipfs-example": "^1.3.3" }, "private": true, diff --git a/examples/serving-websites-from-web-browsers/package.json b/examples/serving-websites-from-web-browsers/package.json index 3090048..befffb5 100644 --- a/examples/serving-websites-from-web-browsers/package.json +++ b/examples/serving-websites-from-web-browsers/package.json @@ -20,24 +20,24 @@ "dep-check": "aegir dep-check" }, "dependencies": { - "@chainsafe/libp2p-noise": "^16.1.3", - "@chainsafe/libp2p-yamux": "^7.0.1", - "@libp2p/circuit-relay-v2": "^3.2.14", + "@chainsafe/libp2p-noise": "^17.0.0", + "@chainsafe/libp2p-yamux": "^8.0.0", + "@libp2p/circuit-relay-v2": "^4.0.4", "@libp2p/http": "^1.0.0", "@libp2p/http-server": "^1.0.0", - "@libp2p/identify": "^3.0.32", - "@libp2p/ping": "^2.0.32", - "@libp2p/webrtc": "^5.2.15", - "@libp2p/websockets": "^9.2.13", - "@multiformats/multiaddr": "^12.4.0", - "@multiformats/multiaddr-matcher": "^2.0.1", - "it-byte-stream": "^2.0.2", - "libp2p": "^2.8.8" + "@libp2p/identify": "^4.0.4", + "@libp2p/ping": "^3.0.4", + "@libp2p/utils": "^7.0.4", + "@libp2p/webrtc": "^6.0.5", + "@libp2p/websockets": "^10.0.5", + "@multiformats/multiaddr": "^13.0.1", + "@multiformats/multiaddr-matcher": "^3.0.1", + "libp2p": "^3.0.5" }, "devDependencies": { - "aegir": "^47.0.16", + "aegir": "^47.0.22", "test-ipfs-example": "^1.3.3", - "vite": "^7.0.0" + "vite": "^7.1.8" }, "private": true, "type": "module" diff --git a/examples/serving-websites-from-web-browsers/src/proxy.js b/examples/serving-websites-from-web-browsers/src/proxy.js index e0b299b..facb3d8 100644 --- a/examples/serving-websites-from-web-browsers/src/proxy.js +++ b/examples/serving-websites-from-web-browsers/src/proxy.js @@ -7,10 +7,10 @@ import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay import { HTTP_PROTOCOL } from '@libp2p/http' import { identify } from '@libp2p/identify' import { ping } from '@libp2p/ping' +import { byteStream } from '@libp2p/utils' import { webRTC } from '@libp2p/webrtc' import { webSockets } from '@libp2p/websockets' import { multiaddr } from '@multiformats/multiaddr' -import { byteStream } from 'it-byte-stream' import { createLibp2p } from 'libp2p' const args = process.argv.slice(2) diff --git a/examples/websockets-over-libp2p/package.json b/examples/websockets-over-libp2p/package.json index 94046a6..45af3db 100644 --- a/examples/websockets-over-libp2p/package.json +++ b/examples/websockets-over-libp2p/package.json @@ -16,16 +16,16 @@ "test": "test-node-example test/*" }, "dependencies": { - "@chainsafe/libp2p-noise": "^16.1.3", - "@chainsafe/libp2p-yamux": "^7.0.1", + "@chainsafe/libp2p-noise": "^17.0.0", + "@chainsafe/libp2p-yamux": "^8.0.0", "@libp2p/http": "^1.0.0", "@libp2p/http-server": "^1.0.0", - "@libp2p/tcp": "^10.1.13", - "@multiformats/multiaddr": "^12.4.0", - "libp2p": "^2.8.8" + "@libp2p/tcp": "^11.0.4", + "@multiformats/multiaddr": "^13.0.1", + "libp2p": "^3.0.5" }, "devDependencies": { - "aegir": "^47.0.16", + "aegir": "^47.0.22", "test-ipfs-example": "^1.3.3" }, "private": true, diff --git a/package.json b/package.json index 7501ab4..43ef3ec 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "private": true, "scripts": { - "reset": "aegir run clean && aegir clean interop/node_modules packages/*/node_modules node_modules package-lock.json packages/*/package-lock.json interop/*/package-lock.json", + "reset": "aegir run clean && aegir clean examples/*/node_modules packages/*/node_modules node_modules package-lock.json examples/*/package-lock.json packages/*/package-lock.json", "test": "aegir run test", "test:node": "aegir run test:node", "test:chrome": "aegir run test:chrome", diff --git a/packages/http-fetch/package.json b/packages/http-fetch/package.json index c5e08e3..288887f 100644 --- a/packages/http-fetch/package.json +++ b/packages/http-fetch/package.json @@ -140,16 +140,12 @@ "dependencies": { "@achingbrain/http-parser-js": "^0.5.9", "@libp2p/http-utils": "^1.0.0", - "@libp2p/interface": "^2.10.2", - "it-byte-stream": "^2.0.2", + "@libp2p/interface": "^3.0.2", + "@libp2p/utils": "^7.0.4", "uint8arrays": "^5.1.0" }, "devDependencies": { - "@libp2p/logger": "^5.1.18", - "aegir": "^47.0.16", - "it-drain": "^3.0.9", - "it-pair": "^2.0.6", - "sinon-ts": "^2.0.0" + "aegir": "^47.0.22" }, "sideEffects": false } diff --git a/packages/http-fetch/src/index.ts b/packages/http-fetch/src/index.ts index 66b1339..4bad77c 100644 --- a/packages/http-fetch/src/index.ts +++ b/packages/http-fetch/src/index.ts @@ -6,14 +6,12 @@ * socket. */ -import { byteStream } from 'it-byte-stream' +import { byteStream } from '@libp2p/utils' import { readResponse } from './read-response.js' import { sendRequest } from './send-request.js' -import type { ComponentLogger, Logger, Stream } from '@libp2p/interface' +import type { Logger, Stream } from '@libp2p/interface' export interface FetchInit extends RequestInit { - logger: ComponentLogger - /** * The maximum number of bytes that will be parsed as headers, defaults to * 80KB @@ -28,8 +26,8 @@ export interface SendRequestInit extends RequestInit { maxHeaderSize?: number } -export async function fetch (stream: Stream, resource: string | URL, init: FetchInit): Promise { - const log = init.logger.forComponent('libp2p:http:fetch') +export async function fetch (stream: Stream, resource: string | URL, init: FetchInit = {}): Promise { + const log = stream.log.newScope('http-fetch') resource = typeof resource === 'string' ? new URL(resource) : resource const bytes = byteStream(stream) diff --git a/packages/http-fetch/src/read-response.ts b/packages/http-fetch/src/read-response.ts index a162831..5657530 100644 --- a/packages/http-fetch/src/read-response.ts +++ b/packages/http-fetch/src/read-response.ts @@ -3,7 +3,7 @@ import { Response } from '@libp2p/http-utils' import { InvalidResponseError } from './errors.js' import type { SendRequestInit } from './index.js' import type { Stream } from '@libp2p/interface' -import type { ByteStream } from 'it-byte-stream' +import type { ByteStream } from '@libp2p/utils' const nullBodyStatus = [101, 204, 205, 304] @@ -65,11 +65,14 @@ export async function readResponse (bytes: ByteStream, resource: URL, in .then(async () => { let read = 0 while (true) { - init.log('reading response') + init.log('read chunk from response') + const chunk = await bytes.read({ signal: init.signal ?? undefined }) + init.log('read', chunk) + if (chunk == null) { const err = parser.finish() diff --git a/packages/http-fetch/src/send-request.ts b/packages/http-fetch/src/send-request.ts index b448884..81235af 100644 --- a/packages/http-fetch/src/send-request.ts +++ b/packages/http-fetch/src/send-request.ts @@ -3,7 +3,7 @@ import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' import { normalizeContent } from './utils.js' import type { SendRequestInit } from './index.js' import type { Stream } from '@libp2p/interface' -import type { ByteStream } from 'it-byte-stream' +import type { ByteStream } from '@libp2p/utils' export async function sendRequest (bytes: ByteStream, url: URL, init: SendRequestInit): Promise { const headers = new Headers(init.headers) diff --git a/packages/http-fetch/test/index.spec.ts b/packages/http-fetch/test/index.spec.ts index e47a206..f78a52a 100644 --- a/packages/http-fetch/test/index.spec.ts +++ b/packages/http-fetch/test/index.spec.ts @@ -1,26 +1,18 @@ /* eslint-env mocha */ import { readHeaders, responseToStream, streamToRequest } from '@libp2p/http-utils' -import { defaultLogger } from '@libp2p/logger' +import { streamPair } from '@libp2p/utils' import { expect } from 'aegir/chai' -import drain from 'it-drain' -import { duplexPair } from 'it-pair/duplex' -import { stubInterface } from 'sinon-ts' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { fetch } from '../src/index.js' import { cases } from './fixtures/cases.js' import type { Stream } from '@libp2p/interface' -function serve (server: any, handler: (req: Request) => Response | Promise): void { - const stream = stubInterface({ - ...server, - closeWrite: async () => {} - }) - +function serve (server: Stream, handler: (req: Request) => Response | Promise): void { void Promise.resolve().then(async () => { - const info = await readHeaders(stream) - const res = await handler(streamToRequest(info, stream)) - await responseToStream(res, stream) + const info = await readHeaders(server) + const res = await handler(streamToRequest(info, server)) + await responseToStream(res, server) }) } @@ -34,26 +26,23 @@ function echo (server: any): void { describe('@libp2p/http-fetch', () => { it('should make a simple GET request', async () => { - const [client, server] = duplexPair() + const [outboundStream, inboundStream] = await streamPair() - serve(server, (req) => { + serve(inboundStream, (req) => { return new Response('Hello World') }) - const res = await fetch(stubInterface(client), 'https://example.com', { - logger: defaultLogger() - }) + const res = await fetch(outboundStream, 'https://example.com') expect(await res.text()).to.equal('Hello World') }) it('should GET with headers', async () => { - const [client, server] = duplexPair() + const [outboundStream, inboundStream] = await streamPair() - echo(server) + echo(inboundStream) - const res = await fetch(stubInterface(client), 'https://example.com', { - logger: defaultLogger(), + const res = await fetch(outboundStream, 'https://example.com', { headers: { 'X-Test': 'foo' } @@ -63,12 +52,11 @@ describe('@libp2p/http-fetch', () => { }) it('should POST some data', async () => { - const [client, server] = duplexPair() + const [outboundStream, inboundStream] = await streamPair() - echo(server) + echo(inboundStream) - const res = await fetch(stubInterface(client), 'https://example.com', { - logger: defaultLogger(), + const res = await fetch(outboundStream, 'https://example.com', { method: 'POST', body: 'Hello World' }) @@ -77,14 +65,12 @@ describe('@libp2p/http-fetch', () => { }) it('should handle trash', async () => { - const [client, server] = duplexPair() + const [outboundStream, inboundStream] = await streamPair() - void server.sink([uint8ArrayFromString('FOOOOOOOOOOOOOOOOo')]) - void drain(server.source) + inboundStream.send(uint8ArrayFromString('FOOOOOOOOOOOOOOOOo')) + void inboundStream.close() - await expect(fetch(stubInterface(client), 'https://example.com', { - logger: defaultLogger() - })).to.eventually.be.rejected() + await expect(fetch(outboundStream, 'https://example.com')).to.eventually.be.rejected() .with.property('name', 'InvalidResponseError') }) @@ -106,21 +92,17 @@ describe('@libp2p/http-fetch', () => { continue } - const [client, server] = duplexPair() + const [outboundStream, inboundStream] = await streamPair() - void server.sink((async function * () { - const rawHttp = new TextEncoder().encode(httpCase.raw) - // Trickle the response 1 byte at a time - for (let i = 0; i < rawHttp.length; i++) { - yield rawHttp.subarray(i, i + 1) - } - })()) - void drain(server.source) + const rawHttp = new TextEncoder().encode(httpCase.raw) + // Trickle the response 1 byte at a time + for (let i = 0; i < rawHttp.length; i++) { + inboundStream.send(rawHttp.subarray(i, i + 1)) + } + void inboundStream.close() // Request doesn't matter - const resp = await fetch(stubInterface(client), 'https://example.com', { - logger: defaultLogger() - }) + const resp = await fetch(outboundStream, 'https://example.com') expect(resp.status).to.equal(expectedStatusCode) const chunk = (arr: T[], size: number): T[][] => arr.reduce((chunks, el, i) => i % size === 0 ? [...chunks, [el]] : (chunks[chunks.length - 1].push(el), chunks), []) @@ -150,15 +132,13 @@ describe('@libp2p/http-fetch', () => { continue } - const [client, server] = duplexPair() + const [outboundStream, inboundStream] = await streamPair() - void server.sink([uint8ArrayFromString(httpCase.raw)]) - void drain(server.source) + inboundStream.send(uint8ArrayFromString(httpCase.raw)) + void inboundStream.close() // Request doesn't matter - const resp = await fetch(stubInterface(client), 'https://example.com', { - logger: defaultLogger() - }) + const resp = await fetch(outboundStream, 'https://example.com') expect(resp.status).to.equal(expectedStatusCode) const chunk = (arr: T[], size: number): T[][] => arr.reduce((chunks, el, i) => i % size === 0 ? [...chunks, [el]] : (chunks[chunks.length - 1].push(el), chunks), []) diff --git a/packages/http-peer-id-auth/package.json b/packages/http-peer-id-auth/package.json index 019b66a..0de7026 100644 --- a/packages/http-peer-id-auth/package.json +++ b/packages/http-peer-id-auth/package.json @@ -138,15 +138,14 @@ "release": "aegir release" }, "dependencies": { - "@libp2p/crypto": "^5.1.4", - "@libp2p/interface": "^2.10.2", - "@libp2p/peer-id": "^5.1.5", - "@multiformats/multiaddr": "^12.4.0", + "@libp2p/crypto": "^5.1.12", + "@libp2p/interface": "^3.0.2", + "@libp2p/peer-id": "^6.0.3", "uint8-varint": "^2.0.4", "uint8arrays": "^5.1.0" }, "devDependencies": { - "aegir": "^47.0.16" + "aegir": "^47.0.22" }, "sideEffects": false } diff --git a/packages/http-peer-id-auth/src/client.ts b/packages/http-peer-id-auth/src/client.ts index f963c6f..abc69c0 100644 --- a/packages/http-peer-id-auth/src/client.ts +++ b/packages/http-peer-id-auth/src/client.ts @@ -6,8 +6,7 @@ import { InvalidPeerError, InvalidSignatureError, InvalidStateError } from './er import { decodeAuthorizationHeader, encodeAuthParams, generateChallenge, sign, verify } from './utils.js' import { PEER_ID_AUTH_SCHEME } from './index.js' import type { BearerTokenHeader, ClientChallengeResponseHeader, ClientResponse, ServerChallengeHeader, VerifyClientChallengeResponseOptions, VerifyPeer } from './index.js' -import type { PeerId, PrivateKey, PublicKey } from '@libp2p/interface' -import type { AbortOptions } from '@multiformats/multiaddr' +import type { PeerId, PrivateKey, PublicKey, AbortOptions } from '@libp2p/interface' export function isClientChallengeResponse (obj?: any): obj is ClientChallengeResponseHeader { if (obj == null) { diff --git a/packages/http-ping/package.json b/packages/http-ping/package.json index a61dfb7..8677996 100644 --- a/packages/http-ping/package.json +++ b/packages/http-ping/package.json @@ -139,14 +139,14 @@ }, "dependencies": { "@libp2p/http": "^1.0.0", - "@libp2p/interface": "^2.10.2", - "@multiformats/multiaddr": "^12.4.0", - "race-event": "^1.3.0", - "race-signal": "^1.1.3", + "@libp2p/interface": "^3.0.2", + "@multiformats/multiaddr": "^13.0.1", + "race-event": "^1.6.1", + "race-signal": "^2.0.0", "uint8arrays": "^5.1.0" }, "devDependencies": { - "aegir": "^47.0.16" + "aegir": "^47.0.22" }, "sideEffects": false } diff --git a/packages/http-server/package.json b/packages/http-server/package.json index 2f6e79e..eb6cb3e 100644 --- a/packages/http-server/package.json +++ b/packages/http-server/package.json @@ -163,15 +163,15 @@ "@libp2p/http": "^1.0.0", "@libp2p/http-utils": "^1.0.0", "@libp2p/http-websocket": "^1.0.0", - "@libp2p/interface": "^2.10.2", + "@libp2p/interface": "^3.0.2", "events": "^3.3.0", - "race-event": "^1.3.0", + "race-event": "^1.6.1", "readable-stream": "^4.7.0", "uint8arrays": "^5.1.0", - "ws": "^8.18.2" + "ws": "^8.18.3" }, "devDependencies": { - "aegir": "^47.0.16" + "aegir": "^47.0.22" }, "browser": { "./dist/src/node/index.js": "./dist/src/node/index.browser.js", diff --git a/packages/http-server/src/node-server.ts b/packages/http-server/src/node-server.ts index 820dcf0..4313b5a 100644 --- a/packages/http-server/src/node-server.ts +++ b/packages/http-server/src/node-server.ts @@ -28,11 +28,7 @@ class NodeServer implements WebServer { async inject (info: HeaderInfo, stream: Stream, connection: Connection): Promise { // re-yield the headers to enable node to set up the request properly - const streamSource = stream.source - stream.source = (async function * () { - yield info.raw - yield * streamSource - })() + stream.unshift(info.raw) this.server.emit('connection', streamToSocket(stream, connection)) } diff --git a/packages/http-utils/package.json b/packages/http-utils/package.json index 1aefb28..c550505 100644 --- a/packages/http-utils/package.json +++ b/packages/http-utils/package.json @@ -139,21 +139,21 @@ }, "dependencies": { "@achingbrain/http-parser-js": "^0.5.9", - "@libp2p/interface": "^2.10.2", - "@libp2p/peer-id": "^5.1.5", - "@multiformats/multiaddr": "^12.4.0", - "@multiformats/multiaddr-to-uri": "^11.0.0", - "@multiformats/uri-to-multiaddr": "^9.0.1", - "it-byte-stream": "^2.0.2", - "it-queueless-pushable": "^2.0.1", - "it-to-browser-readablestream": "^2.0.11", - "multiformats": "^13.3.6", + "@libp2p/interface": "^3.0.2", + "@libp2p/peer-id": "^6.0.3", + "@libp2p/utils": "^7.0.4", + "@multiformats/multiaddr": "^13.0.1", + "@multiformats/multiaddr-to-uri": "^12.0.0", + "@multiformats/uri-to-multiaddr": "^10.0.0", + "it-to-browser-readablestream": "^2.0.12", + "multiformats": "^13.4.1", + "race-event": "^1.6.1", "readable-stream": "^4.7.0", "uint8arraylist": "^2.4.8", "uint8arrays": "^5.1.0" }, "devDependencies": { - "aegir": "^47.0.16" + "aegir": "^47.0.22" }, "browser": { "node:stream": "readable-stream" diff --git a/packages/http-utils/src/index.ts b/packages/http-utils/src/index.ts index 60d4ba1..5a73139 100644 --- a/packages/http-utils/src/index.ts +++ b/packages/http-utils/src/index.ts @@ -7,18 +7,19 @@ import { HTTPParser } from '@achingbrain/http-parser-js' import { InvalidParametersError, isPeerId, ProtocolError } from '@libp2p/interface' import { peerIdFromString } from '@libp2p/peer-id' -import { isMultiaddr, multiaddr } from '@multiformats/multiaddr' +import { getNetConfig } from '@libp2p/utils' +import { CODE_P2P, isMultiaddr, multiaddr } from '@multiformats/multiaddr' import { multiaddrToUri } from '@multiformats/multiaddr-to-uri' import { uriToMultiaddr } from '@multiformats/uri-to-multiaddr' -import { queuelessPushable } from 'it-queueless-pushable' import itToBrowserReadableStream from 'it-to-browser-readablestream' import { base36 } from 'multiformats/bases/base36' import { base64pad } from 'multiformats/bases/base64' import { sha1 } from 'multiformats/hashes/sha1' +import { raceEvent } from 'race-event' import { Uint8ArrayList } from 'uint8arraylist' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { Request } from './request.js' -import type { AbortOptions, PeerId, Stream } from '@libp2p/interface' +import type { AbortOptions, PeerId, Stream, StreamMessageEvent } from '@libp2p/interface' import type { Multiaddr } from '@multiformats/multiaddr' const DNS_CODECS = ['dns', 'dns4', 'dns6', 'dnsaddr'] @@ -89,10 +90,10 @@ export function streamToRequest (info: HeaderInfo, stream: Stream): globalThis.R } if ((init.method !== 'GET' || info.upgrade) && init.method !== 'HEAD') { - let source: AsyncGenerator = stream.source + let source: AsyncIterable = stream if (!info.upgrade) { - source = takeBytes(stream.source, info.headers.get('content-length')) + source = takeBytes(stream, info.headers.get('content-length')) } init.body = itToBrowserReadableStream(source) @@ -104,13 +105,7 @@ export function streamToRequest (info: HeaderInfo, stream: Stream): globalThis.R } export async function responseToStream (res: Response, stream: Stream): Promise { - const pushable = queuelessPushable() - stream.sink(pushable) - .catch(err => { - stream.abort(err) - }) - - await pushable.push(uint8ArrayFromString([ + stream.send(uint8ArrayFromString([ `HTTP/1.1 ${res.status} ${res.statusText}`, ...writeHeaders(res.headers), '', @@ -118,28 +113,29 @@ export async function responseToStream (res: Response, stream: Stream): Promise< ].join('\r\n'))) if (res.body == null) { - await pushable.end() + await stream.close().catch(err => { + stream.abort(err) + }) return } const reader = res.body.getReader() - let result = await reader.read() while (true) { + const result = await reader.read() + if (result.value != null) { - await pushable.push(result.value) + if (!stream.send(result.value)) { + await stream.onDrain() + } } if (result.done) { break } - - result = await reader.read() } - await pushable.end() - - await stream.closeWrite() + await stream.close() .catch(err => { stream.abort(err) }) @@ -187,7 +183,7 @@ export function writeHeaders (headers: Headers): string[] { return output } -async function * takeBytes (source: AsyncGenerator, bytes?: number | string | null): AsyncGenerator { +async function * takeBytes (source: AsyncIterable, bytes?: number | string | null): AsyncGenerator { bytes = parseInt(`${bytes ?? ''}`) if (bytes == null || isNaN(bytes)) { @@ -329,12 +325,16 @@ export function getHost (addresses: URL | Multiaddr[], headers: Headers): string // try to use remote PeerId as domain if (!isValidHost(host) && Array.isArray(addresses)) { for (const address of addresses) { - const peerStr = address.getPeerId() + const peerStr = address.getComponents() + .findLast(c => c.code === CODE_P2P)?.value // try to extract port from multiaddr if it is available try { - const options = address.toOptions() - port = options.port + const config = getNetConfig(address) + + if (config.port != null) { + port = config.port + } } catch {} if (peerStr != null) { @@ -350,9 +350,11 @@ export function getHost (addresses: URL | Multiaddr[], headers: Headers): string if (!isValidHost(host) && Array.isArray(addresses)) { for (const address of addresses) { try { - const options = address.toOptions() + const config = getNetConfig(address) - host = options.host + if (config.host != null) { + host = config.host + } break } catch {} } @@ -490,55 +492,57 @@ export async function getServerUpgradeHeaders (headers: Headers | Record { - return new Promise((resolve, reject) => { - const parser = new HTTPParser('REQUEST') - const source = queuelessPushable() - const earlyData = new Uint8ArrayList() - let headersComplete = false - - parser[HTTPParser.kOnHeadersComplete] = (info) => { - headersComplete = true - const headers = new Headers() - - // set incoming headers - for (let i = 0; i < info.headers.length; i += 2) { - headers.set(info.headers[i].toLowerCase(), info.headers[i + 1]) - } +export async function readHeaders (stream: Stream, options?: AbortOptions): Promise { + const parser = new HTTPParser('REQUEST') + const earlyData = new Uint8ArrayList() + let headerInfo: HeaderInfo | undefined - resolve({ - ...info, - headers, - raw: earlyData, - method: HTTPParser.methods[info.method] - }) + parser[HTTPParser.kOnHeadersComplete] = (info) => { + const headers = new Headers() + + // set incoming headers + for (let i = 0; i < info.headers.length; i += 2) { + headers.set(info.headers[i].toLowerCase(), info.headers[i + 1]) } - // replace source with request body - const streamSource = stream.source - stream.source = source - - Promise.resolve().then(async () => { - for await (const chunk of streamSource) { - // only use the message parser until the headers have been read - if (!headersComplete) { - earlyData.append(chunk) - parser.execute(chunk.subarray()) - } else { - await source.push(new Uint8ArrayList(chunk)) - } + headerInfo = { + ...info, + headers, + raw: earlyData, + method: HTTPParser.methods[info.method] + } + } + + try { + while (true) { + const { data } = await raceEvent(stream, 'message', options?.signal) + const buf = data.subarray() + + const read = parser.execute(buf, 0, buf.byteLength) + + if (read instanceof Error) { + throw read } - await source.end() - }) - .catch((err: Error) => { - stream.abort(err) - reject(err) - }) - .finally(() => { - parser.finish() - }) - }) + // collect raw header bytes + earlyData.append(buf.subarray(0, read)) + + if (read < buf.byteLength) { + // reading headers finished and we have early data + stream.push(buf.subarray(read)) + } + + if (headerInfo != null) { + return headerInfo + } + } + } catch (err: any) { + stream.abort(err) + } finally { + parser.finish() + } + + throw new Error('Failed to read header info from request') } /** diff --git a/packages/http-utils/src/stream-to-socket.ts b/packages/http-utils/src/stream-to-socket.ts index 0fbf5ee..f37e990 100644 --- a/packages/http-utils/src/stream-to-socket.ts +++ b/packages/http-utils/src/stream-to-socket.ts @@ -1,7 +1,5 @@ import { Duplex } from 'node:stream' -import { byteStream } from 'it-byte-stream' -import type { Connection, Stream } from '@libp2p/interface' -import type { ByteStream } from 'it-byte-stream' +import type { Connection, Logger, Stream } from '@libp2p/interface' import type { Socket, SocketConnectOpts, AddressInfo, SocketReadyState } from 'node:net' const MAX_TIMEOUT = 2_147_483_647 @@ -16,96 +14,124 @@ export class Libp2pSocket extends Duplex { public timeout = MAX_TIMEOUT public allowHalfOpen: boolean + #initStream: Promise #stream?: Stream - #bytes?: ByteStream - constructor () { + #log?: Logger + + constructor (initStream: Promise<{ stream: Stream, connection: Connection }>) { super() this.bytesRead = 0 this.bytesWritten = 0 this.allowHalfOpen = true this.remoteAddress = '' + + this.#initStream = initStream.then(({ stream, connection }) => { + this.#log = stream.log.newScope('libp2p-socket') + this.remoteAddress = connection.remoteAddr.toString() + + stream.addEventListener('message', (evt) => { + this.push(evt.data.subarray()) + }) + + stream.addEventListener('close', (evt) => { + if (evt.error != null) { + this.destroy(evt.error) + } else { + this.push(null) + } + }) + + stream.pause() + + this.emit('connect') + + return stream + }) + .catch(err => { + this.emit('error', err) + throw err + }) } - setStream (stream: Stream, connection: Connection): void { - this.#bytes = byteStream(stream) - this.#stream = stream - this.remoteAddress = connection.remoteAddr.toString() + getStream (cb: (stream: Stream) => void): void { + if (this.#stream != null) { + cb(this.#stream) + return + } + + this.#initStream.then(stream => { + this.#stream = stream + cb(stream) + }, (err) => { + this.emit('error', err) + }) + } + + destroy (error?: Error): this { + return super.destroy(error) } _write (chunk: Uint8Array, encoding: string, cb: (err?: Error) => void): void { - this.#stream?.log('write %d bytes', chunk.byteLength) + this.#log?.('write %d bytes', chunk.byteLength) this.bytesWritten += chunk.byteLength - this.#bytes?.write(chunk) - .then(() => { + + this.getStream(stream => { + if (!stream.send(chunk)) { + stream.onDrain() + .then(() => { + cb() + }, (err) => { + cb(err) + }) + } else { cb() - }, err => { - cb(err) - }) + } + }) } _read (size: number): void { - this.#stream?.log('asked to read %d bytes', size) - - void Promise.resolve().then(async () => { - try { - while (true) { - const chunk = await this.#bytes?.read({ - signal: AbortSignal.timeout(this.timeout) - }) - - if (chunk == null) { - this.#stream?.log('socket readable end closed') - this.push(null) - return - } - - this.bytesRead += chunk.byteLength + this.#log?.('asked to read %d bytes', size) + this.getStream(stream => { + stream.resume() + }) + } - this.#stream?.log('socket read %d bytes', chunk.byteLength) - const more = this.push(chunk.subarray()) + _destroy (err: Error, cb: (err?: Error) => void): void { + this.#log?.('destroy with %d bytes buffered - %e', this.bufferSize, err) - if (!more) { - break - } - } - } catch (err: any) { - this.destroy(err) + this.getStream(stream => { + if (err != null) { + stream.abort(err) + cb() + } else { + stream.close() + .then(() => { + cb() + }) + .catch(err => { + stream.abort(err) + cb(err) + }) } }) } - _destroy (err: Error, cb: (err?: Error) => void): void { - this.#stream?.log('destroy with %d bytes buffered - %e', this.bufferSize, err) + _final (cb: (err?: Error) => void): void { + this.#log?.('final') - if (err != null) { - this.#bytes?.unwrap().abort(err) - cb() - } else { - this.#bytes?.unwrap().close() + this.getStream(stream => { + stream.close() .then(() => { cb() }) .catch(err => { - this.#stream?.abort(err) + stream.abort(err) cb(err) }) - } - } - - _final (cb: (err?: Error) => void): void { - this.#stream?.log('final') - - this.#bytes?.unwrap().closeWrite() - .then(() => { - cb() - }) - .catch(err => { - this.#bytes?.unwrap().abort(err) - cb(err) - }) + }) } public get readyState (): SocketReadyState { @@ -129,7 +155,7 @@ export class Libp2pSocket extends Duplex { } destroySoon (): void { - this.#stream?.log('destroySoon with %d bytes buffered', this.bufferSize) + this.#log?.('destroySoon with %d bytes buffered', this.bufferSize) this.destroy() } @@ -138,24 +164,27 @@ export class Libp2pSocket extends Duplex { connect (port: number, connectionListener?: () => void): this connect (path: string, connectionListener?: () => void): this connect (...args: any[]): this { - this.#stream?.log('connect %o', args) + this.#log?.('connect %o', args) return this } setEncoding (encoding?: BufferEncoding): this { - this.#stream?.log('setEncoding %s', encoding) + this.#log?.('setEncoding %s', encoding) return this } resetAndDestroy (): this { - this.#stream?.log('resetAndDestroy') - this.#stream?.abort(new Error('Libp2pSocket.resetAndDestroy')) + this.#log?.('resetAndDestroy') + + this.getStream(stream => { + stream.abort(new Error('Libp2pSocket.resetAndDestroy')) + }) return this } setTimeout (timeout: number, callback?: () => void): this { - this.#stream?.log('setTimeout %d', timeout) + this.#log?.('setTimeout %d', timeout) if (callback != null) { this.addListener('timeout', callback) @@ -167,31 +196,31 @@ export class Libp2pSocket extends Duplex { } setNoDelay (noDelay?: boolean): this { - this.#stream?.log('setNoDelay %b', noDelay) + this.#log?.('setNoDelay %b', noDelay) return this } setKeepAlive (enable?: boolean, initialDelay?: number): this { - this.#stream?.log('setKeepAlive %b %d', enable, initialDelay) + this.#log?.('setKeepAlive %b %d', enable, initialDelay) return this } address (): AddressInfo | Record { - this.#stream?.log('address') + this.#log?.('address') return {} } unref (): this { - this.#stream?.log('unref') + this.#log?.('unref') return this } ref (): this { - this.#stream?.log('ref') + this.#log?.('ref') return this } @@ -204,8 +233,5 @@ export class Libp2pSocket extends Duplex { } export function streamToSocket (stream: Stream, connection: Connection): Socket { - const socket = new Libp2pSocket() - socket.setStream(stream, connection) - - return socket + return new Libp2pSocket(Promise.resolve({ stream, connection })) } diff --git a/packages/http-websocket/package.json b/packages/http-websocket/package.json index 1da318d..9e54d69 100644 --- a/packages/http-websocket/package.json +++ b/packages/http-websocket/package.json @@ -140,17 +140,17 @@ "dependencies": { "@achingbrain/http-parser-js": "^0.5.9", "@libp2p/http-utils": "^1.0.0", - "@libp2p/interface": "^2.10.2", - "@libp2p/interface-internal": "^2.3.14", - "@libp2p/utils": "^6.6.5", - "@multiformats/multiaddr": "^12.4.0", - "it-byte-stream": "^2.0.2", - "multiformats": "^13.3.6", + "@libp2p/interface": "^3.0.2", + "@libp2p/interface-internal": "^3.0.4", + "@libp2p/utils": "^7.0.4", + "@multiformats/multiaddr": "^13.0.1", + "multiformats": "^13.4.1", + "race-event": "^1.6.1", "uint8arraylist": "^2.4.8", "uint8arrays": "^5.1.0" }, "devDependencies": { - "aegir": "^47.0.16" + "aegir": "^47.0.22" }, "browser": { "./dist/src/auth/agent.js": "./dist/src/auth/agent.browser.js", diff --git a/packages/http-websocket/src/index.ts b/packages/http-websocket/src/index.ts index c7f9180..315aaf9 100644 --- a/packages/http-websocket/src/index.ts +++ b/packages/http-websocket/src/index.ts @@ -55,7 +55,22 @@ export interface WebSocket extends TypedEventTarget { export interface WebSocketInit extends AbortOptions { headers: Headers protocols?: string[] - onHandshakeResponse?(res: Response): Promise + onHandshakeResponse?(res: Response, options: AbortOptions): Promise + + /** + * The WebSocket handshake must complete within this many ms + * + * @default 10_000 + */ + handshakeTimeout?: number + + /** + * When the underlying transport's send buffer becomes full, it must drain + * within this many ms otherwise the stream will be reset + * + * @default 10_000 + */ + drainTimeout?: number } export { WebSocket, RequestWebSocket, StreamWebSocket, ServerWebSocket } from './websocket.js' diff --git a/packages/http-websocket/src/utils.ts b/packages/http-websocket/src/utils.ts index ec4e801..325303d 100644 --- a/packages/http-websocket/src/utils.ts +++ b/packages/http-websocket/src/utils.ts @@ -1,12 +1,12 @@ import { HTTPParser } from '@achingbrain/http-parser-js' import { Response, BAD_REQUEST, toUint8Array, writeHeaders, getServerUpgradeHeaders } from '@libp2p/http-utils' -import { InvalidParametersError } from '@libp2p/interface' +import { InvalidParametersError, StreamMessageEvent } from '@libp2p/interface' import { base64pad } from 'multiformats/bases/base64' +import { raceEvent } from 'race-event' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { StreamWebSocket } from './websocket.js' import type { HeaderInfo } from '@libp2p/http-utils' import type { AbortOptions, Stream } from '@libp2p/interface' -import type { ByteStream } from 'it-byte-stream' export function toBytes (data: string | Blob | Uint8Array | ArrayBuffer | DataView): Uint8Array | Promise { if (data instanceof Uint8Array || data instanceof ArrayBuffer || data instanceof DataView) { @@ -34,7 +34,7 @@ export class CodeError extends Error { } } -export async function readResponse (bytes: ByteStream, options: AbortOptions): Promise { +export async function readResponse (stream: Stream, options: AbortOptions): Promise { return new Promise((resolve, reject) => { let readHeaders = false @@ -58,17 +58,23 @@ export async function readResponse (bytes: ByteStream, options: AbortOptions): P Promise.resolve() .then(async () => { while (true) { - if (readHeaders) { - break - } + const { data } = await raceEvent(stream, 'message', options.signal) + const buf = data.subarray() + + const read = parser.execute(buf, 0, buf.byteLength) - const chunk = await bytes.read(options) + if (read instanceof Error) { + throw read + } - if (chunk == null) { - throw new Error('Stream ended before headers were received') + if (read < buf.byteLength) { + // reading headers finished and we have early data + stream.push(buf.subarray(read)) } - parser.execute(chunk.subarray(), 0, chunk.byteLength) + if (readHeaders) { + break + } } }) .catch((err: Error) => { diff --git a/packages/http-websocket/src/websocket.ts b/packages/http-websocket/src/websocket.ts index 8432516..f30008b 100644 --- a/packages/http-websocket/src/websocket.ts +++ b/packages/http-websocket/src/websocket.ts @@ -1,7 +1,6 @@ import { getHeaders } from '@libp2p/http-utils' import { InvalidParametersError, TypedEventEmitter } from '@libp2p/interface' -import { isPromise } from '@libp2p/utils/is-promise' -import { byteStream } from 'it-byte-stream' +import { isPromise, byteStream } from '@libp2p/utils' import { Uint8ArrayList } from 'uint8arraylist' import { CloseEvent, ErrorEvent } from './events.js' import { encodeMessage, decodeMessage, CLOSE_MESSAGES } from './message.js' @@ -11,8 +10,8 @@ import type { MESSAGE_TYPE } from './message.js' import type { HeaderInfo } from '@libp2p/http-utils' import type { AbortOptions, Stream } from '@libp2p/interface' import type { ConnectionManager } from '@libp2p/interface-internal' +import type { ByteStream } from '@libp2p/utils' import type { Multiaddr } from '@multiformats/multiaddr' -import type { ByteStream } from 'it-byte-stream' import type { IncomingMessage } from 'node:http' import type { Duplex } from 'node:stream' @@ -416,7 +415,9 @@ export class RequestWebSocket extends AbstractWebSocket { } export class WebSocket extends AbstractWebSocket { - private bytes?: ByteStream + private stream?: Stream + private handshakeTimeout: number + private drainTimeout: number constructor (mas: Multiaddr[], url: URL, connectionManager: ConnectionManager, init: WebSocketInit) { super(url, { @@ -424,23 +425,36 @@ export class WebSocket extends AbstractWebSocket { isClient: true }) + this.handshakeTimeout = init.handshakeTimeout ?? 10_000 + this.drainTimeout = init.drainTimeout ?? 10_000 + Promise.resolve() .then(async () => { - const connection = await connectionManager.openConnection(mas, init) - const stream = await connection.newStream(HTTP_PROTOCOL, init) - this.bytes = byteStream(stream) + const signal = AbortSignal.timeout(this.handshakeTimeout) + this.stream = await connectionManager.openStream(mas, HTTP_PROTOCOL, { + ...init, + signal + }) for await (const buf of performClientUpgrade(url, init.protocols, getHeaders(init))) { - await this.bytes.write(buf) + if (!this.stream.send(buf)) { + await this.stream.onDrain({ + signal + }) + } } - const res = await readResponse(this.bytes, {}) + const res = await readResponse(this.stream, { + signal + }) if (res.status !== 101) { throw new Error('Invalid WebSocket handshake - response status ' + res.status) } - await init.onHandshakeResponse?.(res) + await init.onHandshakeResponse?.(res, { + signal + }) // if a protocol was selected by the server, expose it this.protocol = res.headers.get('Sec-WebSocket-Protocol') ?? '' @@ -448,7 +462,7 @@ export class WebSocket extends AbstractWebSocket { this.readyState = this.OPEN this.dispatchEvent(new Event('open')) - for await (const buf of stream.source) { + for await (const buf of this.stream) { this._push(buf) } }) @@ -458,36 +472,39 @@ export class WebSocket extends AbstractWebSocket { } _write (buf: Uint8ArrayList, cb: (err?: Error | null) => void): void { - if (this.bytes == null) { + if (this.stream == null) { cb(new Error('WebSocket was not open')) return } - this.bytes.write(buf) - .then(() => { + if (!this.stream.send(buf)) { + this.stream.onDrain({ + signal: AbortSignal.timeout(this.drainTimeout) + }).then(() => { cb() }, (err) => { cb(err) }) + } else { + cb() + } } _close (err: Error | undefined, cb: () => void): void { - if (this.bytes == null) { + if (this.stream == null) { cb() return } - const stream = this.bytes.unwrap() - if (err != null) { - stream.abort(err) + this.stream.abort(err) cb() return } - stream.close() + this.stream.close() .catch((err) => { - stream.abort(err) + this.stream?.abort(err) }) .finally(() => { cb() diff --git a/packages/http/package.json b/packages/http/package.json index 1b60653..bafbbf9 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -166,14 +166,14 @@ "@libp2p/http-peer-id-auth": "^1.0.0", "@libp2p/http-utils": "^1.0.0", "@libp2p/http-websocket": "^1.0.0", - "@libp2p/interface": "^2.10.2", - "@libp2p/interface-internal": "^2.3.14", - "@multiformats/multiaddr": "^12.4.0", + "@libp2p/interface": "^3.0.2", + "@libp2p/interface-internal": "^3.0.4", + "@multiformats/multiaddr": "^13.0.1", "cookie": "^1.0.2", - "undici": "^7.10.0" + "undici": "^7.16.0" }, "devDependencies": { - "aegir": "^47.0.16" + "aegir": "^47.0.22" }, "browser": { "./dist/src/http.js": "./dist/src/http.browser.js" diff --git a/packages/http/src/http.browser.ts b/packages/http/src/http.browser.ts index eb008f1..587af1b 100644 --- a/packages/http/src/http.browser.ts +++ b/packages/http/src/http.browser.ts @@ -9,9 +9,9 @@ import { HTTPRegistrar } from './registrar.js' import { prepareAndConnect, prepareAndSendRequest, processResponse } from './utils.js' import { WELL_KNOWN_PROTOCOLS_PATH } from './index.js' import type { HTTPInit, HTTP as HTTPInterface, ProtocolMap, FetchInit, HTTPRoute, ConnectInit, MiddlewareOptions } from './index.js' -import type { ComponentLogger, Logger, PeerId, PrivateKey, Startable } from '@libp2p/interface' +import type { ComponentLogger, Logger, PeerId, PrivateKey, Startable, AbortOptions } from '@libp2p/interface' import type { ConnectionManager, Registrar } from '@libp2p/interface-internal' -import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr' +import type { Multiaddr } from '@multiformats/multiaddr' export interface HTTPComponents { privateKey: PrivateKey @@ -198,9 +198,6 @@ export class HTTP implements HTTPInterface, Startable { signal: init.signal ?? undefined }) - return fetch(stream, new URL(`http://${host}${decodeURIComponent(httpPath)}`), { - ...init, - logger: this.components.logger - }) + return fetch(stream, new URL(`http://${host}${decodeURIComponent(httpPath)}`), init) } } diff --git a/packages/http/src/http.ts b/packages/http/src/http.ts index c675f1a..b2500ae 100644 --- a/packages/http/src/http.ts +++ b/packages/http/src/http.ts @@ -16,21 +16,15 @@ import type { Dispatcher } from 'undici' export type { HTTPComponents } from './http.browser.js' function createConnection (connectionManager: ConnectionManager, peer: PeerId | Multiaddr | Multiaddr[], options?: AbortOptions): Socket { - const socket = new Libp2pSocket() - - Promise.resolve() - .then(async () => { - const connection = await connectionManager.openConnection(peer, options) - const stream = await connection.newStream(HTTP_PROTOCOL, options) - - socket.setStream(stream, connection) - socket.emit('connect') - }) - .catch(err => { - socket.emit('error', err) - }) - - return socket + return new Libp2pSocket( + Promise.resolve() + .then(async () => { + const connection = await connectionManager.openConnection(peer, options) + const stream = await connection.newStream(HTTP_PROTOCOL, options) + + return { stream, connection } + }) + ) } interface HTTPDispatcherComponents { diff --git a/packages/http/src/middleware/peer-id-auth.ts b/packages/http/src/middleware/peer-id-auth.ts index 4646525..5a59bcf 100644 --- a/packages/http/src/middleware/peer-id-auth.ts +++ b/packages/http/src/middleware/peer-id-auth.ts @@ -1,6 +1,7 @@ import { MissingAuthHeaderError, DEFAULT_AUTH_TOKEN_TTL, ClientInitiatedHandshake } from '@libp2p/http-peer-id-auth' import { getHost, isWebSocketUpgrade } from '@libp2p/http-utils' import { InvalidMessageError, InvalidParametersError } from '@libp2p/interface' +import { CODE_P2P } from '@multiformats/multiaddr' import type { Middleware, MiddlewareOptions, HTTP } from '../index.js' import type { VerifyPeer } from '@libp2p/http-peer-id-auth' import type { ComponentLogger, PeerId, PrivateKey } from '@libp2p/interface' @@ -180,7 +181,7 @@ function getCacheKey (resource: URL | Multiaddr[], headers: Headers): string { let prefix = '' if (Array.isArray(resource)) { - const peer = resource.map(ma => ma.getPeerId()) + const peer = resource.map(ma => ma.getComponents().findLast(c => c.code === CODE_P2P)?.value) .filter(Boolean) .pop() diff --git a/packages/http/src/registrar.ts b/packages/http/src/registrar.ts index b1e10ef..9c8eedb 100644 --- a/packages/http/src/registrar.ts +++ b/packages/http/src/registrar.ts @@ -5,7 +5,7 @@ import { HTTP_PROTOCOL, WEBSOCKET_HANDLER } from './constants.js' import { initializeRoute } from './routes/utils.js' import { wellKnownRoute } from './routes/well-known.js' import type { WebServer, HTTPRequestHandler, ProtocolMap, WebSocketHandler, HTTPRoute, HandlerRoute } from './index.js' -import type { ComponentLogger, IncomingStreamData, Logger } from '@libp2p/interface' +import type { ComponentLogger, Connection, Logger, Stream } from '@libp2p/interface' import type { Registrar } from '@libp2p/interface-internal' export interface HTTPRegistrarComponents { @@ -38,19 +38,14 @@ export class HTTPRegistrar { } async start (): Promise { - await this.components.registrar.handle(HTTP_PROTOCOL, (data) => { - this.onStream(data) - .catch(err => { - this.log.error('could not handle incoming stream - %e', err) - }) - }) + await this.components.registrar.handle(HTTP_PROTOCOL, this.onStream.bind(this)) } async stop (): Promise { await this.components.registrar.unhandle(HTTP_PROTOCOL) } - private async onStream ({ stream, connection }: IncomingStreamData): Promise { + private async onStream (stream: Stream, connection: Connection): Promise { const info = await readHeaders(stream) if (this.canHandle(info)) { @@ -65,7 +60,8 @@ export class HTTPRegistrar { // pass request to endpoint if available if (this.endpoint == null) { this.log('cannot handle incoming request %s %s and no endpoint configured', info.method, info.url) - await stream.sink([NOT_FOUND_RESPONSE]) + stream.send(NOT_FOUND_RESPONSE) + await stream.close() return } diff --git a/packages/interop/package.json b/packages/interop/package.json index dc18fdd..d687acf 100644 --- a/packages/interop/package.json +++ b/packages/interop/package.json @@ -3,14 +3,6 @@ "version": "0.0.0", "description": "Interop tests for the @libp2p/http collection of modules", "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/libp2p/js-libp2p-http/tree/main/packages/interop#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/libp2p/js-libp2p-http.git" - }, - "bugs": { - "url": "https://github.com/libp2p/js-libp2p-http/issues" - }, "type": "module", "scripts": { "build": "aegir build", @@ -27,32 +19,32 @@ "dep-check": "aegir dep-check" }, "devDependencies": { - "@chainsafe/libp2p-noise": "^16.1.3", - "@chainsafe/libp2p-yamux": "^7.0.1", + "@chainsafe/libp2p-noise": "^17.0.0", + "@chainsafe/libp2p-yamux": "^8.0.0", "@fastify/cookie": "^11.0.2", - "@fastify/websocket": "^11.1.0", - "@libp2p/crypto": "^5.1.4", + "@fastify/websocket": "^11.2.0", + "@libp2p/crypto": "^5.1.12", "@libp2p/http": "^1.0.0", "@libp2p/http-ping": "^1.0.0", "@libp2p/http-server": "^1.0.0", - "@libp2p/interface": "^2.10.2", - "@libp2p/memory": "^1.1.9", - "@libp2p/peer-id": "^5.1.5", - "@libp2p/ping": "^2.0.32", - "@libp2p/websockets": "^9.2.13", - "@multiformats/multiaddr": "^12.4.0", - "@types/cookie-parser": "^1.4.8", - "@types/express": "^5.0.2", + "@libp2p/interface": "^3.0.2", + "@libp2p/memory": "^2.0.4", + "@libp2p/peer-id": "^6.0.3", + "@libp2p/ping": "^3.0.4", + "@libp2p/websockets": "^10.0.5", + "@multiformats/multiaddr": "^13.0.1", + "@types/cookie-parser": "^1.4.9", + "@types/express": "^5.0.3", "@types/ws": "^8.18.1", - "aegir": "^47.0.16", + "aegir": "^47.0.22", "cookie-parser": "^1.4.7", "express": "^5.1.0", - "fastify": "^5.3.3", - "libp2p": "^2.8.8", + "fastify": "^5.6.1", + "libp2p": "^3.0.5", "p-defer": "^4.0.1", - "p-event": "^6.0.1", + "p-event": "^7.0.0", "sinon": "^21.0.0", - "ws": "^8.18.2" + "ws": "^8.18.3" }, "browser": { "./dist/src/auth/agent.js": "./dist/src/auth/agent.browser.js", diff --git a/packages/interop/test/fixtures/get-libp2p.ts b/packages/interop/test/fixtures/get-libp2p.ts index f9d8c83..de829c6 100644 --- a/packages/interop/test/fixtures/get-libp2p.ts +++ b/packages/interop/test/fixtures/get-libp2p.ts @@ -134,6 +134,9 @@ export async function getClient (): Promise> { http: http(), ping: ping(), pingHTTP: pingHTTP() + }, + connectionGater: { + denyDialMultiaddr: () => false } }) } @@ -150,6 +153,9 @@ export async function getLibp2pOverHttpHandler (): Promise< Libp2p<{ http: HTTP, }, connectionManager: { inboundConnectionThreshold: Infinity + }, + connectionGater: { + denyDialMultiaddr: () => false } }) diff --git a/packages/interop/test/ping.spec.ts b/packages/interop/test/ping.spec.ts index b0bac19..15bab55 100644 --- a/packages/interop/test/ping.spec.ts +++ b/packages/interop/test/ping.spec.ts @@ -1,4 +1,5 @@ /* eslint-env mocha */ +/* eslint-disable max-nested-callbacks */ import { noise } from '@chainsafe/libp2p-noise' import { yamux } from '@chainsafe/libp2p-yamux' @@ -9,7 +10,7 @@ import { pingHTTP } from '@libp2p/http-ping' import { stop } from '@libp2p/interface' import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { webSockets } from '@libp2p/websockets' -import { multiaddr } from '@multiformats/multiaddr' +import { CODE_P2P, multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import { createLibp2p } from 'libp2p' import sinon from 'sinon' @@ -54,6 +55,9 @@ describe('ping - HTTP over libp2p', () => { services: { http: http(), pingHTTP: pingHTTP() + }, + connectionGater: { + denyDialMultiaddr: () => false } }) }) @@ -106,7 +110,7 @@ describe('ping - libp2p over HTTP', () => { it(`should perform ping with a HTTP address with a peer id of a ${test.name} server`, async () => { const httpAddr = multiaddr(test.address).encapsulate(`/p2p/${process.env.HTTP_PEER_ID}`) const verifyPeer = sinon.stub().callsFake((peerId) => { - return peerId.equals(httpAddr.getPeerId()) + return peerId.equals(httpAddr.getComponents().findLast(c => c.code === CODE_P2P)?.value) }) await expect(client.services.pingHTTP.ping(httpAddr, { @@ -126,7 +130,7 @@ describe('ping - libp2p over HTTP', () => { const httpAddr = multiaddr(test.address).encapsulate(`/p2p/${peerId}`) const verifyPeer = sinon.stub().callsFake((peerId) => { - return peerId.equals(httpAddr.getPeerId()) + return peerId.equals(httpAddr.getComponents().findLast(c => c.code === CODE_P2P)?.value) }) await expect(client.services.pingHTTP.ping(httpAddr, { @@ -179,7 +183,7 @@ describe('ping - libp2p over WebSockets', () => { it(`should perform ping with a WebSocket address with a peer id of a ${test.name} server`, async () => { const httpAddr = multiaddr(test.address).encapsulate(`/p2p/${process.env.HTTP_PEER_ID}`) const verifyPeer = sinon.stub().callsFake((peerId) => { - return peerId.equals(httpAddr.getPeerId()) + return peerId.equals(httpAddr.getComponents().findLast(c => c.code === CODE_P2P)?.value) }) await expect(client.services.pingHTTP.ping(httpAddr, { @@ -200,7 +204,7 @@ describe('ping - libp2p over WebSockets', () => { const httpAddr = multiaddr(test.address).encapsulate(`/p2p/${peerId}`) const verifyPeer = sinon.stub().callsFake((peerId) => { - return peerId.equals(httpAddr.getPeerId()) + return peerId.equals(httpAddr.getComponents().findLast(c => c.code === CODE_P2P)?.value) }) await expect(client.services.pingHTTP.ping(httpAddr, { @@ -249,6 +253,9 @@ describe('ping - WebSockets over libp2p', () => { services: { http: http(), pingHTTP: pingHTTP() + }, + connectionGater: { + denyDialMultiaddr: () => false } }) }) From 4d0f7c7fb0396f5a39a9f5e50ec93afc02975536 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 2 Oct 2025 16:59:01 +0300 Subject: [PATCH 2/5] chore: fix tests --- packages/http-fetch/package.json | 2 +- packages/http-fetch/src/index.ts | 23 ++-- packages/http-fetch/src/read-response.ts | 143 +++++++++----------- packages/http-fetch/src/send-request.ts | 25 ++-- packages/http-utils/src/stream-to-socket.ts | 54 ++++---- 5 files changed, 122 insertions(+), 125 deletions(-) diff --git a/packages/http-fetch/package.json b/packages/http-fetch/package.json index 288887f..a3e503a 100644 --- a/packages/http-fetch/package.json +++ b/packages/http-fetch/package.json @@ -141,10 +141,10 @@ "@achingbrain/http-parser-js": "^0.5.9", "@libp2p/http-utils": "^1.0.0", "@libp2p/interface": "^3.0.2", - "@libp2p/utils": "^7.0.4", "uint8arrays": "^5.1.0" }, "devDependencies": { + "@libp2p/utils": "^7.0.4", "aegir": "^47.0.22" }, "sideEffects": false diff --git a/packages/http-fetch/src/index.ts b/packages/http-fetch/src/index.ts index 4bad77c..369840c 100644 --- a/packages/http-fetch/src/index.ts +++ b/packages/http-fetch/src/index.ts @@ -6,7 +6,6 @@ * socket. */ -import { byteStream } from '@libp2p/utils' import { readResponse } from './read-response.js' import { sendRequest } from './send-request.js' import type { Logger, Stream } from '@libp2p/interface' @@ -29,15 +28,19 @@ export interface SendRequestInit extends RequestInit { export async function fetch (stream: Stream, resource: string | URL, init: FetchInit = {}): Promise { const log = stream.log.newScope('http-fetch') resource = typeof resource === 'string' ? new URL(resource) : resource - const bytes = byteStream(stream) - await sendRequest(bytes, resource, { - ...init, - log - }) + const [ + response + ] = await Promise.all([ + readResponse(stream, resource, { + ...init, + log + }), + sendRequest(stream, resource, { + ...init, + log + }) + ]) - return readResponse(bytes, resource, { - ...init, - log - }) + return response } diff --git a/packages/http-fetch/src/read-response.ts b/packages/http-fetch/src/read-response.ts index 5657530..7e0eb45 100644 --- a/packages/http-fetch/src/read-response.ts +++ b/packages/http-fetch/src/read-response.ts @@ -3,99 +3,82 @@ import { Response } from '@libp2p/http-utils' import { InvalidResponseError } from './errors.js' import type { SendRequestInit } from './index.js' import type { Stream } from '@libp2p/interface' -import type { ByteStream } from '@libp2p/utils' const nullBodyStatus = [101, 204, 205, 304] -export async function readResponse (bytes: ByteStream, resource: URL, init: SendRequestInit): Promise { - return new Promise((resolve, reject) => { - const body = new TransformStream() - const writer = body.writable.getWriter() - let headersComplete = false +export async function readResponse (stream: Stream, resource: URL, init: SendRequestInit): Promise { + const output = Promise.withResolvers() + const body = new TransformStream() + const writer = body.writable.getWriter() + let headersComplete = false + + const parser = new HTTPParser('RESPONSE') + parser.maxHeaderSize = init.maxHeaderSize ?? HTTPParser.maxHeaderSize + parser[HTTPParser.kOnHeadersComplete] = (info) => { + init.log('response headers complete') + headersComplete = true + const headers = new Headers() + + for (let i = 0; i < info.headers.length; i += 2) { + headers.append(info.headers[i], info.headers[i + 1]) + } - const parser = new HTTPParser('RESPONSE') - parser.maxHeaderSize = init.maxHeaderSize ?? HTTPParser.maxHeaderSize - parser[HTTPParser.kOnHeadersComplete] = (info) => { - init.log('response headers complete') - headersComplete = true - const headers = new Headers() + let responseBody: BodyInit | null = body.readable - for (let i = 0; i < info.headers.length; i += 2) { - headers.append(info.headers[i], info.headers[i + 1]) - } + if (nullBodyStatus.includes(info.statusCode)) { + body.writable.close().catch(() => {}) + body.readable.cancel().catch(() => {}) + responseBody = null + } - let responseBody: BodyInit | null = body.readable + const response = new Response(responseBody, { + status: info.statusCode, + statusText: info.statusMessage, + headers + }) + + output.resolve(response) + } + parser[HTTPParser.kOnBody] = (buf) => { + init.log('response read body %d bytes', buf.byteLength) + writer.write(buf) + .catch((err: Error) => { + output.reject(err) + }) + } + parser[HTTPParser.kOnMessageComplete] = () => { + init.log('response message complete') + writer.close() + .catch((err: Error) => { + output.reject(err) + }) + } - if (nullBodyStatus.includes(info.statusCode)) { - body.writable.close().catch(() => {}) - body.readable.cancel().catch(() => {}) - responseBody = null - } + let read = 0 + stream.addEventListener('message', ({ data }) => { + init.log('response stream read %d bytes', data.byteLength) + read += data.byteLength - const response = new Response(responseBody, { - status: info.statusCode, - statusText: info.statusMessage, - headers - }) + const result = parser.execute(data.subarray(), 0, data.byteLength) - resolve(response) - } - parser[HTTPParser.kOnBody] = (buf) => { - init.log('response read body %d bytes', buf.byteLength) - writer.write(buf) - .catch((err: Error) => { - reject(err) - }) + if (result instanceof Error) { + stream.abort(result) + parser.finish() } - parser[HTTPParser.kOnMessageComplete] = () => { - init.log('response message complete') - writer.close() - .catch((err: Error) => { - reject(err) - }) - - const stream = bytes.unwrap() - stream.close() - .catch(err => { - stream.abort(err) - }) + }) + stream.addEventListener('remoteCloseWrite', () => { + if (!headersComplete) { + output.reject(new InvalidResponseError(`Response ended before headers were received, read ${read} bytes`)) } - Promise.resolve() - .then(async () => { - let read = 0 - while (true) { - init.log('read chunk from response') + parser.finish() - const chunk = await bytes.read({ - signal: init.signal ?? undefined - }) - - init.log('read', chunk) - - if (chunk == null) { - const err = parser.finish() - - if (err != null) { - init.log('response stream ended with error - %e', err) - } else { - init.log('response stream ended') - } - - if (!headersComplete) { - reject(new InvalidResponseError(`Response ended before headers were received, read ${read} bytes`)) - } - - break - } - - init.log('response stream read %d bytes', chunk.byteLength) - read += chunk.byteLength - parser.execute(chunk.subarray(), 0, chunk.byteLength) - } - }) - .catch((err: Error) => { - reject(err) + // close our writable end once the server has finished sending the response + stream.close() + .catch(err => { + stream.abort(err) }) }) + + return output.promise } diff --git a/packages/http-fetch/src/send-request.ts b/packages/http-fetch/src/send-request.ts index 81235af..3a839ae 100644 --- a/packages/http-fetch/src/send-request.ts +++ b/packages/http-fetch/src/send-request.ts @@ -3,9 +3,8 @@ import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' import { normalizeContent } from './utils.js' import type { SendRequestInit } from './index.js' import type { Stream } from '@libp2p/interface' -import type { ByteStream } from '@libp2p/utils' -export async function sendRequest (bytes: ByteStream, url: URL, init: SendRequestInit): Promise { +export async function sendRequest (stream: Stream, url: URL, init: SendRequestInit): Promise { const headers = new Headers(init.headers) const host = headers.get('host') ?? url.hostname @@ -24,27 +23,31 @@ export async function sendRequest (bytes: ByteStream, url: URL, init: Se '' ] - await bytes.write(uint8arrayFromString(req.join('\r\n')), { - signal: init.signal ?? undefined - }) + if (!stream.send(uint8arrayFromString(req.join('\r\n')))) { + await stream.onDrain({ + signal: init.signal ?? undefined + }) + } if (content != null) { init.log('request sending body') - await sendBody(bytes, content, init) + await sendBody(stream, content, init) } } -async function sendBody (bytes: ByteStream, stream: ReadableStream, init: SendRequestInit): Promise { - const reader = stream.getReader() +async function sendBody (stream: Stream, body: ReadableStream, init: SendRequestInit): Promise { + const reader = body.getReader() while (true) { const { done, value } = await reader.read() if (value != null) { init.log('request send %d bytes', value.byteLength) - await bytes.write(value, { - signal: init.signal ?? undefined - }) + if (!stream.send(value)) { + await stream.onDrain({ + signal: init.signal ?? undefined + }) + } } if (done) { diff --git a/packages/http-utils/src/stream-to-socket.ts b/packages/http-utils/src/stream-to-socket.ts index f37e990..368a0db 100644 --- a/packages/http-utils/src/stream-to-socket.ts +++ b/packages/http-utils/src/stream-to-socket.ts @@ -19,7 +19,9 @@ export class Libp2pSocket extends Duplex { #log?: Logger - constructor (initStream: Promise<{ stream: Stream, connection: Connection }>) { + constructor (stream: Stream, connection: Connection) + constructor (initStream: Promise<{ stream: Stream, connection: Connection }>) + constructor (...args: any[]) { super() this.bytesRead = 0 @@ -27,32 +29,38 @@ export class Libp2pSocket extends Duplex { this.allowHalfOpen = true this.remoteAddress = '' - this.#initStream = initStream.then(({ stream, connection }) => { - this.#log = stream.log.newScope('libp2p-socket') - this.remoteAddress = connection.remoteAddr.toString() - - stream.addEventListener('message', (evt) => { - this.push(evt.data.subarray()) - }) - - stream.addEventListener('close', (evt) => { - if (evt.error != null) { - this.destroy(evt.error) - } else { - this.push(null) - } + if (args.length === 2) { + this.gotStream({ stream: args[0], connection: args[1] }) + this.#initStream = Promise.resolve(args[0]) + } else { + this.#initStream = args[0].then(this.gotStream.bind(this), (err: any) => { + this.emit('error', err) + throw err }) + } + } - stream.pause() + private gotStream ({ stream, connection }: { stream: Stream, connection: Connection }): Stream { + this.#log = stream.log.newScope('libp2p-socket') + this.remoteAddress = connection.remoteAddr.toString() - this.emit('connect') + stream.addEventListener('message', (evt) => { + this.push(evt.data.subarray()) + }) - return stream + stream.addEventListener('close', (evt) => { + if (evt.error != null) { + this.destroy(evt.error) + } else { + this.push(null) + } }) - .catch(err => { - this.emit('error', err) - throw err - }) + + stream.pause() + + this.emit('connect') + + return stream } getStream (cb: (stream: Stream) => void): void { @@ -233,5 +241,5 @@ export class Libp2pSocket extends Duplex { } export function streamToSocket (stream: Stream, connection: Connection): Socket { - return new Libp2pSocket(Promise.resolve({ stream, connection })) + return new Libp2pSocket(stream, connection) } From f1767d069bea1dc183f25ad255d294bf787aa2de Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 2 Oct 2025 17:05:48 +0300 Subject: [PATCH 3/5] chore: tests again --- packages/http-fetch/src/index.ts | 5 +++++ packages/http-fetch/src/read-response.ts | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/http-fetch/src/index.ts b/packages/http-fetch/src/index.ts index 369840c..d3fb18c 100644 --- a/packages/http-fetch/src/index.ts +++ b/packages/http-fetch/src/index.ts @@ -42,5 +42,10 @@ export async function fetch (stream: Stream, resource: string | URL, init: Fetch }) ]) + // close our writable end we've sent the request + await stream.close({ + signal: init.signal ?? undefined + }) + return response } diff --git a/packages/http-fetch/src/read-response.ts b/packages/http-fetch/src/read-response.ts index 7e0eb45..a0fcaf6 100644 --- a/packages/http-fetch/src/read-response.ts +++ b/packages/http-fetch/src/read-response.ts @@ -72,12 +72,6 @@ export async function readResponse (stream: Stream, resource: URL, init: SendReq } parser.finish() - - // close our writable end once the server has finished sending the response - stream.close() - .catch(err => { - stream.abort(err) - }) }) return output.promise From 05e7116db77d6f3aad22b6c48282f2bf65515134 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 2 Oct 2025 17:17:36 +0300 Subject: [PATCH 4/5] chore: tests --- packages/interop/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/interop/package.json b/packages/interop/package.json index d687acf..7665076 100644 --- a/packages/interop/package.json +++ b/packages/interop/package.json @@ -14,7 +14,6 @@ "test:node": "aegir test -t node --cov", "test:electron-main": "aegir test -t electron-main", "test:chrome-webworker": "aegir test -t webworker", - "test:webkit": "aegir test -t browser -- --browser webkit", "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", "dep-check": "aegir dep-check" }, From 813b54e15f32d8da9e49629a84ea46f184822ec8 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 2 Oct 2025 17:23:53 +0300 Subject: [PATCH 5/5] chore: revert --- packages/interop/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/interop/package.json b/packages/interop/package.json index 7665076..d687acf 100644 --- a/packages/interop/package.json +++ b/packages/interop/package.json @@ -14,6 +14,7 @@ "test:node": "aegir test -t node --cov", "test:electron-main": "aegir test -t electron-main", "test:chrome-webworker": "aegir test -t webworker", + "test:webkit": "aegir test -t browser -- --browser webkit", "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", "dep-check": "aegir dep-check" },