@abide/abide 0.41.1 → 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 -294
- package/CHANGELOG.md +82 -0
- package/README.md +51 -95
- package/package.json +3 -2
- package/src/abideLsp.ts +46 -8
- package/src/abideResolverPlugin.ts +107 -19
- 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/devClientFingerprint.ts +2 -1
- 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/escapeRegex.ts +9 -0
- package/src/lib/shared/extraForwardHeaders.ts +1 -1
- package/src/lib/shared/fileName.ts +9 -0
- package/src/lib/shared/fileStem.ts +3 -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/programNameForPackage.ts +3 -1
- 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 +6 -4
- 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/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +45 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/awaitPlan.ts +48 -0
- package/src/lib/ui/compile/compileShadow.ts +117 -31
- package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
- package/src/lib/ui/compile/desugarSignals.ts +42 -6
- package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
- package/src/lib/ui/compile/generateBuild.ts +45 -62
- package/src/lib/ui/compile/generateSSR.ts +90 -60
- package/src/lib/ui/compile/ifPlan.ts +30 -0
- package/src/lib/ui/compile/makeVarNamer.ts +10 -0
- package/src/lib/ui/compile/offsetToPosition.ts +9 -0
- package/src/lib/ui/compile/parseTemplate.ts +565 -115
- package/src/lib/ui/compile/stripEffects.ts +17 -9
- package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
- package/src/lib/ui/compile/switchPlan.ts +22 -0
- package/src/lib/ui/compile/tryPlan.ts +29 -0
- package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +15 -2
- package/src/lib/ui/createScope.ts +20 -2
- package/src/lib/ui/dom/appendText.ts +1 -5
- package/src/lib/ui/dom/applyResolved.ts +1 -5
- package/src/lib/ui/dom/disposeRange.ts +6 -10
- package/src/lib/ui/dom/hydrate.ts +2 -5
- package/src/lib/ui/dom/isComment.ts +6 -0
- 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/runtime/createDoc.ts +3 -3
- package/src/lib/ui/runtime/pathSegments.ts +10 -0
- package/src/lib/ui/socketChannel.ts +7 -1
- package/src/lib/ui/types/Scope.ts +20 -5
- package/template/.zed/settings.json +4 -0
- package/template/CLAUDE.md +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +4 -4
- 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
|
@@ -68,9 +68,10 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
68
68
|
|
|
69
69
|
/*
|
|
70
70
|
Loads a socket module on first use and reads its registry entry — the
|
|
71
|
-
one statement of the load/lookup failure semantics
|
|
72
|
-
logged here
|
|
73
|
-
|
|
71
|
+
one statement of the load/lookup failure semantics. A throwing module is
|
|
72
|
+
logged here; memoizeByKey evicts the rejected load so the next frame
|
|
73
|
+
retries (a transient import failure doesn't poison the name). Each face
|
|
74
|
+
maps the failure kind to its own rendering: sub emits err+end frames, pub
|
|
74
75
|
drops silently, rest answers with HTTP errors.
|
|
75
76
|
*/
|
|
76
77
|
async function resolveEntry(
|
|
@@ -200,10 +201,7 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
200
201
|
send(ws, { type: 'end', sub: frame.sub })
|
|
201
202
|
}
|
|
202
203
|
|
|
203
|
-
async function handlePub(
|
|
204
|
-
ws: ServerWebSocket<unknown>,
|
|
205
|
-
frame: Extract<SocketClientFrame, { type: 'pub' }>,
|
|
206
|
-
): Promise<void> {
|
|
204
|
+
async function handlePub(frame: Extract<SocketClientFrame, { type: 'pub' }>): Promise<void> {
|
|
207
205
|
const resolution = await resolveEntry(frame.socket)
|
|
208
206
|
if ('failure' in resolution) {
|
|
209
207
|
return
|
|
@@ -233,11 +231,6 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
233
231
|
} catch (publishError) {
|
|
234
232
|
abideLog.error(publishError)
|
|
235
233
|
}
|
|
236
|
-
/*
|
|
237
|
-
ws parameter retained for future per-ws auth context (cookies on
|
|
238
|
-
upgrade) the canPublish hook would consult.
|
|
239
|
-
*/
|
|
240
|
-
void ws
|
|
241
234
|
}
|
|
242
235
|
|
|
243
236
|
/*
|
|
@@ -252,7 +245,8 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
252
245
|
clientPublish policy and validated against its schema.
|
|
253
246
|
|
|
254
247
|
Loads the socket module on first hit (same cache the ws path uses) so
|
|
255
|
-
its defineSocket call populates the registry.
|
|
248
|
+
its defineSocket call populates the registry. A socket exposed to neither
|
|
249
|
+
mcp nor cli answers 404 here — its `clients` flags gate this surface.
|
|
256
250
|
*/
|
|
257
251
|
async function rest(req: Request, name: string): Promise<Response> {
|
|
258
252
|
return socketsLog.trace(`socket-rest ${name}`, () => restImpl(req, name))
|
|
@@ -266,6 +260,13 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
266
260
|
: error(404)
|
|
267
261
|
}
|
|
268
262
|
const { entry } = resolution
|
|
263
|
+
/* The REST face is the CLI/MCP transport (the browser uses the ws multiplex). A
|
|
264
|
+
socket advertised to neither non-browser surface has no REST consumer, so its
|
|
265
|
+
`clients` flags are access control here: 404 as if unmounted, not 403, so a
|
|
266
|
+
browser-only socket's existence doesn't leak over this surface. */
|
|
267
|
+
if (!entry.clients.mcp && !entry.clients.cli) {
|
|
268
|
+
return error(404)
|
|
269
|
+
}
|
|
269
270
|
const tailParam = new URL(req.url).searchParams.get('tail')
|
|
270
271
|
const parsedTail = tailParam !== null ? Number(tailParam) : undefined
|
|
271
272
|
// A non-numeric ?tail= yields NaN; treat it as absent rather than letting
|
|
@@ -338,7 +339,7 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
338
339
|
return
|
|
339
340
|
}
|
|
340
341
|
if (frame.type === 'pub') {
|
|
341
|
-
void handlePub(
|
|
342
|
+
void handlePub(frame)
|
|
342
343
|
return
|
|
343
344
|
}
|
|
344
345
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HttpMethod } from '../../../shared/types/HttpMethod.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
One operation a socket exposes to the non-browser surfaces. A socket
|
|
@@ -18,5 +18,5 @@ export type SocketOperation = {
|
|
|
18
18
|
// HTTP face of the operation: `/__abide/sockets/<name>`.
|
|
19
19
|
restUrl: string
|
|
20
20
|
// GET for tail, POST for publish.
|
|
21
|
-
method:
|
|
21
|
+
method: HttpMethod
|
|
22
22
|
}
|
|
@@ -9,12 +9,26 @@ export class HttpError extends Error {
|
|
|
9
9
|
readonly status: number
|
|
10
10
|
readonly statusText: string
|
|
11
11
|
readonly response: Response
|
|
12
|
+
/* Set when the handler returned a typed error (`error(errors.x(...))`) or a
|
|
13
|
+
validation 422: `kind` is the declared error name (or 'validation'), `data` the
|
|
14
|
+
payload it carried — parsed off the `{ $abideError, data }` body by decodeResponse.
|
|
15
|
+
`data` is typed `unknown` (a throw can't carry the rpc's per-kind type to the
|
|
16
|
+
catch); narrow it yourself — for `kind: 'validation'` the shape is the exported
|
|
17
|
+
`ValidationErrorData` (`{ issues, fields }`). Both undefined for a plain `error(status, text)`.
|
|
18
|
+
The framework also reserves `kind: 'queued'` for a durable (`outbox: true`) call
|
|
19
|
+
parked because the server was unreachable — `data` then holds the parked OutboxEntry,
|
|
20
|
+
so `(error.data as OutboxEntry).settled` awaits the eventual delivered result or
|
|
21
|
+
server refusal (the entry's own `error` carries the underlying cause). */
|
|
22
|
+
readonly kind?: string
|
|
23
|
+
readonly data?: unknown
|
|
12
24
|
|
|
13
|
-
constructor(response: Response) {
|
|
25
|
+
constructor(response: Response, kind?: string, data?: unknown) {
|
|
14
26
|
super(`HTTP ${response.status} ${response.statusText || 'error'}`)
|
|
15
27
|
this.name = 'HttpError'
|
|
16
28
|
this.status = response.status
|
|
17
29
|
this.statusText = response.statusText
|
|
18
30
|
this.response = response
|
|
31
|
+
this.kind = kind
|
|
32
|
+
this.data = data
|
|
19
33
|
}
|
|
20
34
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
HTTP statuses that mean "the server didn't process this request" — the gateway /
|
|
3
|
+
availability family. A response with one of these is treated like a transport failure
|
|
4
|
+
by a durable RPC: the call still throws (the error framework is unchanged), and the
|
|
5
|
+
request is parked for replay on recovery. Everything else (4xx, 500, …) means the
|
|
6
|
+
server received and handled it — that flows to the error framework, never the outbox.
|
|
7
|
+
|
|
8
|
+
502 Bad Gateway · 503 Service Unavailable · 504 Gateway Timeout (abide's own client
|
|
9
|
+
timeout surfaces here too) · 520–527, 530 — Cloudflare/CDN origin-unreachable.
|
|
10
|
+
*/
|
|
11
|
+
export const UNREACHABLE_STATUSES: ReadonlySet<number> = new Set([
|
|
12
|
+
502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 527, 530,
|
|
13
|
+
])
|
|
@@ -2,11 +2,11 @@ import { carriesBodyArgs } from './carriesBodyArgs.ts'
|
|
|
2
2
|
import { encodeRefJson } from './encodeRefJson.ts'
|
|
3
3
|
import { queryStringFromArgs } from './queryStringFromArgs.ts'
|
|
4
4
|
import { REF_JSON_HEADER } from './REF_JSON_HEADER.ts'
|
|
5
|
-
import type {
|
|
5
|
+
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
|
-
Builds the Request a
|
|
9
|
-
both sides (server
|
|
8
|
+
Builds the Request a rpc helper uses to invoke its handler. Same shape on
|
|
9
|
+
both sides (server defineRpc + client remoteProxy) so the cache key
|
|
10
10
|
derivation and SSR snapshot round-trip identically. $rpc URLs are flat
|
|
11
11
|
(no `:name` segments): GET/DELETE/HEAD serialise args onto the query
|
|
12
12
|
string; POST/PUT/PATCH send them as application/json.
|
|
@@ -24,7 +24,7 @@ export function buildRpcRequest({
|
|
|
24
24
|
baseUrl,
|
|
25
25
|
headers,
|
|
26
26
|
}: {
|
|
27
|
-
method:
|
|
27
|
+
method: HttpMethod
|
|
28
28
|
url: string
|
|
29
29
|
args: unknown
|
|
30
30
|
baseUrl: string
|
|
@@ -64,7 +64,7 @@ export function buildRpcRequest({
|
|
|
64
64
|
})
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function appendQuery(method:
|
|
67
|
+
function appendQuery(method: HttpMethod, url: string, args: unknown): string {
|
|
68
68
|
if (args === undefined) {
|
|
69
69
|
return url
|
|
70
70
|
}
|
package/src/lib/shared/cache.ts
CHANGED
|
@@ -180,8 +180,8 @@ export function cache<Args, Return>(
|
|
|
180
180
|
}
|
|
181
181
|
/*
|
|
182
182
|
Warm path: a value pre-decoded onto the entry — by the SSR cache
|
|
183
|
-
snapshot the client seeds its store from
|
|
184
|
-
|
|
183
|
+
snapshot the client seeds its store from — is served without a network
|
|
184
|
+
round-trip. It resolves on a
|
|
185
185
|
microtask (a settled Promise), not synchronously, so every cache() read
|
|
186
186
|
is uniformly `Promise<Return>` and `.then`/`.catch`/`.finally` chain
|
|
187
187
|
cleanly. Raw callers take the Response path; after an invalidate the
|
|
@@ -337,7 +337,7 @@ function invokeRemote<Args>(
|
|
|
337
337
|
const request = getRemoteMeta(promise)
|
|
338
338
|
if (!request) {
|
|
339
339
|
throw new Error(
|
|
340
|
-
'[abide] cache() received a function whose call did not record metadata — was it produced by a
|
|
340
|
+
'[abide] cache() received a function whose call did not record metadata — was it produced by a rpc helper?',
|
|
341
341
|
)
|
|
342
342
|
}
|
|
343
343
|
registerEntry(store, key, promise, options, request, () => rawFn(args as Args))
|
|
@@ -512,24 +512,30 @@ function invalidate<Args, Return>(arg?: CacheSelector<Args, Return>, args?: Args
|
|
|
512
512
|
const matches = selectorMatcher(arg, args, prefix)
|
|
513
513
|
invalidateTripwire(selectorLabel(arg, args, prefix))
|
|
514
514
|
for (const store of cacheStores()) {
|
|
515
|
+
const matched: string[] = []
|
|
515
516
|
const affected: string[] = []
|
|
516
517
|
/* Deleting the current entry mid-iteration is spec-safe on a Map; no snapshot needed. */
|
|
517
518
|
for (const entry of store.entries.values()) {
|
|
518
519
|
if (!matches(entry)) {
|
|
519
520
|
continue
|
|
520
521
|
}
|
|
522
|
+
matched.push(entry.key)
|
|
521
523
|
if (entry.invalidation) {
|
|
522
524
|
scheduleInvalidationRefetch(store, entry)
|
|
523
525
|
} else {
|
|
524
526
|
store.entries.delete(entry.key)
|
|
525
|
-
/*
|
|
526
|
-
|
|
527
|
+
/* Flag the next read a reload (refreshing()) — but only if a reader is
|
|
528
|
+
holding the value now; with none on screen the next read is a first
|
|
529
|
+
load, and an ungated add would linger forever on the tab store. */
|
|
530
|
+
if (store.hasReader(entry.key)) {
|
|
531
|
+
store.pendingRefresh.add(entry.key)
|
|
532
|
+
}
|
|
527
533
|
affected.push(entry.key)
|
|
528
534
|
}
|
|
529
|
-
store.markLifecycle(entry.key)
|
|
530
535
|
}
|
|
531
|
-
|
|
532
|
-
|
|
536
|
+
/* Every match changed state (probes re-derive); only the dropped subset
|
|
537
|
+
changed its visible value (readers re-read) — swr matches stay put. */
|
|
538
|
+
notify(store, matched, affected)
|
|
533
539
|
}
|
|
534
540
|
}
|
|
535
541
|
|
|
@@ -555,94 +561,6 @@ function selectorLabel<Args, Return>(
|
|
|
555
561
|
|
|
556
562
|
cache.invalidate = invalidate
|
|
557
563
|
|
|
558
|
-
type EntryWrite = { store: CacheStore; entry: CacheEntry; prior: unknown; next: unknown }
|
|
559
|
-
|
|
560
|
-
/*
|
|
561
|
-
Core value-fold shared by the authoritative (cache.on context.patch) and
|
|
562
|
-
optimistic (cache.patch) write paths. Folds `updater` into every decoded remote
|
|
563
|
-
entry matching the selector, writing the result to entry.value so the warm-sync
|
|
564
|
-
read path serves it and emitting the keys so readers re-run (ADR-0007). Only
|
|
565
|
-
entry.value is written; entry.promise is left untouched so raw readers of the
|
|
566
|
-
same key keep reading the wire Response. Producer entries (no request) are
|
|
567
|
-
skipped — patching is a decoded-value operation. The current value is entry.value
|
|
568
|
-
when warm (hydrated or already patched), else the entry's settled Response decoded
|
|
569
|
-
— hence async; the first fold of a live-fetched entry hops a decode, subsequent
|
|
570
|
-
ones are synchronous. Returns the keys touched (the cache.on context registers
|
|
571
|
-
them for reconnect resync) and a `restore` that reverts each write iff it still
|
|
572
|
-
stands — the optimistic path runs it on a rejected call, the authoritative path
|
|
573
|
-
discards it (a broadcast is truth, never undone).
|
|
574
|
-
*/
|
|
575
|
-
async function foldEntries<Args, Return>(
|
|
576
|
-
arg: CacheSelector<Args, Return>,
|
|
577
|
-
updater: (current: Return) => Return,
|
|
578
|
-
args: Args | undefined,
|
|
579
|
-
prefix: string | undefined,
|
|
580
|
-
): Promise<{ touched: string[]; restore: () => void }> {
|
|
581
|
-
const matches = selectorMatcher(arg, args, prefix ?? selectorPrefix(arg, args))
|
|
582
|
-
const touched: string[] = []
|
|
583
|
-
const writes: EntryWrite[] = []
|
|
584
|
-
for (const store of cacheStores()) {
|
|
585
|
-
const affected: string[] = []
|
|
586
|
-
for (const entry of store.entries.values()) {
|
|
587
|
-
if (!matches(entry) || entry.request === undefined) {
|
|
588
|
-
continue
|
|
589
|
-
}
|
|
590
|
-
const prior = entry.value
|
|
591
|
-
const current = (prior ??
|
|
592
|
-
(await decodeResponse(
|
|
593
|
-
await shareable(entry.promise as Promise<Response>),
|
|
594
|
-
))) as Return
|
|
595
|
-
const next = structuredClone(updater(current))
|
|
596
|
-
entry.value = next
|
|
597
|
-
entry.settled = true
|
|
598
|
-
entry.refreshing = false
|
|
599
|
-
store.markLifecycle(entry.key)
|
|
600
|
-
affected.push(entry.key)
|
|
601
|
-
writes.push({ store, entry, prior, next })
|
|
602
|
-
}
|
|
603
|
-
emit(store, affected)
|
|
604
|
-
store.markLifecycle()
|
|
605
|
-
touched.push(...affected)
|
|
606
|
-
}
|
|
607
|
-
return { touched, restore: () => revertWrites(writes) }
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/*
|
|
611
|
-
Reverts each optimistic write iff entry.value still holds it — a refetch or a
|
|
612
|
-
later write that already replaced it is the newer truth and is left intact — then
|
|
613
|
-
notifies readers of the reverted keys per store.
|
|
614
|
-
*/
|
|
615
|
-
function revertWrites(writes: EntryWrite[]): void {
|
|
616
|
-
const reverted = new Map<CacheStore, string[]>()
|
|
617
|
-
for (const { store, entry, prior, next } of writes) {
|
|
618
|
-
if (store.entries.get(entry.key) !== entry || entry.value !== next) {
|
|
619
|
-
continue
|
|
620
|
-
}
|
|
621
|
-
entry.value = prior
|
|
622
|
-
store.markLifecycle(entry.key)
|
|
623
|
-
reverted.set(store, [...(reverted.get(store) ?? []), entry.key])
|
|
624
|
-
}
|
|
625
|
-
for (const [store, keys] of reverted) {
|
|
626
|
-
emit(store, keys)
|
|
627
|
-
store.markLifecycle()
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/*
|
|
632
|
-
The authoritative-broadcast fold: a cache.on frame is the truth, so foldEntries'
|
|
633
|
-
write stands (no rollback) and only the touched keys are returned for coverage.
|
|
634
|
-
*/
|
|
635
|
-
async function patchEntries<Args, Return>(
|
|
636
|
-
arg: CacheSelector<Args, Return>,
|
|
637
|
-
updater: (current: Return) => Return,
|
|
638
|
-
args?: Args,
|
|
639
|
-
/* The caller (on().patch) resolves the prefix for its coverage label; reuse it so selectorPrefix runs once per fold. */
|
|
640
|
-
prefix?: string,
|
|
641
|
-
): Promise<string[]> {
|
|
642
|
-
const { touched } = await foldEntries(arg, updater, args, prefix)
|
|
643
|
-
return touched
|
|
644
|
-
}
|
|
645
|
-
|
|
646
564
|
/*
|
|
647
565
|
Event-driven cache maintenance: subscribes to a Subscribable (socket or rpc
|
|
648
566
|
stream) and runs `handler` once per frame — the declarative home for "this
|
|
@@ -685,21 +603,6 @@ function on<T>(
|
|
|
685
603
|
coverage.set(selectorLabel(arg, args), () => invalidate(arg, args))
|
|
686
604
|
invalidate(arg, args)
|
|
687
605
|
},
|
|
688
|
-
/*
|
|
689
|
-
Register the selector (not the delta) for reconnect: a discarded delta
|
|
690
|
-
can't be replayed, so a transport gap resyncs the patched keys by full
|
|
691
|
-
invalidate — reusing the same coverage machinery as invalidate above.
|
|
692
|
-
*/
|
|
693
|
-
patch<Args, Return>(
|
|
694
|
-
arg: CacheSelector<Args, Return>,
|
|
695
|
-
updater: (current: Return) => Return,
|
|
696
|
-
args?: Args,
|
|
697
|
-
): Promise<string[]> {
|
|
698
|
-
/* Resolve the prefix once; the coverage label and patchEntries both consume it. */
|
|
699
|
-
const prefix = selectorPrefix(arg, args)
|
|
700
|
-
coverage.set(selectorLabel(arg, args, prefix), () => invalidate(arg, args))
|
|
701
|
-
return patchEntries(arg, updater, args, prefix)
|
|
702
|
-
},
|
|
703
606
|
signal: controller.signal,
|
|
704
607
|
}
|
|
705
608
|
/* `let`: the reconnect path swaps in a fresh iterator; dispose closes the current one. */
|
|
@@ -741,56 +644,6 @@ function on<T>(
|
|
|
741
644
|
|
|
742
645
|
cache.on = on
|
|
743
646
|
|
|
744
|
-
/*
|
|
745
|
-
Optimistic write: applies `updater` as a prediction now — the reactive read
|
|
746
|
-
shows it immediately — runs `call`, then reconciles. On resolve the server is
|
|
747
|
-
the truth: the prediction is dropped and the selector invalidated, so the value
|
|
748
|
-
refetches authoritatively, coalesced per the read's own swr window
|
|
749
|
-
(cache(fn, { swr: { throttle } }) bounds an optimistic-write storm with no
|
|
750
|
-
extra knob here; without swr it is a plain drop-and-refetch). On reject the
|
|
751
|
-
prediction rolls back. The returned promise is transparent over `call` —
|
|
752
|
-
resolves to `call`'s value (the mutation result, e.g. a created id), rejects
|
|
753
|
-
with its error, settling only after the cache reflects the reconciled state: an
|
|
754
|
-
explicit await reads truth, an ignored call is fire-and-forget (a pre-attached
|
|
755
|
-
catch keeps an un-awaited rejection from surfacing as unhandled, while the await
|
|
756
|
-
still receives it). `call` is required — a global authoritative write with no
|
|
757
|
-
reconciling op would let a caller author cache values, breaking the
|
|
758
|
-
producer-is-the-source invariant (ADR-0001); that path stays cache.on's
|
|
759
|
-
context.patch. Single-flight per key: rollback restores by snapshot, so keep one
|
|
760
|
-
mutation per key in flight (disable the trigger while pending) — concurrent
|
|
761
|
-
same-key optimism wants a layered entry value, deferred (ADR-0009). The reactive
|
|
762
|
-
read holds the value; the return carries the mutation result — separate by
|
|
763
|
-
design (ADR-0009).
|
|
764
|
-
*/
|
|
765
|
-
function patch<Args, Return, Result>(
|
|
766
|
-
arg: CacheSelector<Args, Return>,
|
|
767
|
-
updater: (current: Return) => Return,
|
|
768
|
-
call: Promise<Result>,
|
|
769
|
-
args?: Args,
|
|
770
|
-
): Promise<Result> {
|
|
771
|
-
const prefix = selectorPrefix(arg, args)
|
|
772
|
-
const folded = foldEntries(arg, updater, args, prefix)
|
|
773
|
-
const settled = (async () => {
|
|
774
|
-
try {
|
|
775
|
-
const result = await call
|
|
776
|
-
/* Wait for the prediction to land before reconciling to server truth. */
|
|
777
|
-
await folded
|
|
778
|
-
invalidate(arg, args)
|
|
779
|
-
return result
|
|
780
|
-
} catch (error) {
|
|
781
|
-
const { restore } = await folded
|
|
782
|
-
restore()
|
|
783
|
-
throw error
|
|
784
|
-
}
|
|
785
|
-
})()
|
|
786
|
-
/* Ignore-safe: an un-awaited rejection must not report unhandled; an explicit
|
|
787
|
-
await still receives it (this no-op handler and the await both fire). */
|
|
788
|
-
settled.catch(() => undefined)
|
|
789
|
-
return settled
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
cache.patch = patch
|
|
793
|
-
|
|
794
647
|
/*
|
|
795
648
|
Schedules a coalesced refetch per the entry's swr policy. No window (swr: true):
|
|
796
649
|
fire immediately (throttle defaults to 0, so the leading-edge branch always
|
|
@@ -852,8 +705,9 @@ function fireRefetch(store: CacheStore, entry: CacheEntry): void {
|
|
|
852
705
|
}
|
|
853
706
|
entry.refreshing = true
|
|
854
707
|
policy.lastFiredAt = Date.now()
|
|
855
|
-
/*
|
|
856
|
-
|
|
708
|
+
/* Mark, don't emit: refreshing() re-derives when revalidation begins, but the
|
|
709
|
+
stale value is still on screen — the settle handlers emit once it lands. */
|
|
710
|
+
notify(store, [entry.key], [])
|
|
857
711
|
const inflight = policy.refetch()
|
|
858
712
|
inflight.then(
|
|
859
713
|
(result) => {
|
|
@@ -876,8 +730,8 @@ function fireRefetch(store: CacheStore, entry: CacheEntry): void {
|
|
|
876
730
|
if (entry.ttl !== undefined && entry.ttl !== 0) {
|
|
877
731
|
armTtlExpiry(store, entry, entry.ttl)
|
|
878
732
|
}
|
|
879
|
-
|
|
880
|
-
|
|
733
|
+
/* Fresh value landed — mark and emit so readers re-read. */
|
|
734
|
+
notify(store, [entry.key], [entry.key])
|
|
881
735
|
},
|
|
882
736
|
(error) => {
|
|
883
737
|
entry.refreshing = false
|
|
@@ -925,11 +779,16 @@ so a live read replaces it and surfaces the proper error once.
|
|
|
925
779
|
function settleRefetchFailure(store: CacheStore, entry: CacheEntry, status?: number): void {
|
|
926
780
|
if (status === 404) {
|
|
927
781
|
evictIfCurrent(store, entry)
|
|
928
|
-
|
|
929
|
-
|
|
782
|
+
/* Same reader-gating as invalidate: only flag a reload if one is on screen. */
|
|
783
|
+
if (store.hasReader(entry.key)) {
|
|
784
|
+
store.pendingRefresh.add(entry.key)
|
|
785
|
+
}
|
|
786
|
+
/* Value gone — mark and emit so the next read replaces it. */
|
|
787
|
+
notify(store, [entry.key], [entry.key])
|
|
930
788
|
return
|
|
931
789
|
}
|
|
932
|
-
|
|
790
|
+
/* Mark, don't emit: stale value kept, only the refreshing flag cleared. */
|
|
791
|
+
notify(store, [entry.key], [])
|
|
933
792
|
}
|
|
934
793
|
|
|
935
794
|
/* Folds new tags into an entry's existing set without duplicating them. */
|
|
@@ -970,6 +829,29 @@ function attachPolicy(
|
|
|
970
829
|
entry.invalidation = { refetch, throttle: policy.throttle, debounce: policy.debounce }
|
|
971
830
|
}
|
|
972
831
|
|
|
832
|
+
/*
|
|
833
|
+
The single notification seam, holding the cache's freshness invariant in one
|
|
834
|
+
place instead of by hand at every mutation site. Two reader audiences, two
|
|
835
|
+
channels: every key in `marked` had its state change, so the lifecycle channels
|
|
836
|
+
fire and the pending()/refreshing() probes re-derive; only the keys in `emitted`
|
|
837
|
+
had their VISIBLE value change, so the 'invalidate' event fires and the reading
|
|
838
|
+
scope re-reads. The two sets diverge by design — a refetch start marks but does
|
|
839
|
+
not emit (the stale value is still on screen), an invalidate-drop does both, an
|
|
840
|
+
swr invalidate marks the whole match set while emitting only the dropped subset.
|
|
841
|
+
The trailing store-wide mark fires even when nothing matched, so a bare probe
|
|
842
|
+
still re-derives (to the same value); marks coalesce per microtask, so a key in
|
|
843
|
+
both lists is not double work.
|
|
844
|
+
*/
|
|
845
|
+
function notify(store: CacheStore, marked: string[], emitted: string[]): void {
|
|
846
|
+
marked.forEach((key) => {
|
|
847
|
+
store.markLifecycle(key)
|
|
848
|
+
})
|
|
849
|
+
if (emitted.length > 0) {
|
|
850
|
+
emit(store, emitted)
|
|
851
|
+
}
|
|
852
|
+
store.markLifecycle()
|
|
853
|
+
}
|
|
854
|
+
|
|
973
855
|
function emit(store: CacheStore, keys: string[]): void {
|
|
974
856
|
if (keys.length === 0) {
|
|
975
857
|
return
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
Whether a
|
|
4
|
+
Whether a rpc carries its args in the request body (POST/PUT/PATCH) vs
|
|
5
5
|
on the query string (GET/DELETE/HEAD). Single source for the split so the
|
|
6
6
|
synthesized Request (buildRpcRequest), the handler-side parse (parseArgs),
|
|
7
7
|
the cache key (keyForRemoteCall), and the OpenAPI doc can't disagree.
|
|
8
8
|
*/
|
|
9
|
-
const BODY_METHODS = new Set<
|
|
9
|
+
const BODY_METHODS = new Set<HttpMethod>(['POST', 'PUT', 'PATCH'])
|
|
10
10
|
|
|
11
|
-
export function carriesBodyArgs(method:
|
|
11
|
+
export function carriesBodyArgs(method: HttpMethod): boolean {
|
|
12
12
|
return BODY_METHODS.has(method)
|
|
13
13
|
}
|
|
@@ -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 {
|
|
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
|
*/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Escapes the regex metacharacters in `value` so it can be embedded literally
|
|
3
|
+
inside a `new RegExp(...)` pattern. Shared by the $rpc/$sockets import stripper,
|
|
4
|
+
the resolver plugin's virtual-namespace matcher, and the SSR snippet-call
|
|
5
|
+
rewriter so the same escaping is applied one way everywhere.
|
|
6
|
+
*/
|
|
7
|
+
export function escapeRegex(value: string): string {
|
|
8
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
9
|
+
}
|
|
@@ -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.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The bare leaf filename of a path — directory stripped, extension kept —
|
|
3
|
+
e.g. `users/list.ts` → `list.ts`. The extension-stripping `fileStem` builds on
|
|
4
|
+
this; call sites that match a full filename (`layout.abide`, `page.abide`) use
|
|
5
|
+
it directly so the leaf-grab is written one way.
|
|
6
|
+
*/
|
|
7
|
+
export function fileName(path: string): string {
|
|
8
|
+
return path.split('/').pop() ?? ''
|
|
9
|
+
}
|