Commit Graph
240 Commits
Author SHA1 Message Date
codingetandClaude 720a0c9d59 feat(example-app): SharedWorker demo with main-thread fallback
- Add src/worker.ts as the webpack worker entry (imports @webnet/tsconnect-worker/worker)
- IpnProvider: add useWorker/workerAvailable state; when useWorker is on,
  useBuildIpnWorker() creates the client and its embedded store is passed to
  IpnStoreProvider; when off, existing useBuildIpn() path runs unchanged
- Debug: add "use SharedWorker" checkbox (checked by default when available,
  disabled when not); state dropdown shows indexeddb/memory in worker mode
  and localStorage/sessionStorage/disable in main-thread mode; taildrop
  option label reflects opfs vs memory depending on mode
- useBuildIpnWorker: add optional workerOptions param instead of hardcoding
  { type: "module" } — webpack bundles the worker as a classic script, so
  the caller passes no options; module-mode callers opt in explicitly
- Add @webnet/tsconnect-react, @webnet/tsconnect-redux, @webnet/tsconnect-worker
  to example-app package.json dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:35:07 +00:00
codingetandClaude c65dd00f71 fix(tsconnect-react): narrow useBuildIpnWorker worker arg to string | URL
Accepting a SharedWorker instance was unsafe: in StrictMode, React runs
effects twice in quick succession, and since the connect() promise is
async, clientRef.current is still null when the second effect fires —
so both calls would share the same sw.port, causing two IpnWorkerClient
instances to collide on one MessagePort.

With string | URL, each call to new SharedWorker() fires a fresh connect
event in the worker and returns an independent MessagePort, so the
StrictMode double-invoke is safe: the first connection gets disconnect()ed
when its promise resolves (cleanedUp is true), and the second becomes
the active client.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:35:07 +00:00
codingetandClaude 6a6fbc3e5b feat(tsconnect-react): add useBuildIpnWorker hook for SharedWorker clients
- Add disconnect() to IpnWorkerClient (releases Web Lock without shutdown)
- Widen IpnContext from IPN to IpnClient
- Add useBuildIpnWorker(worker, config, runParams) hook with StrictMode-safe
  cleanup: disconnect() fires if the cleanup runs before connect() resolves,
  or immediately after if it resolves first
- Export useBuildIpnWorker from package index
- Add @webnet/tsconnect-worker to package dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:34:14 +00:00
codingetandClaude 1a94620141 fix(tsconnect): move IndexedDBState and use IpnClient in helpers
Place IndexedDBState in the state-storage section alongside
InMemoryState and WebStorageState rather than between the
getIceServers doc comment and its function body.

Change getIceServers to accept IpnClient instead of the raw IPN
interface so callers can pass either IPN or IpnWorkerClient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:34:14 +00:00
codingetandClaude b4273702db feat(tsconnect-worker): round-2 improvements
- Move IndexedDBState to @webnet/tsconnect/helpers and export from
  package index; remove it from tsconnect-worker/storage.ts
- Add IpnClient interface, IpnSSHTermConfig, and IpnPacketConn to
  @webnet/tsconnect; IPN class and IpnWorkerClient both implement it
- Make IPN.ssh() async to unify the sync/async split
- Add preloadedState parameter to buildIpnStore; worker now sends a
  preloadState message (full Redux state) on connect instead of
  replaying synthetic actions
- WorkerConn implements RawTransport, WorkerTCPListener implements
  RawListener, WorkerPacketConn implements IpnPacketConn
- WorkerSSHSession.resize/close fire-and-forget (sync) to implement
  IPNSSHSession; remove resizeResult/closeResult round-trips
- ReadableStream transfer fallback via MessageChannel sub-protocol for
  Safari compatibility on sendFile and openWaitingFile
- Add stateStorage: "indexeddb" | "memory" option to WorkerConfig
- Export TLSCertKeyPair, WhoIsResponse, PingType, PingResult,
  DNSQueryResult from @webnet/tsconnect package index
