@abide/abide 0.42.0 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/AGENTS.md +234 -300
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +50 -93
  4. package/package.json +3 -2
  5. package/src/abideResolverPlugin.ts +104 -14
  6. package/src/buildCli.ts +1 -1
  7. package/src/discoveryEntry.ts +3 -3
  8. package/src/lib/bundle/BundleMenuItem.ts +1 -1
  9. package/src/lib/cli/createClient.ts +12 -12
  10. package/src/lib/cli/dispatchCommand.ts +1 -1
  11. package/src/lib/cli/printSessionStatus.ts +1 -1
  12. package/src/lib/cli/resolveCliTarget.ts +1 -1
  13. package/src/lib/cli/runCli.ts +1 -1
  14. package/src/lib/cli/types/CliManifest.ts +1 -1
  15. package/src/lib/cli/types/CliManifestEntry.ts +2 -2
  16. package/src/lib/mcp/annotationsForMethod.ts +5 -5
  17. package/src/lib/mcp/createMcpServer.ts +3 -3
  18. package/src/lib/mcp/mcpSurface.ts +14 -14
  19. package/src/lib/mcp/types/McpServerOptions.ts +1 -1
  20. package/src/lib/server/DELETE.ts +4 -4
  21. package/src/lib/server/GET.ts +4 -4
  22. package/src/lib/server/HEAD.ts +4 -4
  23. package/src/lib/server/PATCH.ts +4 -4
  24. package/src/lib/server/POST.ts +4 -4
  25. package/src/lib/server/PUT.ts +4 -4
  26. package/src/lib/server/agent.ts +4 -4
  27. package/src/lib/server/env.ts +1 -1
  28. package/src/lib/server/error.ts +49 -7
  29. package/src/lib/server/json.ts +1 -1
  30. package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
  31. package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
  32. package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
  33. package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
  34. package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
  35. package/src/lib/server/rpc/parseArgs.ts +6 -6
  36. package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
  37. package/src/lib/server/rpc/registerRpc.ts +6 -0
  38. package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
  39. package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
  40. package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
  41. package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
  42. package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
  43. package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
  44. package/src/lib/server/rpc/unprocessed.ts +6 -7
  45. package/src/lib/server/rpc/validationError.ts +17 -0
  46. package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
  47. package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
  48. package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
  49. package/src/lib/server/runtime/createServer.ts +4 -4
  50. package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
  51. package/src/lib/server/runtime/crossOriginGate.ts +4 -4
  52. package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
  53. package/src/lib/server/runtime/registryManifests.ts +2 -2
  54. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  55. package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
  56. package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
  57. package/src/lib/server/runtime/types/RequestStore.ts +1 -1
  58. package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
  59. package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
  60. package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
  61. package/src/lib/shared/HttpError.ts +15 -1
  62. package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
  63. package/src/lib/shared/buildRpcRequest.ts +5 -5
  64. package/src/lib/shared/cache.ts +50 -168
  65. package/src/lib/shared/carriesBodyArgs.ts +4 -4
  66. package/src/lib/shared/createCacheStore.ts +12 -0
  67. package/src/lib/shared/createRemoteFunction.ts +11 -6
  68. package/src/lib/shared/decodeResponse.ts +2 -2
  69. package/src/lib/shared/detectRpcMethod.ts +17 -0
  70. package/src/lib/shared/emitLogRecord.ts +3 -3
  71. package/src/lib/shared/extraForwardHeaders.ts +1 -1
  72. package/src/lib/shared/findExportCallSite.ts +30 -273
  73. package/src/lib/shared/forwardHeaders.ts +2 -2
  74. package/src/lib/shared/httpErrorFor.ts +26 -0
  75. package/src/lib/shared/isAsciiWhitespace.ts +5 -0
  76. package/src/lib/shared/isIdentPart.ts +9 -0
  77. package/src/lib/shared/isIdentStart.ts +8 -0
  78. package/src/lib/shared/isReadOnlyMethod.ts +4 -4
  79. package/src/lib/shared/keyForRemoteCall.ts +4 -4
  80. package/src/lib/shared/log.ts +1 -1
  81. package/src/lib/shared/outboxProbeSlot.ts +20 -0
  82. package/src/lib/shared/pending.ts +4 -1
  83. package/src/lib/shared/prepareRpcModule.ts +96 -14
  84. package/src/lib/shared/probeRegistries.ts +17 -4
  85. package/src/lib/shared/queryStringFromArgs.ts +1 -1
  86. package/src/lib/shared/remoteMetaStore.ts +2 -2
  87. package/src/lib/shared/resolveClientFlags.ts +1 -1
  88. package/src/lib/shared/skipNonCode.ts +230 -0
  89. package/src/lib/shared/streamResponse.ts +9 -6
  90. package/src/lib/shared/stripImport.ts +3 -3
  91. package/src/lib/shared/types/CacheEntry.ts +2 -4
  92. package/src/lib/shared/types/CacheOnContext.ts +1 -11
  93. package/src/lib/shared/types/CacheStore.ts +13 -7
  94. package/src/lib/shared/types/ClientFlags.ts +1 -1
  95. package/src/lib/shared/types/ErrorConstructors.ts +17 -0
  96. package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
  97. package/src/lib/shared/types/ErrorSpec.ts +11 -0
  98. package/src/lib/shared/types/HttpMethod.ts +1 -0
  99. package/src/lib/shared/types/Outbox.ts +9 -0
  100. package/src/lib/shared/types/OutboxEntry.ts +27 -0
  101. package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
  102. package/src/lib/shared/types/RemoteCallable.ts +2 -2
  103. package/src/lib/shared/types/RemoteFunction.ts +21 -6
  104. package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
  105. package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
  106. package/src/lib/shared/types/RpcInvoker.ts +1 -1
  107. package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
  108. package/src/lib/shared/types/ValidationErrorData.ts +20 -0
  109. package/src/lib/shared/url.ts +3 -3
  110. package/src/lib/shared/writeRpcDts.ts +7 -7
  111. package/src/lib/shared/writeTestRpcDts.ts +3 -3
  112. package/src/lib/test/createTestApp.ts +10 -10
  113. package/src/lib/ui/compile/awaitPlan.ts +48 -0
  114. package/src/lib/ui/compile/compileShadow.ts +89 -21
  115. package/src/lib/ui/compile/desugarSignals.ts +42 -6
  116. package/src/lib/ui/compile/generateBuild.ts +43 -60
  117. package/src/lib/ui/compile/generateSSR.ts +84 -54
  118. package/src/lib/ui/compile/ifPlan.ts +30 -0
  119. package/src/lib/ui/compile/parseTemplate.ts +41 -11
  120. package/src/lib/ui/compile/stripEffects.ts +17 -9
  121. package/src/lib/ui/compile/switchPlan.ts +22 -0
  122. package/src/lib/ui/compile/tryPlan.ts +29 -0
  123. package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
  124. package/src/lib/ui/compile/types/TemplateNode.ts +6 -2
  125. package/src/lib/ui/createScope.ts +20 -2
  126. package/src/lib/ui/dom/disposeRange.ts +6 -10
  127. package/src/lib/ui/dom/hydrate.ts +2 -5
  128. package/src/lib/ui/dom/mount.ts +1 -2
  129. package/src/lib/ui/dom/withScope.ts +6 -9
  130. package/src/lib/ui/effect.ts +4 -2
  131. package/src/lib/ui/navigate.ts +40 -13
  132. package/src/lib/ui/outbox.ts +30 -110
  133. package/src/lib/ui/remoteProxy.ts +160 -10
  134. package/src/lib/ui/router.ts +45 -15
  135. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
  136. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
  137. package/src/lib/ui/socketChannel.ts +7 -1
  138. package/src/lib/ui/types/Scope.ts +20 -5
  139. package/template/CLAUDE.md +1 -1
  140. package/template/src/server/rpc/getHello.ts +3 -3
  141. package/template/test/app.test.ts +2 -2
  142. package/src/lib/server/rpc/registerVerb.ts +0 -6
  143. package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
  144. package/src/lib/shared/detectVerbMethod.ts +0 -17
  145. package/src/lib/shared/types/HttpVerb.ts +0 -1
  146. package/src/lib/ui/types/Outbox.ts +0 -14
