@abide/abide 0.53.0 → 0.55.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 +330 -494
- package/CHANGELOG.md +106 -0
- package/README.md +166 -146
- package/package.json +3 -4
- package/src/checkAbide.ts +1 -1
- package/src/lib/server/rpc/defineRpc.ts +2 -1
- package/src/lib/server/rpc/types/RpcHelper.ts +29 -22
- package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +3 -2
- package/src/lib/server/runtime/amendBroadcaster.ts +53 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +54 -20
- package/src/lib/server/runtime/runWithRequestScope.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +9 -0
- package/src/lib/server/sockets/amendFamilyEntry.ts +39 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -0
- package/src/lib/server/sockets/defineSocket.ts +11 -1
- package/src/lib/shared/AMEND_TOPIC_PREFIX.ts +9 -0
- package/src/lib/shared/SEEDS.ts +33 -0
- package/src/lib/shared/SOCKET_SEED.ts +12 -0
- package/src/lib/shared/amend.ts +48 -0
- package/src/lib/shared/amendBroadcastSlot.ts +19 -0
- package/src/lib/shared/applyAmendLocally.ts +20 -0
- package/src/lib/shared/attachRpcSelectorMethods.ts +5 -5
- package/src/lib/shared/buildSocketOverChannel.ts +9 -2
- package/src/lib/shared/cache.ts +203 -84
- package/src/lib/shared/cacheReaderSocketSlot.ts +11 -0
- package/src/lib/shared/createCacheStore.ts +8 -0
- package/src/lib/shared/createRemoteFunction.ts +4 -1
- package/src/lib/shared/encodeSeedValue.ts +24 -0
- package/src/lib/shared/jsonSchemaForType.ts +3 -16
- package/src/lib/shared/peek.ts +4 -7
- package/src/lib/shared/socketTailsSlot.ts +13 -0
- package/src/lib/shared/types/AmendApply.ts +15 -0
- package/src/lib/shared/types/CacheReaderHook.ts +11 -0
- package/src/lib/shared/types/RemoteFunction.ts +30 -19
- package/src/lib/shared/types/SocketTails.ts +13 -0
- package/src/lib/shared/types/SsrPayload.ts +6 -0
- package/src/lib/test/createTestApp.ts +6 -0
- package/src/lib/ui/amendReaderHook.ts +125 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -6
- package/src/lib/ui/compile/analyzeComponent.ts +0 -2
- package/src/lib/ui/compile/awaitSubjectExpr.ts +23 -0
- package/src/lib/ui/compile/cachedShadowProgram.ts +39 -0
- package/src/lib/ui/compile/compileComponent.ts +1 -3
- package/src/lib/ui/compile/componentOrdinals.ts +39 -0
- package/src/lib/ui/compile/desugarSignals.ts +65 -36
- package/src/lib/ui/compile/generateBuild.ts +25 -24
- package/src/lib/ui/compile/generateSSR.ts +37 -24
- package/src/lib/ui/compile/hoistableAwaits.ts +1 -11
- package/src/lib/ui/compile/interpolationClassifierForRoot.ts +8 -13
- package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +46 -5
- package/src/lib/ui/compile/lowerCompoundAssignment.ts +49 -0
- package/src/lib/ui/compile/lowerContext.ts +26 -6
- package/src/lib/ui/compile/lowerDocAccess.ts +19 -31
- package/src/lib/ui/compile/lowerScript.ts +1 -11
- package/src/lib/ui/compile/lowerUpdateExpression.ts +32 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +25 -45
- package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +10 -15
- package/src/lib/ui/compile/shadowInterpolationClassifier.ts +1 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +3 -5
- package/src/lib/ui/compile/wrapReactionCellSources.ts +10 -6
- package/src/lib/ui/compile/wrapReactiveSeeds.ts +44 -0
- package/src/lib/ui/compile/wrapSeed.ts +28 -0
- package/src/lib/ui/computed.ts +16 -0
- package/src/lib/ui/createScope.ts +4 -3
- package/src/lib/ui/dom/anchorCursor.ts +2 -1
- package/src/lib/ui/dom/anchoredBranch.ts +14 -3
- package/src/lib/ui/dom/appendStatic.ts +11 -20
- package/src/lib/ui/dom/appendText.ts +41 -43
- package/src/lib/ui/dom/appendTextAt.ts +10 -57
- package/src/lib/ui/dom/awaitBlock.ts +31 -45
- package/src/lib/ui/dom/awaitSubject.ts +118 -0
- package/src/lib/ui/dom/claimText.ts +38 -0
- package/src/lib/ui/dom/cloneStatic.ts +2 -8
- package/src/lib/ui/dom/discardAndRebuild.ts +23 -0
- package/src/lib/ui/dom/discardBoundary.ts +5 -4
- package/src/lib/ui/dom/each.ts +3 -5
- package/src/lib/ui/dom/fillBoundary.ts +2 -1
- package/src/lib/ui/dom/hydrate.ts +5 -12
- package/src/lib/ui/dom/mountChild.ts +73 -5
- package/src/lib/ui/dom/mountSwappableRange.ts +2 -1
- package/src/lib/ui/dom/openMarker.ts +2 -4
- package/src/lib/ui/dom/outlet.ts +4 -3
- package/src/lib/ui/dom/readCell.ts +26 -11
- package/src/lib/ui/dom/skeleton.ts +2 -7
- package/src/lib/ui/dom/switchBlock.ts +2 -1
- package/src/lib/ui/dom/tryBlock.ts +27 -39
- package/src/lib/ui/dom/when.ts +6 -2
- package/src/lib/ui/linked.ts +14 -0
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +12 -16
- package/src/lib/ui/runtime/AsyncCellError.ts +2 -1
- package/src/lib/ui/runtime/BRANCH_SEGMENT.ts +24 -0
- package/src/lib/ui/runtime/CELL_SEED.ts +4 -6
- package/src/lib/ui/runtime/DOC_SEED.ts +5 -6
- package/src/lib/ui/runtime/RENDER.ts +7 -5
- package/src/lib/ui/runtime/RESUME.ts +5 -7
- package/src/lib/ui/runtime/SEED_MARKS.ts +14 -0
- package/src/lib/ui/runtime/SuspenseSignal.ts +2 -2
- package/src/lib/ui/runtime/claimChild.ts +5 -7
- package/src/lib/ui/runtime/claimExpected.ts +2 -6
- package/src/lib/ui/runtime/claimMarker.ts +13 -0
- package/src/lib/ui/runtime/claimRun.ts +22 -0
- package/src/lib/ui/runtime/consumeSeed.ts +38 -0
- package/src/lib/ui/runtime/createAsyncCell.ts +56 -8
- package/src/lib/ui/runtime/createComputedNode.ts +1 -0
- package/src/lib/ui/runtime/createDoc.ts +5 -1
- package/src/lib/ui/runtime/createEffectNode.ts +1 -0
- package/src/lib/ui/runtime/createSignalNode.ts +1 -0
- package/src/lib/ui/runtime/parkCursor.ts +10 -0
- package/src/lib/ui/runtime/readNode.ts +21 -1
- package/src/lib/ui/runtime/renderPath.ts +2 -2
- package/src/lib/ui/runtime/routeBareSeed.ts +28 -0
- package/src/lib/ui/runtime/runHydrationPass.ts +69 -0
- package/src/lib/ui/runtime/runNode.ts +14 -0
- package/src/lib/ui/runtime/types/Cell.ts +4 -2
- package/src/lib/ui/runtime/types/Doc.ts +3 -1
- package/src/lib/ui/runtime/types/HydrationCursor.ts +8 -0
- package/src/lib/ui/runtime/types/ReactiveNode.ts +5 -0
- package/src/lib/ui/settleAsyncCells.ts +30 -3
- package/src/lib/ui/socketProxy.ts +17 -1
- package/src/lib/ui/startClient.ts +23 -0
- package/src/lib/ui/tryEncodeResume.ts +6 -15
- package/src/lib/ui/types/Scope.ts +1 -1
- package/src/lib/ui/watch.ts +21 -16
- package/src/serverEntry.ts +15 -0
- package/src/lib/shared/patch.ts +0 -41
- package/src/lib/ui/compile/COMPOUND_ASSIGNMENT_OPERATORS.ts +0 -19
- package/src/lib/ui/dom/mountStreamedChild.ts +0 -84
- package/src/lib/ui/dom/readCellBlocking.ts +0 -29
|
@@ -10,15 +10,42 @@ earlier settled layer's cells. `Promise.allSettled` never rejects the render: a
|
|
|
10
10
|
rejected cell settles into its `error()` state, surfaced at the read site (via
|
|
11
11
|
`$$readCell`), not here. On the client the list is always empty (cells register
|
|
12
12
|
server-side only), so the barrier is a no-op await.
|
|
13
|
+
|
|
14
|
+
It drains to a FIXPOINT — "await until this branch of the graph stops pausing" —
|
|
15
|
+
not a single snapshot. Pending propagates down dependency edges: a blocking cell
|
|
16
|
+
whose seed reads a still-pending blocking dependency stays pending and registers no
|
|
17
|
+
promise (its seed threw a `SuspenseSignal`, caught as pending, not error). When the
|
|
18
|
+
dependency settles inside this await, the reactive flush is synchronous, so the
|
|
19
|
+
dependent's seed re-runs and registers ITS promise before the loop re-checks — the
|
|
20
|
+
whole chain resolves in order. The per-render list isolation (`isolateCellBarrier`)
|
|
21
|
+
keeps the loop draining only this render's cascade, never a concurrent sibling's.
|
|
22
|
+
|
|
23
|
+
The round cap bounds a pathological feedback edge — a cell whose settle writes a
|
|
24
|
+
signal its own seed reads re-registers a fresh promise every round, and an
|
|
25
|
+
unbounded fixpoint would hang the SSR request forever. Legitimate chains resolve
|
|
26
|
+
one dependency level per round, so the cap is far above any real graph; hitting
|
|
27
|
+
it renders with whatever settled (the old snapshot-drain behavior) and warns.
|
|
13
28
|
*/
|
|
29
|
+
const MAX_SETTLE_ROUNDS = 1000
|
|
30
|
+
|
|
14
31
|
// @documentation plumbing
|
|
15
32
|
export async function settleAsyncCells(): Promise<void> {
|
|
16
33
|
const pending = activePendingCells()
|
|
17
34
|
if (pending === undefined) {
|
|
18
35
|
return
|
|
19
36
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
let rounds = 0
|
|
38
|
+
while (pending.promises.length > 0) {
|
|
39
|
+
if (rounds >= MAX_SETTLE_ROUNDS) {
|
|
40
|
+
console.warn(
|
|
41
|
+
'[abide] settleAsyncCells did not reach a fixpoint after ' +
|
|
42
|
+
`${MAX_SETTLE_ROUNDS} rounds — an async cell is re-registering ` +
|
|
43
|
+
'a promise on every settle (a seed that reads state its own ' +
|
|
44
|
+
'resolution writes). Rendering with the values settled so far.',
|
|
45
|
+
)
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
rounds += 1
|
|
49
|
+
await Promise.allSettled(pending.promises.splice(0))
|
|
23
50
|
}
|
|
24
51
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { buildSocketOverChannel } from '../shared/buildSocketOverChannel.ts'
|
|
2
|
+
import { decodeRefJson } from '../shared/decodeRefJson.ts'
|
|
3
|
+
import { SOCKET_SEED } from '../shared/SOCKET_SEED.ts'
|
|
2
4
|
import type { Socket } from '../shared/types/Socket.ts'
|
|
3
5
|
import { getSocketChannel } from './socketChannel.ts'
|
|
4
6
|
import { watch } from './watch.ts'
|
|
@@ -22,7 +24,21 @@ socketProxy API, not the wire layer.
|
|
|
22
24
|
*/
|
|
23
25
|
// @documentation plumbing
|
|
24
26
|
export function socketProxy<T>(name: string): Socket<T> {
|
|
25
|
-
|
|
27
|
+
/* Warm-seed from the server's retained frame (shipped in `__SSR__.sockets`, drained into
|
|
28
|
+
SOCKET_SEED by startClient before mount) so `peek(socket)` returns the SAME value the SSR
|
|
29
|
+
render committed to instead of undefined on this not-yet-connected client — otherwise the two
|
|
30
|
+
disagree and hydration discards the server markup. A frame that failed to serialize server-side
|
|
31
|
+
simply isn't present, and the socket falls back to a cold peek. */
|
|
32
|
+
const seeded = SOCKET_SEED[name]
|
|
33
|
+
let initialFrame: T | undefined
|
|
34
|
+
if (seeded !== undefined) {
|
|
35
|
+
try {
|
|
36
|
+
initialFrame = decodeRefJson(seeded) as T
|
|
37
|
+
} catch {
|
|
38
|
+
initialFrame = undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const socket = buildSocketOverChannel<T>(name, getSocketChannel, initialFrame)
|
|
26
42
|
/* Overwrite the shared builder's inert `.watch` with the real reaction sugar
|
|
27
43
|
(`socket.watch(handler)` ≡ `watch(socket, handler)`). Attached here so the ui-only
|
|
28
44
|
`watch` primitive never rides into a server bundle. */
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { amendBroadcastSlot } from '../shared/amendBroadcastSlot.ts'
|
|
2
|
+
import { applyAmendLocally } from '../shared/applyAmendLocally.ts'
|
|
1
3
|
import { applyCacheStalenessLocally } from '../shared/applyCacheStalenessLocally.ts'
|
|
4
|
+
import { cacheReaderSocketSlot } from '../shared/cacheReaderSocketSlot.ts'
|
|
2
5
|
import { cacheStalenessSlot } from '../shared/cacheStalenessSlot.ts'
|
|
3
6
|
import { cacheStoreSlot } from '../shared/cacheStoreSlot.ts'
|
|
4
7
|
import { createCacheStore } from '../shared/createCacheStore.ts'
|
|
5
8
|
import { pageSlot } from '../shared/pageSlot.ts'
|
|
9
|
+
import { SOCKET_SEED } from '../shared/SOCKET_SEED.ts'
|
|
6
10
|
import { sharedCacheStoreSlot } from '../shared/sharedCacheStoreSlot.ts'
|
|
7
11
|
import type { SsrPayload } from '../shared/types/SsrPayload.ts'
|
|
8
12
|
import type { StreamedResolution } from '../shared/types/StreamedResolution.ts'
|
|
13
|
+
import { createAmendReaderHook } from './amendReaderHook.ts'
|
|
9
14
|
import { probeNavigation } from './probeNavigation.ts'
|
|
10
15
|
import { router } from './router.ts'
|
|
11
16
|
import { CELL_SEED } from './runtime/CELL_SEED.ts'
|
|
@@ -57,6 +62,13 @@ export function startClient(
|
|
|
57
62
|
broadcaster instead — the ADR-0041 side-swap). Same function as the slot fallback so
|
|
58
63
|
the local-apply path can't diverge. */
|
|
59
64
|
cacheStalenessSlot.resolver = () => applyCacheStalenessLocally
|
|
65
|
+
/* amend(args, value) applies to THIS tab's cache too — the server entry broadcasts instead
|
|
66
|
+
(the ADR-0043 side-swap). Same function as the slot fallback for the same reason. */
|
|
67
|
+
amendBroadcastSlot.resolver = () => applyAmendLocally
|
|
68
|
+
/* Open/close a per-call amend value subscription as reactive readers of a key come and go
|
|
69
|
+
(ADR-0043), so a server amend(args, value) push lands on keys this tab is reading. */
|
|
70
|
+
const amendReader = createAmendReaderHook()
|
|
71
|
+
cacheReaderSocketSlot.resolver = () => amendReader.hook
|
|
60
72
|
/* Seed both SSR cache partitions through the one streamed-resolution sink: `ssr.cache`
|
|
61
73
|
(inline — reads settled at render-return, in __SSR__) and `__abideResumeCache` (pending
|
|
62
74
|
{#await} reads whose `__abideResolve(...)` chunks the stream pushed during parse, before
|
|
@@ -80,6 +92,12 @@ export function startClient(
|
|
|
80
92
|
if (ssr.docs !== undefined) {
|
|
81
93
|
Object.assign(DOC_SEED, ssr.docs)
|
|
82
94
|
}
|
|
95
|
+
/* Seed the socket warm partition into SOCKET_SEED before mount: a hydrating `socketProxy` reads
|
|
96
|
+
its name key to seed `lastFrame`, so `peek(socket)` returns the server's retained frame instead
|
|
97
|
+
of undefined on the not-yet-connected client — congruent with the SSR HTML. */
|
|
98
|
+
if (ssr.sockets !== undefined) {
|
|
99
|
+
Object.assign(SOCKET_SEED, ssr.sockets)
|
|
100
|
+
}
|
|
83
101
|
/* Keep the cache channel live past boot: replace the head's buffering collector with
|
|
84
102
|
the store-connected sink so a post-boot resolution — the inline doc-stream cache
|
|
85
103
|
script — seeds the store through `seedStreamedResolution` directly instead of pushing
|
|
@@ -94,6 +112,11 @@ export function startClient(
|
|
|
94
112
|
const disposeRouter = router(target, routes, layoutRoutes, probeNavigation)
|
|
95
113
|
return () => {
|
|
96
114
|
disposeStaleness()
|
|
115
|
+
amendReader.dispose()
|
|
116
|
+
/* Clear the reader hook so a post-teardown cache read doesn't re-open amend subscriptions
|
|
117
|
+
— unlike the staleness/store slots (whose leaked resolver stays benign), a live hook on
|
|
118
|
+
the read path must not outlive the client it belongs to. */
|
|
119
|
+
cacheReaderSocketSlot.resolver = undefined
|
|
97
120
|
disposeRouter()
|
|
98
121
|
}
|
|
99
122
|
}
|
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { encodeSeedValue } from '../shared/encodeSeedValue.ts'
|
|
2
2
|
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
3
3
|
|
|
4
|
-
/* ref-json-encode an await-resume entry, or `undefined` if it can't be serialized
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
entry and warn, so the client re-runs that one branch's promise while every other
|
|
4
|
+
/* ref-json-encode an await-resume entry, or `undefined` if it can't be serialized — the
|
|
5
|
+
await-resume arm of the shared serialize-or-degrade policy (`encodeSeedValue`): drop
|
|
6
|
+
just this entry, so the client re-runs that one branch's promise while every other
|
|
8
7
|
branch stays seeded. Shared by the streaming (`renderToStream`) and buffered/seed
|
|
9
|
-
(`resumeSeedScript`) paths
|
|
8
|
+
(`resumeSeedScript`) paths. */
|
|
10
9
|
export function tryEncodeResume(entry: ResumeEntry, id: number | string): string | undefined {
|
|
11
|
-
|
|
12
|
-
return encodeRefJson(entry)
|
|
13
|
-
} catch (cause) {
|
|
14
|
-
console.warn(
|
|
15
|
-
`[abide] resume for await ${id} is not serializable; client will refetch it`,
|
|
16
|
-
cause,
|
|
17
|
-
)
|
|
18
|
-
return undefined
|
|
19
|
-
}
|
|
10
|
+
return encodeSeedValue(entry, `resume for await ${id}`, 'the client will refetch it')
|
|
20
11
|
}
|
|
@@ -25,7 +25,7 @@ export type Scope = {
|
|
|
25
25
|
readonly parent: Scope | undefined
|
|
26
26
|
/* data — mirrors Doc */
|
|
27
27
|
read: <T>(path: string) => T
|
|
28
|
-
replace: (path: string, value: unknown) =>
|
|
28
|
+
replace: (path: string, value: unknown) => unknown
|
|
29
29
|
add: (path: string, value: unknown) => void
|
|
30
30
|
remove: (path: string) => void
|
|
31
31
|
apply: (patch: Patch) => void
|
package/src/lib/ui/watch.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cache } from '../shared/cache.ts'
|
|
2
|
+
import { isAsyncIterable } from '../shared/isAsyncIterable.ts'
|
|
2
3
|
import { REMOTE_FUNCTION } from '../shared/REMOTE_FUNCTION.ts'
|
|
3
4
|
import type { CacheOnContext } from '../shared/types/CacheOnContext.ts'
|
|
4
5
|
import type { NamedAsyncIterable } from '../shared/types/NamedAsyncIterable.ts'
|
|
@@ -8,6 +9,7 @@ import { effect } from './effect.ts'
|
|
|
8
9
|
import { generationGuard } from './runtime/generationGuard.ts'
|
|
9
10
|
import type { EffectResult } from './runtime/types/EffectResult.ts'
|
|
10
11
|
import type { State } from './runtime/types/State.ts'
|
|
12
|
+
import { untrack } from './runtime/untrack.ts'
|
|
11
13
|
|
|
12
14
|
/*
|
|
13
15
|
The single reaction primitive: `watch(source, handler)` names its trigger and runs
|
|
@@ -25,6 +27,14 @@ into a server bundle; server-side it is an inert no-op. Unlike bare `watch(…)`
|
|
|
25
27
|
the SSR back-end strips — a `.watch(…)` member call survives to the server and relies on
|
|
26
28
|
that inert stub.
|
|
27
29
|
|
|
30
|
+
Tracking contract (ADR-0044): the named source(s) are the sole triggers — the handler
|
|
31
|
+
is a sink, run untracked, so a reactive read in its body never becomes an accidental
|
|
32
|
+
extra trigger. Only the bare `watch(thunk)` form auto-tracks everything it reads (that
|
|
33
|
+
IS its job — it is the compiler binding). This makes all explicit-source forms uniform:
|
|
34
|
+
the subscribable (cache.on, per-frame) and rpc (handler in a .then microtask) branches
|
|
35
|
+
already ran their handlers out of the tracking window; the two synchronous cell branches
|
|
36
|
+
now match by wrapping the handler in `untrack`.
|
|
37
|
+
|
|
28
38
|
Sources (discriminated at runtime, monomorphic per branch):
|
|
29
39
|
watch(thunk) // compiler binding form — auto-tracked, == effect(thunk)
|
|
30
40
|
watch(count, n => …) // a state cell → handler(newValue)
|
|
@@ -77,7 +87,7 @@ export function watch(
|
|
|
77
87
|
const handler = argsOrHandler as (value: unknown, context?: CacheOnContext) => void
|
|
78
88
|
/* A subscribable (socket / stream): per-frame delivery with reconnect-replay — the
|
|
79
89
|
existing cache.on loop is the single implementation this branch delegates to. */
|
|
80
|
-
if (
|
|
90
|
+
if (isAsyncIterable(source)) {
|
|
81
91
|
return cache.on(
|
|
82
92
|
source as NamedAsyncIterable<unknown>,
|
|
83
93
|
handler as (frame: unknown, context: CacheOnContext) => void | Promise<void>,
|
|
@@ -85,7 +95,7 @@ export function watch(
|
|
|
85
95
|
}
|
|
86
96
|
/* An rpc without args → run the smart read reactively, pipe its value to the handler. A
|
|
87
97
|
RemoteFunction is a callable, so accept `function` too — a state cell is a branded-free
|
|
88
|
-
object and never matches, and a streaming rpc was already caught by
|
|
98
|
+
object and never matches, and a streaming rpc was already caught by isAsyncIterable above. */
|
|
89
99
|
if (
|
|
90
100
|
source !== null &&
|
|
91
101
|
(typeof source === 'object' || typeof source === 'function') &&
|
|
@@ -93,34 +103,29 @@ export function watch(
|
|
|
93
103
|
) {
|
|
94
104
|
return reactToRpc(source, undefined, handler)
|
|
95
105
|
}
|
|
96
|
-
/* Multiple cells → fire on any change; hand the handler the current values.
|
|
106
|
+
/* Multiple cells → fire on any change; hand the handler the current values. Read every
|
|
107
|
+
cell (the tracked triggers), then run the handler untracked so a reactive read in its
|
|
108
|
+
body is not captured as an extra trigger (ADR-0044). */
|
|
97
109
|
if (Array.isArray(source)) {
|
|
98
110
|
const cells = source as ReadonlyArray<State<unknown>>
|
|
99
111
|
return effect(() => {
|
|
100
112
|
const values = cells.map((cell) => cell.value)
|
|
101
|
-
handler(values)
|
|
113
|
+
untrack(() => handler(values))
|
|
102
114
|
})
|
|
103
115
|
}
|
|
104
|
-
/* A single state cell → handler(newValue) on change.
|
|
116
|
+
/* A single state cell → handler(newValue) on change. The cell read is the tracked
|
|
117
|
+
trigger; the handler is a sink, run untracked (ADR-0044). */
|
|
105
118
|
const cell = source as State<unknown>
|
|
106
119
|
return effect(() => {
|
|
107
|
-
|
|
120
|
+
const value = cell.value
|
|
121
|
+
untrack(() => handler(value))
|
|
108
122
|
})
|
|
109
123
|
}
|
|
110
124
|
|
|
111
|
-
/* True for a socket / rpc stream — anything async-iterable. */
|
|
112
|
-
function isSubscribable(source: unknown): boolean {
|
|
113
|
-
return (
|
|
114
|
-
source !== null &&
|
|
115
|
-
(typeof source === 'object' || typeof source === 'function') &&
|
|
116
|
-
typeof (source as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function'
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
125
|
/*
|
|
121
126
|
Reacts to an rpc's cached value: an effect that runs the smart read (subscribing
|
|
122
127
|
its key synchronously, so the effect re-runs when the value changes — a refresh,
|
|
123
|
-
|
|
128
|
+
an amend, an invalidate) and pipes the resolved value to the handler. Triggers the
|
|
124
129
|
read, unlike peek: `watch` observes a live query, so it keeps it flowing.
|
|
125
130
|
*/
|
|
126
131
|
function reactToRpc(fn: unknown, args: unknown, handler: (value: unknown) => void): () => void {
|
package/src/serverEntry.ts
CHANGED
|
@@ -26,10 +26,12 @@ import { shell } from './_virtual/shell.ts'
|
|
|
26
26
|
import { sockets } from './_virtual/sockets.ts'
|
|
27
27
|
import { exitWithParent } from './lib/bundle/exitWithParent.ts'
|
|
28
28
|
import { loadEnvFromBinaryDir } from './lib/cli/loadEnvFromBinaryDir.ts'
|
|
29
|
+
import { broadcastAmend } from './lib/server/runtime/amendBroadcaster.ts'
|
|
29
30
|
import { broadcastCacheStaleness } from './lib/server/runtime/cacheStalenessBroadcaster.ts'
|
|
30
31
|
import { createServer } from './lib/server/runtime/createServer.ts'
|
|
31
32
|
import { requestContext } from './lib/server/runtime/requestContext.ts'
|
|
32
33
|
import { resolvePageSnapshot } from './lib/server/runtime/resolvePageSnapshot.ts'
|
|
34
|
+
import { amendBroadcastSlot } from './lib/shared/amendBroadcastSlot.ts'
|
|
33
35
|
import { cacheStalenessSlot } from './lib/shared/cacheStalenessSlot.ts'
|
|
34
36
|
import { cacheStoreSlot } from './lib/shared/cacheStoreSlot.ts'
|
|
35
37
|
import { createCacheStore } from './lib/shared/createCacheStore.ts'
|
|
@@ -40,6 +42,7 @@ import { pendingAsyncCellsSlot } from './lib/shared/pendingAsyncCellsSlot.ts'
|
|
|
40
42
|
import { resolvedCellsSlot } from './lib/shared/resolvedCellsSlot.ts'
|
|
41
43
|
import { runningAsStandaloneBinary } from './lib/shared/runningAsStandaloneBinary.ts'
|
|
42
44
|
import { sharedCacheStoreSlot } from './lib/shared/sharedCacheStoreSlot.ts'
|
|
45
|
+
import { socketTailsSlot } from './lib/shared/socketTailsSlot.ts'
|
|
43
46
|
import { streamedCellsSlot } from './lib/shared/streamedCellsSlot.ts'
|
|
44
47
|
|
|
45
48
|
/*
|
|
@@ -89,6 +92,14 @@ so its server socket code never enters the client reachability graph.
|
|
|
89
92
|
*/
|
|
90
93
|
cacheStalenessSlot.resolver = () => broadcastCacheStaleness
|
|
91
94
|
|
|
95
|
+
/*
|
|
96
|
+
Server-side amend(args, value) broadcasts the keyed value to every authorized reader
|
|
97
|
+
instead of mutating a throwaway request store (ADR-0043). Imported ONLY here so the
|
|
98
|
+
broadcaster's socket code never enters the client reachability graph — the same import
|
|
99
|
+
discipline as the staleness broadcaster above.
|
|
100
|
+
*/
|
|
101
|
+
amendBroadcastSlot.resolver = () => broadcastAmend
|
|
102
|
+
|
|
92
103
|
/* One process-wide fallback list for reads with no request in flight (boot/cron/socket) —
|
|
93
104
|
real requests each carry their own `pendingAsyncCells`, so the SSR barrier drains a
|
|
94
105
|
per-request list and concurrent renders never cross-contaminate. */
|
|
@@ -99,6 +110,10 @@ const sharedResolvedCells = { entries: [] }
|
|
|
99
110
|
resolvedCellsSlot.resolver = () => requestContext.getStore()?.resolvedCells ?? sharedResolvedCells
|
|
100
111
|
const sharedStreamedCells = { entries: [] }
|
|
101
112
|
streamedCellsSlot.resolver = () => requestContext.getStore()?.streamedCells ?? sharedStreamedCells
|
|
113
|
+
/* No shared fallback: `defineSocket.peek` records into this on every retained-frame read, including
|
|
114
|
+
reads outside any request (ws handlers, boot) — a fallback list would accumulate entries no render
|
|
115
|
+
ever drains. Off-request this resolves undefined and the record is skipped (see socketTailsSlot). */
|
|
116
|
+
socketTailsSlot.resolver = () => requestContext.getStore()?.socketTails
|
|
102
117
|
const sharedDocSnapshots = { entries: [] }
|
|
103
118
|
docSnapshotsSlot.resolver = () => requestContext.getStore()?.docSnapshots ?? sharedDocSnapshots
|
|
104
119
|
|
package/src/lib/shared/patch.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript'
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
Maps a compound-assignment operator to its plain binary counterpart, for lowering
|
|
5
|
-
`x += y` into an unconditional read-combine-write. Logical assignments
|
|
6
|
-
(`||=`/`&&=`/`??=`) lower the same way — the patch/cell write always fires,
|
|
7
|
-
consistent with how `+=` lowers. Shared by lowerDocAccess (the `$$model` write path)
|
|
8
|
-
and renameSignalRefs (the `$$writeCell` linked-cell path) so the two lowerings can't
|
|
9
|
-
drift.
|
|
10
|
-
*/
|
|
11
|
-
export const COMPOUND_ASSIGNMENT_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
12
|
-
[ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.PlusToken],
|
|
13
|
-
[ts.SyntaxKind.MinusEqualsToken, ts.SyntaxKind.MinusToken],
|
|
14
|
-
[ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskToken],
|
|
15
|
-
[ts.SyntaxKind.SlashEqualsToken, ts.SyntaxKind.SlashToken],
|
|
16
|
-
[ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.BarBarToken],
|
|
17
|
-
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, ts.SyntaxKind.AmpersandAmpersandToken],
|
|
18
|
-
[ts.SyntaxKind.QuestionQuestionEqualsToken, ts.SyntaxKind.QuestionQuestionToken],
|
|
19
|
-
])
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
|
|
2
|
-
import { claimChild } from '../runtime/claimChild.ts'
|
|
3
|
-
import { OWNER } from '../runtime/OWNER.ts'
|
|
4
|
-
import { RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
5
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
|
-
import type { UiComponent } from '../runtime/types/UiComponent.ts'
|
|
7
|
-
import { withOptionalPath } from '../runtime/withOptionalPath.ts'
|
|
8
|
-
import { withoutHydration } from '../runtime/withoutHydration.ts'
|
|
9
|
-
import { discardBoundary } from './discardBoundary.ts'
|
|
10
|
-
import { mountRange } from './mountRange.ts'
|
|
11
|
-
import { openMarker } from './openMarker.ts'
|
|
12
|
-
|
|
13
|
-
/*
|
|
14
|
-
The client mount for a HOISTABLE child (ADR-0039) — a DUAL-MODE adopter. The server decides per
|
|
15
|
-
render whether a hoistable child inlined (fast/settled) or streamed (still-pending), and the client
|
|
16
|
-
build can't know which, so this probes the hydration cursor:
|
|
17
|
-
|
|
18
|
-
- `<!--[-->` (RANGE_OPEN) next → the server INLINED the child; adopt its range exactly as
|
|
19
|
-
`mountChild` does (byte-identical to the pre-ADR-0039 path).
|
|
20
|
-
- `<!--abide:await:CHILDPATH-->` next → the server STREAMED it; `__abideSwap` has already spliced
|
|
21
|
-
the child's `<!--[-->…<!--]-->` between the boundary markers before the bundle ran, so claim the
|
|
22
|
-
boundary open, adopt the inner range in place, claim the boundary close.
|
|
23
|
-
- no hydration (a client-side navigation) → a plain create-mode mount, like `mountChild`.
|
|
24
|
-
|
|
25
|
-
CHILDPATH is the child's render-path — `withPath(ordinal, () => CURRENT_PATH.current)` — computed the
|
|
26
|
-
same way the server's `renderPath(ordinal)` wrote the boundary id, so the two agree with no counter.
|
|
27
|
-
A streamed child re-mounts (no RESUME value of its own — the boundary is html-only); nested `{#await}`
|
|
28
|
-
blocks and async cells inside it adopt through their own existing channels.
|
|
29
|
-
*/
|
|
30
|
-
// @documentation plumbing
|
|
31
|
-
export function mountStreamedChild(
|
|
32
|
-
parent: Node,
|
|
33
|
-
factory: UiComponent,
|
|
34
|
-
props: Parameters<UiComponent>[1],
|
|
35
|
-
before: Node | null = null,
|
|
36
|
-
ordinal?: number,
|
|
37
|
-
): void {
|
|
38
|
-
const run = <T>(build: () => T): T => withOptionalPath(ordinal, build)
|
|
39
|
-
const mount = (): { dispose: () => void } =>
|
|
40
|
-
run(() => mountRange(parent, factory.build, props, before))
|
|
41
|
-
|
|
42
|
-
const hydration = RENDER.hydration
|
|
43
|
-
if (hydration === undefined) {
|
|
44
|
-
/* Call mount() first, THEN register its dispose — `OWNER.current?.push(mount())` would
|
|
45
|
-
short-circuit the whole expression (never mounting) when there is no owner. */
|
|
46
|
-
const handle = mount()
|
|
47
|
-
OWNER.current?.push(handle.dispose)
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/* Probe the cursor without consuming it. Streamed iff the next node is this child's boundary. */
|
|
52
|
-
const childPath = run(() => CURRENT_PATH.current)
|
|
53
|
-
const cursor = claimChild(hydration, parent)
|
|
54
|
-
const streamed =
|
|
55
|
-
cursor !== null &&
|
|
56
|
-
cursor.nodeType === 8 &&
|
|
57
|
-
(cursor as Comment).data === `abide:await:${childPath}`
|
|
58
|
-
|
|
59
|
-
if (!streamed) {
|
|
60
|
-
/* INLINE — the server inlined a settled child; adopt its `[ … ]` range in place. */
|
|
61
|
-
const handle = mount()
|
|
62
|
-
OWNER.current?.push(handle.dispose)
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/* STREAMED — claim the boundary open (advances the cursor to the swapped-in `[`). */
|
|
67
|
-
const open = openMarker(parent, `abide:await:${childPath}`)
|
|
68
|
-
const inner = claimChild(hydration, parent)
|
|
69
|
-
const warm = inner !== null && inner.nodeType === 8 && (inner as Comment).data === RANGE_OPEN
|
|
70
|
-
if (warm) {
|
|
71
|
-
const handle = mount()
|
|
72
|
-
openMarker(parent, `/abide:await:${childPath}`)
|
|
73
|
-
OWNER.current?.push(handle.dispose)
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
/* COLD (defensive — renderToStream drains every await before closing, so a warm boundary is the
|
|
77
|
-
deterministic path). The fragment never arrived: discard the empty boundary and create-mount
|
|
78
|
-
the child fresh at that position; its streamed cells resolve post-mount via receiveStreamedCell. */
|
|
79
|
-
const after = discardBoundary(parent, open, `/abide:await:${childPath}`, hydration)
|
|
80
|
-
const handle = withoutHydration(() =>
|
|
81
|
-
run(() => mountRange(parent, factory.build, props, after)),
|
|
82
|
-
)
|
|
83
|
-
OWNER.current?.push(handle.dispose)
|
|
84
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { isAsyncCell } from '../../shared/isAsyncCell.ts'
|
|
2
|
-
import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
|
|
3
|
-
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
4
|
-
import { readCell } from './readCell.ts'
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
The blocking-cell read the compiler lowers to `$$readCellBlocking(NAME)` for an `await`-marked
|
|
8
|
-
cell (ADR-0042 D5.3). Exactly `readCell`, with one added rule ahead of it: a cell with no value
|
|
9
|
-
*yet* throws a `SuspenseSignal` so the enclosing region SUSPENDS until the value resolves rather
|
|
10
|
-
than rendering against `undefined`. This is what makes an `await` binding read as a resolved `T`
|
|
11
|
-
(never `undefined`-while-pending) on both sides — the client mirrors the server flush barrier.
|
|
12
|
-
|
|
13
|
-
The suspend is keyed on `cell.pending()` (in-flight AND no value), NOT `peek() === undefined`:
|
|
14
|
-
- a blocking cell that legitimately resolves to `undefined` is not pending, so it does not
|
|
15
|
-
suspend forever;
|
|
16
|
-
- a warm-seeded cell on hydrate is `refreshing()` (has value, in flight), not `pending()`, so it
|
|
17
|
-
reads its held value and adopts the SSR markup with no suspend and no flash (ADR-0042 D4).
|
|
18
|
-
Everything past the pending gate — the throwing peek (error-with-no-retained-value → `AsyncCellError`
|
|
19
|
-
to `{#try}`, a retained value winning over pending/error via SWR), a function call, or a `.value`
|
|
20
|
-
read — is `readCell` verbatim, so the two reads can never drift.
|
|
21
|
-
*/
|
|
22
|
-
// @documentation plumbing
|
|
23
|
-
export function readCellBlocking(cell: unknown): unknown {
|
|
24
|
-
/* No value yet → suspend the reading region (distinct from an error, which `readCell` routes). */
|
|
25
|
-
if (isAsyncCell(cell) && (cell as AsyncComputed<unknown>).pending()) {
|
|
26
|
-
throw new SuspenseSignal(cell as AsyncComputed<unknown>)
|
|
27
|
-
}
|
|
28
|
-
return readCell(cell)
|
|
29
|
-
}
|