@abide/abide 0.116.3 → 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 +7 -0
- package/docs/RULEBOOK.md +2 -1
- package/package.json +1 -1
- package/src/compiler/compileApp.ts +19 -0
- package/src/compiler/emitComponent.ts +19 -1
- package/src/compiler/emitSource.ts +4 -0
- package/src/server/abideModules.ts +21 -6
- package/src/server/answerRpc.ts +3 -12
- package/src/server/argsFor.ts +5 -8
- package/src/server/derivedArgs.ts +25 -0
- package/src/server/faceOf.ts +10 -2
- package/src/server/index.ts +3 -0
- package/src/shared/DERIVED_ARGS.ts +6 -0
- package/src/shared/reactive/scoped.ts +39 -5
- package/src/shared/rpc.ts +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.116.3
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
package/docs/RULEBOOK.md
CHANGED
|
@@ -754,7 +754,8 @@ mutation's `declare` built while its write is in flight.
|
|
|
754
754
|
where every value it sweeps is.
|
|
755
755
|
|
|
756
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
|
|
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.
|
|
758
759
|
|
|
759
760
|
# 14. Tracking
|
|
760
761
|
|
package/package.json
CHANGED
|
@@ -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
|
*
|
|
@@ -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(
|
|
@@ -120,7 +120,8 @@ export function serveCompilation(root: string, compilation: Compilation): void {
|
|
|
120
120
|
const real = realOrAsGiven(root)
|
|
121
121
|
const handlerFiles = addCompilation(real, compilation, RUNTIME)
|
|
122
122
|
installSourceMaps(serverMapsOf(compilation, real), real)
|
|
123
|
-
if (handlerFiles.length !== 0)
|
|
123
|
+
if (handlerFiles.length !== 0)
|
|
124
|
+
Bun.plugin(handlerFilesPlugin(handlerFiles, RUNTIME.handlers))
|
|
124
125
|
if (registered) return
|
|
125
126
|
registered = true
|
|
126
127
|
Bun.plugin(modulesPlugin(RUNTIME))
|
|
@@ -132,7 +133,13 @@ export function serveCompilation(root: string, compilation: Compilation): void {
|
|
|
132
133
|
// plugin per compilation because the filter is fixed at registration, and the runtime's `onLoad`
|
|
133
134
|
// cannot fall through, so the filter names the handler files and nothing else. The face it names
|
|
134
135
|
// is resolved by `modulesPlugin`, registered once.
|
|
135
|
-
|
|
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 {
|
|
136
143
|
return {
|
|
137
144
|
name: 'abide-handler-files',
|
|
138
145
|
setup(build) {
|
|
@@ -142,10 +149,18 @@ function handlerFilesPlugin(paths: string[]): Bun.BunPlugin {
|
|
|
142
149
|
`^(?:${paths.map(RegExp.escape).join('|')})$`,
|
|
143
150
|
),
|
|
144
151
|
},
|
|
145
|
-
(args) =>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
},
|
|
149
164
|
)
|
|
150
165
|
},
|
|
151
166
|
}
|
package/src/server/answerRpc.ts
CHANGED
|
@@ -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
|
|
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 }
|
package/src/server/argsFor.ts
CHANGED
|
@@ -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
|
|
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'
|
|
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
|
-
|
|
37
|
-
|
|
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
|
+
}
|
package/src/server/faceOf.ts
CHANGED
|
@@ -3,6 +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 type { JsonValue } from '#shared/JsonValue.ts'
|
|
6
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
|
|
@@ -45,8 +46,15 @@ export type Asked =
|
|
|
45
46
|
| 'abide'
|
|
46
47
|
| 'handler'
|
|
47
48
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
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
|
+
}
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
60
|
* The face an import of `file` is answered with, asked `asked` from `importer` in `lane` — or
|
package/src/server/index.ts
CHANGED
|
@@ -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')
|
|
@@ -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
|
}
|
|
@@ -152,14 +167,33 @@ export function scoped(
|
|
|
152
167
|
/**
|
|
153
168
|
* Whether the value `value` forwards to is built for the scope that is current — always, for a
|
|
154
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).
|
|
155
174
|
*/
|
|
156
175
|
export function builtHere(value: unknown): boolean {
|
|
157
176
|
const filed = (value as Partial<Forwarder> | null)?.[FILED]
|
|
158
177
|
if (filed === undefined) return true
|
|
159
178
|
const scope = filed.global ? undefined : currentScope()
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
163
197
|
}
|
|
164
198
|
|
|
165
199
|
/**
|
package/src/shared/rpc.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
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'
|
|
@@ -52,7 +53,7 @@ import { MUTATION } from './reactive/MUTATION.ts'
|
|
|
52
53
|
import { dropAnswered } from './reactive/Reactive.ts'
|
|
53
54
|
import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
|
|
54
55
|
import { builtHere, isForwarder } from './reactive/scoped.ts'
|
|
55
|
-
import { checkArgs } from './refuse.ts'
|
|
56
|
+
import { checkArgs, validationError } from './refuse.ts'
|
|
56
57
|
import { rpcRequest } from './rpcRequest.ts'
|
|
57
58
|
import type { Schema } from './Schema.ts'
|
|
58
59
|
|
|
@@ -162,6 +163,16 @@ type Derived<Member, Whole = Member> = Member extends
|
|
|
162
163
|
: never
|
|
163
164
|
: never
|
|
164
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
|
+
|
|
165
176
|
/** Which generated surfaces carry a handler (20.1). */
|
|
166
177
|
export type Clients = {
|
|
167
178
|
ui?: boolean
|
|
@@ -440,6 +451,11 @@ export function declare(method: Method): Declare {
|
|
|
440
451
|
}
|
|
441
452
|
const call = (given?: unknown, options?: CallOptions): unknown => {
|
|
442
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))
|
|
443
459
|
const route =
|
|
444
460
|
rpc.address === undefined ? undefined : routeAt(rpc.address)
|
|
445
461
|
if (route?.segments !== undefined) {
|
|
@@ -469,7 +485,7 @@ export function declare(method: Method): Declare {
|
|
|
469
485
|
// 13.19 — AND NOT YET BUILT, NONE OF THEM BUILDS IT: building an unkeyed value starts its body
|
|
470
486
|
// (1.7), so `save.busy()` over a server's never-called `POST(async () => …)` ran the write and
|
|
471
487
|
// reported it in flight. Until a call reaches it the handler has no entry, and answers as one
|
|
472
|
-
// with no entries does.
|
|
488
|
+
// with no entries does — the asking reader subscribed to the call that builds it (`builtHere`).
|
|
473
489
|
const built = value as Record<
|
|
474
490
|
(typeof SELECTION_MEMBERS)[number],
|
|
475
491
|
(pattern?: unknown) => unknown
|
|
@@ -487,6 +503,7 @@ export function declare(method: Method): Declare {
|
|
|
487
503
|
maxBodySize: options?.maxBodySize,
|
|
488
504
|
clients: options?.clients ?? {},
|
|
489
505
|
guarded: rungs.length > 0,
|
|
506
|
+
[DERIVED_ARGS]: undefined as HeldToAnnotation[typeof DERIVED_ARGS],
|
|
490
507
|
isError: isFailedNamed,
|
|
491
508
|
url(args?: Record<string, unknown>): string {
|
|
492
509
|
return rpcRequest(method, routeAt(addressOf(rpc)), args).url
|