@abide/abide 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/AGENTS.md +365 -320
  2. package/CHANGELOG.md +80 -0
  3. package/README.md +203 -168
  4. package/package.json +12 -8
  5. package/src/abideLsp.ts +11 -12
  6. package/src/abideResolverPlugin.ts +9 -4
  7. package/src/lib/bundle/disconnected.abide +3 -0
  8. package/src/lib/cli/printCommandHelp.ts +43 -0
  9. package/src/lib/cli/printSessionHelp.ts +2 -1
  10. package/src/lib/cli/{printHelp.ts → printTopLevelHelp.ts} +3 -40
  11. package/src/lib/cli/runCli.ts +2 -1
  12. package/src/lib/mcp/buildPrompts.ts +25 -0
  13. package/src/lib/mcp/dispatchMcpRequest.ts +8 -6
  14. package/src/lib/mcp/mcpResourceServerSlot.ts +5 -10
  15. package/src/lib/mcp/mcpSurface.ts +13 -252
  16. package/src/lib/mcp/mcpTools.ts +187 -0
  17. package/src/lib/mcp/renderPrompt.ts +25 -0
  18. package/src/lib/mcp/types/McpSurface.ts +15 -0
  19. package/src/lib/mcp/types/PromptDescriptor.ts +5 -0
  20. package/src/lib/mcp/types/PromptMessage.ts +2 -0
  21. package/src/lib/mcp/types/ToolDescriptor.ts +7 -0
  22. package/src/lib/mcp/types/ToolResult.ts +2 -0
  23. package/src/lib/server/agent.ts +1 -1
  24. package/src/lib/server/jsonl.ts +2 -2
  25. package/src/lib/server/rpc/defineRpc.ts +5 -0
  26. package/src/lib/server/runtime/containsTraversal.ts +21 -17
  27. package/src/lib/server/runtime/createAppAssetServer.ts +11 -16
  28. package/src/lib/server/runtime/createServer.ts +101 -71
  29. package/src/lib/server/runtime/createUiPageRenderer.ts +23 -4
  30. package/src/lib/server/runtime/devClientFingerprint.ts +3 -3
  31. package/src/lib/server/runtime/devHotModuleResponse.ts +2 -1
  32. package/src/lib/server/runtime/finalizeResponse.ts +32 -0
  33. package/src/lib/server/runtime/snapshotEntryFromCache.ts +8 -17
  34. package/src/lib/server/socket.ts +1 -1
  35. package/src/lib/server/sockets/createSocketDispatcher.ts +28 -5
  36. package/src/lib/server/sockets/defineSocket.ts +26 -7
  37. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +1 -1
  38. package/src/lib/server/sockets/types/SocketRoutes.ts +1 -1
  39. package/src/lib/server/sse.ts +2 -2
  40. package/src/lib/shared/DEFER.ts +8 -0
  41. package/src/lib/shared/activeCacheStore.ts +2 -2
  42. package/src/lib/shared/activePage.ts +2 -2
  43. package/src/lib/shared/attachRpcSelectorMethods.ts +42 -0
  44. package/src/lib/shared/attachSocketSelectorMethods.ts +34 -0
  45. package/src/lib/shared/basePath.ts +2 -2
  46. package/src/lib/shared/baseSlot.ts +6 -5
  47. package/src/lib/shared/bodyValueForKind.ts +1 -2
  48. package/src/lib/shared/buildSocketOverChannel.ts +40 -4
  49. package/src/lib/shared/cache.ts +477 -110
  50. package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
  51. package/src/lib/shared/cacheStoreSlot.ts +9 -5
  52. package/src/lib/{server/runtime → shared}/createReachable.ts +2 -2
  53. package/src/lib/shared/createRemoteFunction.ts +58 -9
  54. package/src/lib/shared/createResolverSlot.ts +24 -31
  55. package/src/lib/shared/createSocketSubRegistry.ts +2 -2
  56. package/src/lib/shared/decodeRefJson.ts +4 -2
  57. package/src/lib/shared/decodeResponse.ts +8 -6
  58. package/src/lib/shared/done.ts +14 -0
  59. package/src/lib/shared/encodeRefJson.ts +4 -2
  60. package/src/lib/shared/globalCacheStoreSlot.ts +9 -6
  61. package/src/lib/shared/hydratingSlot.ts +12 -0
  62. package/src/lib/shared/isLayoutFile.ts +12 -0
  63. package/src/lib/shared/keyForRemoteCall.ts +2 -1
  64. package/src/lib/shared/keyPrefixForRemote.ts +12 -0
  65. package/src/lib/shared/matchRoute.ts +175 -0
  66. package/src/lib/shared/normalizePathname.ts +11 -0
  67. package/src/lib/shared/pageSlot.ts +14 -5
  68. package/src/lib/shared/pageUrlForFile.ts +3 -3
  69. package/src/lib/shared/parseRouteSegments.ts +16 -7
  70. package/src/lib/shared/patch.ts +41 -0
  71. package/src/lib/shared/peek.ts +35 -0
  72. package/src/lib/shared/prepareRemoteExport.ts +40 -0
  73. package/src/lib/shared/prepareRpcModule.ts +98 -16
  74. package/src/lib/shared/prepareSocketModule.ts +11 -15
  75. package/src/lib/shared/queryStringFromArgs.ts +11 -0
  76. package/src/lib/shared/reachable.ts +102 -0
  77. package/src/lib/shared/refresh.ts +22 -0
  78. package/src/lib/shared/requestScopeSlot.ts +10 -7
  79. package/src/lib/shared/routeParamsShape.ts +9 -9
  80. package/src/lib/shared/rpcErrorRegistry.ts +69 -0
  81. package/src/lib/shared/selectorPrefix.ts +3 -10
  82. package/src/lib/shared/setOwnProperty.ts +19 -0
  83. package/src/lib/shared/snippet.ts +1 -1
  84. package/src/lib/shared/subscribableProbes.ts +111 -0
  85. package/src/lib/shared/tailProbeSlot.ts +8 -1
  86. package/src/lib/shared/types/CacheEntry.ts +9 -15
  87. package/src/lib/shared/types/CacheOptions.ts +8 -0
  88. package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
  89. package/src/lib/shared/types/RemoteCallable.ts +25 -7
  90. package/src/lib/shared/types/RemoteFunction.ts +48 -13
  91. package/src/lib/shared/types/ResolverSlot.ts +7 -4
  92. package/src/lib/shared/types/RpcError.ts +26 -0
  93. package/src/lib/shared/types/SmartReadOptions.ts +32 -0
  94. package/src/lib/shared/types/Socket.ts +54 -0
  95. package/src/lib/shared/types/SsrBootState.ts +27 -0
  96. package/src/lib/shared/types/SsrPayload.ts +21 -0
  97. package/src/lib/shared/types/Subscribable.ts +13 -13
  98. package/src/lib/shared/types/TailHooks.ts +2 -1
  99. package/src/lib/shared/url.ts +29 -14
  100. package/src/lib/shared/wakeHydrationPeeks.ts +20 -0
  101. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  102. package/src/lib/test/createScriptedSurface.ts +1 -1
  103. package/src/lib/test/createTestApp.ts +2 -2
  104. package/src/lib/test/createTestSocketChannel.ts +3 -3
  105. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +5 -6
  106. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +10 -7
  107. package/src/lib/ui/compile/abideUiPlugin.ts +2 -2
  108. package/src/lib/ui/compile/analyzeComponent.ts +28 -6
  109. package/src/lib/ui/compile/compileModule.ts +137 -11
  110. package/src/lib/ui/compile/compileShadow.ts +101 -84
  111. package/src/lib/ui/compile/desugarSignals.ts +120 -107
  112. package/src/lib/ui/compile/generateBuild.ts +48 -16
  113. package/src/lib/ui/compile/generateSSR.ts +117 -14
  114. package/src/lib/ui/compile/identifierReferencePattern.ts +13 -0
  115. package/src/lib/ui/compile/lowerContext.ts +10 -4
  116. package/src/lib/ui/compile/lowerScript.ts +72 -5
  117. package/src/lib/ui/compile/parseTemplate.ts +1 -14
  118. package/src/lib/ui/compile/prepareNestedScript.ts +31 -17
  119. package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
  120. package/src/lib/ui/compile/signalCallee.ts +24 -0
  121. package/src/lib/ui/compile/stripEffects.ts +14 -11
  122. package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
  123. package/src/lib/ui/compile/types/TemplateNode.ts +0 -5
  124. package/src/lib/ui/dom/appendStatic.ts +9 -1
  125. package/src/lib/ui/dom/appendText.ts +15 -3
  126. package/src/lib/ui/dom/assertClaimedText.ts +20 -0
  127. package/src/lib/ui/dom/awaitBlock.ts +19 -83
  128. package/src/lib/ui/dom/bindSelectValue.ts +48 -0
  129. package/src/lib/ui/dom/each.ts +12 -1
  130. package/src/lib/ui/dom/hydrate.ts +10 -0
  131. package/src/lib/ui/dom/mountChild.ts +2 -5
  132. package/src/lib/ui/dom/mountRange.ts +0 -75
  133. package/src/lib/ui/effect.ts +4 -3
  134. package/src/lib/ui/installHotBridge.ts +4 -2
  135. package/src/lib/ui/remoteProxy.ts +18 -2
  136. package/src/lib/ui/renderToStream.ts +9 -13
  137. package/src/lib/ui/resumeSeedScript.ts +2 -2
  138. package/src/lib/ui/router.ts +21 -2
  139. package/src/lib/ui/runtime/RESUME.ts +0 -6
  140. package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
  141. package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
  142. package/src/lib/ui/scope.ts +6 -3
  143. package/src/lib/ui/seedBootState.ts +53 -0
  144. package/src/lib/ui/socketChannel.ts +2 -2
  145. package/src/lib/ui/socketProxy.ts +9 -3
  146. package/src/lib/ui/startClient.ts +16 -30
  147. package/src/lib/ui/state.ts +44 -13
  148. package/src/lib/ui/sync.ts +11 -5
  149. package/src/lib/ui/tryEncodeResume.ts +2 -5
  150. package/src/lib/ui/types/Scope.ts +14 -10
  151. package/src/lib/ui/watch.ts +140 -0
  152. package/src/serverEntry.ts +6 -6
  153. package/template/CLAUDE.md +1 -1
  154. package/template/package.json +1 -1
  155. package/template/src/server/rpc/getHello.ts +3 -3
  156. package/template/src/ui/pages/page.abide +2 -2
  157. package/template/test/app.test.ts +1 -1
  158. package/src/lib/server/reachable.ts +0 -45
  159. package/src/lib/server/sockets/types/Socket.ts +0 -23
  160. package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
  161. package/src/lib/shared/DEFER_MIN_ARRAY_LENGTH.ts +0 -14
  162. package/src/lib/shared/baseResolver.ts +0 -10
  163. package/src/lib/shared/cacheKeyOf.ts +0 -7
  164. package/src/lib/shared/cacheKeyStore.ts +0 -8
  165. package/src/lib/shared/cacheStoreResolver.ts +0 -12
  166. package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
  167. package/src/lib/shared/pageResolver.ts +0 -16
  168. package/src/lib/shared/recordCacheKey.ts +0 -6
  169. package/src/lib/shared/requestScopeResolver.ts +0 -12
  170. package/src/lib/shared/setBaseResolver.ts +0 -4
  171. package/src/lib/shared/setCacheStoreResolver.ts +0 -4
  172. package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
  173. package/src/lib/shared/setPageResolver.ts +0 -4
  174. package/src/lib/shared/setRequestScopeResolver.ts +0 -4
  175. package/src/lib/shared/toBunRoutePattern.ts +0 -28
  176. package/src/lib/shared/types/TailOptions.ts +0 -10
  177. package/src/lib/ui/deferResume.ts +0 -36
  178. package/src/lib/ui/dom/firstElementBetween.ts +0 -14
  179. package/src/lib/ui/matchRoute.ts +0 -106
  180. package/src/lib/ui/runtime/scheduleWake.ts +0 -28
  181. package/src/lib/ui/runtime/whenIdle.ts +0 -21
  182. package/src/lib/ui/runtime/whenVisible.ts +0 -105
  183. package/src/lib/ui/tail.ts +0 -324
  184. /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
  185. /package/src/lib/{server/sockets → shared}/types/SocketServerFrame.ts +0 -0
@@ -1,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
  }