@abide/abide 0.41.0 → 0.41.1
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 +327 -326
- package/CHANGELOG.md +44 -0
- package/README.md +87 -78
- package/package.json +1 -1
- package/src/lib/ui/compile/SSR_ESCAPE.ts +3 -1
- package/src/lib/ui/compile/compileShadow.ts +5 -0
- package/src/lib/ui/compile/generateBuild.ts +11 -4
- package/src/lib/ui/compile/generateSSR.ts +17 -8
- package/src/lib/ui/compile/skeletonContext.ts +67 -79
- package/src/lib/ui/compile/templateAnchorAdapter.ts +61 -0
- package/src/lib/ui/compile/templateElementAdapter.ts +44 -0
- package/src/lib/ui/compile/walkAnchorOrder.ts +57 -0
- package/src/lib/ui/compile/walkElementOrder.ts +60 -0
- package/src/lib/ui/dom/appendSnippet.ts +9 -8
- package/src/lib/ui/dom/applyResolved.ts +3 -4
- package/src/lib/ui/dom/awaitBlock.ts +37 -31
- package/src/lib/ui/dom/buildDetachedRange.ts +30 -0
- package/src/lib/ui/dom/depthZeroNodes.ts +34 -0
- package/src/lib/ui/dom/domAnchorAdapter.ts +29 -0
- package/src/lib/ui/dom/domElementAdapter.ts +20 -0
- package/src/lib/ui/dom/each.ts +7 -11
- package/src/lib/ui/dom/eachAsync.ts +12 -17
- package/src/lib/ui/dom/isElement.ts +6 -0
- package/src/lib/ui/dom/markerDepthDelta.ts +19 -0
- package/src/lib/ui/dom/mountRange.ts +4 -3
- package/src/lib/ui/dom/mountSlot.ts +4 -3
- package/src/lib/ui/dom/on.ts +6 -1
- package/src/lib/ui/dom/replaceRange.ts +24 -0
- package/src/lib/ui/dom/skeleton.ts +35 -92
- package/src/lib/ui/dom/switchBlock.ts +13 -10
- package/src/lib/ui/dom/when.ts +13 -10
- package/src/lib/ui/runtime/RANGE_MARKER.ts +16 -0
- package/src/lib/ui/runtime/batch.ts +22 -0
- package/src/lib/ui/runtime/clientPage.ts +3 -8
- package/src/lib/ui/runtime/createDoc.ts +9 -13
- package/src/lib/ui/seedResolved.ts +28 -0
- package/src/lib/ui/startClient.ts +6 -4
- package/src/lib/ui/types/ResolvedFrame.ts +15 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { flushEffects } from './flushEffects.ts'
|
|
2
|
+
import { REACTIVE_CONTEXT } from './REACTIVE_CONTEXT.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Runs `fn` with reactive writes coalesced: effects dirtied inside queue once and
|
|
6
|
+
flush a single time when the outermost batch exits, so a burst of writes (e.g. an
|
|
7
|
+
event handler setting several signals) re-runs each dependent effect once instead
|
|
8
|
+
of once per write. Nests safely — only the depth-0 exit flushes — so a batched
|
|
9
|
+
write that calls into another batched write (a handler invoking a doc patch) still
|
|
10
|
+
flushes once, at the top. Same idiom `createDoc`/`clientPage` inline, factored out.
|
|
11
|
+
*/
|
|
12
|
+
export function batch<T>(fn: () => T): T {
|
|
13
|
+
REACTIVE_CONTEXT.batchDepth += 1
|
|
14
|
+
try {
|
|
15
|
+
return fn()
|
|
16
|
+
} finally {
|
|
17
|
+
REACTIVE_CONTEXT.batchDepth -= 1
|
|
18
|
+
if (REACTIVE_CONTEXT.batchDepth === 0) {
|
|
19
|
+
flushEffects()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { PageSnapshot } from '../../shared/types/PageSnapshot.ts'
|
|
2
2
|
import { state } from '../state.ts'
|
|
3
|
-
import {
|
|
4
|
-
import { REACTIVE_CONTEXT } from './REACTIVE_CONTEXT.ts'
|
|
3
|
+
import { batch } from './batch.ts'
|
|
5
4
|
import type { State } from './types/State.ts'
|
|
6
5
|
|
|
7
6
|
/*
|
|
@@ -107,15 +106,11 @@ export const clientPage: { value: PageSnapshot } = {
|
|
|
107
106
|
(e.g. `page.url` + `page.params.id`) re-runs once per field and transiently
|
|
108
107
|
observes a half-updated snapshot (new url, stale id). Same batch idiom as
|
|
109
108
|
`createDoc` — flush once, after every cell is reconciled. */
|
|
110
|
-
|
|
111
|
-
try {
|
|
109
|
+
batch(() => {
|
|
112
110
|
routeCell.value = next.route
|
|
113
111
|
urlCell.value = next.url
|
|
114
112
|
navigatingCell.value = next.navigating
|
|
115
113
|
reconcileParams(next.params)
|
|
116
|
-
}
|
|
117
|
-
REACTIVE_CONTEXT.batchDepth -= 1
|
|
118
|
-
}
|
|
119
|
-
flushEffects()
|
|
114
|
+
})
|
|
120
115
|
},
|
|
121
116
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { applyPatchToTree } from './applyPatchToTree.ts'
|
|
2
|
+
import { batch } from './batch.ts'
|
|
2
3
|
import { createComputedNode } from './createComputedNode.ts'
|
|
3
4
|
import { createSignalNode } from './createSignalNode.ts'
|
|
4
|
-
import { flushEffects } from './flushEffects.ts'
|
|
5
5
|
import { PATCH_BUS } from './PATCH_BUS.ts'
|
|
6
|
-
import { REACTIVE_CONTEXT } from './REACTIVE_CONTEXT.ts'
|
|
7
6
|
import { readNode } from './readNode.ts'
|
|
8
7
|
import { trigger } from './trigger.ts'
|
|
9
8
|
import type { Cell } from './types/Cell.ts'
|
|
@@ -157,8 +156,7 @@ export function createDoc(initial: unknown): Doc {
|
|
|
157
156
|
const nonShiftingAdd =
|
|
158
157
|
patch.op === 'add' &&
|
|
159
158
|
(!parentIsArray || leafKey === '-' || Number(leafKey) === arrayLength - 1)
|
|
160
|
-
|
|
161
|
-
try {
|
|
159
|
+
batch(() => {
|
|
162
160
|
if (segments.length === 0) {
|
|
163
161
|
wakeSubtree('', true, true)
|
|
164
162
|
} else if (!structural) {
|
|
@@ -178,15 +176,13 @@ export function createDoc(initial: unknown): Doc {
|
|
|
178
176
|
} else {
|
|
179
177
|
wakeSubtree(parentPath, true, true)
|
|
180
178
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
flushEffects()
|
|
179
|
+
/* Announce the change before effects flush, so a patch an effect emits in
|
|
180
|
+
reaction lands AFTER this one on the bus — the journal stays chronological.
|
|
181
|
+
Emitting inside the batch keeps it ahead of the depth-0 flush on batch exit. */
|
|
182
|
+
if (PATCH_BUS.active) {
|
|
183
|
+
PATCH_BUS.emit({ doc: self, patch, inverse: inverseOf(patch, before) })
|
|
184
|
+
}
|
|
185
|
+
})
|
|
190
186
|
}
|
|
191
187
|
|
|
192
188
|
/* The patch that undoes `patch`, from the pre-image `before` (a value the change
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RESUME } from './runtime/RESUME.ts'
|
|
2
|
+
import { seedStreamedResolution } from './seedStreamedResolution.ts'
|
|
3
|
+
import type { ResolvedFrame } from './types/ResolvedFrame.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
The single client intake seam for SSR warm-state seeding. Both warm-seed channels —
|
|
7
|
+
the cache-snapshot channel (a settled `cache()` value, keyed by cache key) and the
|
|
8
|
+
await-resume channel (an `await`-block resolved value, keyed by boundary id) — answer
|
|
9
|
+
the same "ship a server-settled value so hydration doesn't re-fetch" question, but land
|
|
10
|
+
in two distinct stores: the cache STORE (read by `cache()`) and the RESUME MANIFEST
|
|
11
|
+
(read by `awaitBlock` on adopt). This routes a discriminated `ResolvedFrame` to the
|
|
12
|
+
matching store so every consumer — startClient's boot drain, the live `__abideResolve`,
|
|
13
|
+
applyResolved's stream swap — registers through ONE call instead of poking each store
|
|
14
|
+
inline. The codecs stay split by source: the cache value is an HTTP body capped at plain
|
|
15
|
+
JSON (it must agree with the live `decodeResponse` read), the resume value is an in-process
|
|
16
|
+
graph carried as ref-json text and decoded lazily at the read site.
|
|
17
|
+
*/
|
|
18
|
+
// @documentation plumbing
|
|
19
|
+
export function seedResolved(frame: ResolvedFrame): void {
|
|
20
|
+
if (frame.kind === 'cache') {
|
|
21
|
+
seedStreamedResolution(frame.resolution)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
/* The resume value rides as raw ref-json text; store it unparsed so the inline
|
|
25
|
+
stream-swap script (vanilla, runs before the bundle's codec loads) can register
|
|
26
|
+
through this same seam. `awaitBlock` decodes it at the read. */
|
|
27
|
+
RESUME[frame.id] = frame.resume
|
|
28
|
+
}
|
|
@@ -11,7 +11,7 @@ import { probeNavigation } from './probeNavigation.ts'
|
|
|
11
11
|
import { router } from './router.ts'
|
|
12
12
|
import { clientPage } from './runtime/clientPage.ts'
|
|
13
13
|
import type { RouteLoader } from './runtime/types/RouteLoader.ts'
|
|
14
|
-
import {
|
|
14
|
+
import { seedResolved } from './seedResolved.ts'
|
|
15
15
|
|
|
16
16
|
/* The server's __SSR__ payload this entry consumes. */
|
|
17
17
|
type SsrPayload = { cache?: CacheSnapshotEntry[]; base?: string }
|
|
@@ -65,14 +65,16 @@ export function startClient(
|
|
|
65
65
|
const streamed =
|
|
66
66
|
(globalThis as { __abideResumeCache?: StreamedResolution[] }).__abideResumeCache ?? []
|
|
67
67
|
for (const resolution of [...(ssr.cache ?? []), ...streamed]) {
|
|
68
|
-
|
|
68
|
+
seedResolved({ kind: 'cache', resolution })
|
|
69
69
|
}
|
|
70
70
|
/* Keep the cache channel live past boot: replace the head's buffering collector with
|
|
71
71
|
the store-connected sink so a post-load resolution — streaming SPA navigation or a
|
|
72
72
|
socket-delivered SSR frame, both routed through applyResolved — seeds the store
|
|
73
|
-
directly instead of pushing to a buffer nothing drains again.
|
|
73
|
+
directly instead of pushing to a buffer nothing drains again. The inline doc-stream
|
|
74
|
+
script only ever hands this a cache `StreamedResolution`, so wrap it as a cache frame
|
|
75
|
+
through the one intake seam. */
|
|
74
76
|
;(globalThis as { __abideResolve?: (resolution: StreamedResolution) => void }).__abideResolve =
|
|
75
|
-
|
|
77
|
+
(resolution) => seedResolved({ kind: 'cache', resolution })
|
|
76
78
|
|
|
77
79
|
return router(target, routes, layoutRoutes, probeNavigation)
|
|
78
80
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StreamedResolution } from '../../shared/types/StreamedResolution.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A warm-state-seed frame for the single client intake (`seedResolved`), discriminated by
|
|
5
|
+
`kind`. Both kinds ship a server-settled value so hydration adopts without a re-fetch,
|
|
6
|
+
but route to distinct stores:
|
|
7
|
+
- `cache` — a `StreamedResolution` for the cache store, read by a warm `cache()` call.
|
|
8
|
+
- `resume` — an `await`-block boundary id plus its ref-json-encoded value STRING for the
|
|
9
|
+
RESUME manifest, decoded lazily by `awaitBlock` when it adopts the branch.
|
|
10
|
+
The payloads stay distinct (cache snapshot vs boundary-keyed value); the unified thing is
|
|
11
|
+
the intake seam, not the payload.
|
|
12
|
+
*/
|
|
13
|
+
export type ResolvedFrame =
|
|
14
|
+
| { kind: 'cache'; resolution: StreamedResolution }
|
|
15
|
+
| { kind: 'resume'; id: number; resume: string }
|