feat(taildrive): discover peers with shares
CI / lint (pull_request) Successful in 1m49s
CI / format (pull_request) Successful in 2m0s
CI / typecheck (pull_request) Canceled after 0s
CI / typetest (pull_request) Canceled after 0s
CI / node-tests (pull_request) Canceled after 0s
CI / browser-tests (pull_request) Canceled after 0s
CI / install (pull_request) Canceled after 5m12s

Co-Authored-By: gpt-5.6-sol <noreply@openai.com>
This commit is contained in:
2026-08-03 01:23:10 +00:00
co-authored by Codex
parent ab987f50d0
commit a0ad4bf17b
5 changed files with 171 additions and 0 deletions
+1
View File
@@ -134,3 +134,4 @@ The test suite for `packages/http` was mostly generated by Claude Code, which al
- **CI — bounded Node heap (issue #136)**: Claude Code (Claude Opus 5) added a workflow-level `NODE_OPTIONS: --max-old-space-size=4096` to `.gitea/workflows/ci.yml`, so every CI job that runs Node (including the per-file workers `node --test` spawns and Turbo-invoked package scripts, which inherit the variable) fails with a legible V8 heap-limit error instead of growing until the runner's own memory limit kills the task and any jobs sharing the machine.
- **`@webnet/tsconnect` — minimal build output (issue #154)**: `gpt-5.6-sol` changed the Tailscale package build to use a temporary staging directory and copy only `main.wasm`, `build-info.json`, `wasm_exec.js`, and `cacert.pem` into `dist/`, excluding unused upstream demo bundles, source maps, styles, and package metadata from published packages and Turbo caches.
- **turbo setup - cache issue**: written collaboratively between human codinget and agent claude-opus-5 who double checked and debugged my work.
- **`@webnet/taildrive` — share-bearing peer discovery**: `gpt-5.6-sol` added a Webnet-only `listDrivePeers(ipn)` helper that probes the existing candidates through the shared WebDAV client, positively retains peers exporting at least one share, bounds concurrent and malformed responses, and exposes a reusable per-peer `DAVClient` factory for a later whole-tailnet `AsyncVFS` hierarchy. Focused tests cover populated, empty, invalid, unreachable, and ordered peer results.
+3
View File
@@ -10495,11 +10495,14 @@
"name": "@webnet/taildrive",
"version": "0.1.0",
"dependencies": {
"@webnet/drive": "*",
"@webnet/http": "*",
"@webnet/transport": "*",
"@webnet/tsconnect": "*"
},
"devDependencies": {
"@types/node": "^25.6.0",
"@webnet/vfs": "*",
"c8": "^11.0.0",
"tsx": "^4.21.0",
"typescript": "^6.0.2"
+7
View File
@@ -4,6 +4,10 @@
"description": "Taildrive utilities: WebDAV bridge for WASM tsconnect nodes and client helpers",
"type": "module",
"exports": {
"./client": {
"types": "./dist/client/index.d.ts",
"default": "./dist/client/index.js"
},
"./server": {
"types": "./dist/server/index.d.ts",
"default": "./dist/server/index.js"
@@ -16,11 +20,14 @@
"typecheck": "tsc --project tsconfig.json --noEmit"
},
"dependencies": {
"@webnet/drive": "*",
"@webnet/http": "*",
"@webnet/transport": "*",
"@webnet/tsconnect": "*"
},
"devDependencies": {
"@types/node": "^25.6.0",
"@webnet/vfs": "*",
"c8": "^11.0.0",
"tsx": "^4.21.0",
"typescript": "^6.0.2"
@@ -0,0 +1,96 @@
import assert from "node:assert/strict"
import { suite, test } from "node:test"
import { createDAVHandler } from "@webnet/drive"
import { Server, type Handler } from "@webnet/http/server"
import type { IpnClient, IPNDrivePeer } from "@webnet/tsconnect"
import { loopbackListener } from "@webnet/transport/loopback"
import type { RawDialer } from "@webnet/transport"
import { MemoryVFS } from "@webnet/vfs/memory"
import { createTaildriveClient, listDrivePeers } from "./index.js"
type TestServer = {
dialer: RawDialer
close(): Promise<void>
}
function serve(handler: Handler): TestServer {
const [listener, dialer] = loopbackListener()
const stopped = new Server(handler).listen(listener, { onError: () => {} })
return {
dialer,
async close() {
listener.close()
await stopped
},
}
}
async function serveDrive(shares: string[]): Promise<TestServer> {
const vfs = new MemoryVFS()
for (const share of shares) await vfs.mkdir(`/${share}`)
return serve(createDAVHandler(vfs, { prefix: "/v0/drive" }))
}
function peer(name: string): IPNDrivePeer {
return { name, peerAPIURL: `http://${name}`, stableNodeID: `${name}-id`, online: true }
}
function fakeIpn(peers: IPNDrivePeer[], servers: Map<string, RawDialer>): IpnClient {
return {
listDrivePeers: async () => peers,
dial: async (_network: "tcp" | "tcp4" | "tcp6", address: string) => {
const separator = address.lastIndexOf(":")
const host = address.slice(0, separator)
const port = Number(address.slice(separator + 1))
const dialer = servers.get(host)
if (!dialer) throw new Error("unreachable")
return dialer.dial(host, port)
},
} as IpnClient
}
suite("Taildrive client", () => {
test("filters out empty, non-WebDAV, and unreachable peers while preserving order", async () => {
const first = await serveDrive(["documents"])
const empty = await serveDrive([])
const second = await serveDrive(["photos", "music"])
const invalid = serve(async (ctx) => {
ctx.res.body = "not WebDAV"
})
const servers = new Map<string, RawDialer>([
["first", first.dialer],
["empty", empty.dialer],
["second", second.dialer],
["invalid", invalid.dialer],
])
const ipn = fakeIpn(
[peer("first"), peer("empty"), peer("unreachable"), peer("invalid"), peer("second")],
servers,
)
try {
assert.deepEqual(
(await listDrivePeers(ipn)).map(({ name }) => name),
["first", "second"],
)
} finally {
await Promise.all([first.close(), empty.close(), second.close(), invalid.close()])
}
})
test("creates a reusable AsyncVFS client rooted at a peer's shares", async () => {
const server = await serveDrive(["documents"])
const ipn = fakeIpn([peer("files")], new Map([["files", server.dialer]]))
try {
const client = createTaildriveClient(ipn, peer("files"))
assert.deepEqual(
(await client.readdir("/")).map(({ name }) => name),
["documents"],
)
await client.transferState()
} finally {
await server.close()
}
})
})
+64
View File
@@ -0,0 +1,64 @@
import { DAVClient } from "@webnet/drive/client"
import { PooledDialer } from "@webnet/http/client"
import type { IpnClient, IPNDrivePeer } from "@webnet/tsconnect"
import type { RawDialer } from "@webnet/transport"
const DRIVE_PATH = "/v0/drive"
const PROBE_TIMEOUT = 5_000
const MAX_PROBES = 8
const MAX_RESPONSE_SIZE = 1024 * 1024
function addr(host: string, port: number): string {
return `${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`
}
function ipnDialer(ipn: IpnClient): RawDialer {
return {
dial: (host, port) => ipn.dial("tcp", addr(host, port)),
dialTls: (host, port) =>
ipn.dialTLS(addr(host, port), {
serverName: host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host,
}),
}
}
function driveBase(peer: IPNDrivePeer): string {
return peer.peerAPIURL.replace(/\/+$/, "") + DRIVE_PATH
}
/** Create an AsyncVFS client for all Taildrive shares exported by one peer. */
export function createTaildriveClient(ipn: IpnClient, peer: IPNDrivePeer): DAVClient {
return new DAVClient({ dialer: ipnDialer(ipn), base: driveBase(peer) })
}
/**
* List peers that positively answer as WebDAV servers and export at least one share.
* Unreachable, invalid, and empty peers are omitted without failing the whole listing.
*/
export async function listDrivePeers(ipn: IpnClient): Promise<IPNDrivePeer[]> {
const peers = await ipn.listDrivePeers()
const pool = new PooledDialer(ipnDialer(ipn), {
keepAlive: false,
maxPerOrigin: 1,
max: MAX_PROBES,
headersTimeout: PROBE_TIMEOUT,
bodyTimeout: PROBE_TIMEOUT,
maxBodyLength: MAX_RESPONSE_SIZE,
})
try {
const results = await Promise.all(
peers.map(async (peer) => {
try {
const client = new DAVClient({ dialer: pool, base: driveBase(peer) })
return (await client.readdir("/")).length > 0 ? peer : null
} catch {
return null
}
}),
)
return results.filter((peer): peer is IPNDrivePeer => peer !== null)
} finally {
await pool.shutdown()
}
}