@abide/abide 0.45.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 (176) hide show
  1. package/AGENTS.md +365 -320
  2. package/CHANGELOG.md +96 -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 +7 -1
  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 +47 -11
  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/prepareNestedScript.ts +31 -17
  118. package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
  119. package/src/lib/ui/compile/signalCallee.ts +24 -0
  120. package/src/lib/ui/compile/stripEffects.ts +14 -11
  121. package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
  122. package/src/lib/ui/dom/appendStatic.ts +9 -1
  123. package/src/lib/ui/dom/appendText.ts +15 -3
  124. package/src/lib/ui/dom/assertClaimedText.ts +20 -0
  125. package/src/lib/ui/dom/awaitBlock.ts +17 -56
  126. package/src/lib/ui/dom/bindSelectValue.ts +48 -0
  127. package/src/lib/ui/dom/each.ts +12 -1
  128. package/src/lib/ui/dom/hydrate.ts +10 -0
  129. package/src/lib/ui/effect.ts +4 -3
  130. package/src/lib/ui/installHotBridge.ts +4 -2
  131. package/src/lib/ui/remoteProxy.ts +18 -2
  132. package/src/lib/ui/renderToStream.ts +9 -13
  133. package/src/lib/ui/resumeSeedScript.ts +2 -2
  134. package/src/lib/ui/router.ts +21 -2
  135. package/src/lib/ui/runtime/RESUME.ts +0 -6
  136. package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
  137. package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
  138. package/src/lib/ui/scope.ts +6 -3
  139. package/src/lib/ui/seedBootState.ts +53 -0
  140. package/src/lib/ui/socketChannel.ts +2 -2
  141. package/src/lib/ui/socketProxy.ts +9 -3
  142. package/src/lib/ui/startClient.ts +16 -30
  143. package/src/lib/ui/state.ts +44 -13
  144. package/src/lib/ui/sync.ts +11 -5
  145. package/src/lib/ui/tryEncodeResume.ts +2 -5
  146. package/src/lib/ui/types/Scope.ts +14 -10
  147. package/src/lib/ui/watch.ts +140 -0
  148. package/src/serverEntry.ts +6 -6
  149. package/template/CLAUDE.md +1 -1
  150. package/template/package.json +1 -1
  151. package/template/src/server/rpc/getHello.ts +3 -3
  152. package/template/src/ui/pages/page.abide +2 -2
  153. package/template/test/app.test.ts +1 -1
  154. package/src/lib/server/reachable.ts +0 -45
  155. package/src/lib/server/sockets/types/Socket.ts +0 -23
  156. package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
  157. package/src/lib/shared/baseResolver.ts +0 -10
  158. package/src/lib/shared/cacheKeyOf.ts +0 -7
  159. package/src/lib/shared/cacheKeyStore.ts +0 -8
  160. package/src/lib/shared/cacheStoreResolver.ts +0 -12
  161. package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
  162. package/src/lib/shared/pageResolver.ts +0 -16
  163. package/src/lib/shared/recordCacheKey.ts +0 -6
  164. package/src/lib/shared/requestScopeResolver.ts +0 -12
  165. package/src/lib/shared/setBaseResolver.ts +0 -4
  166. package/src/lib/shared/setCacheStoreResolver.ts +0 -4
  167. package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
  168. package/src/lib/shared/setPageResolver.ts +0 -4
  169. package/src/lib/shared/setRequestScopeResolver.ts +0 -4
  170. package/src/lib/shared/toBunRoutePattern.ts +0 -28
  171. package/src/lib/shared/types/TailOptions.ts +0 -10
  172. package/src/lib/ui/deferResume.ts +0 -29
  173. package/src/lib/ui/matchRoute.ts +0 -106
  174. package/src/lib/ui/tail.ts +0 -324
  175. /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
  176. /package/src/lib/{server/sockets → shared}/types/SocketServerFrame.ts +0 -0
