@abide/abide 0.116.1 → 0.116.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.116.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 9007822: A `bind:` writes its value before an `on<event>` on the same element runs — `<input type="file" bind:files={picked} onchange={use}>` hands `use` the files just picked, and an `oninput` beside `bind:value` reads the text just typed, wherever the two are written in the tag (RULEBOOK 32.75).
8
+ - ecf1ff0: a `bind:` through a computed key writes back — `bind:value={params[key]}` and `bind:value={params[s.k]}` inside a `{#for}` write the entry the key names when the input fires, and follow the key when it changes, where they were handed the read and never wrote; a `bind:` through an optional step (`params?.a`) is now a build error (RULEBOOK 32.75) rather than a read that writes nothing
9
+ - ee8cce7: a handler with no `args` gate whose parameter is one record now carries the `ValidationError` the wire refuses its args with — `rpc.isError(e, 'ValidationError')` narrows to it and `e.data` is `Issues<Args>`, where it narrowed to `never`. A handler with no args, a scalar, a list, a union of records or `unknown` derives no schema and still carries none
10
+ - fe68a88: a handler no call has reached is not busy, and asking runs nothing — on a server, `save.busy()` over a never-called `POST(async () => …)` ran the write and reported it in flight; it now answers as a handler with no entries (`busy` false, `ready` and `synced` true), for a `GET` with no args as well. A probe over a mutation a `refresh()` invalidated no longer sends the write again (RULEBOOK 3.14, 13.19)
11
+ - 874c919: under `mount` in `bun test`, a dynamic `import('#src/…')` from a component's browser module resolves as the static one does, rather than failing with "Cannot find package '#src'"
12
+ - f60ea39: under `mount` in `bun test`, a component's browser module calling a private handler (one its `<script server>` declares) is answered by that handler in the test's process, rather than fetching an `/abide-client:/…` address and failing with 503 — as a handler file's call already was
13
+ - 3e999ce: a throttled value lets its first change through at once — `reactive(x, { throttle })` then `s.set(v)` reads `v` back in the same turn, where the value it was built with had opened a window that held the first `set` back. RULEBOOK 5.12
14
+
15
+ ## 0.116.2
16
+
17
+ ### Patch Changes
18
+
19
+ - ef77e8d: a comment directly above an at-rule in a component's `<style>` no longer breaks its scoping — `/* … */ @media (…) { .x {…} }` scopes `.x` as it would with no comment
20
+ - 99631d1: the registry names `isFailed`, says `route.url` includes the fragment and `route.name` is the same under any mount path, and shows a `GET` over a plain function or a value taking `ReactiveOptions`; a scaffolded app's AGENTS.md names the `abide-ignore` escape, exporting a `Reactive` unwrapped, never `raw()` on untrusted markup, and `config` / `cookies()` over `Bun.env` and header parsing
21
+
3
22
  ## 0.116.1
4
23
 
5
24
  ### Patch Changes
package/docs/REGISTRY.md CHANGED
@@ -125,10 +125,10 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
125
125
 
126
126
  | Name | Signature | Meaning | Rules |
127
127
  | --- | --- | --- | --- |
128
- | `s.busy` | `() => boolean` | Work is in flight for this value. | 3.1, 3.2, 3.3, 3.4, 3.7, 3.9, 5.15, 7.5, 7.8, 11.19, 11.20, 11.21, 11.22, 11.50 |
129
- | `s.ready` | `() => boolean` | There is a value to serve. | 3.1, 3.2, 3.3, 3.5, 3.6, 3.8, 7.8, 11.20, 11.54, 11.58 |
130
- | `s.error` | `() => Failures \| undefined` | The standing refusal. | 3.1, 3.2, 3.3, 3.5, 4.10, 4.11, 4.12 |
131
- | `s.synced` | `() => boolean` | What is held is what the producer made. | 3.1, 3.2, 3.3, 3.10, 3.11, 3.12, 3.13 |
128
+ | `s.busy` | `() => boolean` | Work is in flight for this value. | 3.1, 3.2, 3.3, 3.4, 3.7, 3.9, 3.14, 5.15, 7.5, 7.8, 11.19, 11.20, 11.21, 11.22, 11.50 |
129
+ | `s.ready` | `() => boolean` | There is a value to serve. | 3.1, 3.2, 3.3, 3.5, 3.6, 3.8, 3.14, 7.8, 11.20, 11.54, 11.58 |
130
+ | `s.error` | `() => Failures \| undefined` | The standing refusal. | 3.1, 3.2, 3.3, 3.5, 3.14, 4.10, 4.11, 4.12 |
131
+ | `s.synced` | `() => boolean` | What is held is what the producer made. | 3.1, 3.2, 3.3, 3.10, 3.11, 3.12, 3.13, 3.14 |
132
132
  | `s.isError` | `<Name extends Failures['name']>(error: unknown, name: Name) => error is Extract<Failures, { name: Name }>` | Whether a caught failure is the one named. | 3.1 |
133
133
 
134
134
  ### Triggers
@@ -183,6 +183,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
183
183
  | `refuse.named` | `<Name extends string>(name: Name, status?: number, message?: string) => ((data?: undefined) => Failed<Name, undefined>) & { is: (error: unknown) => error is Failed<Name, undefined> }` | A reusable factory for a named, narrowable failure. | 15.3, 15.13, 15.18 |
184
184
  | `refuse.named` | `<Name extends string, Data extends JsonValue>(name: Name, status: number \| undefined, message: string \| ((data: Data) => string) \| undefined, options: { schema: Schema<Data> }) => ((data?: Data) => Failed<Name, Data>) & { is: (error: unknown) => error is Failed<Name, Data> }` | The same, carrying data its schema declares. | 15.3, 15.11, 15.12, 15.13, 15.14, 15.15, 15.18 |
185
185
  | `Failed<Name, Data>` | `Error & { name: Name; status: number; message: string; data: Data }` | The one refusal type, in-process and over a wire alike. | 15.1, 15.2, 15.6, 16.40, 16.41, 15.21 |
186
+ | `isFailed` | `(value: unknown) => value is Failed<string, unknown>` | Whether a value is a refusal. | 15.6 |
186
187
  | `return myError(data)` | `Failed<Name, Data>` | The one spelling for every refusal. | 15.4, 15.5 |
187
188
  | `notFound` | `(data?: { path: string; method: string }) => Failed<'NotFound', { path: string; method: string }>` | abide's own refusal for an address nothing answers. | 15.7, 15.9, 15.22 |
188
189
  | `validationError` | `<T>(data?: Issues<T>) => Failed<'ValidationError', Issues<T>>` | abide's own refusal for a schema or an `args` gate that refused, or a call's route parameter no segment can spell. | 15.8, 15.9 |
@@ -219,14 +220,14 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
219
220
  | `Args` | `Record<string, JsonValue> \| undefined` | The arguments, in the body on a mutation and in the URL on a `GET` or `DELETE`. | 11.24, 16.18, 19.15 |
220
221
  | `Middleware` | `<Ctx, Result>(next: (ctx?: Ctx) => Promise<Result>, ctx: Ctx) => Result \| Promise<Result>` | One rung of `middleware`. | 16.14, 16.15, 16.16, 16.21 |
221
222
  | `GET` | `<Value, Args, Failures, RungFailures, Produced = Value>(handler: Reactive<Value, Value, Failures, Produced> \| Keyed<Value, Args, Value, Failures, Produced>, options?: RpcOptions<Args, RungFailures>) => Rpc<Value, Args, Failures \| RungFailures, Produced>` | Declares a read any surface may call. | 16.1, 16.2, 16.5, 16.6, 16.30, 16.31, 16.32, 16.68, 16.76, 16.34, 16.35, 16.36, 16.37, 16.39, 16.44, 16.45, 16.46, 16.47, 16.75, 16.50, 16.51, 16.52, 16.64, 18.1, 18.3, 18.6, 18.8, 18.10, 18.12, 18.21, 18.24, 18.27 |
222
- | `GET` | `<Value, Args, Failures, RungFailures, Produced = Value>(handler: (args?: Args) => Value, options?: RpcOptions<Args, RungFailures> & ReactiveOptions<Value, Value, Failures, Args>) => Rpc<Value, Args, Failures \| RungFailures, Produced>` | The plain-function overload, whose options build the value it becomes beside its address. | 16.1, 16.6, 16.7 |
223
- | `GET` | `<Value, Failures, RungFailures>(handler: Value \| PromiseLike<Value>, options?: RpcOptions<undefined, RungFailures> & ReactiveOptions<Value, Value, Failures>) => Rpc<Value, undefined, Failures \| RungFailures>` | The value overload, for what the handler owns outright. | 16.1, 16.6, 16.7 |
223
+ | `GET` | `<Value, Args, Failures, RungFailures, Produced = Value>(handler: (args?: Args) => Value, options?: RpcOptions<Args, RungFailures> & ReactiveOptions<Value, Value, Failures, Args>) => Rpc<Value, Args, Failures \| RungFailures, Produced>` | The plain-function overload, whose `ReactiveOptions` (`ttl`, `global`, `debounce` and the rest) build the value it becomes beside its address. | 16.1, 16.6, 16.7 |
224
+ | `GET` | `<Value, Failures, RungFailures>(handler: Value \| PromiseLike<Value>, options?: RpcOptions<undefined, RungFailures> & ReactiveOptions<Value, Value, Failures>) => Rpc<Value, undefined, Failures \| RungFailures>` | The value overload, for what the handler owns outright, whose `ReactiveOptions` build the value it becomes. | 16.1, 16.6, 16.7 |
224
225
  | `POST` / `PUT` / `PATCH` / `DELETE` | same as `GET` | Declares a mutation. | 16.2, 16.7, 16.32, 16.68, 16.76, 16.38, 16.53 |
