@abide/abide 0.43.1 → 0.44.1

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 (206) hide show
  1. package/AGENTS.md +293 -219
  2. package/CHANGELOG.md +63 -0
  3. package/README.md +191 -67
  4. package/package.json +2 -2
  5. package/src/abideLsp.ts +8 -4
  6. package/src/abideResolverPlugin.ts +17 -2
  7. package/src/build.ts +9 -0
  8. package/src/buildDisconnected.ts +2 -0
  9. package/src/devEntry.ts +44 -9
  10. package/src/lib/bundle/probeAbideServer.ts +1 -1
  11. package/src/lib/cli/parseArgvForRpc.ts +25 -26
  12. package/src/lib/cli/runCli.ts +9 -24
  13. package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
  14. package/src/lib/cli/types/CliManifest.ts +6 -4
  15. package/src/lib/mcp/annotationsForMethod.ts +1 -1
  16. package/src/lib/mcp/toolResultFromResponse.ts +1 -1
  17. package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
  18. package/src/lib/server/AppModule.ts +1 -1
  19. package/src/lib/server/cli/handleCliDownload.ts +7 -5
  20. package/src/lib/server/cli/installScript.ts +3 -3
  21. package/src/lib/server/prompts/promptRegistry.ts +1 -1
  22. package/src/lib/server/prompts/types/Prompt.ts +2 -1
  23. package/src/lib/server/rpc/defineRpc.ts +9 -1
  24. package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
  25. package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
  26. package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
  27. package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
  28. package/src/lib/server/runtime/createServer.ts +50 -12
  29. package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
  30. package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
  31. package/src/lib/server/runtime/crossOriginGate.ts +1 -1
  32. package/src/lib/server/runtime/devHotModuleResponse.ts +3 -3
  33. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
  34. package/src/lib/server/runtime/gzipResponse.ts +14 -16
  35. package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
  36. package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
  37. package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
  38. package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
  39. package/src/lib/server/runtime/streamFromIterator.ts +2 -2
  40. package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
  41. package/src/lib/server/runtime/types/RequestStore.ts +16 -0
  42. package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
  43. package/src/lib/server/socket.ts +1 -1
  44. package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
  45. package/src/lib/server/sockets/defineSocket.ts +5 -1
  46. package/src/lib/server/sockets/socketRegistry.ts +2 -2
  47. package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
  48. package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
  49. package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
  50. package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
  51. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
  52. package/src/lib/shared/activeCacheStore.ts +5 -14
  53. package/src/lib/shared/activePage.ts +5 -19
  54. package/src/lib/shared/augmentModule.ts +18 -0
  55. package/src/lib/shared/basePath.ts +6 -5
  56. package/src/lib/shared/baseResolver.ts +10 -0
  57. package/src/lib/shared/baseSlot.ts +6 -12
  58. package/src/lib/shared/bodyValueForKind.ts +35 -0
  59. package/src/lib/shared/cache.ts +22 -2
  60. package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
  61. package/src/lib/shared/cacheStoreResolver.ts +12 -0
  62. package/src/lib/shared/cacheStoreSlot.ts +5 -13
  63. package/src/lib/shared/changeAffectsClient.ts +35 -0
  64. package/src/lib/shared/createPushIterator.ts +1 -1
  65. package/src/lib/shared/createResolverSlot.ts +37 -0
  66. package/src/lib/shared/debugGate.ts +90 -0
  67. package/src/lib/shared/decodeResponse.ts +11 -8
  68. package/src/lib/shared/emitLogRecord.ts +18 -4
  69. package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
  70. package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
  71. package/src/lib/shared/hasReplayableRequest.ts +17 -0
  72. package/src/lib/shared/html.ts +7 -26
  73. package/src/lib/shared/isDebugEnabled.ts +3 -10
  74. package/src/lib/shared/isDebugNegated.ts +4 -6
  75. package/src/lib/shared/manifestModule.ts +2 -2
  76. package/src/lib/shared/online.ts +1 -1
  77. package/src/lib/shared/outboxProbeSlot.ts +1 -1
  78. package/src/lib/shared/pageResolver.ts +16 -0
  79. package/src/lib/shared/pageSlot.ts +5 -14
  80. package/src/lib/shared/pageUrlForFile.ts +1 -1
  81. package/src/lib/shared/requestScopeResolver.ts +12 -0
  82. package/src/lib/shared/requestScopeSlot.ts +7 -12
  83. package/src/lib/shared/responseBodyKind.ts +35 -0
  84. package/src/lib/shared/routeParamsShape.ts +20 -0
  85. package/src/lib/shared/setBaseResolver.ts +2 -4
  86. package/src/lib/shared/setCacheStoreResolver.ts +3 -5
  87. package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
  88. package/src/lib/shared/setPageResolver.ts +2 -5
  89. package/src/lib/shared/setRequestScopeResolver.ts +3 -5
  90. package/src/lib/shared/snapshotShippable.ts +22 -0
  91. package/src/lib/shared/types/CacheEntry.ts +1 -1
  92. package/src/lib/shared/types/CacheInvalidation.ts +3 -3
  93. package/src/lib/shared/types/ClientFlags.ts +5 -3
  94. package/src/lib/shared/types/DebugGate.ts +10 -0
  95. package/src/lib/shared/types/LastConnection.ts +1 -1
  96. package/src/lib/shared/types/ResolverSlot.ts +10 -0
  97. package/src/lib/shared/url.ts +4 -1
  98. package/src/lib/shared/withBase.ts +1 -2
  99. package/src/lib/shared/writeDts.ts +10 -3
  100. package/src/lib/shared/writePublicAssetsDts.ts +5 -9
  101. package/src/lib/shared/writeRoutesDts.ts +14 -46
  102. package/src/lib/shared/writeRpcDts.ts +13 -12
  103. package/src/lib/shared/writeTestRpcDts.ts +11 -11
  104. package/src/lib/shared/writeTestSocketsDts.ts +9 -9
  105. package/src/lib/test/createTestSocketChannel.ts +3 -3
  106. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
  107. package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
  108. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
  109. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
  110. package/src/lib/ui/compile/abideUiPlugin.ts +10 -8
  111. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +27 -13
  112. package/src/lib/ui/compile/awaitPlan.ts +14 -2
  113. package/src/lib/ui/compile/catchBinding.ts +8 -0
  114. package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
  115. package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
  116. package/src/lib/ui/compile/compileComponent.ts +4 -4
  117. package/src/lib/ui/compile/compileModule.ts +56 -21
  118. package/src/lib/ui/compile/compileSSR.ts +12 -10
  119. package/src/lib/ui/compile/compileShadow.ts +163 -12
  120. package/src/lib/ui/compile/composeProps.ts +3 -3
  121. package/src/lib/ui/compile/createShadowLanguageService.ts +58 -4
  122. package/src/lib/ui/compile/createShadowProgram.ts +2 -1
  123. package/src/lib/ui/compile/createShadowScope.ts +42 -0
  124. package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
  125. package/src/lib/ui/compile/desugarSignals.ts +12 -7
  126. package/src/lib/ui/compile/eachPlan.ts +61 -0
  127. package/src/lib/ui/compile/elementPlan.ts +55 -0
  128. package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
  129. package/src/lib/ui/compile/generateBuild.ts +156 -127
  130. package/src/lib/ui/compile/generateSSR.ts +112 -73
  131. package/src/lib/ui/compile/ifPlan.ts +7 -1
  132. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
  133. package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
  134. package/src/lib/ui/compile/isTextLeaf.ts +8 -7
  135. package/src/lib/ui/compile/lowerContext.ts +36 -46
  136. package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
  137. package/src/lib/ui/compile/lowerScript.ts +1 -1
  138. package/src/lib/ui/compile/markupTokens.ts +313 -0
  139. package/src/lib/ui/compile/pagePropsType.ts +23 -0
  140. package/src/lib/ui/compile/parseTemplate.ts +135 -62
  141. package/src/lib/ui/compile/reactiveBinding.ts +47 -0
  142. package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
  143. package/src/lib/ui/compile/skeletonable.ts +5 -5
  144. package/src/lib/ui/compile/snippetPlan.ts +28 -0
  145. package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
  146. package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
  147. package/src/lib/ui/compile/switchPlan.ts +7 -1
  148. package/src/lib/ui/compile/tryPlan.ts +8 -1
  149. package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
  150. package/src/lib/ui/compile/types/Binding.ts +24 -0
  151. package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
  152. package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
  153. package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
  154. package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
  155. package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
  156. package/src/lib/ui/compile/types/TemplateNode.ts +9 -9
  157. package/src/lib/ui/compile/withBindings.ts +45 -0
  158. package/src/lib/ui/createScope.ts +2 -2
  159. package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
  160. package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
  161. package/src/lib/ui/dom/appendText.ts +1 -1
  162. package/src/lib/ui/dom/applyResolved.ts +1 -1
  163. package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
  164. package/src/lib/ui/dom/cloneStatic.ts +3 -2
  165. package/src/lib/ui/dom/each.ts +26 -7
  166. package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
  167. package/src/lib/ui/dom/mountChild.ts +8 -1
  168. package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
  169. package/src/lib/ui/dom/readCall.ts +6 -4
  170. package/src/lib/ui/dom/scopeLabel.ts +3 -3
  171. package/src/lib/ui/dom/skeleton.ts +10 -1
  172. package/src/lib/ui/dom/switchBlock.ts +17 -63
  173. package/src/lib/ui/dom/types/EachRow.ts +4 -0
  174. package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
  175. package/src/lib/ui/dom/when.ts +11 -64
  176. package/src/lib/ui/history.ts +1 -2
  177. package/src/lib/ui/html.ts +28 -0
  178. package/src/lib/ui/installHotBridge.ts +0 -2
  179. package/src/lib/ui/renderChain.ts +16 -3
  180. package/src/lib/ui/router.ts +24 -3
  181. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
  182. package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
  183. package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
  184. package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
  185. package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
  186. package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
  187. package/src/lib/ui/runtime/createDoc.ts +107 -13
  188. package/src/lib/ui/runtime/createEffectNode.ts +5 -5
  189. package/src/lib/ui/runtime/flushEffects.ts +22 -9
  190. package/src/lib/ui/runtime/historyEntries.ts +1 -1
  191. package/src/lib/ui/runtime/track.ts +4 -2
  192. package/src/lib/ui/runtime/trigger.ts +34 -20
  193. package/src/lib/ui/runtime/types/Route.ts +2 -2
  194. package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
  195. package/src/lib/ui/runtime/types/UiProps.ts +9 -12
  196. package/src/lib/ui/runtime/walkPath.ts +15 -3
  197. package/src/lib/ui/startClient.ts +41 -14
  198. package/src/lib/ui/state.ts +3 -3
  199. package/src/lib/ui/types/Scope.ts +3 -3
  200. package/src/serverBuildPlugins.ts +11 -5
  201. package/src/zodCjsPlugin.ts +51 -0
  202. package/template/.zed/settings.json +7 -1
  203. package/template/package.json +3 -0
  204. package/template/src/ui/pages/layout.abide +3 -5
  205. package/src/lib/shared/matchesDebugPattern.ts +0 -16
  206. package/src/lib/shared/parseDebugPatterns.ts +0 -21
