brustjs 0.1.34-alpha → 0.1.36-alpha

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.
@@ -4,7 +4,7 @@
4
4
  import { renderToPipeableStream, renderToString } from 'react-dom/server.node'
5
5
  import { createElement, type ReactNode, type ComponentType } from 'react'
6
6
  import { Writable } from 'node:stream'
7
- import { consumeIslandUsedFlag } from '../islands/island.tsx'
7
+ import { IslandUsedContext, createIslandUsedBox } from '../islands/island.tsx'
8
8
  import { ISLANDS_IMPORTMAP_AND_BOOTSTRAP } from '../islands/importmap.ts'
9
9
  import { injectCssLink } from './inject-css-link.ts'
10
10
  import { getCssHrefs, getCssHrefsForRoute } from '../css.ts'
@@ -15,11 +15,26 @@ import { getDevClientSnippet } from '../dev/inject.ts'
15
15
 
16
16
  export interface RenderBranchStreamingArgs {
17
17
  element: ReactNode
18
+ /** The render's SAB sub-view (whole buffer at slots=1). All chunk encodes
19
+ * write at offset 0 of this view → automatically the slot's base. */
18
20
  view: Uint8Array
21
+ /** The render slot index, passed to every napi.renderChunk* call so Rust
22
+ * reads/writes the matching SAB sub-region. Default 0 (single-slot path). */
23
+ slot?: number
19
24
  workerId: bigint
20
25
  napi: {
21
- renderChunk: (workerId: bigint, len: number, sabBytes: Uint8Array) => Promise<void>
22
- renderChunkFinal: (workerId: bigint, len: number, sabBytes: Uint8Array) => Promise<void>
26
+ renderChunk: (
27
+ workerId: bigint,
28
+ slot: number,
29
+ len: number,
30
+ sabBytes: Uint8Array,
31
+ ) => Promise<void>
32
+ renderChunkFinal: (
33
+ workerId: bigint,
34
+ slot: number,
35
+ len: number,
36
+ sabBytes: Uint8Array,
37
+ ) => Promise<void>
23
38
  }
24
39
  errorBoundary: ComponentType<{ error: Error }>
25
40
  /** Status for the successful (non-error) render. Default 200. Used by
@@ -97,15 +112,18 @@ function concatBuffers(parts: Uint8Array[], withBootstrap: boolean): Uint8Array
97
112
 
98
113
  export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<void> {
99
114
  const { element, view, workerId, napi, errorBoundary } = args
115
+ const slot = args.slot ?? 0
100
116
  const successStatus = args.status ?? 200
101
117
  const extraHeaders = args.headers ?? {}
102
118
 
103
- // Reset the islands flag at the start of every render the streaming path
104
- // (which doesn't read the flag at the end) would otherwise leak its setting
105
- // to the next render. consumeIslandUsedFlag() reads-and-resets so calling
106
- // here is safe; the actual read for the buffering path happens at _final
107
- // time and sees only flips made during THIS render's React work.
108
- consumeIslandUsedFlag()
119
+ // Request-scoped islands signal: a fresh box per render, provided to every
120
+ // <Island> through IslandUsedContext. The buffering path reads `box.used` at
121
+ // _final to decide whether to prepend the importmap + bootstrap. Per-render
122
+ // (not a module flag) so concurrent renders in one isolate (renderSlots>1)
123
+ // never cross-contaminate React restores each render's context across
124
+ // Suspense resumption. No start-of-render reset needed: the box starts false.
125
+ const islandUsedBox = createIslandUsedBox()
126
+ const renderTree = createElement(IslandUsedContext.Provider, { value: islandUsedBox }, element)
109
127
 
110
128
  return new Promise<void>((resolve, reject) => {
111
129
  let finalSent = false
@@ -113,7 +131,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
113
131
  if (finalSent) return
114
132
  finalSent = true
115
133
  try {
116
- await napi.renderChunk(workerId, 0, view)
134
+ await napi.renderChunk(workerId, slot, 0, view)
117
135
  resolve()
118
136
  } catch (e) {
119
137
  reject(e)
@@ -139,7 +157,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
139
157
  // Wait for the header chunk to be flushed before sending body chunks.
140
158
  if (headerSent) await headerSent
141
159
  const len = encodeBodyChunk(view, chunk)
142
- await napi.renderChunk(workerId, len, view)
160
+ await napi.renderChunk(workerId, slot, len, view)
143
161
  }
144
162
  cb()
145
163
  } catch (e) {
@@ -149,7 +167,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
149
167
  async final(cb: (e?: Error | null) => void) {
150
168
  try {
151
169
  if (mode === 'buffering') {
152
- const islandsUsed = consumeIslandUsedFlag()
170
+ const islandsUsed = islandUsedBox.used
153
171
  let body = concatBuffers(buffer, islandsUsed)
154
172
  const perRouteHrefs = args.routePath ? getCssHrefsForRoute(args.routePath) : []
155
173
  body = injectCssLink(body, [...getCssHrefs(), ...perRouteHrefs])
@@ -162,7 +180,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
162
180
  headers: extraHeaders,
163
181
  })
164
182
  const len = encodeFirstChunk(view, meta, body)
165
- await napi.renderChunkFinal(workerId, len, view)
183
+ await napi.renderChunkFinal(workerId, slot, len, view)
166
184
  finalSent = true
167
185
  resolve()
168
186
  mode = 'done'
@@ -184,7 +202,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
184
202
  let allReadyFired = false
185
203
  let stream: ReturnType<typeof renderToPipeableStream>
186
204
  try {
187
- stream = renderToPipeableStream(element, {
205
+ stream = renderToPipeableStream(renderTree, {
188
206
  onShellReady() {
189
207
  // React fires onAllReady synchronously AFTER onShellReady in the
190
208
  // same microtask queue flush when there is no pending Suspense.
@@ -247,7 +265,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
247
265
  ;(async () => {
248
266
  try {
249
267
  const len = encodeFirstChunk(view, meta, flushed)
250
- await napi.renderChunk(workerId, len, view)
268
+ await napi.renderChunk(workerId, slot, len, view)
251
269
  resolveHeader()
252
270
  } catch (e) {
253
271
  rejectHeader(e)
@@ -267,7 +285,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
267
285
  ;(async () => {
268
286
  try {
269
287
  const len = encodeFirstChunk(view, meta, encoder.encode(html))
270
- await napi.renderChunkFinal(workerId, len, view)
288
+ await napi.renderChunkFinal(workerId, slot, len, view)
271
289
  finalSent = true
272
290
  resolve()
273
291
  } catch (e) {
@@ -285,7 +303,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
285
303
  ;(async () => {
286
304
  try {
287
305
  const len = encodeFirstChunk(view, meta, encoder.encode('Internal Server Error'))
288
- await napi.renderChunkFinal(workerId, len, view)
306
+ await napi.renderChunkFinal(workerId, slot, len, view)
289
307
  finalSent = true
290
308
  resolve()
291
309
  } catch (e) {
@@ -307,7 +325,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
307
325
  ;(async () => {
308
326
  try {
309
327
  const len = encodeFirstChunk(view, meta, encoder.encode('Internal Server Error'))
310
- await napi.renderChunkFinal(workerId, len, view)
328
+ await napi.renderChunkFinal(workerId, slot, len, view)
311
329
  finalSent = true
312
330
  resolve()
313
331
  } catch (ee) {
package/runtime/routes.ts CHANGED
@@ -612,15 +612,19 @@ export interface MakeRendererOptions {
612
612
  actions?: EndpointDef[]
613
613
  /** MCP server instance — built once per worker at module top-level. */
614
614
  mcp?: import('./mcp/server.ts').McpServer
615
+ /** Render slots per worker. The `view` SAB is partitioned into this many
616
+ * disjoint sub-views; each render is dispatched with a `slot` index and
617
+ * operates on `view.subarray(slot*sub, slot*sub+sub)`. Default 1 (the whole
618
+ * view → byte-identical to the pre-multi-slot path). */
619
+ slots?: number
615
620
  }
