@abide/abide 0.46.0 → 0.48.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 (194) hide show
  1. package/AGENTS.md +370 -320
  2. package/CHANGELOG.md +94 -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/buildCacheSnapshot.ts +6 -6
  27. package/src/lib/server/runtime/containsTraversal.ts +21 -17
  28. package/src/lib/server/runtime/createAppAssetServer.ts +11 -16
  29. package/src/lib/server/runtime/createServer.ts +101 -71
  30. package/src/lib/server/runtime/createUiPageRenderer.ts +23 -4
  31. package/src/lib/server/runtime/devClientFingerprint.ts +3 -3
  32. package/src/lib/server/runtime/devHotModuleResponse.ts +2 -1
  33. package/src/lib/server/runtime/finalizeResponse.ts +32 -0
  34. package/src/lib/server/runtime/snapshotEntryFromCache.ts +8 -17
  35. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  36. package/src/lib/server/runtime/types/InspectorCacheSnapshot.ts +4 -4
  37. package/src/lib/server/runtime/types/InspectorContext.ts +1 -1
  38. package/src/lib/server/socket.ts +1 -1
  39. package/src/lib/server/sockets/createSocketDispatcher.ts +28 -5
  40. package/src/lib/server/sockets/defineSocket.ts +26 -7
  41. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +1 -1
  42. package/src/lib/server/sockets/types/SocketRoutes.ts +1 -1
  43. package/src/lib/server/sse.ts +2 -2
  44. package/src/lib/shared/DEFER.ts +8 -0
  45. package/src/lib/shared/activeCacheStore.ts +2 -2
  46. package/src/lib/shared/activePage.ts +2 -2
  47. package/src/lib/shared/attachRpcSelectorMethods.ts +42 -0
  48. package/src/lib/shared/attachSocketSelectorMethods.ts +34 -0
  49. package/src/lib/shared/basePath.ts +2 -2
  50. package/src/lib/shared/baseSlot.ts +6 -5
  51. package/src/lib/shared/bodyValueForKind.ts +1 -2
  52. package/src/lib/shared/buildSocketOverChannel.ts +40 -4
  53. package/src/lib/shared/cache.ts +522 -117
  54. package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
  55. package/src/lib/shared/cacheStoreSlot.ts +9 -5
  56. package/src/lib/shared/cacheStores.ts +3 -3
  57. package/src/lib/{server/runtime → shared}/createReachable.ts +2 -2
  58. package/src/lib/shared/createRemoteFunction.ts +58 -9
  59. package/src/lib/shared/createResolverSlot.ts +24 -31
  60. package/src/lib/shared/createSocketSubRegistry.ts +2 -2
  61. package/src/lib/shared/decodeRefJson.ts +4 -2
  62. package/src/lib/shared/decodeResponse.ts +8 -6
  63. package/src/lib/shared/done.ts +14 -0
  64. package/src/lib/shared/encodeRefJson.ts +4 -2
  65. package/src/lib/shared/hydratingSlot.ts +12 -0
  66. package/src/lib/shared/isLayoutFile.ts +12 -0
  67. package/src/lib/shared/keyForRemoteCall.ts +2 -1
  68. package/src/lib/shared/keyPrefixForRemote.ts +12 -0
  69. package/src/lib/shared/matchRoute.ts +175 -0
  70. package/src/lib/shared/normalizePathname.ts +11 -0
  71. package/src/lib/shared/pageSlot.ts +14 -5
  72. package/src/lib/shared/pageUrlForFile.ts +3 -3
  73. package/src/lib/shared/parseRouteSegments.ts +16 -7
  74. package/src/lib/shared/patch.ts +41 -0
  75. package/src/lib/shared/peek.ts +35 -0
  76. package/src/lib/shared/prepareRemoteExport.ts +40 -0
  77. package/src/lib/shared/prepareRpcModule.ts +98 -16
  78. package/src/lib/shared/prepareSocketModule.ts +11 -15
  79. package/src/lib/shared/queryStringFromArgs.ts +11 -0
  80. package/src/lib/shared/reachable.ts +102 -0
  81. package/src/lib/shared/refresh.ts +22 -0
  82. package/src/lib/shared/requestScopeSlot.ts +10 -7
  83. package/src/lib/shared/routeParamsShape.ts +9 -9
  84. package/src/lib/shared/rpcErrorRegistry.ts +69 -0
  85. package/src/lib/shared/selectorPrefix.ts +3 -10
  86. package/src/lib/shared/setOwnProperty.ts +19 -0
  87. package/src/lib/shared/sharedCacheStore.ts +14 -0
  88. package/src/lib/shared/sharedCacheStoreSlot.ts +10 -0
  89. package/src/lib/shared/snippet.ts +1 -1
  90. package/src/lib/shared/subscribableProbes.ts +111 -0
  91. package/src/lib/shared/tailProbeSlot.ts +8 -1
  92. package/src/lib/shared/types/CacheEntry.ts +9 -15
  93. package/src/lib/shared/types/CacheOptions.ts +23 -11
  94. package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
  95. package/src/lib/shared/types/CacheStats.ts +1 -1
  96. package/src/lib/shared/types/RemoteCallable.ts +25 -7
  97. package/src/lib/shared/types/RemoteFunction.ts +48 -13
  98. package/src/lib/shared/types/ResolverSlot.ts +7 -4
  99. package/src/lib/shared/types/RpcError.ts +26 -0
  100. package/src/lib/shared/types/SmartReadOptions.ts +36 -0
  101. package/src/lib/shared/types/Socket.ts +54 -0
  102. package/src/lib/shared/types/SsrBootState.ts +27 -0
  103. package/src/lib/shared/types/SsrPayload.ts +21 -0
  104. package/src/lib/shared/types/Subscribable.ts +13 -13
  105. package/src/lib/shared/types/TailHooks.ts +2 -1
  106. package/src/lib/shared/url.ts +29 -14
  107. package/src/lib/shared/wakeHydrationPeeks.ts +20 -0
  108. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  109. package/src/lib/test/createScriptedSurface.ts +1 -1
  110. package/src/lib/test/createTestApp.ts +8 -8
  111. package/src/lib/test/createTestSocketChannel.ts +3 -3
  112. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +5 -6
  113. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +10 -7
  114. package/src/lib/ui/compile/abideUiPlugin.ts +2 -2
  115. package/src/lib/ui/compile/analyzeComponent.ts +28 -6
  116. package/src/lib/ui/compile/compileModule.ts +137 -11
  117. package/src/lib/ui/compile/compileShadow.ts +101 -84
  118. package/src/lib/ui/compile/desugarSignals.ts +120 -107
  119. package/src/lib/ui/compile/generateBuild.ts +48 -16
  120. package/src/lib/ui/compile/generateSSR.ts +117 -14
  121. package/src/lib/ui/compile/identifierReferencePattern.ts +13 -0
  122. package/src/lib/ui/compile/lowerContext.ts +10 -4
  123. package/src/lib/ui/compile/lowerScript.ts +72 -5
  124. package/src/lib/ui/compile/parseTemplate.ts +1 -14
  125. package/src/lib/ui/compile/prepareNestedScript.ts +31 -17
  126. package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
  127. package/src/lib/ui/compile/signalCallee.ts +24 -0
  128. package/src/lib/ui/compile/stripEffects.ts +14 -11
  129. package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
  130. package/src/lib/ui/compile/types/TemplateNode.ts +0 -5
  131. package/src/lib/ui/dom/appendStatic.ts +9 -1
  132. package/src/lib/ui/dom/appendText.ts +15 -3
  133. package/src/lib/ui/dom/assertClaimedText.ts +20 -0
  134. package/src/lib/ui/dom/awaitBlock.ts +19 -83
  135. package/src/lib/ui/dom/bindSelectValue.ts +48 -0
  136. package/src/lib/ui/dom/each.ts +12 -1
  137. package/src/lib/ui/dom/hydrate.ts +10 -0
  138. package/src/lib/ui/dom/mountChild.ts +2 -5
  139. package/src/lib/ui/dom/mountRange.ts +0 -75
  140. package/src/lib/ui/effect.ts +4 -3
  141. package/src/lib/ui/installHotBridge.ts +4 -2
  142. package/src/lib/ui/remoteProxy.ts +18 -2
  143. package/src/lib/ui/renderToStream.ts +9 -13
  144. package/src/lib/ui/resumeSeedScript.ts +2 -2
  145. package/src/lib/ui/router.ts +21 -2
  146. package/src/lib/ui/runtime/RESUME.ts +0 -6
  147. package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
  148. package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
  149. package/src/lib/ui/scope.ts +6 -3
  150. package/src/lib/ui/seedBootState.ts +53 -0
  151. package/src/lib/ui/socketChannel.ts +2 -2
  152. package/src/lib/ui/socketProxy.ts +9 -3
  153. package/src/lib/ui/startClient.ts +17 -31
  154. package/src/lib/ui/state.ts +44 -13
  155. package/src/lib/ui/sync.ts +11 -5
  156. package/src/lib/ui/tryEncodeResume.ts +2 -5
  157. package/src/lib/ui/types/Scope.ts +14 -10
  158. package/src/lib/ui/watch.ts +140 -0
  159. package/src/serverEntry.ts +13 -11
  160. package/template/CLAUDE.md +1 -1
  161. package/template/package.json +1 -1
  162. package/template/src/server/rpc/getHello.ts +3 -3
  163. package/template/src/ui/pages/page.abide +2 -2
  164. package/template/test/app.test.ts +1 -1
  165. package/src/lib/server/reachable.ts +0 -45
  166. package/src/lib/server/sockets/types/Socket.ts +0 -23
  167. package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
  168. package/src/lib/shared/DEFER_MIN_ARRAY_LENGTH.ts +0 -14
  169. package/src/lib/shared/baseResolver.ts +0 -10
  170. package/src/lib/shared/cacheKeyOf.ts +0 -7
  171. package/src/lib/shared/cacheKeyStore.ts +0 -8
  172. package/src/lib/shared/cacheStoreResolver.ts +0 -12
  173. package/src/lib/shared/globalCacheStore.ts +0 -15
  174. package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
  175. package/src/lib/shared/globalCacheStoreSlot.ts +0 -9
  176. package/src/lib/shared/pageResolver.ts +0 -16
  177. package/src/lib/shared/recordCacheKey.ts +0 -6
  178. package/src/lib/shared/requestScopeResolver.ts +0 -12
  179. package/src/lib/shared/setBaseResolver.ts +0 -4
  180. package/src/lib/shared/setCacheStoreResolver.ts +0 -4
  181. package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
  182. package/src/lib/shared/setPageResolver.ts +0 -4
  183. package/src/lib/shared/setRequestScopeResolver.ts +0 -4
  184. package/src/lib/shared/toBunRoutePattern.ts +0 -28
  185. package/src/lib/shared/types/TailOptions.ts +0 -10
  186. package/src/lib/ui/deferResume.ts +0 -36
  187. package/src/lib/ui/dom/firstElementBetween.ts +0 -14
  188. package/src/lib/ui/matchRoute.ts +0 -106
  189. package/src/lib/ui/runtime/scheduleWake.ts +0 -28
  190. package/src/lib/ui/runtime/whenIdle.ts +0 -21
  191. package/src/lib/ui/runtime/whenVisible.ts +0 -105
  192. package/src/lib/ui/tail.ts +0 -324
  193. /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
  194. /package/src/lib/{server/sockets → shared}/types/SocketServerFrame.ts +0 -0