@@ -1,17 +1,9 @@
1
- import { contentTypeOf } from '../../shared/contentTypeOf.ts'
2
- import { isStreamingResponse } from '../../shared/isStreamingResponse.ts'
1
+ import type { ResponseBodyKind } from '../../shared/responseBodyKind.ts'
2
+ import { responseBodyKind } from '../../shared/responseBodyKind.ts'
3
3
  import { acceptsGzip } from './acceptsGzip.ts'
4
4
  import { flushingGzipStream } from './flushingGzipStream.ts'
5
5
  import { STREAMED_HTML_HEADER } from './STREAMED_HTML_HEADER.ts'
6
6
 
7
- /*
8
- Compressible Content-Types — text and structured-text payloads. Binary or
9
- already-compressed bodies (images, fonts, archives, zstd/gzip blobs) gain
10
- nothing from a second pass and only burn CPU.
11
- */
12
- const COMPRESSIBLE_TYPE =
13
- /^(?:text\/|application\/(?:json|javascript|xml|[\w.-]+\+(?:json|xml))|image\/svg)/
14
-
15
7
  /*
16
8
  Gzips a dynamic response (SSR HTML, rpc/json replies, the plain 404) when the
17
9
  client accepts it. Static assets never reach here — they already carry a
@@ -23,6 +15,12 @@ expose a string body's length before send, and measuring would mean buffering th
23
15
  body — the framing overhead on the rare tiny body is negligible against
24
16
  compressing every page and rpc payload.
25
17
 
18
+ `kind` is the body class the dispatch pipeline already computed (S2: classify
19
+ once, thread it in) — only `compressible` bodies gzip; `streaming` and `opaque`
20
+ pass through. Standalone callers (the health probe) omit it and it's derived
21
+ here. The streamed-HTML marker is handled independent of `kind` since it's
22
+ stripped even on a skip path.
23
+
26
24
  Buffered bodies take the web CompressionStream (best ratio, one flush at close).
27
25
  The streamed SSR document self-marks (STREAMED_HTML_HEADER) and takes a
28
26
  per-chunk-flushing gzip instead: the plain CompressionStream buffers the head
@@ -30,7 +28,7 @@ until its deflate window fills, which defeats streaming (the browser can't
30
28
  preload-scan the head or paint the pending shell until the stream nearly closes).
31
29
  The marker is stripped so it never reaches the client.
32
30
  */
