@abide/abide 0.116.2 → 0.116.4

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.4
4
+
5
+ ### Patch Changes
6
+
7
+ - f4acbfd: a handler whose args schema is derived from its type annotation (no `args` gate) now refuses bad args in process as it does over the wire — a direct call, a call from another handler or `app.run`, and every call under `mount` answer 422 `ValidationError` with the same `Issues` before the body runs, where before the body ran with the bad value
8
+ - be88738: a reader that probed a handler before its first call wakes when the call starts — on a server and under `mount`, `disabled={save.busy()}` read before any call stayed enabled for the whole write, because the unreached answer (0.116.3) subscribed the reader to nothing. It now subscribes the reader to the first call in its scope, and still builds nothing and runs no handler (RULEBOOK 3.3, 13.19)
9
+
10
+ ## 0.116.3
11
+
12
+ ### Patch Changes
13
+
14
+ - 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).
15
+ - 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
16
+ - 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
17
+ - 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)
18
+ - 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'"
19
+ - 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
20
+ - 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
21
+
3
22
  ## 0.116.2
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
@@ -223,11 +223,11 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
223
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
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 |
225
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 |
226
- | `rpc.busy` | `(pattern?: Partial<Args>) => boolean` | `busy` narrowed to the handler's matching entries. | 13.2, 13.3, 13.12, 46.11 |
227
- | `rpc.ready` | `(pattern?: Partial<Args>) => boolean` | `ready` narrowed to the handler's matching entries. | 13.11, 13.12 |
228
- | `rpc.synced` | `(pattern?: Partial<Args>) => boolean` | `synced` narrowed to the handler's matching entries. | 13.12, 13.18 |
229
- | `rpc.invalidate` | `(pattern?: Partial<Args>) => void` | `invalidate` narrowed to the handler's matching entries. | 7.2, 7.3, 13.4, 13.12 |
230
- | `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 |
231
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 |
232
232
  | `rpc.raw` | `(args: Args, init?: RequestInit) => Promise<Response>` | The same call handed back as the raw response. | 16.11, 16.42 |
233
233
  | `rpc.method` | `string` | The HTTP method. | 16.2 |
@@ -552,14 +552,14 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
552
552
  | formatting whitespace | What a run of whitespace renders as. | 33.17, 33.18 |
553
553
  | `{raw(...)}` | Raw HTML. | 32.2, 32.25, 32.26, 32.30, 32.31, 32.70 |
554
554
  | `name={expr}` | A reactive attribute or property. | 32.3, 32.20, 32.23, 32.37, 32.70, 32.71 |
555
- | `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 |
556
556
  | `name="…{expr}…"` | An interpolated quoted value. | 32.3, 32.71 |
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 |
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 |
559
- | `bind:checked` | A boolean bind on an input. | 32.7, 32.72, 32.74 |
560
- | `bind:open` | The same, on a `<details>`. | 32.7, 32.72, 32.74 |
561
- | `bind:group` | Radio or checkbox membership. | 32.8, 32.72, 32.74 |
562
- | `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 |
563
563
  | `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | 32.5, 32.6 |
564
564
  | `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | 32.9, 32.38, 32.66, 32.68, 32.69 |
565
565
  | `class:name={cond}` | Toggles a class. | 32.10, 32.71 |
@@ -738,7 +738,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
738
738
 
739
739
  | Name | Signature | Meaning | Rules |
740
740
  | --- | --- | --- | --- |
741
- | `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 |
742
742
  | `Mounted` | `{ stop(): void; [Symbol.dispose](): void }` | A component `mount` hydrated, until it is stopped. | 38.119 |
743
743
 
744
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,10 @@ 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, and reading `busy`, `ready` or
758
+ `synced` there MUST subscribe the reader to the first call that reaches it in that scope.
759
+
754
760
  # 14. Tracking
755
761
 
756
762
  14.1 A `Reactive` MUST be tracked only where something downstream will be rebuilt from it.
@@ -885,8 +891,9 @@ alone.
885
891
  options declare one.
886
892
 
887
893
  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.
894
+ declared, the undeclared `Failed` named `HttpError` per 17.4, with no data, which the transport
895
+ may answer any call with, and, wherever the `Schema` 19.9 derives from its `Args` is an object's,
896
+ the undeclared `ValidationError` of 15.8 with `Issues` of the `Args` as its data.
890
897
 
891
898
  16.9 `rpc.url` MUST take the `Args`, MUST write each parameter of its route into the path, and MUST
892
899
  carry the rest in the query on a `GET` or a `DELETE` alone.
@@ -1870,7 +1877,7 @@ as a name bound to a `Reactive` is, per 31.1 to 31.7.
1870
1877
  component.
1871
1878
 
1872
1879
  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).
1880
+ path where its operand is a path into a value (31.8), a computed step's key read at each write.
1874
1881
 
1875
1882
  32.6 `bind:prop` on a component MUST hand the child the caller's `Reactive`, or its `{ get, set }` pair,
1876
1883
  as that prop's write path.
@@ -2086,6 +2093,12 @@ nothing for it.
2086
2093
  32.74 A `bind:` on an element naming none of `value`, `checked`, `open`, `group`, `files` and
2087
2094
  `element` MUST be a build error.
2088
2095
 
2096
+ 32.75 A `bind:` on an element MUST write its operand before any `on<event>` listener on that
2097
+ element, written or spread, runs for the same event, wherever the two are written in the tag.
2098
+
2099
+ 32.76 A `bind:` whose operand is a path into a value that takes an optional step (`?.`) MUST be a
2100
+ build error.
2101
+
2089
2102
  # 33. Props
2090
2103
 
2091
2104
  33.1 `props` MUST be callable from a `<script>` block only, and calling it from a `<script module>`