- Add @webnet/transport to tsconnect-worker dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:34:14 +00:00
codingetandClaude 75a841a15d docs(readme): update AI disclosure for tsconnect-worker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:31:20 +00:00
codingetandClaude 5ee74ffc9d feat(tsconnect-worker): add SharedWorker binding for tsconnect
Wraps the IPN and tsconnect-redux store in a SharedWorker, exposing every
IPN method to clients via message-port RPC with per-object MessageChannels
for Conn, TCPListener, PacketConn, drive handler, and SSH sessions.

Key design points:
- IndexedDBState: sync-read/write Map cache with async IDB flush, replacing
  the unavailable localStorage in SharedWorkerGlobalScope
- Redux store on the worker broadcasts every dispatched action to all clients;
  new clients receive a full state snapshot as synthetic actions on connect
- Web Locks API tracks client liveness: when the client's tab closes its lock
  is released and the worker closes all resources that client opened
  (Conn, TCPListener, PacketConn, drive handler)
- Graceful IPN shutdown when the last client leaves
- FsaFileOps (OPFS) available for Taildrop when fileOps: true in config
- Two tsconfigs: DOM lib for client code, WebWorker lib for worker.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:30:15 +00:00
codingetandClaude 6b769462a4 docs(readme): update AI disclosure for WebRTC DataChannel transport
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 19:48:57 +00:00
codingetandClaude f0be079706 test(transport/webrtc): achieve 100% branch coverage
Add five tests covering the previously-missed branches:
- read() after clean channel close (no stored error)
- read() after error event re-throws the stored error
- error event with null error field falls back to generic message
- close event while write drain is pending rejects the write
- open-phase error with null error field uses generic message

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 19:45:20 +00:00
codingetandClaude eab3d69723 feat(transport): add WebRTC DataChannel transport
Implements RawTransport over an RTCDataChannel with write-side and
receive-side backpressure. Write-side: send() is always synchronous, but
write() defers its promise until bufferedamountlow fires when
bufferedAmount exceeds sendHighWatermark, using
bufferedAmountLowThreshold for the resume threshold. Receive-side:
removes the message event listener when buffered bytes exceed
receiveHighWatermark, letting SCTP's internal buffer fill and signal
flow control back to the sender; re-adds the listener once read() drains
below receiveLowWatermark.

Factory functions openDataChannel (creates + waits for open) and
acceptDataChannel (wraps an incoming channel) return a transport only
once the channel is fully open. Both accept separate send/receive
watermark options. All WebRTC types are defined as structural interfaces
so the package requires no DOM lib and tests use an in-process mock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 19:21:05 +00:00
codinget 922e14b480 fix(tsconnect-react): clean up useBuildIpn hook 2026-06-17 22:16:42 +00:00
codinget 567ee517a6 fix(tsconnect-redux): ordering protection in exit node and file target updates 2026-06-17 22:14:01 +00:00
codinget c049145481 feat(tsconnect-redux): add exit node suggestion and file targets to the ipn store 2026-06-17 21:52:40 +00:00
codinget f0aebd08cf fix(tsconnect-react): build ipn in effect to avoid react issues 2026-06-17 21:46:00 +00:00
codingetandClaude 332a766814 docs(readme): update AI disclosure for NodeListener bug fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:37:25 +00:00
codingetandClaude 9bfd119b21 fix(transport): fix NodeListener double-callback and connection leak
When close() was called, it fired #errcallback synchronously and also
left the server's "close" event free to fire it a second time.
Null out #errcallback before invoking it so both event-driven paths
are guarded and only one caller receives the rejection.

