@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.
Files changed (91) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +38 -0
  3. package/docs/REGISTRY.md +45 -12
  4. package/docs/RULEBOOK.md +29 -16
  5. package/docs/abide.md +999 -459
  6. package/package.json +1 -1
  7. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  8. package/src/compiler/LoweredFile.ts +609 -0
  9. package/src/compiler/Lowering.ts +46 -4
  10. package/src/compiler/SHARED_NAMES.ts +2 -0
  11. package/src/compiler/ShadowHost.ts +670 -0
  12. package/src/compiler/ShadowMap.ts +1 -1
  13. package/src/compiler/TypeSession.ts +71 -1715
  14. package/src/compiler/appLints.ts +109 -0
  15. package/src/compiler/checkedShadow.ts +1 -1
  16. package/src/compiler/classifyRead.ts +16 -1
  17. package/src/compiler/compileApp.ts +2 -2
  18. package/src/compiler/declaredExports.ts +44 -17
  19. package/src/compiler/diagnoseSources.ts +232 -0
  20. package/src/compiler/formatAbide.ts +5 -6
  21. package/src/compiler/handlersUnder.ts +16 -35
  22. package/src/compiler/projectShadow.ts +1 -0
  23. package/src/compiler/reactiveReads.ts +235 -0
  24. package/src/compiler/reactiveTypes.ts +13 -0
  25. package/src/compiler/readsIn.ts +180 -81
  26. package/src/compiler/routesDeclaration.ts +46 -0
  27. package/src/compiler/scaffold.ts +7 -2
  28. package/src/compiler/serveLanguage.ts +26 -20
  29. package/src/compiler/splitImports.ts +2 -2
  30. package/src/server/BRIDGE_VERSION.ts +1 -1
  31. package/src/server/DesktopSession.ts +213 -0
  32. package/src/server/Followers.ts +399 -0
  33. package/src/server/MODULE_QUERIES.ts +6 -2
  34. package/src/server/PageRender.ts +201 -0
  35. package/src/server/abideModules.ts +7 -7
  36. package/src/server/answerFailure.ts +3 -2
  37. package/src/server/answerRequest.ts +6 -70
  38. package/src/server/answerRpc.ts +48 -260
  39. package/src/server/answerSocket.ts +41 -463
  40. package/src/server/appKit.ts +1 -1
  41. package/src/server/argsContract.ts +1 -1
  42. package/src/server/createApp.ts +8 -2
  43. package/src/server/csp.ts +1 -1
  44. package/src/server/deferredBuilds.ts +3 -16
  45. package/src/server/faceOf.ts +1 -1
  46. package/src/server/generatedSurfaces.ts +2 -1
  47. package/src/server/handlerFileMount.ts +1 -1
  48. package/src/server/index.ts +1 -1
  49. package/src/server/installServer.ts +5 -9
  50. package/src/server/invokeHandler.ts +230 -0
  51. package/src/server/mountHandlers.ts +184 -0
  52. package/src/server/openDesktopWindow.ts +308 -0
  53. package/src/server/preload.ts +18 -0
  54. package/src/server/principal.ts +1 -1
  55. package/src/server/remote.ts +1 -1
  56. package/src/server/render.ts +31 -59
  57. package/src/server/requestScope.ts +5 -36
  58. package/src/server/runDesktop.ts +19 -447
  59. package/src/server/seedBuffer.ts +11 -11
  60. package/src/server/servedApp.ts +1 -1
  61. package/src/server/shell.ts +1 -1
  62. package/src/server/surfaceOf.ts +1 -1
  63. package/src/server/versionOf.ts +1 -1
  64. package/src/shared/RPC_SERVER.ts +27 -31
  65. package/src/shared/Schema.ts +1 -2
  66. package/src/shared/index.ts +1 -1
  67. package/src/shared/{note.ts → issueReport.ts} +22 -0
  68. package/src/shared/loadCall.ts +66 -0
  69. package/src/shared/reactive/Keyed.ts +23 -15
  70. package/src/shared/reactive/Reactive.ts +46 -1002
  71. package/src/shared/reactive/byOf.ts +2 -5
  72. package/src/shared/reactive/callLoad.ts +8 -14
  73. package/src/shared/reactive/cellShape.ts +734 -0
  74. package/src/shared/reactive/index.ts +12 -24
  75. package/src/shared/reactive/pairReactive.ts +5 -3
  76. package/src/shared/reactive/producer.ts +121 -0
  77. package/src/shared/reactive/scoped.ts +3 -2
  78. package/src/shared/reactive/selections.ts +209 -0
  79. package/src/shared/reactive/watch.ts +3 -4
  80. package/src/shared/rpc.ts +8 -5
  81. package/src/shared/validateJson.ts +1 -2
  82. package/src/shared/writePath.ts +29 -0
  83. package/src/ui/Router.ts +53 -69
  84. package/src/ui/outboxQueue.ts +2 -8
  85. package/src/ui/outcomeOf.ts +120 -0
  86. package/src/ui/socketWire.ts +3 -3
  87. package/src/ui/wire.ts +9 -10
  88. package/src/shared/LOAD_OFFLINE.ts +0 -15
  89. package/src/shared/LOAD_SIGNAL.ts +0 -10
  90. package/src/shared/issuesOf.ts +0 -18
  91. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
@@ -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 (`?text`), a component's stylesheet
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, type Mounted } from './answerRpc.ts'
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')
@@ -1,4 +1,4 @@
1
- import type { Mount } from './answerRpc.ts'
1
+ import type { Mount } from './mountHandlers.ts'
2
2
 
3
3
  /**
4
4
  * The row of a handler file (16.73): its default, answered at the route the file is placed at and
@@ -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 './deferredBuilds.ts'
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. Three are filled elsewhere: `SCOPING.provider` at
6
- // `requestScope.ts`'s import (its comment says why), `RPC_SERVER.raw` per boot, and
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 = reachedInRender
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
+ }