@abide/abide 0.46.0 → 0.47.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 +365 -320
- package/CHANGELOG.md +80 -0
- package/README.md +203 -168
- package/package.json +12 -8
- package/src/abideLsp.ts +11 -12
- package/src/abideResolverPlugin.ts +9 -4
- package/src/lib/bundle/disconnected.abide +3 -0
- package/src/lib/cli/printCommandHelp.ts +43 -0
- package/src/lib/cli/printSessionHelp.ts +2 -1
- package/src/lib/cli/{printHelp.ts → printTopLevelHelp.ts} +3 -40
- package/src/lib/cli/runCli.ts +2 -1
- package/src/lib/mcp/buildPrompts.ts +25 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +8 -6
- package/src/lib/mcp/mcpResourceServerSlot.ts +5 -10
- package/src/lib/mcp/mcpSurface.ts +13 -252
- package/src/lib/mcp/mcpTools.ts +187 -0
- package/src/lib/mcp/renderPrompt.ts +25 -0
- package/src/lib/mcp/types/McpSurface.ts +15 -0
- package/src/lib/mcp/types/PromptDescriptor.ts +5 -0
- package/src/lib/mcp/types/PromptMessage.ts +2 -0
- package/src/lib/mcp/types/ToolDescriptor.ts +7 -0
- package/src/lib/mcp/types/ToolResult.ts +2 -0
- package/src/lib/server/agent.ts +1 -1
- package/src/lib/server/jsonl.ts +2 -2
- package/src/lib/server/rpc/defineRpc.ts +5 -0
- package/src/lib/server/runtime/containsTraversal.ts +21 -17
- package/src/lib/server/runtime/createAppAssetServer.ts +11 -16
- package/src/lib/server/runtime/createServer.ts +101 -71
- package/src/lib/server/runtime/createUiPageRenderer.ts +23 -4
- package/src/lib/server/runtime/devClientFingerprint.ts +3 -3
- package/src/lib/server/runtime/devHotModuleResponse.ts +2 -1
- package/src/lib/server/runtime/finalizeResponse.ts +32 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +8 -17
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +28 -5
- package/src/lib/server/sockets/defineSocket.ts +26 -7
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +1 -1
- package/src/lib/server/sockets/types/SocketRoutes.ts +1 -1
- package/src/lib/server/sse.ts +2 -2
- package/src/lib/shared/DEFER.ts +8 -0
- package/src/lib/shared/activeCacheStore.ts +2 -2
- package/src/lib/shared/activePage.ts +2 -2
- package/src/lib/shared/attachRpcSelectorMethods.ts +42 -0
- package/src/lib/shared/attachSocketSelectorMethods.ts +34 -0
- package/src/lib/shared/basePath.ts +2 -2
- package/src/lib/shared/baseSlot.ts +6 -5
- package/src/lib/shared/bodyValueForKind.ts +1 -2
- package/src/lib/shared/buildSocketOverChannel.ts +40 -4
- package/src/lib/shared/cache.ts +477 -110
- package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
- package/src/lib/shared/cacheStoreSlot.ts +9 -5
- package/src/lib/{server/runtime → shared}/createReachable.ts +2 -2
- package/src/lib/shared/createRemoteFunction.ts +58 -9
- package/src/lib/shared/createResolverSlot.ts +24 -31
- package/src/lib/shared/createSocketSubRegistry.ts +2 -2
- package/src/lib/shared/decodeRefJson.ts +4 -2
- package/src/lib/shared/decodeResponse.ts +8 -6
- package/src/lib/shared/done.ts +14 -0
- package/src/lib/shared/encodeRefJson.ts +4 -2
- package/src/lib/shared/globalCacheStoreSlot.ts +9 -6
- package/src/lib/shared/hydratingSlot.ts +12 -0
- package/src/lib/shared/isLayoutFile.ts +12 -0
- package/src/lib/shared/keyForRemoteCall.ts +2 -1
- package/src/lib/shared/keyPrefixForRemote.ts +12 -0
- package/src/lib/shared/matchRoute.ts +175 -0
- package/src/lib/shared/normalizePathname.ts +11 -0
- package/src/lib/shared/pageSlot.ts +14 -5
- package/src/lib/shared/pageUrlForFile.ts +3 -3
- package/src/lib/shared/parseRouteSegments.ts +16 -7
- package/src/lib/shared/patch.ts +41 -0
- package/src/lib/shared/peek.ts +35 -0
- package/src/lib/shared/prepareRemoteExport.ts +40 -0
- package/src/lib/shared/prepareRpcModule.ts +98 -16
- package/src/lib/shared/prepareSocketModule.ts +11 -15
- package/src/lib/shared/queryStringFromArgs.ts +11 -0
- package/src/lib/shared/reachable.ts +102 -0
- package/src/lib/shared/refresh.ts +22 -0
- package/src/lib/shared/requestScopeSlot.ts +10 -7
- package/src/lib/shared/routeParamsShape.ts +9 -9
- package/src/lib/shared/rpcErrorRegistry.ts +69 -0
- package/src/lib/shared/selectorPrefix.ts +3 -10
- package/src/lib/shared/setOwnProperty.ts +19 -0
- package/src/lib/shared/snippet.ts +1 -1
- package/src/lib/shared/subscribableProbes.ts +111 -0
- package/src/lib/shared/tailProbeSlot.ts +8 -1
- package/src/lib/shared/types/CacheEntry.ts +9 -15
- package/src/lib/shared/types/CacheOptions.ts +8 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
- package/src/lib/shared/types/RemoteCallable.ts +25 -7
- package/src/lib/shared/types/RemoteFunction.ts +48 -13
- package/src/lib/shared/types/ResolverSlot.ts +7 -4
- package/src/lib/shared/types/RpcError.ts +26 -0
- package/src/lib/shared/types/SmartReadOptions.ts +32 -0
- package/src/lib/shared/types/Socket.ts +54 -0
- package/src/lib/shared/types/SsrBootState.ts +27 -0
- package/src/lib/shared/types/SsrPayload.ts +21 -0
- package/src/lib/shared/types/Subscribable.ts +13 -13
- package/src/lib/shared/types/TailHooks.ts +2 -1
- package/src/lib/shared/url.ts +29 -14
- package/src/lib/shared/wakeHydrationPeeks.ts +20 -0
- package/src/lib/shared/writeTestSocketsDts.ts +1 -1
- package/src/lib/test/createScriptedSurface.ts +1 -1
- package/src/lib/test/createTestApp.ts +2 -2
- package/src/lib/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +5 -6
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +10 -7
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -2
- package/src/lib/ui/compile/analyzeComponent.ts +28 -6
- package/src/lib/ui/compile/compileModule.ts +137 -11
- package/src/lib/ui/compile/compileShadow.ts +101 -84
- package/src/lib/ui/compile/desugarSignals.ts +120 -107
- package/src/lib/ui/compile/generateBuild.ts +48 -16
- package/src/lib/ui/compile/generateSSR.ts +117 -14
- package/src/lib/ui/compile/identifierReferencePattern.ts +13 -0
- package/src/lib/ui/compile/lowerContext.ts +10 -4
- package/src/lib/ui/compile/lowerScript.ts +72 -5
- package/src/lib/ui/compile/parseTemplate.ts +1 -14
- package/src/lib/ui/compile/prepareNestedScript.ts +31 -17
- package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
- package/src/lib/ui/compile/signalCallee.ts +24 -0
- package/src/lib/ui/compile/stripEffects.ts +14 -11
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +0 -5
- package/src/lib/ui/dom/appendStatic.ts +9 -1
- package/src/lib/ui/dom/appendText.ts +15 -3
- package/src/lib/ui/dom/assertClaimedText.ts +20 -0
- package/src/lib/ui/dom/awaitBlock.ts +19 -83
- package/src/lib/ui/dom/bindSelectValue.ts +48 -0
- package/src/lib/ui/dom/each.ts +12 -1
- package/src/lib/ui/dom/hydrate.ts +10 -0
- package/src/lib/ui/dom/mountChild.ts +2 -5
- package/src/lib/ui/dom/mountRange.ts +0 -75
- package/src/lib/ui/effect.ts +4 -3
- package/src/lib/ui/installHotBridge.ts +4 -2
- package/src/lib/ui/remoteProxy.ts +18 -2
- package/src/lib/ui/renderToStream.ts +9 -13
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/router.ts +21 -2
- package/src/lib/ui/runtime/RESUME.ts +0 -6
- package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
- package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
- package/src/lib/ui/scope.ts +6 -3
- package/src/lib/ui/seedBootState.ts +53 -0
- package/src/lib/ui/socketChannel.ts +2 -2
- package/src/lib/ui/socketProxy.ts +9 -3
- package/src/lib/ui/startClient.ts +16 -30
- package/src/lib/ui/state.ts +44 -13
- package/src/lib/ui/sync.ts +11 -5
- package/src/lib/ui/tryEncodeResume.ts +2 -5
- package/src/lib/ui/types/Scope.ts +14 -10
- package/src/lib/ui/watch.ts +140 -0
- package/src/serverEntry.ts +6 -6
- package/template/CLAUDE.md +1 -1
- package/template/package.json +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +2 -2
- package/template/test/app.test.ts +1 -1
- package/src/lib/server/reachable.ts +0 -45
- package/src/lib/server/sockets/types/Socket.ts +0 -23
- package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
- package/src/lib/shared/DEFER_MIN_ARRAY_LENGTH.ts +0 -14
- package/src/lib/shared/baseResolver.ts +0 -10
- package/src/lib/shared/cacheKeyOf.ts +0 -7
- package/src/lib/shared/cacheKeyStore.ts +0 -8
- package/src/lib/shared/cacheStoreResolver.ts +0 -12
- package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
- package/src/lib/shared/pageResolver.ts +0 -16
- package/src/lib/shared/recordCacheKey.ts +0 -6
- package/src/lib/shared/requestScopeResolver.ts +0 -12
- package/src/lib/shared/setBaseResolver.ts +0 -4
- package/src/lib/shared/setCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setPageResolver.ts +0 -4
- package/src/lib/shared/setRequestScopeResolver.ts +0 -4
- package/src/lib/shared/toBunRoutePattern.ts +0 -28
- package/src/lib/shared/types/TailOptions.ts +0 -10
- package/src/lib/ui/deferResume.ts +0 -36
- package/src/lib/ui/dom/firstElementBetween.ts +0 -14
- package/src/lib/ui/matchRoute.ts +0 -106
- package/src/lib/ui/runtime/scheduleWake.ts +0 -28
- package/src/lib/ui/runtime/whenIdle.ts +0 -21
- package/src/lib/ui/runtime/whenVisible.ts +0 -105
- package/src/lib/ui/tail.ts +0 -324
- /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
- /package/src/lib/{server/sockets → shared}/types/SocketServerFrame.ts +0 -0
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Maps a page-relative path (under `src/ui/pages/`) to its URL route. Pages are
|
|
3
3
|
folder-based: every leaf is `page.abide` or `layout.abide`, and the URL
|
|
4
4
|
is the directory path. Pages mount at the directory path; layouts mount at
|
|
5
|
-
the directory prefix. Dynamic segments keep their `[name]` / `[...rest]`
|
|
6
|
-
shape —
|
|
7
|
-
|
|
5
|
+
the directory prefix. Dynamic segments keep their `[name]` / `[[name]]` / `[...rest]`
|
|
6
|
+
shape — the shared matchRoute resolves them at dispatch on both sides;
|
|
7
|
+
consumers see the readable form in `page.route`.
|
|
8
8
|
*/
|
|
9
9
|
export function pageUrlForFile(relPath: string): string {
|
|
10
10
|
const segments = relPath.split('/')
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
export type RouteSegment =
|
|
2
2
|
| { kind: 'literal'; value: string }
|
|
3
|
-
| { kind: 'param'; name: string; catchAll: boolean }
|
|
3
|
+
| { kind: 'param'; name: string; catchAll: boolean; optional: boolean }
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
6
|
Splits a abide route URL into typed segments. `[name]` becomes a param,
|
|
7
|
-
`[...rest]`
|
|
8
|
-
by
|
|
9
|
-
(client-side `Routes` type augmentation) so the
|
|
10
|
-
on what counts as a param.
|
|
7
|
+
`[[name]]` an optional param, `[...rest]` a catch-all param, anything else a
|
|
8
|
+
literal. Used by matchRoute (both the server's fetch dispatch and the client
|
|
9
|
+
router) and writeRoutesDts (client-side `Routes` type augmentation) so the
|
|
10
|
+
consumers can't drift on what counts as a param.
|
|
11
11
|
*/
|
|
12
12
|
export function parseRouteSegments(routeUrl: string): RouteSegment[] {
|
|
13
13
|
return routeUrl.split('/').map((segment) => {
|
|
14
|
+
if (segment.startsWith('[[') && segment.endsWith(']]')) {
|
|
15
|
+
const name = segment.slice(2, -2)
|
|
16
|
+
/* `[[...rest]]` is redundant — a catch-all already matches zero
|
|
17
|
+
segments — but normalizing beats mis-parsing brackets into the name. */
|
|
18
|
+
if (name.startsWith('...')) {
|
|
19
|
+
return { kind: 'param', name: name.slice(3), catchAll: true, optional: false }
|
|
20
|
+
}
|
|
21
|
+
return { kind: 'param', name, catchAll: false, optional: true }
|
|
22
|
+
}
|
|
14
23
|
if (segment.startsWith('[...') && segment.endsWith(']')) {
|
|
15
|
-
return { kind: 'param', name: segment.slice(4, -1), catchAll: true }
|
|
24
|
+
return { kind: 'param', name: segment.slice(4, -1), catchAll: true, optional: false }
|
|
16
25
|
}
|
|
17
26
|
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
18
|
-
return { kind: 'param', name: segment.slice(1, -1), catchAll: false }
|
|
27
|
+
return { kind: 'param', name: segment.slice(1, -1), catchAll: false, optional: false }
|
|
19
28
|
}
|
|
20
29
|
return { kind: 'literal', value: segment }
|
|
21
30
|
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cache } from './cache.ts'
|
|
2
|
+
import type { CacheOptions } from './types/CacheOptions.ts'
|
|
3
|
+
import type { CacheSelector } from './types/CacheSelector.ts'
|
|
4
|
+
import type { RemoteFunction } from './types/RemoteFunction.ts'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Mutate the retained value of the matching cached read(s) in place — reactive
|
|
8
|
+
(readers re-render), no network. The optimistic-update / real-time primitive:
|
|
9
|
+
patch a cached list from a socket frame (`on(chat, m => getList.patch(l => [...l,
|
|
10
|
+
m]))`) or apply an optimistic edit before a write lands. The updater receives the
|
|
11
|
+
current decoded value and returns the next.
|
|
12
|
+
|
|
13
|
+
patch(getFoo, args, updater) → that exact call
|
|
14
|
+
patch(getFoo, updater) → every args-variant of that rpc
|
|
15
|
+
patch({ tags }, updater) → every entry sharing a tag
|
|
16
|
+
|
|
17
|
+
Instance sugar `getFoo.patch(args?, updater)` ≡ `patch(getFoo, args, updater)`.
|
|
18
|
+
The updater is always the last argument; a not-yet-read key has nothing to patch.
|
|
19
|
+
*/
|
|
20
|
+
// @documentation cache
|
|
21
|
+
export function patch<Args, Return>(
|
|
22
|
+
fn: RemoteFunction<Args, Return> | ((args?: Args) => Promise<Return>),
|
|
23
|
+
args: Args | undefined,
|
|
24
|
+
updater: (current: Return) => Return,
|
|
25
|
+
): void
|
|
26
|
+
export function patch<Args, Return>(
|
|
27
|
+
fn: RemoteFunction<Args, Return> | ((args?: Args) => Promise<Return>),
|
|
28
|
+
updater: (current: Return) => Return,
|
|
29
|
+
): void
|
|
30
|
+
export function patch<Return>(
|
|
31
|
+
selector: Pick<CacheOptions, 'tags'>,
|
|
32
|
+
updater: (current: Return) => Return,
|
|
33
|
+
): void
|
|
34
|
+
export function patch(selector: unknown, argsOrUpdater: unknown, maybeUpdater?: unknown): void {
|
|
35
|
+
/* Updater is always last: 3 args → (selector, args, updater); 2 args → (selector, updater). */
|
|
36
|
+
const updater = (maybeUpdater !== undefined ? maybeUpdater : argsOrUpdater) as (
|
|
37
|
+
current: unknown,
|
|
38
|
+
) => unknown
|
|
39
|
+
const args = maybeUpdater !== undefined ? argsOrUpdater : undefined
|
|
40
|
+
cache.patch(selector as CacheSelector<unknown, unknown>, args, updater)
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cache } from './cache.ts'
|
|
2
|
+
import type { RemoteFunction } from './types/RemoteFunction.ts'
|
|
3
|
+
import type { Socket } from './types/Socket.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
The value member of the probe family: the currently-retained value, synchronously,
|
|
7
|
+
without triggering anything — `T | undefined` (undefined when nothing is retained
|
|
8
|
+
yet). For a cached read it is the retained cache value (reactive in a tracking
|
|
9
|
+
scope — re-runs when a refresh lands or a patch mutates it; a one-shot snapshot
|
|
10
|
+
otherwise). For a subscribable (socket / stream) it is the latest frame, read off
|
|
11
|
+
the source's own `.peek()`. Instance sugar `getFoo.peek(args?)` ≡ `peek(getFoo,
|
|
12
|
+
args?)`, `socket.peek()` ≡ `peek(socket)`.
|
|
13
|
+
|
|
14
|
+
peek(getFoo, args?) → the retained value for that call
|
|
15
|
+
peek(socket) → the latest frame
|
|
16
|
+
*/
|
|
17
|
+
// @documentation probes
|
|
18
|
+
export function peek<Args, Return>(
|
|
19
|
+
fn: RemoteFunction<Args, Return>,
|
|
20
|
+
args?: Args,
|
|
21
|
+
): Return | undefined
|
|
22
|
+
export function peek<T>(source: Socket<T>): T | undefined
|
|
23
|
+
export function peek(source: unknown, args?: unknown): unknown {
|
|
24
|
+
/* A subscribable (socket/stream) carries its own latest-frame probe; an rpc does not
|
|
25
|
+
have Symbol.asyncIterator, so this cleanly splits the two even though both expose a
|
|
26
|
+
`.peek` method. */
|
|
27
|
+
if (
|
|
28
|
+
source !== null &&
|
|
29
|
+
typeof source === 'object' &&
|
|
30
|
+
typeof (source as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function'
|
|
31
|
+
) {
|
|
32
|
+
return (source as { peek: () => unknown }).peek()
|
|
33
|
+
}
|
|
34
|
+
return cache.peek(source as RemoteFunction<unknown, unknown>, args as never)
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ExportCallSite, findExportCallSite } from './findExportCallSite.ts'
|
|
2
|
+
import { importNamesToStrip } from './importNamesToStrip.ts'
|
|
3
|
+
import { stripImport } from './stripImport.ts'
|
|
4
|
+
|
|
5
|
+
export type RemoteExport = {
|
|
6
|
+
/* The source with the user's dead server-helper import removed. */
|
|
7
|
+
stripped: string
|
|
8
|
+
/* The one exported `helper(...)` call the surface must rewrite. */
|
|
9
|
+
site: ExportCallSite
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
The scan-once envelope every file-based remote surface ($rpc, $sockets) shares:
|
|
14
|
+
strip the user's server-helper import under both the project's chosen name and
|
|
15
|
+
the canonical package name (so the dead import can't side-effect-load the stub
|
|
16
|
+
into the server bundle), then locate the single exported helper call. Each
|
|
17
|
+
surface passes the subpaths it places under a name (`server/GET`…/`server/socket`)
|
|
18
|
+
and the ident predicate that recognizes its call; the character-by-character
|
|
19
|
+
walk still happens exactly once. Returns undefined when the module declares no
|
|
20
|
+
matching export (a plain `.ts` helper alongside the surface), and throws the
|
|
21
|
+
surface's own single-export error when it declares more than one.
|
|
22
|
+
*/
|
|
23
|
+
export function prepareRemoteExport(
|
|
24
|
+
source: string,
|
|
25
|
+
importName: string,
|
|
26
|
+
subpathsForName: (name: string) => string[],
|
|
27
|
+
identMatches: (ident: string) => boolean,
|
|
28
|
+
singleExportError: string,
|
|
29
|
+
): RemoteExport | undefined {
|
|
30
|
+
const stripped = importNamesToStrip(importName).reduce(
|
|
31
|
+
(current, name) =>
|
|
32
|
+
subpathsForName(name).reduce((acc, subpath) => stripImport(acc, subpath), current),
|
|
33
|
+
source,
|
|
34
|
+
)
|
|
35
|
+
const site = findExportCallSite(stripped, identMatches, singleExportError)
|
|
36
|
+
if (!site) {
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
return { stripped, site }
|
|
40
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { findExportCallSite } from './findExportCallSite.ts'
|
|
2
|
-
import { importNamesToStrip } from './importNamesToStrip.ts'
|
|
3
1
|
import { isReadOnlyMethod } from './isReadOnlyMethod.ts'
|
|
2
|
+
import { prepareRemoteExport } from './prepareRemoteExport.ts'
|
|
4
3
|
import { skipNonCode } from './skipNonCode.ts'
|
|
5
|
-
import { stripImport } from './stripImport.ts'
|
|
6
4
|
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
7
5
|
|
|
8
6
|
const RPC_NAMES = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] as const
|
|
@@ -15,6 +13,9 @@ export type PreparedRpcModule = {
|
|
|
15
13
|
method: HttpMethod
|
|
16
14
|
/* `outbox: true` in the opts — the client proxy is emitted durable. */
|
|
17
15
|
durable: boolean
|
|
16
|
+
/* The handler calls jsonl()/sse() — the client proxy is emitted streaming (bare call
|
|
17
|
+
returns the Subscribable). Congruent with the RemoteCallable conditional by construction. */
|
|
18
|
+
streaming: boolean
|
|
18
19
|
exportName: string
|
|
19
20
|
rewriteForServer: (url: string) => string
|
|
20
21
|
}
|
|
@@ -52,32 +53,43 @@ export function prepareRpcModule(
|
|
|
52
53
|
): PreparedRpcModule | undefined {
|
|
53
54
|
/*
|
|
54
55
|
The "no barrels" surface places each method at its own path
|
|
55
|
-
(`abide/server/GET`, `abide/server/POST`, …)
|
|
56
|
-
the user's method import
|
|
57
|
-
stub module into the server bundle.
|
|
58
|
-
project's chosen name or the canonical package name, so strip both.
|
|
56
|
+
(`abide/server/GET`, `abide/server/POST`, …), so the strip subpaths are
|
|
57
|
+
every method under each import name — the user's method import must not
|
|
58
|
+
linger and side-effect-load the stub module into the server bundle.
|
|
59
59
|
*/
|
|
60
|
-
const
|
|
61
|
-
(current, name) =>
|
|
62
|
-
RPC_NAMES.reduce(
|
|
63
|
-
(acc, method) => stripImport(acc, `${name}/server/${method}`),
|
|
64
|
-
current,
|
|
65
|
-
),
|
|
60
|
+
const prepared = prepareRemoteExport(
|
|
66
61
|
source,
|
|
62
|
+
importName,
|
|
63
|
+
(name) => RPC_NAMES.map((method) => `${name}/server/${method}`),
|
|
64
|
+
(ident) => RPC_SET.has(ident),
|
|
65
|
+
SINGLE_EXPORT_ERROR,
|
|
67
66
|
)
|
|
68
|
-
|
|
69
|
-
if (!site) {
|
|
67
|
+
if (!prepared) {
|
|
70
68
|
return undefined
|
|
71
69
|
}
|
|
70
|
+
const { stripped, site } = prepared
|
|
72
71
|
const method = site.ident as HttpMethod
|
|
73
72
|
const durable = detectDurable(stripped, site.parenStart, site.parenEnd, method)
|
|
73
|
+
const streaming = detectStreaming(stripped, site.parenStart, site.parenEnd)
|
|
74
74
|
return {
|
|
75
75
|
method,
|
|
76
76
|
durable,
|
|
77
|
+
streaming,
|
|
77
78
|
exportName: site.exportName,
|
|
78
79
|
rewriteForServer(url: string): string {
|
|
79
80
|
const binding = `__abideDefineRpc__(${JSON.stringify(method)}, ${JSON.stringify(url)}, `
|
|
80
|
-
|
|
81
|
+
const head = stripped.slice(0, site.callStart) + binding
|
|
82
|
+
if (!streaming) {
|
|
83
|
+
return head + stripped.slice(site.parenStart + 1)
|
|
84
|
+
}
|
|
85
|
+
/* Inject the build-time streaming flag into the handler's opts, preserving any author
|
|
86
|
+
opts by spread. Split the call's top-level args (handler + optional opts) — dropping
|
|
87
|
+
a trailing-comma empty part — so a trailing comma or absent opts can't produce
|
|
88
|
+
`...()`. `head` ends after the METHOD( paren; keep everything from `)` onward. */
|
|
89
|
+
const parts = splitTopLevelArgs(stripped, site.parenStart, site.parenEnd)
|
|
90
|
+
const [handler, opts] = parts
|
|
91
|
+
const injected = opts ? `{ streaming: true, ...(${opts}) }` : '{ streaming: true }'
|
|
92
|
+
return `${head}${handler}, ${injected}${stripped.slice(site.parenEnd)}`
|
|
81
93
|
},
|
|
82
94
|
}
|
|
83
95
|
}
|
|
@@ -111,6 +123,76 @@ function detectDurable(
|
|
|
111
123
|
return durable
|
|
112
124
|
}
|
|
113
125
|
|
|
126
|
+
const STREAM_HELPERS = new Set(['jsonl', 'sse'])
|
|
127
|
+
const IDENT_START = /[A-Za-z_$]/
|
|
128
|
+
const IDENT_PART = /[A-Za-z0-9_$]/
|
|
129
|
+
|
|
130
|
+
/* True when the handler returns a streaming body — it calls abide's jsonl()/sse(), the only
|
|
131
|
+
constructors of a `TypedResponse<AsyncIterable<Frame>>`. Congruent by construction with the
|
|
132
|
+
RemoteCallable conditional (Return is an AsyncIterable iff the handler calls jsonl/sse). Scans
|
|
133
|
+
the whole call-arg region depth-blind (the opts never mention jsonl/sse), skipping
|
|
134
|
+
strings/comments/regex via skipNonCode so a mention in a literal can't misfire. Same
|
|
135
|
+
literal-only limit as `outbox`: an indirection through a wrapper function isn't seen. */
|
|
136
|
+
function detectStreaming(source: string, parenStart: number, parenEnd: number): boolean {
|
|
137
|
+
let i = parenStart + 1
|
|
138
|
+
while (i < parenEnd) {
|
|
139
|
+
const skipped = skipNonCode(source, i)
|
|
140
|
+
if (skipped !== undefined) {
|
|
141
|
+
i = skipped
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (IDENT_START.test(source[i] ?? '') && !IDENT_PART.test(source[i - 1] ?? '')) {
|
|
145
|
+
let j = i + 1
|
|
146
|
+
while (j < parenEnd && IDENT_PART.test(source[j] ?? '')) {
|
|
147
|
+
j += 1
|
|
148
|
+
}
|
|
149
|
+
if (STREAM_HELPERS.has(source.slice(i, j))) {
|
|
150
|
+
let k = j
|
|
151
|
+
while (k < parenEnd && /\s/.test(source[k] ?? '')) {
|
|
152
|
+
k += 1
|
|
153
|
+
}
|
|
154
|
+
/* `jsonl(` a call, `jsonl<` a generic call — either is the streaming constructor. */
|
|
155
|
+
if (source[k] === '(' || source[k] === '<') {
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
i = j
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
i += 1
|
|
163
|
+
}
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* The call's top-level arguments, trimmed, with empty parts (a trailing comma) dropped —
|
|
168
|
+
`[handler]` or `[handler, opts]`. Depth-aware and skips strings/comments/regex so commas
|
|
169
|
+
inside the handler body or opts don't miscount. */
|
|
170
|
+
function splitTopLevelArgs(source: string, parenStart: number, parenEnd: number): string[] {
|
|
171
|
+
const parts: string[] = []
|
|
172
|
+
let depth = 0
|
|
173
|
+
let start = parenStart + 1
|
|
174
|
+
let i = parenStart + 1
|
|
175
|
+
while (i < parenEnd) {
|
|
176
|
+
const skipped = skipNonCode(source, i)
|
|
177
|
+
if (skipped !== undefined) {
|
|
178
|
+
i = skipped
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
const c = source[i]
|
|
182
|
+
if (c === '(' || c === '{' || c === '[') {
|
|
183
|
+
depth += 1
|
|
184
|
+
} else if (c === ')' || c === '}' || c === ']') {
|
|
185
|
+
depth -= 1
|
|
186
|
+
} else if (c === ',' && depth === 0) {
|
|
187
|
+
parts.push(source.slice(start, i).trim())
|
|
188
|
+
start = i + 1
|
|
189
|
+
}
|
|
190
|
+
i += 1
|
|
191
|
+
}
|
|
192
|
+
parts.push(source.slice(start, parenEnd).trim())
|
|
193
|
+
return parts.filter((part) => part.length > 0)
|
|
194
|
+
}
|
|
195
|
+
|
|
114
196
|
/*
|
|
115
197
|
The text of the call's final argument — the opts object for a `METHOD(handler, opts)` call.
|
|
116
198
|
Walks the arg list depth-aware, skipping strings / templates / comments / regex (skipNonCode)
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { importNamesToStrip } from './importNamesToStrip.ts'
|
|
3
|
-
import { stripImport } from './stripImport.ts'
|
|
1
|
+
import { prepareRemoteExport } from './prepareRemoteExport.ts'
|
|
4
2
|
|
|
5
3
|
const SINGLE_EXPORT_ERROR =
|
|
6
4
|
'[abide] $sockets module contains more than one `socket(...)` export — each file must declare exactly one socket'
|
|
@@ -14,27 +12,25 @@ export type PreparedSocketModule = {
|
|
|
14
12
|
Scans a `$sockets/**` module once and returns its declared export name
|
|
15
13
|
plus a closure that, given the socket name, emits the server-side
|
|
16
14
|
rewrite (`__abideDefineSocket__("<name>"[, opts])` spliced into the
|
|
17
|
-
original source). The
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
original source). The strip + find envelope is the shared prepareRemoteExport
|
|
16
|
+
scan; this module only supplies the socket import subpath and ident, then
|
|
17
|
+
splices its own binding.
|
|
20
18
|
*/
|
|
21
19
|
export function prepareSocketModule(
|
|
22
20
|
source: string,
|
|
23
21
|
importName: string,
|
|
24
22
|
): PreparedSocketModule | undefined {
|
|
25
|
-
|
|
26
|
-
Strip the user's `socket` import under the project's chosen name and the
|
|
27
|
-
canonical package name so the dead import can't side-effect-load the
|
|
28
|
-
socket helper into the server bundle.
|
|
29
|
-
*/
|
|
30
|
-
const stripped = importNamesToStrip(importName).reduce(
|
|
31
|
-
(current, name) => stripImport(current, `${name}/server/socket`),
|
|
23
|
+
const prepared = prepareRemoteExport(
|
|
32
24
|
source,
|
|
25
|
+
importName,
|
|
26
|
+
(name) => [`${name}/server/socket`],
|
|
27
|
+
(ident) => ident === 'socket',
|
|
28
|
+
SINGLE_EXPORT_ERROR,
|
|
33
29
|
)
|
|
34
|
-
|
|
35
|
-
if (!site) {
|
|
30
|
+
if (!prepared) {
|
|
36
31
|
return undefined
|
|
37
32
|
}
|
|
33
|
+
const { stripped, site } = prepared
|
|
38
34
|
return {
|
|
39
35
|
exportName: site.exportName,
|
|
40
36
|
rewriteForServer(name: string): string {
|
|
@@ -11,6 +11,10 @@ is stable under arg reordering. Returns the query body without a leading
|
|
|
11
11
|
walk — no entries / filtered / URLSearchParams intermediates — to keep the
|
|
12
12
|
hot GET-cache path allocation-light. Callers validate that `args` is a
|
|
13
13
|
plain object first; this assumes that.
|
|
14
|
+
|
|
15
|
+
A non-primitive VALUE throws: String() would flatten every object to
|
|
16
|
+
'[object Object]', so distinct calls would collide onto one request URL and
|
|
17
|
+
one cache key — the first call's data silently served for the second.
|
|
14
18
|
*/
|
|
15
19
|
export function queryStringFromArgs(args: Record<string, unknown>, sort: boolean): string {
|
|
16
20
|
const keys = sort ? Object.keys(args).sort() : Object.keys(args)
|
|
@@ -20,6 +24,13 @@ export function queryStringFromArgs(args: Record<string, unknown>, sort: boolean
|
|
|
20
24
|
if (value === undefined) {
|
|
21
25
|
continue
|
|
22
26
|
}
|
|
27
|
+
const kind = typeof value
|
|
28
|
+
if (value !== null && (kind === 'object' || kind === 'function' || kind === 'symbol')) {
|
|
29
|
+
const got = Array.isArray(value) ? 'array' : kind
|
|
30
|
+
throw new TypeError(
|
|
31
|
+
`[abide] query arg "${key}" must be a primitive — got ${got} (query-carrying methods can't encode structured values; move the arg to a POST body)`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
23
34
|
query += query ? '&' : ''
|
|
24
35
|
query += `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
|
|
25
36
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createReachable } from './createReachable.ts'
|
|
2
|
+
import { parseBoundedEnvInt } from './parseBoundedEnvInt.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Isomorphic outbound reachability. `await reachable(host)` HEADs the host's
|
|
6
|
+
origin: the first call awaits a real probe (faithful — a down host costs the
|
|
7
|
+
full timeout, an up host one handshake) and starts a background poll that
|
|
8
|
+
re-probes every TTL, so every later call resolves instantly off the warm
|
|
9
|
+
value, fresh within one TTL. A host going down is caught within ~failureLimit
|
|
10
|
+
polls; recovery flips it back automatically.
|
|
11
|
+
|
|
12
|
+
if (!(await reachable('api.example.com'))) return error(503)
|
|
13
|
+
|
|
14
|
+
No host asks about the app's own backend: constant true on the server (the
|
|
15
|
+
server IS the backend — health()'s rule) and on a loopback origin (`abide
|
|
16
|
+
dev`, a desktop bundle's embedded server — this machine, reachable by
|
|
17
|
+
construction even with the network gone); a deployed origin probes like any
|
|
18
|
+
other host.
|
|
19
|
+
|
|
20
|
+
A bare host defaults to https; pass an explicit http://… for a non-TLS host.
|
|
21
|
+
Answers "can I connect to this host," NOT "is my endpoint healthy": any
|
|
22
|
+
completed HTTP response (even 4xx/5xx, even a 405 to HEAD) counts as reachable;
|
|
23
|
+
only a connection failure or timeout reads as unreachable.
|
|
24
|
+
|
|
25
|
+
Same callable both sides. The server has no ambient connectivity signal, so
|
|
26
|
+
this is the honest way to fail a doomed outbound call fast — see online() for
|
|
27
|
+
the inbound/client-reported counterpart. The browser probes no-cors (a
|
|
28
|
+
completed opaque response proves connectivity without the host's CORS
|
|
29
|
+
blessing) and composes navigator.onLine in at read time, so a lost network
|
|
30
|
+
reports false instantly instead of waiting out the warm value — except for
|
|
31
|
+
loopback hosts, which need no network.
|
|
32
|
+
|
|
33
|
+
ABIDE_REACHABLE_TTL (poll cadence / freshness, ms) and ABIDE_REACHABLE_TIMEOUT
|
|
34
|
+
(per-HEAD bound, ms) tune the server defaults; the browser has no env, so it
|
|
35
|
+
runs the defaults. The timeout is deliberately generous so a healthy-but-
|
|
36
|
+
distant host over a slow link is not mis-read as down.
|
|
37
|
+
*/
|
|
38
|
+
const env = typeof process === 'undefined' ? undefined : process.env
|
|
39
|
+
const TTL_MS = parseBoundedEnvInt(env?.ABIDE_REACHABLE_TTL, 1_000, 600_000) ?? 30_000
|
|
40
|
+
const TIMEOUT_MS = parseBoundedEnvInt(env?.ABIDE_REACHABLE_TIMEOUT, 100, 60_000) ?? 3_000
|
|
41
|
+
/* Stop polling a host nobody has read in a few TTLs; the next read restarts it cold. */
|
|
42
|
+
const IDLE_MS = TTL_MS * 3
|
|
43
|
+
|
|
44
|
+
/* Status-agnostic HEAD: a completed response proves connectivity; reject/timeout does not.
|
|
45
|
+
The browser probes no-cors — an opaque response still completes, so a foreign origin
|
|
46
|
+
without CORS headers can't mis-read an up host as down. */
|
|
47
|
+
async function probeOrigin(origin: string): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
await fetch(origin, {
|
|
50
|
+
method: 'HEAD',
|
|
51
|
+
mode: typeof window === 'undefined' ? undefined : 'no-cors',
|
|
52
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
53
|
+
})
|
|
54
|
+
return true
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const registry = createReachable({
|
|
61
|
+
probe: probeOrigin,
|
|
62
|
+
intervalMs: TTL_MS,
|
|
63
|
+
idleMs: IDLE_MS,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/* Loopback = this machine: no network between, so no probe and no offline gate. */
|
|
67
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
68
|
+
return (
|
|
69
|
+
hostname === 'localhost' ||
|
|
70
|
+
hostname.endsWith('.localhost') ||
|
|
71
|
+
hostname.startsWith('127.') ||
|
|
72
|
+
hostname === '[::1]'
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Mirrors the registry's normalization: a bare host defaults to https. */
|
|
77
|
+
function hostnameOf(host: string | URL): string {
|
|
78
|
+
const url = typeof host === 'string' && !/^https?:\/\//i.test(host) ? `https://${host}` : host
|
|
79
|
+
return new URL(url).hostname
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// @documentation observability
|
|
83
|
+
export function reachable(host?: string | URL): Promise<boolean> {
|
|
84
|
+
if (typeof window === 'undefined') {
|
|
85
|
+
/* No host = the app's own backend, and the server IS the backend. */
|
|
86
|
+
return host === undefined ? Promise.resolve(true) : registry.reachable(host)
|
|
87
|
+
}
|
|
88
|
+
if (host === undefined) {
|
|
89
|
+
/* The app's own backend = the page's origin. Loopback served this very page
|
|
90
|
+
from this machine — reachable by construction, never worth a poll. */
|
|
91
|
+
if (isLoopbackHost(location.hostname)) {
|
|
92
|
+
return Promise.resolve(true)
|
|
93
|
+
}
|
|
94
|
+
host = location.origin
|
|
95
|
+
}
|
|
96
|
+
/* window-gated because Bun defines a partial navigator; `=== false` because only an
|
|
97
|
+
explicit offline report is reliable. A loopback host is exempt — no network needed. */
|
|
98
|
+
if (navigator.onLine === false && !isLoopbackHost(hostnameOf(host))) {
|
|
99
|
+
return Promise.resolve(false)
|
|
100
|
+
}
|
|
101
|
+
return registry.reachable(host)
|
|
102
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cache } from './cache.ts'
|
|
2
|
+
import type { CacheSelector } from './types/CacheSelector.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Refetch every cached read matching the selector, keeping the stale value visible
|
|
6
|
+
until the fresh one swaps in (refreshing() true meanwhile) — the smart-call
|
|
7
|
+
refetch. Because the smart read retains its value (SWR unconditional), a refresh
|
|
8
|
+
always refetches-and-swaps; it never drops to a pending blank. Follows the shared
|
|
9
|
+
selector grammar:
|
|
10
|
+
|
|
11
|
+
refresh(getFoo, args) → that exact call
|
|
12
|
+
refresh(getFoo) → every args-variant of that rpc
|
|
13
|
+
refresh({ tags }) → every entry sharing a tag
|
|
14
|
+
refresh() → every entry
|
|
15
|
+
|
|
16
|
+
Instance sugar `getFoo.refresh(args?)` ≡ `refresh(getFoo, args?)`. Reports, never
|
|
17
|
+
retains a spinner — pair with `refreshing()` to surface the in-flight reload.
|
|
18
|
+
*/
|
|
19
|
+
// @documentation cache
|
|
20
|
+
export function refresh<Args, Return>(arg?: CacheSelector<Args, Return>, args?: Args): void {
|
|
21
|
+
cache.refresh(arg, args)
|
|
22
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createResolverSlot } from './createResolverSlot.ts'
|
|
2
|
+
import type { RequestScopeInfo } from './types/RequestScopeInfo.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
undefined outside any request
|
|
8
|
-
|
|
5
|
+
The request-scope slot. The server installs an ALS-backed resolver
|
|
6
|
+
(createServer, reading the RequestStore); the client a module-singleton seeded
|
|
7
|
+
from __SSR__ (startClient). No fallback creator — an unset resolver (or one
|
|
8
|
+
returning undefined outside any request) means "no scope": callers read
|
|
9
|
+
`.resolver?.()` and treat undefined as absent (trace() returns undefined and
|
|
10
|
+
log lines print without the context prefix). Test helpers snapshot/poke
|
|
11
|
+
`.resolver` directly.
|
|
9
12
|
*/
|
|
10
|
-
export const requestScopeSlot =
|
|
13
|
+
export const requestScopeSlot = createResolverSlot<RequestScopeInfo>()
|
|
@@ -3,18 +3,18 @@ import { parseRouteSegments } from './parseRouteSegments.ts'
|
|
|
3
3
|
/*
|
|
4
4
|
The TypeScript type literal for a route's path params — `{ "id": string }` for
|
|
5
5
|
`/post/[id]`, `Record<string, never>` for a param-less route. Walks the `[name]` /
|
|
6
|
-
`[...rest]` segments; catch-all
|
|
7
|
-
(
|
|
8
|
-
|
|
9
|
-
truth shared by `writeRoutesDts` (page.params typing) and the
|
|
10
|
-
(page `props()` typing).
|
|
6
|
+
`[[name]]` / `[...rest]` segments; a catch-all maps to `string` under its declared
|
|
7
|
+
name (matchRoute captures it there, `''` when it consumed nothing), and an
|
|
8
|
+
`[[optional]]` maps to an optional key (absent from params when unmatched). The
|
|
9
|
+
single source of truth shared by `writeRoutesDts` (page.params typing) and the
|
|
10
|
+
`.abide` check shadow (page `props()` typing).
|
|
11
11
|
*/
|
|
12
12
|
export function routeParamsShape(routeUrl: string): string {
|
|
13
|
-
const
|
|
13
|
+
const entries = parseRouteSegments(routeUrl)
|
|
14
14
|
.filter((segment) => segment.kind === 'param')
|
|
15
|
-
.map((segment) => segment.name)
|
|
16
|
-
if (
|
|
15
|
+
.map((segment) => `${JSON.stringify(segment.name)}${segment.optional ? '?' : ''}: string`)
|
|
16
|
+
if (entries.length === 0) {
|
|
17
17
|
return 'Record<string, never>'
|
|
18
18
|
}
|
|
19
|
-
return `{ ${
|
|
19
|
+
return `{ ${entries.join('; ')} }`
|
|
20
20
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createSignalNode } from '../ui/runtime/createSignalNode.ts'
|
|
2
|
+
import { track } from '../ui/runtime/track.ts'
|
|
3
|
+
import { trigger } from '../ui/runtime/trigger.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
The rpc's error memory — the last error per call identity (the keyForRemoteCall
|
|
7
|
+
key), recorded at the rpc call boundary and cleared on a later success or
|
|
8
|
+
invalidate. Orthogonal to the cache entry lifecycle (design Fork 1): the cache
|
|
9
|
+
still evicts on error exactly as before; this remembers the error for reactive
|
|
10
|
+
reads. One shared signal node backs reactivity — readers `track` it, writers
|
|
11
|
+
`trigger` it — the same coarse lifecycle-channel pattern the cache probes use.
|
|
12
|
+
*/
|
|
13
|
+
const node = createSignalNode(undefined)
|
|
14
|
+
const errors = new Map<string, unknown>()
|
|
15
|
+
|
|
16
|
+
/* Insertion order = recency; a re-record moves the key to the tail so readAny returns latest. */
|
|
17
|
+
function bump(key: string, error: unknown): void {
|
|
18
|
+
errors.delete(key)
|
|
19
|
+
errors.set(key, error)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const rpcErrorRegistry = {
|
|
23
|
+
record(key: string, error: unknown): void {
|
|
24
|
+
/* Client-only: on the server the boundary runs per request, and this Map is a single
|
|
25
|
+
module global — recording would leak errors across requests/users and grow unbounded.
|
|
26
|
+
SSR errors surface through `{:catch}`; the reactive probe is a client feature, so the
|
|
27
|
+
server keeps the Map empty (reads return undefined there). */
|
|
28
|
+
if (typeof window === 'undefined') {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
bump(key, error)
|
|
32
|
+
trigger(node)
|
|
33
|
+
},
|
|
34
|
+
clear(key: string): void {
|
|
35
|
+
if (errors.delete(key)) {
|
|
36
|
+
trigger(node)
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
/* Clears every error whose key is, or is prefixed by, the selector's `method url` —
|
|
40
|
+
so `invalidate(fn)` resets a whole rpc's errors, and `invalidate(fn, args)` (an exact
|
|
41
|
+
key prefix) resets just that call. Covers bare-call errors that never became entries. */
|
|
42
|
+
clearMatching(prefix: string): void {
|
|
43
|
+
let changed = false
|
|
44
|
+
for (const key of errors.keys()) {
|
|
45
|
+
if (key === prefix || key.startsWith(`${prefix} `)) {
|
|
46
|
+
errors.delete(key)
|
|
47
|
+
changed = true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (changed) {
|
|
51
|
+
trigger(node)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
read(key: string): unknown {
|
|
55
|
+
track(node)
|
|
56
|
+
return errors.get(key)
|
|
57
|
+
},
|
|
58
|
+
/* Most-recent error whose key is, or is prefixed by, the fn selector's `method url`. */
|
|
59
|
+
readAny(prefix: string): unknown {
|
|
60
|
+
track(node)
|
|
61
|
+
let latest: unknown
|
|
62
|
+
for (const [key, error] of errors) {
|
|
63
|
+
if (key === prefix || key.startsWith(`${prefix} `)) {
|
|
64
|
+
latest = error
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return latest
|
|
68
|
+
},
|
|
69
|
+
}
|