@abide/abide 0.44.0 → 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.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +64 -0
- package/README.md +171 -181
- package/package.json +2 -1
- package/src/abideResolverPlugin.ts +6 -1
- package/src/devEntry.ts +43 -9
- package/src/lib/cli/parseArgvForRpc.ts +21 -24
- package/src/lib/cli/runCli.ts +9 -24
- package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
- package/src/lib/server/error.ts +48 -53
- package/src/lib/server/json.ts +4 -3
- package/src/lib/server/jsonl.ts +1 -1
- package/src/lib/server/rpc/defineRpc.ts +11 -9
- package/src/lib/server/rpc/types/RemoteHandler.ts +5 -7
- package/src/lib/server/rpc/types/RpcHelper.ts +125 -35
- package/src/lib/server/rpc/types/TypedError.ts +18 -0
- package/src/lib/server/rpc/validationError.ts +3 -3
- package/src/lib/server/runtime/STATUS_TEXT.ts +22 -0
- package/src/lib/server/runtime/createServer.ts +36 -12
- package/src/lib/server/runtime/devHotModuleResponse.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/installAmbientScopeStore.ts +33 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -0
- package/src/lib/server/runtime/typedErrorResponse.ts +26 -0
- package/src/lib/server/runtime/types/RequestStore.ts +25 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/sockets/defineSocket.ts +5 -1
- package/src/lib/server/sse.ts +1 -1
- package/src/lib/shared/HttpError.ts +1 -1
- package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
- package/src/lib/shared/activeCacheStore.ts +5 -14
- package/src/lib/shared/activePage.ts +5 -19
- package/src/lib/shared/augmentModule.ts +18 -0
- package/src/lib/shared/basePath.ts +6 -5
- package/src/lib/shared/baseResolver.ts +10 -0
- package/src/lib/shared/baseSlot.ts +6 -12
- package/src/lib/shared/cache.ts +46 -4
- package/src/lib/shared/cacheEntryFromSnapshot.ts +21 -1
- package/src/lib/shared/cacheKeyOf.ts +7 -0
- package/src/lib/shared/cacheKeyStore.ts +8 -0
- package/src/lib/shared/cacheStoreResolver.ts +12 -0
- package/src/lib/shared/cacheStoreSlot.ts +5 -13
- package/src/lib/shared/changeAffectsClient.ts +35 -0
- package/src/lib/shared/createResolverSlot.ts +37 -0
- package/src/lib/shared/debugGate.ts +90 -0
- package/src/lib/shared/emitLogRecord.ts +18 -4
- package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
- package/src/lib/shared/httpErrorFor.ts +1 -1
- package/src/lib/shared/isDebugEnabled.ts +3 -10
- package/src/lib/shared/isDebugNegated.ts +4 -6
- package/src/lib/shared/pageResolver.ts +16 -0
- package/src/lib/shared/pageSlot.ts +5 -14
- package/src/lib/shared/recordCacheKey.ts +6 -0
- package/src/lib/shared/requestScopeResolver.ts +12 -0
- package/src/lib/shared/requestScopeSlot.ts +7 -12
- package/src/lib/shared/responseBodyKind.ts +35 -0
- package/src/lib/shared/setBaseResolver.ts +2 -4
- package/src/lib/shared/setCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setPageResolver.ts +2 -5
- package/src/lib/shared/setRequestScopeResolver.ts +3 -5
- package/src/lib/shared/toTagSet.ts +3 -3
- package/src/lib/shared/types/CacheEntry.ts +15 -0
- package/src/lib/shared/types/CacheOptions.ts +5 -5
- package/src/lib/shared/types/CacheSnapshotEntry.ts +5 -0
- package/src/lib/shared/types/DebugGate.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +6 -6
- package/src/lib/shared/types/RemoteFunction.ts +8 -4
- package/src/lib/shared/types/ResolverSlot.ts +10 -0
- package/src/lib/shared/writeDts.ts +8 -1
- package/src/lib/shared/writePublicAssetsDts.ts +5 -9
- package/src/lib/shared/writeRoutesDts.ts +12 -16
- package/src/lib/shared/writeRpcDts.ts +13 -12
- package/src/lib/shared/writeTestRpcDts.ts +11 -11
- package/src/lib/shared/writeTestSocketsDts.ts +9 -9
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +9 -7
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +24 -12
- package/src/lib/ui/compile/compileModule.ts +37 -16
- package/src/lib/ui/compile/compileShadow.ts +75 -1
- package/src/lib/ui/compile/createShadowLanguageService.ts +29 -3
- package/src/lib/ui/compile/generateBuild.ts +7 -1
- package/src/lib/ui/compile/generateSSR.ts +10 -3
- package/src/lib/ui/compile/parseTemplate.ts +4 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +1 -1
- package/src/lib/ui/createScope.ts +11 -4
- package/src/lib/ui/deferResume.ts +29 -0
- package/src/lib/ui/dom/appendText.ts +19 -4
- package/src/lib/ui/dom/awaitBlock.ts +74 -26
- package/src/lib/ui/dom/eachAsync.ts +11 -18
- package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
- package/src/lib/ui/dom/skeleton.ts +10 -1
- package/src/lib/ui/dom/switchBlock.ts +15 -63
- package/src/lib/ui/dom/when.ts +11 -64
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/matchRoute.ts +18 -1
- package/src/lib/ui/remoteProxy.ts +11 -1
- package/src/lib/ui/renderToStream.ts +13 -8
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/runtime/CURRENT_SCOPE.ts +14 -1
- package/src/lib/ui/runtime/RESUME.ts +6 -0
- package/src/lib/ui/runtime/ambientScopeBacking.ts +26 -0
- package/src/lib/ui/runtime/createDoc.ts +102 -12
- package/src/lib/ui/runtime/createEffectNode.ts +7 -1
- package/src/lib/ui/runtime/flushEffects.ts +18 -0
- package/src/lib/ui/runtime/generationGuard.ts +40 -0
- package/src/lib/ui/runtime/runNode.ts +9 -1
- package/src/lib/ui/runtime/types/SsrRender.ts +4 -3
- package/src/lib/ui/scope.ts +6 -8
- package/src/lib/ui/tryEncodeResume.ts +5 -2
- package/src/lib/ui/types/Scope.ts +3 -4
- package/src/zodCjsPlugin.ts +16 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +0 -19
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
- package/src/lib/shared/types/ErrorConstructors.ts +0 -17
- 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`.
|
|
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
|
-
`
|
|
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
|
|
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` —
|
|
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(
|
|
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,69 @@
|
|
|
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
|
+
|
|
43
|
+
## 0.44.1
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- 767b415: 02-zod-plugin-memo ([`0e00b21`](https://github.com/briancray/abide/commit/0e00b217f99352a7d3def4841172c5bff287bc5c))
|
|
48
|
+
- 767b415: 06-wakeSubtree-prefix-index ([`23ab920`](https://github.com/briancray/abide/commit/23ab9202fd0cb41e01811e78b76eac2843c9cf8f))
|
|
49
|
+
- 767b415: 09-when-via-switch ([`48f8d50`](https://github.com/briancray/abide/commit/48f8d5063cf74060004ad38691f584abcff0cab2))
|
|
50
|
+
- 767b415: 08-dts-codegen ([`5170542`](https://github.com/briancray/abide/commit/51705426e55a700140cc7bc38f7e21f30759fce3))
|
|
51
|
+
- 767b415: 03-socket-publish-gate ([`56fae49`](https://github.com/briancray/abide/commit/56fae49bf393fdfb3aa35cecc618a39443354ce4))
|
|
52
|
+
- 767b415: 10-cli-tokenizer ([`5e305c1`](https://github.com/briancray/abide/commit/5e305c154efd5f77d0a57e78831b72a30b4d9155))
|
|
53
|
+
- 767b415: 14-resolver-slot-factory-JUDGMENT ([`6e00809`](https://github.com/briancray/abide/commit/6e00809c343e24599d54b2304a50a9cecd90619f))
|
|
54
|
+
- 767b415: catch top-level await in a .abide <script> during abide check ([`76ff64e`](https://github.com/briancray/abide/commit/76ff64e9fb9ffed108ae2fde53f2ded2bf3a0abe))
|
|
55
|
+
- 767b415: 12-warm-clone-PARTIAL ([`8e58e0b`](https://github.com/briancray/abide/commit/8e58e0b2c1891174cc809797f949f01619529b89))
|
|
56
|
+
- 767b415: 05-compiler-double-pass ([`9216dae`](https://github.com/briancray/abide/commit/9216dae4c26e27fce49f39b116bd8b1dcf1dd226))
|
|
57
|
+
- 767b415: 04-lsp-shadow-cache ([`9f9f5fc`](https://github.com/briancray/abide/commit/9f9f5fc84792204259d591fa4aff261b2a19c46d))
|
|
58
|
+
- 767b415: 13-dev-rebuild-scoping-JUDGMENT ([`b5a1d49`](https://github.com/briancray/abide/commit/b5a1d49e6607a8fcf37efe03e9fe6f6b2c31ea45))
|
|
59
|
+
- 767b415: gate skeleton fragment-staging on a connected parent ([`c9380ee`](https://github.com/briancray/abide/commit/c9380eebdd9f6773ccffd446665b1acbc78b1123))
|
|
60
|
+
- 767b415: 07-debug-gate ([`c96466e`](https://github.com/briancray/abide/commit/c96466e73ea1f521d728b11aac44262530c3b09c))
|
|
61
|
+
- 767b415: rename paint's escape var to avoid shadowing the global ([`caf1322`](https://github.com/briancray/abide/commit/caf1322adc6598bc6ea32e0ba9047ba768b6f5e1))
|
|
62
|
+
- 767b415: reflow generateBuild attr-kind union to biome's multiline form ([`f9b3669`](https://github.com/briancray/abide/commit/f9b36690e4add2c955abb8f0127a9af9d513be53))
|
|
63
|
+
- 767b415: 01-bugs-B1-B2 ([`fa8659d`](https://github.com/briancray/abide/commit/fa8659d276265c0fbb4f7ca0740fc74702f00168))
|
|
64
|
+
- 767b415: address code-review findings ([`fb02df5`](https://github.com/briancray/abide/commit/fb02df58c924d7206fce35874cc80309949c9e59))
|
|
65
|
+
- 767b415: 11-server-log-micro ([`fb689bc`](https://github.com/briancray/abide/commit/fb689bc05bee9ba127ddf206e925e3a9e04841fd))
|
|
66
|
+
|
|
3
67
|
## 0.44.0
|
|
4
68
|
|
|
5
69
|
### Minor 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
|
-
|
|
4
|
+
spec — on a single Bun runtime.**
|
|
5
5
|
|
|
6
|
-
abide is
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
the
|
|
35
|
-
valibot,
|
|
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
|
-
|
|
45
|
-
|
|
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,
|
|
70
|
+
One declaration, five surfaces:
|
|
50
71
|
|
|
51
72
|
```text
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 —
|
|
69
|
-
> `timeout`
|
|
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>` —
|
|
76
|
-
|
|
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/
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
229
|
-
.muted { text-decoration: line-through; }
|
|
223
|
+
.live { color: teal; }
|
|
230
224
|
</style>
|
|
231
225
|
```
|
|
232
226
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
233
|
+
const { text = '' } = props()
|
|
240
234
|
</script>
|
|
241
235
|
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
</
|
|
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.
|
|
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",
|
|
@@ -257,10 +257,15 @@ export function abideResolverPlugin({
|
|
|
257
257
|
return {
|
|
258
258
|
name: 'abide-resolver',
|
|
259
259
|
setup(build) {
|
|
260
|
-
/* Fresh edge graph each build (dev watch reuses the
|
|
260
|
+
/* Fresh edge graph + resolution cache each build (dev watch reuses the
|
|
261
|
+
plugin instance). The resolution cache memoises (path → resolved file)
|
|
262
|
+
within a build; clearing it per build keeps the memo but stops a stale
|
|
263
|
+
miss — a path resolved before its file existed — surviving into the next
|
|
264
|
+
build, so a freshly-created file resolves instead of staying "not found".
|
|
261
265
|
onStart is build-time only — absent in the runtime/preload plugin context. */
|
|
262
266
|
build.onStart?.(() => {
|
|
263
267
|
importerOf.clear()
|
|
268
|
+
resolveExtensionCache.clear()
|
|
264
269
|
})
|
|
265
270
|
|
|
266
271
|
/*
|