In accept(), the once("connection") listener was never removed when the
promise was rejected (e.g. via close()), so any late-arriving connection
would create a NodeTransport that was immediately dropped.
Replace the bare once() with a named handler that the error callback
removes before rejecting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:36:41 +00:00
codingetandClaude 2bf551b616 chore: fix lint errors and apply prettier formatting
no-empty, no-useless-assignment, and prefer-const violations introduced
by this branch's earlier test additions, plus stray formatting drift.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:22:57 +00:00
codingetandClaude 8b735cc674 docs(readme): update AI disclosure for taildrive bridge iter()/stream() fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:10:13 +00:00
codingetandClaude 5d7ba382d2 fix(taildrive): update WebDAV bridge to iter()/stream() contract
bridgeDriveHandler still exposed ctx.req.stream() as an AsyncIterable
and had no iter() method, and didn't support ReadableStream response
bodies — both stale from before packages/http's stream()/iter() split.
Add a BYOB-aware byte stream for the request body and a ReadableStream
branch when writing the response body.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:09:51 +00:00
codingetandClaude 6163500b61 test(http): add flush-threshold tests for writeChunkedBodyFromStream
Covers default (non-BYOB) and BYOB byte-stream paths at chunk sizes just
below, at, and just above FLUSH_THRESHOLD (65536). Verifies both the
correctness of the chunked encoding output and that intermediate flushes
to the underlying transport occur at the right boundary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:51:40 +00:00
codingetandClaude 6fb28013b1 feat(http, drive): rename stream() to iter(), add ReadableStream body support
- ReadableHttp.stream() renamed to iter() (returns AsyncIterable)
- ReadableHttp.stream() now returns ReadableStream<Uint8Array> with BYOB
  byte-stream support where the environment supports it
- WritableHttp.body now accepts ReadableStream<Uint8Array>; sent as
  chunked transfer encoding with BYOB reads and periodic 64 KiB flushes
- Removed iterableToStream() and streamToIterable() helpers from
  packages/drive (no longer needed)
- Updated all consumers in packages/drive to use the new APIs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:51:40 +00:00
codingetandClaude 90b8c80843 chore: rebase tailscale submodule onto current origin/webnet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:40:16 +00:00
codingetandClaude 42f8176a41 docs: update AI disclosure for taildrive WebDAV bridge work
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:24:34 +00:00
codingetandClaude 859f048e30 test(tsconnect,taildrive): add unit tests for drive bridge and listDrivePeers
Covers bridgeDriveHandler request/response translation (headers, body
streaming, status, all Body union variants) and IPN.serveDrive /
listDrivePeers argument validation and JSON handling, using fake
RawIPN/JsDriveRequest/JsDriveResponse objects so no WASM build is needed.
Bumps the tailscale submodule pointer to the rebased feat/drive-wasm-bridge
tip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:24:15 +00:00
codingetandClaude 01a49a0784 feat(tsconnect,taildrive): add taildrive WebDAV server bridge and listDrivePeers
Add drive types to @webnet/tsconnect:
- DriveSharePermission, DrivePermissions, JsDriveRequest, JsDriveResponse,
  RawDriveHandler, IPNDrivePeer types in types.ts
- setDriveHandler / listDrivePeers on the raw IPN interface

Add IPN class methods in ipn.ts:
- serveDrive(fn): registers a WebDAV handler for /v0/drive peerapi endpoint
- listDrivePeers(): lists peers with PeerCapabilityTaildriveSharer in their ACL caps

Add new package @webnet/taildrive with ./server sub-export:
- bridgeDriveHandler(handler: Handler): RawDriveHandler adapts an
  @webnet/http Handler to the Go bridge's raw callback interface, streaming
  the request body chunk-by-chunk and piping the response body to write()/end()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:30:48 +00:00
codingetandClaude 000e118a35 docs: update AI disclosure for service advertisement fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:24:01 +00:00
codingetandClaude 563f786969 test(tsconnect): poll for self.services instead of checking the first netmap
The first notifyNetMap after setServices() can be an unrelated update
(other nodes joining) that fires before the service advertisement
propagates, so checking only the first one was flaky. Poll like the
peer.services test already does.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:21:53 +00:00
codingetandClaude 772db900c4 chore: bump tailscale submodule — avoid nil services slice in netmap JSON
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:09:37 +00:00
codingetandClaude ac468bff98 chore: bump tailscale submodule — restart map on setServices
Points to 3a9f6f4 which adds RestartMap() to controlclient.Auto and
calls it from SetExplicitServices, so notifyNetMap fires immediately
after setServices instead of waiting for the next periodic control
plane push.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 01:10:34 +00:00
codingetandClaude afe5109343 test(tsconnect): add integration tests for setServices
Covers three scenarios:
- setServices() resolves without error
- self.services is reflected in the next notifyNetMap after advertising
- A second IPN (watcher) sees the peer's services in its netmap update