@@ -44,6 +44,13 @@ export function createCacheStore(): CacheStore {
44
44
  events.removeEventListener('invalidate', onInvalidate)
45
45
  if (subscribers.get(key) === registered) {
46
46
  subscribers.delete(key)
47
+ /* The reload marker is only ever consumed by the NEXT read of this
48
+ key (registerEntry). With the last reactive reader gone there is
49
+ no scope left to show refreshing() for it, and a future remount
50
+ reads with nothing on screen — a first-ever load, not a reload.
51
+ Drop the marker so the tab-scoped store can't accrete one per
52
+ invalidated-but-never-reread key over a session. */
53
+ pendingRefresh.delete(key)
47
54
  }
48
55
  }
49
56
  })
@@ -96,6 +103,11 @@ export function createCacheStore(): CacheStore {
96
103
  entries,
97
104
  events,
98
105
  subscribe,
106
+ /* True while a reactive scope is reading this key — i.e. someone is holding
107
+ its value on screen. invalidate() gates its reload marker on this: a key
108
+ with no live reader has nothing to reload into, so the next read is a
109
+ first-ever load, not a refresh. */
110
+ hasReader: (key: string) => subscribers.has(key),
99
111
  trackLifecycle,
100
112
  markLifecycle,
101
113
  pendingRefresh,
@@ -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 { HttpVerb } from './types/HttpVerb.ts'
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 defineVerb (in-process handler invocation) and the
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 defineVerb
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: HttpVerb
38
+ method: HttpMethod
39
39
  url: string
40
40
  clients: ClientFlags
41
- /* Server-side only: exempts a mutating verb from the router's same-origin CSRF gate. */
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 verb may receive a FormData in place of typed Args (the upload
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 { HttpError } from './HttpError.ts'
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 new HttpError(response)
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, verb+path), then renders it through the active
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> <verb path> [channel] <message> +0.00ms`. The
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 verb+path pair is one field — it's 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 (defineVerb) and the MCP
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, and TypeScript generics for an
4
- `export const <name> = <IDENT>(...)` binding the caller cares about.
5
- On a match returns the identifier text, the export name, and the byte
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 c = source[i]
36
- const afterSlash = skipSlashConstruct(source, i)
37
- if (afterSlash !== undefined) {
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 && isWhitespace(source[j])) {
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 && isWhitespace(source[j])) {
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 c = source[i]
220
- if (c === '"' || c === "'") {
221
- i = skipString(source, i + 1, c)
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 respecting
258
- nested `()` so brackets inside object literals or nested calls don't
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 c = source[i]
266
- if (c === '"' || c === "'") {
267
- i = skipString(source, i + 1, c)
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 && isWhitespace(source[i])) {
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 && isWhitespace(source[i])) {
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 && isWhitespace(source[i])) {
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 && isWhitespace(source[i])) {
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
- defineVerb uses this when an SSR pass calls a verb in-process; the MCP
8
- dispatcher uses it when piping a tool invocation through verb.fetch.
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 { HttpVerb } from './types/HttpVerb.ts'
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 verbs auto-expose to MCP: reads flip MCP on
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<HttpVerb>(['GET', 'HEAD'])
10
+ const READ_ONLY_METHODS = new Set<HttpMethod>(['GET', 'HEAD'])
11
11
 
12
- export function isReadOnlyMethod(method: HttpVerb): boolean {
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 { HttpVerb } from './types/HttpVerb.ts'
4
+ import type { HttpMethod } from './types/HttpMethod.ts'
5
5
 
6
6
  /*
7
- Derives a cache key from a verb-defined remote function and its args. The
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 verb split mirrors buildRpcRequest exactly.
12
+ canonical JSON. The rpc split mirrors buildRpcRequest exactly.
13
13
  */
14
- export function keyForRemoteCall(method: HttpVerb, url: string, args: unknown): string {
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)) {
@@ -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, verb+path) when one is active, plus a channel — the
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