@abide/abide 0.121.1 → 0.123.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/ABIDE.md +4 -0
- package/CHANGELOG.md +17 -0
- package/docs/REGISTRY.md +15 -15
- package/docs/RULEBOOK.md +36 -14
- package/docs/abide.md +33 -33
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +4 -2
- package/src/compiler/REACTIVE_MEMBERS.ts +2 -3
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/TypeSession.ts +1 -1
- package/src/compiler/classifyRead.ts +18 -4
- package/src/compiler/projectShadow.ts +1 -0
- package/src/compiler/reactiveTypes.ts +2 -2
- package/src/compiler/readsIn.ts +144 -49
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/answerFailure.ts +16 -0
- package/src/server/answerRequest.ts +16 -21
- package/src/server/answerRpc.ts +4 -3
- package/src/server/assistant.ts +25 -4
- package/src/server/createApp.ts +5 -1
- package/src/server/csp.ts +2 -2
- package/src/server/hooks.ts +39 -8
- package/src/shared/Middleware.ts +24 -4
- package/src/shared/csp.ts +2 -2
- package/src/shared/index.ts +2 -1
- package/src/shared/propCell.ts +18 -0
- package/src/shared/rpc.ts +2 -2
- package/src/shared/writePath.ts +1 -1
package/src/compiler/readsIn.ts
CHANGED
|
@@ -246,27 +246,22 @@ export async function readsIn(
|
|
|
246
246
|
)
|
|
247
247
|
}),
|
|
248
248
|
)
|
|
249
|
+
const owned = await ownMembers(checker, nodes.flat(), (node) =>
|
|
250
|
+
valueTypeOf(checker, node),
|
|
251
|
+
)
|
|
249
252
|
const holding: Promise<void>[] = []
|
|
250
253
|
const located: { node: Node; read: Read }[] = []
|
|
251
254
|
for (let index = 0; index < nodes.length; index += 1)
|
|
252
255
|
for (const node of nodes[index] as (Node | undefined)[]) {
|
|
253
256
|
if (node === undefined) continue
|
|
254
|
-
const read = classifyRead(node)
|
|
257
|
+
const read = classifyRead(node, owned.has(node))
|
|
255
258
|
if (read === undefined) continue
|
|
256
259
|
read.byValue = byValue[index] as boolean
|
|
257
260
|
read.local = index >= topLevel
|
|
258
261
|
reads.push(read)
|
|
259
262
|
located.push({ node, read })
|
|
260
263
|
if (read.shape === 'value')
|
|
261
|
-
holding.push(
|
|
262
|
-
declaredType(checker, node).then(async (type) => {
|
|
263
|
-
if (
|
|
264
|
-
type !== undefined &&
|
|
265
|
-
(await isReactiveType(checker, abide, type, true))
|
|
266
|
-
)
|
|
267
|
-
read.shape = 'asWritten'
|
|
268
|
-
}),
|
|
269
|
-
)
|
|
264
|
+
holding.push(holdIn(checker, abide, node, read))
|
|
270
265
|
}
|
|
271
266
|
await Promise.all(holding)
|
|
272
267
|
await props
|
|
@@ -392,7 +387,7 @@ async function bindingsOfReads(
|
|
|
392
387
|
* The tracking and type warnings a file's reactive reads earn.
|
|
393
388
|
*
|
|
394
389
|
* The syntax alone decides 14.14 to 14.16 (`trackingWarnings`);
|
|
395
|
-
* 14.17
|
|
390
|
+
* 14.17 asks what a read hands back, which is the return of the `Reactive`'s call.
|
|
396
391
|
*/
|
|
397
392
|
async function warningsFor(
|
|
398
393
|
checker: Checker,
|
|
@@ -424,45 +419,16 @@ async function warningsFor(
|
|
|
424
419
|
isBinaryExpression(parent) &&
|
|
425
420
|
parent.left === node &&
|
|
426
421
|
parent.operatorToken.kind === SyntaxKind.QuestionQuestionToken
|
|
427
|
-
|
|
428
|
-
isPropertyAccessExpression(parent) &&
|
|
429
|
-
parent.expression === node &&
|
|
430
|
-
isIdentifier(parent.name) &&
|
|
431
|
-
REACTIVE_MEMBERS.has(parent.name.text)
|
|
432
|
-
? parent.name.text
|
|
433
|
-
: undefined
|
|
434
|
-
if (!nullish && member === undefined) continue
|
|
422
|
+
if (!nullish) continue
|
|
435
423
|
typed.push(
|
|
436
424
|
valueTypeOf(checker, node).then(async (value) => {
|
|
437
|
-
if (value === undefined) return
|
|
438
425
|
// 14.17 — the fallback of a read that is never nullish is never taken: an unlanded
|
|
439
426
|
// read is a hole the position fills, not an `undefined` (2.2).
|
|
440
|
-
if (
|
|
427
|
+
if (value !== undefined && !(await admitsNullish(value)))
|
|
441
428
|
warnings.push({
|
|
442
429
|
at: read.start,
|
|
443
430
|
message: `abide: \`${name} ?? …\` never takes its fallback — a read of \`${name}\` is not undefined while it loads, and its type says it is not undefined after. Ask \`${name}.ready()\` to tell the two apart. RULEBOOK 14.17.`,
|
|
444
431
|
})
|
|
445
|
-
if (member === undefined) return
|
|
446
|
-
// Per member, because a union's properties are only those every member shares, and
|
|
447
|
-
// `undefined` shares none — so `HTMLVideoElement | undefined` would declare no `error`.
|
|
448
|
-
for (const held of await unionMembers(value)) {
|
|
449
|
-
if ((held.flags & NULLISH) !== 0) continue
|
|
450
|
-
for (const property of await checker.getPropertiesOfType(
|
|
451
|
-
held,
|
|
452
|
-
))
|
|
453
|
-
if (property.name === member) {
|
|
454
|
-
const dot = (
|
|
455
|
-
parent as { questionDotToken?: unknown }
|
|
456
|
-
).questionDotToken
|
|
457
|
-
? '?.'
|
|
458
|
-
: '.'
|
|
459
|
-
warnings.push({
|
|
460
|
-
at: read.start,
|
|
461
|
-
message: `abide: \`${name}${dot}${member}\` reaches the Reactive's own \`${member}\`, and the value it holds has one too. Write \`${name}()${dot}${member}\` for the value's. RULEBOOK 31.13.`,
|
|
462
|
-
})
|
|
463
|
-
return
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
432
|
}),
|
|
467
433
|
)
|
|
468
434
|
}
|
|
@@ -779,7 +745,7 @@ function valueLeaves(node: Node, leaves: Node[]): void {
|
|
|
779
745
|
const ROUTE_MEMBERS = new Set(['url', 'params', 'name', 'navigating'])
|
|
780
746
|
|
|
781
747
|
/**
|
|
782
|
-
* 31.
|
|
748
|
+
* 31.15 — `route`'s four values, each read by name as a name bound to a `Reactive` is.
|
|
783
749
|
*
|
|
784
750
|
* `route.params.id` reads `route.params().id`, and the explicit call keeps compiling (31.7). Told
|
|
785
751
|
* by the symbol, as `props` is, so an app's own `route` is not abide's.
|
|
@@ -1142,13 +1108,18 @@ async function propsReads(
|
|
|
1142
1108
|
),
|
|
1143
1109
|
),
|
|
1144
1110
|
)
|
|
1111
|
+
const owned = await ownMembers(checker, found.flat(), (node) =>
|
|
1112
|
+
checker.getTypeAtLocation(node),
|
|
1113
|
+
)
|
|
1114
|
+
// 33.27 — each prop asked whether its position holds the `Reactive`, all at once.
|
|
1115
|
+
const holding: Promise<void>[] = []
|
|
1145
1116
|
for (let at = 0; at < found.length; at += 1) {
|
|
1146
1117
|
const binding = bindings[at] as (typeof bindings)[number]
|
|
1147
1118
|
if (binding.rest) {
|
|
1148
1119
|
restReads(found[at] as (Node | undefined)[], reads)
|
|
1149
1120
|
continue
|
|
1150
1121
|
}
|
|
1151
|
-
const uses: Read[] = []
|
|
1122
|
+
const uses: { node: Node; read: Read }[] = []
|
|
1152
1123
|
let written = false
|
|
1153
1124
|
for (const node of found[at] as (Node | undefined)[]) {
|
|
1154
1125
|
// The binding itself is the pattern's, and not a use of the name.
|
|
@@ -1157,15 +1128,16 @@ async function propsReads(
|
|
|
1157
1128
|
node.parent?.kind === SyntaxKind.BindingElement
|
|
1158
1129
|
)
|
|
1159
1130
|
continue
|
|
1160
|
-
const read = classifyRead(node)
|
|
1131
|
+
const read = classifyRead(node, owned.has(node))
|
|
1161
1132
|
if (read === undefined) continue
|
|
1162
1133
|
if (
|
|
1163
1134
|
read.shape === 'write' ||
|
|
1164
1135
|
read.shape === 'update' ||
|
|
1165
|
-
isBindOperand(positions, node)
|
|
1136
|
+
isBindOperand(positions, node) ||
|
|
1137
|
+
writesThrough(node, owned)
|
|
1166
1138
|
)
|
|
1167
1139
|
written = true
|
|
1168
|
-
uses.push(read)
|
|
1140
|
+
uses.push({ node, read })
|
|
1169
1141
|
}
|
|
1170
1142
|
if (!written) {
|
|
1171
1143
|
for (const node of found[at] as (Node | undefined)[]) {
|
|
@@ -1173,18 +1145,141 @@ async function propsReads(
|
|
|
1173
1145
|
const read = classifyPropRead(node)
|
|
1174
1146
|
if (read === undefined) continue
|
|
1175
1147
|
read.local = binding.local
|
|
1176
|
-
|
|
1148
|
+
if (read.shape !== 'value') {
|
|
1149
|
+
reads.push(read)
|
|
1150
|
+
continue
|
|
1151
|
+
}
|
|
1152
|
+
holding.push(
|
|
1153
|
+
holdsProp(checker, abide, node, owned.has(node)).then(
|
|
1154
|
+
(held) => {
|
|
1155
|
+
if (!held) return void reads.push(read)
|
|
1156
|
+
// Classified as a page's own name is, reaching a cell over the accessor.
|
|
1157
|
+
const asHeld = classifyRead(node) as Read
|
|
1158
|
+
if (asHeld.shape === 'value')
|
|
1159
|
+
asHeld.shape = 'asWritten'
|
|
1160
|
+
asHeld.held = true
|
|
1161
|
+
asHeld.local = binding.local
|
|
1162
|
+
reads.push(asHeld)
|
|
1163
|
+
},
|
|
1164
|
+
),
|
|
1165
|
+
)
|
|
1177
1166
|
}
|
|
1178
1167
|
continue
|
|
1179
1168
|
}
|
|
1180
1169
|
binding.call.path.push({ name: binding.key, start: 0, end: 0 })
|
|
1181
1170
|
for (const read of binding.defaults) read.through = true
|
|
1182
|
-
|
|
1171
|
+
// A written prop's cell is already a `Reactive`, and a position due one holds it.
|
|
1172
|
+
for (const { node, read } of uses) {
|
|
1183
1173
|
read.through = true
|
|
1184
1174
|
read.local = binding.local
|
|
1185
1175
|
reads.push(read)
|
|
1176
|
+
if (read.shape === 'value')
|
|
1177
|
+
holding.push(holdIn(checker, abide, node, read))
|
|
1186
1178
|
}
|
|
1187
1179
|
}
|
|
1180
|
+
await Promise.all(holding)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* 31.5 — the `Reactive`'s member a property access reaches by name, or `undefined` where the name
|
|
1185
|
+
* is not one — `n.ready`, `n.set`. Whether it is the `Reactive`'s is the value's type's to say
|
|
1186
|
+
* (`ownMembers`).
|
|
1187
|
+
*/
|
|
1188
|
+
function memberNamed(node: Node): string | undefined {
|
|
1189
|
+
const parent = node.parent
|
|
1190
|
+
return isPropertyAccessExpression(parent) &&
|
|
1191
|
+
parent.expression === node &&
|
|
1192
|
+
isIdentifier(parent.name) &&
|
|
1193
|
+
REACTIVE_MEMBERS.has(parent.name.text)
|
|
1194
|
+
? parent.name.text
|
|
1195
|
+
: undefined
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* 31.5 — the occurrences among `nodes` reaching a member the VALUE's own type declares, where the
|
|
1200
|
+
* access is the value's rather than the `Reactive`'s — a `Map`'s `set` is the map's. Asked before
|
|
1201
|
+
* any is classified, so each is classified once.
|
|
1202
|
+
*
|
|
1203
|
+
* Per union member, because a union's properties are only those every member shares, and
|
|
1204
|
+
* `undefined` shares none — so `HTMLVideoElement | undefined` would declare no `error`. A value
|
|
1205
|
+
* typed `any` or `unknown` declares none: its type says nothing of what it holds.
|
|
1206
|
+
*/
|
|
1207
|
+
async function ownMembers(
|
|
1208
|
+
checker: Checker,
|
|
1209
|
+
nodes: readonly (Node | undefined)[],
|
|
1210
|
+
valueOf: (node: Node) => Promise<Type | undefined>,
|
|
1211
|
+
): Promise<Set<Node>> {
|
|
1212
|
+
const owned = new Set<Node>()
|
|
1213
|
+
const asked: Promise<void>[] = []
|
|
1214
|
+
for (const node of nodes) {
|
|
1215
|
+
if (node === undefined) continue
|
|
1216
|
+
const member = memberNamed(node)
|
|
1217
|
+
if (member === undefined) continue
|
|
1218
|
+
asked.push(
|
|
1219
|
+
valueOf(node).then(async (value) => {
|
|
1220
|
+
if (value === undefined) return
|
|
1221
|
+
for (const held of await unionMembers(value))
|
|
1222
|
+
if (
|
|
1223
|
+
(held.flags & NULLISH) === 0 &&
|
|
1224
|
+
(await checker.getPropertyOfType(held, member)) !==
|
|
1225
|
+
undefined
|
|
1226
|
+
)
|
|
1227
|
+
return void owned.add(node)
|
|
1228
|
+
}),
|
|
1229
|
+
)
|
|
1230
|
+
}
|
|
1231
|
+
await Promise.all(asked)
|
|
1232
|
+
return owned
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* 31.7, 33.6, 33.7 — `n.set(v)` and `n.patch(…)` are writes by name, called or handed on, where the
|
|
1237
|
+
* value's own type declares no member of that name — a `Map`'s `set` writes the map, not the prop.
|
|
1238
|
+
*/
|
|
1239
|
+
function writesThrough(node: Node, owned: ReadonlySet<Node>): boolean {
|
|
1240
|
+
const member = memberNamed(node)
|
|
1241
|
+
return (member === 'set' || member === 'patch') && !owned.has(node)
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* 33.27 — whether a prop the child only reads stands where its `Reactive` is due, as a page's own
|
|
1246
|
+
* name would: an `await` operand (31.3), a `Reactive`'s member its value does not declare (31.5),
|
|
1247
|
+
* or a position typed as a `Reactive` or a `Selection` (31.3, 31.13).
|
|
1248
|
+
*/
|
|
1249
|
+
async function holdsProp(
|
|
1250
|
+
checker: Checker,
|
|
1251
|
+
abide: Abide,
|
|
1252
|
+
node: Node,
|
|
1253
|
+
ownsMember: boolean,
|
|
1254
|
+
): Promise<boolean> {
|
|
1255
|
+
if (node.parent.kind === SyntaxKind.AwaitExpression) return true
|
|
1256
|
+
if (memberNamed(node) !== undefined) return !ownsMember
|
|
1257
|
+
return typedReactive(checker, abide, node)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* 31.3, 31.13 — `read` holds the `Reactive` where `node` stands in a position whose declared type
|
|
1262
|
+
* is a `Reactive` or a `Selection`.
|
|
1263
|
+
*/
|
|
1264
|
+
async function holdIn(
|
|
1265
|
+
checker: Checker,
|
|
1266
|
+
abide: Abide,
|
|
1267
|
+
node: Node,
|
|
1268
|
+
read: Read,
|
|
1269
|
+
): Promise<void> {
|
|
1270
|
+
if (await typedReactive(checker, abide, node)) read.shape = 'asWritten'
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** 31.3, 31.13 — whether `node`'s position is declared a `Reactive` or a `Selection`. */
|
|
1274
|
+
async function typedReactive(
|
|
1275
|
+
checker: Checker,
|
|
1276
|
+
abide: Abide,
|
|
1277
|
+
node: Node,
|
|
1278
|
+
): Promise<boolean> {
|
|
1279
|
+
const type = await declaredType(checker, node)
|
|
1280
|
+
return (
|
|
1281
|
+
type !== undefined && (await isReactiveType(checker, abide, type, true))
|
|
1282
|
+
)
|
|
1188
1283
|
}
|
|
1189
1284
|
|
|
1190
1285
|
/**
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
|
|
2
2
|
// server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
|
|
3
|
-
export const BRIDGE_VERSION = '0.
|
|
3
|
+
export const BRIDGE_VERSION = '0.5.1'
|
|
@@ -10,6 +10,7 @@ import { coversRoute, parseRoutePattern } from '#shared/routePattern.ts'
|
|
|
10
10
|
|
|
11
11
|
import type { AppSources } from './answerRequest.ts'
|
|
12
12
|
import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
|
|
13
|
+
import { prefers } from './prefers.ts'
|
|
13
14
|
import { boundPage, renderDocument } from './render.ts'
|
|
14
15
|
import { beginRender, currentRequestScope } from './requestScope.ts'
|
|
15
16
|
|
|
@@ -100,3 +101,18 @@ export async function answerFailure(
|
|
|
100
101
|
headers.set('content-type', 'text/plain; charset=utf-8')
|
|
101
102
|
return new Response(failure.message, { status, headers })
|
|
102
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A failure at an address whose lane only the request can say: a page's, painted, where it prefers
|
|
107
|
+
* a document, and a handler's otherwise. 15.22.
|
|
108
|
+
*/
|
|
109
|
+
export function laneFailure(
|
|
110
|
+
app: ErrorPages,
|
|
111
|
+
pathname: string,
|
|
112
|
+
failure: Failed<string, unknown>,
|
|
113
|
+
request: Request,
|
|
114
|
+
): Response | Promise<Response> {
|
|
115
|
+
return prefers(request, 'text/html', 'application/json')
|
|
116
|
+
? answerFailure(app, pathname, failure)
|
|
117
|
+
: answered(failure)
|
|
118
|
+
}
|
|
@@ -22,7 +22,7 @@ import { DEBUG_META } from '#shared/DEBUG_META.ts'
|
|
|
22
22
|
import { decodedOrKept } from '#shared/decodedOrKept.ts'
|
|
23
23
|
import { ERROR_META } from '#shared/ERROR_META.ts'
|
|
24
24
|
import { escapeAttribute } from '#shared/escapeText.ts'
|
|
25
|
-
import {
|
|
25
|
+
import { isFailed } from '#shared/Failed.ts'
|
|
26
26
|
import { HandlerRoute } from '#shared/HandlerRoute.ts'
|
|
27
27
|
import { health } from '#shared/health.ts'
|
|
28
28
|
import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
import type { Method, Rpc } from '#shared/rpc.ts'
|
|
46
46
|
import { rpcRequest } from '#shared/rpcRequest.ts'
|
|
47
47
|
|
|
48
|
-
import {
|
|
48
|
+
import { answerFailure, laneFailure } from './answerFailure.ts'
|
|
49
49
|
import {
|
|
50
50
|
answerRpc,
|
|
51
51
|
type Mounted,
|
|
@@ -64,12 +64,11 @@ import { settingOn } from './config.ts'
|
|
|
64
64
|
import { noteCallInRender } from './deferredBuilds.ts'
|
|
65
65
|
import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
|
|
66
66
|
import { answerMcp, openApi } from './generatedSurfaces.ts'
|
|
67
|
-
import { type
|
|
67
|
+
import { failureOf, type LaneRung, rungsOver } from './hooks.ts'
|
|
68
68
|
import { chunksOf, type Manifest } from './manifestOf.ts'
|
|
69
69
|
import { muxAdmission } from './muxUpgrade.ts'
|
|
70
70
|
import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
|
|
71
71
|
import { plainAnswer } from './plainAnswer.ts'
|
|
72
|
-
import { prefers } from './prefers.ts'
|
|
73
72
|
import { PRINCIPAL_MOUNT } from './principal.ts'
|
|
74
73
|
import {
|
|
75
74
|
boundPage,
|
|
@@ -625,7 +624,18 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
|
625
624
|
if (row.page !== undefined)
|
|
626
625
|
row.answers.set(
|
|
627
626
|
row.page,
|
|
628
|
-
answerInside(
|
|
627
|
+
answerInside(
|
|
628
|
+
sources,
|
|
629
|
+
row.page,
|
|
630
|
+
// 37.14 — a page's lane paints a refusal as its error page does (35.2).
|
|
631
|
+
rungsOver(row.pattern, (failure, request) =>
|
|
632
|
+
answerFailure(
|
|
633
|
+
sources,
|
|
634
|
+
new URL(request.url).pathname,
|
|
635
|
+
failure,
|
|
636
|
+
),
|
|
637
|
+
),
|
|
638
|
+
),
|
|
629
639
|
)
|
|
630
640
|
for (const handler of row.handlers.values())
|
|
631
641
|
row.answers.set(
|
|
@@ -640,7 +650,7 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
|
640
650
|
function answerInside(
|
|
641
651
|
app: AppSources,
|
|
642
652
|
found: Ranked | Mounted,
|
|
643
|
-
rungs:
|
|
653
|
+
rungs: LaneRung[],
|
|
644
654
|
): (ctx: { request: Request }) => Promise<Response> {
|
|
645
655
|
return compose(rungs, ({ request }) => answerMatched(app, request, found))
|
|
646
656
|
}
|
|
@@ -774,21 +784,6 @@ async function methodRefused(
|
|
|
774
784
|
return answer
|
|
775
785
|
}
|
|
776
786
|
|
|
777
|
-
/**
|
|
778
|
-
* A failure at an address whose lane only the request can say: a page's, painted, where it prefers
|
|
779
|
-
* a document, and a handler's otherwise. 15.22.
|
|
780
|
-
*/
|
|
781
|
-
function laneFailure(
|
|
782
|
-
app: AppSources,
|
|
783
|
-
pathname: string,
|
|
784
|
-
failure: Failed<string, unknown>,
|
|
785
|
-
request: Request,
|
|
786
|
-
): Response | Promise<Response> {
|
|
787
|
-
return prefers(request, 'text/html', 'application/json')
|
|
788
|
-
? answerFailure(app, pathname, failure)
|
|
789
|
-
: answered(failure)
|
|
790
|
-
}
|
|
791
|
-
|
|
792
787
|
/**
|
|
793
788
|
* A public file's answer, conditional and range-aware off the etag read at boot.
|
|
794
789
|
*
|
package/src/server/answerRpc.ts
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
crossingOrigin,
|
|
45
45
|
crossingRefusal,
|
|
46
46
|
} from './crossingOrigin.ts'
|
|
47
|
-
import { type
|
|
47
|
+
import { failureOf, type LaneRung, rungsOver } from './hooks.ts'
|
|
48
48
|
import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
|
|
49
49
|
import { prefers } from './prefers.ts'
|
|
50
50
|
import {
|
|
@@ -120,7 +120,7 @@ export type Mounted = Mount & {
|
|
|
120
120
|
* The rungs filed over `scope`, read once at mount (16.14) — what an http call and a socket
|
|
121
121
|
* subscription to it both pass through (37.2, 18.26).
|
|
122
122
|
*/
|
|
123
|
-
rungs:
|
|
123
|
+
rungs: LaneRung[]
|
|
124
124
|
handler: Rpc<unknown, unknown> & Gated<unknown> & HeldToAnnotation
|
|
125
125
|
/**
|
|
126
126
|
* The args' JSON Schema, probed once at mount.
|
|
@@ -203,7 +203,8 @@ export async function mountHandlers(
|
|
|
203
203
|
...mount,
|
|
204
204
|
// 37.1, 16.72 — the route its file's hooks are filed at, which the build read off
|
|
205
205
|
// where the file answers (23.19's mount for a package's), never its place on disk.
|
|
206
|
-
|
|
206
|
+
// 37.14 — a handler's lane answers a refusal as Problem Details (15.1).
|
|
207
|
+
rungs: rungsOver(mount.scope, answered),
|
|
207
208
|
handler,
|
|
208
209
|
argsDocument: declared ?? (mount.args as Document | undefined),
|
|
209
210
|
})
|
package/src/server/assistant.ts
CHANGED
|
@@ -122,37 +122,58 @@ export async function* assistant(
|
|
|
122
122
|
options.mcp === true ? appSurface() : NO_SURFACE,
|
|
123
123
|
askerOf(request()),
|
|
124
124
|
)
|
|
125
|
+
const signal = request().signal
|
|
125
126
|
const events = provider.run(turns, tools, {
|
|
126
127
|
maxTurns: options.maxTurns ?? MAX_TURNS,
|
|
127
|
-
signal
|
|
128
|
+
signal,
|
|
128
129
|
})
|
|
129
130
|
// 28.7 — a closed namespace runs the provider's stream untouched.
|
|
130
|
-
yield* ASSISTANT_LOG.enabled()
|
|
131
|
+
yield* ASSISTANT_LOG.enabled()
|
|
132
|
+
? traced(events, messages.length, signal)
|
|
133
|
+
: events
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* `events`, and one line on `abide:assistant` once they end, however they end. 44.31.
|
|
138
|
+
*
|
|
139
|
+
* AN UNFINISHED CONVERSATION SAYS WHY, an abort read ahead of a throw: an aborted provider may throw
|
|
140
|
+
* as it unwinds, and the abort is what ended it.
|
|
141
|
+
*/
|
|
134
142
|
async function* traced(
|
|
135
143
|
events: AsyncGenerator<AssistantEvent>,
|
|
136
144
|
messageCount: number,
|
|
145
|
+
signal: AbortSignal,
|
|
137
146
|
): AsyncGenerator<AssistantEvent> {
|
|
138
147
|
const started = performance.now()
|
|
139
148
|
let calls = 0
|
|
140
149
|
let done: Extract<AssistantEvent, { type: 'done' }> | undefined
|
|
150
|
+
let failure: string | undefined
|
|
141
151
|
try {
|
|
142
152
|
for await (const event of events) {
|
|
143
153
|
if (event.type === 'tool') calls += 1
|
|
144
154
|
else if (event.type === 'done') done = event
|
|
145
155
|
yield event
|
|
146
156
|
}
|
|
157
|
+
} catch (thrown) {
|
|
158
|
+
failure = thrown instanceof Error ? thrown.message : String(thrown)
|
|
159
|
+
throw thrown
|
|
147
160
|
} finally {
|
|
161
|
+
const ending =
|
|
162
|
+
done?.stop ??
|
|
163
|
+
(signal.aborted
|
|
164
|
+
? 'unfinished aborted'
|
|
165
|
+
: failure === undefined
|
|
166
|
+
? 'unfinished stopped'
|
|
167
|
+
: 'unfinished failed')
|
|
148
168
|
ASSISTANT_LOG.debug(
|
|
149
|
-
|
|
169
|
+
ending,
|
|
150
170
|
`${messageCount} messages`,
|
|
151
171
|
`${calls} tool calls`,
|
|
152
172
|
done === undefined
|
|
153
173
|
? 'no usage'
|
|
154
174
|
: `${done.usage.input}/${done.usage.output} tokens`,
|
|
155
175
|
`${(performance.now() - started).toFixed(1)}ms`,
|
|
176
|
+
...(ending === 'unfinished failed' ? [failure] : []),
|
|
156
177
|
)
|
|
157
178
|
}
|
|
158
179
|
}
|
package/src/server/createApp.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { PRINCIPAL_HEADER } from '#shared/PRINCIPAL_HEADER.ts'
|
|
|
17
17
|
import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
|
|
18
18
|
|
|
19
19
|
import { servedTable } from './abideModules.ts'
|
|
20
|
+
import { laneFailure } from './answerFailure.ts'
|
|
20
21
|
import { answerRequest, type AppSources } from './answerRequest.ts'
|
|
21
22
|
import type { Mount } from './answerRpc.ts'
|
|
22
23
|
import { Mux, type MuxConnection } from './answerSocket.ts'
|
|
@@ -278,7 +279,10 @@ export async function bootEntered(
|
|
|
278
279
|
// 16.14, 37.3 — THE APP LANE'S ONION, composed once over the whole request path: the root
|
|
279
280
|
// route's rungs, the ones below it running after routing (37.2, `answerRequest`). 21.20 — the
|
|
280
281
|
// center takes its own time back out of what `fetch` counts for the whole onion.
|
|
281
|
-
|
|
282
|
+
// 37.14 — before routing, the lane is the request's to say (15.22).
|
|
283
|
+
const rungs = rootRungs((failure, request) =>
|
|
284
|
+
laneFailure(booted, new URL(request.url).pathname, failure, request),
|
|
285
|
+
)
|
|
282
286
|
const answer = compose(rungs, async ({ request }) => {
|
|
283
287
|
const began = performance.now()
|
|
284
288
|
const answered = await answerRequest(booted, request)
|
package/src/server/csp.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// THE NONCE IS MINTED BY WHICHEVER ASKS FIRST — a page reading `csp.nonce` while it renders, or this
|
|
6
6
|
// rung writing a document's header — so the two agree, and an answer that is not a document mints none.
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type { Rung } from '#shared/Middleware.ts'
|
|
9
9
|
|
|
10
10
|
import { randomToken } from './randomToken.ts'
|
|
11
11
|
import { EARLY_CALL_HASH } from './render.ts'
|
|
@@ -64,7 +64,7 @@ function policy(
|
|
|
64
64
|
|
|
65
65
|
export function cspRung(
|
|
66
66
|
sources: Record<string, string[]> | undefined,
|
|
67
|
-
):
|
|
67
|
+
): Rung<{ request: Request }, Response> {
|
|
68
68
|
const written = policy(sources)
|
|
69
69
|
return async (next) => {
|
|
70
70
|
const answer = await next()
|
package/src/server/hooks.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { type Failed, isFailed } from '#shared/Failed.ts'
|
|
15
15
|
import { namedLogger } from '#shared/log.ts'
|
|
16
|
-
import type {
|
|
16
|
+
import type { Rung } from '#shared/Middleware.ts'
|
|
17
17
|
import { aborted, refuse } from '#shared/refuse.ts'
|
|
18
18
|
import {
|
|
19
19
|
coversRoute,
|
|
@@ -26,7 +26,25 @@ import { config } from './config.ts'
|
|
|
26
26
|
import type { LauncherOptions } from './launcherOptions.ts'
|
|
27
27
|
import { currentRequestScope } from './requestScope.ts'
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
type AppContext = { request: Request }
|
|
30
|
+
|
|
31
|
+
/** A rung `middleware` files: what `next` answered passed on, or a refusal in its place (37.14). */
|
|
32
|
+
export type AppRung = (
|
|
33
|
+
next: (ctx?: AppContext) => Promise<Response>,
|
|
34
|
+
ctx: AppContext,
|
|
35
|
+
) =>
|
|
36
|
+
| Response
|
|
37
|
+
| Failed<string, unknown>
|
|
38
|
+
| Promise<Response | Failed<string, unknown>>
|
|
39
|
+
|
|
40
|
+
/** An app rung as its lane composes it, a refusal it returned already answered (37.14). */
|
|
41
|
+
export type LaneRung = Rung<AppContext, Response>
|
|
42
|
+
|
|
43
|
+
/** How a lane answers a refusal one of its rungs returned, in that lane's representation (37.14). */
|
|
44
|
+
export type RefusalAnswer = (
|
|
45
|
+
failure: Failed<string, unknown>,
|
|
46
|
+
request: Request,
|
|
47
|
+
) => Response | Promise<Response>
|
|
30
48
|
|
|
31
49
|
export type Env = Record<string, string | undefined>
|
|
32
50
|
export type ConfigDefaults = (env: Env) => unknown
|
|
@@ -106,11 +124,24 @@ export function inOrder<T>(list: readonly Filed<T>[]): T[] {
|
|
|
106
124
|
return items
|
|
107
125
|
}
|
|
108
126
|
|
|
127
|
+
/**
|
|
128
|
+
* `rung` with a refusal it returns answered by `answer`.
|
|
129
|
+
*
|
|
130
|
+
* 37.14, 37.15 — PER LINK, not at the onion's edge: a rung outside this one is handed the answer
|
|
131
|
+
* from `next`, so one that reads a `Response` (29.2's) reads one whichever rung inside it refused.
|
|
132
|
+
*/
|
|
133
|
+
function answering(rung: AppRung, answer: RefusalAnswer): LaneRung {
|
|
134
|
+
return async (next, ctx) => {
|
|
135
|
+
const result = await rung(next, ctx)
|
|
136
|
+
return isFailed(result) ? answer(result, ctx.request) : result
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
109
140
|
/** The rungs filed at the root route, which run before routing. 37.3. */
|
|
110
|
-
export function rootRungs():
|
|
111
|
-
const rungs:
|
|
141
|
+
export function rootRungs(answer: RefusalAnswer): LaneRung[] {
|
|
142
|
+
const rungs: LaneRung[] = []
|
|
112
143
|
for (const entry of HOOKS.rungs)
|
|
113
|
-
if (entry.route === ROOT) rungs.push(entry.item)
|
|
144
|
+
if (entry.route === ROOT) rungs.push(answering(entry.item, answer))
|
|
114
145
|
return rungs
|
|
115
146
|
}
|
|
116
147
|
|
|
@@ -120,12 +151,12 @@ export function rootRungs(): AppRung[] {
|
|
|
120
151
|
* 37.2 — every rung filed at `route` or at a route that is its front, the root's aside: those ran
|
|
121
152
|
* before routing (37.3).
|
|
122
153
|
*/
|
|
123
|
-
export function rungsOver(route: string):
|
|
154
|
+
export function rungsOver(route: string, answer: RefusalAnswer): LaneRung[] {
|
|
124
155
|
const segments = parseRoutePattern(route)
|
|
125
|
-
const rungs:
|
|
156
|
+
const rungs: LaneRung[] = []
|
|
126
157
|
for (const entry of HOOKS.rungs)
|
|
127
158
|
if (entry.route !== ROOT && coversRoute(entry.segments, segments))
|
|
128
|
-
rungs.push(entry.item)
|
|
159
|
+
rungs.push(answering(entry.item, answer))
|
|
129
160
|
return rungs
|
|
130
161
|
}
|
|
131
162
|
|
package/src/shared/Middleware.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
// ONE RUNG OF AN ONION, and the onion. The app lane's `middleware` (37.3) and a handler's rungs
|
|
2
2
|
// (16.11) are the same shape over a different `Ctx`, so they compose through the one function.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import type { Failed } from './Failed.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A rung written once for every lane whose context carries `Extra`: generic over the context it is
|
|
8
|
+
* handed and over what `next` answers, so it passes `next()`'s answer on, refuses in its place, or
|
|
9
|
+
* answers with an `Answer` of its own.
|
|
10
|
+
*/
|
|
11
|
+
export type Middleware<Answer = never, Extra extends object = object> = <
|
|
12
|
+
Given extends { request: Request } & Extra,
|
|
13
|
+
Result,
|
|
14
|
+
>(
|
|
15
|
+
next: (ctx?: Given) => Promise<Result>,
|
|
16
|
+
ctx: Given,
|
|
17
|
+
) =>
|
|
18
|
+
| Result
|
|
19
|
+
| Answer
|
|
20
|
+
| Failed<string, unknown>
|
|
21
|
+
| Promise<Result | Answer | Failed<string, unknown>>
|
|
22
|
+
|
|
23
|
+
/** A rung as one lane composes it, over that lane's context and what its center answers. */
|
|
24
|
+
export type Rung<Ctx, Result> = (
|
|
5
25
|
next: (ctx?: Ctx) => Promise<Result>,
|
|
6
26
|
ctx: Ctx,
|
|
7
27
|
) => Result | Promise<Result>
|
|
@@ -17,17 +37,17 @@ export type Middleware<Ctx, Result> = (
|
|
|
17
37
|
* `next()` with no argument hands the inner link the ctx this rung was given.
|
|
18
38
|
*/
|
|
19
39
|
export function compose<Ctx, Result>(
|
|
20
|
-
rungs: readonly
|
|
40
|
+
rungs: readonly Rung<Ctx, Result>[],
|
|
21
41
|
terminal: (ctx: Ctx) => Promise<Result>,
|
|
22
42
|
): (ctx: Ctx) => Promise<Result> {
|
|
23
43
|
let link = terminal
|
|
24
44
|
for (let at = rungs.length - 1; at >= 0; at -= 1)
|
|
25
|
-
link = linkOf(rungs[at] as
|
|
45
|
+
link = linkOf(rungs[at] as Rung<Ctx, Result>, link)
|
|
26
46
|
return link
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
function linkOf<Ctx, Result>(
|
|
30
|
-
rung:
|
|
50
|
+
rung: Rung<Ctx, Result>,
|
|
31
51
|
inner: (ctx: Ctx) => Promise<Result>,
|
|
32
52
|
): (ctx: Ctx) => Promise<Result> {
|
|
33
53
|
return async (ctx) => rung((given) => inner(given ?? ctx), ctx)
|
package/src/shared/csp.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// mint one for, so `csp.nonce` answers the one its document was served with, which is what a
|
|
4
4
|
// binding that wrote it on the server hydrates to.
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { Rung as LaneRung } from './Middleware.ts'
|
|
7
7
|
|
|
8
|
-
type Rung =
|
|
8
|
+
type Rung = LaneRung<{ request: Request }, Response>
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* The server's content-security-policy half, as slots `#server/installServer.ts` fills from
|