@@ -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
@@ -0,0 +1,140 @@
1
+ import { cache } from '../shared/cache.ts'
2
+ import { REMOTE_FUNCTION } from '../shared/REMOTE_FUNCTION.ts'
3
+ import type { CacheOnContext } from '../shared/types/CacheOnContext.ts'
4
+ import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
5
+ import type { Subscribable } from '../shared/types/Subscribable.ts'
6
+ import { effect } from './effect.ts'
7
+ import { generationGuard } from './runtime/generationGuard.ts'
8
+ import type { EffectResult } from './runtime/types/EffectResult.ts'
9
+ import type { State } from './runtime/types/State.ts'
10
+
11
+ /*
12
+ The single reaction primitive: `watch(source, handler)` names its trigger and runs
13
+ `handler` with the source's new value whenever it changes. It unifies three
14
+ previously-separate things — the author `effect`, `socket.on`, and `cache.on` —
15
+ and it is also the compiler's binding primitive (emitted as `$$watch(thunk)` for
16
+ `{expr}` / `class:` / `bind:*`). Client-only: SSR-inert here and stripped by the
17
+ compiler. Returns a scope-tied disposer.
18
+
19
+ Instance sugar mirrors the global for the two subscribable/rpc sources (the other
20
+ sources have no instance home): `socket.watch(handler)` ≡ `watch(socket, handler)`, and
21
+ `getUser.watch(handler)` / `getUser.watch(args, handler)` ≡ `watch(getUser, …)`. The
22
+ method is client-attached (socketProxy / remoteProxy) so this ui primitive never rides
23
+ into a server bundle; server-side it is an inert no-op. Unlike bare `watch(…)` — which
24
+ the SSR back-end strips — a `.watch(…)` member call survives to the server and relies on
25
+ that inert stub.
26
+
27
+ Sources (discriminated at runtime, monomorphic per branch):
28
+ watch(thunk) // compiler binding form — auto-tracked, == effect(thunk)
29
+ watch(count, n => …) // a state cell → handler(newValue)
30
+ watch([a, b], vals => …) // multiple cells → fires on any change
31
+ watch(socket, frame => …) // a subscribable → handler per frame (cache.on loop)
32
+ watch(getUser, user => …) // an rpc → runs the smart read, handler(resolved value)
33
+ watch(getUser, args, user => …) // an rpc with args
34
+ */
35
+ // @documentation reactive-state
36
+ export function watch(thunk: () => EffectResult): () => void
37
+ export function watch<T>(source: State<T>, handler: (value: T) => void): () => void
38
+ export function watch(
39
+ sources: ReadonlyArray<State<unknown>>,
40
+ handler: (values: unknown[]) => void,
41
+ ): () => void
42
+ export function watch<Frame>(
43
+ source: Subscribable<Frame>,
44
+ handler: (frame: Frame, context: CacheOnContext) => void | Promise<void>,
45
+ ): () => void
46
+ export function watch<Args, Return>(
47
+ fn: RemoteFunction<Args, Return>,
48
+ handler: (value: Return) => void,
49
+ ): () => void
50
+ export function watch<Args, Return>(
51
+ fn: RemoteFunction<Args, Return>,
52
+ args: Args,
53
+ handler: (value: Return) => void,
54
+ ): () => void
55
+ export function watch(
56
+ source: unknown,
57
+ argsOrHandler?: unknown,
58
+ maybeHandler?: unknown,
59
+ ): () => void {
60
+ /* Client lifecycle, exactly like the effect it wraps: no runtime window guard (that would
61
+ also silence the compiler's generated bindings, which build on the client where the test
62
+ DOM sets no global `window`). SSR-safety comes from the compiler stripping author
63
+ `watch(...)` calls, and the subscribable branch's own server guard (cache.on). */
64
+ /* Compiler binding form watch(thunk): a bare auto-tracked effect (== today's $$effect). */
65
+ if (argsOrHandler === undefined) {
66
+ return effect(source as () => EffectResult)
67
+ }
68
+ /* watch(fn, args, handler): an rpc selector with explicit args. */
69
+ if (maybeHandler !== undefined) {
70
+ return reactToRpc(source, argsOrHandler, maybeHandler as (value: unknown) => void)
71
+ }
72
+ const handler = argsOrHandler as (value: unknown, context?: CacheOnContext) => void
73
+ /* A subscribable (socket / stream): per-frame delivery with reconnect-replay — the
74
+ existing cache.on loop is the single implementation this branch delegates to. */
75
+ if (isSubscribable(source)) {
76
+ return cache.on(
77
+ source as Subscribable<unknown>,
78
+ handler as (frame: unknown, context: CacheOnContext) => void | Promise<void>,
79
+ )
80
+ }
81
+ /* An rpc without args → run the smart read reactively, pipe its value to the handler. A
82
+ RemoteFunction is a callable, so accept `function` too — a state cell is a branded-free
83
+ object and never matches, and a streaming rpc was already caught by isSubscribable above. */
84
+ if (
85
+ source !== null &&
86
+ (typeof source === 'object' || typeof source === 'function') &&
87
+ REMOTE_FUNCTION in source
88
+ ) {
89
+ return reactToRpc(source, undefined, handler)
90
+ }
91
+ /* Multiple cells → fire on any change; hand the handler the current values. */
92
+ if (Array.isArray(source)) {
93
+ const cells = source as ReadonlyArray<State<unknown>>
94
+ return effect(() => {
95
+ const values = cells.map((cell) => cell.value)
96
+ handler(values)
97
+ })
98
+ }
99
+ /* A single state cell → handler(newValue) on change. */
100
+ const cell = source as State<unknown>
101
+ return effect(() => {
102
+ handler(cell.value)
103
+ })
104
+ }
105
+
106
+ /* True for a socket / rpc stream — anything async-iterable. */
107
+ function isSubscribable(source: unknown): boolean {
108
+ return (
109
+ source !== null &&
110
+ (typeof source === 'object' || typeof source === 'function') &&
111
+ typeof (source as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function'
112
+ )
113
+ }
114
+
115
+ /*
116
+ Reacts to an rpc's cached value: an effect that runs the smart read (subscribing
117
+ its key synchronously, so the effect re-runs when the value changes — a refresh,
118
+ a patch, an invalidate) and pipes the resolved value to the handler. Triggers the
119
+ read, unlike peek: `watch` observes a live query, so it keeps it flowing.
120
+ */
121
+ function reactToRpc(fn: unknown, args: unknown, handler: (value: unknown) => void): () => void {
122
+ const call = fn as (args: unknown) => Promise<unknown>
123
+ /* The bare call routes through cache.read (cache-managed flight, not scope-abortable), so
124
+ a slow flight can settle AFTER a faster re-run's flight OR after the owner tears down.
125
+ Guard the handler on the generation so only the current flight's value lands and a
126
+ post-teardown settle is dropped — a re-run renews, teardown bumps (both via the shared
127
+ generationGuard). */
128
+ const guard = generationGuard()
129
+ return effect(() => {
130
+ const generation = guard.renew()
131
+ void Promise.resolve(call(args)).then(
132
+ (value) => {
133
+ if (guard.live(generation)) {
134
+ handler(value)
135
+ }
136
+ },
137
+ () => undefined,
138
+ )
139
+ })
140
+ }
@@ -29,12 +29,12 @@ import { loadEnvFromBinaryDir } from './lib/cli/loadEnvFromBinaryDir.ts'
29
29
  import { createServer } from './lib/server/runtime/createServer.ts'
30
30
  import { requestContext } from './lib/server/runtime/requestContext.ts'
31
31
  import { resolvePageSnapshot } from './lib/server/runtime/resolvePageSnapshot.ts'
32
+ import { cacheStoreSlot } from './lib/shared/cacheStoreSlot.ts'
32
33
  import { createCacheStore } from './lib/shared/createCacheStore.ts'
34
+ import { globalCacheStoreSlot } from './lib/shared/globalCacheStoreSlot.ts'
33
35
  import { loadEnvFromDataDir } from './lib/shared/loadEnvFromDataDir.ts'
36
+ import { pageSlot } from './lib/shared/pageSlot.ts'
34
37
  import { runningAsStandaloneBinary } from './lib/shared/runningAsStandaloneBinary.ts'
35
- import { setCacheStoreResolver } from './lib/shared/setCacheStoreResolver.ts'
36
- import { setGlobalCacheStoreResolver } from './lib/shared/setGlobalCacheStoreResolver.ts'
37
- import { setPageResolver } from './lib/shared/setPageResolver.ts'
38
38
 
39
39
  /*
40
40
  Resolve config into process.env before anything reads it (createServer reads
@@ -65,16 +65,16 @@ await import('./_virtual/config.ts')
65
65
  // In a bundle, tie this server's life to the launcher's (no-op standalone).
66
66
  exitWithParent()
67
67
 
68
- setCacheStoreResolver(() => requestContext.getStore()?.cache)
68
+ cacheStoreSlot.resolver = () => requestContext.getStore()?.cache
69
69
 
70
- setPageResolver(resolvePageSnapshot)
70
+ pageSlot.resolver = resolvePageSnapshot
71
71
 
72
72
  /*
73
73
  Process-level store for cache(fn, { global: true }) — one per server process,
74
74
  outlives every request so memoised external calls are shared across them.
75
75
  */
76
76
  const globalCacheStore = createCacheStore()
77
- setGlobalCacheStoreResolver(() => globalCacheStore)
77
+ globalCacheStoreSlot.resolver = () => globalCacheStore
78
78
 
79
79
  await createServer({
80
80
  pages,
@@ -14,7 +14,7 @@ at `node_modules/@abide/abide/README.md`.
14
14
  ## Conventions (see AGENTS.md for the full list)
15
15
 
16
16
  - One export per file, named after the file. No barrels — import each name by its
17
- own path (`@abide/abide/server/GET`, `@abide/abide/shared/cache`, …).
17
+ own path (`@abide/abide/server/GET`, `@abide/abide/shared/refresh`, …).
18
18
  - RPCs live in `src/server/rpc/<name>.ts`; sockets in `src/server/sockets/`;
19
19
  pages are `**/page.abide`, layouts `**/layout.abide`.
20
20
  - Generated types land in `src/.abide/` — do not hand-edit them; run `abide dev`/
@@ -13,7 +13,7 @@
13
13
  "test": "bun test"
14
14
  },
15
15
  "dependencies": {
16
- "@abide/abide": "^0.27.0"
16
+ "@abide/abide": "^0.46.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/bun": "^1.3.14"
@@ -25,9 +25,9 @@ dedupe). Other helpers are siblings, one per file: `abide/server/error`,
25
25
  `abide/server/redirect`, `abide/server/sse`, `abide/server/jsonl`.
26
26
 
27
27
  Every rpc value also exposes `.raw(args?)` (returns the underlying
28
- `Response`) and `.stream(args?)` (returns a `Subscribable` over the frames
29
- feed it to `tail()`) for callers that need headers/status or want to iterate
30
- jsonl/sse frames.
28
+ `Response`) for callers that need headers or status. A streaming rpc a
29
+ jsonl/sse handler returns a `Subscribable` over the frames from its bare
30
+ call (`fn(args)`); iterate it with `for await` or react to it with `watch(fn(args), frame => …)`.
31
31
  */
32
32
 
33
33
  import { GET } from '@abide/abide/server/GET'
@@ -5,15 +5,15 @@ page.abide mounts at that folder's URL; the root layout.abide wraps it.
5
5
 
6
6
  The blocking await-block below (the `then` clause sits in the `{#await}` head)
7
7
  resolves on the server during SSR and renders inline — no pending placeholder. The
8
+ bare rpc call IS the smart read: cached, coalesced, and reactive by default. The
8
9
  decoded body is captured into the per-request cache, serialized into the HTML, and
9
10
  replayed on the client during hydration with no second fetch. A `{:then}` *branch*
10
11
  instead would stream the resolution in out of order.
11
12
  */
12
- import { cache } from '@abide/abide/shared/cache'
13
13
  import { getHello } from '$server/rpc/getHello.ts'
14
14
  </script>
15
15
 
16
- {#await cache(getHello)() then hello}
16
+ {#await getHello() then hello}
17
17
  <h1>{hello.message}</h1>
18
18
  {/await}
19
19
  <p>Edit <code>src/ui/pages/page.abide</code> and the page hot-reloads.</p>
@@ -5,7 +5,7 @@ same wiring as `abide start`, no fixtures) and hands back the whole surface:
5
5
  app.fetch(path) — pages and raw HTTP, origin + mount base resolved
6
6
  app.rpc.<name>(args) — RPCs over the real pipeline (CSRF, cookies, base),
7
7
  typed and decoded; .raw(args) for the Response
8
- app.sockets.<name> — a live Socket: iterate it, .tail(n), .publish(m)
8
+ app.sockets.<name> — a live Socket: iterate it (for await), .peek(), .broadcast(m)
9
9
  app.health() — the /__abide/health payload
10
10
 
11
11
  `app.rpc` / `app.sockets` are typed from your own RPCs and sockets — generated
@@ -1,45 +0,0 @@
1
- import { parseBoundedEnvInt } from '../shared/parseBoundedEnvInt.ts'
2
- import { createReachable } from './runtime/createReachable.ts'
3
-
4
- /*
5
- Server-only outbound reachability for an external host. `await reachable(host)`
6
- HEADs the host's origin: the first call awaits a real probe (faithful — a down
7
- host costs the full timeout, an up host one handshake) and starts a background
8
- poll that re-probes every TTL, so every later call resolves instantly off the
9
- warm value, fresh within one TTL. A down host going down is caught within
10
- ~failureLimit polls; recovery flips it back automatically.
11
-
12
- if (!(await reachable('api.example.com'))) return error(503)
13
-
14
- A bare host defaults to https; pass an explicit http://… for a non-TLS host.
15
- Answers "can I connect to this host," NOT "is my endpoint healthy": any
16
- completed HTTP response (even 4xx/5xx, even a 405 to HEAD) counts as reachable;
17
- only a connection failure or timeout reads as unreachable. There is no ambient
18
- server-side connectivity signal, so this is the honest way to fail a doomed
19
- outbound call fast — see online() for the inbound/client-reported counterpart.
20
-
21
- ABIDE_REACHABLE_TTL (poll cadence / freshness, ms) and ABIDE_REACHABLE_TIMEOUT
22
- (per-HEAD bound, ms) tune the defaults; the timeout is deliberately generous so
23
- a healthy-but-distant host over a slow link is not mis-read as down.
24
- */
25
- const TTL_MS = parseBoundedEnvInt(process.env.ABIDE_REACHABLE_TTL, 1_000, 600_000) ?? 30_000
26
- const TIMEOUT_MS = parseBoundedEnvInt(process.env.ABIDE_REACHABLE_TIMEOUT, 100, 60_000) ?? 3_000
27
- /* Stop polling a host nobody has read in a few TTLs; the next read restarts it cold. */
28
- const IDLE_MS = TTL_MS * 3
29
-
30
- /* Status-agnostic HEAD: a completed response proves connectivity; reject/timeout does not. */
31
- async function probeOrigin(origin: string): Promise<boolean> {
32
- try {
33
- await fetch(origin, { method: 'HEAD', signal: AbortSignal.timeout(TIMEOUT_MS) })
34
- return true
35
- } catch {
36
- return false
37
- }
38
- }
39
-
40
- // @documentation observability
41
- export const reachable = createReachable({
42
- probe: probeOrigin,
43
- intervalMs: TTL_MS,
44
- idleMs: IDLE_MS,
45
- }).reachable
@@ -1,23 +0,0 @@
1
- import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
2
- import type { TailHooks } from '../../../shared/types/TailHooks.ts'
3
-
4
- /*
5
- Bidirectional named broadcast primitive. Declared once with `socket<T>()`
6
- inside a file under `src/server/sockets/`; the same import resolves to a server-side
7
- fan-out and a client-side ws proxy by build target. Iterating the socket
8
- is the live stream — no replay. `.tail(count)` opens a subscription seeded
9
- with the last `count` frames of the retained tail (declared via
10
- `{ tail: n }`; no-arg = the whole retained tail) before going live — the
11
- optional Subscribable retention capability the reactive `tail()` consumer
12
- seeds from. `publish` is isomorphic: server code publishes in-process
13
- and fans out to remote subscribers; client code sends a `pub` frame the
14
- dispatcher validates against the topic's `clientPublish` flag. `clients`
15
- exposes which adapter surfaces (browser / mcp / cli) advertise this
16
- socket.
17
- */
18
- export interface Socket<T> extends AsyncIterable<T> {
19
- readonly name: string
20
- readonly clients: ClientFlags
21
- publish(message: T): void
22
- tail(count?: number, hooks?: TailHooks): AsyncIterable<T>
23
- }
@@ -1,9 +0,0 @@
1
- /*
2
- Brand on the invoker cache() returns; its property value is the wrapped fn,
3
- so inspection shows what a wrapper wraps. Detection is certain (set by us,
4
- never heuristic), which is why misuse throws instead of warning: a wrapper
5
- used as a selector matches nothing (no url/method, no producer id), and
6
- without detection a re-wrapped wrapper would silently downgrade a remote to
7
- an anonymous producer (no url/method, no shared key, no SSR snapshot).
8
- */
9
- export const CACHE_WRAPPED: unique symbol = Symbol('abide.cacheWrapped')
@@ -1,10 +0,0 @@
1
- import { createResolverSlot } from './createResolverSlot.ts'
2
-
3
- /*
4
- The mount-base slot/resolver/reader bundle. The server entry installs an
5
- APP_URL-derived resolver at boot; the client entry one reading
6
- window.__SSR__.base. No lazy fallback creator — `fallback` is a plain string set
7
- directly by isolated tests, and basePath() supplies the '' default. baseSlot /
8
- basePath re-export the slot and reader; setBaseResolver the setter.
9
- */
10
- export const baseResolver = createResolverSlot<string>()
@@ -1,7 +0,0 @@
1
- import { cacheKeyStore } from './cacheKeyStore.ts'
2
-
3
- /* The store key behind a cache() read's promise, or undefined if it carries none
4
- (a producer read, a raw Response read, or a non-cache promise). */
5
- export function cacheKeyOf(promise: Promise<unknown>): string | undefined {
6
- return cacheKeyStore.get(promise)
7
- }
@@ -1,8 +0,0 @@
1
- /*
2
- WeakMap recording the cache key behind a `cache()` read's returned promise, so a
3
- consumer holding only the promise can recover its key. Mirrors remoteMetaStore (which
4
- records the synthesized Request the same way). The SSR resume path uses it to decide
5
- whether a `{#await cache()}` value is a large cache-backed read it can defer — shipping a
6
- `{ defer, key }` marker instead of the value. Collected with the promise.
7
- */
8
- export const cacheKeyStore = new WeakMap<Promise<unknown>, string>()