@@ -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'
33
34
  import { loadEnvFromDataDir } from './lib/shared/loadEnvFromDataDir.ts'
35
+ import { pageSlot } from './lib/shared/pageSlot.ts'
34
36
  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'
37
+ import { sharedCacheStoreSlot } from './lib/shared/sharedCacheStoreSlot.ts'
38
38
 
39
39
  /*
40
40
  Resolve config into process.env before anything reads it (createServer reads
@@ -65,16 +65,18 @@ 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)
69
-
70
- setPageResolver(resolvePageSnapshot)
71
-
72
68
  /*
73
- Process-level store for cache(fn, { global: true }) — one per server process,
74
- outlives every request so memoised external calls are shared across them.
69
+ Process-level ("shared") store for cache(fn, { shared: true }) — one per server
70
+ process, outlives every request so memoised external calls are shared across them.
71
+ It is also the store a read with no request in flight resolves to, so boot/cron/
72
+ socket-handler reads coalesce into a real store instead of an orphan fallback.
75
73
  */
76
- const globalCacheStore = createCacheStore()
77
- setGlobalCacheStoreResolver(() => globalCacheStore)
74
+ const sharedCacheStore = createCacheStore()
75
+ sharedCacheStoreSlot.resolver = () => sharedCacheStore
76
+
77
+ cacheStoreSlot.resolver = () => requestContext.getStore()?.cache ?? sharedCacheStore
78
+
79
+ pageSlot.resolver = resolvePageSnapshot
78
80
 