@@ -2750,6 +2763,9 @@ hydration woke before it settles; it MUST throw where the component was not impo
2750
2763
  `ABIDE_PRINCIPAL_SECRET` MUST seal and verify the principal cookie with a secret the preload
2751
2764
  supplies, and `abide start` MUST NOT supply that secret.
2752
2765
 
2766
+ 38.121 Under `mount`, a call the component's browser module makes to a private handler MUST be
2767
+ answered in the test's process by the handler the component's server module declares.
2768
+
2753
2769
  # 39. Head and view transitions
2754
2770
 
2755
2771
  39.1 A `<head>` element in a `.abide` file MUST contribute its children to the document head.
package/docs/abide.md CHANGED
@@ -6856,10 +6856,10 @@ Arrive here knowing which name you want. Each page is read off the registry: the
6856
6856
 
6857
6857
  | Name | Signature | Meaning | Rules |
6858
6858
  | --- | --- | --- | --- |
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), [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), [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), [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) |
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) |
6863
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) |
6864
6864
 
6865
6865
  ## Triggers
@@ -7407,11 +7407,11 @@ Reactive primitives: what is written in place rather than imported.
7407
7407
  | `Value` | `unknown` | What the addressed `Reactive` holds. | [16.3](/rulebook#16.3), [16.30](/rulebook#16.30) |
7408
7408
  | `Produced` | `unknown` | What it yields, per the unit rule. | [1.2](/rulebook#1.2), [16.3](/rulebook#16.3) |
7409
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) |
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), [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) |
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) |
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) |
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) |
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) |
7415
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) |
7416
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) |
7417
7417
  | `rpc.method` | `string` | The HTTP method. | [16.2](/rulebook#16.2) |
@@ -8899,14 +8899,14 @@ Generated surfaces: what is written in place rather than imported.
8899
8899
  | formatting whitespace | What a run of whitespace renders as. | [33.17](/rulebook#33.17), [33.18](/rulebook#33.18) |
8900
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) |
8901
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) |
8902
- | `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) |
8903
8903
  | `name="…{expr}…"` | An interpolated quoted value. | [32.3](/rulebook#32.3), [32.71](/rulebook#32.71) |
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) |
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) |
8906
- | `bind:checked` | A boolean bind on an input. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
8907
- | `bind:open` | The same, on a `<details>`. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
8908
- | `bind:group` | Radio or checkbox membership. | [32.8](/rulebook#32.8), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
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) |
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) |
8910
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) |
8911
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) |
8912
8912
  | `class:name={cond}` | Toggles a class. | [32.10](/rulebook#32.10), [32.71](/rulebook#32.71) |
@@ -9469,7 +9469,7 @@ Configuration: what is written in place rather than imported.
9469
9469
 
9470
9470
  | Name | Signature | Meaning | Rules |
9471
9471
  | --- | --- | --- | --- |
9472
- | `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) |
9473
9473
 
9474
9474
  ## Taught in
9475
9475
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.116.2",
3
+ "version": "0.116.4",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -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
 
@@ -11,6 +11,7 @@ import { COMPONENT_QUERY } from '#server/MODULE_QUERIES.ts'
11
11
  import { ROUTES_TABLE, SERVER_TABLE } from '#server/TABLES.ts'
12
12
  import { VIRTUAL_DIRECTORY } from '#server/VIRTUAL_DIRECTORY.ts'
13
13
  import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
14
+ import type { JsonValue } from '#shared/JsonValue.ts'
14
15
  import { ROUTING_SPECIFIER, WIRE_SPECIFIER } from '#shared/SPECIFIERS.ts'