Also extends connectIPN() to accept additional IPNRunOptions so callers
can wire notifyNetMap (and other callbacks) at run() time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:36:06 +00:00
codingetandClaude c44845eafd chore: update AI disclosure for service advertisement work
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:29:12 +00:00
codingetandClaude dbe1a3bcb2 fix(tsconnect-redux): stabilise service selector references
getSelfServices returned a new [] literal on every call when self was
null/undefined, causing re-render loops. Replace with a shared constant.

getPeersByService and the new getPeersByServiceDescription are wrapped
with createSelector so the result array is only replaced when the peers
slice or the selector arguments actually change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:25:18 +00:00
codingetandClaude cd18cd0000 chore: bump tailscale submodule to webnet/tailscale#9
Points the submodule at feat/tsconnect-service-advertisement
(dd9c9f68), which adds SetExplicitServices and the setServices
WASM binding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:25:18 +00:00
codingetandClaude 8ebc16fff1 feat(tsconnect): expose service advertisement to JS
Add IPNService type and a services field on every netmap node (self and
peers). Browser WASM nodes can now call ipn.setServices([{proto, port,
description?}]) to advertise TCP/UDP services on the tailnet; the
declaration is distributed to all peers via the control server.

Two convenience selectors added to tsconnect-redux:
  getPeersByService(proto, port) — peers advertising a given service
  getSelfServices()              — services declared by this node

Bumps the tailscale submodule to webnet/tailscale#9.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:24:00 +00:00
codingetandClaude 9820b6645b feat(tsconnect): add getIceServers() to use DERP nodes as WebRTC STUN
DERP servers run an integrated RFC 5389 STUN server (UDP 3478 by
default). Since tsconnect in the browser relays all traffic through
DERP over WebSocket and has no direct UDP path, WebRTC is the only
way to establish a direct peer-to-peer connection. getIceServers()
fetches the tailnet's DERPMap via LocalAPI and converts it to an
RTCIceServer[] list suitable for RTCPeerConnection({ iceServers }).

Unit tests cover parsing, port defaulting, and all filter conditions
(STUNPort < 0, empty HostName, non-200 status) — no WASM required.
Integration test verifies a live IPN returns at least one stun: URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:08:36 +00:00
codingetandClaude a502735fd3 docs(readme): update AI disclosure for WasmSource, Node.js compat, shutdown signal, test suite
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:44:16 +00:00
codingetandClaude 73a74dcb15 test(tsconnect): add unit and integration test suite
Unit tests cover InMemoryFileOps (read/write/stat/list/rename/remove, seek,
misuse guards, constructor seed) and InMemoryState. No WASM needed.

Integration tests spin up three real Tailscale nodes against a headscale
control server, verifying initIPN WASM loading, /localapi/v0/status, and
two-node TCP dial/listen. Credentials loaded from .env.local (gitignored).

All IPN instances share one Go WASM runtime (factory compiled once); shutdown()
on any one exits the runtime, so a single before()/after() pair wraps the suite.

test:coverage script added for consistency with other packages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:44:04 +00:00
codingetandClaude 9dbce83f9f fix(tsconnect): resolve shutdown() race using go.run() as exit signal
go.run() returns a Promise that resolves exactly when the Go runtime exits.
Storing it in initIPN and awaiting it in shutdown() eliminates a race where
Go deletes _inst before a callback-based resolve fires.

Go side: add nil guards on lb and ln so shutdown() is safe even when run()
was never called (e.g. testing IPN construction without connecting).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:43:46 +00:00
codingetandClaude 29d82feaca fix(tsconnect): node.js compat — safesocket unique addr, listen addr normalisation
Two Go-side bugs fixed in the tailscale submodule:
- safesocket_js.go: hardcoded "Tailscale-IPN" memconn addr caused log.Fatal
  on a second newIPN() call; atomic counter now gives each instance a unique name.
- wasm_js.go: listen() rejected ":port" (any-interface form); netstack requires
  an explicit bind addr; now normalises :port → 0.0.0.0:port.

