@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
@@ -1,13 +1,14 @@
1
+ import { attachSocketSelectorMethods } from '../../shared/attachSocketSelectorMethods.ts'
1
2
  import { createPushIterator } from '../../shared/createPushIterator.ts'
2
3
  import { encodeRefJson } from '../../shared/encodeRefJson.ts'
3
4
  import { resolveClientFlags } from '../../shared/resolveClientFlags.ts'
4
5
  import { socketTapSlot } from '../../shared/socketTapSlot.ts'
6
+ import type { Socket } from '../../shared/types/Socket.ts'
7
+ import type { SocketServerFrame } from '../../shared/types/SocketServerFrame.ts'
5
8
  import type { TailHooks } from '../../shared/types/TailHooks.ts'
6
9
  import { getActiveServer } from '../runtime/getActiveServer.ts'
7
10
  import { registerSocket } from './registerSocket.ts'
8
- import type { Socket } from './types/Socket.ts'
9
11
  import type { SocketOptions } from './types/SocketOptions.ts'
10
- import type { SocketServerFrame } from './types/SocketServerFrame.ts'
11
12
 
12
13
  /*
13
14
  Server-side construction of a Socket. The bundler rewrites every
@@ -23,14 +24,18 @@ is the live stream — no replay. `chat.tail(count)` opens a subscription
23
24
  seeded with the last `count` retained frames (no-arg = the whole
24
25
  retained tail, clamped to the declared `tail` size). When `ttl` is set,
25
26
  retained frames older than `ttl` ms are evicted lazily on every
26
- read/append — no timer runs in the background. `chat.publish(m)` is isomorphic —
27
+ read/append — no timer runs in the background. `chat.broadcast(m)` is isomorphic —
27
28
  called server-side it both notifies in-process iterators and broadcasts
28
29
  to remote subscribers; called client-side (via socketProxy) it sends a
29
30
  `pub` frame the dispatcher validates and forwards.
30
31
  */
31
32
  // @documentation plumbing
