@abide/abide 0.105.0 → 0.106.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 +24 -0
- package/bin/abide.ts +4 -1
- package/docs/REGISTRY.md +156 -44
- package/docs/RULEBOOK.md +375 -87
- package/docs/abide.md +7243 -10256
- package/package.json +4 -1
- package/scaffold/AGENTS.md +2 -0
- package/scaffold/src/getGreeting.ts +6 -0
- package/scaffold/src/index.abide +3 -1
- package/scaffold/src/tests/getGreeting.test.ts +6 -0
- package/scaffold/src/tests/index.test.ts +18 -0
- package/src/compiler/DOCS_SITE.ts +2 -0
- package/src/compiler/Lowering.ts +234 -14
- package/src/compiler/PositionTable.ts +14 -7
- package/src/compiler/PositionedRefusal.ts +13 -0
- package/src/compiler/Refusals.ts +18 -11
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowMap.ts +37 -0
- package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
- package/src/compiler/TypeSession.ts +410 -31
- package/src/compiler/appLayout.ts +18 -2
- package/src/compiler/argsSchemaOf.ts +55 -9
- package/src/compiler/checkApp.ts +33 -0
- package/src/compiler/checkedShadow.ts +36 -22
- package/src/compiler/citedClauses.ts +14 -0
- package/src/compiler/classifyRead.ts +6 -0
- package/src/compiler/classifyTemplate.ts +2 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +27 -6
- package/src/compiler/emitClient.ts +304 -103
- package/src/compiler/handlersUnder.ts +4 -0
- package/src/compiler/invocationProps.ts +1 -1
- package/src/compiler/isIgnored.ts +27 -0
- package/src/compiler/parseAbide.ts +176 -76
- package/src/compiler/projectShadow.ts +30 -6
- package/src/compiler/readSources.ts +1 -1
- package/src/compiler/readsIn.ts +100 -1
- package/src/compiler/renderMarkup.ts +36 -5
- package/src/compiler/scaffold.ts +19 -6
- package/src/compiler/serveLanguage.ts +156 -21
- package/src/compiler/templateContext.ts +150 -0
- package/src/compiler/tokenize.ts +6 -0
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/Inbox.ts +8 -0
- package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
- package/src/server/MOUNTABLE.ts +6 -0
- package/src/server/abideModules.ts +8 -1
- package/src/server/answerRequest.ts +3 -1
- package/src/server/answerRpc.ts +26 -12
- package/src/server/answerSocket.ts +26 -1
- package/src/server/argsFor.ts +4 -3
- package/src/server/assistant.ts +19 -13
- package/src/server/bridgeRelay.ts +108 -20
- package/src/server/claudeApi.ts +7 -7
- package/src/server/claudeCode.ts +11 -11
- package/src/server/commands.ts +73 -9
- package/src/server/createApp.ts +41 -7
- package/src/server/devWorker.ts +3 -3
- package/src/server/generatedSurfaces.ts +37 -4
- package/src/server/muxUpgrade.ts +47 -0
- package/src/server/preload.ts +13 -2
- package/src/server/render.ts +4 -1
- package/src/server/runCompiled.ts +1 -0
- package/src/server/seedBuffer.ts +31 -1
- package/src/server/serveDev.ts +83 -72
- package/src/server/surfaceOf.ts +3 -0
- package/src/server/testMount.ts +53 -0
- package/src/shared/ADDRESSES.ts +1 -1
- package/src/shared/ERROR_META.ts +3 -0
- package/src/shared/Schema.ts +1 -1
- package/src/shared/WORK.ts +14 -12
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/boundValue.ts +81 -0
- package/src/shared/index.ts +7 -2
- package/src/shared/jsonSchema.ts +25 -2
- package/src/shared/log.ts +11 -1
- package/src/shared/reactive/Keyed.ts +30 -6
- package/src/shared/reactive/PATCHES.ts +1 -1
- package/src/shared/reactive/Reactive.ts +118 -60
- package/src/shared/reactive/canonicalKey.ts +1 -1
- package/src/shared/reactive/index.ts +13 -5
- package/src/shared/reactive/recordWrites.ts +1 -1
- package/src/shared/reactive/watch.ts +26 -4
- package/src/shared/rpc.ts +6 -1
- package/src/shared/url.ts +7 -3
- package/src/shared/validateJson.ts +1 -1
- package/src/ui/ArmRegion.ts +2 -2
- package/src/ui/List.ts +9 -9
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +194 -36
- package/src/ui/Scope.ts +29 -0
- package/src/ui/adoptWalk.ts +3 -3
- package/src/ui/applyHead.ts +25 -3
- package/src/ui/bindProperty.ts +70 -22
- package/src/ui/index.ts +1 -1
- package/src/ui/leave.ts +5 -0
- package/src/ui/listenForRoutes.ts +14 -3
- package/src/ui/reconcile.ts +48 -28
- package/src/ui/regions.ts +36 -4
- package/src/ui/rows.ts +108 -28
package/src/server/commands.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { shell } from './shell.ts'
|
|
|
21
21
|
import { withoutTrailingSlash } from './withoutTrailingSlash.ts'
|
|
22
22
|
|
|
23
23
|
/** What a command is handed: the words after the flags, the command's own first, and the flags. */
|
|
24
|
-
export type Given = ReturnType<typeof
|
|
24
|
+
export type Given = ReturnType<typeof parseFlags>
|
|
25
25
|
|
|
26
26
|
type Command = (given: Given) => Promise<number | undefined>
|
|
27
27
|
|
|
@@ -42,6 +42,8 @@ type SourceCommands = Record<
|
|
|
42
42
|
export type Host = {
|
|
43
43
|
/** What usage and every message speak as: `abide`, or the name the executable was run by. */
|
|
44
44
|
name: string
|
|
45
|
+
/** What `--version` prints: abide's own, or the compiled app's. 38.115. */
|
|
46
|
+
version: string
|
|
45
47
|
/**
|
|
46
48
|
* The app the invocation means, found once off its flags (38.25): every command that serves,
|
|
47
49
|
* boots, lists or announces it derives from this one handle.
|
|
@@ -60,26 +62,70 @@ export async function runCommands(
|
|
|
60
62
|
host: Host,
|
|
61
63
|
argv: string[],
|
|
62
64
|
): Promise<number | undefined> {
|
|
65
|
+
const commands = commandsOf(host)
|
|
63
66
|
// 38.2 — EVERYTHING AFTER THE FILE IS THE SCRIPT'S, flags included, so `run` is taken before the
|
|
64
67
|
// parser sees an argument it does not know.
|
|
65
|
-
const given
|
|
68
|
+
const given =
|
|
66
69
|
argv[0] === 'run' ? { positionals: argv, values: {} } : parsed(argv)
|
|
70
|
+
// 38.113 — a flag the parser refuses is one line and the usage, never the parser's stack.
|
|
71
|
+
if (typeof given === 'string') {
|
|
72
|
+
console.error(`${host.name}: ${given}\n\n${usage(host, commands)}`)
|
|
73
|
+
return 1
|
|
74
|
+
}
|
|
67
75
|
const command = given.positionals[0]
|
|
68
|
-
const commands = commandsOf(host)
|
|
69
76
|
if (command === 'help' || given.values.help) {
|
|
70
77
|
console.log(usage(host, commands))
|
|
71
78
|
return 0
|
|
72
79
|
}
|
|
80
|
+
if (given.values.version) {
|
|
81
|
+
console.log(host.version)
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
73
84
|
if (command === undefined)
|
|
74
85
|
return await shellOn(host, host.app(given), given)
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
let nearest: string | undefined
|
|
87
|
+
let nearestDistance = NEAREST_COMMAND_DISTANCE + 1
|
|
88
|
+
for (const [name, , body] of commands) {
|
|
89
|
+
const word = name.split(' ')[0] as string
|
|
90
|
+
if (word === command) return await body(given)
|
|
91
|
+
const distance = editDistance(word, command)
|
|
92
|
+
if (distance < nearestDistance) {
|
|
93
|
+
nearest = word
|
|
94
|
+
nearestDistance = distance
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 38.114 — a near miss is named, and the usage is left for a word nothing is near.
|
|
77
98
|
console.error(
|
|
78
|
-
|
|
99
|
+
nearest === undefined
|
|
100
|
+
? `${host.name}: unknown command '${command}'\n\n${usage(host, commands)}`
|
|
101
|
+
: `${host.name}: unknown command '${command}' — did you mean \`${host.name} ${nearest}\`?`,
|
|
79
102
|
)
|
|
80
103
|
return 1
|
|
81
104
|
}
|
|
82
105
|
|
|
106
|
+
// Two edits covers a dropped, doubled or swapped letter; three reaches unrelated words.
|
|
107
|
+
const NEAREST_COMMAND_DISTANCE = 2
|
|
108
|
+
|
|
109
|
+
/** The Levenshtein distance between two words: the fewest insertions, deletions and substitutions. */
|
|
110
|
+
function editDistance(from: string, to: string): number {
|
|
111
|
+
let previous: number[] = []
|
|
112
|
+
for (let column = 0; column <= to.length; column++) previous.push(column)
|
|
113
|
+
for (let row = 1; row <= from.length; row++) {
|
|
114
|
+
const current = [row]
|
|
115
|
+
for (let column = 1; column <= to.length; column++)
|
|
116
|
+
current.push(
|
|
117
|
+
Math.min(
|
|
118
|
+
(previous[column] as number) + 1,
|
|
119
|
+
(current[column - 1] as number) + 1,
|
|
120
|
+
(previous[column - 1] as number) +
|
|
121
|
+
(from[row - 1] === to[column - 1] ? 0 : 1),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
previous = current
|
|
125
|
+
}
|
|
126
|
+
return previous[to.length] as number
|
|
127
|
+
}
|
|
128
|
+
|
|
83
129
|
// A command, and whether it is one of `SourceCommands` — which a host without them still answers
|
|
84
130
|
// (38.51), and does not list (38.52).
|
|
85
131
|
type Row = [usage: string, meaning: string, run: Command, sourced: boolean]
|
|
@@ -190,8 +236,10 @@ function usage(host: Host, commands: Row[]): string {
|
|
|
190
236
|
text += ` ${command.padEnd(width)} ${meaning}\n`
|
|
191
237
|
if (!sourced)
|
|
192
238
|
return `${text}
|
|
193
|
-
--port
|
|
194
|
-
--url
|
|
239
|
+
--port the listen port the app is served on
|
|
240
|
+
--url the running app the shell, call, watch, openapi and mcp talk to
|
|
241
|
+
--method the method call sends, where the handler answers more than one
|
|
242
|
+
--version the version of this executable's app
|
|
195
243
|
`
|
|
196
244
|
text += `
|
|
197
245
|
--root the app's source directory, where its pages sit —
|
|
@@ -202,11 +250,26 @@ function usage(host: Host, commands: Row[]): string {
|
|
|
202
250
|
--url the running app the shell, call, watch, openapi and mcp talk to
|
|
203
251
|
--target the platform compile builds for — linux-x64, darwin-arm64, windows-x64…
|
|
204
252
|
--platforms several of them, comma-separated, one executable each
|
|
253
|
+
--method the method call sends, where the handler answers more than one
|
|
254
|
+
--version the version of abide
|
|
205
255
|
`
|
|
206
256
|
return text
|
|
207
257
|
}
|
|
208
258
|
|
|
259
|
+
/** The words and flags `argv` holds, or the parser's refusal of a flag, as one line. */
|
|
209
260
|
function parsed(argv: string[]) {
|
|
261
|
+
try {
|
|
262
|
+
return parseFlags(argv)
|
|
263
|
+
} catch (thrown) {
|
|
264
|
+
const { code, message } = thrown as { code?: string; message: string }
|
|
265
|
+
if (!code?.startsWith('ERR_PARSE_ARGS_')) throw thrown
|
|
266
|
+
// Bun's message goes on to advise `--`, which no command here reads.
|
|
267
|
+
const refusal = message.split('. ')[0] as string
|
|
268
|
+
return `${refusal[0]?.toLowerCase()}${refusal.slice(1)}`
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseFlags(argv: string[]) {
|
|
210
273
|
return parseArgs({
|
|
211
274
|
args: argv,
|
|
212
275
|
options: {
|
|
@@ -225,6 +288,7 @@ function parsed(argv: string[]) {
|
|
|
225
288
|
platforms: { type: 'string' },
|
|
226
289
|
json: { type: 'boolean' },
|
|
227
290
|
help: { type: 'boolean', short: 'h' },
|
|
291
|
+
version: { type: 'boolean', short: 'v' },
|
|
228
292
|
},
|
|
229
293
|
allowPositionals: true,
|
|
230
294
|
})
|
|
@@ -361,7 +425,6 @@ async function run(
|
|
|
361
425
|
return undefined
|
|
362
426
|
}
|
|
363
427
|
|
|
364
|
-
/** `call`: one call to one handler on a running app. 38.8. */
|
|
365
428
|
/** `watch <address> [args]`: a `GET`'s value and each patch, as its event stream sends them (38.69). */
|
|
366
429
|
async function watch(host: Host, given: Given): Promise<number> {
|
|
367
430
|
const {
|
|
@@ -380,6 +443,7 @@ async function watch(host: Host, given: Given): Promise<number> {
|
|
|
380
443
|
})
|
|
381
444
|
}
|
|
382
445
|
|
|
446
|
+
/** `call`: one call to one handler on a running app. 38.8. */
|
|
383
447
|
async function call(host: Host, app: AppHandle, given: Given): Promise<number> {
|
|
384
448
|
const {
|
|
385
449
|
positionals: [, address, ...args],
|
package/src/server/createApp.ts
CHANGED
|
@@ -17,11 +17,17 @@ import { Mux, type MuxConnection } from './answerSocket.ts'
|
|
|
17
17
|
import { appDirectories } from './appDirectories.ts'
|
|
18
18
|
import { holdsApp } from './appTree.ts'
|
|
19
19
|
import { buildDirectory } from './buildDirectory.ts'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
APP_PACKAGE,
|
|
22
|
+
type AppPackage,
|
|
23
|
+
config,
|
|
24
|
+
packageUnder,
|
|
25
|
+
positiveSetting,
|
|
26
|
+
} from './config.ts'
|
|
21
27
|
import { McpSurface } from './generatedSurfaces.ts'
|
|
22
28
|
import { HOOKS, inOrder, reportUnexpected, rootRungs } from './hooks.ts'
|
|
23
29
|
import { installServer } from './installServer.ts'
|
|
24
|
-
import { admitsMux, upgraded } from './muxUpgrade.ts'
|
|
30
|
+
import { admitsMux, muxSocketOptions, upgraded } from './muxUpgrade.ts'
|
|
25
31
|
import { plainAnswer } from './plainAnswer.ts'
|
|
26
32
|
import { realOrAsGiven } from './realOrAsGiven.ts'
|
|
27
33
|
import {
|
|
@@ -39,6 +45,11 @@ import { traceResponse } from './trace.ts'
|
|
|
39
45
|
// 28.8 — one line per request.
|
|
40
46
|
const REQUEST_LOG = namedLogger('abide:request')
|
|
41
47
|
|
|
48
|
+
// 42.18 — HALF OF `docker stop`'s TEN-SECOND GRACE, the shortest a common supervisor gives before its
|
|
49
|
+
// `SIGKILL`: the other half is `onStop`'s, so the teardown runs before the kill rather than being cut
|
|
50
|
+
// by it.
|
|
51
|
+
const STOP_TIMEOUT = 5000
|
|
52
|
+
|
|
42
53
|
/** The app object: what a host, a test or a script holds the app as. */
|
|
43
54
|
export interface App {
|
|
44
55
|
fetch(request: Request): Promise<Response>
|
|
@@ -360,11 +371,33 @@ export async function bootEntered(
|
|
|
360
371
|
// stream ended, which a listener's stop otherwise waits on for as long as it streams.
|
|
361
372
|
mux?.closeAll(1001, 'the app is stopping')
|
|
362
373
|
endHeld()
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
374
|
+
const drain = (async () => {
|
|
375
|
+
await bound?.stop()
|
|
376
|
+
if (inFlight > 0)
|
|
377
|
+
await new Promise<void>((done) => {
|
|
378
|
+
drained = done
|
|
379
|
+
})
|
|
380
|
+
return true
|
|
381
|
+
})()
|
|
382
|
+
// 42.17, 42.18 — THE DRAIN HAS A DEADLINE: past it, every connection still answering is
|
|
383
|
+
// closed and `onStop` runs without waiting on the rest. A request `App.fetch` was handed with
|
|
384
|
+
// no socket bound has no connection to close, and is only no longer waited on.
|
|
385
|
+
let deadline: ReturnType<typeof setTimeout> | undefined
|
|
386
|
+
const finished = await Promise.race([
|
|
387
|
+
drain,
|
|
388
|
+
new Promise<false>((done) => {
|
|
389
|
+
deadline = setTimeout(
|
|
390
|
+
() => done(false),
|
|
391
|
+
// setTimeout reads anything past a signed 32-bit count as 1 ms.
|
|
392
|
+
Math.min(
|
|
393
|
+
positiveSetting('ABIDE_STOP_TIMEOUT', STOP_TIMEOUT),
|
|
394
|
+
2 ** 31 - 1,
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
}),
|
|
398
|
+
])
|
|
399
|
+
clearTimeout(deadline)
|
|
400
|
+
if (!finished) await bound?.stop(true)
|
|
368
401
|
// What the requests drained here left behind them.
|
|
369
402
|
endHeld()
|
|
370
403
|
if (LISTENING.server === bound) LISTENING.server = undefined
|
|
@@ -409,6 +442,7 @@ export async function bootEntered(
|
|
|
409
442
|
: answered
|
|
410
443
|
},
|
|
411
444
|
websocket: {
|
|
445
|
+
...muxSocketOptions(),
|
|
412
446
|
open(socket) {
|
|
413
447
|
socket.data.connection = opened.connect(
|
|
414
448
|
socket.data.request,
|
package/src/server/devWorker.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
import { type BootedApp, bootApp, originUnder } from './createApp.ts'
|
|
38
38
|
import { keepServing } from './faultPolicy.ts'
|
|
39
39
|
import { type Manifest, NO_MANIFEST } from './manifestOf.ts'
|
|
40
|
-
import { admitsMux } from './muxUpgrade.ts'
|
|
40
|
+
import { admitsMux, muxSocketOptions } from './muxUpgrade.ts'
|
|
41
41
|
import { LISTENING } from './requestScope.ts'
|
|
42
42
|
import {
|
|
43
43
|
describe,
|
|
@@ -82,7 +82,7 @@ export type ToWorker =
|
|
|
82
82
|
* Out: ready, a frame the mux sends a browser, and what answers a request (`FromRelay`).
|
|
83
83
|
*/
|
|
84
84
|
export type FromWorker =
|
|
85
|
-
| { kind: 'ready' }
|
|
85
|
+
| { kind: 'ready'; muxOptions: ReturnType<typeof muxSocketOptions> }
|
|
86
86
|
| { kind: 'socket-send'; connection: number; text: string }
|
|
87
87
|
| FromRelay
|
|
88
88
|
|
|
@@ -151,7 +151,7 @@ async function receive(message: ToWorker): Promise<void> {
|
|
|
151
151
|
if (message.kind === 'start') {
|
|
152
152
|
opening = open(message)
|
|
153
153
|
await opening
|
|
154
|
-
toListener({ kind: 'ready' })
|
|
154
|
+
toListener({ kind: 'ready', muxOptions: muxSocketOptions() })
|
|
155
155
|
return
|
|
156
156
|
}
|
|
157
157
|
if (
|
|
@@ -19,7 +19,7 @@ import { type Reactive, reactive } from '#shared/reactive/index.ts'
|
|
|
19
19
|
import { parseRoutePattern } from '#shared/routePattern.ts'
|
|
20
20
|
import { argsInURL, rpcRequest } from '#shared/rpcRequest.ts'
|
|
21
21
|
import { answered } from './answerFailure.ts'
|
|
22
|
-
import type
|
|
22
|
+
import { bodyWithin, type Mounted } from './answerRpc.ts'
|
|
23
23
|
import type { ToolDefinition } from './assistant.ts'
|
|
24
24
|
import { appIdentity } from './config.ts'
|
|
25
25
|
import { crossingOrigin, crossingRefusal } from './crossingOrigin.ts'
|
|
@@ -97,7 +97,13 @@ function openApiOf(handlers: Map<string, Mounted>): {
|
|
|
97
97
|
responses: {
|
|
98
98
|
'200': {
|
|
99
99
|
description: 'The value.',
|
|
100
|
-
|
|
100
|
+
// 20.15 — what the handler answers, where its type says (19.21).
|
|
101
|
+
content: {
|
|
102
|
+
'application/json':
|
|
103
|
+
each.result === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { schema: each.result },
|
|
106
|
+
},
|
|
101
107
|
},
|
|
102
108
|
'204': { description: 'Nothing to say.' },
|
|
103
109
|
default: {
|
|
@@ -214,7 +220,6 @@ export function openApi(
|
|
|
214
220
|
|
|
215
221
|
const published = new WeakMap<Map<string, Mounted>, Reactive<Published[]>>()
|
|
216
222
|
|
|
217
|
-
|
|
218
223
|
type JsonRpc = {
|
|
219
224
|
jsonrpc: '2.0'
|
|
220
225
|
id?: number | string | null
|
|
@@ -276,6 +281,17 @@ export class McpSurface {
|
|
|
276
281
|
name: tool.name,
|
|
277
282
|
description: tool.description ?? '',
|
|
278
283
|
inputSchema: tool.schema ?? { type: 'object' },
|
|
284
|
+
// 20.14 — what the method promises: a `GET` is safe, and a `GET`, a `PUT` and a
|
|
285
|
+
// `DELETE` idempotent. Any other write may destroy, and nothing says it does not.
|
|
286
|
+
annotations: {
|
|
287
|
+
readOnlyHint: tool.method === 'GET',
|
|
288
|
+
destructiveHint: tool.method !== 'GET',
|
|
289
|
+
idempotentHint:
|
|
290
|
+
tool.method === 'GET' ||
|
|
291
|
+
tool.method === 'PUT' ||
|
|
292
|
+
tool.method === 'DELETE',
|
|
293
|
+
},
|
|
294
|
+
outputSchema: structured(tool) ? tool.result : undefined,
|
|
279
295
|
})
|
|
280
296
|
}
|
|
281
297
|
return listing
|
|
@@ -350,6 +366,12 @@ export class McpSurface {
|
|
|
350
366
|
return {
|
|
351
367
|
result: {
|
|
352
368
|
content: [{ type: 'text', text }],
|
|
369
|
+
// 20.16 — the same value as structured content, where the tool declares
|
|
370
|
+
// its shape; a refusal is not that value.
|
|
371
|
+
structuredContent:
|
|
372
|
+
failed || !structured(tool)
|
|
373
|
+
? undefined
|
|
374
|
+
: JSON.parse(text),
|
|
353
375
|
isError: failed,
|
|
354
376
|
},
|
|
355
377
|
}
|
|
@@ -360,6 +382,14 @@ export class McpSurface {
|
|
|
360
382
|
}
|
|
361
383
|
}
|
|
362
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Whether a tool declares an `outputSchema`: MCP's structured content is an object, so only a
|
|
387
|
+
* result schema that is one. 20.16.
|
|
388
|
+
*/
|
|
389
|
+
function structured(tool: Listed): boolean {
|
|
390
|
+
return (tool.result as { type?: unknown } | undefined)?.type === 'object'
|
|
391
|
+
}
|
|
392
|
+
|
|
363
393
|
/**
|
|
364
394
|
* Answers one MCP JSON-RPC request over `surface`, or `undefined` where it publishes nothing.
|
|
365
395
|
*
|
|
@@ -377,9 +407,12 @@ export async function answerMcp(
|
|
|
377
407
|
// with the caller's own cookies, and a `text/plain` body is sent cross-origin with no preflight.
|
|
378
408
|
const crossing = crossingOrigin(request.headers.get('origin'), request.url)
|
|
379
409
|
if (crossing !== null) return answered(crossingRefusal(crossing, 'POST'))
|
|
410
|
+
// 16.78 — under the environment's body ceiling, as a handler's body is.
|
|
411
|
+
const body = await bodyWithin(request, undefined)
|
|
412
|
+
if (isFailed(body)) return answered(body)
|
|
380
413
|
let message: JsonRpc
|
|
381
414
|
try {
|
|
382
|
-
message = (await
|
|
415
|
+
message = (await body.json()) as JsonRpc
|
|
383
416
|
} catch {
|
|
384
417
|
return {
|
|
385
418
|
jsonrpc: '2.0',
|
package/src/server/muxUpgrade.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { SOCKET_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
12
12
|
import { unmounted } from '#shared/mount.ts'
|
|
13
|
+
import { positiveSetting } from './config.ts'
|
|
13
14
|
import { plainAnswer } from './plainAnswer.ts'
|
|
14
15
|
|
|
15
16
|
const MUX_ADMITTED = 204
|
|
@@ -70,3 +71,49 @@ export function upgraded<Data>(
|
|
|
70
71
|
? undefined
|
|
71
72
|
: plainAnswer('expected a websocket', 400)
|
|
72
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The limits a listener holds every connection to the mux to, `App.listen`'s and `abide dev`'s alike —
|
|
77
|
+
* read off the resolved configuration, which `abide dev`'s worker reports to its listener.
|
|
78
|
+
*
|
|
79
|
+
* 18.28 — A FRAME IN past `ABIDE_MAX_SOCKET_MESSAGE_SIZE`, a mebibyte unless set: a frame names an
|
|
80
|
+
* address and its args, which a `GET` carries in its URL. 18.29 — A CONNECTION IDLE is pinged, and
|
|
81
|
+
* closed where two minutes pass with nothing back. 18.30 — WHAT IS SENT is buffered to bun's 16 MiB,
|
|
82
|
+
* past which `send` drops it and the mux closes the peer (`SocketPeer`).
|
|
83
|
+
*/
|
|
84
|
+
export function muxSocketOptions(): {
|
|
85
|
+
maxPayloadLength: number
|
|
86
|
+
idleTimeout: number
|
|
87
|
+
sendPings: boolean
|
|
88
|
+
backpressureLimit: number
|
|
89
|
+
} {
|
|
90
|
+
return {
|
|
91
|
+
maxPayloadLength: positiveSetting(
|
|
92
|
+
'ABIDE_MAX_SOCKET_MESSAGE_SIZE',
|
|
93
|
+
1024 * 1024,
|
|
94
|
+
),
|
|
95
|
+
idleTimeout: 120,
|
|
96
|
+
sendPings: true,
|
|
97
|
+
backpressureLimit: 16 * 1024 * 1024,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Sends `text` to `socket`, and closes it where bun dropped the frame rather than sending or buffering it.
|
|
103
|
+
*
|
|
104
|
+
* 18.30 — A FOLLOWER THAT MISSED A FRAME stands at a version the next one does not build on, so the
|
|
105
|
+
* peer is closed rather than written past: the browser reconnects naming the version it holds (18.9)
|
|
106
|
+
* and is sent what stands. No buffer of our own — what bun buffers is the bound.
|
|
107
|
+
*/
|
|
108
|
+
export function sendOrClose(
|
|
109
|
+
socket:
|
|
110
|
+
| {
|
|
111
|
+
send(text: string): unknown
|
|
112
|
+
close(code: number, reason: string): unknown
|
|
113
|
+
}
|
|
114
|
+
| undefined,
|
|
115
|
+
text: string,
|
|
116
|
+
): void {
|
|
117
|
+
if (socket?.send(text) === 0)
|
|
118
|
+
socket.close(1013, 'the connection fell behind')
|
|
119
|
+
}
|
package/src/server/preload.ts
CHANGED
|
@@ -8,6 +8,17 @@
|
|
|
8
8
|
import { appRoot, servedBuild } from './createApp.ts'
|
|
9
9
|
|
|
10
10
|
const root = appRoot(undefined)
|
|
11
|
-
// A run from a directory that holds no app — a workspace's root
|
|
11
|
+
// A run from a directory that holds no app preloads nothing — nor from a workspace's root, whose
|
|
12
|
+
// packages' `.abide` files are each their own app's rather than one app laid out across them.
|
|
12
13
|
// Fresh whatever `NODE_ENV` says: `bun test` sets it only where nothing else has.
|
|
13
|
-
if (root !== undefined
|
|
14
|
+
if (root !== undefined && !(await isWorkspaceRoot(root)))
|
|
15
|
+
await servedBuild(root, true)
|
|
16
|
+
|
|
17
|
+
async function isWorkspaceRoot(directory: string): Promise<boolean> {
|
|
18
|
+
const manifest = Bun.file(`${directory}/package.json`)
|
|
19
|
+
if (!(await manifest.exists())) return false
|
|
20
|
+
const json = (await manifest.json()) as { workspaces?: unknown }
|
|
21
|
+
return json.workspaces !== undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { type Mounted, mount } from './testMount.ts'
|
package/src/server/render.ts
CHANGED
|
@@ -76,7 +76,10 @@ export async function composePage(
|
|
|
76
76
|
if (at < layouts.length)
|
|
77
77
|
levelProps.children = (): string => {
|
|
78
78
|
const below = levels[at + 1] as Rendered
|
|
79
|
-
|
|
79
|
+
// THE SLOT IS TAKEN BEFORE THE HEAD IS READ: reading it builds the level, which
|
|
80
|
+
// pushes the heads deeper still, and the tree's order is what the merge keys on.
|
|
81
|
+
const slot = heads.push('') - 1
|
|
82
|
+
heads[slot] = below.head
|
|
80
83
|
return below.body
|
|
81
84
|
}
|
|
82
85
|
const level = (
|
package/src/server/seedBuffer.ts
CHANGED
|
@@ -31,7 +31,12 @@ import {
|
|
|
31
31
|
streamingOrSettled,
|
|
32
32
|
} from '#shared/reactive/index.ts'
|
|
33
33
|
import { positiveSetting } from './config.ts'
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
currentRequestScope,
|
|
36
|
+
heldByApp,
|
|
37
|
+
type PageRender,
|
|
38
|
+
releaseScope,
|
|
39
|
+
} from './requestScope.ts'
|
|
35
40
|
|
|
36
41
|
const HYDRATE_LOG = namedLogger('abide:hydrate')
|
|
37
42
|
|
|
@@ -64,6 +69,10 @@ type Entry = {
|
|
|
64
69
|
release: () => void
|
|
65
70
|
// 42.11 — unfiles the entry from the app whose render left it, which drops it when it stops.
|
|
66
71
|
unhold: () => void
|
|
72
|
+
// 41.13 — lets the render's scope go, which the entry holds while what it reached is in flight.
|
|
73
|
+
letGo: () => void
|
|
74
|
+
// Whether a call took it, which then answers from what the render reached until that settles.
|
|
75
|
+
taken: boolean
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
// INDEXED BY THE THREE THINGS 41.18 MATCHES ON — caller, handler, args key — so a call finds its entry
|
|
@@ -103,6 +112,22 @@ export function seedFor(caller: string): PageRender['seed'] {
|
|
|
103
112
|
timer: undefined,
|
|
104
113
|
release: NOTHING_HELD,
|
|
105
114
|
unhold: NOTHING_HELD,
|
|
115
|
+
letGo: NOTHING_HELD,
|
|
116
|
+
taken: false,
|
|
117
|
+
}
|
|
118
|
+
// 41.13 — THE RENDER'S SCOPE IS HELD WHILE WHAT IT REACHED IS IN FLIGHT: the page is answered
|
|
119
|
+
// before a slow value lands, and letting the scope go stops that value's run — a stream the
|
|
120
|
+
// browser's call would join then never closes. Let go once it settles, a stream at its close.
|
|
121
|
+
const scope = currentRequestScope()
|
|
122
|
+
if (scope !== undefined && isReactive(reached)) {
|
|
123
|
+
scope.holds += 1
|
|
124
|
+
let held = true
|
|
125
|
+
entry.letGo = () => {
|
|
126
|
+
if (!held) return
|
|
127
|
+
held = false
|
|
128
|
+
releaseScope(scope)
|
|
129
|
+
}
|
|
130
|
+
Promise.resolve(reached).then(entry.letGo, entry.letGo)
|
|
106
131
|
}
|
|
107
132
|
const bound = positiveSetting(
|
|
108
133
|
'ABIDE_MAX_STREAM_BUFFER_SIZE',
|
|
@@ -208,6 +233,9 @@ export function takeSeed(
|
|
|
208
233
|
}
|
|
209
234
|
const entry = entries[0]
|
|
210
235
|
if (entry === undefined) return undefined
|
|
236
|
+
entry.taken = true
|
|
237
|
+
// And no longer than the call that took it: a stream that never closes ends with its reader.
|
|
238
|
+
currentRequestScope()?.stops.push(entry.letGo)
|
|
211
239
|
drop(caller, entry)
|
|
212
240
|
// READ AS WELL AS SWEPT: a timer can fire late, and an entry past its life answers nothing.
|
|
213
241
|
if (entry.expires <= Date.now()) {
|
|
@@ -244,6 +272,8 @@ function drop(caller: string, entry: Entry): void {
|
|
|
244
272
|
clearTimeout(entry.timer)
|
|
245
273
|
entry.release()
|
|
246
274
|
entry.unhold()
|
|
275
|
+
// Nothing will answer from an entry no call took, so its run need not finish.
|
|
276
|
+
if (!entry.taken) entry.letGo()
|
|
247
277
|
const byRpc = BUFFER.get(caller)
|
|
248
278
|
const byKey = byRpc?.get(entry.rpc)
|
|
249
279
|
const entries = byKey?.get(entry.key)
|