@statewalker/webrun-http-browser 0.3.3 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +406 -51
  2. package/demo/demo-1.html +109 -194
  3. package/demo/demo-2.html +118 -299
  4. package/dist/core/data-calls.d.ts +5 -0
  5. package/dist/core/data-calls.d.ts.map +1 -0
  6. package/dist/core/data-channels.d.ts +17 -0
  7. package/dist/core/data-channels.d.ts.map +1 -0
  8. package/dist/core/index.d.ts +6 -0
  9. package/dist/core/index.d.ts.map +1 -0
  10. package/dist/core/message-target.d.ts +2 -0
  11. package/dist/core/message-target.d.ts.map +1 -0
  12. package/dist/core/registry.d.ts +18 -0
  13. package/dist/core/registry.d.ts.map +1 -0
  14. package/dist/http/http-send-recieve.d.ts +36 -0
  15. package/dist/http/http-send-recieve.d.ts.map +1 -0
  16. package/dist/http/index.d.ts +4 -0
  17. package/dist/http/index.d.ts.map +1 -0
  18. package/dist/index.d.ts +4 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +2670 -807
  21. package/dist/relay/index-sw.d.ts +7 -0
  22. package/dist/relay/index-sw.d.ts.map +1 -0
  23. package/dist/relay/index.d.ts +69 -0
  24. package/dist/relay/index.d.ts.map +1 -0
  25. package/dist/relay/split-service-url.d.ts +13 -0
  26. package/dist/relay/split-service-url.d.ts.map +1 -0
  27. package/dist/relay-sw.d.ts +2 -0
  28. package/dist/relay-sw.d.ts.map +1 -0
  29. package/dist/relay-sw.js +1660 -0
  30. package/dist/sw/http-sw-dispatcher.d.ts +28 -0
  31. package/dist/sw/http-sw-dispatcher.d.ts.map +1 -0
  32. package/dist/sw/index.d.ts +3 -0
  33. package/dist/sw/index.d.ts.map +1 -0
  34. package/dist/sw/sw-dispatcher.d.ts +74 -0
  35. package/dist/sw/sw-dispatcher.d.ts.map +1 -0
  36. package/dist/sw-worker.d.ts +2 -0
  37. package/dist/sw-worker.d.ts.map +1 -0
  38. package/dist/sw-worker.js +1597 -0
  39. package/dist/sw.d.ts +2 -0
  40. package/dist/sw.d.ts.map +1 -0
  41. package/dist/sw.js +1883 -0
  42. package/package.json +51 -37
  43. package/public/index.html +61 -97
  44. package/public/index.js +7 -4
  45. package/public/sw-worker.js +4 -0
  46. package/public-relay/heartbeat.js +4 -3
  47. package/public-relay/relay-sw.js +4 -6
  48. package/public-relay/relay.html +16 -11
  49. package/src/core/data-calls.ts +46 -0
  50. package/src/core/data-channels.ts +262 -0
  51. package/src/core/index.ts +9 -0
  52. package/src/core/message-target.ts +8 -0
  53. package/src/core/registry.ts +54 -0
  54. package/src/http/http-send-recieve.ts +94 -0
  55. package/src/http/index.ts +7 -0
  56. package/src/index.ts +3 -0
  57. package/src/relay/index-sw.ts +145 -0
  58. package/src/relay/index.ts +261 -0
  59. package/src/relay/split-service-url.ts +30 -0
  60. package/src/relay-sw.ts +7 -0
  61. package/src/sw/http-sw-dispatcher.ts +122 -0
  62. package/src/sw/index.ts +2 -0
  63. package/src/sw/sw-dispatcher.ts +332 -0
  64. package/src/sw-worker.ts +7 -0
  65. package/src/sw.ts +1 -0
  66. package/CHANGELOG.md +0 -1
  67. package/LICENSE +0 -21
  68. package/demo/demo-2 copy.html +0 -598
  69. package/demo/demo-3.html +0 -167
  70. package/dist/index-sw-umd.js +0 -798
  71. package/dist/index-sw-umd.min.js +0 -2
  72. package/dist/index-sw.js +0 -791
  73. package/dist/index-sw.min.js +0 -2
  74. package/dist/index-umd.js +0 -853
  75. package/dist/index-umd.min.js +0 -2
  76. package/dist/index.min.js +0 -2
  77. package/index.js +0 -1
  78. package/src/core/data-calls.js +0 -36
  79. package/src/core/data-channels.js +0 -192
  80. package/src/core/data-send-recieve.js +0 -28
  81. package/src/core/errors.js +0 -13
  82. package/src/core/index.js +0 -4
  83. package/src/http/HttpError.js +0 -59
  84. package/src/http/http-send-recieve.js +0 -40
  85. package/src/http/http-stubs.js +0 -135
  86. package/src/http/index.js +0 -3
  87. package/src/http/readable-streams.js +0 -33
  88. package/src/index.js +0 -7
  89. package/src/relay/index-sw.js +0 -216
  90. package/src/relay/index.js +0 -263
  91. package/src/relay/splitServiceUrl.js +0 -24
  92. package/src/sw/http-sw-dispatcher.js +0 -128
  93. package/src/sw/sw-dispatcher.js +0 -264
