@abide/abide 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/AGENTS.md +365 -320
  2. package/CHANGELOG.md +80 -0
  3. package/README.md +203 -168
  4. package/package.json +12 -8
  5. package/src/abideLsp.ts +11 -12
  6. package/src/abideResolverPlugin.ts +9 -4
  7. package/src/lib/bundle/disconnected.abide +3 -0
  8. package/src/lib/cli/printCommandHelp.ts +43 -0
  9. package/src/lib/cli/printSessionHelp.ts +2 -1
  10. package/src/lib/cli/{printHelp.ts → printTopLevelHelp.ts} +3 -40
  11. package/src/lib/cli/runCli.ts +2 -1
  12. package/src/lib/mcp/buildPrompts.ts +25 -0
  13. package/src/lib/mcp/dispatchMcpRequest.ts +8 -6
  14. package/src/lib/mcp/mcpResourceServerSlot.ts +5 -10
  15. package/src/lib/mcp/mcpSurface.ts +13 -252
  16. package/src/lib/mcp/mcpTools.ts +187 -0
  17. package/src/lib/mcp/renderPrompt.ts +25 -0
  18. package/src/lib/mcp/types/McpSurface.ts +15 -0
  19. package/src/lib/mcp/types/PromptDescriptor.ts +5 -0
  20. package/src/lib/mcp/types/PromptMessage.ts +2 -0
  21. package/src/lib/mcp/types/ToolDescriptor.ts +7 -0
  22. package/src/lib/mcp/types/ToolResult.ts +2 -0
  23. package/src/lib/server/agent.ts +1 -1
  24. package/src/lib/server/jsonl.ts +2 -2
  25. package/src/lib/server/rpc/defineRpc.ts +5 -0
  26. package/src/lib/server/runtime/containsTraversal.ts +21 -17
  27. package/src/lib/server/runtime/createAppAssetServer.ts +11 -16
  28. package/src/lib/server/runtime/createServer.ts +101 -71
  29. package/src/lib/server/runtime/createUiPageRenderer.ts +23 -4
  30. package/src/lib/server/runtime/devClientFingerprint.ts +3 -3
  31. package/src/lib/server/runtime/devHotModuleResponse.ts +2 -1
  32. package/src/lib/server/runtime/finalizeResponse.ts +32 -0
  33. package/src/lib/server/runtime/snapshotEntryFromCache.ts +8 -17
  34. package/src/lib/server/socket.ts +1 -1
  35. package/src/lib/server/sockets/createSocketDispatcher.ts +28 -5
  36. package/src/lib/server/sockets/defineSocket.ts +26 -7
  37. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +1 -1
  38. package/src/lib/server/sockets/types/SocketRoutes.ts +1 -1
  39. package/src/lib/server/sse.ts +2 -2
  40. package/src/lib/shared/DEFER.ts +8 -0
  41. package/src/lib/shared/activeCacheStore.ts +2 -2
  42. package/src/lib/shared/activePage.ts +2 -2
  43. package/src/lib/shared/attachRpcSelectorMethods.ts +42 -0
  44. package/src/lib/shared/attachSocketSelectorMethods.ts +34 -0
  45. package/src/lib/shared/basePath.ts +2 -2
  46. package/src/lib/shared/baseSlot.ts +6 -5
  47. package/src/lib/shared/bodyValueForKind.ts +1 -2
  48. package/src/lib/shared/buildSocketOverChannel.ts +40 -4
  49. package/src/lib/shared/cache.ts +477 -110
  50. package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
  51. package/src/lib/shared/cacheStoreSlot.ts +9 -5
  52. package/src/lib/{server/runtime → shared}/createReachable.ts +2 -2
  53. package/src/lib/shared/createRemoteFunction.ts +58 -9
  54. package/src/lib/shared/createResolverSlot.ts +24 -31
  55. package/src/lib/shared/createSocketSubRegistry.ts +2 -2
  56. package/src/lib/shared/decodeRefJson.ts +4 -2
  57. package/src/lib/shared/decodeResponse.ts +8 -6
  58. package/src/lib/shared/done.ts +14 -0
  59. package/src/lib/shared/encodeRefJson.ts +4 -2
  60. package/src/lib/shared/globalCacheStoreSlot.ts +9 -6
  61. package/src/lib/shared/hydratingSlot.ts +12 -0
  62. package/src/lib/shared/isLayoutFile.ts +12 -0
  63. package/src/lib/shared/keyForRemoteCall.ts +2 -1
  64. package/src/lib/shared/keyPrefixForRemote.ts +12 -0
  65. package/src/lib/shared/matchRoute.ts +175 -0
  66. package/src/lib/shared/normalizePathname.ts +11 -0
  67. package/src/lib/shared/pageSlot.ts +14 -5
  68. package/src/lib/shared/pageUrlForFile.ts +3 -3
  69. package/src/lib/shared/parseRouteSegments.ts +16 -7
  70. package/src/lib/shared/patch.ts +41 -0
  71. package/src/lib/shared/peek.ts +35 -0
  72. package/src/lib/shared/prepareRemoteExport.ts +40 -0
  73. package/src/lib/shared/prepareRpcModule.ts +98 -16
  74. package/src/lib/shared/prepareSocketModule.ts +11 -15
  75. package/src/lib/shared/queryStringFromArgs.ts +11 -0
  76. package/src/lib/shared/reachable.ts +102 -0
  77. package/src/lib/shared/refresh.ts +22 -0
  78. package/src/lib/shared/requestScopeSlot.ts +10 -7
  79. package/src/lib/shared/routeParamsShape.ts +9 -9
  80. package/src/lib/shared/rpcErrorRegistry.ts +69 -0
  81. package/src/lib/shared/selectorPrefix.ts +3 -10
  82. package/src/lib/shared/setOwnProperty.ts +19 -0
  83. package/src/lib/shared/snippet.ts +1 -1
  84. package/src/lib/shared/subscribableProbes.ts +111 -0
  85. package/src/lib/shared/tailProbeSlot.ts +8 -1
  86. package/src/lib/shared/types/CacheEntry.ts +9 -15
  87. package/src/lib/shared/types/CacheOptions.ts +8 -0
  88. package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
  89. package/src/lib/shared/types/RemoteCallable.ts +25 -7
  90. package/src/lib/shared/types/RemoteFunction.ts +48 -13
  91. package/src/lib/shared/types/ResolverSlot.ts +7 -4
  92. package/src/lib/shared/types/RpcError.ts +26 -0
  93. package/src/lib/shared/types/SmartReadOptions.ts +32 -0
  94. package/src/lib/shared/types/Socket.ts +54 -0
  95. package/src/lib/shared/types/SsrBootState.ts +27 -0
  96. package/src/lib/shared/types/SsrPayload.ts +21 -0
  97. package/src/lib/shared/types/Subscribable.ts +13 -13
  98. package/src/lib/shared/types/TailHooks.ts +2 -1
  99. package/src/lib/shared/url.ts +29 -14
  100. package/src/lib/shared/wakeHydrationPeeks.ts +20 -0
  101. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  102. package/src/lib/test/createScriptedSurface.ts +1 -1
  103. package/src/lib/test/createTestApp.ts +2 -2
  104. package/src/lib/test/createTestSocketChannel.ts +3 -3
  105. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +5 -6
  106. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +10 -7
  107. package/src/lib/ui/compile/abideUiPlugin.ts +2 -2
  108. package/src/lib/ui/compile/analyzeComponent.ts +28 -6
  109. package/src/lib/ui/compile/compileModule.ts +137 -11
  110. package/src/lib/ui/compile/compileShadow.ts +101 -84
  111. package/src/lib/ui/compile/desugarSignals.ts +120 -107
  112. package/src/lib/ui/compile/generateBuild.ts +48 -16
  113. package/src/lib/ui/compile/generateSSR.ts +117 -14
  114. package/src/lib/ui/compile/identifierReferencePattern.ts +13 -0
  115. package/src/lib/ui/compile/lowerContext.ts +10 -4
  116. package/src/lib/ui/compile/lowerScript.ts +72 -5
  117. package/src/lib/ui/compile/parseTemplate.ts +1 -14
  118. package/src/lib/ui/compile/prepareNestedScript.ts +31 -17
  119. package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
  120. package/src/lib/ui/compile/signalCallee.ts +24 -0
  121. package/src/lib/ui/compile/stripEffects.ts +14 -11
  122. package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
  123. package/src/lib/ui/compile/types/TemplateNode.ts +0 -5
  124. package/src/lib/ui/dom/appendStatic.ts +9 -1
  125. package/src/lib/ui/dom/appendText.ts +15 -3
  126. package/src/lib/ui/dom/assertClaimedText.ts +20 -0
  127. package/src/lib/ui/dom/awaitBlock.ts +19 -83
  128. package/src/lib/ui/dom/bindSelectValue.ts +48 -0
  129. package/src/lib/ui/dom/each.ts +12 -1
  130. package/src/lib/ui/dom/hydrate.ts +10 -0
  131. package/src/lib/ui/dom/mountChild.ts +2 -5
  132. package/src/lib/ui/dom/mountRange.ts +0 -75
  133. package/src/lib/ui/effect.ts +4 -3
  134. package/src/lib/ui/installHotBridge.ts +4 -2
  135. package/src/lib/ui/remoteProxy.ts +18 -2
  136. package/src/lib/ui/renderToStream.ts +9 -13
  137. package/src/lib/ui/resumeSeedScript.ts +2 -2
  138. package/src/lib/ui/router.ts +21 -2
  139. package/src/lib/ui/runtime/RESUME.ts +0 -6
  140. package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
  141. package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
  142. package/src/lib/ui/scope.ts +6 -3
  143. package/src/lib/ui/seedBootState.ts +53 -0
  144. package/src/lib/ui/socketChannel.ts +2 -2
  145. package/src/lib/ui/socketProxy.ts +9 -3
  146. package/src/lib/ui/startClient.ts +16 -30
  147. package/src/lib/ui/state.ts +44 -13
  148. package/src/lib/ui/sync.ts +11 -5
  149. package/src/lib/ui/tryEncodeResume.ts +2 -5
  150. package/src/lib/ui/types/Scope.ts +14 -10
  151. package/src/lib/ui/watch.ts +140 -0
  152. package/src/serverEntry.ts +6 -6
  153. package/template/CLAUDE.md +1 -1
  154. package/template/package.json +1 -1
  155. package/template/src/server/rpc/getHello.ts +3 -3
  156. package/template/src/ui/pages/page.abide +2 -2
  157. package/template/test/app.test.ts +1 -1
  158. package/src/lib/server/reachable.ts +0 -45
  159. package/src/lib/server/sockets/types/Socket.ts +0 -23
  160. package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
  161. package/src/lib/shared/DEFER_MIN_ARRAY_LENGTH.ts +0 -14
  162. package/src/lib/shared/baseResolver.ts +0 -10
  163. package/src/lib/shared/cacheKeyOf.ts +0 -7
  164. package/src/lib/shared/cacheKeyStore.ts +0 -8
  165. package/src/lib/shared/cacheStoreResolver.ts +0 -12
  166. package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
  167. package/src/lib/shared/pageResolver.ts +0 -16
  168. package/src/lib/shared/recordCacheKey.ts +0 -6
  169. package/src/lib/shared/requestScopeResolver.ts +0 -12
  170. package/src/lib/shared/setBaseResolver.ts +0 -4
  171. package/src/lib/shared/setCacheStoreResolver.ts +0 -4
  172. package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
  173. package/src/lib/shared/setPageResolver.ts +0 -4
  174. package/src/lib/shared/setRequestScopeResolver.ts +0 -4
  175. package/src/lib/shared/toBunRoutePattern.ts +0 -28
  176. package/src/lib/shared/types/TailOptions.ts +0 -10
  177. package/src/lib/ui/deferResume.ts +0 -36
  178. package/src/lib/ui/dom/firstElementBetween.ts +0 -14
  179. package/src/lib/ui/matchRoute.ts +0 -106
  180. package/src/lib/ui/runtime/scheduleWake.ts +0 -28
  181. package/src/lib/ui/runtime/whenIdle.ts +0 -21
  182. package/src/lib/ui/runtime/whenVisible.ts +0 -105
  183. package/src/lib/ui/tail.ts +0 -324
  184. /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
  185. /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 { DeferMarker, ResumeEntry } from './runtime/RESUME.ts'
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 | DeferMarker> => {
44
- const delta: Record<number, ResumeEntry | DeferMarker> = {}
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 | DeferMarker }
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. A cache-backed value defers via
93
- `deferResume` (a `{defer,key}` marker + lazy body seed) — the client adopts the streamed
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
- /* Keep the promise: cache() tagged it with its key, which `deferResume` reads to defer. */
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: deferResume(pending, value),
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 | DeferMarker, id: number): string | undefined {
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 { DeferMarker, ResumeEntry } from './runtime/RESUME.ts'
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 | DeferMarker>): string {
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),
@@ -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 matchRoute),
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 `requestScopeResolver`'s server-installed slot; the
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 { DeferMarker, ResumeEntry } from '../RESUME.ts'
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. A
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 | DeferMarker>
26
+ resume: Record<number, ResumeEntry>
28
27
  }
