@abide/abide 0.44.1 → 0.45.0

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 (58) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +171 -181
  4. package/package.json +2 -1
  5. package/src/lib/server/error.ts +48 -53
  6. package/src/lib/server/json.ts +4 -3
  7. package/src/lib/server/jsonl.ts +1 -1
  8. package/src/lib/server/rpc/defineRpc.ts +2 -8
  9. package/src/lib/server/rpc/types/RemoteHandler.ts +5 -7
  10. package/src/lib/server/rpc/types/RpcHelper.ts +125 -35
  11. package/src/lib/server/rpc/types/TypedError.ts +18 -0
  12. package/src/lib/server/rpc/validationError.ts +3 -3
  13. package/src/lib/server/runtime/STATUS_TEXT.ts +22 -0
  14. package/src/lib/server/runtime/createServer.ts +7 -0
  15. package/src/lib/server/runtime/installAmbientScopeStore.ts +33 -0
  16. package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -0
  17. package/src/lib/server/runtime/typedErrorResponse.ts +26 -0
  18. package/src/lib/server/runtime/types/RequestStore.ts +9 -0
  19. package/src/lib/server/sse.ts +1 -1
  20. package/src/lib/shared/HttpError.ts +1 -1
  21. package/src/lib/shared/cache.ts +26 -4
  22. package/src/lib/shared/cacheEntryFromSnapshot.ts +21 -1
  23. package/src/lib/shared/cacheKeyOf.ts +7 -0
  24. package/src/lib/shared/cacheKeyStore.ts +8 -0
  25. package/src/lib/shared/httpErrorFor.ts +1 -1
  26. package/src/lib/shared/recordCacheKey.ts +6 -0
  27. package/src/lib/shared/toTagSet.ts +3 -3
  28. package/src/lib/shared/types/CacheEntry.ts +15 -0
  29. package/src/lib/shared/types/CacheOptions.ts +5 -5
  30. package/src/lib/shared/types/CacheSnapshotEntry.ts +5 -0
  31. package/src/lib/shared/types/ErrorSpec.ts +6 -6
  32. package/src/lib/shared/types/RemoteFunction.ts +8 -4
  33. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
  34. package/src/lib/ui/compile/generateSSR.ts +10 -3
  35. package/src/lib/ui/createScope.ts +11 -4
  36. package/src/lib/ui/deferResume.ts +29 -0
  37. package/src/lib/ui/dom/appendText.ts +19 -4
  38. package/src/lib/ui/dom/awaitBlock.ts +74 -26
  39. package/src/lib/ui/dom/eachAsync.ts +11 -18
  40. package/src/lib/ui/installHotBridge.ts +2 -0
  41. package/src/lib/ui/matchRoute.ts +18 -1
  42. package/src/lib/ui/remoteProxy.ts +11 -1
  43. package/src/lib/ui/renderToStream.ts +13 -8
  44. package/src/lib/ui/resumeSeedScript.ts +2 -2
  45. package/src/lib/ui/runtime/CURRENT_SCOPE.ts +14 -1
  46. package/src/lib/ui/runtime/RESUME.ts +6 -0
  47. package/src/lib/ui/runtime/ambientScopeBacking.ts +26 -0
  48. package/src/lib/ui/runtime/createEffectNode.ts +7 -1
  49. package/src/lib/ui/runtime/flushEffects.ts +18 -0
  50. package/src/lib/ui/runtime/generationGuard.ts +40 -0
  51. package/src/lib/ui/runtime/runNode.ts +9 -1
  52. package/src/lib/ui/runtime/types/SsrRender.ts +4 -3
  53. package/src/lib/ui/scope.ts +6 -8
  54. package/src/lib/ui/tryEncodeResume.ts +5 -2
  55. package/src/lib/ui/types/Scope.ts +3 -4
  56. package/src/lib/server/rpc/buildErrorConstructors.ts +0 -19
  57. package/src/lib/shared/types/ErrorConstructors.ts +0 -17
  58. package/src/lib/shared/types/ErrorDescriptor.ts +0 -10
