@abide/abide 0.46.0 → 0.48.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 +370 -320
- package/CHANGELOG.md +94 -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/buildCacheSnapshot.ts +6 -6
- 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/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorCacheSnapshot.ts +4 -4
- package/src/lib/server/runtime/types/InspectorContext.ts +1 -1
- 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 +522 -117
- package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
- package/src/lib/shared/cacheStoreSlot.ts +9 -5
- package/src/lib/shared/cacheStores.ts +3 -3
- 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/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/sharedCacheStore.ts +14 -0
- package/src/lib/shared/sharedCacheStoreSlot.ts +10 -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 +23 -11
- package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
- package/src/lib/shared/types/CacheStats.ts +1 -1
- 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 +36 -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 +8 -8
- 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 +17 -31
- 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 +13 -11
- 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/globalCacheStore.ts +0 -15
- package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
- package/src/lib/shared/globalCacheStoreSlot.ts +0 -9
- 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,5 @@
|
|
|
1
|
-
import { deferResume } from './deferResume.ts'
|
|
2
1
|
import { resumeSeedScript } from './resumeSeedScript.ts'
|
|
3
|
-
import type {
|
|
2
|
+
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
4
3
|
import type { SsrAwait, SsrRender } from './runtime/types/SsrRender.ts'
|
|
5
4
|
import { tryEncodeResume } from './tryEncodeResume.ts'
|
|
6
5
|
|
|
@@ -40,8 +39,8 @@ export async function* renderToStream(
|
|
|
40
39
|
instead of refetching. (`resume` is the render body's live object, so late writes
|
|
41
40
|
appear here.) */
|
|
42
41
|
const seededResume = new Set<number>(Object.keys(resume).map(Number))
|
|
43
|
-
const resumeDelta = (): Record<number, ResumeEntry
|
|
44
|
-
const delta: Record<number, ResumeEntry
|
|
42
|
+
const resumeDelta = (): Record<number, ResumeEntry> => {
|
|
43
|
+
const delta: Record<number, ResumeEntry> = {}
|
|
45
44
|
for (const [key, entry] of Object.entries(resume)) {
|
|
46
45
|
const id = Number(key)
|
|
47
46
|
if (!seededResume.has(id)) {
|
|
@@ -84,22 +83,19 @@ export async function* renderToStream(
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
type Settled = { id: number; html: string; resume: ResumeEntry
|
|
86
|
+
type Settled = { id: number; html: string; resume: ResumeEntry }
|
|
88
87
|
|
|
89
88
|
/* Awaits one streaming block's promise and renders the resolved or error branch to
|
|
90
89
|
HTML (the renderers are async so a nested `await` block composes), capturing the
|
|
91
90
|
value (serializable) for the resume manifest. Errors serialize as their message —
|
|
92
|
-
enough for the catch branch, without leaking a stack.
|
|
93
|
-
|
|
94
|
-
branch inert instead of decoding the value; a non-cache value ships inline as before. */
|
|
91
|
+
enough for the catch branch, without leaking a stack. The resolved value ships inline
|
|
92
|
+
so hydration adopts the streamed branch warm. */
|
|
95
93
|
function settle(block: SsrAwait): Promise<Settled> {
|
|
96
|
-
|
|
97
|
-
const pending = block.promise()
|
|
98
|
-
return Promise.resolve(pending).then(
|
|
94
|
+
return Promise.resolve(block.promise()).then(
|
|
99
95
|
async (value) => ({
|
|
100
96
|
id: block.id,
|
|
101
97
|
html: await block.then(value),
|
|
102
|
-
resume:
|
|
98
|
+
resume: { ok: true, value },
|
|
103
99
|
}),
|
|
104
100
|
async (error) => {
|
|
105
101
|
/* No catch branch → surface the rejection (500 before the first flush,
|
|
@@ -123,6 +119,6 @@ function settle(block: SsrAwait): Promise<Settled> {
|
|
|
123
119
|
strings. `tryEncodeResume` handles the serialize-or-refetch policy (undefined → no
|
|
124
120
|
script → the swap consumers skip registration → hydration re-runs that one promise).
|
|
125
121
|
`applyResolved`/the inline swap script store it via `.textContent`; `awaitBlock` decodes it. */
|
|
126
|
-
function encodeStreamResume(resume: ResumeEntry
|
|
122
|
+
function encodeStreamResume(resume: ResumeEntry, id: number): string | undefined {
|
|
127
123
|
return tryEncodeResume(resume, id)?.replace(/</g, '\\u003c')
|
|
128
124
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { safeJsonForScript } from '../shared/safeJsonForScript.ts'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
3
3
|
import { tryEncodeResume } from './tryEncodeResume.ts'
|
|
4
4
|
|
|
5
5
|
/* A self-contained `<script>` seeding the await-resume manifest with the blocking
|
|
@@ -11,7 +11,7 @@ import { tryEncodeResume } from './tryEncodeResume.ts'
|
|
|
11
11
|
early or parse as a line terminator. Shared by the buffered (`createUiPageRenderer`)
|
|
12
12
|
and streaming (`renderToStream`) paths. */
|
|
13
13
|
// @documentation plumbing
|
|
14
|
-
export function resumeSeedScript(resume: Record<number, ResumeEntry
|
|
14
|
+
export function resumeSeedScript(resume: Record<number, ResumeEntry>): string {
|
|
15
15
|
/* ref-json (not JSON) so a value carrying cycles or shared back-references — a
|
|
16
16
|
media tree with parent↔child links — seeds instead of being dropped. `tryEncodeResume`
|
|
17
17
|
drops just an unserializable entry (the client re-runs that one branch's promise),
|
package/src/lib/ui/router.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { hydratingSlot } from '../shared/hydratingSlot.ts'
|
|
1
2
|
import { layoutChainForRoute } from '../shared/layoutChainForRoute.ts'
|
|
3
|
+
import { matchRoute } from '../shared/matchRoute.ts'
|
|
4
|
+
import { wakeHydrationPeeks } from '../shared/wakeHydrationPeeks.ts'
|
|
2
5
|
import { fillBoundary } from './dom/fillBoundary.ts'
|
|
3
6
|
import { outlet } from './dom/outlet.ts'
|
|
4
7
|
import { effect } from './effect.ts'
|
|
5
|
-
import { matchRoute } from './matchRoute.ts'
|
|
6
8
|
import { navigatePath } from './navigate.ts'
|
|
7
9
|
import { CHILD_PRESENT } from './runtime/CHILD_PRESENT.ts'
|
|
8
10
|
import { clientPage } from './runtime/clientPage.ts'
|
|
@@ -81,7 +83,8 @@ function clearRecoveryReloads(url: string): void {
|
|
|
81
83
|
|
|
82
84
|
/*
|
|
83
85
|
A minimal client router on the History API. `router` matches the current path
|
|
84
|
-
against the route patterns (literal / `[name]` / `[...rest]`, via
|
|
86
|
+
against the route patterns (literal / `[name]` / `[[name]]` / `[...rest]`, via
|
|
87
|
+
the shared matchRoute — the same matcher the server dispatches with),
|
|
85
88
|
imports the matching page's chunk — plus every `layout.abide` chunk that wraps it
|
|
86
89
|
(`layoutChainForRoute`) — on demand, and mounts them as a chain: each layout into
|
|
87
90
|
the previous layout's `<slot/>` outlet, the page into the innermost outlet.
|
|
@@ -249,13 +252,21 @@ export function router(
|
|
|
249
252
|
}
|
|
250
253
|
if (hydrating) {
|
|
251
254
|
const previous = RENDER.hydration
|
|
255
|
+
const previousHydrating = hydratingSlot.active
|
|
252
256
|
RENDER.hydration = { next: new Map() }
|
|
257
|
+
hydratingSlot.active = true
|
|
253
258
|
enterRenderPass()
|
|
254
259
|
try {
|
|
255
260
|
run()
|
|
256
261
|
} finally {
|
|
257
262
|
exitRenderPass()
|
|
258
263
|
RENDER.hydration = previous
|
|
264
|
+
hydratingSlot.active = previousHydrating
|
|
265
|
+
/* Wake the peeks this pass withheld for SSR congruence, now that the pass is
|
|
266
|
+
over and the warm value is congruent to show. Only on the outermost unwind. */
|
|
267
|
+
if (!previousHydrating) {
|
|
268
|
+
wakeHydrationPeeks()
|
|
269
|
+
}
|
|
259
270
|
}
|
|
260
271
|
return
|
|
261
272
|
}
|
|
@@ -428,6 +439,14 @@ export function router(
|
|
|
428
439
|
])
|
|
429
440
|
.then(([pageView, resolvedLayouts, decision]) => {
|
|
430
441
|
if (token !== sequence || disposed) {
|
|
442
|
+
/* Consume the first-paint flag even when superseded: the boot SSR DOM
|
|
443
|
+
is valid only for THIS boot navigation. Leaving `first` true would
|
|
444
|
+
let a navigation that started before this settled (to a different
|
|
445
|
+
page) skip its auth probe and hydrate the boot URL's SSR DOM as its
|
|
446
|
+
own — claimExpected then throws and commit swallows it, stranding a
|
|
447
|
+
broken page. Cleared here → the superseding nav builds create-mode
|
|
448
|
+
(clearing the stale SSR DOM) and runs its probe. */
|
|
449
|
+
first = false
|
|
431
450
|
return
|
|
432
451
|
}
|
|
433
452
|
/* handle() redirected: go where it pointed, replacing the blocked
|
|
@@ -16,12 +16,6 @@
|
|
|
16
16
|
same reference. */
|
|
17
17
|
export type ResumeEntry = { ok: true; value: unknown } | { ok: false; error: unknown }
|
|
18
18
|
|
|
19
|
-
/* Deferred-resume marker (Tier 2): a large cache-backed `{#await cache()}` ships this in
|
|
20
|
-
place of its value — just the cache key, so hydration adopts the server branch inert and
|
|
21
|
-
pays no value decode. The client seeds that key lazily and materializes it only on a later
|
|
22
|
-
re-read. Discriminated from a ResumeEntry by the `defer` field. */
|
|
23
|
-
export type DeferMarker = { defer: true; key: string }
|
|
24
|
-
|
|
25
19
|
const globalScope = globalThis as { __abideResume?: Record<number, string> }
|
|
26
20
|
globalScope.__abideResume ??= {}
|
|
27
21
|
|
|
@@ -9,7 +9,7 @@ with `enterScope`/`exitScope` and `await`s inline between them (blocking `{#awai
|
|
|
9
9
|
child renders, slots, a top-level `await`). A module global held across those awaits
|
|
10
10
|
interleaves across concurrent requests — one render resumes to read another's scope.
|
|
11
11
|
Keying the ambient off the per-request store (which the async context propagates
|
|
12
|
-
correctly) isolates it. Mirrors `
|
|
12
|
+
correctly) isolates it. Mirrors `requestScopeSlot`'s server-installed slot; the
|
|
13
13
|
indirection cost lands at build/effect-creation time, never on the signal hot path.
|
|
14
14
|
*/
|
|
15
15
|
type ScopeBacking = { get(): Scope | undefined; set(value: Scope | undefined): void }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResumeEntry } from '../RESUME.ts'
|
|
2
2
|
|
|
3
3
|
/* One STREAMING await block captured during SSR (no `then` on the `await` tag): its
|
|
4
4
|
boundary id, the promise to await, and the async string-renderers for the resolved
|
|
@@ -18,11 +18,10 @@ export type SsrAwait = {
|
|
|
18
18
|
/* The result of a component's server `render()`: the pending-shell HTML, the
|
|
19
19
|
serializable document snapshot for client resume, the STREAMING await blocks to
|
|
20
20
|
flush out of order, and `resume` — the inline-rendered BLOCKING await values keyed
|
|
21
|
-
by boundary id, seeded into the manifest so hydration adopts them without a refetch.
|
|
22
|
-
deferred (cache-backed) blocking value is a `DeferMarker` in place of the value. */
|
|
21
|
+
by boundary id, seeded into the manifest so hydration adopts them without a refetch. */
|
|
23
22
|
export type SsrRender = {
|
|
24
23
|
html: string
|
|
25
24
|
state: unknown
|
|
26
25
|
awaits: SsrAwait[]
|
|
27
|
-
resume: Record<number, ResumeEntry
|
|
26
|
+
resume: Record<number, ResumeEntry>
|
|
28
27
|
}
|
package/src/lib/ui/scope.ts
CHANGED
|
@@ -5,10 +5,13 @@ import type { Scope } from './types/Scope.ts'
|
|
|
5
5
|
/*
|
|
6
6
|
Resolves the current lexical scope — established per lexical level by the compiler,
|
|
7
7
|
so it reads "where you are" with no handle. Outside any scope (boot, a script) it
|
|
8
|
-
mints a detached root once and reuses it.
|
|
9
|
-
|
|
8
|
+
mints a detached root once and reuses it. Now the INTERNAL lowering host: the author
|
|
9
|
+
reactive surface is the imported `state`/`state.linked`/`state.computed`/`effect`; the
|
|
10
|
+
compiler lowers each onto this scope (`$$scope().derive`/`.linked`/`.effect`), and
|
|
11
|
+
`state.share`/`.shared` route through it. Still published so generated code and the
|
|
12
|
+
type-checking shadow can import it; not part of the author-facing surface.
|
|
10
13
|
*/
|
|
11
|
-
// @documentation
|
|
14
|
+
// @documentation plumbing
|
|
12
15
|
export function scope(): Scope {
|
|
13
16
|
if (!CURRENT_SCOPE.current) {
|
|
14
17
|
CURRENT_SCOPE.current = createScope()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { baseSlot } from '../shared/baseSlot.ts'
|
|
2
|
+
import { createTraceContext } from '../shared/createTraceContext.ts'
|
|
3
|
+
import { healthSeedSlot } from '../shared/healthSeedSlot.ts'
|
|
4
|
+
import { requestScopeSlot } from '../shared/requestScopeSlot.ts'
|
|
5
|
+
import { rpcTimeoutSlot } from '../shared/rpcTimeoutSlot.ts'
|
|
6
|
+
import { setAppName } from '../shared/setAppName.ts'
|
|
7
|
+
import type { SsrBootState } from '../shared/types/SsrBootState.ts'
|
|
8
|
+
|
|
9
|
+
/* Seeds every `__SSR__` boot-state field into its ambient slot. The map is typed
|
|
10
|
+
EXHAUSTIVELY over `SsrBootState`, so a boot field can't be added to the payload
|
|
11
|
+
without a seeder here — the compile error is what guarantees a stamped field is
|
|
12
|
+
never silently dropped client-side. Base is seeded first (object-key order), as the
|
|
13
|
+
prior hand-written sequence required: the cache seed that follows keys call urls
|
|
14
|
+
through the base resolver. */
|
|
15
|
+
const SEED: { [K in keyof Required<SsrBootState>]: (value: SsrBootState[K]) => void } = {
|
|
16
|
+
base: (value) => {
|
|
17
|
+
baseSlot.resolver = () => value ?? ''
|
|
18
|
+
},
|
|
19
|
+
app: (value) => setAppName(value),
|
|
20
|
+
health: (value) => {
|
|
21
|
+
healthSeedSlot.payload = value
|
|
22
|
+
},
|
|
23
|
+
clientTimeout: (value) => {
|
|
24
|
+
rpcTimeoutSlot.ms = value
|
|
25
|
+
},
|
|
26
|
+
/* Continue the SSR request's trace (or mint a fresh sampled one if the stamp is
|
|
27
|
+
missing) and publish the browser's request scope: trace(), log line prefixes, and
|
|
28
|
+
the RPC traceparent header resolve through it. elapsedMs is navigation-relative —
|
|
29
|
+
exactly what performance.now() measures — and the path tracks client navigations
|
|
30
|
+
because the resolver reads location per call. */
|
|
31
|
+
trace: (value) => {
|
|
32
|
+
const traceContext = createTraceContext(value)
|
|
33
|
+
requestScopeSlot.resolver = () => ({
|
|
34
|
+
trace: traceContext,
|
|
35
|
+
elapsedMs: performance.now(),
|
|
36
|
+
method: 'GET',
|
|
37
|
+
path: typeof location === 'undefined' ? '' : location.pathname,
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Applies one seeder to its own field — generic over the key so the seeder and its
|
|
43
|
+
value stay correlated (a bare `SEED[key](boot[key])` loses the correlation). */
|
|
44
|
+
function seedField<K extends keyof SsrBootState>(key: K, boot: SsrBootState): void {
|
|
45
|
+
SEED[key](boot[key])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Seeds all boot-state slots from the payload, before mount. */
|
|
49
|
+
export function seedBootState(boot: SsrBootState): void {
|
|
50
|
+
for (const key of Object.keys(SEED) as Array<keyof SsrBootState>) {
|
|
51
|
+
seedField(key, boot)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { SocketClientFrame } from '../server/sockets/types/SocketClientFrame.ts'
|
|
2
|
-
import type { SocketServerFrame } from '../server/sockets/types/SocketServerFrame.ts'
|
|
3
1
|
import { createSocketSubRegistry } from '../shared/createSocketSubRegistry.ts'
|
|
4
2
|
import { decodeRefJson } from '../shared/decodeRefJson.ts'
|
|
5
3
|
import { encodeRefJson } from '../shared/encodeRefJson.ts'
|
|
6
4
|
import { SOCKETS_PATH } from '../shared/SOCKETS_PATH.ts'
|
|
7
5
|
import type { SocketChannel } from '../shared/types/SocketChannel.ts'
|
|
6
|
+
import type { SocketClientFrame } from '../shared/types/SocketClientFrame.ts'
|
|
7
|
+
import type { SocketServerFrame } from '../shared/types/SocketServerFrame.ts'
|
|
8
8
|
import { withBase } from '../shared/withBase.ts'
|
|
9
9
|
|
|
10
10
|
let singleton: SocketChannel | undefined
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { Socket } from '../server/sockets/types/Socket.ts'
|
|
2
1
|
import { buildSocketOverChannel } from '../shared/buildSocketOverChannel.ts'
|
|
2
|
+
import type { Socket } from '../shared/types/Socket.ts'
|
|
3
3
|
import { getSocketChannel } from './socketChannel.ts'
|
|
4
|
+
import { watch } from './watch.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
7
|
Client-side substitute for a server-declared Socket. The bundler emits
|
|
@@ -10,7 +11,7 @@ over the multiplexed ws channel). Both paths produce identical Socket
|
|
|
10
11
|
shapes so user code reads the same on either side.
|
|
11
12
|
|
|
12
13
|
The Socket surface — bare iteration as the live stream, `.tail(n)` seeded
|
|
13
|
-
from the retained tail, `.
|
|
14
|
+
from the retained tail, `.broadcast` sending a server-validated `pub` frame —
|
|
14
15
|
is built by buildSocketOverChannel over the page's lazily-opened singleton
|
|
15
16
|
channel; this module only binds that builder to the browser channel so the
|
|
16
17
|
test harness can reuse the identical surface over its own channel.
|
|
@@ -21,5 +22,10 @@ socketProxy API, not the wire layer.
|
|
|
21
22
|
*/
|
|
22
23
|
// @documentation plumbing
|
|
23
24
|
export function socketProxy<T>(name: string): Socket<T> {
|
|
24
|
-
|
|
25
|
+
const socket = buildSocketOverChannel<T>(name, getSocketChannel)
|
|
26
|
+
/* Overwrite the shared builder's inert `.watch` with the real reaction sugar
|
|
27
|
+
(`socket.watch(handler)` ≡ `watch(socket, handler)`). Attached here so the ui-only
|
|
28
|
+
`watch` primitive never rides into a server bundle. */
|
|
29
|
+
socket.watch = (handler) => watch(socket, handler)
|
|
30
|
+
return socket
|
|
25
31
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
+
import { cacheStoreSlot } from '../shared/cacheStoreSlot.ts'
|
|
1
2
|
import { createCacheStore } from '../shared/createCacheStore.ts'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { setBaseResolver } from '../shared/setBaseResolver.ts'
|
|
6
|
-
import { setCacheStoreResolver } from '../shared/setCacheStoreResolver.ts'
|
|
7
|
-
import { setGlobalCacheStoreResolver } from '../shared/setGlobalCacheStoreResolver.ts'
|
|
8
|
-
import { setPageResolver } from '../shared/setPageResolver.ts'
|
|
9
|
-
import type { CacheSnapshotEntry } from '../shared/types/CacheSnapshotEntry.ts'
|
|
3
|
+
import { pageSlot } from '../shared/pageSlot.ts'
|
|
4
|
+
import { sharedCacheStoreSlot } from '../shared/sharedCacheStoreSlot.ts'
|
|
5
|
+
import type { SsrPayload } from '../shared/types/SsrPayload.ts'
|
|
10
6
|
import type { StreamedResolution } from '../shared/types/StreamedResolution.ts'
|
|
11
7
|
import { probeNavigation } from './probeNavigation.ts'
|
|
8
|
+
import { seedBootState } from './seedBootState.ts'
|
|
12
9
|
|
|
13
10
|
/* Build-time flag the production client defines false (see build.ts `define`) so the dev-only
|
|
14
11
|
hot bridge — and the entire DOM runtime it statically pulls in for re-builds — is dead-code-
|
|
@@ -21,15 +18,6 @@ import { clientPage } from './runtime/clientPage.ts'
|
|
|
21
18
|
import type { RouteLoader } from './runtime/types/RouteLoader.ts'
|
|
22
19
|
import { seedResolved } from './seedResolved.ts'
|
|
23
20
|
|
|
24
|
-
/* The server's __SSR__ payload this entry consumes. */
|
|
25
|
-
type SsrPayload = {
|
|
26
|
-
cache?: CacheSnapshotEntry[]
|
|
27
|
-
base?: string
|
|
28
|
-
app?: string
|
|
29
|
-
health?: Record<string, unknown>
|
|
30
|
-
clientTimeout?: number
|
|
31
|
-
}
|
|
32
|
-
|
|
33
21
|
/*
|
|
34
22
|
The official abide-ui client entry. Reads the server's `window.__SSR__` payload,
|
|
35
23
|
seeds a tab-scoped cache store from the inline snapshot (so a warm `cache()` read
|
|
@@ -67,23 +55,21 @@ export function startClient(
|
|
|
67
55
|
if ((globalThis as { __abideInspect?: boolean }).__abideInspect) {
|
|
68
56
|
import('./installInspectorBridge.ts').then((module) => module.installInspectorBridge())
|
|
69
57
|
}
|
|
70
|
-
const ssr = (globalThis as { __SSR__?: SsrPayload }).__SSR__ ?? {}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
and unbounded RPC fetches. */
|
|
77
|
-
|
|
78
|
-
healthSeedSlot.payload = ssr.health
|
|
79
|
-
rpcTimeoutSlot.ms = ssr.clientTimeout
|
|
58
|
+
const ssr = (globalThis as { __SSR__?: Partial<SsrPayload> }).__SSR__ ?? {}
|
|
59
|
+
/* Seed the per-page __SSR__ stamps into their shared slots before mount: the mount
|
|
60
|
+
base, app name (default log channel), health payload (so health()'s first probe is
|
|
61
|
+
warm), and the env-configured RPC timeout (ABIDE_CLIENT_TIMEOUT, shipped per
|
|
62
|
+
request). Driven by the exhaustive `seedBootState` map so a new boot field can't be
|
|
63
|
+
stamped server-side and silently dropped here. Without seeding: no mount base,
|
|
64
|
+
channel 'app', a cold first health probe, and unbounded RPC fetches. */
|
|
65
|
+
seedBootState(ssr)
|
|
80
66
|
/* The `page` proxy reads route/params/url off the router-updated snapshot. */
|
|
81
|
-
|
|
67
|
+
pageSlot.resolver = () => clientPage.value
|
|
82
68
|
|
|
83
69
|
const store = createCacheStore()
|
|
84
|
-
|
|
85
|
-
/* One tab store: cache(fn, {
|
|
86
|
-
|
|
70
|
+
cacheStoreSlot.resolver = () => store
|
|
71
|
+
/* One tab store: cache(fn, { shared: true }) shares it, so shared is a no-op here. */
|
|
72
|
+
sharedCacheStoreSlot.resolver = () => store
|
|
87
73
|
/* Seed both SSR cache partitions through the one streamed-resolution sink: `ssr.cache`
|
|
88
74
|
(inline — reads settled at render-return, in __SSR__) and `__abideResumeCache` (pending
|
|
89
75
|
{#await} reads whose `__abideResolve(...)` chunks the stream pushed during parse, before
|
package/src/lib/ui/state.ts
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
|
+
import { computed } from './computed.ts'
|
|
2
|
+
import { linked } from './linked.ts'
|
|
1
3
|
import { createSignalNode } from './runtime/createSignalNode.ts'
|
|
2
4
|
import { readNode } from './runtime/readNode.ts'
|
|
3
5
|
import type { State } from './runtime/types/State.ts'
|
|
4
6
|
import { writeNode } from './runtime/writeNode.ts'
|
|
7
|
+
import { scope } from './scope.ts'
|
|
8
|
+
|
|
9
|
+
/* The imported reactive surface: the `state` callable plus its attached members.
|
|
10
|
+
`state`/`state.linked`/`state.computed` are the reactive primitives an author
|
|
11
|
+
imports (`import { state } from '@abide/abide/ui/state'`); the compiler resolves
|
|
12
|
+
the import binding (alias-safe) and lowers each onto the ambient scope. `.share`/
|
|
13
|
+
`.shared` are the context seam — the reference store the runtime scope exposes,
|
|
14
|
+
reached ambiently so a component can pass a named value down its subtree. */
|
|
15
|
+
type StateFn = {
|
|
16
|
+
/* No-arg form for an undefined initial with a declared type: `state<Foo>()` is
|
|
17
|
+
`State<Foo | undefined>`. Without it `state<Foo>(undefined)` is an arity/assign
|
|
18
|
+
error and `state(undefined)` infers `T = undefined` (every `.value` access then
|
|
19
|
+
narrows to `never`). */
|
|
20
|
+
<T>(): State<T | undefined>
|
|
21
|
+
<T>(initial: T, transform?: (next: T, previous: T) => T): State<T>
|
|
22
|
+
/* A writable cell reseeded from a reactive thunk (`state.linked(() => src())`). */
|
|
23
|
+
linked: typeof linked
|
|
24
|
+
/* A read-only cell computed from other cells (`state.computed(() => a() + b())`). */
|
|
25
|
+
computed: typeof computed
|
|
26
|
+
/* Puts a named value on the ambient scope, read down the tree by `state.shared`. */
|
|
27
|
+
share: (key: string, value: unknown) => void
|
|
28
|
+
/* Reads the closest ancestor scope that shared `key`; undefined if none provided. */
|
|
29
|
+
shared: <T>(key: string) => T | undefined
|
|
30
|
+
}
|
|
5
31
|
|
|
6
32
|
/*
|
|
7
33
|
A writable reactive cell — abide's from-scratch reactive primitive, with no
|
|
8
34
|
compiler sigil and no external reactivity-library import. `.value` is a
|
|
9
35
|
plain getter/setter over a signal node, so a read/write shows up as exactly that
|
|
10
|
-
in a stack trace.
|
|
11
|
-
|
|
12
|
-
doc slot, and keeps `state(initial, transform)` as a `.value`
|
|
36
|
+
in a stack trace. Imported and called bare (`let count = state(0)`); the compiler
|
|
37
|
+
resolves the `state` import binding (alias-safe), desugars plain `state(initial)` to
|
|
38
|
+
a serializable `model` doc slot, and keeps `state(initial, transform)` as a `.value`
|
|
39
|
+
cell routed onto the ambient scope; the runtime needs no magic.
|
|
13
40
|
|
|
14
41
|
`transform` is an optional coercion gate on the write path: every `.value =`
|
|
15
42
|
runs it and stores what it returns, with `previous` for clamp-relative writes or
|
|
@@ -18,16 +45,7 @@ mirror of `computed`'s write-through `set` — here the value lives in this cell
|
|
|
18
45
|
the gate *returns* what to store rather than writing an external target. The
|
|
19
46
|
construction `initial` is taken verbatim; the gate runs on writes only.
|
|
20
47
|
*/
|
|
21
|
-
|
|
22
|
-
`State<Foo | undefined>`. Without it `state<Foo>(undefined)` is an arity/assign
|
|
23
|
-
error and `state(undefined)` infers `T = undefined` (every `.value` access then
|
|
24
|
-
narrows to `never`). */
|
|
25
|
-
export function state<T>(): State<T | undefined>
|
|
26
|
-
export function state<T>(initial: T, transform?: (next: T, previous: T) => T): State<T>
|
|
27
|
-
export function state<T>(
|
|
28
|
-
initial?: T,
|
|
29
|
-
transform?: (next: T, previous: T) => T,
|
|
30
|
-
): State<T | undefined> {
|
|
48
|
+
function stateCell<T>(initial?: T, transform?: (next: T, previous: T) => T): State<T | undefined> {
|
|
31
49
|
const node = createSignalNode(initial)
|
|
32
50
|
return {
|
|
33
51
|
get value(): T | undefined {
|
|
@@ -38,3 +56,16 @@ export function state<T>(
|
|
|
38
56
|
},
|
|
39
57
|
}
|
|
40
58
|
}
|
|
59
|
+
|
|
60
|
+
const stateFn = stateCell as StateFn
|
|
61
|
+
stateFn.linked = linked
|
|
62
|
+
stateFn.computed = computed
|
|
63
|
+
/* `.share`/`.shared` route onto the ambient scope, read/written at call time so the
|
|
64
|
+
state ↔ scope module cycle resolves. */
|
|
65
|
+
stateFn.share = (key: string, value: unknown): void => {
|
|
66
|
+
scope().share(key, value)
|
|
67
|
+
}
|
|
68
|
+
stateFn.shared = <T>(key: string): T | undefined => scope().shared<T>(key)
|
|
69
|
+
|
|
70
|
+
// @documentation reactive-state
|
|
71
|
+
export const state = stateFn
|
package/src/lib/ui/sync.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PATCH_BUS } from './runtime/PATCH_BUS.ts'
|
|
2
2
|
import type { Doc } from './runtime/types/Doc.ts'
|
|
3
|
+
import type { Patch } from './runtime/types/Patch.ts'
|
|
3
4
|
import type { SyncTransport } from './types/SyncTransport.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
@@ -17,20 +18,25 @@ this core). Returns a disposer.
|
|
|
17
18
|
*/
|
|
18
19
|
// @documentation plumbing
|
|
19
20
|
export function sync(doc: Doc, transport: SyncTransport): () => void {
|
|
20
|
-
/*
|
|
21
|
-
|
|
21
|
+
/* The specific inbound patches currently being applied — their own bus echo must not be
|
|
22
|
+
re-sent. Tracked by IDENTITY (not a blanket `applying` flag): `doc.apply` emits the
|
|
23
|
+
patch on the bus inside its batch, then flushes effects on batch exit while still
|
|
24
|
+
synchronously inside this apply. A local effect that writes the doc in reaction emits
|
|
25
|
+
a DIFFERENT patch during that flush — a genuine new local change peers need — which a
|
|
26
|
+
blanket flag would wrongly suppress, diverging peers permanently. */
|
|
27
|
+
const applyingPatches = new Set<Patch>()
|
|
22
28
|
|
|
23
29
|
const unsubscribeInbound = transport.subscribe((patch) => {
|
|
24
|
-
|
|
30
|
+
applyingPatches.add(patch)
|
|
25
31
|
try {
|
|
26
32
|
doc.apply(patch)
|
|
27
33
|
} finally {
|
|
28
|
-
|
|
34
|
+
applyingPatches.delete(patch)
|
|
29
35
|
}
|
|
30
36
|
})
|
|
31
37
|
|
|
32
38
|
const unsubscribeBus = PATCH_BUS.subscribe((event) => {
|
|
33
|
-
if (event.doc === doc && !
|
|
39
|
+
if (event.doc === doc && !applyingPatches.has(event.patch)) {
|
|
34
40
|
transport.send(event.patch)
|
|
35
41
|
}
|
|
36
42
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { encodeRefJson } from '../shared/encodeRefJson.ts'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
3
3
|
|
|
4
4
|
/* ref-json-encode an await-resume entry, or `undefined` if it can't be serialized.
|
|
5
5
|
encodeRefJson is total (cycles become back-references, functions fold to undefined),
|
|
@@ -7,10 +7,7 @@ import type { DeferMarker, ResumeEntry } from './runtime/RESUME.ts'
|
|
|
7
7
|
entry and warn, so the client re-runs that one branch's promise while every other
|
|
8
8
|
branch stays seeded. Shared by the streaming (`renderToStream`) and buffered/seed
|
|
9
9
|
(`resumeSeedScript`) paths so the serialize-or-refetch policy lives in one place. */
|
|
10
|
-
export function tryEncodeResume(
|
|
11
|
-
entry: ResumeEntry | DeferMarker,
|
|
12
|
-
id: number | string,
|
|
13
|
-
): string | undefined {
|
|
10
|
+
export function tryEncodeResume(entry: ResumeEntry, id: number | string): string | undefined {
|
|
14
11
|
try {
|
|
15
12
|
return encodeRefJson(entry)
|
|
16
13
|
} catch (cause) {
|
|
@@ -7,18 +7,22 @@ import type { SyncTransport } from './SyncTransport.ts'
|
|
|
7
7
|
|
|
8
8
|
/*
|
|
9
9
|
A lexical scope: the unit that owns a region's reactive data, its lifetime, and
|
|
10
|
-
the capabilities applied to it.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
the capabilities applied to it. This is the INTERNAL shape — the compiler's lowering
|
|
11
|
+
host and the runtime object; it is no longer the author surface. The author reactive
|
|
12
|
+
surface is the imported `state`/`state.linked`/`state.computed`/`effect` (see
|
|
13
|
+
`state.ts`); the compiler resolves those import bindings and lowers each onto this
|
|
14
|
+
scope (`$$scope().derive`/`.linked`/`.effect`, `state.share`/`.shared` → `share`/
|
|
15
|
+
`shared`). The data surface MIRRORS `Doc` (read/replace/add/remove/derive/apply/
|
|
16
|
+
snapshot) so the compiler can target a scope as a component's data binding directly;
|
|
17
|
+
it nests (`child`/`root`) and passes values down the tree as context (`share`/`shared`).
|
|
15
18
|
|
|
19
|
+
The reactive primitives and capabilities remain on this internal shape because the
|
|
20
|
+
lowered runtime calls them (`$$scope().linked(...)`, generated undo/persist) — they are
|
|
21
|
+
withdrawn from the AUTHOR-facing public surface (docs/examples), not the runtime object.
|
|
16
22
|
Capabilities route where the scope's changes go: `record()` to an undo journal,
|
|
17
|
-
`persist()` to durable storage, `broadcast()` to peers — declared once, then
|
|
18
|
-
`
|
|
19
|
-
|
|
20
|
-
but only for generated binding code and the SSR strip (tagged plumbing). The
|
|
21
|
-
`history`/`persist`/`sync` helpers it composes are internal.
|
|
23
|
+
`persist()` to durable storage, `broadcast()` to peers — declared once, then `undo`/
|
|
24
|
+
`redo` act on a recorded scope. The `history`/`persist`/`sync` helpers it composes are
|
|
25
|
+
internal.
|
|
22
26
|
*/
|
|
23
27
|
export type Scope = {
|
|
24
28
|
readonly id: string
|