@abide/abide 0.123.3 → 0.124.0

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 (89) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +31 -0
  3. package/docs/REGISTRY.md +45 -12
  4. package/docs/RULEBOOK.md +24 -14
  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 +17 -0
  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/reactiveReads.ts +235 -0
  23. package/src/compiler/reactiveTypes.ts +13 -0
  24. package/src/compiler/readsIn.ts +180 -81
  25. package/src/compiler/routesDeclaration.ts +46 -0
  26. package/src/compiler/scaffold.ts +7 -2
  27. package/src/compiler/serveLanguage.ts +26 -20
  28. package/src/compiler/splitImports.ts +2 -2
  29. package/src/server/DesktopSession.ts +213 -0
  30. package/src/server/Followers.ts +399 -0
  31. package/src/server/MODULE_QUERIES.ts +6 -2
  32. package/src/server/PageRender.ts +201 -0
  33. package/src/server/abideModules.ts +7 -7
  34. package/src/server/answerFailure.ts +3 -2
  35. package/src/server/answerRequest.ts +6 -70
  36. package/src/server/answerRpc.ts +48 -260
  37. package/src/server/answerSocket.ts +41 -463
  38. package/src/server/appKit.ts +1 -1
  39. package/src/server/argsContract.ts +1 -1
  40. package/src/server/createApp.ts +8 -2
  41. package/src/server/csp.ts +1 -1
  42. package/src/server/deferredBuilds.ts +3 -16
  43. package/src/server/faceOf.ts +1 -1
  44. package/src/server/generatedSurfaces.ts +2 -1
  45. package/src/server/handlerFileMount.ts +1 -1
  46. package/src/server/index.ts +1 -1
  47. package/src/server/installServer.ts +5 -9
  48. package/src/server/invokeHandler.ts +230 -0
  49. package/src/server/mountHandlers.ts +184 -0
  50. package/src/server/openDesktopWindow.ts +308 -0
  51. package/src/server/preload.ts +18 -0
  52. package/src/server/principal.ts +1 -1
  53. package/src/server/remote.ts +1 -1
  54. package/src/server/render.ts +31 -59
  55. package/src/server/requestScope.ts +5 -36
  56. package/src/server/runDesktop.ts +19 -447
  57. package/src/server/seedBuffer.ts +11 -11
  58. package/src/server/servedApp.ts +1 -1
  59. package/src/server/shell.ts +1 -1
  60. package/src/server/surfaceOf.ts +1 -1
  61. package/src/server/versionOf.ts +1 -1
  62. package/src/shared/RPC_SERVER.ts +27 -31
  63. package/src/shared/Schema.ts +1 -2
  64. package/src/shared/index.ts +1 -1
  65. package/src/shared/{note.ts → issueReport.ts} +22 -0
  66. package/src/shared/loadCall.ts +66 -0
  67. package/src/shared/reactive/Keyed.ts +23 -15
  68. package/src/shared/reactive/Reactive.ts +34 -1001
  69. package/src/shared/reactive/byOf.ts +2 -5
  70. package/src/shared/reactive/callLoad.ts +8 -14
  71. package/src/shared/reactive/cellShape.ts +734 -0
  72. package/src/shared/reactive/index.ts +12 -24
  73. package/src/shared/reactive/pairReactive.ts +5 -3
  74. package/src/shared/reactive/producer.ts +121 -0
  75. package/src/shared/reactive/scoped.ts +3 -2
  76. package/src/shared/reactive/selections.ts +209 -0
  77. package/src/shared/reactive/watch.ts +3 -4
  78. package/src/shared/rpc.ts +8 -5
  79. package/src/shared/validateJson.ts +1 -2
  80. package/src/shared/writePath.ts +29 -0
  81. package/src/ui/Router.ts +53 -69
  82. package/src/ui/outboxQueue.ts +2 -8
  83. package/src/ui/outcomeOf.ts +120 -0
  84. package/src/ui/socketWire.ts +3 -3
  85. package/src/ui/wire.ts +9 -10
  86. package/src/shared/LOAD_OFFLINE.ts +0 -15
  87. package/src/shared/LOAD_SIGNAL.ts +0 -10
  88. package/src/shared/issuesOf.ts +0 -18
  89. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
@@ -16,10 +16,7 @@ import {
16
16
  } from '#shared/ADDRESSES.ts'