@@ -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. The returned value is passable: hand it to
9
- a child or a helper and it can read/extend/undo that scope (walk up via `.root()`).
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 reactive-state
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, `.publish` sending a server-validated `pub` frame —
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
- return buildSocketOverChannel<T>(name, getSocketChannel)
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 { healthSeedSlot } from '../shared/healthSeedSlot.ts'
3
- import { rpcTimeoutSlot } from '../shared/rpcTimeoutSlot.ts'
4
- import { setAppName } from '../shared/setAppName.ts'
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 { globalCacheStoreSlot } from '../shared/globalCacheStoreSlot.ts'
4
+ import { pageSlot } from '../shared/pageSlot.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
- setBaseResolver(() => ssr.base ?? '')
72
- /* Seed the per-page __SSR__ stamps into their shared slots before mount: the app
73
- name (default log channel), the health payload (so health()'s first probe is warm),
74
- and the env-configured RPC timeout (ABIDE_CLIENT_TIMEOUT, shipped per request).
75
- Without this the browser falls back to channel 'app', a cold first health probe,
76
- and unbounded RPC fetches. */
77
- setAppName(ssr.app)
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
- setPageResolver(() => clientPage.value)
67
+ pageSlot.resolver = () => clientPage.value
82
68
 
83
69
  const store = createCacheStore()