33
- export function gzipResponse(req: Request, response: Response): Response {
31
+ export function gzipResponse(req: Request, response: Response, kind?: ResponseBodyKind): Response {
34
32
  const streamedHtml = response.headers.has(STREAMED_HTML_HEADER)
35
33
  if (streamedHtml) {
36
34
  response.headers.delete(STREAMED_HTML_HEADER)
@@ -38,11 +36,11 @@ export function gzipResponse(req: Request, response: Response): Response {
38
36
  if (!response.body || response.headers.has('Content-Encoding')) {
39
37
  return response
40
38
  }
41
- if (!acceptsGzip(req) || isStreamingResponse(response)) {
42
- return response
43
- }
44
- const contentType = contentTypeOf(response.headers)
45
- if (!COMPRESSIBLE_TYPE.test(contentType)) {
39
+ /* A streamed-HTML document classifies as `compressible` (text/html) but the
40
+ marker was already stripped above, so re-derive after the strip when the
41
+ pipeline didn't hand a kind in. */
42
+ const bodyKind = kind ?? responseBodyKind(response)
43
+ if (!acceptsGzip(req) || bodyKind !== 'compressible') {
46
44
  return response
47
45
  }
48
46
  const headers = new Headers(response.headers)
@@ -83,7 +83,7 @@ exposed on, so abide's routing and multimodal-by-default exposure are auditable
83
83
  rather than implicit. Three aligned tables — scan a column to spot a missing
84
84
  surface, a row to see one declaration's reach:
85
85
 
86
- - pages: each route with the nearest layout and error boundary wrapping it.
86
+ - pages: each route key.
87
87
  - sockets: name + client surfaces (schema/browser/mcp/cli) and whether clients
88
88
  may publish.
89
89
  - rpcs: method+path (headed `http` since http/openapi are unconditional) +
@@ -95,9 +95,8 @@ then the surface columns, so the glyphs line up vertically across the tables.
95
95
  rpc folds its method into a left-aligned prefix of the identifier cell, so paths
96
96
  still start at a shared column. For sockets and rpcs the `schema`
97
97
  column leads: it's what unlocks the non-browser surfaces, so a missing schema
98
- reddens to flag the gated declaration. Loads the full registry, so it runs once
99
- at boot and only under `abide` debug logging (DEBUG=abide) to avoid forcing
100
- eager imports in production. Best-effort: enumeration failures are swallowed,
98
+ reddens to flag the gated declaration. Loads the full registry. Runs once at boot; suppressed only via DEBUG=-abide
99
+ (the always-on logging opt-out). Best-effort: enumeration failures are swallowed,
101
100
  this is diagnostic only.
102
101
  */
103
102
  export async function logExposedSurfaces(routing: { pages: Pages }): Promise<void> {
@@ -120,7 +120,10 @@ export function runWithRequestScope(
120
120
  })
121
121
  })
122
122
  }
123
- if (response.body && isStreamingResponse(response)) {
123
+ /* Reuse the dispatch pipeline's single S2 classification when it stashed
124
+ one; callers that don't (mcp, the fetch fallback, tests) classify here. */
125
+ const streaming = store.responseStreaming ?? isStreamingResponse(response)
126
+ if (response.body && streaming) {
124
127
  /*
125
128
  A hand-pumped wrap rather than a TransformStream: streams typically
126
129
  end by client disconnect (an SSE tab closing), which cancels the
@@ -1,11 +1,11 @@
1
- import { isReplayableMethod } from '../../shared/isReplayableMethod.ts'
1
+ import { snapshotShippable } from '../../shared/snapshotShippable.ts'
2
2
  import type { CacheSnapshotEntry } from '../../shared/types/CacheSnapshotEntry.ts'
3
3
  import type { CacheStore } from '../../shared/types/CacheStore.ts'
4
4
  import { snapshotEntryFromCache } from './snapshotEntryFromCache.ts'
5
5
 
6
6
  /*
7
7
  Snapshots the request-scoped cache for SSR at a single instant: every replayable
8
- (GET/DELETE) entry settled by now, serialized to a wire-safe CacheSnapshotEntry the
8
+ (GET-only) entry settled by now, serialized to a wire-safe CacheSnapshotEntry the
9
9
  client seeds its store from. Unsettled and non-replayable entries are skipped; a body
10
10
  that can't ship (binary / streaming / rejected) drops out via snapshotEntryFromCache.
11
11
 
@@ -21,12 +21,7 @@ over the wire — see its post-stream `__abideResolve` pass. So for a streaming
21
21
  render-return snapshot is typically empty; the warm cache arrives over the stream.
22
22
  */
23
23
  export async function serializeCacheSnapshot(store: CacheStore): Promise<CacheSnapshotEntry[]> {
24
- const settled = Array.from(store.entries.values()).filter(
25
- (entry) =>
26
- entry.settled === true &&
27
- entry.request !== undefined &&
28
- isReplayableMethod(entry.request.method.toUpperCase()),
29
- )
24
+ const settled = Array.from(store.entries.values()).filter(snapshotShippable)
30
25
  const snapshots = await Promise.all(
31
26
  settled.map((entry) => snapshotEntryFromCache(store, entry)),
32
27
  )
@@ -1,9 +1,10 @@
1
1
  import { contentTypeOf } from '../../shared/contentTypeOf.ts'
2
- import { isReplayableMethod } from '../../shared/isReplayableMethod.ts'
2
+ import { hasReplayableRequest } from '../../shared/hasReplayableRequest.ts'
3
3
  import { isStreamingResponse } from '../../shared/isStreamingResponse.ts'
4
4
  import type { CacheEntry } from '../../shared/types/CacheEntry.ts'
5
5
  import type { CacheSnapshotEntry } from '../../shared/types/CacheSnapshotEntry.ts'
6
6
  import type { CacheStore } from '../../shared/types/CacheStore.ts'
7
+ import type { ReplayableMethod } from '../../shared/types/ReplayableMethod.ts'
7
8
 
8
9
  /*
9
10
  Awaits one cache entry and turns it into a wire-safe snapshot, or undefined
@@ -25,14 +26,16 @@ export async function snapshotEntryFromCache(
25
26
  store: CacheStore,
26
27
  entry: CacheEntry,
27
28
  ): Promise<CacheSnapshotEntry | undefined> {
28
- /* Producer entries have no wire request to replay they never snapshot. */
29
- if (!entry.request) {
30
- return undefined
31
- }
32
- const method = entry.request.method.toUpperCase()
33
- if (!isReplayableMethod(method)) {
29
+ /* The request half of the shared shippability gate: a replayable wire request.
30
+ Producer entries (no request) and non-replayable methods never snapshot — the
31
+ streaming-drain caller hands still-pending entries here, so `settled` is NOT gated
32
+ (the caller's snapshotShippable filter already required it for the entries it picks). */
33
+ if (!hasReplayableRequest(entry) || !entry.request) {
34
34
  return undefined
35
35
  }
36
+ /* `hasReplayableRequest` already verified the uppercased method is replayable; assert it
37
+ for the snapshot's typed `method` field rather than re-running the same Set check. */
38
+ const method = entry.request.method.toUpperCase() as ReplayableMethod
36
39
  const response = await readSettled(entry.promise as Promise<Response>)
37
40
  if (!response) {
38
41
  return undefined
@@ -4,7 +4,7 @@ import { messageFromError } from '../../shared/messageFromError.ts'
4
4
  Shared body builder for the streaming respond helpers (`jsonl`, `sse`).
5
5
  Both flow the same shape — pull from an AsyncIterator, encode each frame
6
6
  to bytes, emit a sentinel `error` frame on a generator throw, and route
7
- ReadableStream's `cancel` into `iter.return()` so the handler's
7
+ ReadableStream's `cancel` into `iterator.return()` so the handler's
8
8
  `for await` exits via its normal control path. Only the per-frame
9
9
  encoding and the optional keepalive payload differ between the two.
10
10
 
@@ -85,7 +85,7 @@ export function streamFromIterator<T>(
85
85
  rejection from its cleanup: a `finally` that throws on `.return()`
86
86
  would otherwise surface as an unhandled rejection (process-fatal
87
87
  under Bun's default) on a client disconnect — a path every
88
- sse/jsonl/resolve stream hits routinely.
88
+ sse/jsonl stream hits routinely.
89
89
  */
90
90
  return iterator.return?.(reason)?.then(
91
91
  () => undefined,
@@ -1,6 +1,6 @@
1
1
  /*
2
- One RPC rpc projected for the inspector: the registry entry reduced to the
3
- serializable facts the UI renders — where it mounts, its method, which
2
+ One declared RPC projected for the inspector: the registry entry reduced to
3
+ the serializable facts the UI renders — where it mounts, its method, which
4
4
  non-browser surfaces advertise it, and its argument/result shapes as JSON
5
5
  Schema. Schemas project through jsonSchemaForSchema (same as MCP/OpenAPI), so
6
6
  a rpc whose library can't render a schema still lists with an opaque shape.
@@ -52,4 +52,20 @@ export type RequestStore = {
52
52
  carried no file parts.
53
53
  */
54
54
  files?: Record<string, File[]>
55
+ /*
56
+ The allowlisted headers forwarded onto in-process rpc Requests during this
57
+ request's SSR pass — derived once from the inbound headers (which don't change
58
+ within the scope) and reused by every defineRpc call. Each call clones it,
59
+ since buildRpcRequest mutates the Headers (content-type/ref-json) before
60
+ constructing the Request. Undefined until the first in-process rpc.
61
+ */
62
+ forwardedHeaders?: Headers
63
+ /*
64
+ The body class of the response the dispatch pipeline produced, stashed so
65
+ runWithRequestScope's closing-record stream monitor reuses the single S2
66
+ classification instead of re-deriving from the Content-Type. Undefined for
67
+ callers that don't classify (mcp, the fetch fallback, tests) — those fall
68
+ back to classifying the response themselves.
69
+ */
70
+ responseStreaming?: boolean
55
71
  }
@@ -17,8 +17,13 @@ export function withResponseDefaults(
17
17
  status?: number,
18
18
  ): ResponseInit {
19
19
  const headers = new Headers(defaultHeaders)
20
- new Headers(init?.headers).forEach((value, key) => {
21
- headers.set(key, value)
22
- })
20
+ /* The dominant case carries no overrides — skip the second Headers
21
+ allocation + per-key copy and ship the defaults alone. Only merge when
22
+ the caller actually supplied headers. */
23
+ if (init?.headers !== undefined) {
24
+ new Headers(init.headers).forEach((value, key) => {
25
+ headers.set(key, value)
26
+ })
27
+ }
23
28
  return { ...init, headers, ...(status !== undefined && { status }) }
24
29
  }
@@ -6,7 +6,6 @@ import type { SocketOptions } from './sockets/types/SocketOptions.ts'
6
6
  Declares a Socket inside a file under `src/server/sockets/`. Each file contains
7
7
  exactly one export, named after the file (e.g. `chat.ts` →
8
8
  `export const chat = socket<ChatMessage>(...)`). The bundler reads the
9
- // @documentation sockets
10
9
  export name from the filename and the socket name from the file path
11
10
  under `src/server/sockets/`, then rewrites this call to bind the name into the
12
11
  runtime implementation (defineSocket on the server, socketProxy on the
@@ -21,6 +20,7 @@ to browser-only when schemaless, all surfaces when a schema is present.
21
20
  This function exists only for the type signature; calling it directly
22
21
  means the bundler plugin didn't process the file, which throws.
23
22
  */
23
+ // @documentation sockets
24
24
  export function socket<Schema extends StandardSchemaV1>(
25
25
  opts: SocketOptions<Schema> & { schema: Schema },
26
26
  ): Socket<StandardSchemaV1.InferOutput<Schema>>
@@ -220,7 +220,7 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
220
220
  }
221
221
  /*
222
222
  publish() runs the topic's optional Standard Schema synchronously
223
- and throws on failure (see defineSocket.validateSync). The
223
+ and throws on failure (see validateSync in defineSocket.ts). The
224
224
  dispatcher invokes us via `void handlePub(...)`, so an unhandled
225
225
  throw would surface as an unhandled promise rejection on every
226
226
  malformed client frame. Catch + log so a buggy client can't take
@@ -317,7 +317,7 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
317
317
  try {
318
318
  /* ref-json from abide's own client; a non-abide client sends a plain-JSON
319
319
  frame, which decodeRefJson rejects (a frame is always an object, never the
320
- `[root, slots]` envelope) — fall back to JSON.parse so raw WS clients work. */
320
+ `[rootValue, slots]` envelope) — fall back to JSON.parse so raw WS clients work. */
321
321
  try {
322
322
  frame = decodeRefJson(text) as SocketClientFrame
323
323
  } catch {
@@ -119,7 +119,11 @@ export function defineSocket<T>(name: string, opts: SocketOptions = {}): Socket<
119
119
  cachedServer = getActiveServer()
120
120
  }
121
121
  const server = cachedServer
122
- if (server) {
122
+ /* Remote fan-out is the only consumer of the encoded frame: in-process iterators
123
+ already received the live object via notify() above. Skip the full message-graph
124
+ walk (encodeRefJson) + native publish when zero ws clients are subscribed to the
125
+ topic — subscriberCount is a cheap C-side check, encodeRefJson is not. */
126
+ if (server && server.subscriberCount(topic) > 0) {
123
127
  /* Typed against the shared wire contract so a `SocketServerFrame` change can't
124
128
  silently drift from this construction site (the dispatcher's `send` is already
125
129
  typed; this was the last `msg` frame built through an unchecked JSON.stringify). */
@@ -3,7 +3,7 @@ import type { SocketRegistryEntry } from './types/SocketRegistryEntry.ts'
3
3
  /*
4
4
  Process-wide registry of every Socket declared in the app. defineSocket
5
5
  inserts on first construction; the dispatcher reads on every `sub` /
6
- `pub` frame so it can find the right Socket by name and check the
7
- opted-in `allowClientPublish` policy.
6
+ `pub` frame and every REST request so it can find the right Socket by
7
+ name and check the opted-in `allowClientPublish` policy.
8
8
  */
9
9
  export const socketRegistry = new Map<string, SocketRegistryEntry>()
@@ -9,9 +9,9 @@ the dispatcher checks the topic's `clientPublish` flag before fanning
9
9
  out.
10
10
 
11
11
  `sub` is the per-subscription id minted client-side; the server treats
12
- it as opaque and routes inbound `msg|err|end` frames back to the same
13
- id so one ws can multiplex many subscriptions to the same or different
14
- sockets.
12
+ it as opaque and routes inbound `replay|err|end` frames back to the same
13
+ id `msg` frames are broadcast keyed by socket name; the client
14
+ demuxes them against every local sub for that socket.
15
15
  */
16
16
  export type SocketClientFrame =
17
17
  | { type: 'sub'; sub: string; socket: string; replay?: number }
@@ -2,7 +2,8 @@
2
2
  Brand on the invoker cache() returns; its property value is the wrapped fn,
3
3
  so inspection shows what a wrapper wraps. Detection is certain (set by us,
4
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 a
6
- re-wrapped wrapper silently downgrades a remote to an anonymous producer.
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).
7
8
  */
8
9
  export const CACHE_WRAPPED: unique symbol = Symbol('abide.cacheWrapped')
@@ -3,6 +3,6 @@ Brand set by createRemoteFunction on both the decoded callable and its raw
3
3
  sibling — the exact answer to "is this a abide remote?" where structural
4
4
  checks ('url' in fn) would misclassify any user function that happens to
5
5
  carry a url property. Read by cache()'s remote/producer discriminator,
6
- selectorMatcher's identity branch, and the route dispatcher's export scan.
6
+ selectorPrefix's identity branch, and the route dispatcher's export scan.
7
7
  */
8
8
  export const REMOTE_FUNCTION: unique symbol = Symbol('abide.remoteFunction')
@@ -0,0 +1,7 @@
1
+ /*
2
+ The `RpcArgs<Fn>` type alias both rpc d.ts writers emit verbatim: it lifts the
3
+ args type out of a RemoteFunction, dropping the FormData upload variant so a
4
+ url()/test-client call types against the rpc's plain args.
5
+ */
6
+ export const RPC_ARGS_TYPE =
7
+ 'type RpcArgs<Fn> = Fn extends (args: infer Args) => unknown ? Exclude<Args, FormData> : never'
@@ -1,8 +1,9 @@
1
1
  /*
2
2
  Content-Type prefixes abide treats as streaming bodies — SSE for the
3
3
  `sse()` helper, JSONL / NDJSON for the `jsonl()` helper. Used by
4
- decodeResponse to refuse a buffered decode and by streamResponse to
5
- choose the frame parser.
4
+ contentBodyKind (called by decodeResponse and the one-shot path in
5
+ streamResponse) to classify content, and by streamResponse to select the
6
+ JSONL frame parser.
6
7
  */
7
8
  export const STREAMING_CONTENT_TYPES = [
8
9
  'text/event-stream',
@@ -1,20 +1,11 @@
1
- import { cacheStoreSlot } from './cacheStoreSlot.ts'
2
- import { createCacheStore } from './createCacheStore.ts'
1
+ import { cacheStoreResolver } from './cacheStoreResolver.ts'
3
2
  import type { CacheStore } from './types/CacheStore.ts'
4
3
 
5
4
  /*
6
- Resolves the active CacheStore. The runtime is registered via
7
- `setCacheStoreResolver` from the server entry (request-scoped via ALS)
8
- or the client entry (module-level singleton). If no resolver is registered,
9
- a single fallback store is created lazily so isolated tests still work.
5
+ Resolves the active CacheStore: the registered resolver's store, or a single
6
+ lazily-created fallback when none is registered (so isolated tests work). The
7
+ fallback creator guarantees a value, hence the non-null assertion.
10
8
  */
11
9
  export function activeCacheStore(): CacheStore {
12
- const fromResolver = cacheStoreSlot.resolver?.()
13
- if (fromResolver) {
14
- return fromResolver
15
- }
16
- if (!cacheStoreSlot.fallback) {
17
- cacheStoreSlot.fallback = createCacheStore()
18
- }
19
- return cacheStoreSlot.fallback
10
+ return cacheStoreResolver.get()!
20
11
  }
@@ -1,25 +1,11 @@
1
- import { pageSlot } from './pageSlot.ts'
1
+ import { pageResolver } from './pageResolver.ts'
2
2
  import type { PageSnapshot } from './types/PageSnapshot.ts'
3
3
 
4
4
  /*
5
- Resolves the active page snapshot. The runtime is registered via
6
- `setPageResolver` from the server entry (request-scoped via ALS) or the
7
- client entry (module-level singleton). If no resolver is registered, a single
8
- fallback snapshot is created lazily so isolated tests still work. Mirrors
9
- activeCacheStore.
5
+ Resolves the active page snapshot: the registered resolver's snapshot, or a
6
+ single lazily-created empty snapshot when none is registered (so isolated tests
7
+ work). The fallback creator guarantees a value, hence the non-null assertion.
10
8
  */
11
9
  export function activePage(): PageSnapshot {
12
- const fromResolver = pageSlot.resolver?.()
13
- if (fromResolver) {
14
- return fromResolver
15
- }
16
- if (!pageSlot.fallback) {
17
- pageSlot.fallback = {
18
- route: '',
19
- params: {},
20
- url: new URL('http://localhost/'),
21
- navigating: false,
22
- }
23
- }
24
- return pageSlot.fallback
10
+ return pageResolver.get()!
25
11
  }
@@ -0,0 +1,18 @@
1
+ /*
2
+ Emits one `declare module '<module>' { interface <iface> { … } }` block, the
3
+ module-augmentation shape every codegen writer (routes, rpc, publicAssets,
4
+ testRpc, testSockets) shares. `entries` is `[key, type]` pairs already in final
5
+ order; each becomes an 8-space-indented `"key": type` member, the indentation
6
+ the consumer src tsconfig include expects. Returns the block body only — the
7
+ writeDts envelope adds banner + footer.
8
+ */
9
+ export function augmentModule(module: string, iface: string, entries: [string, string][]): string {
10
+ const members = entries
11
+ .map(([key, type]) => ` ${JSON.stringify(key)}: ${type}`)
12
+ .join('\n')
13
+ return `declare module '${module}' {
14
+ interface ${iface} {
15
+ ${members}
16
+ }
17
+ }`
18
+ }
@@ -1,10 +1,11 @@
1
- import { baseSlot } from './baseSlot.ts'
1
+ import { baseResolver } from './baseResolver.ts'
2
2
 
3
3
  /*
4
- The current mount base path ('' at root). Resolved per side: the server
5
- installs an APP_URL-derived resolver at boot, the client one reading
6
- window.__SSR__.base. url() reads this to prefix rooted internal paths.
4
+ The current mount base path ('' at root). Resolved per side: the server installs
5
+ an APP_URL-derived resolver at boot, the client one reading window.__SSR__.base.
6
+ url() reads this to prefix rooted internal paths. Defaults to '' when no resolver
7
+ or fallback is set.
7
8
  */
8
9
  export function basePath(): string {
9
- return baseSlot.resolver?.() ?? baseSlot.fallback ?? ''
10
+ return baseResolver.get() ?? ''
10
11
  }
@@ -0,0 +1,10 @@
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,14 +1,8 @@
1
+ import { baseResolver } from './baseResolver.ts'
2
+
1
3
  /*
2
- Internal slot the runtime entries register their mount-base resolver into.
3
- The server entry installs an APP_URL-derived resolver at boot; the client
4
- entry installs one reading window.__SSR__.base. `fallback` is a single value
5
- used only when no resolver is registered — lets isolated tests set a base
6
- without spinning up the runtime. Mirrors pageSlot / cacheStoreSlot.
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.
7
7
  */
8
- export const baseSlot: {
9
- resolver: (() => string) | undefined
10
- fallback: string | undefined
11
- } = {
12
- resolver: undefined,
13
- fallback: undefined,
14
- }
8
+ export const baseSlot = baseResolver.slot
@@ -0,0 +1,35 @@
1
+ import type { ContentBodyKind } from './contentBodyKind.ts'
2
+
3
+ /*
4
+ The single body-kind → value mapping shared by the live read (decodeResponse) and
5
+ the synchronous warm read (warmValueFromSnapshot), so neither can disagree about
6
+ which kind yields what — the divergence behind the streaming-guard-in-one-but-not-
7
+ the-other bug.
8
+
9
+ Generic over the extraction (`Value`) so both async and sync callers reuse it:
10
+ the live read passes Promise-returning accessors (response.json()/text()) and
11
+ handles the deferred kinds (streaming → throw, binary → blob); the warm read
12
+ passes sync accessors (JSON.parse(body)/body) and treats every deferred kind as
13
+ no-warm-value, leaving the live async path to throw/blob exactly as a fresh call
14
+ would. The json/text branches — the only kinds that produce a directly-usable
15
+ value on both sides — are decided here, once.
16
+
17
+ Returns the DEFER sentinel for `streaming` and `binary`: kinds whose handling is
18
+ side-specific (the live read throws on streaming and blobs on binary; the warm
19
+ read has no synchronous equivalent for either), so the caller branches on it.
20
+ */
21
+ export const DEFER: unique symbol = Symbol('defer')
22
+
23
+ export function bodyValueForKind<Value>(
24
+ kind: ContentBodyKind,
25
+ json: () => Value,
26
+ text: () => Value,
27
+ ): Value | typeof DEFER {
28
+ if (kind === 'json') {
29
+ return json()
30
+ }
31
+ if (kind === 'text') {
32
+ return text()
33
+ }
34
+ return DEFER
35
+ }
@@ -92,7 +92,7 @@ request-scoped on the server, which keeps per-user data from leaking across
92
92
  requests; on the client there is one tab store either way, so it is a no-op.
93
93
 
94
94
  Reactivity is implicit: the invoker calls `store.subscribe(key)`, which
95
- registers the surrounding $derived / $effect scope. Invalidating the key
95
+ registers the surrounding scope().computed() / scope().effect() scope. Invalidating the key
96
96
  then re-runs that scope, which calls cache() again and gets a fresh entry.
97
97
  Outside a tracking scope subscribe() is a no-op, so cache() works the same
98
98
  in server code and plain client code.
@@ -199,7 +199,7 @@ export function cache<Args, Return>(
199
199
  fetch hands each reader a fresh object; cloning keeps warm reads the same.
200
200
  */
201
201
  if (!isRaw && existing?.value !== undefined) {
202
- return Promise.resolve(structuredClone(existing.value)) as Promise<Return>
202
+ return Promise.resolve(cloneWarmValue(existing.value)) as Promise<Return>
203
203
  }
204
204
  const responsePromise = invokeRemote(
205
205
  store,
@@ -216,6 +216,26 @@ export function cache<Args, Return>(
216
216
  return read
217
217
  }
218
218
 
219
+ /*
220
+ Deep-copies a warm value so each reader gets its own mutable object — the
221
+ no-shared-mutation invariant the warm path turns on (a live fetch hands every
222
+ reader a fresh object; a warm read must match). A warm value only ever comes
223
+ from the json or text body kinds (bodyValueForKind): json yields JSON.parse
224
+ output and text yields a string, so the whole population is JSON-round-trippable
225
+ by construction — no Date/Map/Blob/cycle a structuredClone would be needed for.
226
+ A primitive (string/number/boolean from a text or scalar-json body) is immutable,
227
+ so it is returned as-is with no copy. An object/array goes through a JSON
228
+ round-trip, ~2x faster than structuredClone for this exact shape (measured:
229
+ 773µs→516µs on a 149KB list, 3.4ms→1.7ms on 474KB) while producing the same
230
+ fresh, mutable, isomorphic-with-a-cold-read copy.
231
+ */
232
+ function cloneWarmValue(value: unknown): unknown {
233
+ if (typeof value !== 'object' || value === null) {
234
+ return value
235
+ }
236
+ return JSON.parse(JSON.stringify(value))
237
+ }
238
+
219
239
  /*
220
240
  Normalises the `swr` option to its window, or undefined when off. `true` (or
221
241
  `{}`) is stale-while-revalidate with no window — refetch immediately on every
@@ -1,3 +1,4 @@
1
+ import { bodyValueForKind, DEFER } from './bodyValueForKind.ts'
1
2
  import { contentBodyKind } from './contentBodyKind.ts'
2
3
  import { contentTypeOf } from './contentTypeOf.ts'
3
4
  import type { CacheEntry } from './types/CacheEntry.ts'
@@ -32,31 +33,35 @@ export function cacheEntryFromSnapshot(entry: CacheSnapshotEntry): CacheEntry {
32
33
 
33
34
  /*
34
35
  Synchronously decodes a snapshot body so the warm entry reads without a
35
- microtask hop on first render. A strict subset of `decodeResponse`: it warms
36
- only the `json`/`text` kinds (the same `contentBodyKind` the live read switches
37
- on, so the two cannot disagree about a body), returning a value identical to
38
- what awaiting the Response would yield. Every other kind non-2xx, 204,
39
- streaming, binary yields no warm value and defers to the async path, which
40
- throws HttpError / returns the streaming error / blobs exactly as a live call
41
- would.
36
+ microtask hop on first render. The json/text branches go through the shared
37
+ `bodyValueForKind` the same mapping the live read (`decodeResponse`) uses — so
38
+ the two cannot disagree about how a body becomes a value, returning a value
39
+ identical to what awaiting the Response would yield. Every non-warmable case
40
+ non-2xx, 204 (the status gate below), streaming, binary (the DEFER sentinel)
41
+ yields no warm value and defers to the async path, which throws HttpError /
42
+ returns the streaming error / blobs exactly as a live call would.
42
43
  */
43
44
  function warmValueFromSnapshot(status: number, headers: Headers, body: string): unknown {
45
+ /* Status gate is side-specific: an error/204 has no warm value here, but the live
46
+ read throws HttpError / returns undefined for the same status — so it stays out of
47
+ the shared kind mapping. */
44
48
  if (status === 204 || status < 200 || status >= 300) {
45
49
  return undefined
46
50
  }
47
51
  const kind = contentBodyKind(contentTypeOf(headers))
48
- if (kind === 'json') {
52
+ const value = bodyValueForKind(
53
+ kind,
49
54
  /* The body may still be malformed JSON; fall back to the async path rather than
50
55
  throwing a SyntaxError synchronously during hydration. */
51
- try {
52
- return JSON.parse(body)
53
- } catch {
54
- return undefined
55
- }
56
- }
57
- if (kind === 'text') {
58
- return body
59
- }
56
+ () => {
57
+ try {
58
+ return JSON.parse(body)
59
+ } catch {
60
+ return undefined
61
+ }
62
+ },
63
+ () => body,
64
+ )
60
65
  /* streaming and binary defer to the async path (the live decode throws / blobs). */
61
- return undefined
66
+ return value === DEFER ? undefined : value
62
67
  }