package/AGENTS.md CHANGED
@@ -81,10 +81,12 @@ For `bun test`, add `preload = ["@abide/abide/preload"]` under `[test]` in
81
81
  The handler receives `InferOutput<inputSchema>` (or the bare arg type when
82
82
  schemaless), reads `request()` / `cookies()` for request scope, and returns a
83
83
  `json` / `jsonl` / `sse` / `error` / `redirect` helper Response or a raw
84
- `Response`. `opts`: `inputSchema`, `outputSchema` (OpenAPI 200 + MCP
84
+ `Response`. A named, client-branchable error is raised by RETURNING an
85
+ `error.typed(name, status, schema?)` constructor — the RPC infers its typed-error
86
+ surface from whichever constructors the handler returns, so there is no `errors:`
87
+ option. `opts`: `inputSchema`, `outputSchema` (OpenAPI 200 + MCP
85
88
  `outputSchema`), `filesSchema` (multipart File parts; send a `FormData`),
86
- `errors` (declared error constructors handed to the handler), `clients`
87
- (`{ browser, mcp, cli }`), `crossOrigin` (exempt a mutating RPC from the
89
+ `clients` (`{ browser, mcp, cli }`), `crossOrigin` (exempt a mutating RPC from the
88
90
  same-origin CSRF gate), `timeout` (ms handler deadline → 504 every surface),
89
91
  `maxBodySize` (received-byte ceiling → 413), and — mutating helpers only —
90
92
  `outbox` (durable replay; `GET(fn, { outbox })` is a compile error). Query args
@@ -130,7 +132,7 @@ at the top, then bare calls.
130
132
 
131
133
  | Form | Meaning |
132
134
  | --- | --- |
133
- | `let x = state(v, transform?)` | Writable cell; assign `x = …` to update |
135
+ | `let x = state(v, transform?)` | Writable state; assign `x = …` to update |
134
136
  | `const d = computed(() => …)` | Read-only derived |
135
137
  | `const l = linked(fn, transform?)` | Writable, re-seeds when `fn`'s deps change |
136
138
  | `effect(() => { … })` | Reaction re-run on dep change; client-only (SSR strips it) |
@@ -196,7 +198,7 @@ the fallback form. No named slots.
196
198
  - `abide/server/json` — JSON `Response` with `Cache-Control: no-store` default; same shape as `Response.json`.
197
199
  - `abide/server/jsonl` — wraps an `AsyncIterable<Frame>` as a JSON Lines (`application/jsonl`) streaming Response.
198
200
  - `abide/server/sse` — wraps an `AsyncIterable<Frame>` as Server-Sent Events (`text/event-stream`).
199
- - `abide/server/error` — plain-text error Response; `error(status, message?)`, message defaults to the status reason phrase.
201
+ - `abide/server/error` — error Responses. `error(status, message?)` = plain-text (message defaults to the status reason phrase). `error.typed(name, status, schema?)` = a reusable named typed-error constructor; returning it from a handler IS the error (serializes `{ $abideError, data }` at `status`), and the RPC infers `rpc.isError(e, name)` narrowing (`.kind` + typed `.data`) from the returned constructors — no `errors:` option.
200
202
  - `abide/server/redirect` — redirect Response; accepts relative URLs, defaults to 302.
201
203
 
202
204
  ### Request scope — @documentation request-scope
@@ -273,7 +275,7 @@ the fallback form. No named slots.
273
275
 
274
276
  ### Reactive state — @documentation reactive-state
275
277
 
276
- - `abide/ui/scope` — `scope(address?)` resolves the current lexical scope (or `scope('/')` the tree root); the sole reactive entry, carrying `state`/`computed`/`linked`/`effect` + data/context/capability methods.
278
+ - `abide/ui/scope` — `scope()` resolves the current lexical scope; the sole reactive entry, carrying `state`/`computed`/`linked`/`effect` + data/context/capability methods (walk to the tree root via the handle's `.root()`).
277
279
 
278
280
  ### Templating — @documentation templating
279
281
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # abide
2
2
 
3
+ ## 0.45.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3fadd43: defer large {#await cache()} with inert hydration + lazy seed ([`2b87f5d`](https://github.com/briancray/abide/commit/2b87f5de270d91c9ffc9f5467051a221e6f9c204))
8
+ - 7aa4032: thread durable outbox bit into RemoteFunction return type ([`4fa34b4`](https://github.com/briancray/abide/commit/4fa34b43fdac7a0aaa91a965f1aca488e1378a27))
9
+ - 3fadd43: Breaking: collapse rpc handler generics into one inferred function type ([`f22bfd3`](https://github.com/briancray/abide/commit/f22bfd3f189214209ff3debb2b4916bd3b3680c7))
10
+ - 6d82974: BREAKING: `CacheOptions.tags` (and the `cache.invalidate` / `pending` `{ tags }` selector) now accepts only a `string[]`, not `string | string[]`. Wrap a single tag in an array:
11
+
12
+ ```ts
13
+ cache(getOrder, { tags: ['orders'] })({ id })
14
+ cache.invalidate({ tags: ['orders'] })
15
+ ```
16
+
17
+ A bare-string tag previously auto-wrapped; the array-only form removes the `typeof` branch and the silent-splat foot-gun (a JS caller passing a multi-char string got it iterated into per-character tags).
18
+
19
+ - ce61cd0: Add `error.typed(name, status, schema?)` — declare a single, reusable typed-error constructor. Returning it from a handler IS the error (it serializes a `{ $abideError, data }` body at `status`), and the rpc reads the constructor's branded return type to expose the error on the client's `rpc.isError(caught, 'name')` (narrowing `.kind` and typed `.data`). Compose by returning whichever constructors you want — no set, no registration:
20
+
21
+ ```ts
22
+ const outOfStock = error.typed('outOfStock', 409, z.object({ sku: z.string() }))
23
+ export const buy = POST(({ sku }) => (inStock(sku) ? json(place(sku)) : outOfStock({ sku })))
24
+ // buy.isError(e, 'outOfStock') → e.data: { sku: string }, inferred from the body
25
+ ```
26
+
27
+ The rpc's typed-error surface is now **inferred from the handler's return type** — the errors a handler returns are the errors it can raise — so there is no `errors:` rpc option and no `ctx.errors` handler param. A typed error you only ever `throw` (rather than `return`) narrows kind-only, like a plain `error()`.
28
+
29
+ BREAKING: removes the rpc `errors:` option and the handler's `{ errors }` second argument. Replace `POST((args, { errors }) => error(errors.x(data)), { inputSchema, errors: { x: { status, data } } })` with a module-scope `const x = error.typed('x', status, schema)` constructor returned from the handler: `POST((args) => x(data), { inputSchema })`.
30
+
31
+ ### Patch Changes
32
+
33
+ - 7aa4032: percent-decode client route params to match SSR ([`1034dfb`](https://github.com/briancray/abide/commit/1034dfb3d451be117da7416f5f0bae611f82e3df))
34
+ - 7aa4032: drop in-flight await settle after owner teardown ([`109e56e`](https://github.com/briancray/abide/commit/109e56e52851382a53a259d614671d0fb0e1fd08))
35
+ - 7aa4032: make effect flush non-reentrant and pin effect teardown scope ([`28c9e79`](https://github.com/briancray/abide/commit/28c9e79b4e7a1905483c469182a58d7df8388c0e))
36
+ - 7aa4032: drop scope('/') address param ([`3b3400d`](https://github.com/briancray/abide/commit/3b3400d5c87f151701da927d66912c2715e511fe))
37
+ - 3fadd43: remove cell from the public Scope surface ([`408bd25`](https://github.com/briancray/abide/commit/408bd25864f67fc88da531793b6233806f7ab3a4))
38
+ - 7aa4032: isolate SSR ambient scope per request ([`52d3e1c`](https://github.com/briancray/abide/commit/52d3e1cfc941df5d044fb2e6e6477bbf85c96ed4))
39
+ - 7aa4032: extract shared generationGuard for async teardown invalidation ([`5775422`](https://github.com/briancray/abide/commit/5775422acb2625cb4bb864c0b9f77611e1a530a2))
40
+ - 7aa4032: dispose scope children in reverse (LIFO) ([`9f56ea2`](https://github.com/briancray/abide/commit/9f56ea20dacb6ae28161b3a604dbc94d2f69ca98))
41
+ - 18d9eb1: fix hydration null-deref when a reactive text binding first renders an empty string — the server emits no text node, so the client now synthesizes its own node at the claim cursor instead of dereferencing null in the bind effect
42
+
3
43
  ## 0.44.1
4
44
 
5
45
  ### Patch Changes
package/README.md CHANGED
@@ -1,26 +1,24 @@
1
1
  # abide
2
2
 
3
3
  **One typed declaration fans out to HTTP, a CLI, an MCP tool, and an OpenAPI
4
- operationand the same callable runs on the server and in the browser.**
4
+ spec — on a single Bun runtime.**
5
5
 
6
- abide is an isomorphic, multimodal HTTP framework for a single Bun runtime: you
7
- declare a typed RPC once and it becomes a browser fetch, an in-process SSR call,
8
- an MCP tool, a CLI subcommand, and an OpenAPI operation; the bundler swaps the
9
- runtime per side so the same name behaves the same on both. Built for humans
10
- *and* machines.
6
+ abide is a type-safe isomorphic framework: you declare an RPC, a socket, and a
7
+ `.abide` component once, and the bundler swaps the runtime per side the same
8
+ callable is a direct in-process call on the server and a typed `fetch` in the
9
+ browser. Built for humans *and* machines: every schema-carrying RPC is
10
+ simultaneously a browser endpoint, a CLI subcommand, an MCP tool, and an
11
+ OpenAPI operation.
11
12
 
12
- - One direct dependency (TypeScript). One runtime (Bun ≥ 1.3).
13
- - `.abide` single-file components render on the server and hydrate in place.
13
+ One direct dependency (TypeScript). One runtime (Bun ≥ 1.3.0).
14
14
 
15
15
  ## Quick start
16
16
 
17
17
  ```sh
18
- bunx abide scaffold my-app # scaffolds, installs, and starts the dev server
19
- ```
20
-
21
- Or clone the kitchen-sink example:
18
+ # Scaffold a project, install it, and start the dev server (in a TTY).
19
+ bunx abide scaffold my-app
22
20
 
23
- ```sh
21
+ # Or explore the full feature set in the kitchen-sink example.
24
22
  git clone https://github.com/briancray/abide
25
23
  cd abide/examples/kitchen-sink
26
24
  bun install
@@ -29,10 +27,11 @@ bun run dev
29
27
 
30
28
  ## RPCs
31
29
 
32
- An RPC is one export under `src/server/rpc/`. The file path is the URL; the
33
- export's schema validates the args and projects the MCP tool, the CLI flags, and
34
- the OpenAPI operation. The schema contract is Standard Schema — pass a zod,
35
- valibot, or arktype schema directly, unadapted.
30
+ An RPC is one export per file under `src/server/rpc/`. The file path is the URL
31
+ (under `/rpc/`), the import (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`/`HEAD`) picks
32
+ the HTTP method, and an optional `inputSchema` — any Standard Schema (zod,
33
+ valibot, arktype, unadapted) — validates the args and projects the MCP tool, the
34
+ CLI flags, and the OpenAPI operation.
36
35
 
37
36
  ```ts
38
37
  // src/server/rpc/getMessages.ts
@@ -40,213 +39,204 @@ import { GET } from '@abide/abide/server/GET'
40
39
  import { json } from '@abide/abide/server/json'
41
40
  import { z } from 'zod'
42
41
 
42
+ const rooms: Record<string, { id: string; text: string }[]> = {}
43
+
44
+ // GET /rpc/getMessages?room=lobby
43
45
  export const getMessages = GET(
44
- async ({ channel }) => json(await db.recentMessages(channel)),
45
- { inputSchema: z.object({ channel: z.coerce.string() }) },
46
+ (args: { room: string }) => json(rooms[args.room] ?? []),
47
+ { inputSchema: z.object({ room: z.coerce.string() }) },
48
+ )
49
+ ```
50
+
51
+ ```ts
52
+ // src/server/rpc/postMessage.ts
53
+ import { POST } from '@abide/abide/server/POST'
54
+ import { json } from '@abide/abide/server/json'
55
+ import { error } from '@abide/abide/server/error'
56
+ import { z } from 'zod'
57
+
58
+ const rateLimited = error.typed('rateLimited', 429)
59
+
60
+ // A mutating method never auto-exposes to MCP — opt in explicitly.
61
+ export const postMessage = POST(
62
+ (args: { room: string; text: string }) => {
63
+ if (args.text.length > 500) return rateLimited()
64
+ return json({ ok: true })
65
+ },
66
+ { inputSchema: z.object({ room: z.string(), text: z.string() }), clients: { mcp: true } },
46
67
  )
47
68
  ```
48
69
 
49
- One declaration, every surface:
70
+ One declaration, five surfaces:
50
71
 
51
72
  ```text
52
- export const getMessages = GET(fn, { inputSchema })
53
-
54
- ┌───────────────┬─────────────┼─────────────┬───────────────┐
55
- ▼ ▼ ▼
56
- SSR call browser fetch MCP tool CLI command OpenAPI op
57
- cache(fn)() typed proxy (read-only) getMessages /openapi.json
58
- (in-process) (swapped fn())
73
+ export const getMessages = GET(fn, { inputSchema })
74
+
75
+ ┌────────────┬─────────────┼─────────────┬────────────┐
76
+ ▼ ▼ ▼
77
+ cache(fn)() typed fetch MCP tool CLI subcmd OpenAPI op
78
+ (SSR, warm (browser, (read-only (any schema- (/openapi
79
+ hydration) swapped) GET/HEAD) carrying) .json)
59
80
  ```
60
81
 
61
- A schema unlocks the CLI for any method and auto-exposes read-only methods
62
- (`GET`/`HEAD`) as MCP tools; a mutating method (`POST`/`PUT`/`PATCH`/`DELETE`)
63
- never auto-exposes to MCP — it needs an explicit `clients: { mcp: true }`.
64
- Consume the RPC four ways: `cache(getMessages)({ channel })` in-process during
65
- SSR, the swapped `fetch` proxy in the browser, `getMessages.raw(args)` for the
66
- underlying `Response`, and `getMessages.stream(args)` for a frame iterable.
82
+ A schema unlocks the CLI for any RPC and MCP for read-only methods (`GET`/`HEAD`);
83
+ a mutating method (`POST`/`PUT`/`PATCH`/`DELETE`) reaches MCP only with an
84
+ explicit `clients: { mcp: true }`. Consume an RPC four ways: `cache(fn)()`
85
+ in-process (warm SSR + hydration), the swapped `fetch` in the browser,
86
+ `fn.raw(args)` for the underlying `Response`, and `fn.stream(args)` to iterate
87
+ `jsonl`/`sse` frames.
67
88
 
68
- > Query args travel as strings — validate with `z.coerce.*`. A per-RPC
69
- > `timeout` (504 on every surface) is distinct from the client-side
89
+ > Query args travel as strings — coerce them (`z.coerce.number()`). A per-RPC
90
+ > `timeout` returns 504 on every surface; it is distinct from the client-wide
70
91
  > `ABIDE_CLIENT_TIMEOUT`.
71
92
 
72
93
  ## Sockets
73
94
 
74
95
  A socket is one broadcast topic per file under `src/server/sockets/`. A
75
- `Socket<T>` is an isomorphic `AsyncIterable<T>` — `for await (const m of chat)`
76
- is the live stream and every socket multiplexes onto one WebSocket at
77
- `/__abide/sockets`.
96
+ `Socket<T>` is an isomorphic `AsyncIterable<T>` — iterate for the live stream,
97
+ `.tail(count)` to seed from the retained tail. Every socket multiplexes onto one
98
+ WebSocket at `/__abide/sockets`; the server publishes in-process and fans out,
99
+ the browser proxy sends a `pub` frame.
78
100
 
79
101
  ```ts
80
- // src/server/sockets/chat.ts
102
+ // src/server/sockets/messages.ts
81
103
  import { socket } from '@abide/abide/server/socket'
82
104
  import { z } from 'zod'
83
105
 
84
- export const chat = socket({
85
- schema: z.object({ id: z.string(), author: z.string(), text: z.string() }),
86
- tail: 50, // retain the last 50 frames for replay
87
- ttl: 60_000, // evict retained frames older than 60s
106
+ export const messages = socket({
107
+ schema: z.object({ room: z.string(), text: z.string() }),
108
+ tail: 50, // retain the last 50 frames for late joiners / reconnects
109
+ ttl: 60_000, // evict retained frames older than 60s before replay
88
110
  })
89
111
  ```
90
112
 
91
- Each socket also has an HTTP face at `/__abide/sockets/<name>`: `GET` returns the
92
- retained tail, `POST` publishes a frame (gated by `clientPublish`).
113
+ The HTTP face lives at `/__abide/sockets/<name>`: `GET` returns the retained
114
+ tail, `POST` publishes (gated by `clientPublish`).
93
115
 
94
116
  ## Components
95
117
 
96
- A `.abide` component renders on the server and hydrates in the browser. The page
97
- below imports the RPC and the socket above and exercises the whole template
98
- grammar. Reactive state is reached through `scope()` the documented default is
99
- to destructure it once at the top of the `<script>` and call the primitives bare.
118
+ A `.abide` component is a single file: a leading `<script>` (module imports plus
119
+ reactive setup), the template, and an optional component-scoped `<style>`. This
120
+ page imports the RPC from §RPCs and the socket from §Sockets and drives them
121
+ through the whole template grammar.
122
+
123
+ Reactive state is reached only through `scope()`. The authoring default is the
124
+ destructure-once idiom — pull the primitives off `scope()` at the top of the
125
+ `<script>`, then call them bare. `computed` is read-only; `effect` is client-only
126
+ (stripped from SSR). A bare `state`/`computed`/`linked`/`effect` with no
127
+ `scope()` destructure is a compile error.
100
128
 
101
129
  ```html
102
130
  <script>
103
- import { cache } from '@abide/abide/shared/cache'
104
- import { tail } from '@abide/abide/ui/tail'
105
- import { html } from '@abide/abide/ui/html'
106
- import { getMessages } from '$server/rpc/getMessages'
107
- import { sendMessage } from '$server/rpc/sendMessage'
108
- import { chat } from '$server/sockets/chat'
109
- import Card from '$ui/Card.abide'
110
- import Message from '$ui/Message.abide'
111
- import Risky from '$ui/Risky.abide'
112
-
113
- // props() is the ambient prop reader — no import; destructure with defaults + rest
114
- const { channel = 'general', ...rest } = props()
115
-
116
- // destructure-once: reach every reactive primitive through scope(), then call bare
117
- const { state, computed, linked, effect } = scope()
118
-
119
- let draft = state('')
120
- let count = state(0)
121
- let muted = state(false)
122
- let topic = state('all')
123
- const doubled = computed(() => count * 2)
124
- const banner = linked(() => `# ${channel}`)
125
-
126
- // live reads off the §3 socket: latest frame, plus a window of the last 20
127
- const latest = tail(chat)
128
- const recent = tail(chat, { last: 20 })
129
-
130
- // effect runs client-only (stripped from SSR)
131
- effect(() => {
132
- document.title = `${channel} (${count})`
133
- })
134
-
135
- // an event handler calling a mutating RPC
136
- async function send() {
137
- await sendMessage({ channel, text: draft })
131
+ import { cache } from '@abide/abide/shared/cache'
132
+ import { tail } from '@abide/abide/ui/tail'
133
+ import { html } from '@abide/abide/ui/html'
134
+ import { getMessages } from '$server/rpc/getMessages.ts'
135
+ import { postMessage } from '$server/rpc/postMessage.ts'
136
+ import { messages } from '$server/sockets/messages.ts'
137
+ import MessageCard from '$ui/MessageCard.abide'
138
+
139
+ const { state, computed, linked, effect } = scope()
140
+ const { room = 'lobby' } = props()
141
+
142
+ let draft = state('')
143
+ let showRaw = state(false)
144
+ let filter = state('all')
145
+ const trimmed = computed(() => draft.trim())
146
+ const outgoing = linked(() => room + ': ' + trimmed)
147
+ const banner = html`<strong>#${room}</strong>`
148
+
149
+ // client-only reaction — never runs during SSR
150
+ effect(() => console.log('draft is now', draft))
151
+
152
+ // the derived two-way binding target, read down / written up
153
+ const get = () => outgoing
154
+ const set = (value) => (draft = value)
155
+ // mutating RPC from an event handler
156
+ const send = () => {
157
+ postMessage({ room, text: trimmed })
138
158
  draft = ''
139
- count = count + 1
140
- }
141
-
142
- // the derived two-way binding's accessors
143
- const get = () => count
144
- const set = (value) => (count = Number(value))
145
-
146
- // an attachment: runs against the element at mount
147
- const focus = (node) => node.focus()
159
+ }
148
160
  </script>
149
161
 
150
- <main {...rest}>
151
- <h1 class:muted={muted} style:opacity={muted ? 0.5 : 1}>{banner}</h1>
152
- <p>seen {count} — doubled {doubled}</p>
153
- <p>{html`<em>latest:</em> ${latest?.text ?? '—'}`}</p>
154
-
155
- <form onsubmit={send}>
156
- <input bind:value={draft} attach={focus} placeholder="message" />
157
- <label><input type="checkbox" bind:checked={muted} /> mute</label>
158
- <label><input type="radio" bind:group={topic} value="all" /> all</label>
159
- <label><input type="radio" bind:group={topic} value="mine" /> mine</label>
160
- <input type="range" min="0" max="50" bind:value={{ get, set }} />
161
- <button>send</button>
162
- </form>
163
-
164
- {#snippet row(message)}
165
- <li>{html`<strong>${message.author}</strong>`}: {message.text}</li>
166
- {/snippet}
167
-
168
- {#if recent.length === 0}
169
- <p>No messages yet.</p>
170
- {:else if muted}
171
- <p>Muted — {recent.length} hidden.</p>
172
- <script>
173
- // a nested script scoped to this branch — its own scope(), no imports
174
- const { computed } = scope()
175
- const note = computed(() => `${recent.length} while muted`)
176
- </script>
177
- <small>{note}</small>
178
- <style>
179
- small { color: gray; }
180
- </style>
181
- {:else}
182
- <ul>
183
- {#for message, i of recent by message.id}
184
- <Card title={`#${i}`}>{row(message)}</Card>
185
- {/for}
186
- </ul>
187
- {/if}
188
-
189
- <h2>filter</h2>
190
- {#switch topic}
191
- {:case 'all'}<p>showing everything</p>
192
- {:case 'mine'}<p>showing mine</p>
193
- {:default}<p>unknown filter</p>
194
- {/switch}
195
-
196
- <h2>history</h2>
197
- {#await cache(getMessages)({ channel })}
198
- <p>loading…</p>
199
- {:then list}
200
- <ul>
201
- {#for entry of list by entry.id}
202
- <Message {...entry} />
203
- {/for}
204
- </ul>
205
- {:catch error}
206
- <p>failed: {error.message}</p>
207
- {:finally}
208
- <hr />
209
- {/await}
210
-
211
- <h2>live</h2>
212
- <ul>
213
- {#for await frame of chat}
214
- <li>{frame.text}</li>
162
+ {#snippet chip(label)}
163
+ <span class="rounded bg-slate-100 px-2 text-xs">{label}</span>
164
+ {/snippet}
165
+
166
+ <section class="p-4" attach={(node) => node.focus()} {...{ id: 'chat' }}>
167
+ <h1 class:live={showRaw} style:opacity={showRaw ? 1 : 0.6}>{banner}</h1>
168
+
169
+ {#await cache(getMessages)({ room })}
170
+ <p>loading…</p>
171
+ {:then history}
172
+ {#if history.length > 0}
173
+ {#for message, i of history by message.id}
174
+ <MessageCard {...message}>
175
+ {chip(i)}
176
+ <p>{message.text}</p>
177
+ </MessageCard>
178
+ {/for}
179
+ {:else if showRaw}
180
+ <p>no history yet</p>
181
+ {:else}
182
+ <p>say hello</p>
183
+ {/if}
184
+ {:catch error}
185
+ <p>failed to load: {error.message}</p>
186
+ {:finally}
187
+ <hr />
188
+ {/await}
189
+
190
+ {#for await frame of messages.tail(10)}
191
+ <p class="live-frame">{frame.text}</p>
215
192
  {/for}
216
- </ul>
217
193
 
218
- {#try}
219
- <Risky />
220
- {:catch error}
221
- <p>widget crashed: {error.message}</p>
222
- {:finally}
223
- <span>boundary settled</span>
224
- {/try}
225
- </main>
194
+ {#switch filter}
195
+ {:case 'all'}<p>showing all rooms</p>
196
+ {:case 'mine'}<p>showing my rooms</p>
197
+ {:default}<p>—</p>
198
+ {/switch}
199
+
200
+ {#try}
201
+ {#if showRaw}
202
+ <script>const { computed } = scope()
203
+ const label = computed(() => 'raw view active')</script>
204
+ <pre>{label}</pre>
205
+ {/if}
206
+ {:catch error}
207
+ <p>render error: {error.message}</p>
208
+ {:finally}
209
+ <small>rendered</small>
210
+ {/try}
211
+
212
+ <form onsubmit={send}>
213
+ <input bind:value={draft} placeholder="message" />
214
+ <label><input type="checkbox" bind:checked={showRaw} /> raw</label>
215
+ <label><input type="radio" bind:group={filter} value="all" /> all</label>
216
+ <label><input type="radio" bind:group={filter} value="mine" /> mine</label>
217
+ <input bind:value={{ get, set }} />
218
+ <button type="submit">send</button>
219
+ </form>
220
+ </section>
226
221
 
227
222
  <style>
228
- main { font-family: system-ui; }
229
- .muted { text-decoration: line-through; }
223
+ .live { color: teal; }
230
224
  </style>
231
225
  ```
232
226
 
233
- A capitalised tag is a child component; the content nested inside it renders
234
- where the child calls `{children()}` (the `<slot>` element was removed — there
235
- are no named slots). `{#if children}…{:else}…{/if}` is the fallback form:
227
+ Content nested inside a capitalised child component renders where the child
228
+ calls `{children()}` the single slot, with `{#if children}…{:else}…{/if}` as
229
+ its fallback (there is no `<slot>` element and no named slots):
236
230
 
237
231
  ```html
238
232
  <script>
239
- const { title = 'Card' } = props()
233
+ const { text = '' } = props()
240
234
  </script>
241
235
 
242
- <section class="card">
243
- <h3>{title}</h3>
244
- {#if children}{children()}{:else}<p>empty</p>{/if}
245
- </section>
246
-
247
- <style>
248
- .card { border: 1px solid #ddd; }
249
- </style>
236
+ <article class="card rounded border p-2">
237
+ <p class="font-medium">{text}</p>
238
+ {#if children}{children()}{:else}<em>no body</em>{/if}
239
+ </article>
250
240
  ```
251
241
 
252
242
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.44.1",
3
+ "version": "0.45.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
@@ -75,6 +75,7 @@
75
75
  "./ui/html": "./src/lib/ui/html.ts",
76
76
  "./ui/effect": "./src/lib/ui/effect.ts",
77
77
  "./ui/scope": "./src/lib/ui/scope.ts",
78
+ "./ui/deferResume": "./src/lib/ui/deferResume.ts",
78
79
  "./ui/enterScope": "./src/lib/ui/enterScope.ts",
79
80
  "./ui/exitScope": "./src/lib/ui/exitScope.ts",
80
81
  "./ui/outbox": "./src/lib/ui/outbox.ts",