@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
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { bodyValueForKind
|
|
1
|
+
import { bodyValueForKind } from './bodyValueForKind.ts'
|
|
2
2
|
import { contentBodyKind } from './contentBodyKind.ts'
|
|
3
3
|
import { contentTypeOf } from './contentTypeOf.ts'
|
|
4
|
+
import { DEFER } from './DEFER.ts'
|
|
4
5
|
import type { CacheEntry } from './types/CacheEntry.ts'
|
|
5
6
|
import type { CacheSnapshotEntry } from './types/CacheSnapshotEntry.ts'
|
|
6
7
|
|
|
@@ -19,38 +20,18 @@ export function cacheEntryFromSnapshot(entry: CacheSnapshotEntry): CacheEntry {
|
|
|
19
20
|
statusText: entry.statusText,
|
|
20
21
|
headers,
|
|
21
22
|
})
|
|
22
|
-
/* Deferred seed: decode nothing now — hand back a memoized materializer the first read
|
|
23
|
-
invokes, so hydration pays no payload decode. Eager seed: decode up front as before. */
|
|
24
|
-
const warm = entry.lazy
|
|
25
|
-
? memoizeWarm(() => warmValueFromSnapshot(entry.status, headers, entry.body))
|
|
26
|
-
: undefined
|
|
27
23
|
return {
|
|
28
24
|
key: entry.key,
|
|
29
25
|
promise: Promise.resolve(response),
|
|
30
26
|
request: new Request(entry.url, { method: entry.method }),
|
|
31
27
|
ttl: undefined,
|
|
32
28
|
expiresAt: undefined,
|
|
33
|
-
value:
|
|
34
|
-
warm,
|
|
29
|
+
value: warmValueFromSnapshot(entry.status, headers, entry.body),
|
|
35
30
|
settled: true,
|
|
36
31
|
hydrated: true,
|
|
37
32
|
}
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
/* Wraps a warm decode so it runs at most once — the materialized value (including a
|
|
41
|
-
legitimate undefined for a non-warmable status) is cached after the first call. */
|
|
42
|
-
function memoizeWarm(decode: () => unknown): () => unknown {
|
|
43
|
-
let materialized = false
|
|
44
|
-
let value: unknown
|
|
45
|
-
return () => {
|
|
46
|
-
if (!materialized) {
|
|
47
|
-
value = decode()
|
|
48
|
-
materialized = true
|
|
49
|
-
}
|
|
50
|
-
return value
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
35
|
/*
|
|
55
36
|
Synchronously decodes a snapshot body so the warm entry reads without a
|
|
56
37
|
microtask hop on first render. The json/text branches go through the shared
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createCacheStore } from './createCacheStore.ts'
|
|
2
|
+
import { createResolverSlot } from './createResolverSlot.ts'
|
|
3
|
+
import type { CacheStore } from './types/CacheStore.ts'
|
|
2
4
|
|
|
3
5
|
/*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
The active-CacheStore slot. The server entry installs an ALS-backed resolver
|
|
7
|
+
(request-scoped); the client entry a module-singleton one. With no resolver
|
|
8
|
+
registered, a single fallback store is created lazily so isolated tests work
|
|
9
|
+
without booting the runtime; test helpers snapshot/poke `.resolver` and
|
|
10
|
+
`.fallback` directly. activeCacheStore is the public read.
|
|
7
11
|
*/
|
|
8
|
-
export const cacheStoreSlot =
|
|
12
|
+
export const cacheStoreSlot = createResolverSlot<CacheStore>(createCacheStore)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createLivenessWatch } from '
|
|
1
|
+
import { createLivenessWatch } from './createLivenessWatch.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
The reachability registry behind `abide/
|
|
4
|
+
The reachability registry behind `abide/shared/reachable`. A probe transport
|
|
5
5
|
and the timings are injected so the public name wires them from env + a real
|
|
6
6
|
HEAD while tests drive scripted outcomes on tiny intervals.
|
|
7
7
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attachRpcSelectorMethods } from './attachRpcSelectorMethods.ts'
|
|
2
|
+
import { cache } from './cache.ts'
|
|
2
3
|
import { HttpError } from './HttpError.ts'
|
|
3
4
|
import { keyForRemoteCall } from './keyForRemoteCall.ts'
|
|
4
5
|
import { REMOTE_FUNCTION } from './REMOTE_FUNCTION.ts'
|
|
5
6
|
import { recordRemoteMeta } from './recordRemoteMeta.ts'
|
|
7
|
+
import { rpcErrorRegistry } from './rpcErrorRegistry.ts'
|
|
6
8
|
import { subscribableFromResponse } from './subscribableFromResponse.ts'
|
|
9
|
+
import type { CacheOptions } from './types/CacheOptions.ts'
|
|
7
10
|
import type { ClientFlags } from './types/ClientFlags.ts'
|
|
8
11
|
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
9
12
|
import type { RawRemoteFunction } from './types/RawRemoteFunction.ts'
|
|
@@ -47,8 +50,21 @@ export function createRemoteFunction<Args, Return>(opts: {
|
|
|
47
50
|
opts?: RpcOptions,
|
|
48
51
|
) => Promise<Response>
|
|
49
52
|
parseArgsForFetch?: (request: Request) => Promise<Args | undefined>
|
|
53
|
+
/* A streaming rpc (handler returns jsonl()/sse()): the bare call returns the Subscribable
|
|
54
|
+
directly (the iterable IS the value) rather than decoding one Response body. Emitted by
|
|
55
|
+
the bundler's syntactic scan; false/undefined keeps the decode-a-Response path. */
|
|
56
|
+
streaming?: boolean
|
|
50
57
|
}): RemoteFunction<Args, Return> {
|
|
51
|
-
const {
|
|
58
|
+
const {
|
|
59
|
+
method,
|
|
60
|
+
url,
|
|
61
|
+
clients,
|
|
62
|
+
crossOrigin,
|
|
63
|
+
buildRequest,
|
|
64
|
+
invoke,
|
|
65
|
+
parseArgsForFetch,
|
|
66
|
+
streaming,
|
|
67
|
+
} = opts
|
|
52
68
|
|
|
53
69
|
/*
|
|
54
70
|
Dispatch is the one-stop entry for both the plain call (no prebuilt
|
|
@@ -90,19 +106,51 @@ export function createRemoteFunction<Args, Return>(opts: {
|
|
|
90
106
|
Object.defineProperty(rawCall, REMOTE_FUNCTION, { value: true })
|
|
91
107
|
const raw = rawCall as RawRemoteFunction<Args>
|
|
92
108
|
|
|
93
|
-
function callable(
|
|
94
|
-
|
|
109
|
+
function callable(
|
|
110
|
+
args: Args | FormData,
|
|
111
|
+
opts?: RpcOptions,
|
|
112
|
+
): Promise<Return> | Subscribable<Return> {
|
|
113
|
+
/* A streaming rpc (jsonl/sse) returns the Subscribable directly — the iterable IS the
|
|
114
|
+
value (for await / state(fn(args))). Deferred fetch, keyForRemoteCall-keyed so tail()
|
|
115
|
+
dedupes readers; no decode, so the error-capture path below doesn't apply. */
|
|
116
|
+
if (streaming) {
|
|
117
|
+
return subscribableFromResponse(keyForRemoteCall(method, url, args), () =>
|
|
118
|
+
raw(args as Args, opts),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
/* The bare call IS the smart read: route through the cache store's smart-read
|
|
122
|
+
path so a replayable read is coalesced, retained (SWR unconditional), and
|
|
123
|
+
reactive, while a write is coalesce-only — the raw fetch moves to `.raw`.
|
|
124
|
+
`cache.read(callable, …)` brand-reads `callable.raw` for the undecoded variant
|
|
125
|
+
and decodes on the way out, so pass the callable (which carries `.raw`), not
|
|
126
|
+
`raw`. `opts` supplies cache options (ttl/tags/throttle/debounce); per-call
|
|
127
|
+
transport options live on `.raw` now. */
|
|
128
|
+
const key = keyForRemoteCall(method, url, args)
|
|
129
|
+
return cache
|
|
130
|
+
.read(callable as RemoteFunction<Args, Return>, args as Args, opts as CacheOptions)
|
|
131
|
+
.then(
|
|
132
|
+
/* Capture the rejection into the rpc error registry (design Part 4) keyed by call
|
|
133
|
+
identity, and clear it on success — the reactive `fn.error()` probe reads it. */
|
|
134
|
+
(value) => {
|
|
135
|
+
rpcErrorRegistry.clear(key)
|
|
136
|
+
return value as Return
|
|
137
|
+
},
|
|
138
|
+
(error: unknown) => {
|
|
139
|
+
/* A parked durable write throws a `kind: 'queued'` sentinel — it's pending
|
|
140
|
+
retry, not a failure. Don't record it, so fn.error() stays undefined for a
|
|
141
|
+
merely-parked write (pending() already reflects it via the outbox). */
|
|
142
|
+
if (!(error instanceof HttpError && error.kind === 'queued')) {
|
|
143
|
+
rpcErrorRegistry.record(key, error)
|
|
144
|
+
}
|
|
145
|
+
throw error
|
|
146
|
+
},
|
|
147
|
+
)
|
|
95
148
|
}
|
|
96
149
|
callable.method = method
|
|
97
150
|
callable.url = url
|
|
98
151
|
callable.clients = clients
|
|
99
152
|
callable.crossOrigin = crossOrigin
|
|
100
153
|
callable.raw = raw
|
|
101
|
-
callable.stream = (args?: Args | FormData): Subscribable<Return> => {
|
|
102
|
-
return subscribableFromResponse(keyForRemoteCall(method, url, args), () =>
|
|
103
|
-
raw(args as Args),
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
154
|
/* Uniform runtime guard for every rpc — the per-rpc data typing lives entirely in the
|
|
107
155
|
RpcErrorGuard<Errors> signature RemoteFunction projects onto it (Errors flows from the
|
|
108
156
|
rpc helper's declared type, not from here). */
|
|
@@ -132,5 +180,6 @@ export function createRemoteFunction<Args, Return>(opts: {
|
|
|
132
180
|
: (request: Request): Promise<Response> => {
|
|
133
181
|
return dispatch(undefined, undefined, request)
|
|
134
182
|
}
|
|
183
|
+
attachRpcSelectorMethods(callable as RemoteFunction<Args, Return>)
|
|
135
184
|
return callable as RemoteFunction<Args, Return>
|
|
136
185
|
}
|
|
@@ -1,37 +1,30 @@
|
|
|
1
1
|
import type { ResolverSlot } from './types/ResolverSlot.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
The slot
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
returns the resolved value, falling back to
|
|
8
|
-
when no resolver is registered so isolated
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
The resolver-slot seam the runtime entries share for cache store, page,
|
|
5
|
+
request-scope, and mount-base state. A runtime entry registers a side-specific
|
|
6
|
+
resolver by assigning `.resolver` (ALS-backed on the server, a module
|
|
7
|
+
singleton on the client); `get()` returns the resolved value, falling back to
|
|
8
|
+
a single lazily-created fallback when no resolver is registered so isolated
|
|
9
|
+
tests work without booting the runtime. Pass `createFallback` for a
|
|
10
|
+
lazily-built, cached fallback (cache store, page); omit it for a slot whose
|
|
11
|
+
fallback is a plain value set directly (mount base) — `get()` then returns
|
|
12
|
+
`T | undefined`.
|
|
13
13
|
*/
|
|
14
|
-
export function createResolverSlot<T>(createFallback?: () => T): {
|
|
15
|
-
slot: ResolverSlot<T>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return fromResolver
|
|
29
|
-
}
|
|
30
|
-
if (createFallback && slot.fallback === undefined) {
|
|
31
|
-
slot.fallback = createFallback()
|
|
32
|
-
}
|
|
33
|
-
return slot.fallback
|
|
14
|
+
export function createResolverSlot<T>(createFallback?: () => T): ResolverSlot<T> {
|
|
15
|
+
const slot: ResolverSlot<T> = {
|
|
16
|
+
resolver: undefined,
|
|
17
|
+
fallback: undefined,
|
|
18
|
+
get(): T | undefined {
|
|
19
|
+
const fromResolver = slot.resolver?.()
|
|
20
|
+
if (fromResolver !== undefined) {
|
|
21
|
+
return fromResolver
|
|
22
|
+
}
|
|
23
|
+
if (createFallback && slot.fallback === undefined) {
|
|
24
|
+
slot.fallback = createFallback()
|
|
25
|
+
}
|
|
26
|
+
return slot.fallback
|
|
27
|
+
},
|
|
34
28
|
}
|
|
35
|
-
|
|
36
|
-
return { slot, set, get }
|
|
29
|
+
return slot
|
|
37
30
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { SocketClientFrame } from '../server/sockets/types/SocketClientFrame.ts'
|
|
2
|
-
import type { SocketServerFrame } from '../server/sockets/types/SocketServerFrame.ts'
|
|
3
1
|
import type { SocketChannel } from './types/SocketChannel.ts'
|
|
2
|
+
import type { SocketClientFrame } from './types/SocketClientFrame.ts'
|
|
3
|
+
import type { SocketServerFrame } from './types/SocketServerFrame.ts'
|
|
4
4
|
import type { SocketSubCallbacks } from './types/SocketSubCallbacks.ts'
|
|
5
5
|
|
|
6
6
|
/*
|
|
@@ -6,7 +6,8 @@ them. The shell must exist before its contents are filled so a back-reference to
|
|
|
6
6
|
ancestor resolves to the already-allocated object — which is what reconnects a cycle.
|
|
7
7
|
Inline primitives need no shell; they decode directly where they sit.
|
|
8
8
|
*/
|
|
9
|
-
import { REF_JSON_TAGS } from './REF_JSON_TAGS'
|
|
9
|
+
import { REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
|
|
10
|
+
import { setOwnProperty } from './setOwnProperty.ts'
|
|
10
11
|
|
|
11
12
|
export function decodeRefJson(text: string): unknown {
|
|
12
13
|
const parsed = JSON.parse(text)
|
|
@@ -68,7 +69,8 @@ function fillShell(slot: unknown, shell: unknown, shells: unknown[]): void {
|
|
|
68
69
|
const target = shell as Record<string, unknown>
|
|
69
70
|
const record = slot as Record<string, unknown>
|
|
70
71
|
for (const key of Object.keys(record)) {
|
|
71
|
-
|
|
72
|
+
// setOwnProperty: a wire `__proto__` key must not hijack the shell's prototype.
|
|
73
|
+
setOwnProperty(target, key, resolveValue(record[key], shells))
|
|
72
74
|
}
|
|
73
75
|
return
|
|
74
76
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { bodyValueForKind
|
|
1
|
+
import { bodyValueForKind } from './bodyValueForKind.ts'
|
|
2
2
|
import { contentBodyKind } from './contentBodyKind.ts'
|
|
3
3
|
import { contentTypeOf } from './contentTypeOf.ts'
|
|
4
|
+
import { DEFER } from './DEFER.ts'
|
|
4
5
|
import { httpErrorFor } from './httpErrorFor.ts'
|
|
5
6
|
|
|
6
7
|
/*
|
|
@@ -17,10 +18,11 @@ propagation), and the success path types as Promise<Return> cleanly.
|
|
|
17
18
|
Streaming Content-Types (SSE / JSONL / NDJSON) throw a clear error
|
|
18
19
|
rather than silently doing the wrong thing: response.text() would hang
|
|
19
20
|
forever on a never-ending body and response.json() would fail mid-parse.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
A streaming rpc's bare call already returns a Subscribable (the type makes
|
|
22
|
+
`await fn(args)` a compile error), so this is a backstop for the paths that
|
|
23
|
+
still decode a raw Response — `cache()` and the one-shot stream reader. The
|
|
24
|
+
error points callers at the right tools: `state(fn(args))` for a reactive
|
|
25
|
+
view, or `for await (… of fn(args))` for direct iteration.
|
|
24
26
|
|
|
25
27
|
Callers that need headers, streaming, or per-status branching should use
|
|
26
28
|
the `.raw(args)` escape hatch on the remote function instead — that
|
|
@@ -37,7 +39,7 @@ export async function decodeResponse(response: Response): Promise<unknown> {
|
|
|
37
39
|
const kind = contentBodyKind(contentType)
|
|
38
40
|
if (kind === 'streaming') {
|
|
39
41
|
throw new Error(
|
|
40
|
-
`[abide] response at ${response.url} is a stream (${contentType}) — use
|
|
42
|
+
`[abide] response at ${response.url} is a stream (${contentType}) — a streaming rpc's bare call already returns a Subscribable: use state(fn(args)) for a reactive view or \`for await (… of fn(args))\` for iteration, not await/cache()`,
|
|
41
43
|
)
|
|
42
44
|
}
|
|
43
45
|
/* json/text go through the shared mapping warmValueFromSnapshot also uses, so a warm
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { tailProbeSlot } from './tailProbeSlot.ts'
|
|
2
|
+
import type { Subscribable } from './types/Subscribable.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Reactive terminal-state reader for a stream: true once the source has closed (its
|
|
6
|
+
tail entry status === 'done'). Stream-only — a cache entry's "done" is just
|
|
7
|
+
`!pending && !refreshing`, so there is no cache-selector form. The residual bit the
|
|
8
|
+
`pending` / `refreshing` / `error` probes don't cover (see design Part 4). No prober
|
|
9
|
+
registered (server render, or tail never imported) reads as not-done.
|
|
10
|
+
*/
|
|
11
|
+
// @documentation probes
|
|
12
|
+
export function done(subscribable: Subscribable<unknown>): boolean {
|
|
13
|
+
return tailProbeSlot.probe?.(subscribable.name)?.done ?? false
|
|
14
|
+
}
|
|
@@ -11,7 +11,8 @@ symbols can't be serialised and encode as undefined, matching JSON.stringify. Ou
|
|
|
11
11
|
shape is `[rootValue, slots]`. Decode with decodeRefJson. Not streaming — the whole
|
|
12
12
|
graph is walked up front.
|
|
13
13
|
*/
|
|
14
|
-
import { REF_JSON_TAGS } from './REF_JSON_TAGS'
|
|
14
|
+
import { REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
|
|
15
|
+
import { setOwnProperty } from './setOwnProperty.ts'
|
|
15
16
|
|
|
16
17
|
export function encodeRefJson(value: unknown): string {
|
|
17
18
|
// Hoisted containers only; slots[i] is addressed by ['~r', i]. Primitives stay inline.
|
|
@@ -86,7 +87,8 @@ export function encodeRefJson(value: unknown): string {
|
|
|
86
87
|
// (no intermediate pairs array / closures) on this per-object always-hot path.
|
|
87
88
|
const encoded: Record<string, unknown> = {}
|
|
88
89
|
for (const key of Object.keys(record)) {
|
|
89
|
-
|
|
90
|
+
// setOwnProperty: an own `__proto__` key must reach the wire, not the accessor.
|
|
91
|
+
setOwnProperty(encoded, key, encodeValue(record[key]))
|
|
90
92
|
}
|
|
91
93
|
return encoded
|
|
92
94
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createResolverSlot } from './createResolverSlot.ts'
|
|
2
|
+
import type { CacheStore } from './types/CacheStore.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
|
-
Slot for the process-level cache store
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Slot for the process-level cache store used by cache() entries opting into
|
|
6
|
+
`global: true`. The server entry registers a module-singleton store outliving
|
|
7
|
+
any one request; the client entry points it at its single tab store so
|
|
8
|
+
`global` is a no-op there. No fallback creator — unset means no global store
|
|
9
|
+
is registered, in which case globalCacheStore() falls back to the active
|
|
10
|
+
(request/tab) store. Test helpers snapshot/poke `.resolver` directly.
|
|
8
11
|
*/
|
|
9
|
-
export const globalCacheStoreSlot =
|
|
12
|
+
export const globalCacheStoreSlot = createResolverSlot<CacheStore>()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Set true only while the client adopts the server-rendered DOM (the hydration
|
|
3
|
+
render pass — see the router's hydrate branch and `hydrate`). Read by `peek`: the
|
|
4
|
+
server materializes no cache value (materializeRetained/cacheEntryFromSnapshot are
|
|
5
|
+
client-only), so server-side peek is uniformly undefined and the SSR render always
|
|
6
|
+
shows the fallback. A snapshot-seeded warm value surfacing DURING hydration would
|
|
7
|
+
diverge from that server text and corrupt the claimed text node, so peek withholds
|
|
8
|
+
it until the pass ends — `wakeHydrationPeeks` then re-runs the scope on the now-
|
|
9
|
+
congruent value. A plain boolean, save/restore-nested so a child hydrate can't clear
|
|
10
|
+
an outer pass early; false on the server (no hydration) and after boot.
|
|
11
|
+
*/
|
|
12
|
+
export const hydratingSlot: { active: boolean } = { active: false }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fileName } from './fileName.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Whether a module path is a router `layout.abide` — an EXACT leaf-filename match, the
|
|
5
|
+
one predicate every site shares. `endsWith('layout.abide')` is wrong: it also matches
|
|
6
|
+
`cardlayout.abide`, which the bundler's loader (exact match) compiles as a plain
|
|
7
|
+
component, so an `endsWith` site would hot-compile / fingerprint it as a layout and
|
|
8
|
+
diverge from the shipped bundle. Mirrors the loader so classification can't drift.
|
|
9
|
+
*/
|
|
10
|
+
export function isLayoutFile(path: string): boolean {
|
|
11
|
+
return fileName(path) === 'layout.abide'
|
|
12
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canonicalJson } from './canonicalJson.ts'
|
|
2
2
|
import { carriesBodyArgs } from './carriesBodyArgs.ts'
|
|
3
|
+
import { keyPrefixForRemote } from './keyPrefixForRemote.ts'
|
|
3
4
|
import { queryStringFromArgs } from './queryStringFromArgs.ts'
|
|
4
5
|
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
5
6
|
|
|
@@ -12,7 +13,7 @@ synthesized Request can't disagree); POST/PUT/PATCH join args after a space as
|
|
|
12
13
|
canonical JSON. The rpc split mirrors buildRpcRequest exactly.
|
|
13
14
|
*/
|
|
14
15
|
export function keyForRemoteCall(method: HttpMethod, url: string, args: unknown): string {
|
|
15
|
-
const prefix =
|
|
16
|
+
const prefix = keyPrefixForRemote(method, url)
|
|
16
17
|
if (!carriesBodyArgs(method)) {
|
|
17
18
|
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
18
19
|
const search = queryStringFromArgs(args as Record<string, unknown>, true)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The cache/error-registry key prefix a remote function's entries share: `${method} ${url}`
|
|
5
|
+
(method + route template). The single definition of the prefix grammar — keyForRemoteCall
|
|
6
|
+
appends `?query`/` body` onto it, and keyMatchesPrefix extends it — so every consumer that
|
|
7
|
+
prefix-matches (`fn.error()`, the cache selector prefix, the MCP tool labels) composes the
|
|
8
|
+
SAME prefix and can't silently mismatch if the format ever changes.
|
|
9
|
+
*/
|
|
10
|
+
export function keyPrefixForRemote(method: HttpMethod, url: string): string {
|
|
11
|
+
return `${method} ${url}`
|
|
12
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { normalizePathname } from './normalizePathname.ts'
|
|
2
|
+
import type { RouteSegment } from './parseRouteSegments.ts'
|
|
3
|
+
import { parseRouteSegments } from './parseRouteSegments.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Per-route parse cache. The route set is stable across a session, so parsing a
|
|
7
|
+
pattern into segments is done once per route rather than on every match. Keyed
|
|
8
|
+
by the pattern string; entries never need eviction since routes don't churn.
|
|
9
|
+
*/
|
|
10
|
+
const PARSED_ROUTES = new Map<string, RouteSegment[]>()
|
|
11
|
+
|
|
12
|
+
function parsedRoute(route: string): RouteSegment[] {
|
|
13
|
+
let segments = PARSED_ROUTES.get(route)
|
|
14
|
+
if (segments === undefined) {
|
|
15
|
+
segments = parseRouteSegments(route)
|
|
16
|
+
PARSED_ROUTES.set(route, segments)
|
|
17
|
+
}
|
|
18
|
+
return segments
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* One pattern segment's specificity rank — lower is more specific. */
|
|
22
|
+
function segmentRank(segment: RouteSegment): number {
|
|
23
|
+
if (segment.kind === 'literal') {
|
|
24
|
+
return 0
|
|
25
|
+
}
|
|
26
|
+
if (segment.catchAll) {
|
|
27
|
+
return 3
|
|
28
|
+
}
|
|
29
|
+
return segment.optional ? 2 : 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Positional specificity between two routes that both match a path: walk both
|
|
33
|
+
patterns left to right, and at the first position where the segment kinds
|
|
34
|
+
differ the more specific kind wins — literal > `[name]` > `[[name]]` >
|
|
35
|
+
`[...rest]`. So `/a/[b]` beats `/[a]/b`, and a literal head beats a param
|
|
36
|
+
head even against a catch-all tail (matching how Bun's own router ranked).
|
|
37
|
+
Prefix-equal patterns: the shorter one is the exact match. A full tie keeps
|
|
38
|
+
the earlier-registered route — deterministic, unlike count-based scoring,
|
|
39
|
+
because kind sequences that differ anywhere are ordered at that position. */
|
|
40
|
+
function moreSpecific(candidate: RouteSegment[], best: RouteSegment[]): boolean {
|
|
41
|
+
const shared = Math.min(candidate.length, best.length)
|
|
42
|
+
for (let index = 0; index < shared; index += 1) {
|
|
43
|
+
const candidateRank = segmentRank(candidate[index] as RouteSegment)
|
|
44
|
+
const bestRank = segmentRank(best[index] as RouteSegment)
|
|
45
|
+
if (candidateRank !== bestRank) {
|
|
46
|
+
return candidateRank < bestRank
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return candidate.length < best.length
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
The route matcher — the single grammar of record on both sides: the server's
|
|
54
|
+
fetch dispatch and the client router resolve a pathname through this same
|
|
55
|
+
function, so params decode and precedence agree by construction. Given the
|
|
56
|
+
registered route patterns and a pathname, returns the matching route + decoded
|
|
57
|
+
params, or undefined. Segments are literal / `[name]` / `[[name]]` (optional) /
|
|
58
|
+
`[...rest]` (catch-all), via the shared `parseRouteSegments`. The most specific
|
|
59
|
+
match wins (see moreSpecific).
|
|
60
|
+
*/
|
|
61
|
+
export function matchRoute(
|
|
62
|
+
routes: string[],
|
|
63
|
+
pathname: string,
|
|
64
|
+
): { route: string; params: Record<string, string> } | undefined {
|
|
65
|
+
/* Match the canonical slash form — `//users` and `/users/` both match
|
|
66
|
+
`/users` — otherwise the extra empty segment rejects the path or lets
|
|
67
|
+
`/users/[id]` capture an empty id. The server redirects a request whose
|
|
68
|
+
raw pathname differs from this form before dispatching (see createServer),
|
|
69
|
+
so its auth middleware guards the same string this matcher routes. */
|
|
70
|
+
const normalized = normalizePathname(pathname)
|
|
71
|
+
const pathSegments = normalized.split('/')
|
|
72
|
+
let best: { route: string; params: Record<string, string>; parsed: RouteSegment[] } | undefined
|
|
73
|
+
for (const route of routes) {
|
|
74
|
+
const parsed = parsedRoute(route)
|
|
75
|
+
const params = matchSegments(parsed, pathSegments)
|
|
76
|
+
if (params === undefined) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (best === undefined || moreSpecific(parsed, best.parsed)) {
|
|
80
|
+
best = { route, params, parsed }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return best === undefined ? undefined : { route: best.route, params: best.params }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Percent-decodes a captured value. Lenient — a malformed sequence (`/%E0%A4%A`)
|
|
87
|
+
keeps the raw text rather than throwing, so a bad escape in a navigation or
|
|
88
|
+
request can't crash matching; the downstream lookup just misses naturally. */
|
|
89
|
+
function decodeParam(value: string): string {
|
|
90
|
+
try {
|
|
91
|
+
return decodeURIComponent(value)
|
|
92
|
+
} catch {
|
|
93
|
+
return value
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Matches one parsed pattern against the path's segments, capturing decoded
|
|
98
|
+
params; undefined on mismatch. */
|
|
99
|
+
function matchSegments(
|
|
100
|
+
segments: RouteSegment[],
|
|
101
|
+
pathSegments: string[],
|
|
102
|
+
): Record<string, string> | undefined {
|
|
103
|
+
const params: Record<string, string> = {}
|
|
104
|
+
return matchFrom(segments, pathSegments, 0, 0, params) ? params : undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
Depth-first walk over the pattern. An optional segment first tries to consume a
|
|
109
|
+
path segment (greedy — `/[[a]]/[[b]]` on `/x` captures `a`), and on a deeper
|
|
110
|
+
mismatch backtracks to try absence, un-capturing its param so a failed branch
|
|
111
|
+
can't leak into the succeeding one. Recursion depth is bounded by the pattern's
|
|
112
|
+
segment count, not the request path. A catch-all consumes every remaining
|
|
113
|
+
segment (possibly none), decoding each sub-segment while keeping `/` separators
|
|
114
|
+
— so an encoded `%2F` stays inside one sub-segment.
|
|
115
|
+
*/
|
|
116
|
+
function matchFrom(
|
|
117
|
+
segments: RouteSegment[],
|
|
118
|
+
pathSegments: string[],
|
|
119
|
+
segmentIndex: number,
|
|
120
|
+
pathIndex: number,
|
|
121
|
+
params: Record<string, string>,
|
|
122
|
+
): boolean {
|
|
123
|
+
if (segmentIndex === segments.length) {
|
|
124
|
+
/* Pattern exhausted — the path must be too (no extra segments). */
|
|
125
|
+
return pathIndex === pathSegments.length
|
|
126
|
+
}
|
|
127
|
+
const segment = segments[segmentIndex]
|
|
128
|
+
if (segment === undefined) {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
if (segment.kind === 'param' && segment.catchAll) {
|
|
132
|
+
params[segment.name] = pathSegments.slice(pathIndex).map(decodeParam).join('/')
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
const value = pathSegments[pathIndex]
|
|
136
|
+
if (segment.kind === 'literal') {
|
|
137
|
+
if (value !== segment.value) {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
return matchFrom(segments, pathSegments, segmentIndex + 1, pathIndex + 1, params)
|
|
141
|
+
}
|
|
142
|
+
if (segment.optional) {
|
|
143
|
+
/* Consume first; on failure restore the prior capture (a same-named
|
|
144
|
+
earlier `[name]` must survive the backtrack) and retry as absent. An
|
|
145
|
+
optional never captures an empty segment. */
|
|
146
|
+
if (value !== undefined && value !== '') {
|
|
147
|
+
const previous = params[segment.name]
|
|
148
|
+
params[segment.name] = decodeParam(value)
|
|
149
|
+
if (matchFrom(segments, pathSegments, segmentIndex + 1, pathIndex + 1, params)) {
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
if (previous === undefined) {
|
|
153
|
+
delete params[segment.name]
|
|
154
|
+
} else {
|
|
155
|
+
params[segment.name] = previous
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (matchFrom(segments, pathSegments, segmentIndex + 1, pathIndex, params)) {
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
/* Absent at the bare root: `/` splits into ['', ''] and slash collapsing
|
|
162
|
+
can't remove that dangling empty segment, so an absent optional also
|
|
163
|
+
swallows it — `/[[lang]]` must match `/` (the path url() generates). */
|
|
164
|
+
if (value === '') {
|
|
165
|
+
return matchFrom(segments, pathSegments, segmentIndex + 1, pathIndex + 1, params)
|
|
166
|
+
}
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
/* A required `[name]` param never captures an empty segment (e.g. `/users//5`). */
|
|
170
|
+
if (value === undefined || value === '') {
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
params[segment.name] = decodeParam(value)
|
|
174
|
+
return matchFrom(segments, pathSegments, segmentIndex + 1, pathIndex + 1, params)
|
|
175
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The canonical slash form of a pathname: duplicate slashes collapsed, one
|
|
3
|
+
trailing slash stripped (except root) — so `//users/` canonicalizes to
|
|
4
|
+
`/users`. matchRoute matches this form on both sides, and the server 308s a
|
|
5
|
+
page request whose raw pathname differs (see createServer), so the app.handle
|
|
6
|
+
auth seam always guards exactly the string the matcher routes.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizePathname(pathname: string): string {
|
|
9
|
+
const collapsed = pathname.includes('//') ? pathname.replace(/\/{2,}/g, '/') : pathname
|
|
10
|
+
return collapsed.length > 1 && collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed
|
|
11
|
+
}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createResolverSlot } from './createResolverSlot.ts'
|
|
2
|
+
import type { PageSnapshot } from './types/PageSnapshot.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
The active-page slot. The server entry installs an ALS-backed resolver
|
|
6
|
+
(request-scoped, so concurrent and streaming renders never share state); the
|
|
7
|
+
client entry a module-singleton one. With no resolver registered, a single
|
|
8
|
+
empty snapshot is created lazily so isolated tests work; test helpers
|
|
9
|
+
snapshot/poke `.resolver` and `.fallback` directly. activePage is the public
|
|
10
|
+
read.
|
|
7
11
|
*/
|
|
8
|
-
export const pageSlot =
|
|
12
|
+
export const pageSlot = createResolverSlot<PageSnapshot>(() => ({
|
|
13
|
+
route: '',
|
|
14
|
+
params: {},
|
|
15
|
+
url: new URL('http://localhost/'),
|
|
16
|
+
navigating: false,
|
|
17
|
+
}))
|