package/README.md CHANGED
@@ -1,73 +1,428 @@
1
1
  # @statewalker/webrun-http-browser
2
2
 
3
- This module simulates HTTP server using Service Workers.
4
- It allows to develop, test, run and debug server-side code directly in the browser.
5
- After that the same code can be deployed in Deno / Deno Deploy / Cloudflare / Node JS environments (with adapters).
3
+ ServiceWorker-based HTTP server for browsers. You write ordinary
4
+ `(Request) ⇒ Response` handlers in JavaScript; a ServiceWorker intercepts
5
+ same-origin `fetch()` calls and routes them to your handlers — no network
6
+ round-trip, no external server, no bundler tricks required.
6
7
 
7
- ## Demo
8
+ Two modes, picked by how the SW is hosted:
8
9
 
9
- * [https://observablehq.com/@kotelnikov/webrun-http-service](https://observablehq.com/@kotelnikov/webrun-http-service) - an Observable page demonstrating how it works. You can play with the code here.
10
- * [Demo 1](./demo/demo-1.html) - a dynamic web site with a server-side API, a HTML page and a CSS file
11
- * [Demo 2](./demo/demo-2.html) - a virtual file server exposing your local disk content
10
+ - **Same-origin** (`@statewalker/webrun-http-browser/sw`) — your app
11
+ registers its own SW and mounts handlers next to the page.
12
+ - **Relay** (default entry) — a SW running at a shared relay origin
13
+ (CDN / unpkg / your own host) serves requests for any page that embeds
14
+ a hidden relay iframe. Cross-origin friendly.
12
15
 
13
- ## Features
16
+ ## Why it exists
14
17
 
15
- This module provides a lightweight full-stack development environment in the browser:
16
- * Code, execute and debug the whole stack in the browser. Even without internet connection.
17
- * Your data and code don’t leave your browser
18
- * Instant reproducable environment without installation
19
- * Easily embeddable in your code
20
- * Client and server-side code in the browser based on standards:
21
- - Use module imports for scripts
22
- - Send requests with fetch
23
- - Handle HTTP queries with the Request/Response API
18
+ The browser already has everything needed to be an HTTP server: `Request`,
19
+ `Response`, `ReadableStream`, `ServiceWorker`. Two things are missing from
20
+ the raw platform APIs, and this package fills them:
24
21
 
22
+ 1. **Plumbing for same-origin SW dispatch.** Browsers let a SW intercept
23
+ `fetch` events, but you still have to build URL routing, MessageChannel
24
+ wiring between the page and the SW, and recovery after SW restarts.
25
+ 2. **A way to use a SW from a page that isn't on the SW's origin.** The
26
+ relay mode lets *any* page (Observable, notebooks, a `file://` demo,
27
+ unpkg, a third-party host) share a SW hosted somewhere else. The page
28
+ never registers a SW of its own — it just embeds a hidden iframe.
25
29
 
26
- ## UseCases
30
+ Combining both modes means the same handler code works in an app you
31
+ control *and* in an embed you don't.
27
32
 
28
- * Create rich documentations, tutorials, demos
29
- * Embed in your rich application - in the new generation Notion, Airtable or Figma
30
- * Deliver self-contained prototype environments to your clients
33
+ ## Install
31
34
 
32
- ## How It Works
35
+ ```sh
36
+ npm install @statewalker/webrun-http-browser
37
+ ```
38
+
39
+ Browser-only — it needs `navigator.serviceWorker`, so a secure context
40
+ (`https://` or `localhost`) is required. No peer dependencies.
41
+
42
+ ## How to use
43
+
44
+ ```sh
45
+ npm install @statewalker/webrun-http-browser
46
+ ```
47
+
48
+ | Subpath | Purpose |
49
+ | --- | --- |
50
+ | `@statewalker/webrun-http-browser` | Page-side relay API: `newRemoteRelayChannel`, `initHttpService`, `callHttpService`, `splitServiceUrl`, `initServiceWorker`, `newServiceWorkerPort`, `getRelayWindowMessageHandler`; the MessagePort call primitives (`callChannel`, `handleChannelCalls`, `newInvokationChannel`, `sendStream`, `handleStreams`, `newRegistry`); plus everything re-exported from `@statewalker/webrun-http-streams` (`HttpError`, the client/server stubs), `@statewalker/webrun-streams` (stream and error helpers) and the `MessageTarget` family from `@statewalker/webrun-rpc` |
51
+ | `@statewalker/webrun-http-browser/sw` | Same-origin adapter classes: `SwHttpAdapter` (page), `SwHttpDispatcher` (SW), `startHttpDispatcher` bootstrap |
52
+ | `@statewalker/webrun-http-browser/relay-sw` | IIFE bundle of the relay SW runtime — load via `importScripts` from a loader script in your relay origin |
53
+ | `@statewalker/webrun-http-browser/sw-worker` | IIFE bundle of the same-origin SW runtime — ditto, for same-origin apps |
54
+
55
+ ## Examples
56
+
57
+ > Every example below needs a real browser: a ServiceWorker, and for relay
58
+ > mode an iframe on the relay origin. None of them run under Node, and
59
+ > ServiceWorkers only register over `http://localhost` or HTTPS. The runnable
60
+ > versions are in [`public/`](./public) and [`demo/`](./demo) — see
61
+ > [Running the bundled examples](#running-the-bundled-examples).
62
+
63
+ ### Relay mode — cross-origin
64
+
65
+ Your page ↔ hidden relay iframe ↔ relay ServiceWorker. The relay SW claims
66
+ URLs shaped `<relay-origin>/~<service-key>/…` and forwards each request to
67
+ whichever page registered that `key`.
68
+
69
+ ```ts
70
+ import {
71
+ newRemoteRelayChannel,
72
+ initHttpService,
73
+ callHttpService,
74
+ } from "@statewalker/webrun-http-browser";
75
+
76
+ // 1. Embed the relay iframe and open a MessagePort into its SW.
77
+ const connection = await newRemoteRelayChannel({
78
+ url: new URL("https://my-relay.example/public-relay/relay.html"),
79
+ });
80
+
81
+ // 2. Register a handler for service "FS".
82
+ const baseUrl = `${connection.baseUrl}~FS`;
83
+ await initHttpService(
84
+ async (request) =>
85
+ new Response(`Hello ${new URL(request.url).pathname}`),
86
+ { key: "FS", port: connection.port },
87
+ );
88
+
89
+ // 3a. Any browser tab loading the service URL now hits your handler:
90
+ await fetch(`${baseUrl}/anything`);
91
+
92
+ // 3b. …or call it directly through the same port, bypassing `fetch`
93
+ // (useful when the caller isn't on the relay origin):
94
+ const res = await callHttpService(
95
+ new Request(`${baseUrl}/anything`),
96
+ { key: "FS", port: connection.port },
97
+ );
98
+ ```
33
99
 
34
- The core of this module is based on the following native browser technologies: ServiceWorker and MessageChannels.
100
+ [`demo/demo-1.html`](./demo/demo-1.html) wires this to a Hono router
101
+ serving a mini site; [`demo/demo-2.html`](./demo/demo-2.html) pipes a
102
+ local-disk folder (File System Access API) through it.
35
103
 
36
- A ServiceWorker is used as the "server", intercepting HTTP calls and delegating their handling to registered modules via MessageChannels.
37
- So in the same browser-based application you can register a standard HTTP endpoint and call it.
104
+ ### Same-origin mode
105
+
106
+ Your page registers its own SW, handlers are local to the page:
107
+
108
+ ```ts
109
+ import { SwHttpAdapter } from "@statewalker/webrun-http-browser/sw";
110
+
111
+ const KEY = "demo"; // also the first URL segment the SW routes here
112
+ const adapter = new SwHttpAdapter({
113
+ key: KEY,
114
+ serviceWorkerUrl: new URL("./sw-worker.js", import.meta.url).toString(),
115
+ });
116
+ await adapter.start();
117
+
118
+ const { baseUrl } = await adapter.register(`${KEY}/api/`, async (request) => {
119
+ return new Response(JSON.stringify({ now: Date.now() }), {
120
+ headers: { "Content-Type": "application/json" },
121
+ });
122
+ });
123
+
124
+ // fetch(`${baseUrl}anything`) is intercepted by the SW.
125
+ ```
126
+
127
+ The SW script itself ships as a pre-built IIFE bundle. Put a tiny loader
128
+ next to your app pages so the SW's default scope covers them:
38
129
 
39
- Example:
40
130
  ```js
41
- import { httpService, endpointUrl } from "...";
131
+ // public/sw-worker.js — served next to your app pages.
132
+ importScripts(
133
+ "/path/to/node_modules/@statewalker/webrun-http-browser/dist/sw-worker.js",
134
+ );
135
+ ```
136
+
137
+ The working example lives in [`public/`](./public).
138
+
139
+ ### Running the bundled examples
140
+
141
+ ```sh
142
+ pnpm run example:same-origin # public/index.html — same-origin SW demo
143
+ pnpm run example:relay-site # demo/demo-1.html — relay + Hono dynamic site
144
+ pnpm run example:relay-files # demo/demo-2.html — relay + local-disk file server
145
+ pnpm run serve # just a static server on :5173 (no auto-open)
146
+ ```
147
+
148
+ Each `example:*` script builds first, starts a static server on `:5173`,
149
+ then opens the target page in the default browser. ServiceWorkers only
150
+ register over `http://localhost` or HTTPS, so always visit through
151
+ `http://localhost:5173/…` — `file://` won't work.
152
+
153
+ #### [`public/index.html`](./public/index.html) — minimal same-origin SW
154
+
155
+ The smallest possible in-browser HTTP server. The page registers
156
+ `public/sw-worker.js` (which `importScripts`es the shipped
157
+ `dist/sw-worker.js`), constructs a `SwHttpAdapter` with key `"demo"`,
158
+ and registers a single handler at `demo/api/` that returns JSON. The
159
+ page then makes a standard `fetch(baseUrl + "anything")` and logs the
160
+ result.
161
+
162
+ Why it's interesting:
42
163
 
43
- // Server-side code:
44
- httpService.register(async (request) => { // request: Request
45
- return new Response("Hello, world!", {
46
- headers: {
47
- "Content-Type": "text/plain"
48
- }
49
- })
50
- })
164
+ - **No framework, no glue, ~40 lines of inline JS.** This is the
165
+ unwrapped pattern — everything
166
+ [`@statewalker/webrun-site-host`](../webrun-site-host) and
167
+ [`@statewalker/webrun-site-builder`](../webrun-site-builder) build on
168
+ top of. Useful as a reference for exactly what the SW lifecycle
169
+ looks like at its lowest level.
170
+ - **Shows the SW-routing contract.** The adapter's `key: "demo"` is
171
+ the first URL segment the SW uses to find this page's registration;
172
+ `adapter.register(\`${KEY}/api/\`, ...)` mounts the handler prefix
173
+ under the same key. The mapping is visible and inspectable — great
174
+ for debugging your own SW-based code.
51
175
 
52
- // Client-side code:
176
+ #### [`demo/demo-1.html`](./demo/demo-1.html) — relay + Hono dynamic site
53
177
 
54
- const res = await fetch(endpointUrl);
55
- const text = await res.text();
56
- console.log(text);
178
+ A full-blown mini web site running in a single tab, behind the
179
+ **relay** ServiceWorker. The page spins up a Hono router with a
180
+ `/api/:name` endpoint and a static-file catch-all, registers it as
181
+ service `MY_SITE`, and embeds the service root in an iframe. Inside
182
+ the iframe, typing into an input fires `fetch("./api/" + name)` and
183
+ renders the JSON response — the whole back-end is the Hono app
184
+ running in the outer tab.
57
185
 
186
+ Why it's interesting:
187
+
188
+ - **An entire web framework running client-side.** Hono is a normal
189
+ Node/Deno/CF-Workers framework — here it's loaded from esm.sh and
190
+ mounted inside the browser with no server involvement. The
191
+ `(Request) ⇒ Response` contract makes this transparent.
192
+ - **Relay mode = cross-origin friendly.** Because the SW lives at the
193
+ relay origin (not the page's origin), this pattern also works when
194
+ your page is served from Observable, unpkg, a notebook, or a static
195
+ `file://` — places where registering your own SW isn't possible.
196
+ The hidden relay iframe does the SW registration on your behalf.
197
+ - **Two ways to call the service.** The iframe uses plain `fetch()`
198
+ through the SW; any other browser tab pointing at
199
+ `<relay-origin>/~MY_SITE/...` is also routed to this tab's Hono
200
+ app. Demonstrates that the page hosting the handler and the caller
201
+ don't have to share an origin.
202
+
203
+ #### [`demo/demo-2.html`](./demo/demo-2.html) — FS Access API folder as a site
204
+
205
+ Click **Open folder**, grant read access, and any directory on your
206
+ local disk is exposed as an in-browser HTTP site under
207
+ `<relay-origin>/~FS/…`. The left panel shows a live file tree; clicking
208
+ a file loads it in the iframe preview. The service handler is a ~20-line
209
+ function that resolves paths via
210
+ [`FileSystemDirectoryHandle.getFileHandle`](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle/getFileHandle)
211
+ and streams the file's bytes back through the SW.
212
+
213
+ Why it's interesting:
214
+
215
+ - **Zero installs, real files.** Browse arbitrary directories as if
216
+ they were hosted — open a local project's `index.html` and it just
217
+ runs. Relative URLs inside the hosted files resolve correctly because
218
+ the SW serves every asset, CSS, and JS under the same origin.
219
+ - **Permissioned + sandboxed.** The browser's File System Access API
220
+ provides the "backend" (read permission granted per-folder by the
221
+ user); the relay SW provides the "network". You get the ergonomics
222
+ of a local HTTP dev server without running one.
223
+ - **Directory picker + request router in <100 lines.** No build step,
224
+ no tooling. Shows how small the glue between a platform API and a
225
+ `(Request) ⇒ Response` handler can be.
226
+
227
+ ## Exports
228
+
229
+ The package root re-exports everything from
230
+ [`@statewalker/webrun-streams`](../webrun-streams) and
231
+ [`@statewalker/webrun-http-streams`](../webrun-http-streams), so existing
232
+ imports keep working after those extractions. Its own surface is below.
233
+
234
+ ### Relay mode
235
+
236
+ | Export | Kind | Purpose |
237
+ | --- | --- | --- |
238
+ | `newRemoteRelayChannel(opts?)` | function | Embeds the hidden relay iframe, handshakes a `MessageChannel`, resolves a `RemoteRelayChannel`. |
239
+ | `RemoteRelayChannelOptions` | interface | `baseUrl`, `url`, `container` — where the relay lives and what to append the iframe to. |
240
+ | `RemoteRelayChannel` | interface | `{ baseUrl, port, close() }`. |
241
+ | `initHttpService(handler, opts)` | function | Registers `handler` as the server for a service `key` on the relay. Returns a cleanup. |
242
+ | `callHttpService(request, opts)` | function | Sends a `Request` to the service under `key`; resolves its `Response`. |
243
+ | `ServiceOptions` | interface | `{ key: string; port: MessageTarget }` — shared by the two above. |
244
+ | `getRelayWindowMessageHandler(opts?)` | function | The `window.onmessage` handler that runs *inside* the relay iframe. |
245
+ | `RelayWindowHandlerOptions` | interface | `swUrl`, `scopeUrl` for that handler. |
246
+ | `splitServiceUrl(url, separator?)` | function | Splits a relay URL into service key + remaining path (default separator `~`). |
247
+ | `SplitServiceUrl` | interface | Its result shape. |
248
+
249
+ ### ServiceWorker lifecycle
250
+
251
+ | Export | Kind | Purpose |
252
+ | --- | --- | --- |
253
+ | `initServiceWorker(opts)` | function | Registers a SW and resolves once it is activated **and controlling the page**. |
254
+ | `InitServiceWorkerOptions` | interface | `{ swUrl, scopeUrl?, type? }`. |
255
+ | `newServiceWorkerPort()` | function | A `MessagePort` that transparently bridges to the controlling SW. |
256
+
257
+ ### Connection registry
258
+
259
+ | Export | Kind | Purpose |
260
+ | --- | --- | --- |
261
+ | `initializeConnection(opts)` | function | Sends `CONNECT` for a service `key`; resolves a `MessagePort`, or `null` if no such service. |
262
+ | `InitializeConnectionOptions` | interface | `{ key, communicationPort, ...extra }` — extra fields ride along in the CONNECT payload. |
263
+ | `registerConnectionsHandler(opts)` | function | Registers a `key` and answers inbound `CONNECT`s. Returns a cleanup that unregisters. |
264
+ | `RegisterConnectionsHandlerOptions` | interface | `{ key, handler, communicationPort }`. |
265
+
266
+ ### Messaging primitives
267
+
268
+ | Export | Kind | Purpose |
269
+ | --- | --- | --- |
270
+ | `callChannel(target, type, data, port?)` | function | One typed request/response over a `MessageTarget`. |
271
+ | `handleChannelCalls(target, type, handler)` | function | Answer those calls. Returns an unsubscribe. |
272
+ | `ChannelCallHandler` | type | The handler signature the two above exchange. |
273
+ | `newInvokationChannel(opts)` | function | Multiplexed invocations over one target. |
274
+ | `InvocationChannel` / `NewInvocationChannelOptions` | interface | Its result and options. |
275
+ | `handleStreams(...)` / `StreamHandler<T>` | function / type | Stream-shaped invocations over the same channel. |
276
+
277
+ > **These stream primitives have no backpressure.** `sendStream`'s chunk sender
278
+ > discards the promise it is handed, so a fast producer over a slow consumer
279
+ > accumulates without bound; there is also no per-stream timeout and no chunking
280
+ > to a transport's message ceiling. `@statewalker/webrun-rpc`'s `duplexOverPort`
281
+ > is the replacement — one `Duplex` over one port, with the confirmation
282
+ > withheld until the consumer has pulled. Migrating this package onto it is
283
+ > planned, not done.
284
+ | `MessageTarget` / `MessageSource` / `MessageSink` / `MessageListener` | interface / type | The structural port view everything above accepts — a `MessagePort`, a `Worker`, or a SW bridge. Defined in [`@statewalker/webrun-streams`](../webrun-streams) and re-exported here. |
285
+ | `newRegistry(onError?)` | function | Small cleanup registry used for teardown. |
286
+ | `Registry` / `NewRegistryResult` / `CleanupAction` | interface / type | Its shapes. |
287
+
288
+ ### HTTP over a port
289
+
290
+ | Export | Kind | Purpose |
291
+ | --- | --- | --- |
292
+ | `sendHttpRequest(port, request)` | function | **Deprecated.** Ship a `Request` over a `MessageTarget`, await the `Response`. |
293
+ | `handleHttpRequests(port, handler)` | function | **Deprecated.** Serve an `HttpHandler` on the other end of one. |
294
+
295
+ ### Subpath entry points
296
+
297
+ | Entry | Purpose |
298
+ | --- | --- |
299
+ | `@statewalker/webrun-http-browser/sw` | `SwHttpAdapter` — the same-origin ServiceWorker adapter. |
300
+ | `@statewalker/webrun-http-browser/relay-sw` | IIFE relay SW runtime, loadable via `importScripts(...)`. |
301
+ | `@statewalker/webrun-http-browser/sw-worker` | IIFE same-origin SW runtime, loadable via `importScripts(...)`. |
302
+
303
+ ## Internals
304
+
305
+ ### Source layout
306
+
307
+ ```
308
+ src/
309
+ ├── core/ ┐
310
+ │ ├── data-calls.ts │ Transport primitives over a
311
+ │ ├── data-channels.ts │ `MessageTarget`: one-shot
312
+ │ ├── message-target.ts │ `callChannel` / `handleChannelCalls`,
313
+ │ └── registry.ts │ the request/response
314
+ │ │ `newInvokationChannel`, streaming
315
+ │ │ `sendStream` / `handleStreams` with
316
+ │ │ backpressure, and `newRegistry`.
317
+ │ │ Also re-exports
318
+ │ │ `@statewalker/webrun-streams`.
319
+ │ ┘
320
+ ├── http/ ┐
321
+ │ ├── http-send-recieve.ts │ Browser-specific HTTP transport:
322
+ │ │ │ `handleHttpRequests` /
323
+ │ │ │ `sendHttpRequest` over `MessageTarget`s,
324
+ │ │ │ built on the client/server stubs.
325
+ │ └── index.ts │ Re-exports
326
+ │ ┘ `@statewalker/webrun-http-streams`.
327
+ ├── sw/ ┐
328
+ │ ├── sw-dispatcher.ts │ Same-origin mode:
329
+ │ │ │ `SwPortHandler` (page) /
330
+ │ │ │ `SwPortDispatcher` (SW side,
331
+ │ │ │ IndexedDB-persisted client index).
332
+ │ ├── http-sw-dispatcher.ts │ `SwHttpAdapter` /
333
+ │ │ │ `SwHttpDispatcher` /
334
+ │ └── index.ts │ `startHttpDispatcher`.
335
+ │ ┘
336
+ ├── relay/ ┐
337
+ │ ├── index.ts │ Relay mode page-side:
338
+ │ │ │ `newRemoteRelayChannel`,
339
+ │ │ │ `initHttpService`,
340
+ │ │ │ `callHttpService`,
341
+ │ │ │ `getRelayWindowMessageHandler`.
342
+ │ ├── index-sw.ts │ `startRelayServiceWorker` — the SW
343
+ │ │ │ side (registry keyed by service key).
344
+ │ └── split-service-url.ts │ `<base>/~<key>/<path>` parser.
345
+ │ ┘
346
+ ├── index.ts — public entry: core + http + relay.
347
+ ├── sw.ts — `./sw` subpath entry.
348
+ ├── relay-sw.ts — relay SW bootstrap (IIFE target).
349
+ └── sw-worker.ts — same-origin SW bootstrap (IIFE target).
58
350
  ```
59
351
 
60
- ## They Play Well Together...
352
+ ### Design notes
353
+
354
+ - **Two SW strategies**. Same-origin mode needs the SW to be served next
355
+ to the app (scope-rooted loader); relay mode puts the SW anywhere and
356
+ ferries messages through an iframe, at the cost of a `CONNECT`
357
+ round-trip per call. Pick the stricter mode when you own the origin.
358
+ - **Adapter key = URL segment**. For the same-origin path, the adapter's
359
+ `key` option **must match** the first URL segment the SW routes to it:
360
+ if `key: "demo"` and the SW scope is `/public/`, handlers answer at
361
+ `/public/demo/…`. The SW extracts the segment from the URL and looks up
362
+ `handlersIndex` by key. This is why
363
+ `adapter.register(\`${KEY}/api/\`, …)` prefixes the registration path
364
+ with the same key.
365
+ - **IIFE for SW bundles**. The SW runtime bundles (`relay-sw.js`,
366
+ `sw-worker.js`) are IIFE rather than ESM so a classic
367
+ `importScripts(...)` loader script can pull them in. Registering as
368
+ `{ type: "module" }` SWs would work but is subject to
369
+ `Service-Worker-Allowed` header games for a scope wider than the
370
+ bundle's directory.
371
+ - **ESM page-side bundles are self-contained**. `dist/index.js` and
372
+ `dist/sw.js` inline their dependencies (`idb-keyval`,
373
+ `@statewalker/webrun-http-streams`, `@statewalker/webrun-streams`) so a
374
+ page can load them straight from a static host without a bundler or
375
+ import map.
376
+ - **Streaming uses `newAsyncGenerator`** (from `@statewalker/webrun-streams`,
377
+ via `recieveIterator`). The queue-based async generator gives explicit
378
+ backpressure — each `next(value)` returns a `Promise<boolean>` that resolves
379
+ once the consumer has dequeued — and drains in-flight producers on consumer
380
+ exit.
381
+ - **SW client registry is IndexedDB-persisted**. Both `SwPortDispatcher`
382
+ (same-origin) and `relay/index-sw.ts` keep their client-lookup tables in
383
+ IndexedDB so a SW wake-up after idle doesn't lose its bindings.
384
+
385
+ ### Constraints
386
+
387
+ - **Request bodies are buffered on Firefox.** The stubs this package uses
388
+ (`newHttpClientStub` / `newHttpServerStub` from
389
+ [`@statewalker/webrun-http-streams`](../webrun-http-streams)) stream request
390
+ bodies wherever the runtime implements `Request.prototype.body`. Firefox
391
+ does not (checked against 146), so on that browser the whole request body is
392
+ buffered into memory on both the sending and the receiving side — a large
393
+ upload is a proportionally large allocation, and a handler that wanted to
394
+ stream its request body cannot. Response streaming is unaffected on every
395
+ browser. See that package's README for the details and for the Safari
396
+ caveat.
397
+ - **ServiceWorker scope rules apply.** A SW registered at `/public/sw-worker.js`
398
+ only controls pages and fetches under `/public/`. If you need a broader
399
+ scope, the SW script must be served with the
400
+ `Service-Worker-Allowed` HTTP header, *or* live higher in the origin.
401
+ - **`http://localhost` or HTTPS only.** Browsers refuse to register SWs
402
+ on other `http://` origins.
403
+ - **Relay mode needs an iframe-capable sandbox.** Pages with strict CSP
404
+ that blocks `frame-src` to the relay origin can't use the relay path.
405
+ - **Consumer-side `fetch()` only works from pages under the SW's scope.**
406
+ When your caller is on another origin, use `callHttpService(request,
407
+ …)` — it reaches the SW through the iframe's MessagePort and bypasses
408
+ the browser's fetch routing.
409
+
410
+ ### Dependencies
411
+
412
+ Runtime:
413
+
414
+ - `@statewalker/webrun-http-streams` — the `newHttpClientStub` /
415
+ `newHttpServerStub` pair this package's MessagePort transport is built
416
+ on, plus `HttpError`. Workspace-local.
417
+ - `@statewalker/webrun-streams` — iterator/stream primitives and
418
+ serialisable errors. Workspace-local.
419
+ - `idb-keyval` — tiny (<1 KB) IndexedDB KV used by both SW modes to keep
420
+ client/service registrations across SW restarts.
421
+
422
+ Dev: TypeScript, vitest, rolldown, rimraf, `http-server` (for the
423
+ `example:*` scripts), `@types/node` (catalog versions from the monorepo
424
+ root).
61
425
 
62
- This in-browser HTTP Server allows to implement the following functionalities:
63
- - Serve your content from your Local Disk:
64
- - using [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API)
65
- - using [Origin Private File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API)
66
- - Add version control with Git - using [IsomorphicGit](https://isomorphic-git.org/)
67
- - Provide direct P2P sharing with others – via [WebRTC](https://webrtc.org/)
68
- - Implement client/server applications using persistent SQLite on Origin Private File System - using [SQLite Wasm](https://developer.chrome.com/blog/sqlite-wasm-in-the-browser-backed-by-the-origin-private-file-system/)
69
- - Deploy your local site on Edge - via [Deno Deploy](https://deno.com/deploy)
70
- - Distribute your work in any browser via IPFS / [LibP2P](https://github.com/libp2p/js-libp2p)
71
- - Integration with existing powerful APIs like https://trpc.io/
426
+ ## License
72
427
 
73
- ...and everything in the browser!
428
+ MIT © statewalker — see [LICENSE](../../LICENSE).