32
33
  export function defineSocket<T>(name: string, opts: SocketOptions = {}): Socket<T> {
33
- const retention = opts.tail ?? 0
34
+ /* Retention defaults to 1: the socket keeps its last frame so a late joiner, a
35
+ reconnect, and `.peek()` all have the current value to seed from. Bare iteration
36
+ still replays nothing (iterate(0) below) — a real-time `watch(socket, …)` reaction
37
+ must see only live frames, never re-process the retained one. `tail: 0` opts out. */
38
+ const retention = opts.tail ?? 1
34
39
  const ttl = opts.ttl
35
40
  const schema = opts.schema
36
41
  /*
@@ -150,8 +155,10 @@ export function defineSocket<T>(name: string, opts: SocketOptions = {}): Socket<
150
155
  }
151
156
  })
152
157
  pruneExpired(Date.now())
158
+ /* Floor: a fractional count (`?tail=1.5` over REST) must not
159
+ produce a fractional buffer index. */
153
160
  const replayCount =
154
- replay === 'all' ? buffer.length : Math.min(replay, buffer.length)
161
+ replay === 'all' ? buffer.length : Math.min(Math.floor(replay), buffer.length)
155
162
  if (replayCount > 0) {
156
163
  const start = buffer.length - replayCount
157
164
  for (let index = start; index < buffer.length; index++) {
@@ -168,13 +175,25 @@ export function defineSocket<T>(name: string, opts: SocketOptions = {}): Socket<
168
175
  }
169
176
  }
170
177
 
171
- const self: Socket<T> = {
178
+ const self = {
172
179
  name,
173
180
  clients,
174
- publish,
181
+ /* `broadcast` is the public fan-out; `publish` remains the internal function name. */
182
+ broadcast: publish,
175
183
  tail: (count?: number, hooks?: TailHooks) => iterate(count ?? 'all', hooks),
184
+ /* The latest retained frame (after ttl pruning), or undefined when none is held. */
185
+ peek: () => {
186
+ pruneExpired(Date.now())
187
+ return buffer.length > 0 ? (buffer[buffer.length - 1] as BufferEntry).value : undefined
188
+ },
189
+ /* No-op on the server: it is the source of truth, so there is nothing to re-pull. */
190
+ refresh: () => undefined,
191
+ /* Inert on the server: reaction is a client-only concern, and an author `socket.watch(…)`
192
+ that survives the SSR effect-strip must be a safe no-op here. Returns a disposer. */
193
+ watch: () => () => {},
176
194
  [Symbol.asyncIterator]: () => iterate(0)[Symbol.asyncIterator](),
177
195
  }
196
+ attachSocketSelectorMethods(self)
178
197
  registerSocket({
179
198
  socket: self as Socket<unknown>,
180
199
  allowClientPublish: opts.clientPublish ?? false,
@@ -1,6 +1,6 @@
1
1
  import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
2
+ import type { Socket } from '../../../shared/types/Socket.ts'
2
3
  import type { StandardSchemaV1 } from '../../../shared/types/StandardSchemaV1.ts'
3
- import type { Socket } from './Socket.ts'
4
4
 
5
5
  /*
6
6
  Per-socket registry record. The Socket itself stays uniform between
@@ -1,4 +1,4 @@
1
- import type { Socket } from './Socket.ts'
1
+ import type { Socket } from '../../../shared/types/Socket.ts'
2
2
 
3
3
  /*
4
4
  Manifest of socket-name → module loader. Produced by the resolver
@@ -35,7 +35,7 @@ const KEEPALIVE_INTERVAL_MS = 15000
35
35
  export function sse<Frame>(
36
36
  iterable: AsyncIterable<Frame>,
37
37
  init?: ResponseInit,
38
- ): TypedResponse<Frame> {
38
+ ): TypedResponse<AsyncIterable<Frame>> {
39
39
  const body = streamFromIterator(iterable, {
40
40
  encodeFrame: (value) => `data: ${JSON.stringify(value)}\n\n`,
41
41
  encodeError: (message) => sseErrorFrame.encode(message),
@@ -50,5 +50,5 @@ export function sse<Frame>(
50
50
  'X-Content-Type-Options': 'nosniff',
51
51
  Connection: 'keep-alive',
52
52
  }),
53
- ) as TypedResponse<Frame>
53
+ ) as TypedResponse<AsyncIterable<Frame>>
54
54
  }
@@ -0,0 +1,8 @@
1
+ /*
2
+ The sentinel `bodyValueForKind` returns for the `streaming` and `binary` body kinds —
3
+ those whose handling is side-specific (the live read throws on streaming and blobs on
4
+ binary; the warm read has no synchronous equivalent), so the caller branches on it
5
+ rather than receiving a directly-usable value. A `unique symbol` so a body value can
6
+ never collide with it.
7
+ */
8
+ export const DEFER: unique symbol = Symbol('defer')
@@ -1,4 +1,4 @@
1
- import { cacheStoreResolver } from './cacheStoreResolver.ts'
1
+ import { cacheStoreSlot } from './cacheStoreSlot.ts'
2
2
  import type { CacheStore } from './types/CacheStore.ts'
3
3
 
4
4
  /*
@@ -7,5 +7,5 @@ lazily-created fallback when none is registered (so isolated tests work). The
7
7
  fallback creator guarantees a value, hence the non-null assertion.
8
8
  */
9
9
  export function activeCacheStore(): CacheStore {
10
- return cacheStoreResolver.get()!
10
+ return cacheStoreSlot.get()!
11
11
  }
@@ -1,4 +1,4 @@
1
- import { pageResolver } from './pageResolver.ts'
1
+ import { pageSlot } from './pageSlot.ts'
2
2
  import type { PageSnapshot } from './types/PageSnapshot.ts'
3
3
 
4
4
  /*
@@ -7,5 +7,5 @@ single lazily-created empty snapshot when none is registered (so isolated tests
7
7
  work). The fallback creator guarantees a value, hence the non-null assertion.
8
8
  */
9
9
  export function activePage(): PageSnapshot {
10
- return pageResolver.get()!
10
+ return pageSlot.get()!
11
11
  }
@@ -0,0 +1,42 @@
1
+ import { keyForRemoteCall } from './keyForRemoteCall.ts'
2
+ import { keyPrefixForRemote } from './keyPrefixForRemote.ts'
3
+ import { patch } from './patch.ts'
4
+ import { peek } from './peek.ts'
5
+ import { pending } from './pending.ts'
6
+ import { refresh } from './refresh.ts'
7
+ import { refreshing } from './refreshing.ts'
8
+ import { rpcErrorRegistry } from './rpcErrorRegistry.ts'
9
+ import type { RemoteFunction } from './types/RemoteFunction.ts'
10
+
11
+ /*
12
+ Attaches the pre-bound selector sugar onto an assembled RemoteFunction:
13
+ `fn.pending(args?)` ≡ `pending(fn, args?)`, likewise refreshing / refresh / peek,
14
+ `fn.patch(args?, updater)` ≡ `patch(fn, args, updater)`, and `fn.error(args?)` — the typed
15
+ last error from the rpc error registry (most-recent across the rpc when args omitted, that
16
+ exact call when given). The cached read is the bare call `fn(args, opts)` itself; refetch is
17
+ `fn.refresh(args?)`. The methods only reference the globals at call time, so the shared import
18
+ edge carries no module-init dependency (safe against any cache ↔ createRemoteFunction cycle).
19
+ Attached in createRemoteFunction so the server (defineRpc) and client (remoteProxy) shapes are
20
+ identical. `patch` is attached uniformly; the type omits it for a streaming rpc (harmless at
21
+ runtime — a stream has no cache entry).
22
+ */
23
+ export function attachRpcSelectorMethods<Args, Return>(fn: RemoteFunction<Args, Return>): void {
24
+ Object.assign(fn, {
25
+ pending: (args?: Args) => pending(fn, args),
26
+ refreshing: (args?: Args) => refreshing(fn, args),
27
+ refresh: (args?: Args) => refresh(fn, args),
28
+ peek: (args?: Args) => peek(fn, args),
29
+ patch: (argsOrUpdater?: unknown, updater?: unknown) =>
30
+ (patch as (fn: unknown, a?: unknown, b?: unknown) => void)(fn, argsOrUpdater, updater),
31
+ /* Reaction sugar is client-only (`watch` is a ui primitive that must not ride into a
32
+ server bundle), so the shared attach binds an inert no-op — the server (defineRpc)
33
+ shape carries `.watch` too, and an author `fn.watch(…)` surviving the SSR effect-strip
34
+ is a safe no-op. The client proxy (remoteProxy) overwrites this with the real
35
+ `watch(fn, …)`. */
36
+ watch: () => () => {},
37
+ error: (args?: Args) =>
38
+ args === undefined
39
+ ? rpcErrorRegistry.readAny(keyPrefixForRemote(fn.method, fn.url))
40
+ : rpcErrorRegistry.read(keyForRemoteCall(fn.method, fn.url, args)),
41
+ })
42
+ }
@@ -0,0 +1,34 @@
1
+ import { done } from './done.ts'
2
+ import { pending } from './pending.ts'
3
+ import { refreshing } from './refreshing.ts'
4
+ import { tailProbeSlot } from './tailProbeSlot.ts'
5
+ import type { Socket } from './types/Socket.ts'
6
+
7
+ /* The socket surface before the probe sugar is attached — every Socket member except the
8
+ pre-bound probes this module adds. Both construction sites (defineSocket / buildSocketOverChannel)
9
+ build this shape, then the assertion below narrows it to a full Socket. */
10
+ type SocketWithoutProbes<T> = Omit<Socket<T>, 'pending' | 'refreshing' | 'done' | 'error'>
11
+
12
+ /*
13
+ Attaches the pre-bound probe sugar onto an assembled Socket, mirroring
14
+ attachRpcSelectorMethods for the rpc side: `socket.pending()` ≡ `pending(socket)`,
15
+ likewise refreshing / done, and `socket.error()` reads the stream's terminal error (the
16
+ tail prober's `error` field — instance-only, no bare global, same as the rpc's `.error`).
17
+ The socket is the pre-bound stream selector. The methods reference the globals at call
18
+ time, so the shared import edge carries no module-init dependency. Called by both
19
+ buildSocketOverChannel (consumer / test) and defineSocket (server) so the two Socket
20
+ shapes stay identical; on the server the tail prober is unregistered, so the probes read
21
+ the same fallbacks the globals give there (pending true until a frame, refreshing/done
22
+ false, error undefined). An assertion return narrows the base object to a full Socket at
23
+ each call site, so neither construction literal needs a cast.
24
+ */
25
+ export function attachSocketSelectorMethods<T>(
26
+ socket: SocketWithoutProbes<T>,
27
+ ): asserts socket is Socket<T> {
28
+ Object.assign(socket, {
29
+ pending: () => pending(socket),
30
+ refreshing: () => refreshing(socket),
31
+ done: () => done(socket),
32
+ error: () => tailProbeSlot.probe?.(socket.name)?.error,
33
+ })
34
+ }
@@ -1,4 +1,4 @@
1
- import { baseResolver } from './baseResolver.ts'
1
+ import { baseSlot } from './baseSlot.ts'
2
2
 
3
3
  /*
4
4
  The current mount base path ('' at root). Resolved per side: the server installs
@@ -7,5 +7,5 @@ url() reads this to prefix rooted internal paths. Defaults to '' when no resolve
7
7
  or fallback is set.
8
8
  */
9
9
  export function basePath(): string {
10
- return baseResolver.get() ?? ''
10
+ return baseSlot.get() ?? ''
11
11
  }
@@ -1,8 +1,9 @@
1
- import { baseResolver } from './baseResolver.ts'
1
+ import { createResolverSlot } from './createResolverSlot.ts'
2
2
 
3
3
  /*
4
- Internal slot the runtime entries register their mount-base resolver into (see
5
- baseResolver). Exposed so test helpers snapshot/poke `.resolver` and `.fallback`
6
- directly.
4
+ The mount-base slot. The server entry installs an APP_URL-derived resolver at
5
+ boot; the client entry one reading window.__SSR__.base. No lazy fallback
6
+ creator — `.fallback` is a plain string set directly by isolated tests, and
7
+ basePath() (the public read) supplies the '' default.
7
8
  */
8
- export const baseSlot = baseResolver.slot
9
+ export const baseSlot = createResolverSlot<string>()
@@ -1,4 +1,5 @@
1
1
  import type { ContentBodyKind } from './contentBodyKind.ts'
2
+ import { DEFER } from './DEFER.ts'
2
3
 
3
4
  /*
4
5
  The single body-kind → value mapping shared by the live read (decodeResponse) and
@@ -18,8 +19,6 @@ Returns the DEFER sentinel for `streaming` and `binary`: kinds whose handling is
18
19
  side-specific (the live read throws on streaming and blobs on binary; the warm
19
20
  read has no synchronous equivalent for either), so the caller branches on it.
20
21
  */
21
- export const DEFER: unique symbol = Symbol('defer')
22
-
23
22
  export function bodyValueForKind<Value>(
24
23
  kind: ContentBodyKind,
25
24
  json: () => Value,
@@ -1,8 +1,11 @@
1
- import type { Socket } from '../server/sockets/types/Socket.ts'
1
+ import { attachSocketSelectorMethods } from './attachSocketSelectorMethods.ts'
2
2
  import { browserClientFlags } from './browserClientFlags.ts'
3
3
  import { createPushIterator } from './createPushIterator.ts'
4
+ import { SOCKETS_PATH } from './SOCKETS_PATH.ts'
5
+ import type { Socket } from './types/Socket.ts'
4
6
  import type { SocketChannel } from './types/SocketChannel.ts'
5
7
  import type { TailHooks } from './types/TailHooks.ts'
8
+ import { withBase } from './withBase.ts'
6
9
 
7
10
  /* Per-channel-agnostic sub id counter; uniqueness within a channel is all the
8
11
  lifecycle routing needs, so one monotonic source across every socket is fine. */
@@ -24,6 +27,10 @@ export function buildSocketOverChannel<T>(
24
27
  name: string,
25
28
  resolveChannel: () => SocketChannel,
26
29
  ): Socket<T> {
30
+ /* The latest frame seen across every iterator of this socket — what peek() returns and
31
+ what refresh() re-seeds from the server tail. A plain slot (not reactive): peek(socket)
32
+ is a synchronous snapshot; live reactivity is the for-await / watch(socket) path. */
33
+ let lastFrame: T | undefined
27
34
  function iterate(replay: number | undefined, hooks?: TailHooks): AsyncIterable<T> {
28
35
  return {
29
36
  [Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
@@ -31,9 +38,13 @@ export function buildSocketOverChannel<T>(
31
38
  const channel = resolveChannel()
32
39
  const iterator = createPushIterator<T>(() => channel.unsubscribe(id))
33
40
  channel.subscribe(id, name, replay, {
34
- onMessage: (value) => iterator.push(value as T),
41
+ onMessage: (value) => {
42
+ lastFrame = value as T
43
+ iterator.push(value as T)
44
+ },
35
45
  onReplay: (messages) => {
36
46
  for (const value of messages) {
47
+ lastFrame = value as T
37
48
  iterator.push(value as T)
38
49
  }
39
50
  if (hooks?.replayed) {
@@ -48,11 +59,36 @@ export function buildSocketOverChannel<T>(
48
59
  },
49
60
  }
50
61
  }
51
- return {
62
+ const publish = (message: T) => resolveChannel().publish(name, message)
63
+ const socket = {
52
64
  name,
53
65
  clients: browserClientFlags,
54
- publish: (message: T) => resolveChannel().publish(name, message),
66
+ /* `broadcast` sends a server-validated `pub` frame (the dispatcher gates it on the
67
+ topic's `clientPublish`); `publish` is the internal channel-send function name. */
68
+ broadcast: publish,
55
69
  tail: (count?: number, hooks?: TailHooks) => iterate(count, hooks),
70
+ /* The latest frame this client has seen, synchronously. */
71
+ peek: () => lastFrame,
72
+ /* Re-pull the server's retained tail over the HTTP face (GET /__abide/sockets/<name>
73
+ → JSON array of retained frames) and reset the local latest — a reconnect / ttl
74
+ resync. Fire-and-forget: the fetch runs in the background and updates what peek()
75
+ returns. A failed pull leaves the current value untouched. */
76
+ refresh: () => {
77
+ void fetch(withBase(`${SOCKETS_PATH}/${name}`))
78
+ .then((response) => (response.ok ? response.json() : undefined))
79
+ .then((frames) => {
80
+ if (Array.isArray(frames) && frames.length > 0) {
81
+ lastFrame = frames[frames.length - 1] as T
82
+ }
83
+ })
84
+ .catch(() => undefined)
85
+ },
86
+ /* Inert default so the returned object satisfies Socket<T> without importing the ui-only
87
+ `watch` into this shared builder (which the test harness also uses). The client proxy
88
+ (socketProxy) overwrites it with the real `watch(socket, …)`. */
89
+ watch: () => () => {},
56
90
  [Symbol.asyncIterator]: () => iterate(0)[Symbol.asyncIterator](),
57
91
  }
92
+ attachSocketSelectorMethods(socket)
93
+ return socket
58
94
  }