@abide/abide 0.123.3 → 0.124.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ABIDE.md +8 -5
- package/CHANGELOG.md +38 -0
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +29 -16
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +46 -4
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowHost.ts +670 -0
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +71 -1715
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +44 -17
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/formatAbide.ts +5 -6
- package/src/compiler/handlersUnder.ts +16 -35
- package/src/compiler/projectShadow.ts +1 -0
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readsIn.ts +180 -81
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +26 -20
- package/src/compiler/splitImports.ts +2 -2
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/DesktopSession.ts +213 -0
- package/src/server/Followers.ts +399 -0
- package/src/server/MODULE_QUERIES.ts +6 -2
- package/src/server/PageRender.ts +201 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +6 -70
- package/src/server/answerRpc.ts +48 -260
- package/src/server/answerSocket.ts +41 -463
- package/src/server/appKit.ts +1 -1
- package/src/server/argsContract.ts +1 -1
- package/src/server/createApp.ts +8 -2
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +1 -1
- package/src/server/index.ts +1 -1
- package/src/server/installServer.ts +5 -9
- package/src/server/invokeHandler.ts +230 -0
- package/src/server/mountHandlers.ts +184 -0
- package/src/server/openDesktopWindow.ts +308 -0
- package/src/server/preload.ts +18 -0
- package/src/server/principal.ts +1 -1
- package/src/server/remote.ts +1 -1
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runDesktop.ts +19 -447
- package/src/server/seedBuffer.ts +11 -11
- package/src/server/servedApp.ts +1 -1
- package/src/server/shell.ts +1 -1
- package/src/server/surfaceOf.ts +1 -1
- package/src/server/versionOf.ts +1 -1
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/index.ts +1 -1
- package/src/shared/{note.ts → issueReport.ts} +22 -0
- package/src/shared/loadCall.ts +66 -0
- package/src/shared/reactive/Keyed.ts +23 -15
- package/src/shared/reactive/Reactive.ts +46 -1002
- package/src/shared/reactive/byOf.ts +2 -5
- package/src/shared/reactive/callLoad.ts +8 -14
- package/src/shared/reactive/cellShape.ts +734 -0
- package/src/shared/reactive/index.ts +12 -24
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +8 -5
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/Router.ts +53 -69
- package/src/ui/outboxQueue.ts +2 -8
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/socketWire.ts +3 -3
- package/src/ui/wire.ts +9 -10
- package/src/shared/LOAD_OFFLINE.ts +0 -15
- package/src/shared/LOAD_SIGNAL.ts +0 -10
- package/src/shared/issuesOf.ts +0 -18
- package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
package/src/server/faceOf.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type Lane = { bundling: boolean; browser: boolean }
|
|
|
34
34
|
export type Face = { path: string; namespace: string }
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* What an import asks of a file, by its spelling: its text (
|
|
37
|
+
* What an import asks of a file, by its spelling: its text (`.abide-text`), a component's stylesheet
|
|
38
38
|
* (`?style`), the module it compiles to (`?component`), its browser module (`?client`), its server
|
|
39
39
|
* module (`?server`), a `.abide` named bare, or a module that may be a handler file.
|
|
40
40
|
*/
|
|
@@ -22,12 +22,13 @@ import { parseRoutePattern, type Segment } from '#shared/routePattern.ts'
|
|
|
22
22
|
import { argsInURL, rpcRequest } from '#shared/rpcRequest.ts'
|
|
23
23
|
|
|
24
24
|
import { answered } from './answerFailure.ts'
|
|
25
|
-
import { bodyWithin
|
|
25
|
+
import { bodyWithin } from './answerRpc.ts'
|
|
26
26
|
import type { ToolAnswer, ToolDefinition } from './assistant.ts'
|
|
27
27
|
import { appIdentity } from './config.ts'
|
|
28
28
|
import { crossingOrigin, crossingRefusal } from './crossingOrigin.ts'
|
|
29
29
|
import { IMAGE_TYPES } from './IMAGE_TYPES.ts'
|
|
30
30
|
import { MCP_PROTOCOL_VERSION } from './MCP_PROTOCOL_VERSION.ts'
|
|
31
|
+
import type { Mounted } from './mountHandlers.ts'
|
|
31
32
|
import { type Listed, surfaceOf } from './surfaceOf.ts'
|
|
32
33
|
|
|
33
34
|
const OPENAPI_LOG = namedLogger('abide:openapi')
|
package/src/server/index.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type { ConfigField } from './configFields.ts'
|
|
|
51
51
|
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
|
-
export { callsInRender } from './
|
|
54
|
+
export { callsInRender } from './PageRender.ts'
|
|
55
55
|
// 19.9's run-time half: what the module a handler is loaded through calls to hold it to the schema
|
|
56
56
|
// its annotation derives (15.8). Not an app's name.
|
|
57
57
|
export { derivedArgs } from './argsContract.ts'
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// whichever server module a process happened to import — a compiler or a test that reaches one of
|
|
3
3
|
// them for a function is not made a server by it. Called where a server is entered: `bootApp`,
|
|
4
4
|
// `abide run`, and `@abide/abide/server`, which an app imports to be one. Filling the same functions again
|
|
5
|
-
// changes nothing, so each entry calls it.
|
|
6
|
-
// `requestScope.ts`'s import (its comment says why), `RPC_SERVER.raw` per boot,
|
|
7
|
-
// `LISTENING.timeout` by the dev worker alone.
|
|
5
|
+
// changes nothing, so each entry calls it. Four are filled elsewhere: `SCOPING.provider` at
|
|
6
|
+
// `requestScope.ts`'s import (its comment says why), `RPC_SERVER.raw` per boot,
|
|
7
|
+
// `RPC_SERVER.following` by each app's followers, and `LISTENING.timeout` by the dev worker alone.
|
|
8
8
|
|
|
9
9
|
import { COOKIES } from '#shared/cookies.ts'
|
|
10
10
|
import { CSP } from '#shared/csp.ts'
|
|
@@ -15,12 +15,11 @@ import { PRINCIPAL } from '#shared/principal.ts'
|
|
|
15
15
|
import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
|
|
16
16
|
import { UNEXPECTED } from '#shared/UNEXPECTED.ts'
|
|
17
17
|
|
|
18
|
-
import { reachedInRender } from './answerRequest.ts'
|
|
19
18
|
import { environmentTimeout } from './answerRpc.ts'
|
|
20
|
-
import { broadcast, feedFromServed, swept } from './answerSocket.ts'
|
|
21
19
|
import { configuredMount, derivedAppName } from './config.ts'
|
|
22
20
|
import { cspRung, nonce } from './csp.ts'
|
|
23
21
|
import { handOnError } from './hooks.ts'
|
|
22
|
+
import { recordInRender } from './PageRender.ts'
|
|
24
23
|
import { SERVER_PRINCIPAL } from './principal.ts'
|
|
25
24
|
import { reportHealth } from './reportHealth.ts'
|
|
26
25
|
import { handlerTook, request, requestCookies } from './requestScope.ts'
|
|
@@ -40,9 +39,6 @@ export function installServer(): void {
|
|
|
40
39
|
// 16.11 — the request a handler's rung is handed.
|
|
41
40
|
RPC_SERVER.request = request
|
|
42
41
|
RPC_SERVER.timeout = environmentTimeout
|
|
43
|
-
RPC_SERVER.reached =
|
|
44
|
-
RPC_SERVER.broadcast = broadcast
|
|
45
|
-
RPC_SERVER.swept = swept
|
|
46
|
-
RPC_SERVER.served = feedFromServed
|
|
42
|
+
RPC_SERVER.reached = recordInRender
|
|
47
43
|
RPC_SERVER.handled = handlerTook
|
|
48
44
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// 16.2 — ONE HANDLER, CALLED FOR ONE CALLER, whichever lane the call came over. What a call over
|
|
2
|
+
// http and a subscription over the mux both owe the handler is decided here — the origin gate
|
|
3
|
+
// (16.13, 18.2), the args contract (19.7, 19.9), the rungs a follow is held to (18.8, 18.26), the
|
|
4
|
+
// seed a render left (41.13) and the `global` entry the answer came from (21.15) — and what a lane
|
|
5
|
+
// adds is the decoding of what it was sent and the encoding of what it answers.
|
|
6
|
+
//
|
|
7
|
+
// THE IN-PROCESS CALL is `#shared/rpc.ts`'s, which a browser bundle reaches too, so it cannot be
|
|
8
|
+
// this module's; a call over a lane is that call, made in the request's scope.
|
|
9
|
+
|
|
10
|
+
import { type Failed, isFailed } from '#shared/Failed.ts'
|
|
11
|
+
import { GLOBAL_GETS } from '#shared/GLOBAL_GETS.ts'
|
|
12
|
+
import { compose } from '#shared/Middleware.ts'
|
|
13
|
+
import { heldGenerationOf } from '#shared/reactive/producer.ts'
|
|
14
|
+
import { argsInURL, rpcRequest } from '#shared/rpcRequest.ts'
|
|
15
|
+
|
|
16
|
+
import { argsFor } from './argsContract.ts'
|
|
17
|
+
import { closedTo, crossingRefusal } from './crossingOrigin.ts'
|
|
18
|
+
import type { FollowedHandler, Mounted } from './mountHandlers.ts'
|
|
19
|
+
import { takeSeeded } from './PageRender.ts'
|
|
20
|
+
import { currentRequestScope, rungsTook, timed } from './requestScope.ts'
|
|
21
|
+
import { type Outcome, outcomeOf } from './seedBuffer.ts'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What the origin gate makes of a call from `crossing`: the refusal it is answered with, or the
|
|
25
|
+
* origin its answer admits — `null` for the app's own, and for a read another origin may make but
|
|
26
|
+
* not read (21.19).
|
|
27
|
+
*
|
|
28
|
+
* 16.13 — A MUTATION FROM ANOTHER ORIGIN IS REFUSED, and 18.2 — a subscription, unless the handler
|
|
29
|
+
* declared it may come. 16.24, 36.2 — the same gate and the same own origin for every lane.
|
|
30
|
+
*/
|
|
31
|
+
export function originGate(
|
|
32
|
+
handler: Mounted['handler'],
|
|
33
|
+
crossing: string | null,
|
|
34
|
+
following: boolean,
|
|
35
|
+
): Failed<'HttpError', undefined> | string | null {
|
|
36
|
+
if (!closedTo(handler.crossOrigin, crossing)) return crossing
|
|
37
|
+
if (following) return crossingRefusal(crossing, 'subscription')
|
|
38
|
+
return argsInURL(handler.method)
|
|
39
|
+
? null
|
|
40
|
+
: crossingRefusal(crossing, handler.method)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Which run answered a call, as 21.15's `server-timing` names it. */
|
|
44
|
+
type Run = 'Seeded' | 'Cached' | 'Started' | 'Direct'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A handler's call, made: how it ended, the args it was held to, and the `global` entry it was
|
|
48
|
+
* answered from.
|
|
49
|
+
*/
|
|
50
|
+
export class Invocation {
|
|
51
|
+
readonly outcome: Outcome
|
|
52
|
+
readonly args: unknown
|
|
53
|
+
/** The entry key of a `global` value, or `undefined` for one built per request. */
|
|
54
|
+
readonly key: string | undefined
|
|
55
|
+
/** The `global` entry as the run left it, which the answer reads too. */
|
|
56
|
+
readonly entry: unknown
|
|
57
|
+
readonly run: Run
|
|
58
|
+
readonly started: number
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
outcome: Outcome,
|
|
62
|
+
args: unknown,
|
|
63
|
+
key: string | undefined,
|
|
64
|
+
entry: unknown,
|
|
65
|
+
run: Run,
|
|
66
|
+
started: number,
|
|
67
|
+
) {
|
|
68
|
+
this.outcome = outcome
|
|
69
|
+
this.args = args
|
|
70
|
+
this.key = key
|
|
71
|
+
this.entry = entry
|
|
72
|
+
this.run = run
|
|
73
|
+
this.started = started
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 21.15 — HOW LONG THE HANDLER TOOK, AND WHICH RUN THIS IS, for the network panel: the row
|
|
78
|
+
* already names the handler, so the metric names the run. Read once the answer's head is built,
|
|
79
|
+
* so a stream's time is to its first chunk.
|
|
80
|
+
*/
|
|
81
|
+
timed(): void {
|
|
82
|
+
const timing = currentRequestScope()?.timing
|
|
83
|
+
const took = this.outcome.duration ?? performance.now() - this.started
|
|
84
|
+
// 21.20 — what the rungs took is theirs, and the handler's own body is what `Direct` reports.
|
|
85
|
+
const handled = this.run === 'Direct' ? timing?.handled : undefined
|
|
86
|
+
if (handled !== undefined) rungsTook(took - handled)
|
|
87
|
+
timed(timing, this.run, handled ?? took)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Calls `mounted`'s handler with what a lane decoded: `given` its args, `text` the query a URL or
|
|
93
|
+
* a form carried them as (19.7), `params` what its route matched.
|
|
94
|
+
*
|
|
95
|
+
* Answers the refusal its args were refused with, or the call made. `seeds` is whether a seed a
|
|
96
|
+
* page's render left may answer it (41.7) — 16.51, a `HEAD` leaves it for the `GET` it stands in
|
|
97
|
+
* front of.
|
|
98
|
+
*/
|
|
99
|
+
export async function invokeHandler(
|
|
100
|
+
mounted: Mounted,
|
|
101
|
+
given: unknown,
|
|
102
|
+
text: string | undefined,
|
|
103
|
+
params: Record<string, string | string[]>,
|
|
104
|
+
seeds: boolean,
|
|
105
|
+
): Promise<Invocation | Failed<string, unknown>> {
|
|
106
|
+
const handler = mounted.handler
|
|
107
|
+
let args: unknown
|
|
108
|
+
try {
|
|
109
|
+
args = argsFor(handler, given, text, params)
|
|
110
|
+
} catch (refused) {
|
|
111
|
+
if (isFailed(refused)) return refused
|
|
112
|
+
throw refused
|
|
113
|
+
}
|
|
114
|
+
// 41.7 — the call a hydrating page makes for what its render already reached is answered from
|
|
115
|
+
// that, and the handler is not run again (41.13).
|
|
116
|
+
const seeded = seeds ? takeSeeded(handler, args) : undefined
|
|
117
|
+
// The entry key of a `global` value, taken once for every read of it below.
|
|
118
|
+
const global = GLOBAL_GETS.get(handler)
|
|
119
|
+
const key = global?.key(args)
|
|
120
|
+
// 21.15 — A `global` PRODUCTION ALREADY HELD, and not loading, answers without a run: a run
|
|
121
|
+
// this call started moves the generation, and one it joined was loading when it came.
|
|
122
|
+
const held =
|
|
123
|
+
seeded === undefined && key !== undefined ? global?.at(key) : undefined
|
|
124
|
+
const heldAt = held === undefined ? undefined : heldGenerationOf(held)
|
|
125
|
+
const timing = currentRequestScope()?.timing
|
|
126
|
+
if (timing !== undefined) timing.answering = handler
|
|
127
|
+
const started = performance.now()
|
|
128
|
+
const outcome =
|
|
129
|
+
seeded === undefined ? await outcomeOf(handler(args)) : await seeded
|
|
130
|
+
const entry = key === undefined ? undefined : global?.at(key)
|
|
131
|
+
const run: Run =
|
|
132
|
+
seeded !== undefined
|
|
133
|
+
? 'Seeded'
|
|
134
|
+
: heldAt !== undefined &&
|
|
135
|
+
entry === held &&
|
|
136
|
+
heldGenerationOf(held) === heldAt
|
|
137
|
+
? 'Cached'
|
|
138
|
+
: outcome.duration === undefined
|
|
139
|
+
? 'Started'
|
|
140
|
+
: 'Direct'
|
|
141
|
+
return new Invocation(outcome, args, key, entry, run, started)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The args a subscription to `mounted` follows, held to its contract as a `GET` over http is
|
|
146
|
+
* (18.3) — or a throw of the refusal, the origin gate's included (18.2).
|
|
147
|
+
*/
|
|
148
|
+
export function followedArgs(
|
|
149
|
+
mounted: FollowedHandler,
|
|
150
|
+
given: unknown,
|
|
151
|
+
crossing: string | null,
|
|
152
|
+
): unknown {
|
|
153
|
+
const gate = originGate(mounted.handler, crossing, true)
|
|
154
|
+
if (isFailed(gate)) throw gate
|
|
155
|
+
return argsFor(mounted.handler, given, undefined)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Whether a subscription to `mounted` with `args` is fed its productions, asked over `upgrade`, the
|
|
160
|
+
* request the connection opened with — settled where nothing guards it.
|
|
161
|
+
*
|
|
162
|
+
* 18.8 — A GUARDED `GET`'s RUNGS RUN ONCE, as the subscription opens: one they admit is fed the
|
|
163
|
+
* value, and one they refuse is sent its triggers alone. 18.26 — AND THE MIDDLEWARE ITS FILE'S
|
|
164
|
+
* ROUTE FILED, over the request a `GET` of it would be: one a rung answers in the handler's place is
|
|
165
|
+
* not fed, as an http call it answered would never reach the handler.
|
|
166
|
+
*/
|
|
167
|
+
export function admitsFollower(
|
|
168
|
+
mounted: FollowedHandler,
|
|
169
|
+
args: unknown,
|
|
170
|
+
upgrade: Request,
|
|
171
|
+
): boolean | Promise<boolean> {
|
|
172
|
+
// No route rungs, no turn: a guarded `GET`'s rungs start in this frame's own run.
|
|
173
|
+
if (mounted.rungs.length === 0)
|
|
174
|
+
return mounted.handler.guarded ? handlerAdmits(mounted, args) : true
|
|
175
|
+
return throughRoute(mounted, args, upgrade)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function throughRoute(
|
|
179
|
+
mounted: FollowedHandler,
|
|
180
|
+
args: unknown,
|
|
181
|
+
upgrade: Request,
|
|
182
|
+
): Promise<boolean> {
|
|
183
|
+
if (!(await routeAdmits(mounted, args, upgrade))) return false
|
|
184
|
+
return !mounted.handler.guarded || handlerAdmits(mounted, args)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Whether the handler's own rungs let a call with `args` through. 18.8. */
|
|
188
|
+
function handlerAdmits(
|
|
189
|
+
mounted: FollowedHandler,
|
|
190
|
+
args: unknown,
|
|
191
|
+
): Promise<boolean> {
|
|
192
|
+
return Promise.resolve(
|
|
193
|
+
mounted.handler(args as never) as PromiseLike<unknown>,
|
|
194
|
+
).then(
|
|
195
|
+
() => true,
|
|
196
|
+
() => false,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Whether the rungs over `mounted`'s route let a `GET` of it with `args` through, asked over the
|
|
202
|
+
* upgrade's own request at the handler's address. 18.26.
|
|
203
|
+
*/
|
|
204
|
+
async function routeAdmits(
|
|
205
|
+
mounted: FollowedHandler,
|
|
206
|
+
args: unknown,
|
|
207
|
+
upgrade: Request,
|
|
208
|
+
): Promise<boolean> {
|
|
209
|
+
const asked = rpcRequest(
|
|
210
|
+
'GET',
|
|
211
|
+
mounted.address,
|
|
212
|
+
args as Record<string, unknown> | undefined,
|
|
213
|
+
)
|
|
214
|
+
const request = new Request(new URL(asked.url, upgrade.url), {
|
|
215
|
+
headers: upgrade.headers,
|
|
216
|
+
signal: upgrade.signal,
|
|
217
|
+
})
|
|
218
|
+
// REACHED BY A FLAG, not by the answer's identity: a rung that wraps or re-heads what `next`
|
|
219
|
+
// answered still let the call through, and a shared answer would carry every rung's headers.
|
|
220
|
+
let reached = false
|
|
221
|
+
try {
|
|
222
|
+
const answered = await compose(mounted.rungs, async () => {
|
|
223
|
+
reached = true
|
|
224
|
+
return new Response(null, { status: 204 })
|
|
225
|
+
})({ request })
|
|
226
|
+
return reached && answered.ok
|
|
227
|
+
} catch {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// 16.2 — THE MOUNTED HANDLERS: the table the build writes, each row's handler imported at boot and
|
|
2
|
+
// keyed by its method and address, with the rungs its route filed over it.
|
|
3
|
+
|
|
4
|
+
import type { CopyOptions } from '#shared/CopyOptions.ts'
|
|
5
|
+
import type { JsonValue } from '#shared/JsonValue.ts'
|
|
6
|
+
import { mounted as underMount } from '#shared/mount.ts'
|
|
7
|
+
import type { Gated, HeldToAnnotation } from '#shared/reactive/argsContract.ts'
|
|
8
|
+
import type { Defaults } from '#shared/reactive/parameterDefaults.ts'
|
|
9
|
+
import type { Method, Rpc } from '#shared/rpc.ts'
|
|
10
|
+
|
|
11
|
+
import { answered } from './answerFailure.ts'
|
|
12
|
+
import { derivedArgs } from './argsContract.ts'
|
|
13
|
+
import { type LaneRung, rungsOver } from './hooks.ts'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One row of the handler table the build writes into the server's table (`SERVER_TABLE`): where a
|
|
17
|
+
* handler answers, and its module.
|
|
18
|
+
*
|
|
19
|
+
* The module is relative to the app's root, which both commands that serve already know.
|
|
20
|
+
*/
|
|
21
|
+
export type Mount = {
|
|
22
|
+
address: string
|
|
23
|
+
method: Method
|
|
24
|
+
source: string
|
|
25
|
+
/** The handler file's name, or the private handler's binding. */
|
|
26
|
+
name: string
|
|
27
|
+
/** What the source's module exports it as: `default` for a handler file. */
|
|
28
|
+
export: string
|
|
29
|
+
/** The route its hooks are filed at, which its rungs are read over (37.1, 16.72). */
|
|
30
|
+
scope: string
|
|
31
|
+
/** 23.26 — each matcher its address names, to the anchored source its row is ranked with. */
|
|
32
|
+
matchers: Record<string, string>
|
|
33
|
+
/** Whether it is a private handler, listed nowhere (16.71). */
|
|
34
|
+
private: boolean
|
|
35
|
+
/** Whether a browser is handed a wrapper for it. 20.4. */
|
|
36
|
+
ui: boolean
|
|
37
|
+
/** Whether the command line lists it — the shell's listing and completion, never whether `call` reaches it. 20.3, 38.62. */
|
|
38
|
+
cli: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Whether MCP lists it where its runtime `clients` does not say.
|
|
41
|
+
*
|
|
42
|
+
* 16.37 — false for a handler typed as returning a `Response`, which only the build can tell.
|
|
43
|
+
*/
|
|
44
|
+
mcp: boolean
|
|
45
|
+
/** Its `description`, where the declaration writes one as a literal the build can read. */
|
|
46
|
+
description: string | undefined
|
|
47
|
+
/**
|
|
48
|
+
* What a `GET`'s value declares that the browser's copy behaves by, or `undefined`.
|
|
49
|
+
*
|
|
50
|
+
* 18.4 — set where its declaration names any of it.
|
|
51
|
+
*/
|
|
52
|
+
copy: CopyOptions | undefined
|
|
53
|
+
/**
|
|
54
|
+
* The defaults the handler's parameter writes, for the browser's copy to key by.
|
|
55
|
+
*
|
|
56
|
+
* 11.29 — the server reads its own off the handler.
|
|
57
|
+
*/
|
|
58
|
+
defaults: Defaults | undefined
|
|
59
|
+
/** The JSON Schema the handler's args annotation derives, where it takes a record. 19.9. */
|
|
60
|
+
args: JsonValue | undefined
|
|
61
|
+
/** The JSON Schema of what it answers, where its type says one. 19.21. */
|
|
62
|
+
result: JsonValue | undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A row of the table the build wrote, with the import of the module its handler is declared in —
|
|
67
|
+
* the handler file itself, or a private handler's `.abide` server module, the one the route table
|
|
68
|
+
* loads too, so a page and a call reach one handler.
|
|
69
|
+
*/
|
|
70
|
+
export type MountRow = Mount & { load: () => Promise<Record<string, unknown>> }
|
|
71
|
+
|
|
72
|
+
/** A handler's row, mounted: the handler imported and its args schema probed. */
|
|
73
|
+
export type Mounted = Mount & {
|
|
74
|
+
/**
|
|
75
|
+
* The rungs filed over `scope`, read once at mount (16.14) — what an http call and a socket
|
|
76
|
+
* subscription to it both pass through (37.2, 18.26).
|
|
77
|
+
*/
|
|
78
|
+
rungs: LaneRung[]
|
|
79
|
+
handler: Rpc<unknown, unknown> & Gated<unknown> & HeldToAnnotation
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* What following a handler reads of its row: where it answers, the rungs its route filed, the
|
|
84
|
+
* handler, and the `tags` its copy carries. 18.3, 18.8, 18.26.
|
|
85
|
+
*/
|
|
86
|
+
export type FollowedHandler = Pick<
|
|
87
|
+
Mounted,
|
|
88
|
+
'address' | 'rungs' | 'handler' | 'copy'
|
|
89
|
+
>
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The `GET` a browser follows at each address among `handlers` — one it is handed a wrapper for
|
|
93
|
+
* (20.4) — or `undefined`.
|
|
94
|
+
*/
|
|
95
|
+
export function followedIn(
|
|
96
|
+
handlers: Map<string, Mounted>,
|
|
97
|
+
): (address: string) => FollowedHandler | undefined {
|
|
98
|
+
return (address) => {
|
|
99
|
+
const found = handlers.get(handlerKey('GET', address))
|
|
100
|
+
return found?.ui === true ? found : undefined
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The row of a handler abide mounts itself, keyed as `answerRpc` looks one up.
|
|
106
|
+
*
|
|
107
|
+
* Built on the core's `declare` with no generated surface (20.1) — the app's tree never names it.
|
|
108
|
+
*/
|
|
109
|
+
export function mountedOwn(
|
|
110
|
+
address: string,
|
|
111
|
+
name: string,
|
|
112
|
+
handler: object,
|
|
113
|
+
): [string, Mounted] {
|
|
114
|
+
// 16.10 — READ WHEN IT IS REPORTED, where a mounted handler's is stamped at boot: the mount is
|
|
115
|
+
// the configuration's, and nothing boots this handler.
|
|
116
|
+
Object.defineProperty(handler, 'address', {
|
|
117
|
+
get: () => underMount(address),
|
|
118
|
+
})
|
|
119
|
+
return [
|
|
120
|
+
address,
|
|
121
|
+
{
|
|
122
|
+
address,
|
|
123
|
+
method: 'GET',
|
|
124
|
+
// The root's rungs run before routing (37.3), and nothing is filed below a route
|
|
125
|
+
// of abide's own.
|
|
126
|
+
rungs: [],
|
|
127
|
+
source: '',
|
|
128
|
+
name,
|
|
129
|
+
export: name,
|
|
130
|
+
scope: '/',
|
|
131
|
+
matchers: {},
|
|
132
|
+
private: false,
|
|
133
|
+
ui: false,
|
|
134
|
+
cli: false,
|
|
135
|
+
mcp: false,
|
|
136
|
+
description: undefined,
|
|
137
|
+
copy: undefined,
|
|
138
|
+
defaults: undefined,
|
|
139
|
+
args: undefined,
|
|
140
|
+
result: undefined,
|
|
141
|
+
handler: handler as Mounted['handler'],
|
|
142
|
+
},
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The table's handlers, imported and keyed by method and address.
|
|
148
|
+
*
|
|
149
|
+
* Imported through each row's own loader, which the route table shares, so a page rendering on the
|
|
150
|
+
* server and a call over the wire reach one module and so one handler. AT BOOT rather than on the
|
|
151
|
+
* first call, because a page's render reaches a handler through the page's own import and has to
|
|
152
|
+
* know its address before anything has called it over the wire.
|
|
153
|
+
*/
|
|
154
|
+
export async function mountHandlers(
|
|
155
|
+
table: MountRow[],
|
|
156
|
+
): Promise<{ handlers: Map<string, Mounted> }> {
|
|
157
|
+
const modules = await Promise.all(table.map((mount) => mount.load()))
|
|
158
|
+
const handlers = new Map<string, Mounted>()
|
|
159
|
+
for (let at = 0; at < table.length; at += 1) {
|
|
160
|
+
const mount = table[at] as Mount
|
|
161
|
+
const exported = (modules[at] as Record<string, unknown>)[mount.export]
|
|
162
|
+
const handler = exported as Mounted['handler']
|
|
163
|
+
// 16.9, 16.10 — the address `rpc.url` and `rpc.raw` build on, under the mount — and what a
|
|
164
|
+
// page's render turns a call it made into the browser's request by (41.20).
|
|
165
|
+
handler.address = underMount(mount.address)
|
|
166
|
+
// 19.9 — the schema the build derived off its annotation, for a handler whose module did not
|
|
167
|
+
// file it on load: a bundled executable's faces carry no `derivedArgs` call.
|
|
168
|
+
if (mount.args !== undefined) derivedArgs(handler, mount.args)
|
|
169
|
+
handlers.set(handlerKey(mount.method, mount.address), {
|
|
170
|
+
...mount,
|
|
171
|
+
// 37.1, 16.72 — the route its file's hooks are filed at, which the build read off
|
|
172
|
+
// where the file answers (23.19's mount for a package's), never its place on disk.
|
|
173
|
+
// 37.14 — a handler's lane answers a refusal as Problem Details (15.1).
|
|
174
|
+
rungs: rungsOver(mount.scope, answered),
|
|
175
|
+
handler,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
return { handlers }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** What a handler is keyed by among the mounted: its method and its route. 19.1. */
|
|
182
|
+
export function handlerKey(method: string, address: string): string {
|
|
183
|
+
return `${method} ${address}`
|
|
184
|
+
}
|