225
- | `rpc.busy` | `(pattern?: Partial<Args>) => boolean` | `busy` narrowed to the handler's matching entries. | 13.2, 13.3, 13.12, 46.11 |
226
- | `rpc.ready` | `(pattern?: Partial<Args>) => boolean` | `ready` narrowed to the handler's matching entries. | 13.11, 13.12 |
227
- | `rpc.synced` | `(pattern?: Partial<Args>) => boolean` | `synced` narrowed to the handler's matching entries. | 13.12, 13.18 |
228
- | `rpc.invalidate` | `(pattern?: Partial<Args>) => void` | `invalidate` narrowed to the handler's matching entries. | 7.2, 7.3, 13.4, 13.12 |
229
- | `rpc.refresh` | `(pattern?: Partial<Args>) => void` | `refresh` narrowed to the handler's matching entries. | 7.4, 7.5, 13.4, 13.12 |
226
+ | `rpc.busy` | `(pattern?: Partial<Args>) => boolean` | `busy` narrowed to the handler's matching entries. | 13.2, 13.3, 13.12, 13.19, 46.11 |
227
+ | `rpc.ready` | `(pattern?: Partial<Args>) => boolean` | `ready` narrowed to the handler's matching entries. | 13.11, 13.12, 13.19 |
228
+ | `rpc.synced` | `(pattern?: Partial<Args>) => boolean` | `synced` narrowed to the handler's matching entries. | 13.12, 13.18, 13.19 |
229
+ | `rpc.invalidate` | `(pattern?: Partial<Args>) => void` | `invalidate` narrowed to the handler's matching entries. | 7.2, 7.3, 13.4, 13.12, 13.19 |
230
+ | `rpc.refresh` | `(pattern?: Partial<Args>) => void` | `refresh` narrowed to the handler's matching entries. | 7.4, 7.5, 13.4, 13.12, 13.19 |
230
231
  | `rpc.isError` | `<Name extends Failures['name']>(error: unknown, name: Name) => error is Extract<Failures, { name: Name }>` | Whether a caught failure is the one named. | 16.8 |
231
232
  | `rpc.raw` | `(args: Args, init?: RequestInit) => Promise<Response>` | The same call handed back as the raw response. | 16.11, 16.42 |
232
233
  | `rpc.method` | `string` | The HTTP method. | 16.2 |
@@ -379,10 +380,10 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
379
380
 
380
381
  | Name | Signature | Meaning | Rules |
381
382
  | --- | --- | --- | --- |
382
- | `route.url` | `Reactive<URL>` | Where we are. | 23.1, 23.2, 23.3, 23.4, 30.23, 19.16, 31.16 |
383
+ | `route.url` | `Reactive<URL>` | Where we are, fragment included. | 23.1, 23.2, 23.3, 23.4, 30.20, 30.21, 30.23, 19.16, 31.16 |
383
384
  | `route.params` | `Reactive<Params>` | The matched route's segments. | 23.1, 23.5, 23.9, 30.23, 31.16 |
384
385
  | `Params` | `Record<string, string \| string[]>` | What a matched route's segments are. | 23.5 |
385
- | `route.name` | `Reactive<string>` | The resolution path. | 23.1, 23.6, 31.16 |
386
+ | `route.name` | `Reactive<string>` | The resolution path, the same under any mount path. | 23.1, 23.6, 23.14, 31.16 |
386
387
  | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | 23.1, 31.16 |
387
388
 
388
389
  ## `online`
@@ -551,14 +552,14 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
551
552
  | formatting whitespace | What a run of whitespace renders as. | 33.17, 33.18 |
552
553
  | `{raw(...)}` | Raw HTML. | 32.2, 32.25, 32.26, 32.30, 32.31, 32.70 |
553
554
  | `name={expr}` | A reactive attribute or property. | 32.3, 32.20, 32.23, 32.37, 32.70, 32.71 |
554
- | `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | 32.4 |
555
+ | `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | 32.4, 32.75 |
555
556
  | `name="…{expr}…"` | An interpolated quoted value. | 32.3, 32.71 |
556
- | `bind:value` | Two-way bind on an element. | 5.16, 32.5, 32.58, 32.59, 32.60, 32.61, 32.62, 32.72, 32.74 |
557
- | `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. | 32.6, 33.6, 33.8, 14.18 |
558
- | `bind:checked` | A boolean bind on an input. | 32.7, 32.72, 32.74 |
559
- | `bind:open` | The same, on a `<details>`. | 32.7, 32.72, 32.74 |
560
- | `bind:group` | Radio or checkbox membership. | 32.8, 32.72, 32.74 |
561
- | `bind:files` | A file input's files, as a list. | 32.73, 32.72, 32.74 |
557
+ | `bind:value` | Two-way bind on an element. | 5.16, 32.5, 32.58, 32.59, 32.60, 32.61, 32.62, 32.72, 32.74, 32.75, 32.76 |
558
+ | `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. | 32.6, 33.6, 33.8, 14.18, 32.76 |
559
+ | `bind:checked` | A boolean bind on an input. | 32.7, 32.72, 32.74, 32.75, 32.76 |
560
+ | `bind:open` | The same, on a `<details>`. | 32.7, 32.72, 32.74, 32.75, 32.76 |
561
+ | `bind:group` | Radio or checkbox membership. | 32.8, 32.72, 32.74, 32.75, 32.76 |
562
+ | `bind:files` | A file input's files, as a list. | 32.73, 32.72, 32.74, 32.75, 32.76 |
562
563
  | `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | 32.5, 32.6 |
563
564
  | `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | 32.9, 32.38, 32.66, 32.68, 32.69 |
564
565
  | `class:name={cond}` | Toggles a class. | 32.10, 32.71 |
@@ -737,7 +738,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
737
738
 
738
739
  | Name | Signature | Meaning | Rules |
739
740
  | --- | --- | --- | --- |
740
- | `mount` | `(component: Component) => Promise<Mounted>` | A bound component, rendered into the document's body and hydrated there. | 38.119 |
741
+ | `mount` | `(component: Component) => Promise<Mounted>` | A bound component, rendered into the document's body and hydrated there. | 38.119, 38.121 |
741
742
  | `Mounted` | `{ stop(): void; [Symbol.dispose](): void }` | A component `mount` hydrated, until it is stopped. | 38.119 |
742
743
 
743
744
  # The measurement harness
package/docs/RULEBOOK.md CHANGED
@@ -211,6 +211,8 @@ subscribe it to the value.
211
211
 
212
212
  3.13 A value MUST report `s.synced` false where a value it read or adopts does.
213
213
 
214
+ 3.14 A probe over a value a mutation's `declare` built MUST NOT run its body.
215
+
214
216
  # 4. Writes and gates
215
217
 
216
218
  4.1 `s.set` MUST accept a settled value or a load, and `reactive` MUST accept the same two.
@@ -751,6 +753,9 @@ mutation's `declare` built while its write is in flight.
751
753
  13.18 `synced` over a selection MUST be true where every entry in it is synced, and a bare `synced`
752
754
  where every value it sweeps is.
753
755
 
756
+ 13.19 `busy`, `ready`, `synced`, `refresh` and `invalidate` over an `Rpc` no call has reached in the
757
+ current scope MUST answer as over a `Selection` with no entries.
758
+
754
759
  # 14. Tracking
755
760
 
756
761
  14.1 A `Reactive` MUST be tracked only where something downstream will be rebuilt from it.
@@ -885,8 +890,9 @@ alone.
885
890
  options declare one.
886
891
 
887
892
  16.8 `Failures` on an `Rpc` MUST be the union of what the handler declared, what its rungs
888
- declared, and the undeclared `Failed` named `HttpError` per 17.4, with no data, which the transport
889
- may answer any call with.
893
+ declared, the undeclared `Failed` named `HttpError` per 17.4, with no data, which the transport
894
+ may answer any call with, and, wherever the `Schema` 19.9 derives from its `Args` is an object's,
895
+ the undeclared `ValidationError` of 15.8 with `Issues` of the `Args` as its data.
890
896
 
891
897
  16.9 `rpc.url` MUST take the `Args`, MUST write each parameter of its route into the path, and MUST
892
898
  carry the rest in the query on a `GET` or a `DELETE` alone.