84
- setCacheStoreResolver(() => store)
70
+ cacheStoreSlot.resolver = () => store
85
71
  /* One tab store: cache(fn, { global: true }) shares it, so global is a no-op here. */
86
- setGlobalCacheStoreResolver(() => store)
72
+ globalCacheStoreSlot.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
@@ -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. The compiler enforces scope attachment (bare `state()` is a compile
11
- error use `scope().state()`), desugars plain `state(initial)` to a serializable `model`
12
- doc slot, and keeps `state(initial, transform)` as a `.value` cell; the runtime needs no magic.
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
- /* No-arg form for an undefined initial with a declared type: `state<Foo>()` is
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
@@ -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
- /* True only while applying a received patch, so its bus echo isn't re-sent. */
21
- let applying = false
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
- applying = true
30
+ applyingPatches.add(patch)
25
31
  try {
26
32
  doc.apply(patch)
27
33
  } finally {
28
- applying = false
34
+ applyingPatches.delete(patch)
29
35
  }
30
36
  })
31
37
 
32
38
  const unsubscribeBus = PATCH_BUS.subscribe((event) => {
33
- if (event.doc === doc && !applying) {
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 { DeferMarker, ResumeEntry } from './runtime/RESUME.ts'
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. Its data surface MIRRORS `Doc` (read/replace/add/
11
- remove/derive/apply/snapshot) so the compiler can target a scope as a
12
- component's data binding directly. It nests (`child`/`root`), passes values
13
- down the tree as context (`share`/`shared`), and carries the capability surface as
14
- methods so a scope is a passable value: `<Child parentScope={scope} />`.
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
- `undo`/`redo` act on a recorded scope. `id` is the scope's identity for the
19
- boundary-crossing capabilities — `persist()` defaults its key to it. `scope()` is the only user-facing public entry; `effect` is also exported
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