wasm_exec.js ENOSYS stubs (installed when globalThis.fs is falsy) are the correct
behaviour: all network calls go through JS fetch()/WebSocket. Setting globalThis.fs
to Node.js's real fs caused Go to read /etc/resolv.conf and use host nameservers.

tsconfig.json: add test-file exclude; add DOM.AsyncIterable to lib.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:43:33 +00:00
codingetandClaude 7be31df1f0 feat(tsconnect): accept WasmSource — multi-environment WASM loading
initIPN now accepts a WasmSource union instead of a plain URL string:

  - string | URL          → fetch() + instantiateStreaming (browser / CDN)
  - ArrayBuffer | View    → instantiate (Node.js fs.readFile, Bun)
  - Response              → instantiateStreaming (Cloudflare Worker bindings)
  - ReadableStream        → Response-wrapped instantiateStreaming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:43:17 +00:00
codingetandClaude 805df7c6ef docs(readme): update AI disclosure for IPN.shutdown()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 00:23:28 +00:00
codingetandClaude 250b033878 feat(tsconnect): add IPN.shutdown() method
Adds a shutdown() method to the IPN class (TypeScript) and RawIPN
interface (types.ts), backed by the new Go shutdown() on jsIPN in
the tailscale wasm submodule.

Calling shutdown() stops the Tailscale backend, closes all held
resources, and lets the Go runtime exit cleanly — so Node.js processes
can exit without hanging and browser service workers can be paused or
killed for inactivity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 00:23:03 +00:00
codingetandClaude 7c054523cf chore: apply Prettier formatting; update AI disclosure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:23:56 +00:00
codinget b07a236355 fix(example-app): use tsconnect-react and tsconnect-redux instead of the old exports 2026-06-11 22:21:19 +00:00
codingetandClaude a3ef8431f9 chore(submodule): advance tailscale to streaming-taildrop branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 21:13:23 +00:00
codingetandClaude adb140ddf0 feat(taildrop): stream files via ReadableStream, add FsaFileOps
Replace whole-file buffering on both send and receive paths with Web
Streams API so arbitrarily large files can be transferred without being
bounded by available RAM.

Send: IPN.sendFile now accepts a ReadableStream<Uint8Array> + declaredSize
instead of a Uint8Array.  The WASM layer pulls chunks from the stream via
ReadableStreamDefaultReader.read() Promises, feeding them directly to the
HTTP PUT body — no js.CopyBytesToGo of the full file.

Receive: IPN.openWaitingFile now returns Promise<ReadableStream<Uint8Array>>.
The WASM layer wraps the Go io.ReadCloser in a pull-based ReadableStream,
enqueuing 64 KiB chunks on demand — no io.ReadAll.

FileOps: UserIPNFileOps.openReader now returns ReadableStream<Uint8Array>
instead of Uint8Array.  The JS→Go bridge wraps the stream in a new
jsStreamReader (io.ReadCloser) that blocks on a channel per chunk, matching
the existing channel+FuncOf pattern used throughout taildrop.go.

Add FsaFileOps in packages/tsconnect/src/helpers.ts: a UserIPNFileOps
backed by the File System Access API (any FileSystemDirectoryHandle), with
FsaFileOps.createFromOpfs(subdir?) as the OPFS factory.  Received chunks
land directly in OPFS via FileSystemWritableFileStream with no intermediate
buffer.  openReader returns file.stream(), so downloads are also zero-copy
through Go.

InMemoryFileOps.openReader is updated to return a ReadableStream that emits
stored chunks one by one (no concatenation).

The example app is updated to use file.stream()/file.size for send and
showSaveFilePicker (with Response.blob() fallback) for receive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 21:12:09 +00:00
codingetandClaude 959b0fa558 docs(readme): fix OpfsVFS reference — folded into FsaVFS as a factory method
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 02:15:03 +02:00
codingetandClaude 1e582b06ec docs(readme): update package list and overview for monorepo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 02:15:03 +02:00
codingetandClaude c506bd836a docs(readme): update AI disclosure for AGENTS.md changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:44:15 +00:00
codingetandClaude 585ccd52c0 docs(agents): add PR finalisation checklist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:41:48 +00:00