15
16
  import {
16
17
  type AppLayout,
@@ -317,6 +318,7 @@ async function emitApp(
317
318
  refuseSharedRoutes(routes, mounts, refusals)
318
319
  refuseAddressCollision(routes, mounts, layout.published, refusals)
319
320
  const standIns = standInsOf(mounts)
321
+ const derived = derivedOf(mounts)
320
322
  // 37.1 — every module a server block or a handler file runs in, for the boot to import before
321
323
  // `onStart`.
322
324
  const servers: string[] = []
@@ -329,6 +331,7 @@ async function emitApp(
329
331
  entry,
330
332
  standIns.get(entry.relative),
331
333
  entry.text,
334
+ derived.get(entry.relative),
332
335
  )
333
336
  } catch (thrown) {
334
337
  refusals.at(entry.path, thrown)
@@ -368,6 +371,7 @@ async function emitApp(
368
371
  handlers[mount.source] = {
369
372
  scope: mount.scope,
370
373
  client: standIns.get(mount.source) ?? '',
374
+ args: mount.args,
371
375
  }
372
376
  if (routes.length > 0) {
373
377
  tables[ROUTES_TABLE] = clientTable(routes, transitions)
@@ -624,6 +628,21 @@ function standInsOf(mounts: Mount[]): Map<string, string> {
624
628
  return standIns
625
629
  }
626
630
 
631
+ /**
632
+ * 19.9 — the schema each private handler's annotation derives, by its binding, by source: what its
633
+ * server module holds it to (15.8). A handler file's is its face's (`HandlerFace`).
634
+ */
635
+ function derivedOf(mounts: Mount[]): Map<string, Record<string, JsonValue>> {
636
+ const derived = new Map<string, Record<string, JsonValue>>()
637
+ for (const mount of mounts) {
638
+ if (!mount.private || mount.args === undefined) continue
639
+ const held = derived.get(mount.source) ?? {}
640
+ held[mount.name] = mount.args
641
+ derived.set(mount.source, held)
642
+ }
643
+ return derived
644
+ }
645
+
627
646
  /**
628
647
  * One handler's wrapper, bound as `binding` writes it.
629
648
  *
@@ -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
@@ -1,3 +1,4 @@
1
+ import type { JsonValue } from '#shared/JsonValue.ts'
1
2
  import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
2
3
  import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
3
4
  import { Cursor, placeTemplate } from './Cursor.ts'
@@ -50,6 +51,8 @@ export function emitComponent(
50
51
  route = '/',
51
52
  /** 38.97 — where given, every chunk the module is written around (`sourceMapOf`). */
52
53
  chunks?: LoweredChunk[],
54
+ /** 19.9 — the schema each private handler's annotation derives, by its binding. */
55
+ derived: Readonly<Record<string, JsonValue>> = {},
53
56
  ): string {
54
57
  // Only what the page actually uses. An emitted module carrying an import nothing calls is a
55
58
  // name the app's own tooling reports as unused, in a file nobody wrote.
@@ -91,7 +94,7 @@ export function emitComponent(
91
94
  : `${lowering.use('fileRoute')}(${JSON.stringify(route)})
92
95
  ${serverSetup.body}
93
96
  ${lowering.use('fileRoute')}(undefined)
94
- ${privateExports(file.servers.join('\n'))}`
97
+ ${heldToAnnotations(lowering, derived)}${privateExports(file.servers.join('\n'))}`
95
98
  const setup = splitImports(lowering.scripts())
96
99
  const componentBody = setup.body
97
100
 
@@ -159,6 +162,21 @@ ${level}${closed}
159
162
  `
160
163
  }
161
164
 
165
+ /**
166
+ * 15.8, 19.9 — each private handler held to the schema its annotation derives, once its server block
167
+ * has built it: the module is what the server mounts and what a component under `mount` calls
168
+ * (38.121), so a call is held to it whoever makes it.
169
+ */
170
+ function heldToAnnotations(
171
+ lowering: Lowering,
172
+ derived: Readonly<Record<string, JsonValue>>,
173
+ ): string {
174
+ let out = ''
175
+ for (const name in derived)
176
+ out += `${lowering.use('derivedArgs')}(${name}, ${JSON.stringify(derived[name])})\n`
177
+ return out
178
+ }
179
+
162
180
  /**
163
181
  * An emitted module's imports: each framework name `lowering` used, from the specifier it ships
164
182
  * under, and the author's own from both script blocks, already pointed where they landed
@@ -1,4 +1,5 @@
1
1
  import { STYLE_QUERY } from '#server/MODULE_QUERIES.ts'
2
+ import type { JsonValue } from '#shared/JsonValue.ts'
2
3
  import type { Placement } from '#server/placement.ts'
3
4
  import type { CheckerAnswer } from './CheckerAnswer.ts'
4
5
  import { emitClient } from './emitClient.ts'
@@ -35,6 +36,8 @@ export function emitSource(
35
36
  standIns = '',
36
37
  /** 38.97 — the `.abide`'s text, where the build wants each module's source map. */
37
38
  text?: string,
39
+ /** 19.9 — the schema each private handler it declares derives off its annotation, by name. */
40
+ derived?: Readonly<Record<string, JsonValue>>,
38
41
  ): {
39
42
  server: string
40
43
  client: string
@@ -109,6 +112,7 @@ export function emitSource(
109
112
  kind !== 'component',
110
113
  source.scope,
111
114
  serverChunks,
115
+ derived,
112
116
  )
113
117
  const name = relative.slice(relative.lastIndexOf('/') + 1)
114
118
  const browser = withStandIns(
@@ -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
@@ -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'
@@ -118,7 +120,8 @@ export function serveCompilation(root: string, compilation: Compilation): void {
118
120
  const real = realOrAsGiven(root)
119
121
  const handlerFiles = addCompilation(real, compilation, RUNTIME)
120
122
  installSourceMaps(serverMapsOf(compilation, real), real)
121
- if (handlerFiles.length !== 0) Bun.plugin(handlerFilesPlugin(handlerFiles))
123
+ if (handlerFiles.length !== 0)
124
+ Bun.plugin(handlerFilesPlugin(handlerFiles, RUNTIME.handlers))
122
125
  if (registered) return
123
126
  registered = true
124
127
  Bun.plugin(modulesPlugin(RUNTIME))
@@ -130,7 +133,13 @@ export function serveCompilation(root: string, compilation: Compilation): void {
130
133
  // plugin per compilation because the filter is fixed at registration, and the runtime's `onLoad`
131
134
  // cannot fall through, so the filter names the handler files and nothing else. The face it names
132
135
  // is resolved by `modulesPlugin`, registered once.
133
- function handlerFilesPlugin(paths: string[]): Bun.BunPlugin {
136
+ // 15.8, 19.9 — AND HOLDS ITS HANDLER TO THE SCHEMA ITS ANNOTATION DERIVES, once the face has built it:
137
+ // this module is what every import of the file is answered with — the server's mount, a test's own
138
+ // import, a component's under `mount` — so a call is held to it whoever makes it.
139
+ function handlerFilesPlugin(
140
+ paths: string[],
141
+ faces: ReadonlyMap<string, HandlerFace>,
142
+ ): Bun.BunPlugin {
134
143
  return {
135
144
  name: 'abide-handler-files',
136
145
  setup(build) {
@@ -140,10 +149,18 @@ function handlerFilesPlugin(paths: string[]): Bun.BunPlugin {
140
149
  `^(?:${paths.map(RegExp.escape).join('|')})$`,
141
150
  ),
142
151
  },
143
- (args) => ({
144
- contents: `export { default } from ${JSON.stringify(`${args.path}${HANDLER_SUFFIX}`)}\n`,
145
- loader: 'js',
146
- }),
152
+ (args) => {
153
+ const face = JSON.stringify(`${args.path}${HANDLER_SUFFIX}`)
154
+ const schema = faces.get(args.path)?.args
155
+ return {
156
+ contents:
157
+ `export { default } from ${face}\n` +
158
+ (schema === undefined
159
+ ? ''
160
+ : `import __handler from ${face}\nimport { derivedArgs as __derivedArgs } from '${SERVER_SPECIFIER}'\n__derivedArgs(__handler, ${JSON.stringify(schema)})\n`),
161
+ loader: 'js',
162
+ }
163
+ },
147
164
  )
148
165
  },
149
166
  }
@@ -280,6 +297,7 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
280
297
  asking('style', STYLE_QUERY, abideWith(STYLE_QUERY))
281
298
  asking('component', COMPONENT_QUERY, abideWith(COMPONENT_QUERY))
282
299
  asking('client', CLIENT_QUERY, abideWith(CLIENT_QUERY))
300
+ asking('server', SERVER_QUERY, abideWith(SERVER_QUERY))
283
301
  asking('abide', '', /\.abide$/)
284
302
  build.onResolve(
285
303
  { filter: new RegExp(`/${VIRTUAL_DIRECTORY}/[^/]+\\.ts$`) },
@@ -329,10 +347,11 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
329
347
  },
330
348
  )
331
349
 
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,
350
+ // A RELATIVE OR `#` SPECIFIER A FACE IMPORTS AT RUN TIME is resolved from the file the
351
+ // face stands for: bun's runtime reads one from an importer in a namespace as no path
352
+ // at all, and a `#` one, asked by `import()`, as a package. A bare one it resolves
353
+ // itself. Registered after every face, so a `.abide` is still answered as one — and
354
+ // NOT IN A BUNDLE, which needs it for no face it serves,
336
355
  // and where a hook matching every relative import in a package costs the bundle that
337
356
  // package's `sideEffects`: a two-page app grew by 12 KB, the validator one page never
338
357
  // calls moved into the chunk both pages load.
@@ -342,7 +361,7 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
342
361
  build.onResolve({ filter: HANDLER_FACE }, (args) => ({
343
362
  path: args.path,
344
363
  }))
345
- build.onResolve({ filter: /^\.\.?\// }, (args) =>
364
+ build.onResolve({ filter: RELATIVE_OR_SUBPATH }, (args) =>
346
365
  resolving || pathOf(args.importer) === args.importer
347
366
  ? undefined
348
367
  : { path: resolved(args.path, args.importer) },
@@ -379,10 +398,16 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
379
398
  })
380
399
  build.onLoad(
381
400
  { filter: /.*/, namespace: CLIENT_NAMESPACE },
382
- (args) => ({
383
- contents: compiledAt(served, realFileOf(args.path)).client,
384
- loader: 'ts',
385
- }),
401
+ (args) => {
402
+ const path = realFileOf(args.path)
403
+ return {
404
+ contents: withPrivateHandlers(
405
+ compiledAt(served, path).client,
406
+ path,
407
+ ),
408
+ loader: 'ts',
409
+ }
410
+ },
386
411
  )
387
412
  build.onLoad(
388
413
  { filter: /.*/, namespace: BOUND_NAMESPACE },
@@ -425,6 +450,22 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
425
450
  }
426
451
  }
427
452
 
453
+ // 38.121 — A BROWSER MODULE OUTSIDE A BUNDLE HOLDS EACH PRIVATE HANDLER ITSELF, bound from its file's
454
+ // server module, where a bundle's holds the wrapper 16.28 states: no server answers the address that
455
+ // wrapper would fetch, and a handler file's import is already the module the server mounts. Each
456
+ // wrapper is one line of `wireLine`'s, `const <name> = __wire(…)`, and an import is hoisted, so the
457
+ // line is swapped where it stands.
458
+ const PRIVATE_WRAPPER = /^const ([\w$]+) = __wire\(.*$/gm
459
+
460
+ function withPrivateHandlers(client: string, path: string): string {
461
+ const server = JSON.stringify(`${path}${SERVER_QUERY}`)
462
+ return client.replace(
463
+ PRIVATE_WRAPPER,
464
+ (_line, name: string) =>
465
+ `import { ${PRIVATE_EXPORT}${name} as ${name} } from ${server}`,
466
+ )
467
+ }
468
+
428
469
  // AN IMPORTER IN ONE OF THIS PLUGIN'S NAMESPACES CARRIES IT in front of its path at run time — with
429
470
  // a `/` in front of that again, where the bundler hands the path alone.
430
471
  function pathOf(importer: string): string {
@@ -16,7 +16,7 @@ import type { Gated } from '#shared/reactive/argsContract.ts'
16
16
  import { deadlineOf, heldGenerationOf } from '#shared/reactive/index.ts'
17
17
  import type { Defaults } from '#shared/reactive/parameterDefaults.ts'
18
18
  import { notFound, refuse } from '#shared/refuse.ts'
19
- import type { Method, Rpc } from '#shared/rpc.ts'
19
+ import type { HeldToAnnotation, Method, Rpc } from '#shared/rpc.ts'
20
20
  import { argsInURL } from '#shared/rpcRequest.ts'
21
21
  import { VERSION_HEADER } from '#shared/VERSION_HEADER.ts'
22
22
  import { versionInEtag } from '#shared/versionInEtag.ts'
@@ -118,20 +118,13 @@ export type Mounted = Mount & {
118
118
  * subscription to it both pass through (37.2, 18.26).
119
119
  */
120
120
  rungs: AppRung[]
121
- handler: Rpc<unknown, unknown> & Gated<unknown>
121
+ handler: Rpc<unknown, unknown> & Gated<unknown> & HeldToAnnotation
122
122
  /**
123
123
  * The args' JSON Schema, probed once at mount.
124
124
  *
125
125
  * 19.7, 20.11 — a third-party schema converts itself on every probe.
126
126
  */
127
127
  argsDocument: Document | undefined
128
- /**
129
- * What the wire holds args to where the handler declares no `args` gate of its own.
130
- *
131
- * 19.9 — the annotation's schema. An in-process call already holds the type, so the handler
132
- * never runs it.
133
- */
134
- wireArgs: JsonValue | undefined
135
128
  }
136
129
 
137
130
  /**
@@ -172,7 +165,6 @@ export function mountedOwn(
172
165
  result: undefined,
173
166
  handler: handler as Mounted['handler'],
174
167
  argsDocument: undefined,
175
- wireArgs: undefined,
176
168
  },
177
169
  ]
178
170
  }
@@ -198,7 +190,7 @@ export async function mountHandlers(
198
190
  for (let at = 0; at < table.length; at += 1) {
199
191
  const mount = table[at] as Mount
200
192
  const exported = (modules[at] as Record<string, unknown>)[mount.export]
201
- const handler = exported as Rpc<unknown, unknown> & Gated<unknown>
193
+ const handler = exported as Mounted['handler']
202
194
  const declared = argsDocumentOf(handler)
203
195
  // 16.9, 16.10 — the address `rpc.url` and `rpc.raw` build on, under the mount — and what a
204
196
  // page's render turns a call it made into the browser's request by (41.20).
@@ -210,7 +202,6 @@ export async function mountHandlers(
210
202
  rungs: rungsOver(mount.scope),
211
203
  handler,
212
204
  argsDocument: declared ?? (mount.args as Document | undefined),
213
- wireArgs: declared === undefined ? mount.args : undefined,
214
205
  })
215
206
  }
216
207
  return { handlers }
@@ -2,9 +2,9 @@
2
2
  // off the wire: over http (16.2) and a `GET` followed over the socket mux (18.3). Each lane reads the
3
3
  // args off its own transport and answers a refusal in its own form.
4
4
 
5
+ import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
5
6
  import { normalized } from '#shared/reactive/argsContract.ts'
6
7
  import { validationError } from '#shared/refuse.ts'
7
- import { validateJson } from '#shared/validateJson.ts'
8
8
  import type { Mounted } from './answerRpc.ts'
9
9
  import { coerceArgs } from './coerceArgs.ts'
10
10
 
@@ -16,11 +16,10 @@ import { coerceArgs } from './coerceArgs.ts'
16
16
  * 19.7 — the text as the types the handler's `args` declares. 11.27 — then the defaults, ahead of
17
17
  * every key taken after it: the seed a render filed and the `global` entry standing were both filed
18
18
  * under them (11.29). 19.9 — THE WIRE HELD TO THE ANNOTATION, where no `args` schema is declared to
19
- * hold it. Args that did not come are held as an empty record: a schema is only derived where the
20
- * handler's args are a required record, so a required member missing is refused, not read off nothing.
19
+ * hold it — the check the handler carries (`derivedArgs`), which its in-process call makes too (15.8).
21
20
  */
22
21
  export function argsFor(
23
- found: Pick<Mounted, 'handler' | 'argsDocument' | 'wireArgs'>,
22
+ found: Pick<Mounted, 'handler' | 'argsDocument'>,
24
23
  sent: unknown,
25
24
  query: string | undefined,
26
25
  ): unknown {
@@ -33,9 +32,7 @@ export function argsFor(
33
32
  query,
34
33
  )
35
34
  const args = normalized(found.handler, coerced)
36
- if (found.wireArgs !== undefined) {
37
- const issues = validateJson(found.wireArgs, args ?? {})
38
- if (issues !== null) throw validationError(issues)
39
- }
35
+ const issues = found.handler[DERIVED_ARGS]?.(args) ?? null
36
+ if (issues !== null) throw validationError(issues)
40
37
  return args
41
38
  }
@@ -0,0 +1,25 @@
1
+ import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
2
+ import type { JsonValue } from '#shared/JsonValue.ts'
3
+ import type { Gated } from '#shared/reactive/argsContract.ts'
4
+ import type { HeldToAnnotation } from '#shared/rpc.ts'
5
+ import { validateJson } from '#shared/validateJson.ts'
6
+ import { argsDocumentOf } from './coerceArgs.ts'
7
+
8
+ /**
9
+ * Holds `handler`'s args to `schema`, the one the build derived off its annotation, for every call
10
+ * of it from now on: in process and over the wire (15.8, 19.9).
11
+ *
12
+ * What the module a handler is loaded through calls once its handler is built — a handler file's
13
+ * face and a `<script server>`'s module — so the schema is there wherever the handler can be
14
+ * called, whether or not an app mounted it. Not where its `args` gate publishes a document of its
15
+ * own, which holds the args itself (19.7). Args that did not come are held as an empty record: a
16
+ * schema is only derived where the handler's args are a required record, so a required member
17
+ * missing is refused, not read off nothing.
18
+ */
19
+ export function derivedArgs(
20
+ handler: HeldToAnnotation & Gated<unknown>,
21
+ schema: JsonValue,
22
+ ): void {
23
+ if (argsDocumentOf(handler) !== undefined) return
24
+ handler[DERIVED_ARGS] = (args) => validateJson(schema, args ?? {})
25
+ }
@@ -3,7 +3,8 @@
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 type { JsonValue } from '#shared/JsonValue.ts'
7
+ import { HANDLER_SUFFIX, SERVER_QUERY } from './MODULE_QUERIES.ts'
7
8
 
8
9
  // A VIRTUAL FACE'S PATH: its file's, with a suffix that is not a query. Both registries key a module
9
10
  // by its path whatever its namespace, and the runtime's drops a query from it — so a file's text, its
@@ -33,19 +34,27 @@ export type Face = { path: string; namespace: string }
33
34
 
34
35
  /**
35
36
  * 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.
37
+ * (`?style`), the module it compiles to (`?component`), its browser module (`?client`), its server
38
+ * module (`?server`), a `.abide` named bare, or a module that may be a handler file.
38
39
  */
39
40
  export type Asked =
40
41
  | 'text'
41
42
  | 'style'
42
43
  | 'component'
43
44
  | 'client'
45
+ | 'server'
44
46
  | 'abide'
45
47
  | 'handler'
46
48
 
47
- /** One handler file, as `Compilation.handlers` holds it. */
48
- export type HandlerFace = { scope: string; client: string }
49
+ /**
50
+ * One handler file, as `Compilation.handlers` holds it — and 19.9's schema its default's annotation
51
+ * derives, which the module at the file's path holds it to (`derivedArgs`).
52
+ */
53
+ export type HandlerFace = {
54
+ scope: string
55
+ client: string
56
+ args: JsonValue | undefined
57
+ }
49
58
 
50
59
  /**
51
60
  * The face an import of `file` is answered with, asked `asked` from `importer` in `lane` — or
@@ -60,6 +69,8 @@ export type HandlerFace = { scope: string; client: string }
60
69
  * - 32.17 — the module a `.abide` compiles to: in a browser's lane its browser module, else its
61
70
  * server module — in a namespace of its own in a bundle, where bun refuses a `file` path with `..`
62
71
  * in it, which a `[...rest]` directory writes (23.5);
72
+ * - 38.121 — its server module, to a browser module outside a bundle binding its private handlers,
73
+ * and never in a browser's lane (16.28);
63
74
  * - 34.16 — a `.abide` named bare is a module abide did not compile asking: on a server its bound
64
75
  * face, and in a browser the component module itself;
65
76
  * - 16.2, 16.74 — a handler file, however its import is spelled and whoever imports it — the app or a
@@ -89,6 +100,12 @@ export function faceOf(
89
100
  return lane.browser
90
101
  ? { path: file, namespace: COMPONENT_NAMESPACE }
91
102
  : { path: `${file}${CLIENT}`, namespace: CLIENT_NAMESPACE }
103
+ case 'server':
104
+ return lane.browser
105
+ ? new Error(
106
+ `abide: ${realFileOf(importer)} imports ${file}${SERVER_QUERY}, the server module, into the browser. Call its handlers by name. RULEBOOK 16.28.`,
107
+ )
108
+ : { path: file, namespace: componentNamespaceOf(lane) }
92
109
  case 'abide':
93
110
  return inBrowser || importer === `${file}${BOUND}`
94
111
  ? component()
@@ -52,6 +52,9 @@ export { type App, createApp } from './createApp.ts'
52
52
  // 41.28's run-time half: an emitted component reads it on either side of its setup, and one whose
53
53
  // setup made a call answers as a page does. Not an app's name.
54
54
  export { callsInRender } from './deferredBuilds.ts'
55
+ // 19.9's run-time half: what the module a handler is loaded through calls to hold it to the schema
56
+ // its annotation derives (15.8). Not an app's name.
57
+ export { derivedArgs } from './derivedArgs.ts'
55
58
  // 37.1's run-time half: what a compiled server module calls around its `<script server>`, so the
56
59
  // hooks it makes are filed at its route.
57
60
  export {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The member of an `Rpc` holding its args to the schema its annotation derives (19.9): stamped by the
3
+ * module its handler is loaded through, on the server alone, and read by every call of it — in
4
+ * process and over the wire alike (15.8).
5
+ */
6
+ export const DERIVED_ARGS: unique symbol = Symbol.for('abide.derivedArgs')
@@ -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 {
@@ -15,9 +15,10 @@
15
15
  import { contractOf, type Gated } from './argsContract.ts'
16
16
  import { CELL } from './CELL.ts'
17
17
  import { KEYED_BRAND } from './KEYED_BRAND.ts'
18
- import { brandedPrototype, MEMBER_KEYS } from './Reactive.ts'
18
+ import { brandedPrototype, MEMBER_KEYS, signalMoved } from './Reactive.ts'
19
19
  import { SELECTION_MEMBERS } from './SELECTION_MEMBERS.ts'
20
20
  import { currentScope, runInScope, SCOPING } from './scoping.ts'
21
+ import { newTracked, TRACKING, type Tracked, trackProbe } from './tracking.ts'
21
22
 
22
23
  const UNBUILT: unique symbol = Symbol('abide.unbuilt')
23
24
 
@@ -36,11 +37,18 @@ class Filed {
36
37
  home: unknown
37
38
  make: () => unknown
38
39
  global: boolean
40
+ /**
41
+ * 3.3, 13.19 — what a probe asked before the value was built here subscribes to, moved by the
42
+ * build: the home's. A scope files its own in `instances` under this one as its key, and drops
43
+ * it with the scope.
44
+ */
45
+ unbuilt: Tracked
39
46
 
40
47
  constructor(make: () => unknown, lazy: boolean, global: boolean) {
41
48
  this.make = make
42
49
  this.home = lazy ? UNBUILT : make()
43
50
  this.global = global
51
+ this.unbuilt = newTracked()
44
52
  }
45
53
  }
46
54
 
@@ -56,14 +64,21 @@ type Forwarder = { [FILED]: Filed }
56
64
  function instanceOf(filed: Filed): Record<PropertyKey, unknown> {
57
65
  const scope = filed.global ? undefined : currentScope()
58
66
  if (scope === undefined) {
59
- if (filed.home === UNBUILT)
67
+ if (filed.home === UNBUILT) {
60
68
  filed.home = runInScope(undefined, () => filed.make(), undefined)
69
+ signalMoved(filed.unbuilt)
70
+ }
61
71
  return filed.home as Record<PropertyKey, unknown>
62
72
  }
63
73
  let instance = scope.instances.get(filed)
64
74
  if (instance === undefined) {
65
75
  instance = filed.make()
66
76
  scope.instances.set(filed, instance)
77
+ const asked = scope.instances.get(filed.unbuilt) as Tracked | undefined
78
+ if (asked !== undefined) {
79
+ scope.instances.delete(filed.unbuilt)
80
+ signalMoved(asked)
81
+ }
67
82
  }
68
83
  return instance as Record<PropertyKey, unknown>
69
84
  }
@@ -149,6 +164,38 @@ export function scoped(
149
164
  return read
150
165
  }
151
166
 
167
+ /**
168
+ * Whether the value `value` forwards to is built for the scope that is current — always, for a
169
+ * value that is no forwarder. Asking builds nothing, where `instanceOf` builds what it finds unbuilt.
170
+ *
171
+ * 3.3 — AND WHERE IT IS NOT, ASKING SUBSCRIBES THE READER TO ITS BUILD: a guard that probed before
172
+ * the first call has no cell to hear the call start from, so `disabled={save.busy()}` stayed
173
+ * enabled through the whole write. One signal per value per scope, never one per entry (13.3).
174
+ */
175
+ export function builtHere(value: unknown): boolean {
176
+ const filed = (value as Partial<Forwarder> | null)?.[FILED]
177
+ if (filed === undefined) return true
178
+ const scope = filed.global ? undefined : currentScope()
179
+ if (
180
+ scope === undefined
181
+ ? filed.home !== UNBUILT
182
+ : scope.instances.has(filed)
183
+ )
184
+ return true
185
+ const reader = TRACKING.reader
186
+ if (reader === null) return false
187
+ let asked = filed.unbuilt
188
+ if (scope !== undefined) {
189
+ asked = scope.instances.get(filed.unbuilt) as Tracked
190
+ if (asked === undefined) {
191
+ asked = newTracked()
192
+ scope.instances.set(filed.unbuilt, asked)
193
+ }
194
+ }
195
+ trackProbe(asked, reader)
196
+ return false
197
+ }
198
+
152
199
  /**
153
200
  * Whether `value` is request-local on this server — built without `global`, outside every request.
154
201
  */
package/src/shared/rpc.ts CHANGED
@@ -13,9 +13,11 @@
13
13
  // the call hands back a value whose body runs the onion once per args, per request, over the
14
14
  // handler's own.
15
15
 
16
+ import { DERIVED_ARGS } from './DERIVED_ARGS.ts'
16
17
  import { type Failed, isFailedNamed } from './Failed.ts'
17
18
  import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
18
19
  import { HandlerRoute } from './HandlerRoute.ts'
20
+ import type { Issues } from './Issues.ts'
19
21
  import { IN_PROCESS_ORIGIN } from './IN_PROCESS_ORIGIN.ts'
20
22
  import { compose, type Middleware } from './Middleware.ts'
21
23
  import { outbox, type QueuedCall, sendingFor } from './outbox.ts'
@@ -50,8 +52,8 @@ import {
50
52
  import { MUTATION } from './reactive/MUTATION.ts'
51
53
  import { dropAnswered } from './reactive/Reactive.ts'
52
54
  import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
53
- import { isForwarder } from './reactive/scoped.ts'
54
- import { checkArgs } from './refuse.ts'
55
+ import { builtHere, isForwarder } from './reactive/scoped.ts'
56
+ import { checkArgs, validationError } from './refuse.ts'
55
57
  import { rpcRequest } from './rpcRequest.ts'
56
58
  import type { Schema } from './Schema.ts'
57
59
 
@@ -74,7 +76,9 @@ export type CallOptions<Args = unknown, Value = unknown> = {
74
76
  *
75
77
  * 16.8 — AND ITS FAILURES CARRY THE TRANSPORT'S `HttpError`, which no handler declares and every
76
78
  * 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`.
79
+ * `timeout` (504), and any failure status whose body was not a `Failed`, rebuilt by `answered`. And
80
+ * the `ValidationError` the wire holds args to the annotation with (19.9), where `Annotated` says it
81
+ * derives a schema at all.
78
82
  */
79
83
  export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
80
84
  ...args: undefined extends Args
@@ -83,7 +87,7 @@ export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
83
87
  ) => Reactive<
84
88
  Value,
85
89
  Value,
86
- Failures | Failed<'HttpError', undefined>,
90
+ Failures | Failed<'HttpError', undefined> | Annotated<Args>,
87
91
  Produced
88
92
  >) & {
89
93
  method: Method
@@ -104,7 +108,7 @@ export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
104
108
  error: unknown,
105
109
  name: Name,
106
110
  ): error is Extract<
107
- Failures | Failed<'HttpError', undefined>,
111
+ Failures | Failed<'HttpError', undefined> | Annotated<Args>,
108
112
  { name: Name }
109
113
  >
110
114
  /**
@@ -130,6 +134,45 @@ export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
130
134
  guarded: boolean
131
135
  }
132
136
 
137
+ /**
138
+ * The `ValidationError` the wire answers args that do not hold to the schema 19.9 derives from
139
+ * `Args`, and `never` where the derived schema is no object's and the wire holds nothing to it.
140
+ *
141
+ * MIRRORS `argsSchemaOf`, which reads the same `Args` off the `Rpc`'s call: an object's schema is
142
+ * derived for ONE record, `undefined` dropped — not a scalar, a list, a callable or a union of
143
+ * records (`anyOf`), and not `any`, `unknown` or `object`, which constrain nothing. `object` is told
144
+ * from `{}`, the one other record with no keys, by `string`, which only `{}` takes.
145
+ */
146
+ type Annotated<Args> = 0 extends 1 & Args
147
+ ? never
148
+ : unknown extends Args
149
+ ? never
150
+ : Derived<Exclude<Args, undefined>>
151
+ // `Member` is distributed over and `Whole` is not: a member that is not the whole is one arm of a union.
152
+ type Derived<Member, Whole = Member> = Member extends
153
+ | readonly unknown[]
154
+ | ((...given: never[]) => unknown)
155
+ ? never
156
+ : Member extends object
157
+ ? [Whole] extends [Member]
158
+ ? [keyof Member] extends [never]
159
+ ? [string] extends [Member]
160
+ ? Failed<'ValidationError', Issues<Whole>>
161
+ : never
162
+ : Failed<'ValidationError', Issues<Whole>>
163
+ : never
164
+ : never
165
+
166
+ /**
167
+ * 19.9 — the issues an `Rpc`'s args leave against the schema its annotation derives, `null` where
168
+ * they hold: `undefined` until the module its handler is loaded through stamps it (`derivedArgs`),
169
+ * and so in a browser always. A function rather than the schema, so this seam never carries the
170
+ * validator (19.13).
171
+ */
172
+ export type HeldToAnnotation = {
173
+ [DERIVED_ARGS]: ((args: unknown) => Issues<unknown> | null) | undefined
174
+ }
175
+
133
176
  /** Which generated surfaces carry a handler (20.1). */
134
177
  export type Clients = {
135
178
  ui?: boolean
@@ -307,6 +350,18 @@ type Declare = {
307
350
  ): Rpc<Kept<Shaped>, undefined, Refusals<Shaped, Gate, Accepted>>
308
351
  }
309
352
 
353
+ /** What a handler's selection members answer before a call has built its value: no entries' answers. */
354
+ const UNREACHED: Record<
355
+ (typeof SELECTION_MEMBERS)[number],
356
+ (pattern?: unknown) => unknown
357
+ > = {
358
+ busy: () => false,
359
+ ready: () => true,
360
+ synced: () => true,
361
+ refresh: () => undefined,
362
+ invalidate: () => undefined,
363
+ }
364
+
310
365
  /** Builds the handler declarer for one method — what `GET`, `POST` and the rest are. */
311
366
  export function declare(method: Method): Declare {
312
367
  return ((handler: unknown, options?: RpcOptions): unknown => {
@@ -396,6 +451,11 @@ export function declare(method: Method): Declare {
396
451
  }
397
452
  const call = (given?: unknown, options?: CallOptions): unknown => {
398
453
  const args = normalized(contract, given)
454
+ // 15.8, 19.9 — AND HELD TO ITS ANNOTATION, where the build derived a schema off it: a
455
+ // type an in-process caller wrote holds nothing at run time (`as never`, a value off
456
+ // JSON), so the call is refused as the wire refuses it rather than handed to the body.
457
+ const issues = rpc[DERIVED_ARGS]?.(args) ?? null
458
+ if (issues !== null) return reactive(() => validationError(issues))
399
459
  const route =
400
460
  rpc.address === undefined ? undefined : routeAt(rpc.address)
401
461
  if (route?.segments !== undefined) {
@@ -421,10 +481,17 @@ export function declare(method: Method): Declare {
421
481
  }
422
482
  // THE HANDLER'S OWN MEMBERS, handed the pattern. A `Keyed` narrows by it and a `Reactive`'s
423
483
  // take no argument, so one spelling serves both arms and neither is a second mechanism.
424
- const face = value as Record<
484
+ //
485
+ // 13.19 — AND NOT YET BUILT, NONE OF THEM BUILDS IT: building an unkeyed value starts its body
486
+ // (1.7), so `save.busy()` over a server's never-called `POST(async () => …)` ran the write and
487
+ // reported it in flight. Until a call reaches it the handler has no entry, and answers as one
488
+ // with no entries does — the asking reader subscribed to the call that builds it (`builtHere`).
489
+ const built = value as Record<
425
490
  (typeof SELECTION_MEMBERS)[number],
426
491
  (pattern?: unknown) => unknown
427
492
  >
493
+ const faceHere = (): typeof built =>
494
+ builtHere(value) ? built : UNREACHED
428
495
  const rpc = Object.assign(call, contract, {
429
496
  method,
430
497
  description: options?.description,
@@ -436,6 +503,7 @@ export function declare(method: Method): Declare {
436
503
  maxBodySize: options?.maxBodySize,
437
504
  clients: options?.clients ?? {},
438
505
  guarded: rungs.length > 0,
506
+ [DERIVED_ARGS]: undefined as HeldToAnnotation[typeof DERIVED_ARGS],
439
507
  isError: isFailedNamed,
440
508
  url(args?: Record<string, unknown>): string {
441
509
  return rpcRequest(method, routeAt(addressOf(rpc)), args).url
@@ -473,16 +541,16 @@ export function declare(method: Method): Declare {
473
541
  members[name] =
474
542
  name === 'invalidate' || name === 'refresh'
475
543
  ? (pattern?: unknown): unknown => {
476
- const done = face[name](pattern)
544
+ const done = faceHere()[name](pattern)
477
545
  RPC_SERVER.broadcast?.(rpc, name, pattern)
478
546
  return done
479
547
  }
480
- : (pattern?: unknown): unknown => face[name](pattern)
548
+ : (pattern?: unknown): unknown => faceHere()[name](pattern)
481
549
  }
482
550
  // 46.11 — BUSY TOO WHILE A CALL THE OUTBOX HOLDS FOR IT IS IN FLIGHT: a queued call sent is work
483
551
  // in flight for this handler, as a call made now is. 46.1 — and the calls it holds for it.
484
552
  members.busy = (pattern?: unknown): boolean =>
485
- (face.busy(pattern) as boolean) ||
553
+ (faceHere().busy(pattern) as boolean) ||
486
554
  sendingFor(rpc, pattern as Record<string, unknown> | undefined)
487
555
  members.outbox = (pattern?: unknown): QueuedCall[] =>
488
556
  outbox(rpc as never, pattern as Record<string, unknown> | undefined)