616
621
 
617
622
  export function makeRenderer(
618
623
  routes: FlatRoute[],
619
624
  view: Uint8Array,
620
625
  opts: MakeRendererOptions = {},
621
- ): (envelopeJsonOrLen: number | string) => Promise<number> {
626
+ ): (envelopeJson: string, slot?: number) => Promise<number> {
622
627
  const encoder = new TextEncoder()
623
- const decoder = new TextDecoder()
624
628
  const byRouteId = new Map<number, FlatRoute>()
625
629
  routes.forEach((r, i) => {
626
630
  byRouteId.set(i, r)
@@ -630,28 +634,41 @@ export function makeRenderer(
630
634
  byActionId.set(String(i), e)
631
635
  })
632
636
 
637
+ // Slot count: the SAB is partitioned into `slots` disjoint sub-views. At
638
+ // slots=1 the sub-view is the whole buffer (byte-identical to before).
639
+ const slots = Math.max(1, opts.slots ?? 1)
640
+ const sub = Math.floor(view.length / slots)
641
+
633
642
  // napi shim for the chunk channel. The sabBytes arg is ignored by the
634
- // native fn (Rust reads from the pre-registered BufPtr) the call sites
635
- // still pass it so renderBranchStreaming can be unit-tested against a
636
- // mock that captures the bytes.
643
+ // native fn (Rust reads from the pre-registered BufPtr, offset by slot)
644
+ // the call sites still pass it so renderBranchStreaming can be unit-tested
645
+ // against a mock that captures the bytes. The `slot` arg selects the SAB
646
+ // sub-region Rust reads from.
637
647
  const napi = {
638
- renderChunk: async (workerId: bigint, len: number, _sabBytes: Uint8Array): Promise<void> => {
639
- await (native as any).napiRenderChunk(Number(workerId), len)
648
+ renderChunk: async (
649
+ workerId: bigint,
650
+ slot: number,
651
+ len: number,
652
+ _sabBytes: Uint8Array,
653
+ ): Promise<void> => {
654
+ await (native as any).napiRenderChunk(Number(workerId), slot, len)
640
655
  },
641
656
  renderChunkFinal: async (
642
657
  workerId: bigint,
658
+ slot: number,
643
659
  len: number,
644
660
  _sabBytes: Uint8Array,
645
661
  ): Promise<void> => {
646
- await (native as any).napiRenderChunkFinal(Number(workerId), len)
662
+ await (native as any).napiRenderChunkFinal(Number(workerId), slot, len)
647
663
  },
648
664
  }
649
665
 
650
- return async (envelopeJsonOrLen: number | string): Promise<number> => {
651
- const envelopeJson =
652
- typeof envelopeJsonOrLen === 'number'
653
- ? decoder.decode(view.subarray(0, envelopeJsonOrLen))
654
- : envelopeJsonOrLen
666
+ return async (envelopeJson: string, slot = 0): Promise<number> => {
667
+ // The disjoint SAB sub-view this render owns. At slots=1, slotView === the
668
+ // whole view (offset 0, full length) so the K=1 path is byte-identical.
669
+ // The request always arrives as an INLINE JSON string (SAB-request is closed
670
+ // — see the dispatch module doc); the SAB is used only for the RESPONSE.
671
+ const slotView = slots === 1 ? view : view.subarray(slot * sub, slot * sub + sub)
655
672
  const call = JSON.parse(envelopeJson) as RouteCall
656
673
  const wid = opts.getWorkerId?.() ?? 0
657
674
  const workerId = BigInt(wid)
@@ -660,11 +677,18 @@ export function makeRenderer(
660
677
  const flat = byRouteId.get(call.route_id)
661
678
  if (!flat) {
662
679
  console.error(`[brust] unknown route_id=${call.route_id} for path=${call.path}`)
663
- await emitSingleChunkResponse(view, napi, workerId, encoder, {
664
- status: 404,
665
- contentType: 'text/plain; charset=utf-8',
666
- body: 'not found',
667
- })
680
+ await emitSingleChunkResponse(
681
+ slotView,
682
+ napi,
683
+ workerId,
684
+ encoder,
685
+ {
686
+ status: 404,
687
+ contentType: 'text/plain; charset=utf-8',
688
+ body: 'not found',
689
+ },
690
+ slot,
691
+ )
668
692
  return 0
669
693
  }
670
694
  // Inject the permanently-unaborted signal — non-SSE routes don't
@@ -695,7 +719,7 @@ export function makeRenderer(
695
719
  // FAST LANE: single-chunk error. Works for both React (big dispatch
696
720
  // reads the SAB via its fast-lane arm) and native routes (which take
697
721
  // the channel-free dispatch_single_chunk).
698
- return packSingleChunkResponse(view, encoder, {
722
+ return packSingleChunkResponse(slotView, encoder, {
699
723
  status: 500,
700
724
  contentType: 'text/html; charset=utf-8',
701
725
  body: 'internal error',
@@ -705,7 +729,7 @@ export function makeRenderer(
705
729
  if (verdict._brustStream !== STREAM_MARKER) {
706
730
  // Middleware short-circuited with a concrete response. FAST LANE —
707
731
  // single-chunk; same dual-dispatch safety as the error path above.
708
- return packSingleChunkResponse(view, encoder, {
732
+ return packSingleChunkResponse(slotView, encoder, {
709
733
  status: verdict.status,
710
734
  contentType: verdict.contentType ?? 'text/html; charset=utf-8',
711
735
  body: verdict.body,
@@ -750,7 +774,7 @@ export function makeRenderer(
750
774
  console.error(`[brust] loader failed for native route ${flat.fullPath}:`, err)
751
775
  // FAST LANE: native routes take dispatch_single_chunk (no chunk
752
776
  // channel), so every native fallback MUST pack + return a length.
753
- return packSingleChunkResponse(view, encoder, {
777
+ return packSingleChunkResponse(slotView, encoder, {
754
778
  status: 500,
755
779
  contentType: 'text/html; charset=utf-8',
756
780
  body: 'internal error',
@@ -762,7 +786,7 @@ export function makeRenderer(
762
786
  const verdict = chainResult.verdict
763
787
  if (!verdict.render) {
764
788
  // redirect — no template render; fast-lane packed response with Location.
765
- return packSingleChunkResponse(view, encoder, {
789
+ return packSingleChunkResponse(slotView, encoder, {
766
790
  status: verdict.status,
767
791
  contentType: 'text/html; charset=utf-8',
768
792
  body: '',
@@ -828,24 +852,25 @@ export function makeRenderer(
828
852
  const finalBytes = encoder.encode(JSON.stringify(ctx))
829
853
  // The original size check guarded the pre-island bytes; the merged
830
854
  // context (with island props + component html) can be larger. Re-check on finalBytes.
831
- if (finalBytes.length > view.length) {
832
- return packSingleChunkResponse(view, encoder, {
855
+ if (finalBytes.length > slotView.length) {
856
+ return packSingleChunkResponse(slotView, encoder, {
833
857
  status: 413,
834
858
  contentType: 'text/plain; charset=utf-8',
835
859
  body: 'loader data too large for SAB',
836
860
  })
837
861
  }
838
- view.set(finalBytes, 0)
862
+ slotView.set(finalBytes, 0)
839
863
  try {
840
864
  return (native as any).napiRenderJinja(
841
865
  Number(workerId),
866
+ slot,
842
867
  finalBytes.length,
843
868
  flat.nativeTemplate,
844
869
  renderStatus,
845
870
  )
846
871
  } catch (err) {
847
872
  console.error(`[brust] napiRenderJinja failed for "${flat.nativeTemplate}":`, err)
848
- return packSingleChunkResponse(view, encoder, {
873
+ return packSingleChunkResponse(slotView, encoder, {
849
874
  status: 500,
850
875
  contentType: 'text/html; charset=utf-8',
851
876
  body: 'internal error',
@@ -853,14 +878,14 @@ export function makeRenderer(
853
878
  }
854
879
  }
855
880
  const dataBytes = encoder.encode(json)
856
- if (dataBytes.length > view.length) {
857
- return packSingleChunkResponse(view, encoder, {
881
+ if (dataBytes.length > slotView.length) {
882
+ return packSingleChunkResponse(slotView, encoder, {
858
883
  status: 413,
859
884
  contentType: 'text/plain; charset=utf-8',
860
885
  body: 'loader data too large for SAB',
861
886
  })
862
887
  }
863
- view.set(dataBytes, 0)
888
+ slotView.set(dataBytes, 0)
864
889
  try {
865
890
  // FAST LANE: napiRenderJinja is a SYNC napi call — renders Rust-side,
866
891
  // writes the framed response into the SAB, and returns its length
@@ -868,13 +893,14 @@ export function makeRenderer(
868
893
  // fast-lane arm reads the SAB directly (no chunk channel).
869
894
  return (native as any).napiRenderJinja(
870
895
  Number(workerId),
896
+ slot,
871
897
  dataBytes.length,
872
898
  flat.nativeTemplate,
873
899
  renderStatus,
874
900
  )
875
901
  } catch (err) {
876
902
  console.error(`[brust] napiRenderJinja failed for "${flat.nativeTemplate}":`, err)
877
- return packSingleChunkResponse(view, encoder, {
903
+ return packSingleChunkResponse(slotView, encoder, {
878
904
  status: 500,
879
905
  contentType: 'text/html; charset=utf-8',
880
906
  body: 'internal error',
@@ -900,16 +926,24 @@ export function makeRenderer(
900
926
  // bind throw. Shape matches the legacy "internal error" path so
901
927
  // existing integration tests stay green.
902
928
  console.error(`[brust] render setup failed:`, err)
903
- return await emitSingleChunkResponse(view, napi, workerId, encoder, {
904
- status: 500,
905
- contentType: 'text/html; charset=utf-8',
906
- body: 'internal error',
907
- })
929
+ return await emitSingleChunkResponse(
930
+ slotView,
931
+ napi,
932
+ workerId,
933
+ encoder,
934
+ {
935
+ status: 500,
936
+ contentType: 'text/html; charset=utf-8',
937
+ body: 'internal error',
938
+ },
939
+ slot,
940
+ )
908
941
  }
909
942
  const storeSnapshot = collectSnapshot()
910
943
  await renderBranchStreaming({
911
944
  element,
912
- view,
945
+ view: slotView,
946
+ slot,
913
947
  workerId,
914
948
  napi,
915
949
  errorBoundary,
@@ -923,22 +957,22 @@ export function makeRenderer(
923
957
  })
924
958
  }
925
959
  if (call.kind === 'navigation') {
926
- await navigationBranch(call, byRouteId, view, encoder, opts.getWorkerId)
960
+ await navigationBranch(call, byRouteId, slotView, encoder, opts.getWorkerId, slot)
927
961
  return 0
928
962
  }
929
963
  if (call.kind === 'action') {
930
964
  // FAST LANE: pack the framed response into the SAB and return its length.
931
965
  // Rust reads it directly after the Promise settles — no chunk channel.
932
966
  const resp = await dispatchAction(call, byActionId)
933
- return packSingleChunkResponse(view, encoder, resp)
967
+ return packSingleChunkResponse(slotView, encoder, resp)
934
968
  }
935
969
  if (call.kind === 'mcp') {
936
970
  const resp = await mcpBranchToResponse(call, opts.mcp)
937
- return await emitSingleChunkResponse(view, napi, workerId, encoder, resp)
971
+ return await emitSingleChunkResponse(slotView, napi, workerId, encoder, resp, slot)
938
972
  }
939
973
  if (call.kind === 'sse') {
940
974
  try {
941
- await sseBranch(call, view, encoder, routes)
975
+ await sseBranch(call, slotView, encoder, routes)
942
976
  } catch (err) {
943
977
  // Setup-time errors only (BigInt coerce, dynamic import resolve,
944
978
  // napi shim build). Once handleSseStream has started streaming,
@@ -951,7 +985,7 @@ export function makeRenderer(
951
985
  }
952
986
  if (call.kind === 'ws') {
953
987
  try {
954
- await wsBranch(call, view, encoder, routes)
988
+ await wsBranch(call, slotView, encoder, routes)
955
989
  } catch (err) {
956
990
  // Setup-time errors only — same reasoning as sseBranch above.
957
991
  console.error('[brust] wsBranch uncaught:', err)
@@ -963,11 +997,18 @@ export function makeRenderer(
963
997
  // Unknown kind — log and 500. Shouldn't happen unless Rust ships
964
998
  // something out of band.
965
999
  console.error(`[brust] unknown envelope kind in worker:`, (call as { kind?: string }).kind)
966
- return await emitSingleChunkResponse(view, napi, workerId, encoder, {
967
- status: 500,
968
- contentType: 'text/plain; charset=utf-8',
969
- body: 'invalid envelope kind',
970
- })
1000
+ return await emitSingleChunkResponse(
1001
+ slotView,
1002
+ napi,
1003
+ workerId,
1004
+ encoder,
1005
+ {
1006
+ status: 500,
1007
+ contentType: 'text/plain; charset=utf-8',
1008
+ body: 'invalid envelope kind',
1009
+ },
1010
+ slot,
1011
+ )
971
1012
  }
972
1013
  }
973
1014
 
@@ -1013,24 +1054,37 @@ async function navigationBranch(
1013
1054
  view: Uint8Array,
1014
1055
  encoder: TextEncoder,
1015
1056
  getWorkerId: (() => number | null) | undefined,
1057
+ slot = 0,
1016
1058
  ): Promise<void> {
1017
1059
  const workerId = BigInt(getWorkerId?.() ?? 0)
1018
1060
  const napi = {
1019
- renderChunk: async (wid: bigint, len: number, _view: Uint8Array): Promise<void> => {
1020
- await (native as any).napiRenderChunk(Number(wid), len)
1061
+ renderChunk: async (wid: bigint, s: number, len: number, _view: Uint8Array): Promise<void> => {
1062
+ await (native as any).napiRenderChunk(Number(wid), s, len)
1021
1063
  },
1022
- renderChunkFinal: async (wid: bigint, len: number, _view: Uint8Array): Promise<void> => {
1023
- await (native as any).napiRenderChunkFinal(Number(wid), len)
1064
+ renderChunkFinal: async (
1065
+ wid: bigint,
1066
+ s: number,
1067
+ len: number,
1068
+ _view: Uint8Array,
1069
+ ): Promise<void> => {
1070
+ await (native as any).napiRenderChunkFinal(Number(wid), s, len)
1024
1071
  },
1025
1072
  }
1026
1073
 
1027
1074
  const flat = byRouteId.get(call.route_id)
1028
1075
  if (!flat) {
1029
- await emitSingleChunkResponse(view, napi, workerId, encoder, {
1030
- status: 404,
1031
- contentType: 'application/json; charset=utf-8',
1032
- body: '{"error":"not found"}',
1033
- })
1076
+ await emitSingleChunkResponse(
1077
+ view,
1078
+ napi,
1079
+ workerId,
1080
+ encoder,
1081
+ {
1082
+ status: 404,
1083
+ contentType: 'application/json; charset=utf-8',
1084
+ body: '{"error":"not found"}',
1085
+ },
1086
+ slot,
1087
+ )
1034
1088
  return
1035
1089
  }
1036
1090
 
@@ -1059,11 +1113,18 @@ async function navigationBranch(
1059
1113
  navVerdict = (await navChain()) as NavMarkerResponse
1060
1114
  } catch (err) {
1061
1115
  console.error('[brust] navigation middleware threw:', err)
1062
- await emitSingleChunkResponse(view, napi, workerId, encoder, {
1063
- status: 500,
1064
- contentType: 'application/json; charset=utf-8',
1065
- body: '{"error":"middleware threw"}',
1066
- })
1116
+ await emitSingleChunkResponse(
1117
+ view,
1118
+ napi,
1119
+ workerId,
1120
+ encoder,
1121
+ {
1122
+ status: 500,
1123
+ contentType: 'application/json; charset=utf-8',
1124
+ body: '{"error":"middleware threw"}',
1125
+ },
1126
+ slot,
1127
+ )
1067
1128
  return
1068
1129
  }
1069
1130
 
@@ -1071,12 +1132,19 @@ async function navigationBranch(
1071
1132
  // Middleware short-circuited — emit the verdict. Client's non-2xx check
1072
1133
  // triggers the full-reload fallback so the user hits the real route and
1073
1134
  // sees the middleware's challenge page.
1074
- await emitSingleChunkResponse(view, napi, workerId, encoder, {
1075
- status: navVerdict.status,
1076
- contentType: navVerdict.contentType ?? 'application/json; charset=utf-8',
1077
- body: navVerdict.body,
1078
- headers: navVerdict.headers,
1079
- })
1135
+ await emitSingleChunkResponse(
1136
+ view,
1137
+ napi,
1138
+ workerId,
1139
+ encoder,
1140
+ {
1141
+ status: navVerdict.status,
1142
+ contentType: navVerdict.contentType ?? 'application/json; charset=utf-8',
1143
+ body: navVerdict.body,
1144
+ headers: navVerdict.headers,
1145
+ },
1146
+ slot,
1147
+ )
1080
1148
  return
1081
1149
  }
1082
1150
 
@@ -1100,7 +1168,7 @@ async function navigationBranch(
1100
1168
  // full-document native render path).
1101
1169
  let navHeaders: Record<string, string> | undefined
1102
1170
  if (flat.nativeTemplate !== undefined) {
1103
- fullHtml = await renderNativeRouteToHtml(call, flat, view, encoder, workerId)
1171
+ fullHtml = await renderNativeRouteToHtml(call, flat, view, encoder, workerId, slot)
1104
1172
  } else {
1105
1173
  // Wrap loader run (inside buildRenderElement) + render in one store scope so
1106
1174
  // store reads resolve the per-request instance; collect after render.
@@ -1136,19 +1204,33 @@ async function navigationBranch(
1136
1204
  const title = titleMatch ? titleMatch[1].replace(/<!--.*?-->/g, '').trim() : ''
1137
1205
 
1138
1206
  const body = JSON.stringify({ html: innerHtml, title, store })
1139
- await emitSingleChunkResponse(view, napi, workerId, encoder, {
1140
- status: 200,
1141
- contentType: 'application/json; charset=utf-8',
1142
- body,
1143
- headers: navHeaders,
1144
- })
1207
+ await emitSingleChunkResponse(
1208
+ view,
1209
+ napi,
1210
+ workerId,
1211
+ encoder,
1212
+ {
1213
+ status: 200,
1214
+ contentType: 'application/json; charset=utf-8',
1215
+ body,
1216
+ headers: navHeaders,
1217
+ },
1218
+ slot,
1219
+ )
1145
1220
  } catch (err) {
1146
1221
  console.error('[brust] navigation render failed:', err)
1147
- await emitSingleChunkResponse(view, napi, workerId, encoder, {
1148
- status: 500,
1149
- contentType: 'application/json; charset=utf-8',
1150
- body: '{"error":"render failed"}',
1151
- })
1222
+ await emitSingleChunkResponse(
1223
+ view,
1224
+ napi,
1225
+ workerId,
1226
+ encoder,
1227
+ {
1228
+ status: 500,
1229
+ contentType: 'application/json; charset=utf-8',
1230
+ body: '{"error":"render failed"}',
1231
+ },
1232
+ slot,
1233
+ )
1152
1234
  }
1153
1235
  }
1154
1236
 
@@ -1170,6 +1252,7 @@ async function renderNativeRouteToHtml(
1170
1252
  view: Uint8Array,
1171
1253
  encoder: TextEncoder,
1172
1254
  workerId: bigint,
1255
+ slot = 0,
1173
1256
  ): Promise<string> {
1174
1257
  const templateName = flat.nativeTemplate as string
1175
1258
 
@@ -1211,6 +1294,7 @@ async function renderNativeRouteToHtml(
1211
1294
 
1212
1295
  const len = (native as any).napiRenderJinja(
1213
1296
  Number(workerId),
1297
+ slot,
1214
1298
  bytes.length,
1215
1299
  templateName,
1216
1300
  ) as number
@@ -1331,8 +1415,8 @@ function packSingleChunkResponse(
1331
1415
  async function emitSingleChunkResponse(
1332
1416
  view: Uint8Array,
1333
1417
  napi: {
1334
- renderChunk: (w: bigint, len: number, view: Uint8Array) => Promise<void>
1335
- renderChunkFinal: (w: bigint, len: number, view: Uint8Array) => Promise<void>
1418
+ renderChunk: (w: bigint, slot: number, len: number, view: Uint8Array) => Promise<void>
1419
+ renderChunkFinal: (w: bigint, slot: number, len: number, view: Uint8Array) => Promise<void>
1336
1420
  },
1337
1421
  workerId: bigint,
1338
1422
  encoder: TextEncoder,
@@ -1342,6 +1426,7 @@ async function emitSingleChunkResponse(
1342
1426
  body: string | Uint8Array
1343
1427
  headers?: Record<string, string>
1344
1428
  },
1429
+ slot = 0,
1345
1430
  ): Promise<number> {
1346
1431
  const bodyBytes = typeof resp.body === 'string' ? encoder.encode(resp.body) : resp.body
1347
1432
  const meta = JSON.stringify({
@@ -1372,14 +1457,14 @@ async function emitSingleChunkResponse(
1372
1457
  view[1] = errMetaBytes.length & 0xff
1373
1458
  view.set(errMetaBytes, 2)
1374
1459
  view.set(errBody, 2 + errMetaBytes.length)
1375
- await napi.renderChunkFinal(workerId, errTotal, view)
1460
+ await napi.renderChunkFinal(workerId, slot, errTotal, view)
1376
1461
  return 0
1377
1462
  }
1378
1463
  view[0] = (metaBytes.length >> 8) & 0xff
1379
1464
  view[1] = metaBytes.length & 0xff
1380
1465
  view.set(metaBytes, 2)
1381
1466
  view.set(bodyBytes, 2 + metaBytes.length)
1382
- await napi.renderChunkFinal(workerId, total, view)
1467
+ await napi.renderChunkFinal(workerId, slot, total, view)
1383
1468
  return 0
1384
1469
  }
1385
1470