17
17
  import { decodedOrKept } from '#shared/decodedOrKept.ts'
18
18
  import { isFailed } from '#shared/Failed.ts'
19
- import { HandlerRoute } from '#shared/HandlerRoute.ts'
20
19
  import { health } from '#shared/health.ts'
21
- import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
22
- import { KEEP_OPENED } from '#shared/KEEP_OPENED.ts'
23
20
  import { matchOf, type Params, partsOf } from '#shared/matchRoute.ts'
24
21
  import { compose } from '#shared/Middleware.ts'
25
22
  import { mounted } from '#shared/mount.ts'
@@ -27,35 +24,31 @@ import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
27
24
  import { notFound as refusedNotFound, refuse } from '#shared/refuse.ts'
28
25
  import { arrive } from '#shared/route.ts'
29
26
  import type { Segment } from '#shared/routePattern.ts'
30
- import type { Method, Rpc } from '#shared/rpc.ts'
31
- import { rpcRequest } from '#shared/rpcRequest.ts'
27
+ import type { Method } from '#shared/rpc.ts'
32
28
 
33
29
  import { answerFailure, laneFailure } from './answerFailure.ts'
34
- import { answerRpc, type Mounted, mountedOwn } from './answerRpc.ts'
30
+ import { answerRpc } from './answerRpc.ts'
35
31
  import { answerBlobTagged, answerValue } from './answerValue.ts'
36
32
  import { answerBridge } from './bridgeRelay.ts'
37
- import { callerOrMint } from './callerOf.ts'
38
33
  import { compressAnswer } from './compressAnswer.ts'
39
- import { noteCallInRender } from './deferredBuilds.ts'
40
34
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
41
35
  import { answerMcp, openApi } from './generatedSurfaces.ts'
42
36
  import { failureOf, type LaneRung } from './hooks.ts'
37
+ import { type Mounted, mountedOwn } from './mountHandlers.ts'
43
38
  import { muxAdmission } from './muxUpgrade.ts'
44
39
  import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
40
+ import { beginPageRender } from './PageRender.ts'
45
41
  import { plainAnswer } from './plainAnswer.ts'
46
42
  import { PRINCIPAL_MOUNT } from './principal.ts'
47
43
  import { boundPage, type Component, renderDocument } from './render.ts'
48
44
  import {
49
45
  adoptRequest,
50
46
  type AnsweringApp,
51
- beginRender,
52
47
  currentRequestScope,
53
- type PageRender,
54
48
  type RequestScope,
55
49
  within,
56
50
  writtenStatus,
57
51
  } from './requestScope.ts'
58
- import { callsOnly, seedFor } from './seedBuffer.ts'
59
52
 
60
53
  // One row of the server's table the compiler writes (`SERVER_TABLE`). The loader is a dynamic import
61
54
  // so a page's markup is off the boot path until an address asks for it, which is the same reason
@@ -146,9 +139,6 @@ export type AppSources = {
146
139
  explained: ((thrown: unknown) => Response) | undefined
147
140
  }
148
141
 
149
- // 41.29 — the seed sink of a render no page hydrates: every call is left to run when it is made.
150
- const SEEDS_NOTHING: PageRender['seed'] = () => false
151
-
152
142
  // 35.19 — what fetch calls a navigation request's destinations, and a script's `fetch`.
