@abide/abide 0.39.0 → 0.40.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/AGENTS.md +318 -146
- package/CHANGELOG.md +89 -0
- package/README.md +77 -79
- package/package.json +1 -1
- package/src/lib/server/rpc/parseArgs.ts +10 -1
- package/src/lib/server/rpc/runWithVerbTimeout.ts +7 -9
- package/src/lib/server/rpc/types/VerbHelper.ts +18 -21
- package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +8 -7
- package/src/lib/server/sockets/createSocketDispatcher.ts +12 -3
- package/src/lib/server/sockets/defineSocket.ts +3 -1
- package/src/lib/shared/REF_JSON_HEADER.ts +9 -0
- package/src/lib/shared/REF_JSON_TAGS.ts +31 -0
- package/src/lib/shared/assertExhaustive.ts +14 -0
- package/src/lib/shared/buildRpcRequest.ts +8 -1
- package/src/lib/shared/decodeRefJson.ts +110 -0
- package/src/lib/shared/encodeRefJson.ts +106 -0
- package/src/lib/test/createTestSocketChannel.ts +6 -2
- package/src/lib/ui/compile/TS_PRINTER.ts +7 -0
- package/src/lib/ui/compile/analyzeComponent.ts +15 -16
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +66 -0
- package/src/lib/ui/compile/assertTranspiles.ts +19 -0
- package/src/lib/ui/compile/compileModule.ts +56 -34
- package/src/lib/ui/compile/compileSSR.ts +1 -3
- package/src/lib/ui/compile/compileShadow.ts +14 -7
- package/src/lib/ui/compile/desugarSignals.ts +168 -107
- package/src/lib/ui/compile/generateBuild.ts +49 -59
- package/src/lib/ui/compile/generateSSR.ts +30 -21
- package/src/lib/ui/compile/hoistCells.ts +2 -2
- package/src/lib/ui/compile/isWhitespaceText.ts +11 -0
- package/src/lib/ui/compile/lowerContext.ts +23 -10
- package/src/lib/ui/compile/lowerDocAccess.ts +29 -3
- package/src/lib/ui/compile/lowerScript.ts +64 -0
- package/src/lib/ui/compile/parseTemplate.ts +74 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +160 -90
- package/src/lib/ui/compile/resolveBranches.ts +21 -0
- package/src/lib/ui/compile/stripEffects.ts +22 -17
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +12 -2
- package/src/lib/ui/createScope.ts +14 -0
- package/src/lib/ui/dom/appendText.ts +2 -3
- package/src/lib/ui/dom/appendTextAt.ts +2 -3
- package/src/lib/ui/dom/applyResolved.ts +5 -8
- package/src/lib/ui/dom/awaitBlock.ts +22 -2
- package/src/lib/ui/dom/each.ts +4 -0
- package/src/lib/ui/dom/on.ts +7 -0
- package/src/lib/ui/dom/parseRawNodes.ts +17 -0
- package/src/lib/ui/dom/switchBlock.ts +4 -0
- package/src/lib/ui/dom/when.ts +4 -0
- package/src/lib/ui/installInspectorBridge.ts +3 -1
- package/src/lib/ui/navigate.ts +28 -3
- package/src/lib/ui/renderToStream.ts +17 -9
- package/src/lib/ui/resumeSeedScript.ts +16 -6
- package/src/lib/ui/router.ts +108 -15
- package/src/lib/ui/runtime/NODE_STATE.ts +22 -0
- package/src/lib/ui/runtime/RESUME.ts +13 -6
- package/src/lib/ui/runtime/clientPage.ts +114 -9
- package/src/lib/ui/runtime/createComputedNode.ts +5 -3
- package/src/lib/ui/runtime/createEffectNode.ts +3 -1
- package/src/lib/ui/runtime/createSignalNode.ts +4 -2
- package/src/lib/ui/runtime/flushEffects.ts +8 -5
- package/src/lib/ui/runtime/historyEntries.ts +39 -1
- package/src/lib/ui/runtime/localStoragePersistence.ts +14 -8
- package/src/lib/ui/runtime/readNode.ts +8 -7
- package/src/lib/ui/runtime/runNode.ts +18 -2
- package/src/lib/ui/runtime/scope.ts +12 -1
- package/src/lib/ui/runtime/trigger.ts +40 -24
- package/src/lib/ui/runtime/types/ReactiveNode.ts +4 -1
- package/src/lib/ui/runtime/updateIfNecessary.ts +40 -0
- package/src/lib/ui/socketChannel.ts +5 -2
- package/src/lib/ui/tryEncodeResume.ts +20 -0
- package/src/lib/ui/types/Scope.ts +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.40.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - share a single TS printer across compile passes ([`0626e68`](https://github.com/briancray/abide/commit/0626e68500b6cc8e7e2c770184ec4aa0fd06106d))
|
|
8
|
+
|
|
9
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - bind-check generated modules and collect identifiers via AST ([`1e9d350`](https://github.com/briancray/abide/commit/1e9d3506873121ffd978039653f920edbed6c9ce))
|
|
10
|
+
|
|
11
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - value-memoised push-pull reactivity (CLEAN/CHECK/DIRTY) ([`2ae8f14`](https://github.com/briancray/abide/commit/2ae8f141da5873bea0eb31a9a59d81182ca5a12a))
|
|
12
|
+
|
|
13
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - syntax fuzz corpus for the identifier-rewrite passes ([`2e7d604`](https://github.com/briancray/abide/commit/2e7d6043aeeb9ca894bdb45f3f27893fbc5bf2f8))
|
|
14
|
+
|
|
15
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - build control-flow subtrees untracked ([`33c0d8e`](https://github.com/briancray/abide/commit/33c0d8e834e5145a84d872ff496df122bbb0da95))
|
|
16
|
+
|
|
17
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - lower the component script in a single parse ([`53098f8`](https://github.com/briancray/abide/commit/53098f8c2fe36e46e61b41d8a617aba6ac75e96f))
|
|
18
|
+
|
|
19
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - regenerate README and AGENTS surface map ([`6bb6a39`](https://github.com/briancray/abide/commit/6bb6a3906d5aba522a4fe33bffae0911adf0b0fe))
|
|
20
|
+
|
|
21
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - spread reactive page.params before postMessage in inspector bridge ([`7c99395`](https://github.com/briancray/abide/commit/7c993959e9dfc0c7f7038f694f95b59093a10dfc))
|
|
22
|
+
|
|
23
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - make signal-rewrite parent-independent ([`868717a`](https://github.com/briancray/abide/commit/868717a6f7ff824b24ef8a96e5e5f59add5ceed1))
|
|
24
|
+
|
|
25
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - pick runtime imports by tokenizing the output, not substring grep ([`a102986`](https://github.com/briancray/abide/commit/a102986bef90609297246ea001528a32e111be53))
|
|
26
|
+
|
|
27
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - re-apply saved scroll offset until async content grows the page ([`a813b47`](https://github.com/briancray/abide/commit/a813b47387289c0049e63997a72a6805e461b18c))
|
|
28
|
+
|
|
29
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - aliased import mangled when its name collides with a signal ([`abfe25d`](https://github.com/briancray/abide/commit/abfe25dcf7c00e61156b5d36fe82f290fc6c4cfb))
|
|
30
|
+
|
|
31
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - granular reactive page snapshot + in-place same-route navigation ([`b561eb5`](https://github.com/briancray/abide/commit/b561eb52439577d3a75a7872befee4b9f0e98267))
|
|
32
|
+
|
|
33
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - lower the component script in a literal single parse ([`b72b238`](https://github.com/briancray/abide/commit/b72b238377ff6545de549589d36b49ad4ecebb2d))
|
|
34
|
+
|
|
35
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - chain per-expression lowering over a single parse ([`d808317`](https://github.com/briancray/abide/commit/d808317d817ae8a414695b7bfb037b8aff7017c3))
|
|
36
|
+
|
|
37
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - classify signal-rewrite positions at visit time ([`d99577e`](https://github.com/briancray/abide/commit/d99577e2ac77d8742088608fba554ff5c2e799a4))
|
|
38
|
+
|
|
39
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - make template-node generators exhaustive ([`e0f246d`](https://github.com/briancray/abide/commit/e0f246dfbbf4ea4f2dc481fc543a6e9f0a6fa43b))
|
|
40
|
+
|
|
41
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - lower ++/-- on a doc path; guard the whole generated module ([`f49c122`](https://github.com/briancray/abide/commit/f49c122354fc4ef65f4c44db820d703078dd57f4))
|
|
42
|
+
|
|
43
|
+
- [`32d6cb9`](https://github.com/briancray/abide/commit/32d6cb97a9ce2ad63b3fd768c02379e708c6d599) - fold SSR effect-stripping into the single-parse chain ([`fa4515d`](https://github.com/briancray/abide/commit/fa4515d07164765ea0ee609cf1847535a41f95ed))
|
|
44
|
+
|
|
45
|
+
## 0.40.0
|
|
46
|
+
|
|
47
|
+
### Minor Changes
|
|
48
|
+
|
|
49
|
+
- [`05ea7e2`](https://github.com/briancray/abide/commit/05ea7e235b1e7c035d3b4313e12fc83733fa814b) - <template elseif> conditional chains ([`c3237cc`](https://github.com/briancray/abide/commit/c3237ccc0d222c36e8b00ce68595eb92609ef856))
|
|
50
|
+
|
|
51
|
+
- [`15c0ad3`](https://github.com/briancray/abide/commit/15c0ad334f698dfb49a9dcd0e0361925afc4f6c8) - `navigate` takes an options object — `navigate(path, { replace?, keepScroll? })` — replacing the old `navigate(path, replace?)` boolean. **Breaking:** a caller passing a boolean (`navigate(path, true)`) must migrate to `navigate(path, { replace: true })`; a bare `navigate(path)` is unchanged. The new `keepScroll` flag carries the live scroll offset onto the destination entry so an in-page URL swap — e.g. selecting another episode on the same detail page — restores to the current position instead of jumping to top. A push re-buckets the offset under the freshly minted entry id; a `replace` keeps the entry's bucket instead of discarding it.
|
|
52
|
+
|
|
53
|
+
- [`ba56bb2`](https://github.com/briancray/abide/commit/ba56bb28be06fa0a11ab57fb10fa2d6b75ab8656) - Add `share`/`shared` context to `scope()`. The reactive doc does not inherit down the scope tree (each scope owns a separate document), so passing a value from an ancestor to a descendant previously meant threading it through every layer as props. `scope().share(key, value)` now puts a named value on a scope's own side-map, and `scope().shared(key)` reads the closest ancestor (self included) that has the key — an existence-checked, non-tracking upward walk, returning `undefined` when no scope provides it. The value is held by reference and the lookup never subscribes, so reactive context is expressed by sharing a `cell` (or a scope) rather than a plain object — reactivity rides what you share, not the share itself. A shared `undefined` shadows an ancestor (the walk stops on `has`, not truthiness). The shared map is released on `dispose`.
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- [`a5a37f9`](https://github.com/briancray/abide/commit/a5a37f94258a56c4f24eb2b5539a280ce877bdfc) - extract resolveBranches for branch lookup ([`19a28a1`](https://github.com/briancray/abide/commit/19a28a1236e38a78c53a2adff0a981c8ad20b698))
|
|
58
|
+
|
|
59
|
+
- [`a5a37f9`](https://github.com/briancray/abide/commit/a5a37f94258a56c4f24eb2b5539a280ce877bdfc) - sync AGENTS.md surface map for scope share/shared and navigate options ([`3e41255`](https://github.com/briancray/abide/commit/3e4125516164c94cfe9c9e6baa35f9ff9ec9c3e0))
|
|
60
|
+
|
|
61
|
+
- [`a5a37f9`](https://github.com/briancray/abide/commit/a5a37f94258a56c4f24eb2b5539a280ce877bdfc) - use Promise.withResolvers in runWithVerbTimeout ([`504bdca`](https://github.com/briancray/abide/commit/504bdcac285cfd3762c8390fd1b6d640e62029bc))
|
|
62
|
+
|
|
63
|
+
- [`a5a37f9`](https://github.com/briancray/abide/commit/a5a37f94258a56c4f24eb2b5539a280ce877bdfc) - extract VerbBaseOpts for shared verb options ([`d7414ca`](https://github.com/briancray/abide/commit/d7414cab7a557b54395321026d818b9fba82ba34))
|
|
64
|
+
|
|
65
|
+
- [`ccc7f87`](https://github.com/briancray/abide/commit/ccc7f879d0506af084335785026200aeb38fdde4) - `on()` no longer attaches a non-function event handler. A component that omits an optional `on*` prop forwards it as `undefined`, yet the compiler still emits the `on()` call — so the listener fired `undefined(event)` and threw "handler is not a function" on every matching event (a keystroke or input on a search box, a click, …). `on()` now skips attaching when the handler is not a function, so an omitted optional handler is a no-op rather than a per-event crash.
|
|
66
|
+
|
|
67
|
+
- [`c29dcd2`](https://github.com/briancray/abide/commit/c29dcd2796b5caa3ee41857aa612322b1e4220fa) - Parse `html`-branded raw markup in the parent's namespace. The client-side raw-HTML
|
|
68
|
+
binding parsed markup by setting `innerHTML` on a throwaway `<div>`, so the fragment
|
|
69
|
+
parser always used the HTML namespace — markup bound inside an `<svg>` produced
|
|
70
|
+
HTML-namespaced `<path>`/`<circle>` elements that exist in the DOM but never render.
|
|
71
|
+
This only surfaced on client-created or re-rendered nodes; the initial SSR/hydration
|
|
72
|
+
path adopts the server markup verbatim and kept the correct namespace, so an icon
|
|
73
|
+
component (`{html(svgInnerMarkup)}` inside `<svg>`) rendered on first paint but
|
|
74
|
+
vanished the moment its subtree was rebuilt on the client (e.g. a live cache delta).
|
|
75
|
+
A new `parseRawNodes` helper picks an SVG holder when the parent is SVG-namespaced,
|
|
76
|
+
shared by both `appendText` and `appendTextAt`.
|
|
77
|
+
|
|
78
|
+
- [`9a74da6`](https://github.com/briancray/abide/commit/9a74da67c1f65cdb37e7a47af2e7b104cfe7620f) - Stop the signal-ref rewrite from descending into type space. `renameSignalRefs` rewrote every value-position identifier matching a signal name, but never skipped TypeScript type aliases, interfaces, or type annotations — so a component that destructures a prop (`const { option, ...rest } = props<Props>()`) and names that prop in its own type (`type Props = { option?: (value: T) => unknown; … }`) had the type member rewritten into a call (`option(): …`). This only surfaced once a `...rest` binding forced the named prop type to be emitted into the build (a no-rest `props<{…}>()` strips its type argument); with simple property types the mangle landed as accidentally-valid method-signature syntax, but optional/function/union members (`option?: (v) => unknown`, `value?: T | T[] | string`) became invalid TS and failed the page build. The visitor now leaves type aliases, interfaces, and any type node untouched — type space never holds a runtime read, and types erase at build regardless.
|
|
79
|
+
|
|
80
|
+
- [`92597f0`](https://github.com/briancray/abide/commit/92597f0cbabc1e44449c0f0f812048481c38c579) - RPC bodies and socket frames now ship as ref-json (cycles, shared references, and
|
|
81
|
+
the types JSON drops/coerces — `undefined`, `bigint`, `Date`, `Map`, `Set`, `NaN`,
|
|
82
|
+
`±Infinity`, `-0` — survive the wire), and the server still accepts ordinary JSON from
|
|
83
|
+
non-abide clients. abide's own client flags a ref-json body with the `abide-ref-json`
|
|
84
|
+
header so the server decodes it with the matching codec; a request without the header
|
|
85
|
+
(curl, an OpenAPI-generated SDK, a webhook) is read with plain `JSON.parse`, so the
|
|
86
|
+
documented HTTP/OpenAPI body contract keeps working. The header is the discriminator
|
|
87
|
+
rather than the payload shape because ref-json's `[root, slots]` envelope is ambiguous
|
|
88
|
+
with a legitimate plain-JSON two-element array body. Socket frames need no header — a
|
|
89
|
+
frame is always an object, never the envelope array, so a plain-JSON frame falls back
|
|
90
|
+
to `JSON.parse` unambiguously.
|
|
91
|
+
|
|
3
92
|
## 0.39.0
|
|
4
93
|
|
|
5
94
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,159 +1,157 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
-
**One typed
|
|
3
|
+
**One typed declaration fans out to HTTP, a CLI, an MCP tool, and an OpenAPI
|
|
4
|
+
spec — and the bundler swaps the runtime per side.**
|
|
4
5
|
|
|
5
|
-
abide is an isomorphic framework on Bun
|
|
6
|
+
abide is an isomorphic framework on Bun and web standards: you declare a verb,
|
|
7
|
+
a socket, or a component once, and the same callable runs server-side, in the
|
|
8
|
+
browser, from the terminal, and from an agent — the bundler decides which
|
|
9
|
+
runtime each side gets. Built for humans *and* machines.
|
|
6
10
|
|
|
7
|
-
- One dependency
|
|
8
|
-
|
|
11
|
+
- One direct dependency — the TypeScript compiler, used by `abide check`.
|
|
12
|
+
Tailwind is an optional peer. Everything runs on a single Bun runtime
|
|
13
|
+
(≥ 1.3.0); no second toolchain.
|
|
9
14
|
|
|
10
15
|
## Quick start
|
|
11
16
|
|
|
12
17
|
```sh
|
|
13
|
-
bunx abide scaffold my-app #
|
|
18
|
+
bunx abide scaffold my-app # copies the template, installs, starts dev
|
|
14
19
|
```
|
|
15
20
|
|
|
16
|
-
Or
|
|
21
|
+
Or clone the kitchen-sink and run it:
|
|
17
22
|
|
|
18
23
|
```sh
|
|
19
24
|
git clone https://github.com/briancray/abide
|
|
20
|
-
cd abide
|
|
21
|
-
|
|
25
|
+
cd abide/examples/kitchen-sink
|
|
26
|
+
bun install
|
|
27
|
+
bun run dev
|
|
22
28
|
```
|
|
23
29
|
|
|
24
|
-
The whole public surface — every export, CLI command, route, env var, and the `.abide` grammar — is mapped in [`AGENTS.md`](./AGENTS.md).
|
|
25
|
-
|
|
26
30
|
## RPCs
|
|
27
31
|
|
|
28
|
-
An RPC is one export per file under `src/server/rpc/`. The file path is the
|
|
32
|
+
An RPC is one export per file under `src/server/rpc/`. **The file path is the
|
|
33
|
+
URL.** The schema validates arguments and projects the MCP tool, the CLI flags,
|
|
34
|
+
and the OpenAPI operation. The contract is Standard Schema — zod, valibot, or
|
|
35
|
+
arktype, unadapted.
|
|
29
36
|
|
|
30
37
|
```ts
|
|
31
|
-
// src/server/rpc/getMessages.ts
|
|
38
|
+
// src/server/rpc/getMessages.ts — file path is the route: GET /getMessages
|
|
32
39
|
import { GET } from '@abide/abide/server/GET'
|
|
33
40
|
import { json } from '@abide/abide/server/json'
|
|
34
41
|
import { z } from 'zod'
|
|
35
|
-
import { history } from '../../store.ts'
|
|
36
42
|
|
|
37
43
|
const inputSchema = z.object({ room: z.string() })
|
|
38
44
|
|
|
39
|
-
export const getMessages = GET(({ room }) => json(
|
|
45
|
+
export const getMessages = GET(({ room }) => json(board(room)), { inputSchema })
|
|
40
46
|
```
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
One declaration, five surfaces:
|
|
43
49
|
|
|
44
50
|
```text
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
cache(fn)() fetch (read-only) command operation
|
|
51
|
+
getMessages = GET(fn, { inputSchema })
|
|
52
|
+
│
|
|
53
|
+
┌───────────┬─────────────┼────────────┬──────────────┐
|
|
54
|
+
SSR call browser fetch MCP tool CLI command OpenAPI op
|
|
55
|
+
cache(fn)() typed proxy() (read-only) abide ... cli /openapi.json
|
|
51
56
|
```
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
A schema unlocks the CLI everywhere and MCP for read-only verbs; a mutating
|
|
59
|
+
verb never auto-exposes to MCP — it needs explicit `clients: { mcp: true }`.
|
|
60
|
+
Consume the verb four ways: `cache(getMessages)({ room })` resolves in-process
|
|
61
|
+
during SSR, the same call hits a swapped `fetch` in the browser,
|
|
62
|
+
`getMessages.raw(args)` returns the undecoded `Response`, and
|
|
63
|
+
`getMessages.stream(args)` is an iterable view of the body.
|
|
54
64
|
|
|
55
|
-
> Query args travel as strings —
|
|
65
|
+
> Query args travel as strings — validate with `z.coerce.*`. The per-verb
|
|
66
|
+
> `timeout` (504 on every surface) is distinct from `ABIDE_CLIENT_TIMEOUT`.
|
|
56
67
|
|
|
57
68
|
## Sockets
|
|
58
69
|
|
|
59
|
-
A socket is one broadcast topic per file under `src/server/sockets/`. A
|
|
70
|
+
A socket is one broadcast topic per file under `src/server/sockets/`. A
|
|
71
|
+
`Socket<T>` is an isomorphic `AsyncIterable<T>`, and every socket multiplexes
|
|
72
|
+
onto one websocket at `/__abide/sockets`.
|
|
60
73
|
|
|
61
74
|
```ts
|
|
62
|
-
// src/server/sockets/messages.ts
|
|
75
|
+
// src/server/sockets/messages.ts — topic name = file name: `messages`
|
|
63
76
|
import { socket } from '@abide/abide/server/socket'
|
|
64
77
|
import { z } from 'zod'
|
|
65
78
|
|
|
66
|
-
const schema = z.object({
|
|
67
|
-
id: z.string(),
|
|
68
|
-
room: z.string(),
|
|
69
|
-
from: z.string(),
|
|
70
|
-
text: z.string(),
|
|
71
|
-
at: z.number(),
|
|
72
|
-
})
|
|
79
|
+
const schema = z.object({ id: z.string(), room: z.string(), from: z.string(), text: z.string() })
|
|
73
80
|
|
|
74
|
-
// retain the last
|
|
75
|
-
export const messages = socket({ schema, tail:
|
|
81
|
+
// retain the last 50 frames; evict any older than an hour
|
|
82
|
+
export const messages = socket({ schema, tail: 50, ttl: 3_600_000 })
|
|
76
83
|
export type Message = z.infer<typeof schema>
|
|
77
84
|
```
|
|
78
85
|
|
|
79
|
-
The
|
|
86
|
+
The socket also has an HTTP face at `/__abide/sockets/messages`, for clients
|
|
87
|
+
that can't speak the ws multiplex: `GET` returns the retained tail, `POST`
|
|
88
|
+
publishes — gated by `clientPublish` (off by default, so the POST 403s).
|
|
80
89
|
|
|
81
90
|
## Components
|
|
82
91
|
|
|
83
|
-
A
|
|
92
|
+
A `.abide` component is the payoff: it imports the verb and the socket above
|
|
93
|
+
and ties them together with the native `<template>` grammar. Reactive state is
|
|
94
|
+
reached only through `scope()` — `scope().state()` is writable,
|
|
95
|
+
`scope().computed()` is read-only. `props()` and `effect()` are in-scope, no
|
|
96
|
+
import.
|
|
84
97
|
|
|
85
98
|
```html
|
|
86
99
|
<script>
|
|
87
100
|
import { cache } from '@abide/abide/shared/cache'
|
|
88
101
|
import { tail } from '@abide/abide/ui/tail'
|
|
89
102
|
import { getMessages } from '$server/rpc/getMessages.ts'
|
|
90
|
-
import {
|
|
103
|
+
import { postMessage } from '$server/rpc/postMessage.ts'
|
|
91
104
|
import { messages } from '$server/sockets/messages.ts'
|
|
92
105
|
import Avatar from '$ui/Avatar.abide'
|
|
93
106
|
|
|
94
|
-
const { room } = props<{ room
|
|
107
|
+
const { room = 'lobby' } = props<{ room?: string }>()
|
|
108
|
+
|
|
109
|
+
// SSR-warm history; live frames then stream over the ws
|
|
110
|
+
const seed = cache(getMessages)({ room })
|
|
111
|
+
const live = scope().computed(() => tail(messages, { last: 50 }))
|
|
95
112
|
|
|
96
|
-
let from = scope().state('alice')
|
|
97
|
-
let
|
|
113
|
+
let from = scope().state('alice')
|
|
114
|
+
let text = scope().state('')
|
|
98
115
|
let pinned = scope().state(false)
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (draft.trim() === '') return
|
|
105
|
-
await sendMessage({ room, from, text: draft }) // a mutating verb
|
|
106
|
-
draft = ''
|
|
116
|
+
let sort = scope().state('newest')
|
|
117
|
+
|
|
118
|
+
async function send() {
|
|
119
|
+
await postMessage({ room, from, text })
|
|
120
|
+
text = ''
|
|
107
121
|
}
|
|
108
122
|
</script>
|
|
109
123
|
|
|
110
|
-
<!-- a named snippet: a reusable builder, rendered like a function -->
|
|
111
124
|
<template name="bubble" args={msg}>
|
|
112
|
-
<li><Avatar name={msg.from} /> <b>{msg.from}</b
|
|
125
|
+
<li class="flex gap-2"><Avatar name={msg.from} /> <b>{msg.from}</b> {msg.text}</li>
|
|
113
126
|
</template>
|
|
114
127
|
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
<input bind:value={from} />
|
|
119
|
-
<input bind:value={draft} placeholder="message" />
|
|
128
|
+
<form onsubmit={send} class="flex gap-2">
|
|
129
|
+
<input bind:value={from} class="border px-2" />
|
|
130
|
+
<input bind:value={text} placeholder="message" class="flex-1 border px-2" />
|
|
120
131
|
<label><input type="checkbox" bind:checked={pinned} /> pin</label>
|
|
121
|
-
<label><input type="radio" bind:group={
|
|
122
|
-
<
|
|
123
|
-
<button type="submit" disabled={draft.trim() === ''}>send</button>
|
|
132
|
+
<label><input type="radio" bind:group={sort} value="newest" /> newest</label>
|
|
133
|
+
<button disabled={!text} class="border px-3">send</button>
|
|
124
134
|
</form>
|
|
125
135
|
|
|
126
|
-
<template
|
|
127
|
-
<p class="
|
|
128
|
-
<template
|
|
129
|
-
<p>no messages yet</p>
|
|
130
|
-
</template>
|
|
131
|
-
</template>
|
|
132
|
-
|
|
133
|
-
<template switch={filter}>
|
|
134
|
-
<template case={'all'}><p>showing everyone</p></template>
|
|
135
|
-
<template case={'mine'}><p>showing {from}</p></template>
|
|
136
|
-
<template default><p>—</p></template>
|
|
136
|
+
<template switch={sort}>
|
|
137
|
+
<template case={'newest'}><p class="text-xs">newest first</p></template>
|
|
138
|
+
<template default><p class="text-xs">oldest first</p></template>
|
|
137
139
|
</template>
|
|
138
140
|
|
|
139
|
-
<template await={
|
|
140
|
-
<p>loading…</p>
|
|
141
|
+
<template await={seed}>
|
|
142
|
+
<p class="text-xs text-slate-500">loading…</p>
|
|
141
143
|
<template then="history">
|
|
142
|
-
<ul>
|
|
143
|
-
<template each={history
|
|
144
|
+
<ul class="mt-3 space-y-1">
|
|
145
|
+
<template each={live ?? history} as="msg" key="msg.id">
|
|
144
146
|
{bubble(msg)}
|
|
145
147
|
</template>
|
|
146
148
|
</ul>
|
|
147
149
|
</template>
|
|
148
|
-
<template catch="
|
|
149
|
-
<p>failed: {error.message}</p>
|
|
150
|
-
</template>
|
|
150
|
+
<template catch="err"><p class="text-rose-700">{err.message}</p></template>
|
|
151
151
|
</template>
|
|
152
152
|
|
|
153
153
|
<style>
|
|
154
|
-
.
|
|
155
|
-
font-weight: 600;
|
|
156
|
-
}
|
|
154
|
+
li { font-size: 0.875rem; }
|
|
157
155
|
</style>
|
|
158
156
|
```
|
|
159
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { carriesBodyArgs } from '../../shared/carriesBodyArgs.ts'
|
|
2
2
|
import { contentTypeOf } from '../../shared/contentTypeOf.ts'
|
|
3
|
+
import { decodeRefJson } from '../../shared/decodeRefJson.ts'
|
|
3
4
|
import { HttpError } from '../../shared/HttpError.ts'
|
|
5
|
+
import { REF_JSON_HEADER } from '../../shared/REF_JSON_HEADER.ts'
|
|
4
6
|
import type { HttpVerb } from '../../shared/types/HttpVerb.ts'
|
|
5
7
|
import { error } from '../error.ts'
|
|
6
8
|
import { requestContext } from '../runtime/requestContext.ts'
|
|
@@ -92,7 +94,14 @@ export async function parseArgs(
|
|
|
92
94
|
if (contentType.includes('application/json')) {
|
|
93
95
|
const text = await bounded.text()
|
|
94
96
|
if (text !== '') {
|
|
95
|
-
|
|
97
|
+
/* abide's own client flags ref-json with REF_JSON_HEADER (restores
|
|
98
|
+
cycles/shared refs JSON can't carry); a non-abide client (curl, an
|
|
99
|
+
OpenAPI SDK) omits it and sends ordinary JSON — read with plain
|
|
100
|
+
JSON.parse, since the ref-json envelope is ambiguous with a 2-element
|
|
101
|
+
array body. */
|
|
102
|
+
body = bounded.headers.has(REF_JSON_HEADER)
|
|
103
|
+
? decodeRefJson(text)
|
|
104
|
+
: JSON.parse(text)
|
|
96
105
|
}
|
|
97
106
|
} else if (
|
|
98
107
|
contentType.includes('application/x-www-form-urlencoded') ||
|
|
@@ -19,18 +19,16 @@ export function runWithVerbTimeout(
|
|
|
19
19
|
ms: number,
|
|
20
20
|
onTimeout: () => void,
|
|
21
21
|
): Promise<Response> {
|
|
22
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
23
22
|
let timedOut = false
|
|
24
|
-
const deadline =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
23
|
+
const deadline = Promise.withResolvers<Response>()
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
timedOut = true
|
|
26
|
+
onTimeout()
|
|
27
|
+
deadline.resolve(error(504, 'handler timeout'))
|
|
28
|
+
}, ms)
|
|
31
29
|
return (async () => {
|
|
32
30
|
try {
|
|
33
|
-
return await Promise.race([work, deadline])
|
|
31
|
+
return await Promise.race([work, deadline.promise])
|
|
34
32
|
} finally {
|
|
35
33
|
clearTimeout(timer)
|
|
36
34
|
if (timedOut) {
|
|
@@ -3,6 +3,21 @@ import type { RemoteFunction } from '../../../shared/types/RemoteFunction.ts'
|
|
|
3
3
|
import type { StandardSchemaV1 } from '../../../shared/types/StandardSchemaV1.ts'
|
|
4
4
|
import type { RemoteHandler } from './RemoteHandler.ts'
|
|
5
5
|
|
|
6
|
+
/*
|
|
7
|
+
Options every verb overload accepts: the OpenAPI 200 `outputSchema`, the
|
|
8
|
+
`clients` surface flags, the same-origin CSRF exemption (`crossOrigin`), the
|
|
9
|
+
pre-parse body-byte ceiling (`maxBodySize`), and the per-surface handler
|
|
10
|
+
`timeout` (ms). The schema-bearing overloads intersect this with their own
|
|
11
|
+
`inputSchema`/`filesSchema` members.
|
|
12
|
+
*/
|
|
13
|
+
type VerbBaseOpts = {
|
|
14
|
+
outputSchema?: StandardSchemaV1
|
|
15
|
+
clients?: Partial<ClientFlags>
|
|
16
|
+
crossOrigin?: boolean
|
|
17
|
+
maxBodySize?: number
|
|
18
|
+
timeout?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
/*
|
|
7
22
|
Shared signature for every verb helper (GET / POST / …). Three overloads:
|
|
8
23
|
|
|
@@ -50,36 +65,18 @@ export type VerbHelper = {
|
|
|
50
65
|
StandardSchemaV1.InferOutput<InputSchema> & StandardSchemaV1.InferOutput<FilesSchema>,
|
|
51
66
|
Return
|
|
52
67
|
>,
|
|
53
|
-
opts: {
|
|
68
|
+
opts: VerbBaseOpts & {
|
|
54
69
|
inputSchema: InputSchema
|
|
55
70
|
filesSchema: FilesSchema
|
|
56
|
-
outputSchema?: StandardSchemaV1
|
|
57
|
-
clients?: Partial<ClientFlags>
|
|
58
|
-
crossOrigin?: boolean
|
|
59
|
-
maxBodySize?: number
|
|
60
|
-
timeout?: number
|
|
61
71
|
},
|
|
62
72
|
): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return>
|
|
63
73
|
<Return = unknown, InputSchema extends StandardSchemaV1 = StandardSchemaV1>(
|
|
64
74
|
fn: RemoteHandler<StandardSchemaV1.InferOutput<InputSchema>, Return>,
|
|
65
|
-
opts: {
|
|
66
|
-
inputSchema: InputSchema
|
|
67
|
-
outputSchema?: StandardSchemaV1
|
|
68
|
-
clients?: Partial<ClientFlags>
|
|
69
|
-
crossOrigin?: boolean
|
|
70
|
-
maxBodySize?: number
|
|
71
|
-
timeout?: number
|
|
72
|
-
},
|
|
75
|
+
opts: VerbBaseOpts & { inputSchema: InputSchema },
|
|
73
76
|
): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return>
|
|
74
77
|
<Args = undefined, Return = unknown>(
|
|
75
78
|
fn: RemoteHandler<Args, Return>,
|
|
76
|
-
opts:
|
|
77
|
-
outputSchema?: StandardSchemaV1
|
|
78
|
-
clients?: Partial<ClientFlags>
|
|
79
|
-
crossOrigin?: boolean
|
|
80
|
-
maxBodySize?: number
|
|
81
|
-
timeout?: number
|
|
82
|
-
},
|
|
79
|
+
opts: VerbBaseOpts,
|
|
83
80
|
): RemoteFunction<Args, Return>
|
|
84
81
|
<Args = undefined, Return = unknown>(
|
|
85
82
|
fn: RemoteHandler<Args, Return>,
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
2
|
The tiny inline script the abide-ui SSR stream ships in <head>. For each streamed
|
|
3
3
|
`<abide-resolve data-id>` frame it reads the leading `<script type=application/json>`,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
the
|
|
9
|
-
|
|
4
|
+
stores its raw ref-json text into `window.__abideResume` (the resume manifest hydration
|
|
5
|
+
reads, decoding each entry at read time) and swaps the resolved markup into the matching
|
|
6
|
+
`<!--abide:await:ID-->…<!--/abide:await:ID-->` boundary — so the pending shell paints
|
|
7
|
+
instantly and each value lands as it arrives, before the client bundle even loads. It
|
|
8
|
+
stores the text undecoded precisely because it's vanilla and self-contained — no framework
|
|
9
|
+
runtime, hence no ref-json decoder; the bundle decodes on read. Minified to one line so it
|
|
10
|
+
inlines cheaply ahead of the document body.
|
|
10
11
|
*/
|
|
11
12
|
export const SSR_SWAP_SCRIPT =
|
|
12
13
|
"function __abideSwap(){var f=document.querySelector('abide-resolve');while(f){" +
|
|
13
14
|
"var id=f.getAttribute('data-id'),p=f.firstChild,w=document.createTreeWalker(document.body,NodeFilter.SHOW_COMMENT),o=null,c;" +
|
|
14
|
-
"if(p&&p.nodeName==='SCRIPT'){
|
|
15
|
+
"if(p&&p.nodeName==='SCRIPT'){(window.__abideResume=window.__abideResume||{})[id]=p.textContent||'';p.remove();}" +
|
|
15
16
|
"while((c=w.nextNode())){if(c.data==='abide:await:'+id){o=c;break;}}" +
|
|
16
17
|
"if(o){var n=o.nextSibling;while(n&&!(n.nodeType===8&&n.data==='/abide:await:'+id)){var x=n.nextSibling;n.remove();n=x;}" +
|
|
17
18
|
"while(f.firstChild){o.parentNode.insertBefore(f.firstChild,n);}}f.remove();f=document.querySelector('abide-resolve');}}"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ServerWebSocket } from 'bun'
|
|
2
2
|
import { abideLog } from '../../shared/abideLog.ts'
|
|
3
|
+
import { decodeRefJson } from '../../shared/decodeRefJson.ts'
|
|
4
|
+
import { encodeRefJson } from '../../shared/encodeRefJson.ts'
|
|
3
5
|
import { memoizeByKey } from '../../shared/memoizeByKey.ts'
|
|
4
6
|
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
5
7
|
import { error } from '../error.ts'
|
|
@@ -104,7 +106,7 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
104
106
|
if (ws.readyState !== WebSocket.OPEN) {
|
|
105
107
|
return
|
|
106
108
|
}
|
|
107
|
-
ws.send(
|
|
109
|
+
ws.send(encodeRefJson(frame))
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
function addSub(state: ConnectionState, name: string, sub: string): boolean {
|
|
@@ -312,11 +314,18 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
312
314
|
const text = typeof data === 'string' ? data : textDecoder.decode(data)
|
|
313
315
|
let frame: SocketClientFrame
|
|
314
316
|
try {
|
|
315
|
-
|
|
317
|
+
/* ref-json from abide's own client; a non-abide client sends a plain-JSON
|
|
318
|
+
frame, which decodeRefJson rejects (a frame is always an object, never the
|
|
319
|
+
`[root, slots]` envelope) — fall back to JSON.parse so raw WS clients work. */
|
|
320
|
+
try {
|
|
321
|
+
frame = decodeRefJson(text) as SocketClientFrame
|
|
322
|
+
} catch {
|
|
323
|
+
frame = JSON.parse(text) as SocketClientFrame
|
|
324
|
+
}
|
|
316
325
|
} catch {
|
|
317
326
|
return
|
|
318
327
|
}
|
|
319
|
-
//
|
|
328
|
+
// both codecs yield a value for `null`/`42`/`true` too; ignore anything that isn't a frame object.
|
|
320
329
|
if (typeof frame !== 'object' || frame === null) {
|
|
321
330
|
return
|
|
322
331
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createPushIterator } from '../../shared/createPushIterator.ts'
|
|
2
|
+
import { encodeRefJson } from '../../shared/encodeRefJson.ts'
|
|
2
3
|
import { resolveClientFlags } from '../../shared/resolveClientFlags.ts'
|
|
3
4
|
import { socketTapSlot } from '../../shared/socketTapSlot.ts'
|
|
4
5
|
import type { TailHooks } from '../../shared/types/TailHooks.ts'
|
|
@@ -123,7 +124,8 @@ export function defineSocket<T>(name: string, opts: SocketOptions = {}): Socket<
|
|
|
123
124
|
silently drift from this construction site (the dispatcher's `send` is already
|
|
124
125
|
typed; this was the last `msg` frame built through an unchecked JSON.stringify). */
|
|
125
126
|
const frame: SocketServerFrame = { type: 'msg', socket: name, message: validated }
|
|
126
|
-
|
|
127
|
+
// ref-json (matching the dispatcher + client) so a message graph with cycles/shared refs fans out intact.
|
|
128
|
+
server.publish(topic, encodeRefJson(frame))
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Request header the RPC client stamps (value '1') when an `application/json` body is
|
|
3
|
+
ref-json-encoded (encodeRefJson), so the server decodes with the matching codec.
|
|
4
|
+
Its presence is the unambiguous discriminator: ref-json's `[rootValue, slots]`
|
|
5
|
+
envelope collides with a legitimate plain-JSON 2-element array body, so the shape
|
|
6
|
+
alone can't be trusted. Absence means an ordinary JSON body from a non-abide client
|
|
7
|
+
(curl, an OpenAPI-generated SDK, a webhook) — parseArgs reads it with plain JSON.parse.
|
|
8
|
+
*/
|
|
9
|
+
export const REF_JSON_HEADER = 'abide-ref-json'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Value tags for the ref-json codec (encodeRefJson / decodeRefJson). The output is
|
|
3
|
+
a `[rootValue, slots]` pair: `slots` holds only the hoisted CONTAINERS (objects,
|
|
4
|
+
arrays, Maps, Sets) addressed by index; every other value is encoded INLINE at its
|
|
5
|
+
position — a bare JSON primitive, or one of these tagged arrays. Primitives never
|
|
6
|
+
get their own slot, so a primitive-heavy payload stays close to plain-JSON size and
|
|
7
|
+
speed. A tag is always the first element of a JSON array at a value position; bare
|
|
8
|
+
JSON values are literal primitives, and plain objects only ever appear AS slots
|
|
9
|
+
(hoisted), so user data — even an object keyed `~r` or an array shaped like a tag —
|
|
10
|
+
can't collide with these. Shared between encoder and decoder so the tokens can't drift.
|
|
11
|
+
*/
|
|
12
|
+
export const REF_JSON_TAGS = {
|
|
13
|
+
// ['~r', slotIndex] — reference to a hoisted container; breaks cycles, preserves shared identity.
|
|
14
|
+
REF: '~r',
|
|
15
|
+
// ['~a', ...values]
|
|
16
|
+
ARRAY: '~a',
|
|
17
|
+
// ['~m', [[keyValue, valValue], …]]
|
|
18
|
+
MAP: '~m',
|
|
19
|
+
// ['~s', [value, …]]
|
|
20
|
+
SET: '~s',
|
|
21
|
+
// ['~d', epochMs]
|
|
22
|
+
DATE: '~d',
|
|
23
|
+
// ['~x', source, flags]
|
|
24
|
+
REGEXP: '~x',
|
|
25
|
+
// ['~g', decimalString] — BigInt can't go through JSON natively.
|
|
26
|
+
BIGINT: '~g',
|
|
27
|
+
// ['~u'] — undefined (and functions/symbols, folded to it).
|
|
28
|
+
UNDEFINED: '~u',
|
|
29
|
+
// ['~n', token] — the numbers JSON flattens to null: 'NaN' | 'Infinity' | '-Infinity' | '-0'.
|
|
30
|
+
NUMBER: '~n',
|
|
31
|
+
} as const
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Exhaustiveness guard for a discriminated-union dispatch. When every variant is
|
|
3
|
+
handled, the fall-through value narrows to `never`, so passing it here type-checks;
|
|
4
|
+
adding a new variant without a branch makes this a COMPILE error AND, if it slips
|
|
5
|
+
past types, throws at runtime naming the variant — converting a silent mis-dispatch
|
|
6
|
+
(a new template-node kind routed to the wrong branch) into a loud failure. The
|
|
7
|
+
enumeration's completeness becomes the type checker's job instead of inspection's.
|
|
8
|
+
*/
|
|
9
|
+
export function assertExhaustive(value: never, context = 'variant'): never {
|
|
10
|
+
const kind = (value as { kind?: unknown })?.kind
|
|
11
|
+
throw new Error(
|
|
12
|
+
`[abide] non-exhaustive ${context}${kind === undefined ? '' : `: ${String(kind)}`}`,
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { carriesBodyArgs } from './carriesBodyArgs.ts'
|
|
2
|
+
import { encodeRefJson } from './encodeRefJson.ts'
|
|
2
3
|
import { queryStringFromArgs } from './queryStringFromArgs.ts'
|
|
4
|
+
import { REF_JSON_HEADER } from './REF_JSON_HEADER.ts'
|
|
3
5
|
import type { HttpVerb } from './types/HttpVerb.ts'
|
|
4
6
|
|
|
5
7
|
/*
|
|
@@ -49,11 +51,16 @@ export function buildRpcRequest({
|
|
|
49
51
|
body: args,
|
|
50
52
|
})
|
|
51
53
|
}
|
|
54
|
+
/* ref-json (still valid JSON, so the content-type holds) so an arg graph with
|
|
55
|
+
cycles or shared back-references reaches the handler instead of throwing here.
|
|
56
|
+
The REF_JSON_HEADER flags the encoding so parseArgs decodes it with the same
|
|
57
|
+
codec; a non-abide client omits it and parseArgs reads plain JSON.parse. */
|
|
52
58
|
requestHeaders.set('content-type', 'application/json')
|
|
59
|
+
requestHeaders.set(REF_JSON_HEADER, '1')
|
|
53
60
|
return new Request(new URL(url, baseUrl).href, {
|
|
54
61
|
method,
|
|
55
62
|
headers: requestHeaders,
|
|
56
|
-
body:
|
|
63
|
+
body: encodeRefJson(args),
|
|
57
64
|
})
|
|
58
65
|
}
|
|
59
66
|
|