@abide/abide 0.42.0 → 0.43.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/AGENTS.md +234 -300
- package/CHANGELOG.md +34 -0
- package/README.md +50 -93
- package/package.json +3 -2
- package/src/abideResolverPlugin.ts +104 -14
- package/src/buildCli.ts +1 -1
- package/src/discoveryEntry.ts +3 -3
- package/src/lib/bundle/BundleMenuItem.ts +1 -1
- package/src/lib/cli/createClient.ts +12 -12
- package/src/lib/cli/dispatchCommand.ts +1 -1
- package/src/lib/cli/printSessionStatus.ts +1 -1
- package/src/lib/cli/resolveCliTarget.ts +1 -1
- package/src/lib/cli/runCli.ts +1 -1
- package/src/lib/cli/types/CliManifest.ts +1 -1
- package/src/lib/cli/types/CliManifestEntry.ts +2 -2
- package/src/lib/mcp/annotationsForMethod.ts +5 -5
- package/src/lib/mcp/createMcpServer.ts +3 -3
- package/src/lib/mcp/mcpSurface.ts +14 -14
- package/src/lib/mcp/types/McpServerOptions.ts +1 -1
- package/src/lib/server/DELETE.ts +4 -4
- package/src/lib/server/GET.ts +4 -4
- package/src/lib/server/HEAD.ts +4 -4
- package/src/lib/server/PATCH.ts +4 -4
- package/src/lib/server/POST.ts +4 -4
- package/src/lib/server/PUT.ts +4 -4
- package/src/lib/server/agent.ts +4 -4
- package/src/lib/server/env.ts +1 -1
- package/src/lib/server/error.ts +49 -7
- package/src/lib/server/json.ts +1 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
- package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
- package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
- package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
- package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
- package/src/lib/server/rpc/parseArgs.ts +6 -6
- package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
- package/src/lib/server/rpc/registerRpc.ts +6 -0
- package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
- package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
- package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
- package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
- package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
- package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
- package/src/lib/server/rpc/unprocessed.ts +6 -7
- package/src/lib/server/rpc/validationError.ts +17 -0
- package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
- package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
- package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
- package/src/lib/server/runtime/createServer.ts +4 -4
- package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
- package/src/lib/server/runtime/crossOriginGate.ts +4 -4
- package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
- package/src/lib/server/runtime/registryManifests.ts +2 -2
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
- package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
- package/src/lib/server/runtime/types/RequestStore.ts +1 -1
- package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
- package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
- package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
- package/src/lib/shared/HttpError.ts +15 -1
- package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
- package/src/lib/shared/buildRpcRequest.ts +5 -5
- package/src/lib/shared/cache.ts +50 -168
- package/src/lib/shared/carriesBodyArgs.ts +4 -4
- package/src/lib/shared/createCacheStore.ts +12 -0
- package/src/lib/shared/createRemoteFunction.ts +11 -6
- package/src/lib/shared/decodeResponse.ts +2 -2
- package/src/lib/shared/detectRpcMethod.ts +17 -0
- package/src/lib/shared/emitLogRecord.ts +3 -3
- package/src/lib/shared/extraForwardHeaders.ts +1 -1
- package/src/lib/shared/findExportCallSite.ts +30 -273
- package/src/lib/shared/forwardHeaders.ts +2 -2
- package/src/lib/shared/httpErrorFor.ts +26 -0
- package/src/lib/shared/isAsciiWhitespace.ts +5 -0
- package/src/lib/shared/isIdentPart.ts +9 -0
- package/src/lib/shared/isIdentStart.ts +8 -0
- package/src/lib/shared/isReadOnlyMethod.ts +4 -4
- package/src/lib/shared/keyForRemoteCall.ts +4 -4
- package/src/lib/shared/log.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +20 -0
- package/src/lib/shared/pending.ts +4 -1
- package/src/lib/shared/prepareRpcModule.ts +96 -14
- package/src/lib/shared/probeRegistries.ts +17 -4
- package/src/lib/shared/queryStringFromArgs.ts +1 -1
- package/src/lib/shared/remoteMetaStore.ts +2 -2
- package/src/lib/shared/resolveClientFlags.ts +1 -1
- package/src/lib/shared/skipNonCode.ts +230 -0
- package/src/lib/shared/streamResponse.ts +9 -6
- package/src/lib/shared/stripImport.ts +3 -3
- package/src/lib/shared/types/CacheEntry.ts +2 -4
- package/src/lib/shared/types/CacheOnContext.ts +1 -11
- package/src/lib/shared/types/CacheStore.ts +13 -7
- package/src/lib/shared/types/ClientFlags.ts +1 -1
- package/src/lib/shared/types/ErrorConstructors.ts +17 -0
- package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +11 -0
- package/src/lib/shared/types/HttpMethod.ts +1 -0
- package/src/lib/shared/types/Outbox.ts +9 -0
- package/src/lib/shared/types/OutboxEntry.ts +27 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
- package/src/lib/shared/types/RemoteCallable.ts +2 -2
- package/src/lib/shared/types/RemoteFunction.ts +21 -6
- package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
- package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
- package/src/lib/shared/types/RpcInvoker.ts +1 -1
- package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
- package/src/lib/shared/types/ValidationErrorData.ts +20 -0
- package/src/lib/shared/url.ts +3 -3
- package/src/lib/shared/writeRpcDts.ts +7 -7
- package/src/lib/shared/writeTestRpcDts.ts +3 -3
- package/src/lib/test/createTestApp.ts +10 -10
- package/src/lib/ui/compile/awaitPlan.ts +48 -0
- package/src/lib/ui/compile/compileShadow.ts +75 -21
- package/src/lib/ui/compile/desugarSignals.ts +42 -6
- package/src/lib/ui/compile/generateBuild.ts +43 -60
- package/src/lib/ui/compile/generateSSR.ts +84 -54
- package/src/lib/ui/compile/ifPlan.ts +30 -0
- package/src/lib/ui/compile/parseTemplate.ts +41 -11
- package/src/lib/ui/compile/stripEffects.ts +17 -9
- package/src/lib/ui/compile/switchPlan.ts +22 -0
- package/src/lib/ui/compile/tryPlan.ts +29 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +6 -2
- package/src/lib/ui/createScope.ts +20 -2
- package/src/lib/ui/dom/disposeRange.ts +6 -10
- package/src/lib/ui/dom/hydrate.ts +2 -5
- package/src/lib/ui/dom/mount.ts +1 -2
- package/src/lib/ui/dom/withScope.ts +6 -9
- package/src/lib/ui/effect.ts +4 -2
- package/src/lib/ui/navigate.ts +40 -13
- package/src/lib/ui/outbox.ts +30 -110
- package/src/lib/ui/remoteProxy.ts +160 -10
- package/src/lib/ui/router.ts +3 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
- package/src/lib/ui/socketChannel.ts +7 -1
- package/src/lib/ui/types/Scope.ts +20 -5
- package/template/CLAUDE.md +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/test/app.test.ts +2 -2
- package/src/lib/server/rpc/registerVerb.ts +0 -6
- package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
- package/src/lib/shared/detectVerbMethod.ts +0 -17
- package/src/lib/shared/types/HttpVerb.ts +0 -1
- package/src/lib/ui/types/Outbox.ts +0 -14
|
@@ -5,7 +5,7 @@ import { REMOTE_FUNCTION } from './REMOTE_FUNCTION.ts'
|
|
|
5
5
|
import { recordRemoteMeta } from './recordRemoteMeta.ts'
|
|
6
6
|
import { subscribableFromResponse } from './subscribableFromResponse.ts'
|
|
7
7
|
import type { ClientFlags } from './types/ClientFlags.ts'
|
|
8
|
-
import type {
|
|
8
|
+
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
9
9
|
import type { RawRemoteFunction } from './types/RawRemoteFunction.ts'
|
|
10
10
|
import type { RemoteFunction } from './types/RemoteFunction.ts'
|
|
11
11
|
import type { RpcOptions } from './types/RpcOptions.ts'
|
|
@@ -13,7 +13,7 @@ import type { Subscribable } from './types/Subscribable.ts'
|
|
|
13
13
|
|
|
14
14
|
/*
|
|
15
15
|
Assembles the public RemoteFunction shape used identically by the
|
|
16
|
-
server-side
|
|
16
|
+
server-side defineRpc (in-process handler invocation) and the
|
|
17
17
|
client-side remoteProxy (network fetch). Centralising the wiring here
|
|
18
18
|
keeps the call/raw/stream/fetch semantics — including WeakMap meta
|
|
19
19
|
recording, Content-Type decode, and Subscribable derivation — in one
|
|
@@ -24,7 +24,7 @@ place so the two halves can't drift.
|
|
|
24
24
|
base; client uses window.location. The result is memoised inside the
|
|
25
25
|
per-call `getRequest` thunk so the Request is built at most once per
|
|
26
26
|
call regardless of how many readers pull on it.
|
|
27
|
-
- `invoke(args, getRequest)` actually runs the call: server
|
|
27
|
+
- `invoke(args, getRequest)` actually runs the call: server defineRpc
|
|
28
28
|
runs the handler and ignores `getRequest`; client remoteProxy calls
|
|
29
29
|
`fetch(getRequest())`. The thunk lets the server skip the Request
|
|
30
30
|
allocation entirely on the SSR hot path — the only consumer that ever
|
|
@@ -35,10 +35,10 @@ place so the two halves can't drift.
|
|
|
35
35
|
forwards the request through invoke().
|
|
36
36
|
*/
|
|
37
37
|
export function createRemoteFunction<Args, Return>(opts: {
|
|
38
|
-
method:
|
|
38
|
+
method: HttpMethod
|
|
39
39
|
url: string
|
|
40
40
|
clients: ClientFlags
|
|
41
|
-
/* Server-side only: exempts a mutating
|
|
41
|
+
/* Server-side only: exempts a mutating rpc from the router's same-origin CSRF gate. */
|
|
42
42
|
crossOrigin?: boolean
|
|
43
43
|
buildRequest: (args: Args | undefined, opts?: RpcOptions) => Request
|
|
44
44
|
invoke: (
|
|
@@ -75,7 +75,7 @@ export function createRemoteFunction<Args, Return>(opts: {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/*
|
|
78
|
-
A body
|
|
78
|
+
A body rpc may receive a FormData in place of typed Args (the upload
|
|
79
79
|
escape hatch). It flows through dispatch only into buildRpcRequest /
|
|
80
80
|
keyForRemoteCall, both of which take it as-is, so the cast to Args is a
|
|
81
81
|
contained type lie — buildRpcRequest's `instanceof FormData` branch handles
|
|
@@ -103,6 +103,11 @@ export function createRemoteFunction<Args, Return>(opts: {
|
|
|
103
103
|
raw(args as Args),
|
|
104
104
|
)
|
|
105
105
|
}
|
|
106
|
+
/* Uniform runtime guard for every rpc — the per-rpc data typing lives entirely in the
|
|
107
|
+
RpcErrorGuard<Errors> signature RemoteFunction projects onto it (Errors flows from the
|
|
108
|
+
rpc helper's declared type, not from here). */
|
|
109
|
+
callable.isError = (error: unknown, kind: string): boolean =>
|
|
110
|
+
error instanceof HttpError && error.kind === kind
|
|
106
111
|
Object.defineProperty(callable, REMOTE_FUNCTION, { value: true })
|
|
107
112
|
callable.fetch = parseArgsForFetch
|
|
108
113
|
? async (request: Request): Promise<Response> => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { contentBodyKind } from './contentBodyKind.ts'
|
|
2
2
|
import { contentTypeOf } from './contentTypeOf.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { httpErrorFor } from './httpErrorFor.ts'
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
6
|
Decodes a Response into the natural body value based on Content-Type:
|
|
@@ -27,7 +27,7 @@ returns the underlying Response untouched.
|
|
|
27
27
|
*/
|
|
28
28
|
export async function decodeResponse(response: Response): Promise<unknown> {
|
|
29
29
|
if (!response.ok) {
|
|
30
|
-
throw
|
|
30
|
+
throw await httpErrorFor(response)
|
|
31
31
|
}
|
|
32
32
|
if (response.status === 204) {
|
|
33
33
|
return undefined
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Reads the HTTP method of an $rpc module from its source. Every file under
|
|
5
|
+
src/server/rpc/ follows the convention `export const <name> = GET(fn)` (the
|
|
6
|
+
rpc helper picks the method, possibly with an explicit generic
|
|
7
|
+
`GET<{…}>(fn)`), so the helper name at the export is the method. Returns
|
|
8
|
+
undefined when no rpc export matches — the caller skips the file rather
|
|
9
|
+
than guessing. Used by the rpc.d.ts codegen to type url() against
|
|
10
|
+
query-carrying rpcs; matching the same convention the bundler rewrites
|
|
11
|
+
keeps the two from drifting.
|
|
12
|
+
*/
|
|
13
|
+
const RPC_EXPORT = /export\s+const\s+\w+\s*=\s*(GET|POST|PUT|PATCH|DELETE|HEAD)\s*[<(]/
|
|
14
|
+
|
|
15
|
+
export function detectRpcMethod(source: string): HttpMethod | undefined {
|
|
16
|
+
return (source.match(RPC_EXPORT)?.[1] as HttpMethod | undefined) ?? undefined
|
|
17
|
+
}
|
|
@@ -77,7 +77,7 @@ function formatClock(ts: number): string {
|
|
|
77
77
|
|
|
78
78
|
/*
|
|
79
79
|
Builds the record from the emission's own fields plus the ambient request
|
|
80
|
-
scope (trace, elapsed,
|
|
80
|
+
scope (trace, elapsed, method+path), then renders it through the active
|
|
81
81
|
formatter. The scope is read at emission time so a record emitted from a
|
|
82
82
|
stream-flush callback re-entered into the scope still carries its context.
|
|
83
83
|
Internal seam shared by the public logger and the framework's closing-record
|
|
@@ -132,14 +132,14 @@ function consoleFor(level: LogRecord['level']): (...args: unknown[]) => void {
|
|
|
132
132
|
|
|
133
133
|
/*
|
|
134
134
|
The unified tsv line (the default format): tab-separated
|
|
135
|
-
`<clock> <trace8> <
|
|
135
|
+
`<clock> <trace8> <rpc path> [channel] <message> +0.00ms`. The
|
|
136
136
|
wall-clock leads every line (each record carries `ts`); inside a request scope
|
|
137
137
|
the trace column follows and the elapsed-at-emission timing trails; a closing
|
|
138
138
|
record emitted outside one (asset hits sidestep the scope) pads a blank trace
|
|
139
139
|
column and trails its serve duration instead, so request lines stay aligned
|
|
140
140
|
whatever produced them. Every record speaks on a channel
|
|
141
141
|
(the app name, 'abide', or a diagnostic channel), shown as a dim `[name]`
|
|
142
|
-
tag opening the message field. The
|
|
142
|
+
tag opening the message field. The method+path pair is one field — it's the
|
|
143
143
|
line's anchor unit — and the tag folds into the message field so field
|
|
144
144
|
positions stay stable for cut/awk consumers.
|
|
145
145
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
App-configured inbound header names to forward onto in-process rpc Requests,
|
|
3
3
|
on top of the built-in FORWARDED_HEADERS. Set once at boot from
|
|
4
|
-
app.forwardHeaders so the SSR in-process path (
|
|
4
|
+
app.forwardHeaders so the SSR in-process path (defineRpc) and the MCP
|
|
5
5
|
dispatcher honour the same list without re-reading app config per call. A
|
|
6
6
|
module-level slot rather than threaded config because both call sites are deep
|
|
7
7
|
in the request path and the value is fixed for the process lifetime.
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { isAsciiWhitespace } from './isAsciiWhitespace.ts'
|
|
2
|
+
import { isIdentPart } from './isIdentPart.ts'
|
|
3
|
+
import { isIdentStart } from './isIdentStart.ts'
|
|
4
|
+
import { skipNonCode } from './skipNonCode.ts'
|
|
5
|
+
|
|
1
6
|
/*
|
|
2
7
|
Scans a module source character-by-character — skipping strings,
|
|
3
|
-
templates, comments,
|
|
4
|
-
`export const <name> = <IDENT>(...)` binding the caller
|
|
5
|
-
On a match returns the identifier text, the export name, and
|
|
6
|
-
ranges of the call's open and close parens; the $rpc and $sockets
|
|
8
|
+
templates, comments, regex literals (via skipNonCode), and TypeScript
|
|
9
|
+
generics — for an `export const <name> = <IDENT>(...)` binding the caller
|
|
10
|
+
cares about. On a match returns the identifier text, the export name, and
|
|
11
|
+
the byte ranges of the call's open and close parens; the $rpc and $sockets
|
|
7
12
|
rewriters splice their runtime bindings into those ranges.
|
|
8
13
|
|
|
9
14
|
The scanner enforces a single matching export per module: a second match
|
|
@@ -32,20 +37,12 @@ export function findExportCallSite(
|
|
|
32
37
|
const len = source.length
|
|
33
38
|
let i = 0
|
|
34
39
|
while (i < len) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
i = afterSlash
|
|
39
|
-
continue
|
|
40
|
-
}
|
|
41
|
-
if (c === '"' || c === "'") {
|
|
42
|
-
i = skipString(source, i + 1, c)
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
if (c === '`') {
|
|
46
|
-
i = skipTemplate(source, i + 1)
|
|
40
|
+
const skipped = skipNonCode(source, i)
|
|
41
|
+
if (skipped !== undefined) {
|
|
42
|
+
i = skipped
|
|
47
43
|
continue
|
|
48
44
|
}
|
|
45
|
+
const c = source[i]
|
|
49
46
|
if (isIdentStart(c) && !isIdentPart(source[i - 1])) {
|
|
50
47
|
let j = i + 1
|
|
51
48
|
while (j < len && isIdentPart(source[j])) {
|
|
@@ -84,106 +81,9 @@ export function findExportCallSite(
|
|
|
84
81
|
return found
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
function skipString(source: string, start: number, quote: string): number {
|
|
88
|
-
let i = start
|
|
89
|
-
while (i < source.length) {
|
|
90
|
-
const c = source[i]
|
|
91
|
-
if (c === '\\') {
|
|
92
|
-
i += 2
|
|
93
|
-
continue
|
|
94
|
-
}
|
|
95
|
-
if (c === quote) {
|
|
96
|
-
return i + 1
|
|
97
|
-
}
|
|
98
|
-
if (c === '\n') {
|
|
99
|
-
return i
|
|
100
|
-
}
|
|
101
|
-
i++
|
|
102
|
-
}
|
|
103
|
-
return source.length
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function skipTemplate(source: string, start: number): number {
|
|
107
|
-
let i = start
|
|
108
|
-
while (i < source.length) {
|
|
109
|
-
const c = source[i]
|
|
110
|
-
if (c === '\\') {
|
|
111
|
-
i += 2
|
|
112
|
-
continue
|
|
113
|
-
}
|
|
114
|
-
if (c === '`') {
|
|
115
|
-
return i + 1
|
|
116
|
-
}
|
|
117
|
-
if (c === '$' && source[i + 1] === '{') {
|
|
118
|
-
i = skipTemplateExpression(source, i + 2)
|
|
119
|
-
continue
|
|
120
|
-
}
|
|
121
|
-
i++
|
|
122
|
-
}
|
|
123
|
-
return source.length
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function skipTemplateExpression(source: string, start: number): number {
|
|
127
|
-
let depth = 1
|
|
128
|
-
let i = start
|
|
129
|
-
while (i < source.length && depth > 0) {
|
|
130
|
-
const c = source[i]
|
|
131
|
-
if (c === '{') {
|
|
132
|
-
depth++
|
|
133
|
-
i++
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
if (c === '}') {
|
|
137
|
-
depth--
|
|
138
|
-
i++
|
|
139
|
-
continue
|
|
140
|
-
}
|
|
141
|
-
if (c === '"' || c === "'") {
|
|
142
|
-
i = skipString(source, i + 1, c)
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
if (c === '`') {
|
|
146
|
-
i = skipTemplate(source, i + 1)
|
|
147
|
-
continue
|
|
148
|
-
}
|
|
149
|
-
const afterSlash = skipSlashConstruct(source, i)
|
|
150
|
-
if (afterSlash !== undefined) {
|
|
151
|
-
i = afterSlash
|
|
152
|
-
continue
|
|
153
|
-
}
|
|
154
|
-
i++
|
|
155
|
-
}
|
|
156
|
-
return i
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/*
|
|
160
|
-
When `i` sits on a `/` introducing a line comment, block comment, or regex
|
|
161
|
-
literal, returns the index immediately after the construct; undefined when
|
|
162
|
-
the `/` is division (or `i` isn't a `/`). One statement of the skip rules
|
|
163
|
-
shared by every scanner loop so they can't drift.
|
|
164
|
-
*/
|
|
165
|
-
function skipSlashConstruct(source: string, i: number): number | undefined {
|
|
166
|
-
if (source[i] !== '/') {
|
|
167
|
-
return undefined
|
|
168
|
-
}
|
|
169
|
-
const next = source[i + 1]
|
|
170
|
-
if (next === '/') {
|
|
171
|
-
const newline = source.indexOf('\n', i + 2)
|
|
172
|
-
return newline === -1 ? source.length : newline + 1
|
|
173
|
-
}
|
|
174
|
-
if (next === '*') {
|
|
175
|
-
const end = source.indexOf('*/', i + 2)
|
|
176
|
-
return end === -1 ? source.length : end + 2
|
|
177
|
-
}
|
|
178
|
-
if (isRegexContext(source, i)) {
|
|
179
|
-
return skipRegex(source, i + 1)
|
|
180
|
-
}
|
|
181
|
-
return undefined
|
|
182
|
-
}
|
|
183
|
-
|
|
184
84
|
function matchCallTail(source: string, after: number): number | undefined {
|
|
185
85
|
let j = after
|
|
186
|
-
while (j < source.length &&
|
|
86
|
+
while (j < source.length && isAsciiWhitespace(source[j])) {
|
|
187
87
|
j++
|
|
188
88
|
}
|
|
189
89
|
if (source[j] === '<') {
|
|
@@ -192,7 +92,7 @@ function matchCallTail(source: string, after: number): number | undefined {
|
|
|
192
92
|
return undefined
|
|
193
93
|
}
|
|
194
94
|
j = closed
|
|
195
|
-
while (j < source.length &&
|
|
95
|
+
while (j < source.length && isAsciiWhitespace(source[j])) {
|
|
196
96
|
j++
|
|
197
97
|
}
|
|
198
98
|
}
|
|
@@ -216,15 +116,12 @@ function skipGenerics(source: string, start: number): number | undefined {
|
|
|
216
116
|
let bracketDepth = 0
|
|
217
117
|
let i = start
|
|
218
118
|
while (i < source.length) {
|
|
219
|
-
const
|
|
220
|
-
if (
|
|
221
|
-
i =
|
|
222
|
-
continue
|
|
223
|
-
}
|
|
224
|
-
if (c === '`') {
|
|
225
|
-
i = skipTemplate(source, i + 1)
|
|
119
|
+
const skipped = skipNonCode(source, i)
|
|
120
|
+
if (skipped !== undefined) {
|
|
121
|
+
i = skipped
|
|
226
122
|
continue
|
|
227
123
|
}
|
|
124
|
+
const c = source[i]
|
|
228
125
|
if (c === '<') {
|
|
229
126
|
angleDepth++
|
|
230
127
|
} else if (c === '>') {
|
|
@@ -254,28 +151,20 @@ function skipGenerics(source: string, start: number): number | undefined {
|
|
|
254
151
|
}
|
|
255
152
|
|
|
256
153
|
/*
|
|
257
|
-
Walks the call body, skipping strings/templates/comments and
|
|
258
|
-
nested `()` so brackets inside object literals or nested calls
|
|
259
|
-
throw the depth count.
|
|
154
|
+
Walks the call body, skipping strings/templates/comments/regex and
|
|
155
|
+
respecting nested `()` so brackets inside object literals or nested calls
|
|
156
|
+
don't throw the depth count.
|
|
260
157
|
*/
|
|
261
158
|
function findCallEnd(source: string, parenStart: number): number | undefined {
|
|
262
159
|
let depth = 1
|
|
263
160
|
let i = parenStart + 1
|
|
264
161
|
while (i < source.length) {
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
267
|
-
i =
|
|
268
|
-
continue
|
|
269
|
-
}
|
|
270
|
-
if (c === '`') {
|
|
271
|
-
i = skipTemplate(source, i + 1)
|
|
272
|
-
continue
|
|
273
|
-
}
|
|
274
|
-
const afterSlash = skipSlashConstruct(source, i)
|
|
275
|
-
if (afterSlash !== undefined) {
|
|
276
|
-
i = afterSlash
|
|
162
|
+
const skipped = skipNonCode(source, i)
|
|
163
|
+
if (skipped !== undefined) {
|
|
164
|
+
i = skipped
|
|
277
165
|
continue
|
|
278
166
|
}
|
|
167
|
+
const c = source[i]
|
|
279
168
|
if (c === '(') {
|
|
280
169
|
depth++
|
|
281
170
|
} else if (c === ')') {
|
|
@@ -297,14 +186,14 @@ isn't the module's declared export.
|
|
|
297
186
|
*/
|
|
298
187
|
function detectExportName(source: string, callStart: number): string | undefined {
|
|
299
188
|
let i = callStart - 1
|
|
300
|
-
while (i >= 0 &&
|
|
189
|
+
while (i >= 0 && isAsciiWhitespace(source[i])) {
|
|
301
190
|
i--
|
|
302
191
|
}
|
|
303
192
|
if (source[i] !== '=') {
|
|
304
193
|
return undefined
|
|
305
194
|
}
|
|
306
195
|
i--
|
|
307
|
-
while (i >= 0 &&
|
|
196
|
+
while (i >= 0 && isAsciiWhitespace(source[i])) {
|
|
308
197
|
i--
|
|
309
198
|
}
|
|
310
199
|
const nameEnd = i + 1
|
|
@@ -316,14 +205,14 @@ function detectExportName(source: string, callStart: number): string | undefined
|
|
|
316
205
|
return undefined
|
|
317
206
|
}
|
|
318
207
|
const name = source.slice(nameStart, nameEnd)
|
|
319
|
-
while (i >= 0 &&
|
|
208
|
+
while (i >= 0 && isAsciiWhitespace(source[i])) {
|
|
320
209
|
i--
|
|
321
210
|
}
|
|
322
211
|
if (!matchesBackwards(source, i, 'const')) {
|
|
323
212
|
return undefined
|
|
324
213
|
}
|
|
325
214
|
i -= 'const'.length
|
|
326
|
-
while (i >= 0 &&
|
|
215
|
+
while (i >= 0 && isAsciiWhitespace(source[i])) {
|
|
327
216
|
i--
|
|
328
217
|
}
|
|
329
218
|
if (!matchesBackwards(source, i, 'export')) {
|
|
@@ -342,135 +231,3 @@ function matchesBackwards(source: string, end: number, keyword: string): boolean
|
|
|
342
231
|
}
|
|
343
232
|
return start === 0 || !isIdentPart(source[start - 1])
|
|
344
233
|
}
|
|
345
|
-
|
|
346
|
-
function isIdentStart(c: string | undefined): boolean {
|
|
347
|
-
if (c === undefined) {
|
|
348
|
-
return false
|
|
349
|
-
}
|
|
350
|
-
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_' || c === '$'
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function isIdentPart(c: string | undefined): boolean {
|
|
354
|
-
if (c === undefined) {
|
|
355
|
-
return false
|
|
356
|
-
}
|
|
357
|
-
return isIdentStart(c) || (c >= '0' && c <= '9')
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function isWhitespace(c: string | undefined): boolean {
|
|
361
|
-
return c === ' ' || c === '\t' || c === '\n' || c === '\r'
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/*
|
|
365
|
-
A `/` starts a regex literal when the prior expression context expects an
|
|
366
|
-
expression rather than a value — after an open delimiter, operator, or
|
|
367
|
-
expression-prefix keyword (return, typeof, instanceof, in, of, delete,
|
|
368
|
-
void, await, yield, new, throw, case, do). Otherwise `/` is division.
|
|
369
|
-
Without this disambiguation a regex like `/^\//` reads as `/` (division),
|
|
370
|
-
then `^`, `\`, `/`, `/` — and the final `//` pair fakes a line comment
|
|
371
|
-
that swallows the rest of the line, eating any `)` that closes the
|
|
372
|
-
enclosing call.
|
|
373
|
-
*/
|
|
374
|
-
const REGEX_PREFIX_KEYWORDS = new Set([
|
|
375
|
-
'return',
|
|
376
|
-
'typeof',
|
|
377
|
-
'instanceof',
|
|
378
|
-
'in',
|
|
379
|
-
'of',
|
|
380
|
-
'delete',
|
|
381
|
-
'void',
|
|
382
|
-
'await',
|
|
383
|
-
'yield',
|
|
384
|
-
'new',
|
|
385
|
-
'throw',
|
|
386
|
-
'case',
|
|
387
|
-
'do',
|
|
388
|
-
])
|
|
389
|
-
|
|
390
|
-
const REGEX_PUNCTUATION = new Set([
|
|
391
|
-
'(',
|
|
392
|
-
'[',
|
|
393
|
-
'{',
|
|
394
|
-
',',
|
|
395
|
-
';',
|
|
396
|
-
':',
|
|
397
|
-
'?',
|
|
398
|
-
'!',
|
|
399
|
-
'&',
|
|
400
|
-
'|',
|
|
401
|
-
'^',
|
|
402
|
-
'~',
|
|
403
|
-
'+',
|
|
404
|
-
'-',
|
|
405
|
-
'*',
|
|
406
|
-
'%',
|
|
407
|
-
'<',
|
|
408
|
-
'>',
|
|
409
|
-
'=',
|
|
410
|
-
'/',
|
|
411
|
-
])
|
|
412
|
-
|
|
413
|
-
function isRegexContext(source: string, slashIndex: number): boolean {
|
|
414
|
-
let i = slashIndex - 1
|
|
415
|
-
while (i >= 0 && isWhitespace(source[i])) {
|
|
416
|
-
i--
|
|
417
|
-
}
|
|
418
|
-
if (i < 0) {
|
|
419
|
-
return true
|
|
420
|
-
}
|
|
421
|
-
const prev = source[i] as string
|
|
422
|
-
if (REGEX_PUNCTUATION.has(prev)) {
|
|
423
|
-
return true
|
|
424
|
-
}
|
|
425
|
-
if (isIdentPart(prev)) {
|
|
426
|
-
let start = i
|
|
427
|
-
while (start > 0 && isIdentPart(source[start - 1])) {
|
|
428
|
-
start--
|
|
429
|
-
}
|
|
430
|
-
return REGEX_PREFIX_KEYWORDS.has(source.slice(start, i + 1))
|
|
431
|
-
}
|
|
432
|
-
return false
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/*
|
|
436
|
-
Walks past a regex literal body, respecting character classes (`[...]`
|
|
437
|
-
where `/` is literal) and backslash escapes, then consumes trailing
|
|
438
|
-
flag identifiers. Returns the index immediately after the regex. An
|
|
439
|
-
unterminated regex (newline before closing `/`) returns the newline
|
|
440
|
-
position so the outer scanner can resume normally on the next line.
|
|
441
|
-
*/
|
|
442
|
-
function skipRegex(source: string, start: number): number {
|
|
443
|
-
let i = start
|
|
444
|
-
let inClass = false
|
|
445
|
-
while (i < source.length) {
|
|
446
|
-
const c = source[i]
|
|
447
|
-
if (c === '\\') {
|
|
448
|
-
i += 2
|
|
449
|
-
continue
|
|
450
|
-
}
|
|
451
|
-
if (c === '\n') {
|
|
452
|
-
return i
|
|
453
|
-
}
|
|
454
|
-
if (inClass) {
|
|
455
|
-
if (c === ']') {
|
|
456
|
-
inClass = false
|
|
457
|
-
}
|
|
458
|
-
i++
|
|
459
|
-
continue
|
|
460
|
-
}
|
|
461
|
-
if (c === '[') {
|
|
462
|
-
inClass = true
|
|
463
|
-
i++
|
|
464
|
-
continue
|
|
465
|
-
}
|
|
466
|
-
if (c === '/') {
|
|
467
|
-
let j = i + 1
|
|
468
|
-
while (j < source.length && isIdentPart(source[j])) {
|
|
469
|
-
j++
|
|
470
|
-
}
|
|
471
|
-
return j
|
|
472
|
-
}
|
|
473
|
-
i++
|
|
474
|
-
}
|
|
475
|
-
return source.length
|
|
476
|
-
}
|
|
@@ -4,8 +4,8 @@ import { extraForwardHeaders } from './extraForwardHeaders.ts'
|
|
|
4
4
|
Headers abide forwards from an inbound HTTP/MCP request onto every
|
|
5
5
|
synthesized in-process rpc Request — cookies + bearer auth + the four
|
|
6
6
|
forwarding hints proxies set when terminating TLS in front of the app.
|
|
7
|
-
|
|
8
|
-
dispatcher uses it when piping a tool invocation through
|
|
7
|
+
defineRpc uses this when an SSR pass calls a rpc in-process; the MCP
|
|
8
|
+
dispatcher uses it when piping a tool invocation through rpc.fetch.
|
|
9
9
|
|
|
10
10
|
WARNING — this is an allowlist: every inbound header NOT named here (and
|
|
11
11
|
not added via app.forwardHeaders) is DROPPED on the in-process path. A
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { contentBodyKind } from './contentBodyKind.ts'
|
|
2
|
+
import { contentTypeOf } from './contentTypeOf.ts'
|
|
3
|
+
import { HttpError } from './HttpError.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Builds the HttpError for a non-2xx response, parsing a typed-error body
|
|
7
|
+
(`{ $abideError, data }`, emitted by `error(errors.x(...))` and validation 422)
|
|
8
|
+
onto `.kind` / `.data`. Reads a clone so the original `response.body` stays
|
|
9
|
+
unread for callers that inspect it. A non-JSON or malformed body leaves
|
|
10
|
+
`.kind` / `.data` undefined (a plain `error(status, text)`). Shared by the
|
|
11
|
+
plain decode path (decodeResponse) and the streaming path (streamResponse) so
|
|
12
|
+
both surface the same typed error on a non-2xx.
|
|
13
|
+
*/
|
|
14
|
+
export async function httpErrorFor(response: Response): Promise<HttpError> {
|
|
15
|
+
if (contentBodyKind(contentTypeOf(response.headers)) === 'json') {
|
|
16
|
+
try {
|
|
17
|
+
const body = await response.clone().json()
|
|
18
|
+
if (body !== null && typeof body === 'object' && '$abideError' in body) {
|
|
19
|
+
return new HttpError(response, body.$abideError, body.data)
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
/* malformed JSON error body — fall through to a plain HttpError */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return new HttpError(response)
|
|
26
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* True for the source-scanner whitespace set (space, tab, CR, LF). Distinct from
|
|
2
|
+
isWhitespaceText, which classifies template text nodes. */
|
|
3
|
+
export function isAsciiWhitespace(c: string | undefined): boolean {
|
|
4
|
+
return c === ' ' || c === '\t' || c === '\n' || c === '\r'
|
|
5
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isIdentStart } from './isIdentStart.ts'
|
|
2
|
+
|
|
3
|
+
/* True when `c` can continue a JavaScript identifier — an identifier-start char or a digit. */
|
|
4
|
+
export function isIdentPart(c: string | undefined): boolean {
|
|
5
|
+
if (c === undefined) {
|
|
6
|
+
return false
|
|
7
|
+
}
|
|
8
|
+
return isIdentStart(c) || (c >= '0' && c <= '9')
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* True when `c` can begin a JavaScript identifier (ASCII letters, `_`, `$`). Shared by the
|
|
2
|
+
source scanners (findExportCallSite, skipNonCode) to find identifiers and regex flags. */
|
|
3
|
+
export function isIdentStart(c: string | undefined): boolean {
|
|
4
|
+
if (c === undefined) {
|
|
5
|
+
return false
|
|
6
|
+
}
|
|
7
|
+
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_' || c === '$'
|
|
8
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
Read-only (safe) HTTP methods — they don't mutate server state. Abide
|
|
5
|
-
uses this to decide which
|
|
5
|
+
uses this to decide which rpcs auto-expose to MCP: reads flip MCP on
|
|
6
6
|
when a schema is present, mutations require an explicit `clients.mcp`
|
|
7
7
|
opt-in so a model can't delete/overwrite data just because the handler
|
|
8
8
|
carries a schema. Also feeds the MCP tool `readOnlyHint` annotation.
|
|
9
9
|
*/
|
|
10
|
-
const READ_ONLY_METHODS = new Set<
|
|
10
|
+
const READ_ONLY_METHODS = new Set<HttpMethod>(['GET', 'HEAD'])
|
|
11
11
|
|
|
12
|
-
export function isReadOnlyMethod(method:
|
|
12
|
+
export function isReadOnlyMethod(method: HttpMethod): boolean {
|
|
13
13
|
return READ_ONLY_METHODS.has(method)
|
|
14
14
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { canonicalJson } from './canonicalJson.ts'
|
|
2
2
|
import { carriesBodyArgs } from './carriesBodyArgs.ts'
|
|
3
3
|
import { queryStringFromArgs } from './queryStringFromArgs.ts'
|
|
4
|
-
import type {
|
|
4
|
+
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
Derives a cache key from a
|
|
7
|
+
Derives a cache key from a rpc-defined remote function and its args. The
|
|
8
8
|
prefix is `${method} ${url}` where `url` is the route template. GET/DELETE/HEAD
|
|
9
9
|
serialise args onto the URL as `?key=value` (sorted, via queryStringFromArgs —
|
|
10
10
|
the same encoder buildRpcRequest builds its query with, so the key and the
|
|
11
11
|
synthesized Request can't disagree); POST/PUT/PATCH join args after a space as
|
|
12
|
-
canonical JSON. The
|
|
12
|
+
canonical JSON. The rpc split mirrors buildRpcRequest exactly.
|
|
13
13
|
*/
|
|
14
|
-
export function keyForRemoteCall(method:
|
|
14
|
+
export function keyForRemoteCall(method: HttpMethod, url: string, args: unknown): string {
|
|
15
15
|
const prefix = `${method} ${url}`
|
|
16
16
|
if (!carriesBodyArgs(method)) {
|
|
17
17
|
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
package/src/lib/shared/log.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Log } from './types/Log.ts'
|
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
The unified logger: every record carries the request-scope context (short
|
|
8
|
-
trace id, +elapsed,
|
|
8
|
+
trace id, +elapsed, method+path) when one is active, plus a channel — the
|
|
9
9
|
line's speaker. `log(...)`/`warn`/`error`/`trace` speak on the app's own
|
|
10
10
|
always-on channel (the app name, resolved per emission so boot order doesn't
|
|
11
11
|
matter); `log.channel(name)` returns the same shape on a DEBUG-gated
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Internal slot the browser outbox registry registers its prober into, so the
|
|
3
|
+
shared pending() probe can count parked durable-rpc writes without shared/
|
|
4
|
+
importing browser/. Mirrors tailProbeSlot. The prober reads the doc-backed
|
|
5
|
+
queue entries (reactive inside $derived / $effect) and reports whether any
|
|
6
|
+
undelivered entry matches the selector: a durable rpc selector (carries a
|
|
7
|
+
`url`) narrows to its own queue, the bare form spans every registered queue,
|
|
8
|
+
optional args narrow to one parked call by structural compare. Outbox state is
|
|
9
|
+
pending-only — a parked write has no value, so it never contributes to
|
|
10
|
+
refreshing(); probeRegistries reads this slot only on its `field === 'pending'`
|
|
11
|
+
branch, which is why the slot mirrors tailProbeSlot but reports a bare boolean
|
|
12
|
+
rather than a { pending, refreshing } pair. When no prober is registered (server
|
|
13
|
+
render, or no durable rpc was ever used) pending() sees no parked writes,
|
|
14
|
+
exactly as on the server where there are no client queues.
|
|
15
|
+
*/
|
|
16
|
+
export const outboxProbeSlot: {
|
|
17
|
+
probe: ((selector: unknown, args: unknown) => boolean) | undefined
|
|
18
|
+
} = {
|
|
19
|
+
probe: undefined,
|
|
20
|
+
}
|
|
@@ -15,9 +15,12 @@ tail registry (streams). Pending means "no value yet":
|
|
|
15
15
|
pending(subscribable) → that stream awaiting its first frame
|
|
16
16
|
(tail.status === 'pending'; true when nothing
|
|
17
17
|
is reading yet — there is no value either way)
|
|
18
|
+
pending(durableRpc) → that durable rpc's parked writes (a parked write
|
|
19
|
+
has no value yet); the bare form counts them too
|
|
18
20
|
Probes report, never act: reading one opens no fetch and no stream. SSR
|
|
19
21
|
loading state is driven by {#await}, not this. Scan semantics (tap order,
|
|
20
|
-
selector grammar, registry spans) live in
|
|
22
|
+
selector grammar, registry spans, the parked-write term) live in
|
|
23
|
+
probeRegistries.
|
|
21
24
|
*/
|
|
22
25
|
// @documentation probes
|
|
23
26
|
export function pending<Args, Return>(
|