@abide/abide 0.47.0 → 0.49.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 +26 -13
- package/CHANGELOG.md +53 -0
- package/README.md +9 -3
- package/package.json +3 -1
- package/src/buildCli.ts +3 -5
- package/src/buildDisconnected.ts +2 -3
- package/src/bundleApp.ts +2 -3
- package/src/compile.ts +2 -4
- package/src/lib/bundle/installDownloads.ts +13 -3
- package/src/lib/bundle/installMacMenu.ts +13 -3
- package/src/lib/cli/parseArgvForRpc.ts +36 -9
- package/src/lib/cli/tokenizeArgvFlags.ts +4 -2
- package/src/lib/mcp/createMcpResourceServer.ts +13 -2
- package/src/lib/mcp/toolResultFromResponse.ts +40 -12
- package/src/lib/server/rpc/parseArgs.ts +15 -1
- package/src/lib/server/rpc/runWithRpcTimeout.ts +12 -1
- package/src/lib/server/runtime/buildCacheSnapshot.ts +6 -6
- package/src/lib/server/runtime/finalizeResponse.ts +11 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -2
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorCacheSnapshot.ts +4 -4
- package/src/lib/server/runtime/types/InspectorContext.ts +1 -1
- package/src/lib/shared/buildArtifact.ts +17 -0
- package/src/lib/shared/buildRpcRequest.ts +6 -2
- package/src/lib/shared/cache.ts +66 -28
- package/src/lib/shared/cacheStores.ts +3 -3
- package/src/lib/shared/canonicalJson.ts +24 -1
- package/src/lib/shared/createChannelLog.ts +24 -0
- package/src/lib/shared/exitOnBuildFailure.ts +4 -3
- package/src/lib/shared/parseEnv.ts +17 -6
- package/src/lib/shared/refresh.ts +3 -3
- package/src/lib/shared/serializeEnv.ts +18 -6
- package/src/lib/shared/sharedCacheStore.ts +14 -0
- package/src/lib/shared/sharedCacheStoreSlot.ts +10 -0
- package/src/lib/shared/snippet.ts +11 -6
- package/src/lib/shared/types/CacheOptions.ts +15 -11
- package/src/lib/shared/types/CacheStats.ts +1 -1
- package/src/lib/shared/types/SmartReadOptions.ts +15 -11
- package/src/lib/shared/url.ts +5 -0
- package/src/lib/test/createTestApp.ts +6 -6
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/bindListenEvent.ts +5 -6
- package/src/lib/ui/compile/compileSSR.ts +1 -1
- package/src/lib/ui/compile/compileShadow.ts +121 -25
- package/src/lib/ui/compile/composeProps.ts +3 -2
- package/src/lib/ui/compile/desugarSignals.ts +1 -1
- package/src/lib/ui/compile/generateBuild.ts +23 -9
- package/src/lib/ui/compile/generateSSR.ts +49 -37
- package/src/lib/ui/compile/isWhitespaceText.ts +10 -2
- package/src/lib/ui/compile/lowerDocAccess.ts +39 -1
- package/src/lib/ui/compile/parseTemplate.ts +3 -1
- package/src/lib/ui/compile/renameSignalRefs.ts +5 -18
- package/src/lib/ui/compile/resolveReactiveExport.ts +4 -7
- package/src/lib/ui/compile/scopeCss.ts +27 -4
- package/src/lib/ui/dom/awaitBlock.ts +67 -29
- package/src/lib/ui/dom/discardBoundary.ts +24 -6
- package/src/lib/ui/dom/each.ts +15 -8
- package/src/lib/ui/dom/fillBefore.ts +6 -7
- package/src/lib/ui/dom/mergeProps.ts +1 -1
- package/src/lib/ui/dom/mountSlot.ts +1 -1
- package/src/lib/ui/dom/mutateDocArray.ts +38 -0
- package/src/lib/ui/dom/restProps.ts +2 -2
- package/src/lib/ui/dom/spreadProps.ts +3 -4
- package/src/lib/ui/dom/tryBlock.ts +10 -11
- package/src/lib/ui/dom/withScope.ts +7 -0
- package/src/lib/ui/history.ts +14 -7
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/props.ts +17 -0
- package/src/lib/ui/renderChain.ts +7 -3
- package/src/lib/ui/router.ts +23 -17
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +2 -2
- package/src/lib/ui/runtime/applyPatchToTree.ts +16 -3
- package/src/lib/ui/runtime/captureModelDoc.ts +22 -7
- package/src/lib/ui/runtime/createDoc.ts +28 -12
- package/src/lib/ui/runtime/flushEffects.ts +23 -1
- package/src/lib/ui/runtime/scope.ts +11 -0
- package/src/lib/ui/runtime/toTeardown.ts +10 -3
- package/src/lib/ui/runtime/types/UiProps.ts +6 -5
- package/src/lib/ui/runtime/withoutHydration.ts +22 -0
- package/src/lib/ui/startClient.ts +3 -3
- package/src/lib/ui/state.ts +9 -3
- package/src/serverEntry.ts +11 -9
- package/src/lib/shared/globalCacheStore.ts +0 -15
- package/src/lib/shared/globalCacheStoreSlot.ts +0 -12
package/AGENTS.md
CHANGED
|
@@ -92,12 +92,17 @@ validate as args, `File` parts validate against `filesSchema`.
|
|
|
92
92
|
|
|
93
93
|
**Consuming an rpc** — the bare call `fn(args, opts?)` IS the smart read:
|
|
94
94
|
cached, coalesced, reactive, stale-while-revalidate for replayable (GET/HEAD)
|
|
95
|
-
reads; the second arg takes `{ ttl, tags, throttle, debounce,
|
|
96
|
-
(retention and the refetch clock — not transport). `
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
reads; the second arg takes `{ ttl, tags, throttle, debounce, shared, n }`
|
|
96
|
+
(retention and the refetch clock — not transport). `ttl` defaults to `0` on
|
|
97
|
+
the server (coalesce-only — the request is the atomic unit, nothing is
|
|
98
|
+
retained past it) and `Infinity` on the client (retain until invalidate/
|
|
99
|
+
refresh). `shared: true` selects the process-level store instead of the
|
|
100
|
+
request-scoped default (server) — it does not by itself retain, pair it with
|
|
101
|
+
an explicit `ttl` (e.g. `{ shared: true, ttl: Infinity }`) to memoise across
|
|
102
|
+
requests, for an external endpoint, never per-user data; on the client it is
|
|
103
|
+
a no-op (one tab store). A read with no request in flight (e.g. a background
|
|
104
|
+
job) also resolves against the process-level store. During SSR the same call
|
|
105
|
+
resolves
|
|
101
106
|
in-process and its value is baked into the HTML so hydration starts warm —
|
|
102
107
|
there is no `cache()` wrapper; the bare call carries the caching. Around it:
|
|
103
108
|
`fn.raw(args, init?)` returns the raw `Response` (per-call transport options —
|
|
@@ -184,7 +189,7 @@ cells read/written through `.value`.
|
|
|
184
189
|
| `state.share(key, value)` / `state.shared(key)` | members of `state` | Put a named value on the ambient scope / read the closest ancestor's |
|
|
185
190
|
| `watch(source, handler)` | `@abide/abide/ui/watch` | The single reaction primitive (client-only, stripped from SSR). Sources: a bare thunk `watch(() => …)` (auto-tracked effect), a state cell, a cell array, a socket/stream (`handler(frame)` per frame with reconnect replay), an rpc (`watch(fn, args?, handler)` — runs the smart read, `handler(value)` on each change). Returns a scope-tied disposer |
|
|
186
191
|
| `html(str)` / `` html`…` `` | `@abide/abide/ui/html` | Brands trusted raw HTML so `{expr}` inserts nodes instead of escaped text; plain `{value}` always escapes |
|
|
187
|
-
| `props()` |
|
|
192
|
+
| `props()` | `@abide/abide/ui/props` | The prop reader, resolved by import binding (alias-safe) like `state`: `const { name = fallback, ...rest } = props()`; a page/layout's declared props are additive with its route-param shape. `children` is an ordinary declared prop (`const { children } = props<{ children: Snippet }>()`), not ambient |
|
|
188
193
|
|
|
189
194
|
Bindings and directives (the attribute kinds `readAttributes` parses):
|
|
190
195
|
|
|
@@ -216,9 +221,15 @@ error):
|
|
|
216
221
|
| `{#snippet name(args)}…{/snippet}` | Declares a reusable builder, called as an interpolation: `{name(args)}`; a snippet value passes through props like any other value |
|
|
217
222
|
|
|
218
223
|
Components are capitalised tags (`<Panel prop={x}>…</Panel>`); nested content
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
224
|
+
becomes the component's `children` prop — an ordinary declared prop of type
|
|
225
|
+
`Snippet`, read with `const { children } = props<{ children: Snippet }>()`
|
|
226
|
+
and called as `{children()}` — the single fill point (`{#if children}
|
|
227
|
+
{children()}{:else}…{/if}` for a fallback; there are no named slots and no
|
|
228
|
+
`<slot>` element). Slotted content (`<Panel>…</Panel>`) and an explicit
|
|
229
|
+
`children={aSnippet}` attribute set the same prop — slotted content rides in
|
|
230
|
+
as the trailing prop layer, so it wins over an explicit `children` attribute
|
|
231
|
+
on the same tag (`mergeProps`, last layer wins per key). A layout's
|
|
232
|
+
`{children()}` is the route outlet.
|
|
222
233
|
|
|
223
234
|
`<script>` and `<style>` are **not component-root-only**: either may sit
|
|
224
235
|
inside a control-flow branch, scoped to that branch. A nested `<script>`
|
|
@@ -228,8 +239,9 @@ there — imports live in the leading `<script>`). A **root** `<style>` is
|
|
|
228
239
|
component-scoped; a nested `<style>` scopes to its sibling subtree only.
|
|
229
240
|
|
|
230
241
|
Removed forms throw migration errors at parse time: the `<slot>` element (use
|
|
231
|
-
`
|
|
232
|
-
|
|
242
|
+
a declared `children: Snippet` prop, called `{children()}`), `<template
|
|
243
|
+
name>` snippets (use `{#snippet}`), and all `<template
|
|
244
|
+
if/each/await/switch/…>` control flow (use `{#…}` blocks).
|
|
233
245
|
|
|
234
246
|
## Server surface — `abide/server/*`
|
|
235
247
|
|
|
@@ -320,7 +332,7 @@ Probes report, never act — reading one opens no fetch and no stream.
|
|
|
320
332
|
|
|
321
333
|
### Templating — `@documentation templating`
|
|
322
334
|
|
|
323
|
-
- `@abide/abide/shared/snippet` — `snippet(payload)`: brands a snippet payload so a `{expr}` interpolation mounts it (client: a DOM builder; server: the rendered string); the compiler wraps `{#snippet}` bodies in this. Also exports the `Snippet<
|
|
335
|
+
- `@abide/abide/shared/snippet` — `snippet(payload)`: brands a snippet payload so a `{expr}` interpolation mounts it (client: a DOM builder; server: the rendered string); the compiler wraps `{#snippet}` bodies in this. Also exports the `Snippet<Args>` type — a callable `(...args: Args) => SnippetValue`, generic over its call arguments (`children` is `Snippet`, invoked `children()`; a row snippet is `Snippet<[Item]>`, invoked `row(item)`) — plus `SnippetValue` (the internal payload brand) and `snippetPayload(value)` (a branded value's payload, or undefined for plain values).
|
|
324
336
|
|
|
325
337
|
### Shared plumbing — `@documentation plumbing`
|
|
326
338
|
|
|
@@ -332,6 +344,7 @@ Probes report, never act — reading one opens no fetch and no stream.
|
|
|
332
344
|
|
|
333
345
|
- `@abide/abide/ui/state` — the `state` primitive: `state(initial, transform?)` writable cell, `state.computed(fn)` read-only derived, `state.linked(fn, transform?)` writable-reseeded, `state.share(key, value)` / `state.shared(key)` ambient context. In `.abide` files the compiler lowers reads/writes to plain variable syntax; in `.ts` the cell is read/written through `.value`.
|
|
334
346
|
- `@abide/abide/ui/watch` — `watch(source, handler)`: the single reaction primitive over a thunk, cell, cell array, socket/stream, or rpc (see the grammar table). Client-only; the compiler strips author calls from SSR, and the `socket.watch` / `fn.watch` instance sugar is SSR-inert.
|
|
347
|
+
- `@abide/abide/ui/props` — `props<T>()`: the prop reader, resolved by import binding (alias-safe) like `state`; a required import — there is no ambient `props()`. Destructure declared props off it (`const { name, ...rest } = props<T>()`); a page/layout's declared `T` is additive with its auto-typed route-param shape. `children` is an ordinary declared prop, not ambient: `const { children } = props<{ children: Snippet }>()` (`Snippet` from `@abide/abide/shared/snippet`).
|
|
335
348
|
|
|
336
349
|
### Templating — `@documentation templating`
|
|
337
350
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.49.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- dfb4f13: props import required + additive route-shape type; drop ambient children ([`0dbe248`](https://github.com/briancray/abide/commit/0dbe248fc959f3ca8168fb83e6f30619a17eff3b))
|
|
8
|
+
- dfb4f13: slotted content rides the children prop key as a Snippet ([`0de19e5`](https://github.com/briancray/abide/commit/0de19e594dabb6ddd92b08d5e473f13296f17247))
|
|
9
|
+
- dfb4f13: publish abide/ui/props module for the props reader ([`197c1a9`](https://github.com/briancray/abide/commit/197c1a900c0ea81b40d1cbb4a6bec1e021b9e932))
|
|
10
|
+
- dfb4f13: type element attach node param from the element tag ([`444ef35`](https://github.com/briancray/abide/commit/444ef356372f8fc39a0b5786bca2f70dcf750c53))
|
|
11
|
+
- 951e90b: `props` is now a required import (`@abide/abide/ui/props`), resolved by import binding like `state` — a bare ambient `props()` no longer works, and every component that reads props must `import { props } from '@abide/abide/ui/props'`.
|
|
12
|
+
|
|
13
|
+
`children` is now an ordinary declared prop instead of an ambient callable: read it with `const { children } = props<{ children: Snippet }>()` (`Snippet` from `@abide/abide/shared/snippet`). Slotted content (`<Panel>…</Panel>`) and an explicit `children={aSnippet}` attribute now set the same `children` prop key (slotted content wins if both are present). `props<T>()` stays additive — a page/layout still gets its route-param shape auto-typed, and declaring `children` (or any other prop) only adds to it.
|
|
14
|
+
|
|
15
|
+
The `Snippet` type's generic changed from its internal payload to its call arguments: `Snippet<Args extends unknown[] = []>` is now `(...args: Args) => SnippetValue` — `children` is `Snippet` (invoked `children()`), a row/label snippet is `Snippet<[Item]>` (invoked `row(item)`). The former payload-generic form (`Snippet<Payload>` describing the value flowing through) is gone; the internal payload is the newly exported `SnippetValue`.
|
|
16
|
+
|
|
17
|
+
**Behavior change:** an unguarded `{children()}` on a component mounted without children now throws at render, where it previously rendered nothing. Guard with `{#if children}{children()}{:else}…{/if}`, or declare `children: Snippet` as a required (non-optional) prop so `abide check` catches missing children at the call site.
|
|
18
|
+
|
|
19
|
+
Migration: add `import { props } from '@abide/abide/ui/props'` to every component that calls `props()`; for any component that reads `children`, add `import type { Snippet } from '@abide/abide/shared/snippet'` and declare it — `const { children } = props<{ children: Snippet }>()` (or `children?: Snippet` if optional).
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 4a6bf93: mount a reactive/loop-bound component tag by its value, not the cell ([`32a7cb8`](https://github.com/briancray/abide/commit/32a7cb8bdef0800d76b418eefbe550b0a939cd73))
|
|
24
|
+
- dfb4f13: correct UiProps.children to the ordinary prop-thunk shape ([`33a0de7`](https://github.com/briancray/abide/commit/33a0de72dc2fe6ce4a0e025e4158ab192e47f1f0))
|
|
25
|
+
- c7d25e4: type state<T>(undefined) and binding-annotated state() as T | undefined ([`3c40d99`](https://github.com/briancray/abide/commit/3c40d9981617968330a00bc2f034456b80365419))
|
|
26
|
+
- c7d25e4: await a snippet whose body fills a {children()} slot ([`4c2744b`](https://github.com/briancray/abide/commit/4c2744bc1aa42728a80adb3426a77c02c79b30d4))
|
|
27
|
+
- dfb4f13: repair pre-existing gate failures blocking release ([`4c55a4a`](https://github.com/briancray/abide/commit/4c55a4a1cbcb0b2afc842f7cc7134aed590006ce))
|
|
28
|
+
- dfb4f13: layout child sentinel + restProps skip move to the children key ([`500d1d8`](https://github.com/briancray/abide/commit/500d1d8d696147602eb1ce34706cbbbf724ceb0b))
|
|
29
|
+
- dfb4f13: fix stale props-declaration comment in compileShadow ([`532e079`](https://github.com/briancray/abide/commit/532e0796c54aa77c7b155145ae2d16193b7de4e5))
|
|
30
|
+
- c7d25e4: expand a thrown Bun.build AggregateError into per-file diagnostics ([`5643d95`](https://github.com/briancray/abide/commit/5643d95691dd9211bc5f3edb5a2ae77aa3989f63))
|
|
31
|
+
- dfb4f13: children lowers as a destructured prop, not $children ([`60bbbeb`](https://github.com/briancray/abide/commit/60bbbeb15cae41d34e5f5b4f07590bf0a5a1b795))
|
|
32
|
+
- dfb4f13: resolve 35 correctness/ergonomics findings from full-tree audit ([`73a72ac`](https://github.com/briancray/abide/commit/73a72ac1946f901f76d790b38fa3ebbf29b661fe))
|
|
33
|
+
- dfb4f13: extract buildArtifact (build-or-die primitive) ([`938e312`](https://github.com/briancray/abide/commit/938e3129f03cb6787624703422cd9472fbdcb83b))
|
|
34
|
+
- dfb4f13: format spreadProps + renderChain to canonical prettier ([`97b1565`](https://github.com/briancray/abide/commit/97b1565670eb0977b8079425f8e92becd6deb68e))
|
|
35
|
+
- dfb4f13: thunk-wrap layout children sentinel so {#if children} presence reads truthy ([`a884de1`](https://github.com/briancray/abide/commit/a884de1bbb0fcffaea91268cf49aa1d1b24a3d7a))
|
|
36
|
+
- 4a6bf93: drop an await settle whose anchor was detached from the tree ([`ad3d00c`](https://github.com/briancray/abide/commit/ad3d00c36ecfd935721164e7ebff7a7b3fffca4e))
|
|
37
|
+
- dfb4f13: resolve props by import binding, not ambient special-case ([`b624893`](https://github.com/briancray/abide/commit/b624893745aa0e1fdb407047b15f29ac0ceac3ec))
|
|
38
|
+
- dfb4f13: shadow declares props under the author's local alias ([`b821a5b`](https://github.com/briancray/abide/commit/b821a5bfef17233b2aa24502ea397eede62329a0))
|
|
39
|
+
- dfb4f13: extract withoutHydration (5 sites -> 1 helper) ([`c36be09`](https://github.com/briancray/abide/commit/c36be09d64846f76216bfcc7932cdbc026631f04))
|
|
40
|
+
- dfb4f13: redefine Snippet<Args> as a callable; add SnippetValue ([`ecc861f`](https://github.com/briancray/abide/commit/ecc861f589d5149a299b92ba6d08fd362644bcd1))
|
|
41
|
+
|
|
42
|
+
## 0.48.0
|
|
43
|
+
|
|
44
|
+
### Minor Changes
|
|
45
|
+
|
|
46
|
+
- 182cdf5: server reads coalesce-only by default, SWR retain client-only ([`98a3e84`](https://github.com/briancray/abide/commit/98a3e84b1e6bdb50b8950a4197ec4a255c560d2a))
|
|
47
|
+
- 182cdf5: warn on a request-scoped ttl>0 read (add shared or drop ttl) ([`b55425c`](https://github.com/briancray/abide/commit/b55425c986fb4c7cebcb1e8c29d1c616466d7d63))
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- 182cdf5: rename global option/exports/comments to shared ([`1256934`](https://github.com/briancray/abide/commit/125693481ba23152a44375bbe200d575b66fdf9a))
|
|
52
|
+
- 182cdf5: evict outside-request coalesce-only entries; correct shared help copy ([`1b73e21`](https://github.com/briancray/abide/commit/1b73e21c33c8ee985f7109fdedcbe302322ce7d1))
|
|
53
|
+
- 182cdf5: route outside-a-request server reads to the shared store ([`456cfa6`](https://github.com/briancray/abide/commit/456cfa662405714df99545e02ed6e3b851da84df))
|
|
54
|
+
- 182cdf5: Breaking: the `global` cache option is renamed `shared`, and it no longer implies retention. `{ global: true }` (memoise across requests forever) becomes `{ shared: true, ttl: Infinity }` — `shared` now only selects the process store, while `ttl` alone controls retention (default `0` on the server = coalesce-only, `Infinity` on the client). Reads made with no request in flight resolve to the shared store instead of an orphan fallback. ([`d94f0f9`](https://github.com/briancray/abide/commit/d94f0f9cb15a5822b2d5824db7e32da27368fe04))
|
|
55
|
+
|
|
3
56
|
## 0.47.0
|
|
4
57
|
|
|
5
58
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -130,11 +130,16 @@ component you read and write state as plain variables. The same file renders on
|
|
|
130
130
|
the server — blocking awaits inline, streaming awaits out of order, cached
|
|
131
131
|
reads seeded warm — and hydrates in the browser.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
Nested content becomes the component's `children` prop — an ordinary declared
|
|
134
|
+
prop of type `Snippet` — and renders wherever the component calls
|
|
135
|
+
`{children()}`:
|
|
134
136
|
|
|
135
137
|
```html
|
|
136
138
|
<script>
|
|
137
|
-
|
|
139
|
+
import { props } from '@abide/abide/ui/props'
|
|
140
|
+
import type { Snippet } from '@abide/abide/shared/snippet'
|
|
141
|
+
|
|
142
|
+
const { title, children } = props<{ title: string; children?: Snippet }>()
|
|
138
143
|
</script>
|
|
139
144
|
|
|
140
145
|
<section>
|
|
@@ -160,9 +165,10 @@ template grammar:
|
|
|
160
165
|
import { state } from '@abide/abide/ui/state'
|
|
161
166
|
import { watch } from '@abide/abide/ui/watch'
|
|
162
167
|
import { html } from '@abide/abide/ui/html'
|
|
168
|
+
import { props } from '@abide/abide/ui/props'
|
|
163
169
|
import Panel from '$ui/components/Panel.abide'
|
|
164
170
|
|
|
165
|
-
const { room = 'lobby' } = props()
|
|
171
|
+
const { room = 'lobby' } = props<{ room?: string }>()
|
|
166
172
|
|
|
167
173
|
let draft = state('')
|
|
168
174
|
let author = state('anon')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abide/abide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.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",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"./shared/createSubscriber": "./src/lib/shared/createSubscriber.ts",
|
|
78
78
|
"./ui/html": "./src/lib/ui/html.ts",
|
|
79
79
|
"./ui/state": "./src/lib/ui/state.ts",
|
|
80
|
+
"./ui/props": "./src/lib/ui/props.ts",
|
|
80
81
|
"./ui/effect": "./src/lib/ui/effect.ts",
|
|
81
82
|
"./ui/currentScope": "./src/lib/ui/scope.ts",
|
|
82
83
|
"./ui/enterRenderScope": "./src/lib/ui/enterScope.ts",
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"./ui/dom/restProps": "./src/lib/ui/dom/restProps.ts",
|
|
90
91
|
"./ui/dom/spreadAttrs": "./src/lib/ui/dom/spreadAttrs.ts",
|
|
91
92
|
"./ui/dom/readCall": "./src/lib/ui/dom/readCall.ts",
|
|
93
|
+
"./ui/dom/mutateDocArray": "./src/lib/ui/dom/mutateDocArray.ts",
|
|
92
94
|
"./ui/dom/hydrate": "./src/lib/ui/dom/hydrate.ts",
|
|
93
95
|
"./ui/dom/text": "./src/lib/ui/dom/text.ts",
|
|
94
96
|
"./ui/dom/appendText": "./src/lib/ui/dom/appendText.ts",
|
package/src/buildCli.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { dirname, join } from 'node:path'
|
|
|
2
2
|
import { build } from './build.ts'
|
|
3
3
|
import { compile } from './compile.ts'
|
|
4
4
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
5
|
+
import { buildArtifact } from './lib/shared/buildArtifact.ts'
|
|
5
6
|
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
6
7
|
import { exeSuffix } from './lib/shared/exeSuffix.ts'
|
|
7
|
-
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
8
8
|
import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
|
|
9
9
|
import { readPackageJson } from './lib/shared/readPackageJson.ts'
|
|
10
10
|
import type { CompileTarget } from './lib/shared/types/CompileTarget.ts'
|
|
@@ -56,14 +56,13 @@ export async function buildCli({
|
|
|
56
56
|
stdout. We don't `bun build --compile` here because the discovery output is
|
|
57
57
|
throwaway; a plain JS bundle runs faster. Additive — does not clear dist.
|
|
58
58
|
*/
|
|
59
|
-
|
|
59
|
+
await buildArtifact({
|
|
60
60
|
entrypoints: [DISCOVERY_ENTRY],
|
|
61
61
|
target: 'bun',
|
|
62
62
|
outdir: distDir,
|
|
63
63
|
naming: '_discovery.js',
|
|
64
64
|
plugins,
|
|
65
65
|
})
|
|
66
|
-
exitOnBuildFailure(discoveryResult)
|
|
67
66
|
|
|
68
67
|
const proc = Bun.spawn({
|
|
69
68
|
cmd: ['bun', discoveryOut],
|
|
@@ -94,13 +93,12 @@ export async function buildCli({
|
|
|
94
93
|
async function buildTargetPair(platformTarget: CompileTarget, cliOut: string): Promise<string> {
|
|
95
94
|
const serverOut = join(dirname(cliOut), `server${exeSuffix(platformTarget)}`)
|
|
96
95
|
await compile({ cwd, target: platformTarget, outfile: serverOut, buildClient: false })
|
|
97
|
-
|
|
96
|
+
await buildArtifact({
|
|
98
97
|
entrypoints: [CLI_ENTRY],
|
|
99
98
|
target: 'bun',
|
|
100
99
|
compile: { target: platformTarget, outfile: cliOut },
|
|
101
100
|
plugins,
|
|
102
101
|
})
|
|
103
|
-
exitOnBuildFailure(result)
|
|
104
102
|
abideLog.success(`compiled cli + server: ${cliOut}`)
|
|
105
103
|
return cliOut
|
|
106
104
|
}
|
package/src/buildDisconnected.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clientBuildPlugins } from './clientBuildPlugins.ts'
|
|
2
2
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { buildArtifact } from './lib/shared/buildArtifact.ts'
|
|
4
4
|
|
|
5
5
|
const ENTRY = new URL('./bundleDisconnectedEntry.ts', import.meta.url).pathname
|
|
6
6
|
const CSS_ENTRY = new URL('./lib/bundle/disconnected.css', import.meta.url).pathname
|
|
@@ -47,7 +47,7 @@ export async function buildDisconnected({
|
|
|
47
47
|
the entry (which TS can't type), so it compiles to a standalone artifact we
|
|
48
48
|
inline alongside the JS — the component carries no <style> of its own.
|
|
49
49
|
*/
|
|
50
|
-
const result = await
|
|
50
|
+
const result = await buildArtifact({
|
|
51
51
|
entrypoints: [ENTRY, CSS_ENTRY],
|
|
52
52
|
target: 'browser',
|
|
53
53
|
minify: true,
|
|
@@ -55,7 +55,6 @@ export async function buildDisconnected({
|
|
|
55
55
|
define: { __ABIDE_DEV__: 'false' },
|
|
56
56
|
plugins,
|
|
57
57
|
})
|
|
58
|
-
exitOnBuildFailure(result)
|
|
59
58
|
|
|
60
59
|
// Collect the JS bundle + extracted CSS from the in-memory artifacts.
|
|
61
60
|
let js = ''
|
package/src/bundleApp.ts
CHANGED
|
@@ -8,9 +8,9 @@ import { serverBinaryFilename } from './lib/bundle/serverBinaryFilename.ts'
|
|
|
8
8
|
import { signMacApp } from './lib/bundle/signMacApp.ts'
|
|
9
9
|
import { webviewLibName } from './lib/bundle/webviewLibName.ts'
|
|
10
10
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
11
|
+
import { buildArtifact } from './lib/shared/buildArtifact.ts'
|
|
11
12
|
import { bundleLayout } from './lib/shared/bundleLayout.ts'
|
|
12
13
|
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
13
|
-
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
14
14
|
import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
|
|
15
15
|
import { readPackageJson } from './lib/shared/readPackageJson.ts'
|
|
16
16
|
import { serverBuildPlugins } from './serverBuildPlugins.ts'
|
|
@@ -82,7 +82,7 @@ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}):
|
|
|
82
82
|
// 3. Launcher binary — named after the program so CFBundleExecutable matches.
|
|
83
83
|
const launcherSuffix = target.includes('windows') ? '.exe' : ''
|
|
84
84
|
const launcherPath = `${binDir}/${programName}${launcherSuffix}`
|
|
85
|
-
|
|
85
|
+
await buildArtifact({
|
|
86
86
|
entrypoints: [APP_ENTRY],
|
|
87
87
|
target: 'bun',
|
|
88
88
|
compile: { target, outfile: launcherPath },
|
|
@@ -96,7 +96,6 @@ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}):
|
|
|
96
96
|
*/
|
|
97
97
|
define: { __ABIDE_WORKER_ENTRY__: JSON.stringify(WORKER_ENTRY) },
|
|
98
98
|
})
|
|
99
|
-
exitOnBuildFailure(launcherResult)
|
|
100
99
|
|
|
101
100
|
// 4. Webview lib — built from the vendored source if needed, then copied
|
|
102
101
|
// beside the binaries (or into Frameworks on macOS) so the bundle is self-contained.
|
package/src/compile.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { build } from './build.ts'
|
|
2
2
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
3
|
+
import { buildArtifact } from './lib/shared/buildArtifact.ts'
|
|
3
4
|
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
4
5
|
import { exeSuffix } from './lib/shared/exeSuffix.ts'
|
|
5
|
-
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
6
6
|
import type { CompileTarget } from './lib/shared/types/CompileTarget.ts'
|
|
7
7
|
import { serverBuildPlugins } from './serverBuildPlugins.ts'
|
|
8
8
|
|
|
@@ -40,7 +40,7 @@ export async function compile({
|
|
|
40
40
|
|
|
41
41
|
const outPath = outfile ?? `${cwd}/dist/app${exeSuffix(target)}`
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
await buildArtifact({
|
|
44
44
|
entrypoints: [SERVER_ENTRY],
|
|
45
45
|
target: 'bun',
|
|
46
46
|
format: 'esm',
|
|
@@ -56,8 +56,6 @@ export async function compile({
|
|
|
56
56
|
plugins: serverBuildPlugins({ cwd, embedAssets: true }),
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
exitOnBuildFailure(result)
|
|
60
|
-
|
|
61
59
|
abideLog.success(`compiled standalone binary: ${outPath} (target: ${target})`)
|
|
62
60
|
return outPath
|
|
63
61
|
}
|
|
@@ -16,9 +16,19 @@ export function installDownloads(libPath: string, webviewHandle: Pointer | null)
|
|
|
16
16
|
if (process.platform !== 'darwin') {
|
|
17
17
|
return
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/* abide_install_downloads exists only in abide's own webview lib. A vanilla
|
|
20
|
+
ABIDE_WEBVIEW_LIB on darwin lacks it, so dlopen throws — degrade to a no-op
|
|
21
|
+
(downloads simply unhandled) instead of crashing the launcher, matching the
|
|
22
|
+
optional-symbol FFI binders. */
|
|
23
|
+
let symbols: { abide_install_downloads: (handle: Pointer | null) => void }
|
|
24
|
+
let close: () => void
|
|
25
|
+
try {
|
|
26
|
+
;({ symbols, close } = dlopen(libPath, {
|
|
27
|
+
abide_install_downloads: { args: [FFIType.ptr], returns: FFIType.void },
|
|
28
|
+
}))
|
|
29
|
+
} catch {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
22
32
|
symbols.abide_install_downloads(webviewHandle)
|
|
23
33
|
close()
|
|
24
34
|
}
|
|
@@ -30,9 +30,19 @@ export function installMacMenu(
|
|
|
30
30
|
if (process.platform !== 'darwin') {
|
|
31
31
|
return
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/* The abide_install_app_menu symbol is compiled only into abide's own webview lib.
|
|
34
|
+
A user who points ABIDE_WEBVIEW_LIB at a vanilla webview.dylib is on darwin but
|
|
35
|
+
lacks the symbol, so dlopen throws — degrade to a no-op (native menu simply absent)
|
|
36
|
+
rather than crashing the launcher, matching bindConnectedFlag/bindRequestNavigate. */
|
|
37
|
+
let symbols: { abide_install_app_menu: (handle: Pointer | null, config: Uint8Array) => void }
|
|
38
|
+
let close: () => void
|
|
39
|
+
try {
|
|
40
|
+
;({ symbols, close } = dlopen(libPath, {
|
|
41
|
+
abide_install_app_menu: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
|
42
|
+
}))
|
|
43
|
+
} catch {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
36
46
|
const config = JSON.stringify({ appName, fileMenu, menu })
|
|
37
47
|
symbols.abide_install_app_menu(webviewHandle, new TextEncoder().encode(`${config}\0`))
|
|
38
48
|
close()
|
|
@@ -74,8 +74,36 @@ export async function parseArgvForRpc(
|
|
|
74
74
|
}
|
|
75
75
|
const name = token.name
|
|
76
76
|
const propType = properties[name]?.type
|
|
77
|
+
/* Coerce one string token to a schema scalar type (or throw). Shared by scalar
|
|
78
|
+
props and array ELEMENTS so `--tags 1 --tags 2` on `z.array(z.number())` yields
|
|
79
|
+
numbers, not strings the server then rejects. */
|
|
80
|
+
const coerceScalar = (raw: string, type: string | undefined): unknown => {
|
|
81
|
+
if (type === 'number' || type === 'integer') {
|
|
82
|
+
// Reject a blank value explicitly — `Number('')` / `Number(' ')` is 0,
|
|
83
|
+
// not NaN, so the NaN guard alone would silently coerce it to zero.
|
|
84
|
+
const n = raw.trim() === '' ? Number.NaN : Number(raw)
|
|
85
|
+
if (Number.isNaN(n)) {
|
|
86
|
+
throw new Error(`--${name} expects a number, got ${raw}`)
|
|
87
|
+
}
|
|
88
|
+
return n
|
|
89
|
+
}
|
|
90
|
+
if (type === 'boolean') {
|
|
91
|
+
const lowered = raw.trim().toLowerCase()
|
|
92
|
+
if (lowered === 'true' || lowered === '1') {
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
if (lowered === 'false' || lowered === '0') {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`--${name} expects true or false, got ${raw}`)
|
|
99
|
+
}
|
|
100
|
+
return raw
|
|
101
|
+
}
|
|
77
102
|
if (propType === 'boolean') {
|
|
78
|
-
|
|
103
|
+
/* Bare `--flag` / `--no-flag` toggles; inline `--flag=false` honours the RHS
|
|
104
|
+
instead of always resolving to true. */
|
|
105
|
+
args[name] =
|
|
106
|
+
token.value !== undefined ? coerceScalar(token.value, 'boolean') : !token.negated
|
|
79
107
|
continue
|
|
80
108
|
}
|
|
81
109
|
if (token.missingValue || token.value === undefined) {
|
|
@@ -83,18 +111,17 @@ export async function parseArgvForRpc(
|
|
|
83
111
|
}
|
|
84
112
|
const value = token.value
|
|
85
113
|
if (propType === 'number' || propType === 'integer') {
|
|
86
|
-
|
|
87
|
-
// not NaN, so the NaN guard alone would silently coerce it to zero.
|
|
88
|
-
const n = value.trim() === '' ? Number.NaN : Number(value)
|
|
89
|
-
if (Number.isNaN(n)) {
|
|
90
|
-
throw new Error(`--${name} expects a number, got ${value}`)
|
|
91
|
-
}
|
|
92
|
-
args[name] = n
|
|
114
|
+
args[name] = coerceScalar(value, propType)
|
|
93
115
|
continue
|
|
94
116
|
}
|
|
95
117
|
if (propType === 'array') {
|
|
118
|
+
/* Coerce each repeated element per the schema's `items.type` — a string-only
|
|
119
|
+
array arg silently fails server-side Zod validation for numeric/boolean items. */
|
|
120
|
+
const itemType = (properties[name] as { items?: { type?: string } } | undefined)?.items
|
|
121
|
+
?.type
|
|
122
|
+
const element = coerceScalar(value, itemType)
|
|
96
123
|
const existing = args[name]
|
|
97
|
-
args[name] = Array.isArray(existing) ? [...existing,
|
|
124
|
+
args[name] = Array.isArray(existing) ? [...existing, element] : [element]
|
|
98
125
|
continue
|
|
99
126
|
}
|
|
100
127
|
args[name] = value
|
|
@@ -64,9 +64,11 @@ export function* tokenizeArgvFlags(
|
|
|
64
64
|
: undefined
|
|
65
65
|
const negated = negatedName !== undefined && properties[negatedName]?.type === 'boolean'
|
|
66
66
|
const name = negated ? (negatedName as string) : literalName
|
|
67
|
-
// Boolean props and inline `--name=value` consume no following token.
|
|
67
|
+
// Boolean props and inline `--name=value` consume no following token. Pass any
|
|
68
|
+
// inline value through (`--flag=false`) so the parser can honour the RHS instead
|
|
69
|
+
// of the boolean always resolving to true.
|
|
68
70
|
if (properties[name]?.type === 'boolean') {
|
|
69
|
-
yield { name, negated }
|
|
71
|
+
yield { name, negated, value: inlineValue }
|
|
70
72
|
continue
|
|
71
73
|
}
|
|
72
74
|
if (inlineValue !== undefined) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// node:fs existsSync — cheap sync presence check, mirrors createPublicAssetServer
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
+
import { resolve, sep } from 'node:path'
|
|
3
4
|
import { Glob } from 'bun'
|
|
4
5
|
import { mimeForExtension } from '../server/runtime/mimeForExtension.ts'
|
|
5
6
|
import type { Assets } from '../server/runtime/types/Assets.ts'
|
|
@@ -81,8 +82,10 @@ export function createMcpResourceServer({
|
|
|
81
82
|
return undefined
|
|
82
83
|
}
|
|
83
84
|
const relativePath = uri.slice(URI_PREFIX.length)
|
|
84
|
-
// reject `..` traversal
|
|
85
|
-
|
|
85
|
+
// reject `..` traversal on EITHER separator — a backslash-joined `..\` escapes
|
|
86
|
+
// resourcesDir on Windows (its OS layer treats `\` as a separator regardless of
|
|
87
|
+
// how the JS string was built), so a `/`-only split lets it through.
|
|
88
|
+
if (relativePath.split(/[\\/]/).includes('..')) {
|
|
86
89
|
return undefined
|
|
87
90
|
}
|
|
88
91
|
if (mcpResources) {
|
|
@@ -92,6 +95,14 @@ export function createMcpResourceServer({
|
|
|
92
95
|
}
|
|
93
96
|
return contentsFor(relativePath, Bun.gunzipSync(compressed))
|
|
94
97
|
}
|
|
98
|
+
// Belt-and-braces for the on-disk branch: resolve the target and confirm it
|
|
99
|
+
// stays inside resourcesDir, catching any traversal the segment check missed
|
|
100
|
+
// (absolute paths, platform separator quirks).
|
|
101
|
+
const root = resolve(resourcesDir)
|
|
102
|
+
const target = resolve(root, relativePath)
|
|
103
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
95
106
|
const file = Bun.file(`${resourcesDir}/${relativePath}`)
|
|
96
107
|
if (!(await file.exists())) {
|
|
97
108
|
return undefined
|
|
@@ -4,6 +4,12 @@ import { messageFromError } from '../shared/messageFromError.ts'
|
|
|
4
4
|
import { responseErrorText } from '../shared/responseErrorText.ts'
|
|
5
5
|
import { streamResponse } from '../shared/streamResponse.ts'
|
|
6
6
|
|
|
7
|
+
/* MCP `tools/call` is request/response, not a streaming transport, so a genuinely live
|
|
8
|
+
stream must be bounded: return what arrived within these limits (with a truncation note)
|
|
9
|
+
rather than buffering forever and never answering. */
|
|
10
|
+
const MAX_STREAM_FRAMES = 1000
|
|
11
|
+
const STREAM_TOOL_TIMEOUT_MS = 30_000
|
|
12
|
+
|
|
7
13
|
// Frames a value as MCP text content — strings verbatim, everything else as JSON.
|
|
8
14
|
// An undefined body (204 / empty) becomes '' — `JSON.stringify(undefined)` is the
|
|
9
15
|
// value `undefined`, not a string, which would make an invalid text content block.
|
|
@@ -38,26 +44,48 @@ export async function toolResultFromResponse(response: Response): Promise<Record
|
|
|
38
44
|
|
|
39
45
|
if (isStreamingResponse(response)) {
|
|
40
46
|
const frames: unknown[] = []
|
|
47
|
+
// Set when WE stop the stream (cap/timeout), so the iterator's resulting end/throw
|
|
48
|
+
// reads as truncation, not a stream error.
|
|
49
|
+
let truncated: string | undefined
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
truncated = `stream truncated after ${STREAM_TOOL_TIMEOUT_MS}ms — MCP tools/call is request/response, not a live stream`
|
|
52
|
+
void response.body?.cancel()
|
|
53
|
+
}, STREAM_TOOL_TIMEOUT_MS)
|
|
41
54
|
try {
|
|
42
55
|
for await (const frame of streamResponse(response)) {
|
|
43
56
|
frames.push(frame)
|
|
57
|
+
if (frames.length >= MAX_STREAM_FRAMES) {
|
|
58
|
+
truncated = `stream truncated at ${MAX_STREAM_FRAMES} frames`
|
|
59
|
+
void response.body?.cancel()
|
|
60
|
+
break
|
|
61
|
+
}
|
|
44
62
|
}
|
|
45
63
|
} catch (error) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
/* A real mid-stream error (we didn't cancel) surfaces as isError; a throw caused
|
|
65
|
+
by our own cancel falls through to the truncated result below. */
|
|
66
|
+
if (truncated === undefined) {
|
|
67
|
+
clearTimeout(timer)
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{ type: 'text', text: frames.map(asText).join('\n') },
|
|
71
|
+
{ type: 'text', text: `stream error: ${messageFromError(error)}` },
|
|
72
|
+
],
|
|
73
|
+
structuredContent: { frames },
|
|
74
|
+
isError: true,
|
|
75
|
+
}
|
|
56
76
|
}
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timer)
|
|
79
|
+
}
|
|
80
|
+
const content: { type: 'text'; text: string }[] = [
|
|
81
|
+
{ type: 'text', text: frames.map(asText).join('\n') },
|
|
82
|
+
]
|
|
83
|
+
if (truncated !== undefined) {
|
|
84
|
+
content.push({ type: 'text', text: truncated })
|
|
57
85
|
}
|
|
58
86
|
return {
|
|
59
|
-
content
|
|
60
|
-
structuredContent: { frames },
|
|
87
|
+
content,
|
|
88
|
+
structuredContent: truncated === undefined ? { frames } : { frames, truncated },
|
|
61
89
|
}
|
|
62
90
|
}
|
|
63
91
|
|
|
@@ -137,7 +137,21 @@ export async function parseArgs(
|
|
|
137
137
|
needs the schema threaded in here (or a coercing schema adapter at the rpc).
|
|
138
138
|
*/
|
|
139
139
|
const bodyObject = (body ?? {}) as Record<string, unknown>
|
|
140
|
-
|
|
140
|
+
/* Collect the query into an object, arraying repeated keys (`?tag=a&tag=b` → `['a','b']`)
|
|
141
|
+
rather than letting `Object.fromEntries` silently keep only the last — mirrors
|
|
142
|
+
splitFormData, so a repeated field reaches the schema as an array, not a dropped value. */
|
|
143
|
+
const queryObject: Record<string, unknown> = {}
|
|
144
|
+
for (const [key, value] of url.searchParams) {
|
|
145
|
+
const existing = queryObject[key]
|
|
146
|
+
if (!(key in queryObject)) {
|
|
147
|
+
queryObject[key] = value
|
|
148
|
+
} else if (Array.isArray(existing)) {
|
|
149
|
+
existing.push(value)
|
|
150
|
+
} else {
|
|
151
|
+
queryObject[key] = [existing, value]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const merged = { ...queryObject, ...bodyObject }
|
|
141
155
|
if (Object.keys(merged).length === 0) {
|
|
142
156
|
return undefined
|
|
143
157
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { abideLog } from '../../shared/abideLog.ts'
|
|
1
2
|
import { error } from '../error.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -39,7 +40,17 @@ export function runWithRpcTimeout(
|
|
|
39
40
|
(late) => {
|
|
40
41
|
void late.body?.cancel()
|
|
41
42
|
},
|
|
42
|
-
() => {
|
|
43
|
+
(rejection) => {
|
|
44
|
+
/* The handler outlived its timeout and then rejected. A cooperative
|
|
45
|
+
abort (it honoured the composed signal) is expected and stays quiet;
|
|
46
|
+
any other error is a genuine handler bug and would otherwise leave
|
|
47
|
+
zero server-side trace behind the client's 504 — log it. */
|
|
48
|
+
const aborted =
|
|
49
|
+
(rejection as { name?: string } | undefined)?.name === 'AbortError'
|
|
50
|
+
if (!aborted) {
|
|
51
|
+
abideLog.error(rejection)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
43
54
|
)
|
|
44
55
|
}
|
|
45
56
|
}
|