@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.
Files changed (38) hide show
  1. package/AGENTS.md +327 -326
  2. package/CHANGELOG.md +44 -0
  3. package/README.md +87 -78
  4. package/package.json +1 -1
  5. package/src/lib/ui/compile/SSR_ESCAPE.ts +3 -1
  6. package/src/lib/ui/compile/compileShadow.ts +5 -0
  7. package/src/lib/ui/compile/generateBuild.ts +11 -4
  8. package/src/lib/ui/compile/generateSSR.ts +17 -8
  9. package/src/lib/ui/compile/skeletonContext.ts +67 -79
  10. package/src/lib/ui/compile/templateAnchorAdapter.ts +61 -0
  11. package/src/lib/ui/compile/templateElementAdapter.ts +44 -0
  12. package/src/lib/ui/compile/walkAnchorOrder.ts +57 -0
  13. package/src/lib/ui/compile/walkElementOrder.ts +60 -0
  14. package/src/lib/ui/dom/appendSnippet.ts +9 -8
  15. package/src/lib/ui/dom/applyResolved.ts +3 -4
  16. package/src/lib/ui/dom/awaitBlock.ts +37 -31
  17. package/src/lib/ui/dom/buildDetachedRange.ts +30 -0
  18. package/src/lib/ui/dom/depthZeroNodes.ts +34 -0
  19. package/src/lib/ui/dom/domAnchorAdapter.ts +29 -0
  20. package/src/lib/ui/dom/domElementAdapter.ts +20 -0
  21. package/src/lib/ui/dom/each.ts +7 -11
  22. package/src/lib/ui/dom/eachAsync.ts +12 -17
  23. package/src/lib/ui/dom/isElement.ts +6 -0
  24. package/src/lib/ui/dom/markerDepthDelta.ts +19 -0
  25. package/src/lib/ui/dom/mountRange.ts +4 -3
  26. package/src/lib/ui/dom/mountSlot.ts +4 -3
  27. package/src/lib/ui/dom/on.ts +6 -1
  28. package/src/lib/ui/dom/replaceRange.ts +24 -0
  29. package/src/lib/ui/dom/skeleton.ts +35 -92
  30. package/src/lib/ui/dom/switchBlock.ts +13 -10
  31. package/src/lib/ui/dom/when.ts +13 -10
  32. package/src/lib/ui/runtime/RANGE_MARKER.ts +16 -0
  33. package/src/lib/ui/runtime/batch.ts +22 -0
  34. package/src/lib/ui/runtime/clientPage.ts +3 -8
  35. package/src/lib/ui/runtime/createDoc.ts +9 -13
  36. package/src/lib/ui/seedResolved.ts +28 -0
  37. package/src/lib/ui/startClient.ts +6 -4
  38. 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 { flushEffects } from './flushEffects.ts'
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
- REACTIVE_CONTEXT.batchDepth += 1
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
- } finally {
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
- REACTIVE_CONTEXT.batchDepth += 1
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
- } finally {
182
- REACTIVE_CONTEXT.batchDepth -= 1
183
- }
184
- /* Announce the change before flushing effects, so a patch an effect emits in
185
- reaction lands AFTER this one on the bus — the journal stays chronological. */
186
- if (PATCH_BUS.active) {
187
- PATCH_BUS.emit({ doc: self, patch, inverse: inverseOf(patch, before) })
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 { seedStreamedResolution } from './seedStreamedResolution.ts'
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
- seedStreamedResolution(resolution)
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
- seedStreamedResolution
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 }