@openthink/ui-leaf 0.5.0 → 0.6.1
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.
- package/README.md +33 -9
- package/package.json +1 -1
- package/packages/cli/dist/cli.js +309 -128
- package/packages/cli/dist/cli.js.map +1 -1
- package/packages/cli/dist/index.d.ts +28 -19
- package/packages/cli/dist/index.js +300 -125
- package/packages/cli/dist/index.js.map +1 -1
- package/packages/cli/dist/{server-Bp6cms3O.d.ts → server-3vbR-tuu.d.ts} +1 -1
- package/packages/cli/dist/view.d.ts +1 -1
package/README.md
CHANGED
|
@@ -78,7 +78,11 @@ Three reasons:
|
|
|
78
78
|
|
|
79
79
|
## How it works
|
|
80
80
|
|
|
81
|
-
`mount()` compiles your view file once with `Bun.build`, injects data into `window.__UI_LEAF__`, starts a `Bun.serve` HTTP server, and opens the user's default browser. Mutations from the view POST back to a localhost endpoint with a per-launch random token; the runtime dispatches them to the handlers you registered.
|
|
81
|
+
`mount()` compiles your view file once with `Bun.build`, injects data into `window.__UI_LEAF__`, starts a `Bun.serve` HTTP server, and opens the user's default browser. Mutations from the view POST back to a localhost endpoint with a per-launch random token; the runtime dispatches them to the handlers you registered.
|
|
82
|
+
|
|
83
|
+
**Heartbeat and lifecycle.** The browser tab sends periodic heartbeats while the page is open. When heartbeats stop (tab closed or navigated away), `mount()` emits a `disconnected` event but the server stays running — the port, token, and in-memory data are all preserved. When a tab reconnects (heartbeat resumes), a `reconnected` event fires. Any data updates or view swaps that arrive during the disconnected window take effect on the next load. The mount terminates only on an explicit caller close (`view.close()` or the inbound `{type:"close"}` message), a signal (SIGINT/SIGTERM), or an internal error — and `view.closed` resolves to the reason (`"caller"`, `"signal"`, or `"error"`).
|
|
84
|
+
|
|
85
|
+
Multi-tab note: `disconnected` fires only when **all** open tabs go silent — the heartbeat is a single high-water mark across tabs, so closing one tab while another is open emits no event.
|
|
82
86
|
|
|
83
87
|
The transport is HTTP + JSON over loopback. The token is in `window.__UI_LEAF__.token`, served inline in the compiled HTML — so the token only protects against drive-by cross-origin requests in the user's browser, not against other processes on the same machine. Any local process that can reach `127.0.0.1:<port>` can fetch `GET /`, grep the token out, and call `/mutate` or `/api/data` with it; treat any local process you don't trust as having the same access as the view. View bundling resolves React from `ui-leaf`'s installed location, so your project doesn't need to install React.
|
|
84
88
|
|
|
@@ -237,12 +241,22 @@ What the consumer CLI is responsible for (out of ui-leaf's scope):
|
|
|
237
241
|
{"type":"result","id":1,"value":{"ok":true}}
|
|
238
242
|
{"type":"error","id":2,"message":"…"}
|
|
239
243
|
```
|
|
244
|
+
- **Or live-update / control messages:**
|
|
245
|
+
```json
|
|
246
|
+
{"version":"1","type":"update","data":{}}
|
|
247
|
+
{"version":"1","type":"view","source":"<tsx>"}
|
|
248
|
+
{"version":"1","type":"patch","data":{},"view":{"source":"<tsx>"}}
|
|
249
|
+
{"version":"1","type":"reopen"}
|
|
250
|
+
{"version":"1","type":"close"}
|
|
251
|
+
```
|
|
240
252
|
- **stdout (line-delimited JSON):**
|
|
241
253
|
- `{"type":"ready","url":"http://127.0.0.1:54321","port":54321}` — emitted once when the dev server is up
|
|
242
254
|
- `{"type":"mutate","id":1,"name":"refresh","args":{}}` — emitted when a view triggers a mutation; respond on stdin
|
|
243
|
-
- `{"type":"
|
|
255
|
+
- `{"type":"disconnected"}` — browser tab stopped heartbeating; mount stays alive
|
|
256
|
+
- `{"type":"reconnected"}` — browser reconnected after a disconnect
|
|
257
|
+
- `{"type":"closed","reason":"caller"}` — mount terminated; reason is `caller` | `signal` | `error`
|
|
244
258
|
- `{"type":"error","message":"…"}` — emitted on internal failure
|
|
245
|
-
- **Lifecycle:** binary exits 0 on
|
|
259
|
+
- **Lifecycle:** binary exits 0 on `closed`, 1 on internal error. The mount does **not** auto-terminate when the browser disconnects — only an explicit `{type:"close"}` message, stdin close, SIGINT/SIGTERM, or internal error terminates it. Closing stdin from the parent triggers a `caller` close.
|
|
246
260
|
|
|
247
261
|
### Minimal Bash example (read-only view, no mutations)
|
|
248
262
|
|
|
@@ -250,8 +264,10 @@ What the consumer CLI is responsible for (out of ui-leaf's scope):
|
|
|
250
264
|
CONFIG='{"view":"spec","viewsRoot":"/abs/path/to/views","data":{"markdown":"# hi"},"port":0}'
|
|
251
265
|
echo "$CONFIG" | ui-leaf mount
|
|
252
266
|
# → {"type":"ready","url":"http://127.0.0.1:54321","port":54321}
|
|
253
|
-
# (browser opens; user closes tab)
|
|
254
|
-
# → {"type":"
|
|
267
|
+
# (browser opens; user closes tab → disconnected; mount stays running)
|
|
268
|
+
# → {"type":"disconnected"}
|
|
269
|
+
# (send {"type":"close"} on stdin to terminate)
|
|
270
|
+
# → {"type":"closed","reason":"caller"}
|
|
255
271
|
```
|
|
256
272
|
|
|
257
273
|
### Worked example with mutations
|
|
@@ -273,10 +289,18 @@ Child → parent stdout:
|
|
|
273
289
|
Parent → child stdin (after handling the mutation):
|
|
274
290
|
{"type":"result","id":1,"value":{"count":1}}
|
|
275
291
|
|
|
276
|
-
(user closes tab)
|
|
292
|
+
(user closes tab → mount stays running)
|
|
293
|
+
|
|
294
|
+
Child → parent stdout:
|
|
295
|
+
{"type":"disconnected"}
|
|
296
|
+
|
|
297
|
+
(parent decides to shut down)
|
|
298
|
+
|
|
299
|
+
Parent → child stdin:
|
|
300
|
+
{"type":"close"}
|
|
277
301
|
|
|
278
302
|
Child → parent stdout:
|
|
279
|
-
{"type":"closed"}
|
|
303
|
+
{"type":"closed","reason":"caller"}
|
|
280
304
|
```
|
|
281
305
|
|
|
282
306
|
Each pending mutation has a unique `id`. Multiple mutations can be in flight concurrently — match `result`/`error` responses by id.
|
|
@@ -285,8 +309,8 @@ Each pending mutation has a unique `id`. Multiple mutations can be in flight con
|
|
|
285
309
|
|
|
286
310
|
- **Pass `viewsRoot` as an absolute path.** No `cwd/views` default games when invoked from another process.
|
|
287
311
|
- **Pass `port: 0`.** ui-leaf asks the OS for a free port and reports it back in the `ready` event. Lets you run concurrent views without collision.
|
|
288
|
-
- **Lower `heartbeatTimeoutMs`** (e.g. 5000)
|
|
289
|
-
- **Kill the child on parent shutdown** rather than relying on heartbeat — `kill <pid>` from the parent
|
|
312
|
+
- **Lower `heartbeatTimeoutMs`** (e.g. 5000) to get faster `disconnected` events. Note that heartbeat timeout no longer terminates the mount — the mount only terminates on an explicit `{type:"close"}` message, stdin close, or signal. If you want fast shutdown on tab close, listen for `disconnected` and send `{type:"close"}` on stdin.
|
|
313
|
+
- **Kill the child on parent shutdown** rather than relying on heartbeat — `kill <pid>` from the parent, or close stdin (which triggers a `caller` close).
|
|
290
314
|
- **Declare every mutation name** the view will call in the `mutations: []` array. The binary only routes mutations whose names appear in the list; calls to undeclared names get a 404 from `/mutate` with the standard "no mutation handler registered for X" error, and the view's `mutate()` promise rejects.
|
|
291
315
|
|
|
292
316
|
### Driving from Node via `mount()` directly
|