@@ -1870,7 +1876,7 @@ as a name bound to a `Reactive` is, per 31.1 to 31.7.
1870
1876
  component.
1871
1877
 
1872
1878
  32.5 `bind:value` MUST read the property and write it back on input or change, through the member
1873
- path where its operand is a path into a value (31.8).
1879
+ path where its operand is a path into a value (31.8), a computed step's key read at each write.
1874
1880
 
1875
1881
  32.6 `bind:prop` on a component MUST hand the child the caller's `Reactive`, or its `{ get, set }` pair,
1876
1882
  as that prop's write path.
@@ -2086,6 +2092,12 @@ nothing for it.
2086
2092
  32.74 A `bind:` on an element naming none of `value`, `checked`, `open`, `group`, `files` and
2087
2093
  `element` MUST be a build error.
2088
2094
 
2095
+ 32.75 A `bind:` on an element MUST write its operand before any `on<event>` listener on that
2096
+ element, written or spread, runs for the same event, wherever the two are written in the tag.
2097
+
2098
+ 32.76 A `bind:` whose operand is a path into a value that takes an optional step (`?.`) MUST be a
2099
+ build error.
2100
+
2089
2101
  # 33. Props
2090
2102
 
2091
2103
  33.1 `props` MUST be callable from a `<script>` block only, and calling it from a `<script module>`
@@ -2750,6 +2762,9 @@ hydration woke before it settles; it MUST throw where the component was not impo
2750
2762
  `ABIDE_PRINCIPAL_SECRET` MUST seal and verify the principal cookie with a secret the preload
2751
2763
  supplies, and `abide start` MUST NOT supply that secret.
2752
2764
 
2765
+ 38.121 Under `mount`, a call the component's browser module makes to a private handler MUST be
2766
+ answered in the test's process by the handler the component's server module declares.
2767
+
2753
2768
  # 39. Head and view transitions
2754
2769
 
2755
2770
  39.1 A `<head>` element in a `.abide` file MUST contribute its children to the document head.
package/docs/abide.md CHANGED
@@ -43,6 +43,7 @@ at its address with `.md` on the end, and the links below point at those files.
43
43
  - `invalidate` — `reference/invalidate.md`
44
44
  - `refuse` — `reference/refuse.md`
45
45
  - `Failed` — `reference/types/Failed.md`
46
+ - `isFailed` — `reference/isFailed.md`
46
47
  - `notFound` — `reference/notFound.md`
47
48
  - `validationError` — `reference/validationError.md`
48
49
  - `aborted` — `reference/aborted.md`
@@ -439,7 +440,7 @@ let starred = reactive(false)
439
440
  </script>
440
441
 
441
442
  <div class="row">
442
- <button aria-pressed={String(starred)} onclick={() => (starred = !starred)}>
443
+ <button aria-pressed={starred} onclick={() => (starred = !starred)}>
443
444
  {starred ? '★ Starred' : '☆ Star'}
444
445
  </button>
445
446
  <span>Grace Hopper</span>
@@ -2135,7 +2136,7 @@ const { id, name } = props<{ id: string; name: string }>()
2135
2136
  let selected = reactive.share('selectedDeal', () => reactive(''))
2136
2137
  </script>
2137
2138
 
2138
- <button aria-pressed={String(selected === id)} onclick={() => (selected = id)}>
2139
+ <button aria-pressed={selected === id} onclick={() => (selected = id)}>
2139
2140
  {name}
2140
2141
  </button>
