feat(transport): extract @webnet/transport package

Pulls RawTransport, RawDialer, RawListener, Reader, Writer, and PairSync
out of @webnet/http into a new protocol-agnostic @webnet/transport package.
Node and loopback transport implementations move there as sub-exports
(./node, ./loopback); @webnet/http re-exports the types to keep existing
internal imports resolving.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:45:31 +00:00
co-authored by Claude
parent 7f2e021623
commit 1fbe2c844c
29 changed files with 170 additions and 75 deletions
+29
View File
@@ -4908,6 +4908,10 @@
"resolved": "packages/test-app",
"link": true
},
"node_modules/@webnet/transport": {
"resolved": "packages/transport",
"link": true
},
"node_modules/@webnet/tsconnect": {
"resolved": "packages/tsconnect",
"link": true
@@ -12519,6 +12523,7 @@
"name": "@webnet/http",
"version": "0.1.0",
"dependencies": {
"@webnet/transport": "*",
"regexparam": "^3.0.0"
},
"devDependencies": {
@@ -12568,6 +12573,30 @@
"node": ">=14.17"
}
},
"packages/transport": {
"name": "@webnet/transport",
"version": "0.1.0",
"devDependencies": {
"@types/node": "^25.6.0",
"c8": "^11.0.0",
"tsx": "^4.21.0",
"typescript": "^6.0.2"
}
},
"packages/transport/node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"packages/tsconnect": {
"name": "@webnet/tsconnect",
"version": "0.1.0",
+1 -16
View File
@@ -38,22 +38,6 @@
"types": "./dist/server/_internals.d.ts",
"default": "./dist/server/_internals.js"
},
"./node": {
"types": "./dist/node/index.d.ts",
"default": "./dist/node/index.js"
},
"./node/_internals": {
"types": "./dist/node/_internals.d.ts",
"default": "./dist/node/_internals.js"
},
"./loopback": {
"types": "./dist/loopback/index.d.ts",
"default": "./dist/loopback/index.js"
},
"./loopback/_internals": {
"types": "./dist/loopback/_internals.d.ts",
"default": "./dist/loopback/_internals.js"
},
"./websocket": {
"types": "./dist/websocket/index.d.ts",
"default": "./dist/websocket/index.js"
@@ -97,6 +81,7 @@
"typescript": "^6.0.2"
},
"dependencies": {
"@webnet/transport": "*",
"regexparam": "^3.0.0"
}
}
-1
View File
@@ -3,7 +3,6 @@ export { ClientResponseImpl, combineClientConnectionOptions } from "./client/_in
export {
BasicBodyReader,
ChunkedBodyReader,
PairSync,
ReadBuffer,
ReadableHttpImpl,
TimeoutError,
+1 -1
View File
@@ -6,7 +6,7 @@ import type { ClientResponse } from "./types.js"
import { ReadBuffer } from "../common/buffer.js"
import type { RawTransport } from "../common/types.js"
import { skipIfNotIntegration } from "../test-helpers/flags.js"
import { loopbackTransportPair } from "../loopback/transport.js"
import { loopbackTransportPair } from "@webnet/transport/loopback"
import { TimeoutError } from "../common/utils.js"
const enc = new TextEncoder()
@@ -1,8 +1,8 @@
import test, { suite, after } from "node:test"
import { Fetch, makeFetch } from "../client/fetch.js"
import { nodeDialer } from "./transport.js"
import { type Fetch, makeFetch } from "./fetch.js"
import { nodeDialer } from "@webnet/transport/node"
import assert from "node:assert"
import { UnpooledDialer } from "../client/pool.js"
import { UnpooledDialer } from "./pool.js"
import { skipIfNotExternal } from "../test-helpers/flags.js"
function fetchUrlTest(url: string, fetch: Fetch, count: number) {
+1 -1
View File
@@ -2,7 +2,7 @@ import test, { suite } from "node:test"
import assert from "node:assert"
import { fetch, fetchStream, makeFetch } from "./fetch.js"
import { UnpooledDialer } from "./pool.js"
import { loopbackListener, loopbackTransportPair } from "../loopback/transport.js"
import { loopbackListener, loopbackTransportPair } from "@webnet/transport/loopback"
import { ReadBuffer } from "../common/buffer.js"
import { skipIfNotIntegration } from "../test-helpers/flags.js"
+2 -1
View File
@@ -1,7 +1,8 @@
import test, { suite } from "node:test"
import assert from "node:assert"
import { UnpooledDialer, PooledDialer } from "./pool.js"
import { loopbackTransportPair, type LoopbackTransportHalf } from "../loopback/transport.js"
import { loopbackTransportPair } from "@webnet/transport/loopback"
import type { LoopbackTransportHalf } from "@webnet/transport/loopback/_internals"
import type { RawDialer } from "./types.js"
import { skipIfNotIntegration } from "../test-helpers/flags.js"
+1 -5
View File
@@ -1,10 +1,6 @@
import type { RawTransport, ReadableHttp, WritableHttp } from "../common/types.js"
import type { ClientConnectionOptions } from "./connection.js"
export interface RawDialer {
dial(host: string, port: number): Promise<RawTransport>
dialTls?(host: string, port: number): Promise<RawTransport>
}
export type { RawDialer } from "@webnet/transport"
export interface ClientInformationalResponse {
readonly status: number
-1
View File
@@ -1,6 +1,5 @@
export { ReadableHttpImpl, WritableHttpImpl } from "./objects.js"
export { prependTransport, ReadBuffer, WriteBuffer } from "./buffer.js"
export { PairSync } from "./pair.js"
export {
BasicBodyReader,
ChunkedBodyReader,
+1 -24
View File
@@ -1,28 +1,5 @@
import type { BodyReaderOptions } from "./reader.js"
export interface RawTransport {
get closed(): boolean
close(): void | Promise<void>
write(data: Uint8Array): Promise<void | number>
read(): Promise<Uint8Array>
// optional features, should degrade gracefully if absent
// close the write end, not the entire socket
halfClose?(): void | Promise<void>
// true if the read end is closed
readonly readEnded?: boolean
// resolves when fully closed, to cancel ongoing work
readonly whenClosed?: Promise<void>
// `host:port` format
readonly remoteAddr?: string
readonly localAddr?: string
}
export type Reader = Pick<RawTransport, "closed" | "read">
export type Writer = Pick<RawTransport, "write">
export type { RawTransport, Reader, Writer } from "@webnet/transport"
export type SimpleBody = string | Uint8Array
export type StreamBody = AsyncIterable<SimpleBody | { flush: true }>
+1 -1
View File
@@ -4,7 +4,7 @@ import { ServerResponseImpl } from "./objects.js"
import type { RawTransport } from "../common/types.js"
import type { ServerRequestImpl } from "./objects.js"
import { ReadBuffer } from "../common/buffer.js"
import { loopbackTransportPair } from "../loopback/transport.js"
import { loopbackTransportPair } from "@webnet/transport/loopback"
import assert from "node:assert"
import { skipIfNotIntegration } from "../test-helpers/flags.js"
+1 -1
View File
@@ -1,7 +1,7 @@
import test, { suite } from "node:test"
import assert from "node:assert"
import { ServerRequestImpl, ServerResponseImpl } from "./objects.js"
import { loopbackTransportPair } from "../loopback/transport.js"
import { loopbackTransportPair } from "@webnet/transport/loopback"
import { Headers } from "../common/headers.js"
function makeReq({
+1 -12
View File
@@ -1,16 +1,5 @@
import type { RawTransport, ReadableHttp, WritableHttp } from "../common/types.js"
export interface RawListener {
get closed(): boolean
close(): void | Promise<void>
accept(): Promise<RawTransport>
// optional features, should degrade gracefully if absent
// `host:port` format
readonly addr?: string
}
export type { RawListener } from "@webnet/transport"
export interface ServerRequest extends ReadableHttp {
readonly transport: RawTransport
@@ -1,6 +1,6 @@
import test, { suite } from "node:test"
import assert from "node:assert"
import { loopbackTransportPair, loopbackListener } from "../loopback/transport.js"
import { loopbackTransportPair, loopbackListener } from "@webnet/transport/loopback"
import { ServerConnection } from "../server/connection.js"
import { ClientConnection } from "../client/connection.js"
import { ClientRequestImpl } from "../client/objects.js"
+48
View File
@@ -0,0 +1,48 @@
{
"name": "@webnet/transport",
"version": "0.1.0",
"description": "Transport abstractions and implementations for webnet",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./_internals": {
"types": "./dist/_internals.d.ts",
"default": "./dist/_internals.js"
},
"./node": {
"types": "./dist/node/index.d.ts",
"default": "./dist/node/index.js"
},
"./node/_internals": {
"types": "./dist/node/_internals.d.ts",
"default": "./dist/node/_internals.js"
},
"./loopback": {
"types": "./dist/loopback/index.d.ts",
"default": "./dist/loopback/index.js"
},
"./loopback/_internals": {
"types": "./dist/loopback/_internals.d.ts",
"default": "./dist/loopback/_internals.js"
}
},
"scripts": {
"build": "rm -rf dist && tsc --project tsconfig.json",
"test": "tsx --test --test-timeout=10000 'src/**/*.test.ts'",
"test:unit": "RUN_INTEGRATION=false npm run test",
"test:coverage": "c8 --src src --exclude 'src/**/*.test.ts' --exclude 'src/test-helpers/**' --reporter text --reporter lcov node --enable-source-maps --import tsx --test-timeout=10000 --test 'src/**/*.test.ts'",
"test:coverage:unit": "RUN_INTEGRATION=false npm run test:coverage",
"typecheck": "tsc --project tsconfig.json --noEmit"
},
"devDependencies": {
"@types/node": "^25.6.0",
"c8": "^11.0.0",
"tsx": "^4.21.0",
"typescript": "^6.0.2"
}
}
+1
View File
@@ -0,0 +1 @@
export { PairSync } from "./pair.js"
+1
View File
@@ -0,0 +1 @@
export type { RawTransport, Reader, Writer, RawDialer, RawListener } from "./types.js"
@@ -1,7 +1,5 @@
import type { RawDialer } from "../client/types.js"
import { PairSync } from "../common/pair.js"
import type { RawTransport } from "../common/types.js"
import type { RawListener } from "../server/types.js"
import type { RawDialer, RawTransport, RawListener } from "../types.js"
import { PairSync } from "../pair.js"
export class LoopbackTransportHalf implements RawTransport {
#buffers: Uint8Array[] = []
@@ -1,8 +1,6 @@
import { connect, createServer, Server, type Socket } from "node:net"
import { connect as connectTls } from "node:tls"
import type { RawTransport } from "../common/types.js"
import type { RawDialer } from "../client/types.js"
import type { RawListener } from "../server/types.js"
import type { RawTransport, RawDialer, RawListener } from "../types.js"
export class NodeTransport implements RawTransport {
#socket: Socket
@@ -0,0 +1,14 @@
function bool(val: string | undefined, def: boolean): boolean {
if (val === undefined) return def
return val === "1" || val.toLowerCase() === "true"
}
export const RUN_INTEGRATION = bool(process.env.RUN_INTEGRATION, true)
export const RUN_EXTERNAL = bool(process.env.RUN_EXTERNAL, false)
export const skipIfNotIntegration: boolean | string = RUN_INTEGRATION
? false
: "set RUN_INTEGRATION=true to enable"
export const skipIfNotExternal: boolean | string = RUN_EXTERNAL
? false
: "set RUN_EXTERNAL=true to enable"
+40
View File
@@ -0,0 +1,40 @@
export interface RawTransport {
get closed(): boolean
close(): void | Promise<void>
write(data: Uint8Array): Promise<void | number>
read(): Promise<Uint8Array>
// optional features, should degrade gracefully if absent
// close the write end, not the entire socket
halfClose?(): void | Promise<void>
// true if the read end is closed
readonly readEnded?: boolean
// resolves when fully closed, to cancel ongoing work
readonly whenClosed?: Promise<void>
// `host:port` format
readonly remoteAddr?: string
readonly localAddr?: string
}
export type Reader = Pick<RawTransport, "closed" | "read">
export type Writer = Pick<RawTransport, "write">
export interface RawDialer {
dial(host: string, port: number): Promise<RawTransport>
dialTls?(host: string, port: number): Promise<RawTransport>
}
export interface RawListener {
get closed(): boolean
close(): void | Promise<void>
accept(): Promise<RawTransport>
// optional features, should degrade gracefully if absent
// `host:port` format
readonly addr?: string
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"verbatimModuleSyntax": true,
"strict": true,
"skipLibCheck": true,
"lib": ["ES2022"],
"types": ["node"],
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"inlineSourceMap": true,
"inlineSources": true
},
"include": ["src/**/*"],
"exclude": ["src/**/*.test.ts", "src/test-helpers"]
}