153
143
  const PAGE_DESTINATIONS = new Set([
154
144
  'document',
@@ -336,9 +326,6 @@ async function answerMatched(
336
326
  // minted for a request that never becomes a page.
337
327
  if (request.method === 'HEAD')
338
328
  return new Response(null, { status: writtenStatus(), headers })
339
- // 26.5 — the caller the seed buffer is keyed by, minted where the browser carries none: the same
340
- // one `principal.caller` reads for the rest of the request.
341
- const caller = callerOrMint()
342
329
  // 23.1 — WHERE THIS PAGE IS, for its setup to read, in the request's own instances (11.32).
343
330
  // 16.10 — AT THE ADDRESS IT IS SERVED AT, which is the one the browser's router arrives at: the
344
331
  // request reached here with the mount taken off, and a page reading `route.url()` on both sides
@@ -347,23 +334,8 @@ async function answerMatched(
347
334
  at.pathname = mounted(at.pathname)
348
335
  arrive(at.href, page.pattern, params)
349
336
  // 11.32 — the page renders in a scope of its own, so what its values hold is this request's —
350
- // and 41.13, every call it makes is left for the browser's own call to be answered from.
351
- // 41.29 — BUT NOT A PAGE A SCRIPT FETCHED, which nothing hydrates: its entries would answer the
352
- // next call this caller makes with what the render saw, a sign-in since included — save the one
353
- // the worker fetched for the navigation it answers, which is the page that hydrates (45.23).
354
- const keep = request.headers.get(KEEP_HEADER)
355
- const hydrated =
356
- keep === KEEP_OPENED ||
357
- request.headers.get('sec-fetch-dest') !== 'empty'
358
- // 41.30 — SAVE ONE THE WORKER KEEPS, whose early calls are what a later open of it starts.
359
- beginRender({
360
- seed: hydrated
361
- ? seedFor(caller)
362
- : keep !== null
363
- ? callsOnly()
364
- : SEEDS_NOTHING,
365
- calls: [],
366
- })
337
+ // and 41.13, every call it makes is recorded into its render.
338
+ beginPageRender(request)
367
339
  // 41.1, 35.2 — THE DOCUMENT IS THE BUILT PAGE, read whole before there is a `Response` to commit a
368
340
  // status to: a refusal the page read, or a throw, is answered by the nearest error page at its own
369
341
  // status rather than as a 200 the stream then abandons.
@@ -411,42 +383,6 @@ const SURFACES = new Map<
411
383
  [BRIDGE_ADDRESS, (_app, request) => answerBridge(request)],
412
384
  ])
413
385
 
414
- // Each handler's route, compiled once rather than per render that calls it — as its `Rpc` keeps its
415
- // own; compiled again only where it was mounted at another address since.
416
- const ROUTE_OF = new WeakMap<object, HandlerRoute>()
417
-
418
- // What a call made in a page's render reports into: the seed buffer (41.13), and — where the handler
419
- // is mounted — the request the browser will make for it, for the early-call script (41.20). Once per
420
- // render, as the browser's `Rpc` makes it once, and the seed buffer is what says which is the once.
421
- export function reachedInRender(
422
- rpc: object,
423
- args: unknown,
424
- reached: unknown,
425
- ): void {
426
- const page = currentRequestScope()?.page
427
- if (page === undefined) return
428
- noteCallInRender()
429
- if (!page.seed(rpc, args, reached)) return
430
- // 16.10 — the address the browser's own call is made at, stamped under the mount by
431
- // `mountHandlers` — and `undefined` on a handler nothing mounted.
432
- const { address, method } = rpc as Rpc<unknown, unknown>
433
- if (address === undefined) return
434
- let route = ROUTE_OF.get(rpc)
435
- if (route?.address !== address) {
436
- route = new HandlerRoute(address)
437
- ROUTE_OF.set(rpc, route)
438
- }
439
- page.calls.push({
440
- request: rpcRequest(
441
- method,
442
- route,
443
- args as Record<string, unknown> | undefined,
444
- ),
445
- reached,
446
- painted: undefined,
447
- })
448
- }
449
-
450
386
  /**
451
387
  * The server's table the compiler writes (`SERVER_TABLE`): the routes and the error pages.
452
388
  *
@@ -1,21 +1,15 @@
1
- // 16.2 — A HANDLER, ANSWERED OVER HTTP. The call is the in-process one (`#shared/rpc.ts`), made in a
2
- // request's own scope (11.32), and what this module adds is the transport either side of it: the
3
- // args off the request, and the answer onto a response.
1
+ // 16.2 — A HANDLER, ANSWERED OVER HTTP. The call is `invokeHandler`'s, made in a request's own scope
2
+ // (11.32), and what this module adds is the transport either side of it: the args off the request,
3
+ // and the answer onto a response.
4
4
  //
5
- // WIRE-ONLY, per 16.13: the `Origin` gate on a mutation, `maxBodySize`, the `crossOrigin` preflight,
6
- // and 19.7's coercion of the text a URL carried.
5
+ // WIRE-ONLY, per 16.13: `maxBodySize`, the `crossOrigin` preflight, and the text a URL or a form
6
+ // carried the args as.
7
7
 
8
- import type { CopyOptions } from '#shared/CopyOptions.ts'
9
8
  import { type Failed, isFailed } from '#shared/Failed.ts'
10
- import { GLOBAL_GETS } from '#shared/GLOBAL_GETS.ts'
11
9
  import { isRecord } from '#shared/isRecord.ts'
12
- import type { JsonValue } from '#shared/JsonValue.ts'
13
10
  import { mounted as underMount } from '#shared/mount.ts'
14
- import type { Gated, HeldToAnnotation } from '#shared/reactive/argsContract.ts'
15
- import { deadlineOf, heldGenerationOf } from '#shared/reactive/index.ts'
16
- import type { Defaults } from '#shared/reactive/parameterDefaults.ts'
11
+ import { deadlineOf } from '#shared/reactive/producer.ts'
17
12
  import { notFound, refuse } from '#shared/refuse.ts'
18
- import type { Method, Rpc } from '#shared/rpc.ts'
19
13
  import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
20
14
  import { argsInURL } from '#shared/rpcRequest.ts'
21
15
  import { VERSION_HEADER } from '#shared/VERSION_HEADER.ts'
@@ -27,184 +21,24 @@ import {
27
21
  type ErrorPages,
28
22
  handlerHeaders,
29
23
  } from './answerFailure.ts'
30
- import { followAsEvents } from './answerSocket.ts'
31
24
  import { answerValue, namesEtag, sseHeaders } from './answerValue.ts'
32
- import { argsFor, collapsed, derivedArgs } from './argsContract.ts'
33
- import { callerOf } from './callerOf.ts'
25
+ import { collapsed } from './argsContract.ts'
34
26
  import { positiveSetting } from './config.ts'
35
- import {
36
- allows,
37
- closedTo,
38
- crossingOrigin,
39
- crossingRefusal,
40
- } from './crossingOrigin.ts'
41
- import { failureOf, type LaneRung, rungsOver } from './hooks.ts'
27
+ import { allows, crossingOrigin } from './crossingOrigin.ts'
28
+ import { failureOf } from './hooks.ts'
29
+ import { type Invocation, invokeHandler, originGate } from './invokeHandler.ts'
30
+ import type { Mounted } from './mountHandlers.ts'
42
31
  import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
43
32
  import { prefers } from './prefers.ts'
44
- import {
45
- currentRequestScope,
46
- rungsTook,
47
- timed,
48
- unreap,
49
- writtenStatus,
50
- } from './requestScope.ts'
51
- import { type Outcome, outcomeOf, takeSeed } from './seedBuffer.ts'
33
+ import { currentRequestScope, unreap, writtenStatus } from './requestScope.ts'
34
+ import type { Outcome } from './seedBuffer.ts'
52
35
  import { versionOf } from './versionOf.ts'
53
36
 
54
- /**
55
- * One row of the handler table the build writes into the server's table (`SERVER_TABLE`): where a
56
- * handler answers, and its module.
57
- *
58
- * The module is relative to the app's root, which both commands that serve already know.
59
- */
60
- export type Mount = {
61
- address: string
62
- method: Method
63
- source: string
64
- /** The handler file's name, or the private handler's binding. */
65
- name: string
66
- /** What the source's module exports it as: `default` for a handler file. */
67
- export: string
68
- /** The route its hooks are filed at, which its rungs are read over (37.1, 16.72). */
69
- scope: string
70
- /** 23.26 — each matcher its address names, to the anchored source its row is ranked with. */
71
- matchers: Record<string, string>
72
- /** Whether it is a private handler, listed nowhere (16.71). */
73
- private: boolean
74
- /** Whether a browser is handed a wrapper for it. 20.4. */
75
- ui: boolean
76
- /** Whether the command line lists it — the shell's listing and completion, never whether `call` reaches it. 20.3, 38.62. */
77
- cli: boolean
78
- /**
79
- * Whether MCP lists it where its runtime `clients` does not say.
80
- *
81
- * 16.37 — false for a handler typed as returning a `Response`, which only the build can tell.
82
- */
83
- mcp: boolean
84
- /** Its `description`, where the declaration writes one as a literal the build can read. */
85
- description: string | undefined
86
- /**
87
- * What a `GET`'s value declares that the browser's copy behaves by, or `undefined`.
88
- *
89
- * 18.4 — set where its declaration names any of it.
90
- */
91
- copy: CopyOptions | undefined
92
- /**
93
- * The defaults the handler's parameter writes, for the browser's copy to key by.
94
- *
95
- * 11.29 — the server reads its own off the handler.
96
- */
97
- defaults: Defaults | undefined
98
- /** The JSON Schema the handler's args annotation derives, where it takes a record. 19.9. */
99
- args: JsonValue | undefined
100
- /** The JSON Schema of what it answers, where its type says one. 19.21. */
101
- result: JsonValue | undefined
102
- }
103
-
104
- /**
105
- * A row of the table the build wrote, with the import of the module its handler is declared in —
106
- * the handler file itself, or a private handler's `.abide` server module, the one the route table
107
- * loads too, so a page and a call reach one handler.
108
- */
109
- export type MountRow = Mount & { load: () => Promise<Record<string, unknown>> }
110
-
111
- /** A handler's row, mounted: the handler imported and its args schema probed. */
112
- export type Mounted = Mount & {
113
- /**
114
- * The rungs filed over `scope`, read once at mount (16.14) — what an http call and a socket
115
- * subscription to it both pass through (37.2, 18.26).
116
- */
117
- rungs: LaneRung[]
118
- handler: Rpc<unknown, unknown> & Gated<unknown> & HeldToAnnotation
119
- }
120
-
121
- /**
122
- * The row of a handler abide mounts itself, keyed as `answerRpc` looks one up.
123
- *
124
- * Built on the core's `declare` with no generated surface (20.1) — the app's tree never names it.
125
- */
126
- export function mountedOwn(
127
- address: string,
128
- name: string,
129
- handler: object,
130
- ): [string, Mounted] {
131
- // 16.10 — READ WHEN IT IS REPORTED, where a mounted handler's is stamped at boot: the mount is
132
- // the configuration's, and nothing boots this handler.
133
- Object.defineProperty(handler, 'address', {
134
- get: () => underMount(address),
135
- })
136
- return [
137
- address,
138
- {
139
- address,
140
- method: 'GET',
141
- // The root's rungs run before routing (37.3), and nothing is filed below a route
142
- // of abide's own.
143
- rungs: [],
144
- source: '',
145
- name,
146
- export: name,
147
- scope: '/',
148
- matchers: {},
149
- private: false,
150
- ui: false,
151
- cli: false,
152
- mcp: false,
153
- description: undefined,
154
- copy: undefined,
155
- defaults: undefined,
156
- args: undefined,
157
- result: undefined,
158
- handler: handler as Mounted['handler'],
159
- },
160
- ]
161
- }
162
-
163
37
  // 16.58 — the environment's `timeout`, for every call a handler that names none makes.
164
38
  export function environmentTimeout(): number | undefined {
165
39
  return positiveSetting('ABIDE_RPC_TIMEOUT', undefined)
166
40
  }
167
41
 
168
- /**
169
- * The table's handlers, imported and keyed by method and address.
170
- *
171
- * Imported through each row's own loader, which the route table shares, so a page rendering on the
172
- * server and a call over the wire reach one module and so one handler. AT BOOT rather than on the
173
- * first call, because a page's render reaches a handler through the page's own import and has to
174
- * know its address before anything has called it over the wire.
175
- */
176
- export async function mountHandlers(
177
- table: MountRow[],
178
- ): Promise<{ handlers: Map<string, Mounted> }> {
179
- const modules = await Promise.all(table.map((mount) => mount.load()))
180
- const handlers = new Map<string, Mounted>()
181
- for (let at = 0; at < table.length; at += 1) {
182
- const mount = table[at] as Mount
183
- const exported = (modules[at] as Record<string, unknown>)[mount.export]
184
- const handler = exported as Mounted['handler']
185
- // 16.9, 16.10 — the address `rpc.url` and `rpc.raw` build on, under the mount — and what a
186
- // page's render turns a call it made into the browser's request by (41.20).
187
- handler.address = underMount(mount.address)
188
- // 19.9 — the schema the build derived off its annotation, for a handler whose module did not
189
- // file it on load: a bundled executable's faces carry no `derivedArgs` call.
190
- if (mount.args !== undefined) derivedArgs(handler, mount.args)
191
- handlers.set(handlerKey(mount.method, mount.address), {
192
- ...mount,
193
- // 37.1, 16.72 — the route its file's hooks are filed at, which the build read off
194
- // where the file answers (23.19's mount for a package's), never its place on disk.
195
- // 37.14 — a handler's lane answers a refusal as Problem Details (15.1).
196
- rungs: rungsOver(mount.scope, answered),
197
- handler,
198
- })
199
- }
200
- return { handlers }
201
- }
202
-
203
- /** What a handler is keyed by among the mounted: its method and its route. 19.1. */
204
- export function handlerKey(method: string, address: string): string {
205
- return `${method} ${address}`
206
- }
207
-
208
42
  /**
209
43
  * The `Response` to a call of `found` made over http, `params` being what its route matched.
210
44
  *
@@ -228,92 +62,46 @@ export async function answerRpc(
228
62
  return origin !== null && allows(found.handler.crossOrigin, origin)
229
63
  ? preflight(found, origin, request)
230
64
  : answered(notFound({ path: pathname, method: request.method }))
231
- // 16.50 — a `HEAD` is the `GET`'s answer, which the server sends without its body.
232
- const head = request.method === 'HEAD'
233
- const handler = found.handler
234
65
  const inUrl = argsInURL(found.method)
235
66
  const form = isForm(request)
236
- const crossing = crossingOrigin(origin, url)
237
- const closed = closedTo(handler.crossOrigin, crossing)
238
67
  // 21.19 — the origin the answer admits, where the handler declared it may read.
239
- const admitted = closed ? null : crossing
240
- // 16.13 — A MUTATION FROM ANOTHER ORIGIN IS REFUSED unless the handler declared it may come.
241
- if (closed && !inUrl)
242
- return answered(crossingRefusal(crossing, found.method))
243
- const raw = await argsOf(request, url, inUrl, form, handler.maxBodySize)
68
+ const admitted = originGate(
69
+ found.handler,
70
+ crossingOrigin(origin, url),
71
+ false,
72
+ )
73
+ if (isFailed(admitted)) return answered(admitted)
74
+ const raw = await argsOf(
75
+ request,
76
+ url,
77
+ inUrl,
78
+ form,
79
+ found.handler.maxBodySize,
80
+ )
244
81
  if (isFailed(raw)) return answered(raw)
245
- let args: unknown
246
- try {
247
- // A URL's and a form's args are text, and the query tells a bare name from `name=` (19.7).
248
- args = argsFor(
249
- handler,
250
- raw,
251
- inUrl ? url.search : form ? '' : undefined,
252
- params,
253
- )
254
- } catch (refused) {
255
- if (isFailed(refused)) return answered(refused)
256
- throw refused
257
- }
258
- // 41.7 — the call a hydrating page makes for what its render already reached is answered from
259
- // that, and the handler is not run again (41.13).
260
- // 16.51 — a `HEAD` leaves the seed for the `GET` it stands in front of.
261
- const caller = head ? undefined : callerOf()
262
- const seeded =
263
- caller === undefined ? undefined : takeSeed(caller, handler, args)
264
- // The entry key of a `global` value, taken once for every read of it below.
265
- const global = GLOBAL_GETS.get(handler)
266
- const key = global?.key(args)
267
- // 21.15 — A `global` PRODUCTION ALREADY HELD, and not loading, answers without a run: a run
268
- // this call started moves the generation, and one it joined was loading when it came.
269
- const held =
270
- seeded === undefined && key !== undefined ? global?.at(key) : undefined
271
- const heldAt = held === undefined ? undefined : heldGenerationOf(held)
272
- const timing = currentRequestScope()?.timing
273
- if (timing !== undefined) timing.answering = handler
274
- const started = performance.now()
275
- const outcome =
276
- seeded === undefined ? await outcomeOf(handler(args)) : await seeded
277
- // The entry as the run left it, which the answer reads too.
278
- const entry = key === undefined ? undefined : global?.at(key)
279
- const cached =
280
- heldAt !== undefined &&
281
- entry === held &&
282
- heldGenerationOf(held) === heldAt
82
+ // A URL's and a form's args are text, and the query tells a bare name from `name=` (19.7).
83
+ // 16.50 — a `HEAD` is the `GET`'s answer, which the server sends without its body; 16.51 — and
84
+ // it leaves the seed for the `GET` it stands in front of.
85
+ const invoked = await invokeHandler(
86
+ found,
87
+ raw,
88
+ inUrl ? url.search : form ? '' : undefined,
89
+ params,
90
+ request.method !== 'HEAD',
91
+ )
92
+ if (isFailed(invoked)) return answered(invoked)
93
+ const outcome = invoked.outcome
283
94
  // 16.38 — A MUTATION A DOCUMENT MADE — a form posted with no script — is answered as a navigation.
284
95
  if (!inUrl && prefers(request, 'text/html', 'application/json'))
285
96
  return navigated(outcome, request, pathname, pages)
286
- const answer = await answerOutcome(
287
- outcome,
288
- request,
289
- found,
290
- args,
291
- key,
292
- entry,
293
- offline,
294
- )
97
+ const answer = await answerOutcome(invoked, request, found, offline)
295
98
  // A `Response` the handler built is its own — but the origin answer below is laid over it, so it
296
99
  // varies on `Origin` too (21.7), which `handlerHeaders` never reached.
297
100
  if (outcome.settled && outcome.value instanceof Response) {
298
101
  answer.headers.append('vary', 'origin')
299
102
  return admitting(answer, admitted)
300
103
  }
301
- // 21.15 — HOW LONG THE HANDLER TOOK, AND WHICH RUN THIS IS, for the network panel: the row
302
- // already names the handler, so the metric names the run. A stream's time is to its first
303
- // chunk, which the answer's head waited for.
304
- const took = outcome.duration ?? performance.now() - started
305
- const run =
306
- seeded !== undefined
307
- ? 'Seeded'
308
- : cached
309
- ? 'Cached'
310
- : outcome.duration === undefined
311
- ? 'Started'
312
- : 'Direct'
313
- // 21.20 — what the rungs took is theirs, and the handler's own body is what `Direct` reports.
314
- const handled = run === 'Direct' ? timing?.handled : undefined
315
- if (handled !== undefined) rungsTook(took - handled)
316
- timed(timing, run, handled ?? took)
104
+ invoked.timed()
317
105
  return admitting(answer, admitted)
318
106
  }
319
107
 
@@ -335,15 +123,12 @@ function admitting(answer: Response, admitted: string | null): Response {
335
123
  }
336
124
 
337
125
  async function answerOutcome(
338
- outcome: Outcome,
126
+ invoked: Invocation,
339
127
  request: Request,
340
128
  found: Mounted,
341
- args: unknown,
342
- key: string | undefined,
343
- entry: unknown,
344
129
  offline: boolean,
345
130
  ): Promise<Response> {
346
- const handler = found.handler
131
+ const { outcome, entry } = invoked
347
132
  if (!outcome.settled) return answered(failureOf(outcome.error))
348
133
  const value = outcome.value
349
134
  // The `global` value the answer came from, or `undefined` for one built per request. 18.22 — or
@@ -370,7 +155,8 @@ async function answerOutcome(
370
155
  const etag = serving !== undefined ? `"${versionOf(serving)}"` : undefined
371
156
  if (etag !== undefined) {
372
157
  headers.set(VERSION_HEADER, etag)
373
- RPC_SERVER.served?.(handler, serving, key as string)
158
+ for (const followers of RPC_SERVER.following)
159
+ followers.served(found.handler, serving, invoked.key as string)
374
160
  }
375
161
  // 21.22 — A `GET` THE DEVICE KEEPS, where the worker is there to keep it: a value, not a
376
162
  // refusal (16.40) nor a stream with no end to keep (16.32) — a generator's, or the lines a
@@ -404,17 +190,19 @@ async function answerOutcome(
404
190
  // 18.20 — A READER ASKING FOR AN EVENT STREAM IS FOLLOWED, rather than answered once: the value,
405
191
  // then each change, from the version its `Last-Event-ID` names. A handler that streams answers
406
192
  // its own chunks as it always has (16.45).
193
+ const followers = currentRequestScope()?.app?.followers
407
194
  if (
408
195
  etag !== undefined &&
196
+ followers !== undefined &&
409
197
  !streams(outcome.value) &&
410
198
  prefers(request, 'text/event-stream', 'application/json')
411
199
  ) {
412
200
  const events = handlerHeaders()
413
201
  sseHeaders(events)
414
202
  return new Response(
415
- followAsEvents(
203
+ followers.events(
416
204
  found,
417
- args,
205
+ invoked.args,
418
206
  versionInEtag(request.headers.get('last-event-id')),
419
207
  ),
420
208
  { status: 200, headers: events },