79
81
  await createServer({
80
82
  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,14 +0,0 @@
1
- /*
2
- The array-length above which a blocking `{#await cache()}` value is worth deferring (shipped
3
- inert, materialized on idle) rather than inlined and hydrated eagerly at boot. Deferral trades
4
- a live boot for a cheaper one; below this many rows the boot decode is cheap enough that the
5
- trade doesn't pay, so the block stays eager — fully interactive from the first frame, no wake
6
- needed. Only genuinely large grids cross it and take the inert-then-idle-wake path.
7
-
8
- An O(1) `.length` heuristic (measured on the resolved value, before serialization): array-shaped
9
- cache reads — lists, grids, tables — are the payloads big enough to matter, and the ones the
10
- inert path was built for. Non-array values never defer. A deliberately generous default: with
11
- idle-wake, a deferred block is interactive anyway (live before a human acts), so this is a
12
- boot-performance knob, not a correctness one — set high so eager-and-simple is the common case.
13
- */
14
- export const DEFER_MIN_ARRAY_LENGTH = 500
@@ -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>()
@@ -1,12 +0,0 @@
1
- import { createCacheStore } from './createCacheStore.ts'
2
- import { createResolverSlot } from './createResolverSlot.ts'
3
- import type { CacheStore } from './types/CacheStore.ts'
4
-
5
- /*
6
- The active-CacheStore slot/resolver/reader bundle. The server entry installs an
7
- ALS-backed resolver (request-scoped); the client entry a module-singleton one.
8
- With no resolver registered, a single fallback store is created lazily so
9
- isolated tests work without booting the runtime. cacheStoreSlot / activeCacheStore
10
- re-export the slot and reader; setCacheStoreResolver the setter.
11
- */
12
- export const cacheStoreResolver = createResolverSlot<CacheStore>(createCacheStore)
@@ -1,15 +0,0 @@
1
- import { activeCacheStore } from './activeCacheStore.ts'
2
- import { globalCacheStoreSlot } from './globalCacheStoreSlot.ts'
3
- import type { CacheStore } from './types/CacheStore.ts'
4
-
5
- /*
6
- Resolves the process-level CacheStore that `cache(fn, { global: true })` entries
7
- live in. The server entry registers a module-singleton resolver so the store
8
- survives across requests; the client points it at the active tab store. When no
9
- resolver is registered (isolated tests, or a client that never set one) it falls
10
- back to the active store, so `global` degrades to request/tab-scoped rather than
11
- throwing.
12
- */
13
- export function globalCacheStore(): CacheStore {
14
- return globalCacheStoreSlot.resolver?.() ?? activeCacheStore()
15
- }
@@ -1,12 +0,0 @@
1
- import { createResolverSlot } from './createResolverSlot.ts'
2
- import type { CacheStore } from './types/CacheStore.ts'
3
-
4
- /*
5
- Slot + setter for the process-level cache store resolver used by cache() entries
6
- opting into `global: true`. The server entry registers a module-singleton store
7
- outliving any one request; the client entry points it at its single tab store so
8
- `global` is a no-op there. No fallback creator — when unset, globalCacheStore()
9
- falls through to the active store rather than minting an isolated one.
10
- globalCacheStoreSlot / setGlobalCacheStoreResolver re-export the slot and setter.
11
- */
12
- export const globalCacheStoreResolver = createResolverSlot<CacheStore>()
@@ -1,9 +0,0 @@
1
- import { globalCacheStoreResolver } from './globalCacheStoreResolver.ts'
2
-
3
- /*
4
- Slot for the process-level cache store resolver (see globalCacheStoreResolver).
5
- Exposed so test helpers snapshot/poke `.resolver` directly. Unset means no global
6
- store is registered, in which case globalCacheStore() falls back to the active
7
- (request/tab) store.
8
- */
9
- export const globalCacheStoreSlot = globalCacheStoreResolver.slot
@@ -1,16 +0,0 @@
1
- import { createResolverSlot } from './createResolverSlot.ts'
2
- import type { PageSnapshot } from './types/PageSnapshot.ts'
3
-
4
- /*
5
- The active-page slot/resolver/reader bundle. The server entry installs an
6
- ALS-backed resolver (request-scoped, so concurrent and streaming renders never
7
- share state); the client entry a module-singleton one. With no resolver
8
- registered, a single empty snapshot is created lazily so isolated tests work.
9
- pageSlot / activePage re-export the slot and reader; setPageResolver the setter.
10
- */
11
- export const pageResolver = createResolverSlot<PageSnapshot>(() => ({
12
- route: '',
13
- params: {},
14
- url: new URL('http://localhost/'),
15
- navigating: false,
16
- }))
@@ -1,6 +0,0 @@
1
- import { cacheKeyStore } from './cacheKeyStore.ts'
2
-
3
- /* Tags a cache() read's returned promise with its store key (see cacheKeyStore). */
4
- export function recordCacheKey(promise: Promise<unknown>, key: string): void {
5
- cacheKeyStore.set(promise, key)
6
- }
@@ -1,12 +0,0 @@
1
- import { createResolverSlot } from './createResolverSlot.ts'
2
- import type { RequestScopeInfo } from './types/RequestScopeInfo.ts'
3
-
4
- /*
5
- Slot + setter for the request-scope resolver. The server installs an ALS-backed
6
- resolver (createServer, reading the RequestStore); the client a module-singleton
7
- seeded from __SSR__ (startClient). No fallback creator — an unset resolver (or one
8
- returning undefined outside any request) means "no scope": callers read
9
- `.resolver?.()` and treat undefined as absent. requestScopeSlot /
10
- setRequestScopeResolver re-export the slot and setter.
11
- */
12
- export const requestScopeResolver = createResolverSlot<RequestScopeInfo>()
@@ -1,4 +0,0 @@
1
- import { baseResolver } from './baseResolver.ts'
2
-
3
- // Registers the runtime's mount-base resolver. Called once per side at boot.
4
- export const setBaseResolver = baseResolver.set
@@ -1,4 +0,0 @@
1
- import { cacheStoreResolver } from './cacheStoreResolver.ts'
2
-
3
- // Registers the runtime's active-CacheStore resolver. Called once per side at boot.
4
- export const setCacheStoreResolver = cacheStoreResolver.set
@@ -1,4 +0,0 @@
1
- import { globalCacheStoreResolver } from './globalCacheStoreResolver.ts'
2
-
3
- // Registers the process-level cache store resolver. Called once per side at boot.
4
- export const setGlobalCacheStoreResolver = globalCacheStoreResolver.set
@@ -1,4 +0,0 @@
1
- import { pageResolver } from './pageResolver.ts'
2
-
3
- // Registers the runtime's page resolver. Called once per side at boot.
4
- export const setPageResolver = pageResolver.set
@@ -1,4 +0,0 @@
1
- import { requestScopeResolver } from './requestScopeResolver.ts'
2
-
3
- // Registers the runtime's request-scope resolver. Called once per side at boot.
4
- export const setRequestScopeResolver = requestScopeResolver.set
@@ -1,28 +0,0 @@
1
- import { parseRouteSegments } from './parseRouteSegments.ts'
2
-
3
- /*
4
- Translates a abide route URL (`/media/[id]/[...rest]`) into the pattern Bun
5
- needs (`/media/:id/*`) for `Bun.serve({ routes })`. Returns the catch-all
6
- segment's original name alongside so the server can rename Bun's `*` param
7
- back to that name on the way out, keeping page-prop destructuring consistent
8
- with the route file path.
9
- */
10
- export function toBunRoutePattern(routeUrl: string): {
11
- pattern: string
12
- catchAllName: string | undefined
13
- } {
14
- let catchAllName: string | undefined
15
- const pattern = parseRouteSegments(routeUrl)
16
- .map((segment) => {
17
- if (segment.kind === 'literal') {
18
- return segment.value
19
- }
20
- if (segment.catchAll) {
21
- catchAllName = segment.name
22
- return '*'
23
- }
24
- return `:${segment.name}`
25
- })
26
- .join('/')
27
- return { pattern, catchAllName }
28
- }
@@ -1,10 +0,0 @@
1
- /*
2
- Window form of `tail()`: `last` is the rolling window size — the reader holds
3
- the last ≤`last` frames, however they arrived. A source that retains a tail
4
- (a socket declared with `{ tail: n }`) seeds the window by replaying up to
5
- `last` retained frames; a source with no retention (an rpc stream, an
6
- undeclared socket) fills it from live frames only. Integer ≥ 1.
7
- */
8
- export interface TailOptions {
9
- readonly last: number
10
- }
@@ -1,36 +0,0 @@
1
- import { activeCacheStore } from '../shared/activeCacheStore.ts'
2
- import { cacheKeyOf } from '../shared/cacheKeyOf.ts'
3
- import { DEFER_MIN_ARRAY_LENGTH } from '../shared/DEFER_MIN_ARRAY_LENGTH.ts'
4
- import { snapshotShippable } from '../shared/snapshotShippable.ts'
5
- import type { DeferMarker, ResumeEntry } from './runtime/RESUME.ts'
6
-
7
- /*
8
- The resume-manifest entry for a blocking `{#await expr then value}`. When the awaited value
9
- is a shippable cache-backed read, DEFER it: flag the store entry so its SSR snapshot seeds
10
- lazily (the body is decoded on first read, not at hydration) and return a `{ defer, key }`
11
- marker in place of the value. Hydration then adopts the server branch inert and decodes
12
- neither copy — the blocking form's contract: "render it on the server, keep the page, refetch
13
- only when I read or invalidate it." A non-cache value (a plain promise, a computation) or a
14
- cache read whose entry can't ship inlines its value as before. Server-only in practice — the
15
- client codegen never calls it. */
16
- // @documentation plumbing
17
- export function deferResume(promise: unknown, value: unknown): ResumeEntry | DeferMarker {
18
- const isPromise =
19
- promise !== null && typeof (promise as { then?: unknown })?.then === 'function'
20
- const key = isPromise ? cacheKeyOf(promise as Promise<unknown>) : undefined
21
- if (key === undefined) {
22
- return { ok: true, value }
23
- }
24
- const entry = activeCacheStore().entries.get(key)
25
- if (entry === undefined || !snapshotShippable(entry)) {
26
- return { ok: true, value }
27
- }
28
- /* Only defer a genuinely large grid: below the threshold, inline the value and hydrate
29
- eagerly — the block is interactive from the first frame, no inert phase, no wake. This is
30
- what keeps a modest searchable list live instead of frozen-until-idle (the reported gap). */
31
- if (!Array.isArray(value) || value.length < DEFER_MIN_ARRAY_LENGTH) {
32
- return { ok: true, value }
33
- }
34
- entry.deferred = true
35
- return { defer: true, key }
36
- }
@@ -1,14 +0,0 @@
1
- import { isElement } from './isElement.ts'
2
-
3
- /* The first Element in the sibling run `[start, end)` — the node a visible-wake observes for a
4
- deferred region (await branch or component island). Undefined when the run holds no element
5
- (text/comment only), so the caller falls back to an idle wake. Element detection is
6
- method-based (`isElement`), not `nodeType`, so the walk runs under the test mini-dom too. */
7
- export function firstElementBetween(start: Node | null, end: Node | null): Element | undefined {
8
- for (let node = start; node !== null && node !== end; node = node.nextSibling) {
9
- if (isElement(node)) {
10
- return node
11
- }
12
- }
13
- return undefined
14
- }