@abide/abide 0.100.0 → 0.102.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.
- package/CHANGELOG.md +31 -0
- package/bin/abide.ts +227 -163
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +10 -3
- package/scaffold/AGENTS.md +31 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -56
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +124 -39
- package/src/server/answerSocket.ts +141 -48
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +179 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +37 -37
- package/src/server/componentModules.d.ts +3 -1
- package/src/server/compressAnswer.ts +48 -14
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +59 -73
- package/src/server/devWorker.ts +144 -183
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +81 -39
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/GLOBAL_GETS.ts +9 -3
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/RPC_SERVER.ts +2 -2
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- package/src/shared/reactive/Cursor.ts +3 -9
- package/src/shared/reactive/Keyed.ts +4 -3
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +81 -60
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +155 -21
- package/src/shared/reactive/watch.ts +10 -6
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +47 -13
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +38 -31
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
package/src/server/answerRpc.ts
CHANGED
|
@@ -16,6 +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 { routeOf } from '#shared/routePattern.ts'
|
|
19
20
|
import type { Method, Rpc } from '#shared/rpc.ts'
|
|
20
21
|
import { argsInURL } from '#shared/rpcRequest.ts'
|
|
21
22
|
import { VERSION_HEADER } from '#shared/VERSION_HEADER.ts'
|
|
@@ -30,7 +31,12 @@ import { followAsEvents } from './answerSocket.ts'
|
|
|
30
31
|
import { answerValue, namesEtag, sseHeaders } from './answerValue.ts'
|
|
31
32
|
import { argsFor } from './argsFor.ts'
|
|
32
33
|
import { callerOf } from './callerOf.ts'
|
|
33
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
argsDocumentOf,
|
|
36
|
+
coerceArgs,
|
|
37
|
+
collapsed,
|
|
38
|
+
type Document,
|
|
39
|
+
} from './coerceArgs.ts'
|
|
34
40
|
import { positiveSetting } from './config.ts'
|
|
35
41
|
import {
|
|
36
42
|
allows,
|
|
@@ -38,7 +44,7 @@ import {
|
|
|
38
44
|
crossingOrigin,
|
|
39
45
|
crossingRefusal,
|
|
40
46
|
} from './crossingOrigin.ts'
|
|
41
|
-
import { failureOf } from './hooks.ts'
|
|
47
|
+
import { type AppRung, failureOf, rungsOver } from './hooks.ts'
|
|
42
48
|
import { prefers } from './prefers.ts'
|
|
43
49
|
import {
|
|
44
50
|
currentRequestScope,
|
|
@@ -51,7 +57,8 @@ import { type Outcome, outcomeOf, takeSeed } from './seedBuffer.ts'
|
|
|
51
57
|
import { versionOf } from './versionOf.ts'
|
|
52
58
|
|
|
53
59
|
/**
|
|
54
|
-
* One row of the table the build writes
|
|
60
|
+
* One row of the handler table the build writes into the server's table (`SERVER_TABLE`): where a
|
|
61
|
+
* handler answers, and its module.
|
|
55
62
|
*
|
|
56
63
|
* The module is relative to the app's root, which both commands that serve already know.
|
|
57
64
|
*/
|
|
@@ -62,7 +69,7 @@ export type Mount = {
|
|
|
62
69
|
name: string
|
|
63
70
|
/** Whether a browser is handed a wrapper for it. 20.4. */
|
|
64
71
|
ui: boolean
|
|
65
|
-
/** Whether the command line lists it — the shell's listing and completion, never whether `call` reaches it. 20.3, 38.
|
|
72
|
+
/** Whether the command line lists it — the shell's listing and completion, never whether `call` reaches it. 20.3, 38.62. */
|
|
66
73
|
cli: boolean
|
|
67
74
|
/**
|
|
68
75
|
* Whether MCP lists it where its runtime `clients` does not say.
|
|
@@ -88,9 +95,23 @@ export type Mount = {
|
|
|
88
95
|
args: JsonValue | undefined
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A row of the table the build wrote, with the import of the module its handler is declared in —
|
|
100
|
+
* the file's server module, the one the route table loads too, so a page and a call reach one
|
|
101
|
+
* handler.
|
|
102
|
+
*/
|
|
103
|
+
export type MountRow = Mount & { load: () => Promise<Record<string, unknown>> }
|
|
104
|
+
|
|
91
105
|
/** A handler's row, mounted: the handler imported and its args schema probed. */
|
|
92
106
|
export type Mounted = Mount & {
|
|
93
107
|
method: Method
|
|
108
|
+
/** The route of the file declaring it, which its hooks are filed at (37.1, 37.2). */
|
|
109
|
+
route: string
|
|
110
|
+
/**
|
|
111
|
+
* The rungs filed over `route`, read once at mount (16.14) — what an http call and a socket
|
|
112
|
+
* subscription to it both pass through (37.2, 18.26).
|
|
113
|
+
*/
|
|
114
|
+
rungs: AppRung[]
|
|
94
115
|
handler: Rpc<unknown, unknown> & Gated<unknown>
|
|
95
116
|
/**
|
|
96
117
|
* The args' JSON Schema, probed once at mount.
|
|
@@ -127,6 +148,10 @@ export function mountedOwn(
|
|
|
127
148
|
{
|
|
128
149
|
address,
|
|
129
150
|
method: 'GET',
|
|
151
|
+
route: '/',
|
|
152
|
+
// The root's rungs run before routing (37.3), and nothing is filed below a route
|
|
153
|
+
// of abide's own.
|
|
154
|
+
rungs: [],
|
|
130
155
|
source: '',
|
|
131
156
|
name,
|
|
132
157
|
ui: false,
|
|
@@ -149,20 +174,17 @@ export function environmentTimeout(): number | undefined {
|
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
/**
|
|
152
|
-
* The table's handlers, imported and keyed by address.
|
|
177
|
+
* The table's handlers, imported and keyed by method and address.
|
|
153
178
|
*
|
|
154
|
-
* Imported
|
|
155
|
-
* one module and so one handler. AT BOOT rather than on the
|
|
156
|
-
* reaches a handler through the page's own import and has to
|
|
157
|
-
* called it over the wire.
|
|
179
|
+
* Imported through each row's own loader, which the route table shares, so a page rendering on the
|
|
180
|
+
* server and a call over the wire reach one module and so one handler. AT BOOT rather than on the
|
|
181
|
+
* first call, because a page's render reaches a handler through the page's own import and has to
|
|
182
|
+
* know its address before anything has called it over the wire.
|
|
158
183
|
*/
|
|
159
184
|
export async function mountHandlers(
|
|
160
|
-
table:
|
|
161
|
-
moduleOf: (source: string) => Promise<Record<string, unknown>>,
|
|
185
|
+
table: MountRow[],
|
|
162
186
|
): Promise<{ handlers: Map<string, Mounted> }> {
|
|
163
|
-
const modules = await Promise.all(
|
|
164
|
-
table.map((mount) => moduleOf(mount.source)),
|
|
165
|
-
)
|
|
187
|
+
const modules = await Promise.all(table.map((mount) => mount.load()))
|
|
166
188
|
const handlers = new Map<string, Mounted>()
|
|
167
189
|
for (let at = 0; at < table.length; at += 1) {
|
|
168
190
|
const mount = table[at] as Mount
|
|
@@ -172,9 +194,14 @@ export async function mountHandlers(
|
|
|
172
194
|
// 16.9, 16.10 — the address `rpc.url` and `rpc.raw` build on, under the mount — and what a
|
|
173
195
|
// page's render turns a call it made into the browser's request by (41.20).
|
|
174
196
|
handler.address = underMount(mount.address)
|
|
175
|
-
|
|
197
|
+
// 23.19 — the route its address answers below, which is its mount's for a package's file,
|
|
198
|
+
// never the file's place on disk.
|
|
199
|
+
const route = routeOf(mount.address)
|
|
200
|
+
handlers.set(handlerKey(mount.method, mount.address), {
|
|
176
201
|
...mount,
|
|
177
202
|
method: mount.method,
|
|
203
|
+
route,
|
|
204
|
+
rungs: rungsOver(route),
|
|
178
205
|
handler,
|
|
179
206
|
argsDocument: declared ?? (mount.args as Document | undefined),
|
|
180
207
|
wireArgs: declared === undefined ? mount.args : undefined,
|
|
@@ -183,30 +210,34 @@ export async function mountHandlers(
|
|
|
183
210
|
return { handlers }
|
|
184
211
|
}
|
|
185
212
|
|
|
186
|
-
/**
|
|
213
|
+
/** What a handler is keyed by among the mounted: its method and its route. 19.1. */
|
|
214
|
+
export function handlerKey(method: string, address: string): string {
|
|
215
|
+
return `${method} ${address}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The `Response` to a call of `found` made over http, `params` being what its route matched.
|
|
220
|
+
*
|
|
221
|
+
* `found` answers the request's method — or, for an `OPTIONS`, the method its preflight asks
|
|
222
|
+
* about: which handler a request reaches is the route table's to say (19.21).
|
|
223
|
+
*/
|
|
187
224
|
export async function answerRpc(
|
|
188
|
-
|
|
225
|
+
found: Mounted,
|
|
226
|
+
params: Record<string, string | string[]>,
|
|
189
227
|
url: URL,
|
|
190
228
|
request: Request,
|
|
191
229
|
// 16.38 — what paints a refusal for a request that came as a navigation.
|
|
192
230
|
pages: ErrorPages,
|
|
193
231
|
): Promise<Response> {
|
|
194
232
|
const pathname = url.pathname
|
|
195
|
-
const found = handlers.get(pathname)
|
|
196
233
|
const origin = request.headers.get('origin')
|
|
197
234
|
// 16.24 — THE PREFLIGHT, answered by abide where the handler declared `crossOrigin`.
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
allows(found.handler.crossOrigin, origin)
|
|
203
|
-
)
|
|
204
|
-
return preflight(found, origin, request)
|
|
235
|
+
if (request.method === 'OPTIONS')
|
|
236
|
+
return origin !== null && allows(found.handler.crossOrigin, origin)
|
|
237
|
+
? preflight(found, origin, request)
|
|
238
|
+
: answered(notFound({ path: pathname, method: request.method }))
|
|
205
239
|
// 16.50 — a `HEAD` is the `GET`'s answer, which the server sends without its body.
|
|
206
240
|
const head = request.method === 'HEAD'
|
|
207
|
-
// 15.7 — no handler at this address AND this method is one refusal, not two.
|
|
208
|
-
if (found === undefined || found.method !== (head ? 'GET' : request.method))
|
|
209
|
-
return answered(notFound({ path: pathname, method: request.method }))
|
|
210
241
|
const handler = found.handler
|
|
211
242
|
const inUrl = argsInURL(found.method)
|
|
212
243
|
const form = isForm(request)
|
|
@@ -226,10 +257,12 @@ export async function answerRpc(
|
|
|
226
257
|
return answered(overCeiling(ceiling))
|
|
227
258
|
const raw = await argsOf(request, url, inUrl, form, ceiling)
|
|
228
259
|
if (isFailed(raw)) return answered(raw)
|
|
260
|
+
const sent = withParameters(raw, params, inUrl || form, found)
|
|
261
|
+
if (isFailed(sent)) return answered(sent)
|
|
229
262
|
let args: unknown
|
|
230
263
|
try {
|
|
231
264
|
// A URL's and a form's args are text, and the query tells a bare name from `name=` (19.7).
|
|
232
|
-
args = argsFor(found,
|
|
265
|
+
args = argsFor(found, sent, inUrl ? url.search : form ? '' : undefined)
|
|
233
266
|
} catch (refused) {
|
|
234
267
|
if (isFailed(refused)) return answered(refused)
|
|
235
268
|
throw refused
|
|
@@ -243,24 +276,36 @@ export async function answerRpc(
|
|
|
243
276
|
const caller = head ? undefined : callerOf()
|
|
244
277
|
const seeded =
|
|
245
278
|
caller === undefined ? undefined : takeSeed(caller, handler, args)
|
|
279
|
+
// The entry key of a `global` value, taken once for every read of it below.
|
|
280
|
+
const global = GLOBAL_GETS.get(handler)
|
|
281
|
+
const key = global?.key(args)
|
|
246
282
|
// 21.15 — A `global` PRODUCTION ALREADY HELD, and not loading, answers without a run: a run
|
|
247
283
|
// this call started moves the generation, and one it joined was loading when it came.
|
|
248
284
|
const held =
|
|
249
|
-
seeded === undefined ?
|
|
285
|
+
seeded === undefined && key !== undefined ? global?.at(key) : undefined
|
|
250
286
|
const heldAt = held === undefined ? undefined : heldGenerationOf(held)
|
|
251
287
|
const timing = currentRequestScope()?.timing
|
|
252
288
|
if (timing !== undefined) timing.answering = handler
|
|
253
289
|
const started = performance.now()
|
|
254
290
|
const outcome =
|
|
255
291
|
seeded === undefined ? await outcomeOf(handler(args)) : await seeded
|
|
292
|
+
// The entry as the run left it, which the answer reads too.
|
|
293
|
+
const entry = key === undefined ? undefined : global?.at(key)
|
|
256
294
|
const cached =
|
|
257
295
|
heldAt !== undefined &&
|
|
258
|
-
|
|
296
|
+
entry === held &&
|
|
259
297
|
heldGenerationOf(held) === heldAt
|
|
260
298
|
// 16.38 — A MUTATION A DOCUMENT MADE — a form posted with no script — is answered as a navigation.
|
|
261
299
|
if (!inUrl && prefers(request, 'text/html', 'application/json'))
|
|
262
300
|
return navigated(outcome, request, pathname, pages)
|
|
263
|
-
const answer = await answerOutcome(
|
|
301
|
+
const answer = await answerOutcome(
|
|
302
|
+
outcome,
|
|
303
|
+
request,
|
|
304
|
+
found,
|
|
305
|
+
args,
|
|
306
|
+
key,
|
|
307
|
+
entry,
|
|
308
|
+
)
|
|
264
309
|
// A `Response` the handler built is its own — but the origin answer below is laid over it, so it
|
|
265
310
|
// varies on `Origin` too (21.7), which `handlerHeaders` never reached.
|
|
266
311
|
if (outcome.settled && outcome.value instanceof Response) {
|
|
@@ -308,23 +353,24 @@ async function answerOutcome(
|
|
|
308
353
|
request: Request,
|
|
309
354
|
found: Mounted,
|
|
310
355
|
args: unknown,
|
|
356
|
+
key: string | undefined,
|
|
357
|
+
entry: unknown,
|
|
311
358
|
): Promise<Response> {
|
|
312
359
|
const handler = found.handler
|
|
313
360
|
if (!outcome.settled) return answered(failureOf(outcome.error))
|
|
361
|
+
const value = outcome.value
|
|
314
362
|
// The `global` value the answer came from, or `undefined` for one built per request. 18.22 — or
|
|
315
363
|
// for one whose production has moved past what is answered: a seeded answer is what the render
|
|
316
364
|
// reached (41.13). 18.23 — or for a binary.
|
|
317
|
-
const global = GLOBAL_GETS.get(handler)?.(args)
|
|
318
|
-
const value = outcome.value
|
|
319
365
|
const serving =
|
|
320
|
-
|
|
366
|
+
entry !== undefined &&
|
|
321
367
|
!(
|
|
322
368
|
value instanceof Blob ||
|
|
323
369
|
value instanceof ArrayBuffer ||
|
|
324
370
|
ArrayBuffer.isView(value)
|
|
325
371
|
) &&
|
|
326
|
-
(streams(value) || (
|
|
327
|
-
?
|
|
372
|
+
(streams(value) || (entry as { peek(): unknown }).peek() === value)
|
|
373
|
+
? entry
|
|
328
374
|
: undefined
|
|
329
375
|
// 21.3 — what the handler wrote through `response` is laid over it at the exit (21.10).
|
|
330
376
|
const headers = handlerHeaders()
|
|
@@ -337,7 +383,7 @@ async function answerOutcome(
|
|
|
337
383
|
const etag = serving !== undefined ? `"${versionOf(serving)}"` : undefined
|
|
338
384
|
if (etag !== undefined) {
|
|
339
385
|
headers.set(VERSION_HEADER, etag)
|
|
340
|
-
RPC_SERVER.served?.(handler, serving)
|
|
386
|
+
RPC_SERVER.served?.(handler, serving, key as string)
|
|
341
387
|
}
|
|
342
388
|
// 21.8, 21.9, 21.11 — WHAT IS LEFT OF THE LIFE OF THE PRODUCTION ANSWERED, in place of `no-store`
|
|
343
389
|
// and nothing else. A `ttl` of Infinity is bounded by `invalidate` alone (11.35), which no cache
|
|
@@ -423,6 +469,39 @@ async function argsOf(
|
|
|
423
469
|
return parsed
|
|
424
470
|
}
|
|
425
471
|
|
|
472
|
+
/**
|
|
473
|
+
* The args a request sent, with the parameters its route matched laid in.
|
|
474
|
+
*
|
|
475
|
+
* 16.65 — the path's, whatever the method. 16.66 — a parameter the query or the body names as
|
|
476
|
+
* well is refused rather than one of the two chosen. A JSON body's members are typed already, so the
|
|
477
|
+
* path's text is coerced to the declared types before it joins them (19.7); text args are coerced
|
|
478
|
+
* with the rest of the text.
|
|
479
|
+
*/
|
|
480
|
+
function withParameters(
|
|
481
|
+
sent: unknown,
|
|
482
|
+
params: Record<string, string | string[]>,
|
|
483
|
+
text: boolean,
|
|
484
|
+
found: Mounted,
|
|
485
|
+
): unknown | Failed<'HttpError', undefined> {
|
|
486
|
+
let none = true
|
|
487
|
+
for (const name in params) {
|
|
488
|
+
none = false
|
|
489
|
+
if (isRecord(sent) && name in sent)
|
|
490
|
+
return refuse(
|
|
491
|
+
400,
|
|
492
|
+
`The parameter '${name}' is in the path and in the request as well.`,
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
if (none) return sent
|
|
496
|
+
const fromPath = text
|
|
497
|
+
? params
|
|
498
|
+
: (coerceArgs(params, found.argsDocument, '') as Record<
|
|
499
|
+
string,
|
|
500
|
+
unknown
|
|
501
|
+
>)
|
|
502
|
+
return isRecord(sent) ? { ...sent, ...fromPath } : { ...fromPath }
|
|
503
|
+
}
|
|
504
|
+
|
|
426
505
|
/**
|
|
427
506
|
* The request's body read up to `ceiling`, as a `Response`, or a 413 refusal past it.
|
|
428
507
|
*
|
|
@@ -474,6 +553,12 @@ function preflight(found: Mounted, origin: string, request: Request): Response {
|
|
|
474
553
|
* 16.57 — `Infinity` is never reaped, which bun spells 0; any finite bound is capped at the 255
|
|
475
554
|
* seconds bun's idle timeout can hold.
|
|
476
555
|
*
|
|
556
|
+
* FOUR SECONDS PAST THE TIMEOUT, NOT AT IT: a load that goes its timeout is refused at that moment,
|
|
557
|
+
* and a stream's refusal is a frame written then (16.68). bun's sockets check an idle timeout on a
|
|
558
|
+
* tick four seconds wide, so an idle timeout of the same second — or one past it — fired anywhere in
|
|
559
|
+
* the tick before it and reaped the connection ahead of the frame, one stream in two under `abide
|
|
560
|
+
* dev`. Four is the tick, so the refusal always goes out first.
|
|
561
|
+
*
|
|
477
562
|
* ON THE REQUEST THE LISTENER RECEIVED, never the one being answered: under a mount, or past a
|
|
478
563
|
* rung that handed `next` its own, that is a copy, and bun's `timeout` does nothing for a request
|
|
479
564
|
* it was not handed itself.
|
|
@@ -483,7 +568,7 @@ export function raiseTimeout(milliseconds: number): void {
|
|
|
483
568
|
const received = scope?.received
|
|
484
569
|
if (scope === undefined || received === undefined) return
|
|
485
570
|
const seconds = Number.isFinite(milliseconds)
|
|
486
|
-
? Math.min(255, Math.ceil(milliseconds / 1000))
|
|
571
|
+
? Math.min(255, Math.ceil(milliseconds / 1000) + 4)
|
|
487
572
|
: Number.POSITIVE_INFINITY
|
|
488
573
|
if (seconds <= scope.timeout) return
|
|
489
574
|
scope.timeout = seconds
|
|
@@ -10,18 +10,20 @@
|
|
|
10
10
|
// built per request, since a trigger names args rather than a value. A `global` value is also fed its productions, a guarded one only to a
|
|
11
11
|
// subscription its rungs admitted when it opened (18.8) — they run once, in the connection's request.
|
|
12
12
|
|
|
13
|
-
import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
14
13
|
import { type Problem, problemOf } from '#shared/Failed.ts'
|
|
15
14
|
import { GLOBAL_GETS } from '#shared/GLOBAL_GETS.ts'
|
|
16
15
|
import { namedLogger } from '#shared/log.ts'
|
|
16
|
+
import { compose } from '#shared/Middleware.ts'
|
|
17
|
+
import { unmounted } from '#shared/mount.ts'
|
|
18
|
+
import type { BrowserFrame, ServerFrame } from '#shared/muxFrames.ts'
|
|
17
19
|
import { FOLLOWED } from '#shared/reactive/FOLLOWED.ts'
|
|
18
20
|
import { servedCellOf } from '#shared/reactive/index.ts'
|
|
19
21
|
import { matchesPattern } from '#shared/reactive/matchesPattern.ts'
|
|
20
22
|
import { PATCH_SINKS } from '#shared/reactive/PATCH_SINKS.ts'
|
|
21
23
|
import { unowned } from '#shared/reactive/watch.ts'
|
|
22
24
|
import { refuse } from '#shared/refuse.ts'
|
|
23
|
-
import
|
|
24
|
-
import type
|
|
25
|
+
import { rpcRequest } from '#shared/rpcRequest.ts'
|
|
26
|
+
import { handlerKey, type Mounted } from './answerRpc.ts'
|
|
25
27
|
import { argsFor } from './argsFor.ts'
|
|
26
28
|
import { closedTo, crossingOrigin, crossingRefusal } from './crossingOrigin.ts'
|
|
27
29
|
import { failureOf } from './hooks.ts'
|
|
@@ -38,7 +40,7 @@ import { patchesSince, versionOf } from './versionOf.ts'
|
|
|
38
40
|
* The far end of one connection to the mux, which its frames are written to.
|
|
39
41
|
*
|
|
40
42
|
* Bun's socket under `abide start`, and under `abide dev` a stand-in that posts each frame to the
|
|
41
|
-
* listener holding the real one (38.
|
|
43
|
+
* listener holding the real one (38.14).
|
|
42
44
|
*/
|
|
43
45
|
export type Peer = {
|
|
44
46
|
send(text: string): unknown
|
|
@@ -54,19 +56,30 @@ type Follower = {
|
|
|
54
56
|
peer: Peer
|
|
55
57
|
id: number
|
|
56
58
|
args: unknown
|
|
59
|
+
// The entry key its args are filed under (`GLOBAL_GETS`), taken once as it subscribes.
|
|
60
|
+
key: string
|
|
57
61
|
// The `global` entry its productions are pushed from, which is what a refresh would otherwise
|
|
58
62
|
// re-fetch — or `undefined`, and it is sent triggers instead.
|
|
59
63
|
serving: unknown
|
|
60
64
|
stopFeed: (() => void) | undefined
|
|
61
65
|
// 18.6 — the version it was last sent, or named on subscribing.
|
|
62
66
|
sent: number | undefined
|
|
63
|
-
// 13.15 — the literal `tags` the build copied onto the browser's copy, for a sweep to match.
|
|
64
|
-
tags: string[] | undefined
|
|
65
67
|
// 18.8 — whether the handler's rungs let it through when it opened: a guarded `GET` feeds only
|
|
66
68
|
// a follower they admitted, and sends the rest its triggers alone.
|
|
67
69
|
admitted: boolean
|
|
68
70
|
}
|
|
69
|
-
|
|
71
|
+
type Followed = {
|
|
72
|
+
// 13.15 — the literal `tags` the build copied onto the browser's copy, for a sweep to match:
|
|
73
|
+
// the handler's, so one for all of its followers.
|
|
74
|
+
tags: string[] | undefined
|
|
75
|
+
// What a trigger walks, matching each follower's args against its pattern.
|
|
76
|
+
all: Set<Follower>
|
|
77
|
+
// BY ENTRY KEY, so an entry answered walks the followers of its own args alone (18.8). Filed
|
|
78
|
+
// apart from `all` rather than instead of it: a walk of every follower through a set per key
|
|
79
|
+
// measured twice a flat set's.
|
|
80
|
+
byKey: Map<string, Set<Follower>>
|
|
81
|
+
}
|
|
82
|
+
const FOLLOWERS = new Map<object, Followed>()
|
|
70
83
|
|
|
71
84
|
// 13.12 — narrowed by the pattern the trigger was handed, as the handler's own entries are.
|
|
72
85
|
export function broadcast(
|
|
@@ -74,9 +87,9 @@ export function broadcast(
|
|
|
74
87
|
member: 'invalidate' | 'refresh',
|
|
75
88
|
pattern: unknown,
|
|
76
89
|
): void {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
for (const follower of
|
|
90
|
+
const followed = FOLLOWERS.get(rpc)
|
|
91
|
+
if (followed === undefined) return
|
|
92
|
+
for (const follower of followed.all) {
|
|
80
93
|
if (!matchesPattern(follower.args, pattern)) continue
|
|
81
94
|
// A fed copy is refreshed by the production the reload mints (7.18), and invalidated through
|
|
82
95
|
// the cell it is fed from (`FOLLOWED`), whichever trigger reached it — where the entry it is
|
|
@@ -93,13 +106,14 @@ export function swept(
|
|
|
93
106
|
member: 'invalidate' | 'refresh',
|
|
94
107
|
tags: string[] | null,
|
|
95
108
|
): void {
|
|
96
|
-
for (const [rpc,
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
for (const [rpc, followed] of FOLLOWERS) {
|
|
110
|
+
if (tags !== null && !sharesTag(followed.tags, tags)) continue
|
|
111
|
+
for (const follower of followed.all) {
|
|
99
112
|
if (attach(rpc as Mounted['handler'], follower) !== undefined)
|
|
100
113
|
continue
|
|
101
114
|
send(follower.peer, { op: member, id: follower.id })
|
|
102
115
|
}
|
|
116
|
+
}
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
function sharesTag(held: string[] | undefined, swept: string[]): boolean {
|
|
@@ -110,10 +124,14 @@ function sharesTag(held: string[] | undefined, swept: string[]): boolean {
|
|
|
110
124
|
|
|
111
125
|
// 18.8 — AN ENTRY ANSWERED THAT NOBODY IS FED FROM: one made after a follower subscribed for its
|
|
112
126
|
// args, or rebuilt after the one it was fed from was evicted. An entry already fed is the common
|
|
113
|
-
// answer, and asks nothing more.
|
|
114
|
-
export function feedFromServed(
|
|
127
|
+
// answer, and asks nothing more; one that is not reaches the followers filed under its key.
|
|
128
|
+
export function feedFromServed(
|
|
129
|
+
rpc: object,
|
|
130
|
+
serving: unknown,
|
|
131
|
+
key: string,
|
|
132
|
+
): void {
|
|
115
133
|
if (FEEDS.has(serving as object)) return
|
|
116
|
-
const followers = FOLLOWERS.get(rpc)
|
|
134
|
+
const followers = FOLLOWERS.get(rpc)?.byKey.get(key)
|
|
117
135
|
if (followers === undefined) return
|
|
118
136
|
for (const follower of followers)
|
|
119
137
|
attach(rpc as Mounted['handler'], follower)
|
|
@@ -125,10 +143,10 @@ export function feedFromServed(rpc: object, serving: unknown): void {
|
|
|
125
143
|
* Only where that entry is not the one it is fed from already.
|
|
126
144
|
*/
|
|
127
145
|
function attach(rpc: Mounted['handler'], follower: Follower): unknown {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
146
|
+
// 18.8, 18.26 — what its own rungs or its route's refused is sent its triggers alone.
|
|
147
|
+
const serving = !follower.admitted
|
|
148
|
+
? undefined
|
|
149
|
+
: GLOBAL_GETS.get(rpc)?.at(follower.key)
|
|
132
150
|
if (serving === follower.serving) return serving
|
|
133
151
|
follower.stopFeed?.()
|
|
134
152
|
// A version is one entry's count, so what it was sent of another says nothing about this one.
|
|
@@ -234,8 +252,8 @@ export class MuxConnection {
|
|
|
234
252
|
return
|
|
235
253
|
}
|
|
236
254
|
const id = frame.id ?? 0
|
|
237
|
-
// THE ADDRESS
|
|
238
|
-
//
|
|
255
|
+
// THE ADDRESS BELOW THE MOUNT, which is what the handlers are keyed by: a browser names it
|
|
256
|
+
// as its wrapper resolved it, which is a whole URL, or a path under the app's mount.
|
|
239
257
|
const address = addressIn(frame.address ?? '')
|
|
240
258
|
const subscriptions = this.subscriptions
|
|
241
259
|
if (frame.op === 'unsub') {
|
|
@@ -244,7 +262,9 @@ export class MuxConnection {
|
|
|
244
262
|
return
|
|
245
263
|
}
|
|
246
264
|
const handler =
|
|
247
|
-
frame.op === 'sub'
|
|
265
|
+
frame.op === 'sub'
|
|
266
|
+
? this.mux.handlers.get(handlerKey('GET', address))
|
|
267
|
+
: undefined
|
|
248
268
|
const followed = handler?.ui ? handler : undefined
|
|
249
269
|
if (followed === undefined) {
|
|
250
270
|
send(this.peer, {
|
|
@@ -279,16 +299,24 @@ export class MuxConnection {
|
|
|
279
299
|
// 18.8 — A GUARDED `GET`'s RUNGS RUN ONCE, as the subscription opens, in this
|
|
280
300
|
// connection's request: one they admit is fed the value, and one they refuse is sent its
|
|
281
301
|
// triggers alone.
|
|
282
|
-
|
|
302
|
+
// 18.26 — AND THE MIDDLEWARE ITS FILE'S ROUTE FILED, over the request a `GET` of it
|
|
303
|
+
// would be: one a rung answers in the handler's place is not fed, as an http call it
|
|
304
|
+
// answered would never reach the handler.
|
|
305
|
+
const routed = followed.rungs.length > 0
|
|
306
|
+
let admitted = !followed.handler.guarded && !routed
|
|
283
307
|
if (!admitted) {
|
|
284
308
|
subscriptions.get(id)?.()
|
|
285
309
|
subscriptions.set(id, pending)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
310
|
+
// No route rungs, no turn: a guarded `GET`'s rungs start in this frame's own run.
|
|
311
|
+
admitted =
|
|
312
|
+
!routed || (await routeAdmits(this.scope, followed, args))
|
|
313
|
+
if (admitted && followed.handler.guarded)
|
|
314
|
+
admitted = await Promise.resolve(
|
|
315
|
+
followed.handler(args as never) as PromiseLike<unknown>,
|
|
316
|
+
).then(
|
|
317
|
+
() => true,
|
|
318
|
+
() => false,
|
|
319
|
+
)
|
|
292
320
|
if (subscriptions.get(id) !== pending) return
|
|
293
321
|
}
|
|
294
322
|
follow(this, id, followed, args, frame.version, admitted)
|
|
@@ -299,6 +327,39 @@ export class MuxConnection {
|
|
|
299
327
|
}
|
|
300
328
|
}
|
|
301
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Whether the rungs over `mounted`'s route let a `GET` of it with `args` through, asked over the
|
|
332
|
+
* upgrade's own request at the handler's address. 18.26.
|
|
333
|
+
*/
|
|
334
|
+
async function routeAdmits(
|
|
335
|
+
scope: RequestScope,
|
|
336
|
+
mounted: Mounted,
|
|
337
|
+
args: unknown,
|
|
338
|
+
): Promise<boolean> {
|
|
339
|
+
const upgrade = scope.request as Request
|
|
340
|
+
const asked = rpcRequest(
|
|
341
|
+
'GET',
|
|
342
|
+
mounted.address,
|
|
343
|
+
args as Record<string, unknown> | undefined,
|
|
344
|
+
)
|
|
345
|
+
const request = new Request(new URL(asked.url, upgrade.url), {
|
|
346
|
+
headers: upgrade.headers,
|
|
347
|
+
signal: upgrade.signal,
|
|
348
|
+
})
|
|
349
|
+
// REACHED BY A FLAG, not by the answer's identity: a rung that wraps or re-heads what `next`
|
|
350
|
+
// answered still let the call through, and a shared answer would carry every rung's headers.
|
|
351
|
+
let reached = false
|
|
352
|
+
try {
|
|
353
|
+
const answered = await compose(mounted.rungs, async () => {
|
|
354
|
+
reached = true
|
|
355
|
+
return new Response(null, { status: 204 })
|
|
356
|
+
})({ request })
|
|
357
|
+
return reached && answered.ok
|
|
358
|
+
} catch {
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
302
363
|
/**
|
|
303
364
|
* Follows a handler for `id`: its triggers always, its value where `global` and unguarded.
|
|
304
365
|
*
|
|
@@ -331,26 +392,42 @@ function followWith(
|
|
|
331
392
|
admitted: boolean,
|
|
332
393
|
): () => void {
|
|
333
394
|
const rpc = mounted.handler
|
|
395
|
+
const key = GLOBAL_GETS.get(rpc)?.key(args) ?? ''
|
|
334
396
|
const follower: Follower = {
|
|
335
397
|
peer,
|
|
336
398
|
id,
|
|
337
399
|
args,
|
|
400
|
+
key,
|
|
338
401
|
serving: undefined,
|
|
339
402
|
stopFeed: undefined,
|
|
340
403
|
sent: held,
|
|
341
|
-
tags: mounted.copy?.tags,
|
|
342
404
|
admitted,
|
|
343
405
|
}
|
|
344
406
|
attach(rpc, follower)
|
|
345
|
-
let
|
|
407
|
+
let followed = FOLLOWERS.get(rpc)
|
|
408
|
+
if (followed === undefined) {
|
|
409
|
+
followed = {
|
|
410
|
+
tags: mounted.copy?.tags,
|
|
411
|
+
all: new Set(),
|
|
412
|
+
byKey: new Map(),
|
|
413
|
+
}
|
|
414
|
+
FOLLOWERS.set(rpc, followed)
|
|
415
|
+
}
|
|
416
|
+
const byKey = followed.byKey
|
|
417
|
+
let followers = byKey.get(key)
|
|
346
418
|
if (followers === undefined) {
|
|
347
419
|
followers = new Set()
|
|
348
|
-
|
|
420
|
+
byKey.set(key, followers)
|
|
349
421
|
}
|
|
350
422
|
followers.add(follower)
|
|
423
|
+
const all = followed.all
|
|
424
|
+
all.add(follower)
|
|
351
425
|
return () => {
|
|
352
426
|
follower.stopFeed?.()
|
|
353
|
-
|
|
427
|
+
all.delete(follower)
|
|
428
|
+
// A key nobody follows leaves, or the index grows with every key ever followed.
|
|
429
|
+
if (followers.delete(follower) && followers.size === 0)
|
|
430
|
+
byKey.delete(key)
|
|
354
431
|
}
|
|
355
432
|
}
|
|
356
433
|
|
|
@@ -410,19 +487,35 @@ export function followAsEvents(
|
|
|
410
487
|
})
|
|
411
488
|
}
|
|
412
489
|
|
|
413
|
-
/**
|
|
490
|
+
/**
|
|
491
|
+
* One mux frame as the SSE event it is: `event`, `id` where it names a version, and `data`.
|
|
492
|
+
*
|
|
493
|
+
* 18.6 — READ OFF THE FRAME'S HEAD, never parsed: the body was serialized once for every follower
|
|
494
|
+
* (`sendAll`), and a parse and a stringify per event stream would pay it again per follower. Every
|
|
495
|
+
* frame this module writes is `{"op":…,"id":…`, numbers named `base` or `version`, and at most one
|
|
496
|
+
* body last — so the body is the rest of the text, as it was written.
|
|
497
|
+
*/
|
|
414
498
|
function eventOf(text: string): string {
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
499
|
+
const opEnd = text.indexOf('"', 7)
|
|
500
|
+
const op = text.slice(7, opEnd)
|
|
501
|
+
let id = ''
|
|
502
|
+
let data =
|
|
503
|
+
op === 'value' || op === 'error' ? 'null' : op === 'patch' ? '[]' : ''
|
|
504
|
+
// Past `,"id":` — a number, so the next comma is the next member's.
|
|
505
|
+
let at = text.indexOf(',', opEnd + 6)
|
|
506
|
+
while (at !== -1) {
|
|
507
|
+
const nameEnd = text.indexOf('"', at + 2)
|
|
508
|
+
const name = text.slice(at + 2, nameEnd)
|
|
509
|
+
const from = nameEnd + 2
|
|
510
|
+
if (name !== 'base' && name !== 'version') {
|
|
511
|
+
data = text.slice(from, -1)
|
|
512
|
+
break
|
|
513
|
+
}
|
|
514
|
+
at = text.indexOf(',', from)
|
|
515
|
+
if (name === 'version')
|
|
516
|
+
id = `id: ${text.slice(from, at === -1 ? -1 : at)}\n`
|
|
517
|
+
}
|
|
518
|
+
return `event: ${op}\n${id}data: ${data}\n\n`
|
|
426
519
|
}
|
|
427
520
|
|
|
428
521
|
// 18.6 — ONE WATCH PER VALUE SERVED, however many follow it, and each frame's body serialized once
|
|
@@ -516,8 +609,8 @@ function sendAll(serving: unknown, fed: Iterable<Follower>): void {
|
|
|
516
609
|
}
|
|
517
610
|
|
|
518
611
|
function addressIn(named: string): string {
|
|
519
|
-
const
|
|
520
|
-
return
|
|
612
|
+
const path = URL.parse(named)?.pathname ?? named
|
|
613
|
+
return unmounted(path) ?? path
|
|
521
614
|
}
|
|
522
615
|
|
|
523
616
|
/**
|