2141
2142
  ```
@@ -2939,7 +2940,7 @@ const address = reactive(() => url('/companies/[id]', { id: open }))
2939
2940
 
2940
2941
  <div class="tabs">
2941
2942
  {#for id of IDS}
2942
- <button aria-selected={String(open === id)}
2943
+ <button aria-selected={open === id}
2943
2944
  onclick={() => (open = id)}>{id}</button>
2944
2945
  {/for}
2945
2946
  </div>
@@ -3087,7 +3088,7 @@ let draft = reactive('')
3087
3088
 
3088
3089
  <div class="tabs">
3089
3090
  {#for name of TABS}
3090
- <button aria-selected={String(tab === name)}
3091
+ <button aria-selected={tab === name}
3091
3092
  onclick={() => navigate(`?tab=${name}`, { replace: true })}
3092
3093
  >{name}</button>
3093
3094
  {/for}
@@ -3236,7 +3237,7 @@ function followTitle(element: Element) {
3236
3237
 
3237
3238
  <div class="tabs">
3238
3239
  {#for page of PAGES}
3239
- <button aria-selected={String(open === page.address)}
3240
+ <button aria-selected={open === page.address}
3240
3241
  onclick={() => (open = page.address)}>{page.label}</button>
3241
3242
  {/for}
3242
3243
  </div>
@@ -5678,6 +5679,7 @@ import { claudeCode, GET } from '@abide/abide/server'
5678
5679
  // This browser's pairing, for as long as the page watches it.
5679
5680
  export default GET(() => claudeCode.bridge(), {
5680
5681
  description: "This browser's Claude Code pairing, as it changes.",
5682
+ // Exempt from an operator's ABIDE_RPC_TIMEOUT: a pairing idles for minutes.
5681
5683
  timeout: Infinity,
5682
5684
  clients: { mcp: false },
5683
5685
  })
@@ -6596,6 +6598,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
6596
6598
  | [`invalidate`](/reference/invalidate.md) | `@abide/abide` |
6597
6599
  | [`refuse`](/reference/refuse.md) | `@abide/abide` |
6598
6600
  | [`Failed`](/reference/types/Failed.md) | `@abide/abide` |
6601
+ | [`isFailed`](/reference/isFailed.md) | `@abide/abide` |
6599
6602
  | [`notFound`](/reference/notFound.md) | `@abide/abide` |
6600
6603
  | [`validationError`](/reference/validationError.md) | `@abide/abide` |
6601
6604
  | [`aborted`](/reference/aborted.md) | `@abide/abide` |
@@ -6853,10 +6856,10 @@ Arrive here knowing which name you want. Each page is read off the registry: the
6853
6856
 
6854
6857
  | Name | Signature | Meaning | Rules |
6855
6858
  | --- | --- | --- | --- |
6856
- | `s.busy` | `() => boolean` | Work is in flight for this value. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.4](/rulebook#3.4), [3.7](/rulebook#3.7), [3.9](/rulebook#3.9), [5.15](/rulebook#5.15), [7.5](/rulebook#7.5), [7.8](/rulebook#7.8), [11.19](/rulebook#11.19), [11.20](/rulebook#11.20), [11.21](/rulebook#11.21), [11.22](/rulebook#11.22), [11.50](/rulebook#11.50) |
6857
- | `s.ready` | `() => boolean` | There is a value to serve. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.5](/rulebook#3.5), [3.6](/rulebook#3.6), [3.8](/rulebook#3.8), [7.8](/rulebook#7.8), [11.20](/rulebook#11.20), [11.54](/rulebook#11.54), [11.58](/rulebook#11.58) |
6858
- | `s.error` | `() => Failures \| undefined` | The standing refusal. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.5](/rulebook#3.5), [4.10](/rulebook#4.10), [4.11](/rulebook#4.11), [4.12](/rulebook#4.12) |
6859
- | `s.synced` | `() => boolean` | What is held is what the producer made. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.10](/rulebook#3.10), [3.11](/rulebook#3.11), [3.12](/rulebook#3.12), [3.13](/rulebook#3.13) |
6859
+ | `s.busy` | `() => boolean` | Work is in flight for this value. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.4](/rulebook#3.4), [3.7](/rulebook#3.7), [3.9](/rulebook#3.9), [3.14](/rulebook#3.14), [5.15](/rulebook#5.15), [7.5](/rulebook#7.5), [7.8](/rulebook#7.8), [11.19](/rulebook#11.19), [11.20](/rulebook#11.20), [11.21](/rulebook#11.21), [11.22](/rulebook#11.22), [11.50](/rulebook#11.50) |
6860
+ | `s.ready` | `() => boolean` | There is a value to serve. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.5](/rulebook#3.5), [3.6](/rulebook#3.6), [3.8](/rulebook#3.8), [3.14](/rulebook#3.14), [7.8](/rulebook#7.8), [11.20](/rulebook#11.20), [11.54](/rulebook#11.54), [11.58](/rulebook#11.58) |
6861
+ | `s.error` | `() => Failures \| undefined` | The standing refusal. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.5](/rulebook#3.5), [3.14](/rulebook#3.14), [4.10](/rulebook#4.10), [4.11](/rulebook#4.11), [4.12](/rulebook#4.12) |
6862
+ | `s.synced` | `() => boolean` | What is held is what the producer made. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.10](/rulebook#3.10), [3.11](/rulebook#3.11), [3.12](/rulebook#3.12), [3.13](/rulebook#3.13), [3.14](/rulebook#3.14) |
6860
6863
  | `s.isError` | `<Name extends Failures['name']>(error: unknown, name: Name) => error is Extract<Failures, { name: Name }>` | Whether a caught failure is the one named. | [3.1](/rulebook#3.1) |
6861
6864
 
6862
6865
  ## Triggers
@@ -7289,6 +7292,24 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7289
7292
 
7290
7293
  ---
7291
7294
 
7295
+ <!-- reference/isFailed.md -->
7296
+
7297
+ # `isFailed`
7298
+
7299
+ `import { isFailed } from '@abide/abide'`
7300
+
7301
+ ## Refusals
7302
+
7303
+ | Name | Signature | Meaning | Rules |
7304
+ | --- | --- | --- | --- |
7305
+ | `isFailed` | `(value: unknown) => value is Failed<string, unknown>` | Whether a value is a refusal. | [15.6](/rulebook#15.6) |
7306
+
7307
+ ## Taught in
7308
+
7309
+ - [Add pages](/guide/add-pages.md)
7310
+
7311
+ ---
7312
+
7292
7313
  <!-- reference/notFound.md -->
7293
7314
 
7294
7315
  # `notFound`
@@ -7386,11 +7407,11 @@ Reactive primitives: what is written in place rather than imported.
7386
7407
  | `Value` | `unknown` | What the addressed `Reactive` holds. | [16.3](/rulebook#16.3), [16.30](/rulebook#16.30) |
7387
7408
  | `Produced` | `unknown` | What it yields, per the unit rule. | [1.2](/rulebook#1.2), [16.3](/rulebook#16.3) |
7388
7409
  | `Args` | `Record<string, JsonValue> \| undefined` | The arguments, in the body on a mutation and in the URL on a `GET` or `DELETE`. | [11.24](/rulebook#11.24), [16.18](/rulebook#16.18), [19.15](/rulebook#19.15) |
7389
- | `rpc.busy` | `(pattern?: Partial<Args>) => boolean` | `busy` narrowed to the handler's matching entries. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.12](/rulebook#13.12), [46.11](/rulebook#46.11) |
7390
- | `rpc.ready` | `(pattern?: Partial<Args>) => boolean` | `ready` narrowed to the handler's matching entries. | [13.11](/rulebook#13.11), [13.12](/rulebook#13.12) |
7391
- | `rpc.synced` | `(pattern?: Partial<Args>) => boolean` | `synced` narrowed to the handler's matching entries. | [13.12](/rulebook#13.12), [13.18](/rulebook#13.18) |
7392
- | `rpc.invalidate` | `(pattern?: Partial<Args>) => void` | `invalidate` narrowed to the handler's matching entries. | [7.2](/rulebook#7.2), [7.3](/rulebook#7.3), [13.4](/rulebook#13.4), [13.12](/rulebook#13.12) |
7393
- | `rpc.refresh` | `(pattern?: Partial<Args>) => void` | `refresh` narrowed to the handler's matching entries. | [7.4](/rulebook#7.4), [7.5](/rulebook#7.5), [13.4](/rulebook#13.4), [13.12](/rulebook#13.12) |
7410
+ | `rpc.busy` | `(pattern?: Partial<Args>) => boolean` | `busy` narrowed to the handler's matching entries. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.12](/rulebook#13.12), [13.19](/rulebook#13.19), [46.11](/rulebook#46.11) |
7411
+ | `rpc.ready` | `(pattern?: Partial<Args>) => boolean` | `ready` narrowed to the handler's matching entries. | [13.11](/rulebook#13.11), [13.12](/rulebook#13.12), [13.19](/rulebook#13.19) |
7412
+ | `rpc.synced` | `(pattern?: Partial<Args>) => boolean` | `synced` narrowed to the handler's matching entries. | [13.12](/rulebook#13.12), [13.18](/rulebook#13.18), [13.19](/rulebook#13.19) |
7413
+ | `rpc.invalidate` | `(pattern?: Partial<Args>) => void` | `invalidate` narrowed to the handler's matching entries. | [7.2](/rulebook#7.2), [7.3](/rulebook#7.3), [13.4](/rulebook#13.4), [13.12](/rulebook#13.12), [13.19](/rulebook#13.19) |
7414
+ | `rpc.refresh` | `(pattern?: Partial<Args>) => void` | `refresh` narrowed to the handler's matching entries. | [7.4](/rulebook#7.4), [7.5](/rulebook#7.5), [13.4](/rulebook#13.4), [13.12](/rulebook#13.12), [13.19](/rulebook#13.19) |
7394
7415
  | `rpc.isError` | `<Name extends Failures['name']>(error: unknown, name: Name) => error is Extract<Failures, { name: Name }>` | Whether a caught failure is the one named. | [16.8](/rulebook#16.8) |
7395
7416
  | `rpc.raw` | `(args: Args, init?: RequestInit) => Promise<Response>` | The same call handed back as the raw response. | [16.11](/rulebook#16.11), [16.42](/rulebook#16.42) |
7396
7417
  | `rpc.method` | `string` | The HTTP method. | [16.2](/rulebook#16.2) |
@@ -7441,8 +7462,8 @@ Reactive primitives: what is written in place rather than imported.
7441
7462
  | Name | Signature | Meaning | Rules |
7442
7463
  | --- | --- | --- | --- |
7443
7464
  | `GET` | `<Value, Args, Failures, RungFailures, Produced = Value>(handler: Reactive<Value, Value, Failures, Produced> \| Keyed<Value, Args, Value, Failures, Produced>, options?: RpcOptions<Args, RungFailures>) => Rpc<Value, Args, Failures \| RungFailures, Produced>` | Declares a read any surface may call. | [16.1](/rulebook#16.1), [16.2](/rulebook#16.2), [16.5](/rulebook#16.5), [16.6](/rulebook#16.6), [16.30](/rulebook#16.30), [16.31](/rulebook#16.31), [16.32](/rulebook#16.32), [16.68](/rulebook#16.68), [16.76](/rulebook#16.76), [16.34](/rulebook#16.34), [16.35](/rulebook#16.35), [16.36](/rulebook#16.36), [16.37](/rulebook#16.37), [16.39](/rulebook#16.39), [16.44](/rulebook#16.44), [16.45](/rulebook#16.45), [16.46](/rulebook#16.46), [16.47](/rulebook#16.47), [16.75](/rulebook#16.75), [16.50](/rulebook#16.50), [16.51](/rulebook#16.51), [16.52](/rulebook#16.52), [16.64](/rulebook#16.64), [18.1](/rulebook#18.1), [18.3](/rulebook#18.3), [18.6](/rulebook#18.6), [18.8](/rulebook#18.8), [18.10](/rulebook#18.10), [18.12](/rulebook#18.12), [18.21](/rulebook#18.21), [18.24](/rulebook#18.24), [18.27](/rulebook#18.27) |
7444
- | `GET` | `<Value, Args, Failures, RungFailures, Produced = Value>(handler: (args?: Args) => Value, options?: RpcOptions<Args, RungFailures> & ReactiveOptions<Value, Value, Failures, Args>) => Rpc<Value, Args, Failures \| RungFailures, Produced>` | The plain-function overload, whose options build the value it becomes beside its address. | [16.1](/rulebook#16.1), [16.6](/rulebook#16.6), [16.7](/rulebook#16.7) |
7445
- | `GET` | `<Value, Failures, RungFailures>(handler: Value \| PromiseLike<Value>, options?: RpcOptions<undefined, RungFailures> & ReactiveOptions<Value, Value, Failures>) => Rpc<Value, undefined, Failures \| RungFailures>` | The value overload, for what the handler owns outright. | [16.1](/rulebook#16.1), [16.6](/rulebook#16.6), [16.7](/rulebook#16.7) |
7465
+ | `GET` | `<Value, Args, Failures, RungFailures, Produced = Value>(handler: (args?: Args) => Value, options?: RpcOptions<Args, RungFailures> & ReactiveOptions<Value, Value, Failures, Args>) => Rpc<Value, Args, Failures \| RungFailures, Produced>` | The plain-function overload, whose `ReactiveOptions` (`ttl`, `global`, `debounce` and the rest) build the value it becomes beside its address. | [16.1](/rulebook#16.1), [16.6](/rulebook#16.6), [16.7](/rulebook#16.7) |
7466
+ | `GET` | `<Value, Failures, RungFailures>(handler: Value \| PromiseLike<Value>, options?: RpcOptions<undefined, RungFailures> & ReactiveOptions<Value, Value, Failures>) => Rpc<Value, undefined, Failures \| RungFailures>` | The value overload, for what the handler owns outright, whose `ReactiveOptions` build the value it becomes. | [16.1](/rulebook#16.1), [16.6](/rulebook#16.6), [16.7](/rulebook#16.7) |
7446
7467
 
7447
7468
  ## Taught in
7448
7469
 
@@ -8175,9 +8196,9 @@ Generated surfaces: what is written in place rather than imported.
8175
8196
 
8176
8197
  | Name | Signature | Meaning | Rules |
8177
8198
  | --- | --- | --- | --- |
8178
- | `route.url` | `Reactive<URL>` | Where we are. | [23.1](/rulebook#23.1), [23.2](/rulebook#23.2), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [30.23](/rulebook#30.23), [19.16](/rulebook#19.16), [31.16](/rulebook#31.16) |
8199
+ | `route.url` | `Reactive<URL>` | Where we are, fragment included. | [23.1](/rulebook#23.1), [23.2](/rulebook#23.2), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [30.20](/rulebook#30.20), [30.21](/rulebook#30.21), [30.23](/rulebook#30.23), [19.16](/rulebook#19.16), [31.16](/rulebook#31.16) |
8179
8200
  | `route.params` | `Reactive<Params>` | The matched route's segments. | [23.1](/rulebook#23.1), [23.5](/rulebook#23.5), [23.9](/rulebook#23.9), [30.23](/rulebook#30.23), [31.16](/rulebook#31.16) |
8180
- | `route.name` | `Reactive<string>` | The resolution path. | [23.1](/rulebook#23.1), [23.6](/rulebook#23.6), [31.16](/rulebook#31.16) |
8201
+ | `route.name` | `Reactive<string>` | The resolution path, the same under any mount path. | [23.1](/rulebook#23.1), [23.6](/rulebook#23.6), [23.14](/rulebook#23.14), [31.16](/rulebook#31.16) |
8181
8202
  | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | [23.1](/rulebook#23.1), [31.16](/rulebook#31.16) |
8182
8203
 
8183
8204
  ## Taught in
@@ -8878,14 +8899,14 @@ Generated surfaces: what is written in place rather than imported.
8878
8899
  | formatting whitespace | What a run of whitespace renders as. | [33.17](/rulebook#33.17), [33.18](/rulebook#33.18) |
8879
8900
  | `{raw(...)}` | Raw HTML. | [32.2](/rulebook#32.2), [32.25](/rulebook#32.25), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31), [32.70](/rulebook#32.70) |
8880
8901
  | `name={expr}` | A reactive attribute or property. | [32.3](/rulebook#32.3), [32.20](/rulebook#32.20), [32.23](/rulebook#32.23), [32.37](/rulebook#32.37), [32.70](/rulebook#32.70), [32.71](/rulebook#32.71) |
8881
- | `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | [32.4](/rulebook#32.4) |
8902
+ | `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | [32.4](/rulebook#32.4), [32.75](/rulebook#32.75) |
8882
8903
  | `name="…{expr}…"` | An interpolated quoted value. | [32.3](/rulebook#32.3), [32.71](/rulebook#32.71) |
8883
- | `bind:value` | Two-way bind on an element. | [5.16](/rulebook#5.16), [32.5](/rulebook#32.5), [32.58](/rulebook#32.58), [32.59](/rulebook#32.59), [32.60](/rulebook#32.60), [32.61](/rulebook#32.61), [32.62](/rulebook#32.62), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
8884
- | `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. | [32.6](/rulebook#32.6), [33.6](/rulebook#33.6), [33.8](/rulebook#33.8), [14.18](/rulebook#14.18) |
8885
- | `bind:checked` | A boolean bind on an input. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
8886
- | `bind:open` | The same, on a `<details>`. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
8887
- | `bind:group` | Radio or checkbox membership. | [32.8](/rulebook#32.8), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
8888
- | `bind:files` | A file input's files, as a list. | [32.73](/rulebook#32.73), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
8904
+ | `bind:value` | Two-way bind on an element. | [5.16](/rulebook#5.16), [32.5](/rulebook#32.5), [32.58](/rulebook#32.58), [32.59](/rulebook#32.59), [32.60](/rulebook#32.60), [32.61](/rulebook#32.61), [32.62](/rulebook#32.62), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74), [32.75](/rulebook#32.75), [32.76](/rulebook#32.76) |
8905
+ | `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. | [32.6](/rulebook#32.6), [33.6](/rulebook#33.6), [33.8](/rulebook#33.8), [14.18](/rulebook#14.18), [32.76](/rulebook#32.76) |
8906
+ | `bind:checked` | A boolean bind on an input. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74), [32.75](/rulebook#32.75), [32.76](/rulebook#32.76) |
8907
+ | `bind:open` | The same, on a `<details>`. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74), [32.75](/rulebook#32.75), [32.76](/rulebook#32.76) |
8908
+ | `bind:group` | Radio or checkbox membership. | [32.8](/rulebook#32.8), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74), [32.75](/rulebook#32.75), [32.76](/rulebook#32.76) |
8909
+ | `bind:files` | A file input's files, as a list. | [32.73](/rulebook#32.73), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74), [32.75](/rulebook#32.75), [32.76](/rulebook#32.76) |
8889
8910
  | `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | [32.5](/rulebook#32.5), [32.6](/rulebook#32.6) |
8890
8911
  | `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | [32.9](/rulebook#32.9), [32.38](/rulebook#32.38), [32.66](/rulebook#32.66), [32.68](/rulebook#32.68), [32.69](/rulebook#32.69) |
8891
8912
  | `class:name={cond}` | Toggles a class. | [32.10](/rulebook#32.10), [32.71](/rulebook#32.71) |
@@ -9448,7 +9469,7 @@ Configuration: what is written in place rather than imported.
9448
9469
 
9449
9470
  | Name | Signature | Meaning | Rules |
9450
9471
  | --- | --- | --- | --- |
9451
- | `mount` | `(component: Component) => Promise<Mounted>` | A bound component, rendered into the document's body and hydrated there. | [38.119](/rulebook#38.119) |
9472
+ | `mount` | `(component: Component) => Promise<Mounted>` | A bound component, rendered into the document's body and hydrated there. | [38.119](/rulebook#38.119), [38.121](/rulebook#38.121) |
9452
9473
 
9453
9474
  ## Taught in
9454
9475
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.116.1",
3
+ "version": "0.116.3",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -17,6 +17,9 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
17
17
  TypeScript service do not read `.abide` files, so a clean `tsc` says nothing about them. Run
18
18
  `bun run check` after every change, and treat its warnings as bugs: several of them name a read
19
19
  that will not update when its value changes.
20
+ Where the read is meant, `// abide-ignore` and the clause the warning cites, on the line above
21
+ (`// abide-ignore 14.15`), silences that one (RULEBOOK 14.19); a read hoisted out of its place
22
+ only to quiet a warning is the bug it named.
20
23
  - `bun run test` — runs `bun test`. `src/getGreeting.test.ts` is the pattern: a test imports a
21
24
  handler or a `.abide` file and calls it.
22
25
  - `bun run dev` — serves the app on a local port and rebuilds when a file changes.
@@ -44,3 +47,12 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
44
47
  - Import the app's own modules as `#src/…` (see `imports` in `package.json`).
45
48
  - A test imports `.abide` files directly: `bunfig.toml` preloads `@abide/harness/abide/preload`,
46
49
  a DOM to mount into and then `@abide/abide/test`.
50
+
51
+ ## Habits
52
+
53
+ - Export a `Reactive` itself, never wrapped in an object: an `.abide` file reads and writes it by
54
+ name (RULEBOOK 31.1), and one reached through a property is left to `x()` and `x.set(v)` (31.7).
55
+ - Model output and fetched HTML are untrusted. Never hand them to `raw()` (RULEBOOK 32.2)
56
+ unsanitized.
57
+ - Settings come from `onConfig` and `config` (RULEBOOK 27.2, 27.9), which already read the
58
+ environment, and cookies from `cookies()` (22.2) — not `Bun.env` or a parsed `cookie` header.
@@ -372,17 +372,17 @@ export class Lowering {
372
372
  * `contact.address.city = v` to for its set. Lowered as a read, the binding was handed the city
373
373
  * as a string and had nothing to write the typed text back into.
374
374
  *
375
- * A STATIC PATH ONLY — names and dots, which carry no read of their own. `undefined` for any
376
- * other operand, which `reference` lowers as it stands.
375
+ * A COMPUTED STEP IS EMITTED SOURCE, as 31.8's is: `params[s.k]` writes through `s.k` lowered,
376
+ * so the key is read when the input writes and the get reads it too — the binding follows the
377
+ * key. A `!` is a type and no step. `undefined` for an operand that is not a name followed by
378
+ * steps, which `reference` lowers as it stands — save 32.76's optional step, which is refused.
377
379
  */
378
380
  private boundPath(span: Span): string | undefined {
379
- if (this.checking) return undefined
380
381
  const source = this.projection.text
381
382
  const text = source.slice(span.start, span.end)
382
383
  const from = span.start + (text.length - text.trimStart().length)
383
384
  const to = span.start + text.trimEnd().length
384
- const reads = this.answer.reads
385
- const read = reads[this.answer.firstRead(from)]
385
+ const read = this.answer.reads[this.answer.firstRead(from)]
386
386
  if (
387
387
  read === undefined ||
388
388
  read.from !== from ||
@@ -390,12 +390,41 @@ export class Lowering {
390
390
  read.item !== -1
391
391
  )
392
392
  return undefined
393
- const path = source.slice(read.to, to)
394
- if (!/^(\s*\.\s*[A-Za-z_$][\w$]*)+$/.test(path)) return undefined
395
- const name = this.nameOf(read)
393
+ const tokens = tokenize(source.slice(read.to, to))
396
394
  const segments: string[] = []
397
- for (const [, segment] of path.matchAll(/\.\s*([A-Za-z_$][\w$]*)/g))
398
- segments.push(JSON.stringify(segment))
395
+ for (let at = 0; at < tokens.length; at += 1) {
396
+ const token = tokens[at] as Token
397
+ if (token.kind === SyntaxKind.ExclamationToken) continue
398
+ if (token.kind === SyntaxKind.QuestionDotToken)
399
+ throw new Error(
400
+ `abide: bind:{${source.slice(from, to)}} writes through an optional step, and nothing can be written through one. Bind the path without ?., or a { get, set } pair. RULEBOOK 32.76.`,
401
+ )
402
+ if (token.kind === SyntaxKind.DotToken) {
403
+ at += 1
404
+ const step = tokens[at]
405
+ if (step === undefined || !/^[A-Za-z_$][\w$]*$/.test(step.text))
406
+ return undefined
407
+ segments.push(JSON.stringify(step.text))
408
+ continue
409
+ }
410
+ if (token.kind !== SyntaxKind.OpenBracketToken) return undefined
411
+ let depth = 1
412
+ let close = at + 1
413
+ for (; close < tokens.length; close += 1) {
414
+ depth += nesting((tokens[close] as Token).kind)
415
+ if (depth === 0) break
416
+ }
417
+ if (depth > 0 || close === at + 1) return undefined
418
+ segments.push(
419
+ this.lower({
420
+ start: read.to + token.end,
421
+ end: read.to + (tokens[close] as Token).start,
422
+ }).trim(),
423
+ )
424
+ at = close
425
+ }
426
+ if (segments.length === 0) return undefined
427
+ const name = this.nameOf(read)
399
428
  return `{ get: () => ${this.lowerChunk({ start: from, end: to })}, set: (${STORED}) => ${name}.set(${this.use('writePath')}(${name}(), [${segments.join(', ')}], ${STORED})) }`
400
429
  }
401
430
 
@@ -184,7 +184,6 @@ export function emitClient(
184
184
  const classified = classifyAttributes(node, lowering)
185
185
  if (!classified.some(isBound)) continue
186
186
  const units = elementUnits(element, classified, slot, lowering)
187
- for (const unit of units.handlers) lines.push(unit)
188
187
  for (const unit of units.attributes) lines.push(unit)
189
188
  for (const unit of units.elements) lines.push(unit)
190
189
  }
@@ -562,7 +561,7 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
562
561
 
563
562
  for (const line of walkTo(positions, live, lowering)) chain.push(line)
564
563
 
565
- // THE UNITS — text, listeners, attributes, the regions, then the node handed over. The walk
564
+ // THE UNITS — text, attributes and listeners, the regions, then the node handed over. The walk
566
565
  // does not depend on the order.
567
566
  // 32.68 — A `bind:element` CALLBACK RUNS LAST, so the node it is handed is whole: a child
568
567
  // component, an arm or a list inside it has built, and `form.querySelector('input')` finds an
@@ -571,7 +570,6 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
571
570
  // (`Scope.itemWatch`).
572
571
  const items: string[] = []
573
572
  const texts: string[] = []
574
- const handlers: string[] = []
575
573
  const attributes: string[] = []
576
574
  const elements: string[] = []
577
575
  const regions: PlacedNode[] = []
@@ -595,13 +593,11 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
595
593
  lowering,
596
594
  )
597
595
  for (const unit of units.items) items.push(unit)
598
- for (const unit of units.handlers) handlers.push(unit)
599
596
  for (const unit of units.attributes) attributes.push(unit)
600
597
  for (const unit of units.elements) elements.push(unit)
601
598
  }
602
599
  for (const unit of items) lines.push(unit)
603
600
  for (const unit of texts) lines.push(unit)
604
- for (const unit of handlers) lines.push(unit)
605
601
  for (const unit of attributes) lines.push(unit)
606
602
 
607
603
  // 32.67 — WHAT A ROW'S REGIONS AND COMPONENTS BIND reads the row's item through its cell: the
@@ -751,7 +747,6 @@ function elementUnits(
751
747
  lowering: Lowering,
752
748
  ): {
753
749
  items: string[]
754
- handlers: string[]
755
750
  attributes: string[]
756
751
  elements: string[]
757
752
  } {
@@ -849,9 +844,13 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
849
844
  items.push(`const __i${slot} = ${element}\n${itemWrites.join('\n')}`)
850
845
  if (writes.length > 0)
851
846
  attributes.push(`const __a${slot} = ${element}\n${writes.join('\n')}`)
847
+ // 32.75 — A BIND'S LISTENER IS ADDED AHEAD OF THE AUTHOR'S, written or spread, so an
848
+ // `onchange` reading the operand reads what was just picked. After the attribute writes, since
849
+ // a bind reads what they set (`type`, for a number).
852
850
  for (const unit of binds) attributes.push(unit)
851
+ for (const unit of handlers) attributes.push(unit)
853
852
  for (const unit of spreads) attributes.push(unit)
854
- return { items, handlers, attributes, elements }
853
+ return { items, attributes, elements }
855
854
  }
856
855
 
857
856
  // A region's bindings go through its SCOPE, so what replaced the region can stop them (see
@@ -16,7 +16,7 @@ import { PRIVATE_HANDLERS_ADDRESS } from '#shared/ADDRESSES.ts'
16
16
  import { parseRoutePattern } from '#shared/routePattern.ts'
17
17
  import type { PlacedHandler, PlacedSource } from './appLayout.ts'
18
18
  import { declaredExports } from './declaredExports.ts'
19
- import { PRIVATE_EXPORT } from './projectShadow.ts'
19
+ import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
20
20
  import type { Refusals } from './Refusals.ts'
21
21
  import type { Diagnosis, TypeSession } from './TypeSession.ts'
22
22
 
@@ -1,4 +1,5 @@
1
1
  import { SyntaxKind } from 'typescript/unstable/ast'
2
+ import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
2
3
  import { isEventName } from '#shared/isEventName.ts'
3
4
  import { isComponentTag, propKeyOf } from './classifyTemplate.ts'
4
5
  import { isNamedAlone } from './isNamedAlone.ts'
@@ -768,9 +769,6 @@ function refuseExport(source: string): void {
768
769
  }
769
770
  }
770
771
 
771
- /** The prefix a server block's bindings are re-exported under, for the build alone (16.70). */
772
- export const PRIVATE_EXPORT = '__handler_'
773
-
774
772
  /**
775
773
  * 16.70 — the `export { … }` re-exporting each name the server blocks bind under `PRIVATE_EXPORT`,
776
774
  * or `''` where they bind none. One spelling for the checker's view and the emitted module, which
@@ -122,6 +122,9 @@ export function namesViewTransition(css: string): boolean {
122
122
  return false
123
123
  }
124
124
 
125
+ // Whitespace and comments ahead of a statement, which do not decide whether it is an at-rule.
126
+ const LEADING_COMMENTS = /^(?:\s|\/\*[\s\S]*?\*\/)*/
127
+
125
128
  // The block at-rules that hold rules, which `scopeRules` walks into.
126
129
  const NESTING =
127
130
  /^@(media|supports|container|layer|document|starting-style|scope)\b/i
@@ -155,7 +158,7 @@ function scopeRules(css: string, attribute: string): string {
155
158
  }
156
159
  const close = matching(css, open)
157
160
  const body = css.slice(open + 1, close)
158
- const trimmed = prelude.trim()
161
+ const trimmed = prelude.replace(LEADING_COMMENTS, '').trim()
159
162
  if (trimmed.startsWith('@'))
160
163
  out += NESTING.test(trimmed)
161
164
  ? `${prelude}{${scopeRules(body, attribute)}}`
@@ -17,6 +17,12 @@ export const TEXT_QUERY = '?text'
17
17
  */
18
18
  export const CLIENT_QUERY = '?client'
19
19
 
20
+ /**
21
+ * The file's server module, whoever asks — a browser module outside a bundle binding its private
22
+ * handlers (38.121), where `?component` would answer it with itself.
23
+ */
24
+ export const SERVER_QUERY = '?server'
25
+
20
26
  /**
21
27
  * What a handler file is imported as: its own path with this after it, so the plugin can serve it
22
28
  * between the calls that file its hooks (37.1) and a browser its wrapper (16.74). A stack frame names
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The prefix a server block's bindings are re-exported under (16.70): what the build's handler table
3
+ * reads a private handler out of, and what a browser module loaded outside a bundle binds it from.
4
+ */
5
+ export const PRIVATE_EXPORT = '__handler_'
@@ -40,10 +40,12 @@ import {
40
40
  COMPONENT_QUERY,
41
41
  HANDLER_FACE,
42
42
  HANDLER_SUFFIX,
43
+ SERVER_QUERY,
43
44
  STYLE_QUERY,
44
45
  TEXT_QUERY,
45
46
  } from './MODULE_QUERIES.ts'
46
47
  import { MOUNTABLE } from './MOUNTABLE.ts'
48
+ import { PRIVATE_EXPORT } from './PRIVATE_EXPORT.ts'
47
49
  import { realOrAsGiven } from './realOrAsGiven.ts'
48
50
  import { SERVER_TABLE } from './TABLES.ts'
49
51
  import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
@@ -280,6 +282,7 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
280
282
  asking('style', STYLE_QUERY, abideWith(STYLE_QUERY))
281
283
  asking('component', COMPONENT_QUERY, abideWith(COMPONENT_QUERY))
282
284
  asking('client', CLIENT_QUERY, abideWith(CLIENT_QUERY))
285
+ asking('server', SERVER_QUERY, abideWith(SERVER_QUERY))
283
286
  asking('abide', '', /\.abide$/)
284
287
  build.onResolve(
285
288
  { filter: new RegExp(`/${VIRTUAL_DIRECTORY}/[^/]+\\.ts$`) },
@@ -329,10 +332,11 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
329
332
  },
330
333
  )
331
334
 
332
- // A RELATIVE SPECIFIER A FACE IMPORTS AT RUN TIME is resolved from the file the face
333
- // stands for: bun's runtime reads one from an importer in a namespace as no path at
334
- // all. A bare one it resolves itself. Registered after every face, so a `.abide` is
335
- // still answered as one — and NOT IN A BUNDLE, which needs it for no face it serves,
335
+ // A RELATIVE OR `#` SPECIFIER A FACE IMPORTS AT RUN TIME is resolved from the file the
336
+ // face stands for: bun's runtime reads one from an importer in a namespace as no path
337
+ // at all, and a `#` one, asked by `import()`, as a package. A bare one it resolves
338
+ // itself. Registered after every face, so a `.abide` is still answered as one — and
339
+ // NOT IN A BUNDLE, which needs it for no face it serves,
336
340
  // and where a hook matching every relative import in a package costs the bundle that
337
341
  // package's `sideEffects`: a two-page app grew by 12 KB, the validator one page never
338
342
  // calls moved into the chunk both pages load.
@@ -342,7 +346,7 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
342
346
  build.onResolve({ filter: HANDLER_FACE }, (args) => ({
343
347
  path: args.path,
344
348
  }))
345
- build.onResolve({ filter: /^\.\.?\// }, (args) =>
349
+ build.onResolve({ filter: RELATIVE_OR_SUBPATH }, (args) =>
346
350
  resolving || pathOf(args.importer) === args.importer
347
351
  ? undefined
348
352
  : { path: resolved(args.path, args.importer) },
@@ -379,10 +383,16 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
379
383
  })
380
384
  build.onLoad(
381
385
  { filter: /.*/, namespace: CLIENT_NAMESPACE },
382
- (args) => ({
383
- contents: compiledAt(served, realFileOf(args.path)).client,
384
- loader: 'ts',
385
- }),
386
+ (args) => {
387
+ const path = realFileOf(args.path)
388
+ return {
389
+ contents: withPrivateHandlers(
390
+ compiledAt(served, path).client,
391
+ path,
392
+ ),
393
+ loader: 'ts',
394
+ }
395
+ },
386
396
  )
387
397
  build.onLoad(
388
398
  { filter: /.*/, namespace: BOUND_NAMESPACE },
@@ -425,6 +435,22 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
425
435
  }
426
436
  }
427
437
 
438
+ // 38.121 — A BROWSER MODULE OUTSIDE A BUNDLE HOLDS EACH PRIVATE HANDLER ITSELF, bound from its file's
439
+ // server module, where a bundle's holds the wrapper 16.28 states: no server answers the address that
440
+ // wrapper would fetch, and a handler file's import is already the module the server mounts. Each
441
+ // wrapper is one line of `wireLine`'s, `const <name> = __wire(…)`, and an import is hoisted, so the
442
+ // line is swapped where it stands.
443
+ const PRIVATE_WRAPPER = /^const ([\w$]+) = __wire\(.*$/gm
444
+
445
+ function withPrivateHandlers(client: string, path: string): string {
446
+ const server = JSON.stringify(`${path}${SERVER_QUERY}`)
447
+ return client.replace(
448
+ PRIVATE_WRAPPER,
449
+ (_line, name: string) =>
450
+ `import { ${PRIVATE_EXPORT}${name} as ${name} } from ${server}`,
451
+ )
452
+ }
453
+
428
454
  // AN IMPORTER IN ONE OF THIS PLUGIN'S NAMESPACES CARRIES IT in front of its path at run time — with
429
455
  // a `/` in front of that again, where the bundler hands the path alone.
430
456
  function pathOf(importer: string): string {
@@ -3,7 +3,7 @@
3
3
  // adapter that hands bun's hooks to it, because bun decides which hook asks (an `onResolve` for a
4
4
  // specifier, an `onLoad` for a path), and this decides what the answer is.
5
5
 
6
- import { HANDLER_SUFFIX } from './MODULE_QUERIES.ts'
6
+ import { HANDLER_SUFFIX, SERVER_QUERY } from './MODULE_QUERIES.ts'
7
7
 
8
8
  // A VIRTUAL FACE'S PATH: its file's, with a suffix that is not a query. Both registries key a module
9
9
  // by its path whatever its namespace, and the runtime's drops a query from it — so a file's text, its
@@ -33,14 +33,15 @@ export type Face = { path: string; namespace: string }
33
33
 
34
34
  /**
35
35
  * What an import asks of a file, by its spelling: its text (`?text`), a component's stylesheet
36
- * (`?style`), the module it compiles to (`?component`), its browser module (`?client`), a `.abide`
37
- * named bare, or a module that may be a handler file.
36
+ * (`?style`), the module it compiles to (`?component`), its browser module (`?client`), its server
37
+ * module (`?server`), a `.abide` named bare, or a module that may be a handler file.
38
38
  */
39
39
  export type Asked =
40
40
  | 'text'
41
41
  | 'style'
42
42
  | 'component'
43
43
  | 'client'
44
+ | 'server'
44
45
  | 'abide'
45
46
  | 'handler'
46
47
 
@@ -60,6 +61,8 @@ export type HandlerFace = { scope: string; client: string }
60
61
  * - 32.17 — the module a `.abide` compiles to: in a browser's lane its browser module, else its
61
62
  * server module — in a namespace of its own in a bundle, where bun refuses a `file` path with `..`
62
63
  * in it, which a `[...rest]` directory writes (23.5);
64
+ * - 38.121 — its server module, to a browser module outside a bundle binding its private handlers,
65
+ * and never in a browser's lane (16.28);
63
66
  * - 34.16 — a `.abide` named bare is a module abide did not compile asking: on a server its bound
64
67
  * face, and in a browser the component module itself;
65
68
  * - 16.2, 16.74 — a handler file, however its import is spelled and whoever imports it — the app or a
@@ -89,6 +92,12 @@ export function faceOf(
89
92
  return lane.browser
90
93
  ? { path: file, namespace: COMPONENT_NAMESPACE }
91
94
  : { path: `${file}${CLIENT}`, namespace: CLIENT_NAMESPACE }
95
+ case 'server':
96
+ return lane.browser
97
+ ? new Error(
98
+ `abide: ${realFileOf(importer)} imports ${file}${SERVER_QUERY}, the server module, into the browser. Call its handlers by name. RULEBOOK 16.28.`,
99
+ )
100
+ : { path: file, namespace: componentNamespaceOf(lane) }
92
101
  case 'abide':
93
102
  return inBrowser || importer === `${file}${BOUND}`
94
103
  ? component()
@@ -1706,13 +1706,13 @@ function windowHolds(cell: Cell, cap: Cap): boolean {
1706
1706
  /**
1707
1707
  * Whether an arrival passes the window; the first landing always does.
1708
1708
  *
1709
- * 5.15: the first landing goes through, having nothing to collapse against — and a debounce's
1710
- * window is not opened by it, where a throttle's is.
1709
+ * 5.15: the first landing goes through, having nothing to collapse against — and it opens no
1710
+ * window, a throttle's included. 5.12: the first CHANGE is the one a throttle lets through, and a
1711
+ * landing is not a change; a window opened here held back the first `set` after construction, so
1712
+ * `s.set(v); s()` read the value it was built with.
1711
1713
  */
1712
1714
  function passes(cell: Cell, cap: Cap): boolean {
1713
- if (cell.landed) return !windowHolds(cell, cap)
1714
- if (cap.leading) openWindow(cell, cap)
1715
- return true
1715
+ return !cell.landed || !windowHolds(cell, cap)
1716
1716
  }
1717
1717
 
1718
1718
  /**
@@ -3255,11 +3255,16 @@ function bringCurrent(cell: Cell): void {
3255
3255
  if (cell.state !== CLEAN) resolve(cell)
3256
3256
  }
3257
3257
 
3258
- /** What every probe does before it answers: subscribes the reader (3.3), then brings `cell` current. */
3258
+ /**
3259
+ * What every probe does before it answers: subscribes the reader (3.3), then brings `cell` current.
3260
+ *
3261
+ * 3.14 — A MUTATION'S VALUE IS ANSWERED AS IT STANDS: its body is the write, so bringing it current
3262
+ * after a sweep invalidated it (13.16) sent the write again from a `save.busy()`.
3263
+ */
3259
3264
  function probe(cell: Cell): void {
3260
3265
  const reader = TRACKING.reader
3261
3266
  if (reader !== null) trackProbe(cell, reader)
3262
- bringCurrent(cell)
3267
+ if (!cell.mutation) bringCurrent(cell)
3263
3268
  }
3264
3269
 
3265
3270
  function readyNow(cell: Cell): boolean {
@@ -149,6 +149,19 @@ export function scoped(
149
149
  return read
150
150
  }
151
151
 
152
+ /**
153
+ * Whether the value `value` forwards to is built for the scope that is current — always, for a
154
+ * value that is no forwarder. Asking builds nothing, where `instanceOf` builds what it finds unbuilt.
155
+ */
156
+ export function builtHere(value: unknown): boolean {
157
+ const filed = (value as Partial<Forwarder> | null)?.[FILED]
158
+ if (filed === undefined) return true
159
+ const scope = filed.global ? undefined : currentScope()
160
+ return scope === undefined
161
+ ? filed.home !== UNBUILT
162
+ : scope.instances.has(filed)
163
+ }
164
+
152
165
  /**
153
166
  * Whether `value` is request-local on this server — built without `global`, outside every request.
154
167
  */
package/src/shared/rpc.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  import { type Failed, isFailedNamed } from './Failed.ts'
17
17
  import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
18
18
  import { HandlerRoute } from './HandlerRoute.ts'
19
+ import type { Issues } from './Issues.ts'
19
20
  import { IN_PROCESS_ORIGIN } from './IN_PROCESS_ORIGIN.ts'
20
21
  import { compose, type Middleware } from './Middleware.ts'
21
22
  import { outbox, type QueuedCall, sendingFor } from './outbox.ts'
@@ -50,7 +51,7 @@ import {
50
51
  import { MUTATION } from './reactive/MUTATION.ts'
51
52
  import { dropAnswered } from './reactive/Reactive.ts'
52
53
  import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
53
- import { isForwarder } from './reactive/scoped.ts'
54
+ import { builtHere, isForwarder } from './reactive/scoped.ts'
54
55
  import { checkArgs } from './refuse.ts'
55
56
  import { rpcRequest } from './rpcRequest.ts'
56
57
  import type { Schema } from './Schema.ts'
@@ -74,7 +75,9 @@ export type CallOptions<Args = unknown, Value = unknown> = {
74
75
  *
75
76
  * 16.8 — AND ITS FAILURES CARRY THE TRANSPORT'S `HttpError`, which no handler declares and every
76
77
  * call can be answered with: a body over `maxBodySize` (413), a closed `crossOrigin` (403), a
77
- * `timeout` (504), and any failure status whose body was not a `Failed`, rebuilt by `answered`.
78
+ * `timeout` (504), and any failure status whose body was not a `Failed`, rebuilt by `answered`. And
79
+ * the `ValidationError` the wire holds args to the annotation with (19.9), where `Annotated` says it
80
+ * derives a schema at all.
78
81
  */
79
82
  export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
80
83
  ...args: undefined extends Args
@@ -83,7 +86,7 @@ export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
83
86
  ) => Reactive<
84
87
  Value,
85
88
  Value,
86
- Failures | Failed<'HttpError', undefined>,
89
+ Failures | Failed<'HttpError', undefined> | Annotated<Args>,
87
90
  Produced
88
91
  >) & {
89
92
  method: Method
@@ -104,7 +107,7 @@ export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
104
107
  error: unknown,
105
108
  name: Name,
106
109
  ): error is Extract<
107
- Failures | Failed<'HttpError', undefined>,
110
+ Failures | Failed<'HttpError', undefined> | Annotated<Args>,
108
111
  { name: Name }
109
112
  >
110
113
  /**
@@ -130,6 +133,35 @@ export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
130
133
  guarded: boolean
131
134
  }
132
135
 
136
+ /**
137
+ * The `ValidationError` the wire answers args that do not hold to the schema 19.9 derives from
138
+ * `Args`, and `never` where the derived schema is no object's and the wire holds nothing to it.
139
+ *
140
+ * MIRRORS `argsSchemaOf`, which reads the same `Args` off the `Rpc`'s call: an object's schema is
141
+ * derived for ONE record, `undefined` dropped — not a scalar, a list, a callable or a union of
142
+ * records (`anyOf`), and not `any`, `unknown` or `object`, which constrain nothing. `object` is told
143
+ * from `{}`, the one other record with no keys, by `string`, which only `{}` takes.
144
+ */
145
+ type Annotated<Args> = 0 extends 1 & Args
146
+ ? never
147
+ : unknown extends Args
148
+ ? never
149
+ : Derived<Exclude<Args, undefined>>
150
+ // `Member` is distributed over and `Whole` is not: a member that is not the whole is one arm of a union.
151
+ type Derived<Member, Whole = Member> = Member extends
152
+ | readonly unknown[]
153
+ | ((...given: never[]) => unknown)
154
+ ? never
155
+ : Member extends object
156
+ ? [Whole] extends [Member]
157
+ ? [keyof Member] extends [never]
158
+ ? [string] extends [Member]
159
+ ? Failed<'ValidationError', Issues<Whole>>
160
+ : never
161
+ : Failed<'ValidationError', Issues<Whole>>
162
+ : never
163
+ : never
164
+
133
165
  /** Which generated surfaces carry a handler (20.1). */
134
166
  export type Clients = {
135
167
  ui?: boolean
@@ -307,6 +339,18 @@ type Declare = {
307
339
  ): Rpc<Kept<Shaped>, undefined, Refusals<Shaped, Gate, Accepted>>
308
340
  }
309
341
 
342
+ /** What a handler's selection members answer before a call has built its value: no entries' answers. */
343
+ const UNREACHED: Record<
344
+ (typeof SELECTION_MEMBERS)[number],
345
+ (pattern?: unknown) => unknown
346
+ > = {
347
+ busy: () => false,
348
+ ready: () => true,
349
+ synced: () => true,
350
+ refresh: () => undefined,
351
+ invalidate: () => undefined,
352
+ }
353
+
310
354
  /** Builds the handler declarer for one method — what `GET`, `POST` and the rest are. */
311
355
  export function declare(method: Method): Declare {
312
356
  return ((handler: unknown, options?: RpcOptions): unknown => {
@@ -421,10 +465,17 @@ export function declare(method: Method): Declare {
421
465
  }
422
466
  // THE HANDLER'S OWN MEMBERS, handed the pattern. A `Keyed` narrows by it and a `Reactive`'s
423
467
  // take no argument, so one spelling serves both arms and neither is a second mechanism.
424
- const face = value as Record<
468
+ //
469
+ // 13.19 — AND NOT YET BUILT, NONE OF THEM BUILDS IT: building an unkeyed value starts its body
470
+ // (1.7), so `save.busy()` over a server's never-called `POST(async () => …)` ran the write and
471
+ // reported it in flight. Until a call reaches it the handler has no entry, and answers as one
472
+ // with no entries does.
473
+ const built = value as Record<
425
474
  (typeof SELECTION_MEMBERS)[number],
426
475
  (pattern?: unknown) => unknown
427
476
  >
477
+ const faceHere = (): typeof built =>
478
+ builtHere(value) ? built : UNREACHED
428
479
  const rpc = Object.assign(call, contract, {
429
480
  method,
430
481
  description: options?.description,
@@ -473,16 +524,16 @@ export function declare(method: Method): Declare {
473
524
  members[name] =
474
525
  name === 'invalidate' || name === 'refresh'
475
526
  ? (pattern?: unknown): unknown => {
476
- const done = face[name](pattern)
527
+ const done = faceHere()[name](pattern)
477
528
  RPC_SERVER.broadcast?.(rpc, name, pattern)
478
529
  return done
479
530
  }
480
- : (pattern?: unknown): unknown => face[name](pattern)
531
+ : (pattern?: unknown): unknown => faceHere()[name](pattern)
481
532
  }
482
533
  // 46.11 — BUSY TOO WHILE A CALL THE OUTBOX HOLDS FOR IT IS IN FLIGHT: a queued call sent is work
483
534
  // in flight for this handler, as a call made now is. 46.1 — and the calls it holds for it.
484
535
  members.busy = (pattern?: unknown): boolean =>
485
- (face.busy(pattern) as boolean) ||
536
+ (faceHere().busy(pattern) as boolean) ||
486
537
  sendingFor(rpc, pattern as Record<string, unknown> | undefined)
487
538
  members.outbox = (pattern?: unknown): QueuedCall[] =>
488
539
  outbox(rpc as never, pattern as Record<string, unknown> | undefined)