brustjs 0.1.35-alpha → 0.1.37-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.
- package/README.md +18 -13
- package/example/pokedex/app.css +11 -0
- package/example/pokedex/components/AppLayout.tsx +3 -1
- package/example/pokedex/components/TeamBuilder.module.css +47 -0
- package/example/pokedex/components/TeamBuilder.tsx +11 -2
- package/example/pokedex/components/ThemeToggle.tsx +7 -5
- package/example/pokedex/lib/loaders.ts +26 -11
- package/example/pokedex/lib/pokeapi.ts +45 -15
- package/example/pokedex/lib/types.ts +1 -0
- package/example/pokedex/pages/TypeChart.tsx +17 -13
- package/package.json +7 -7
- package/runtime/cli/build.ts +65 -35
- package/runtime/cli/templates/minimal/package.json.tmpl +1 -0
- package/runtime/cli/templates.ts +1 -0
- package/runtime/css/component-build.ts +13 -5
- package/runtime/css/route-deps.ts +26 -11
- package/runtime/css-modules.d.ts +17 -0
- package/runtime/index.d.ts +45 -4
- package/runtime/index.js +53 -52
- package/runtime/index.ts +89 -23
- package/runtime/islands/bootstrap.ts +16 -1
- package/runtime/islands/build.ts +46 -5
- package/runtime/islands/island.tsx +26 -11
- package/runtime/render/stream.ts +36 -18
- package/runtime/routes.ts +183 -82
package/runtime/routes.ts
CHANGED
|
@@ -12,6 +12,7 @@ import * as native from './index.js'
|
|
|
12
12
|
import { renderBranchStreaming } from './render/stream.ts'
|
|
13
13
|
import { runInStoreContext, collectSnapshot } from './store/server-context.ts'
|
|
14
14
|
import { buildStoreScripts } from './render/inject-store.ts'
|
|
15
|
+
import { getCssHrefsForRoute } from './css.ts'
|
|
15
16
|
import { runInRequestCache } from './loader-cache.ts'
|
|
16
17
|
import { runInRequestScope, __scope } from './request-context.ts'
|
|
17
18
|
import {
|
|
@@ -612,15 +613,19 @@ export interface MakeRendererOptions {
|
|
|
612
613
|
actions?: EndpointDef[]
|
|
613
614
|
/** MCP server instance — built once per worker at module top-level. */
|
|
614
615
|
mcp?: import('./mcp/server.ts').McpServer
|
|
616
|
+
/** Render slots per worker. The `view` SAB is partitioned into this many
|
|
617
|
+
* disjoint sub-views; each render is dispatched with a `slot` index and
|
|
618
|
+
* operates on `view.subarray(slot*sub, slot*sub+sub)`. Default 1 (the whole
|
|
619
|
+
* view → byte-identical to the pre-multi-slot path). */
|
|
620
|
+
slots?: number
|
|
615
621
|
}
|
|
616
622
|
|
|
617
623
|
export function makeRenderer(
|
|
618
624
|
routes: FlatRoute[],
|
|
619
625
|
view: Uint8Array,
|
|
620
626
|
opts: MakeRendererOptions = {},
|
|
621
|
-
): (
|
|
627
|
+
): (envelopeJson: string, slot?: number) => Promise<number> {
|
|
622
628
|
const encoder = new TextEncoder()
|
|
623
|
-
const decoder = new TextDecoder()
|
|
624
629
|
const byRouteId = new Map<number, FlatRoute>()
|
|
625
630
|
routes.forEach((r, i) => {
|
|
626
631
|
byRouteId.set(i, r)
|
|
@@ -630,28 +635,41 @@ export function makeRenderer(
|
|
|
630
635
|
byActionId.set(String(i), e)
|
|
631
636
|
})
|
|
632
637
|
|
|
638
|
+
// Slot count: the SAB is partitioned into `slots` disjoint sub-views. At
|
|
639
|
+
// slots=1 the sub-view is the whole buffer (byte-identical to before).
|
|
640
|
+
const slots = Math.max(1, opts.slots ?? 1)
|
|
641
|
+
const sub = Math.floor(view.length / slots)
|
|
642
|
+
|
|
633
643
|
// napi shim for the chunk channel. The sabBytes arg is ignored by the
|
|
634
|
-
// native fn (Rust reads from the pre-registered BufPtr
|
|
635
|
-
// still pass it so renderBranchStreaming can be unit-tested
|
|
636
|
-
// mock that captures the bytes.
|
|
644
|
+
// native fn (Rust reads from the pre-registered BufPtr, offset by slot) —
|
|
645
|
+
// the call sites still pass it so renderBranchStreaming can be unit-tested
|
|
646
|
+
// against a mock that captures the bytes. The `slot` arg selects the SAB
|
|
647
|
+
// sub-region Rust reads from.
|
|
637
648
|
const napi = {
|
|
638
|
-
renderChunk: async (
|
|
639
|
-
|
|
649
|
+
renderChunk: async (
|
|
650
|
+
workerId: bigint,
|
|
651
|
+
slot: number,
|
|
652
|
+
len: number,
|
|
653
|
+
_sabBytes: Uint8Array,
|
|
654
|
+
): Promise<void> => {
|
|
655
|
+
await (native as any).napiRenderChunk(Number(workerId), slot, len)
|
|
640
656
|
},
|
|
641
657
|
renderChunkFinal: async (
|
|
642
658
|
workerId: bigint,
|
|
659
|
+
slot: number,
|
|
643
660
|
len: number,
|
|
644
661
|
_sabBytes: Uint8Array,
|
|
645
662
|
): Promise<void> => {
|
|
646
|
-
await (native as any).napiRenderChunkFinal(Number(workerId), len)
|
|
663
|
+
await (native as any).napiRenderChunkFinal(Number(workerId), slot, len)
|
|
647
664
|
},
|
|
648
665
|
}
|
|
649
666
|
|
|
650
|
-
return async (
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
667
|
+
return async (envelopeJson: string, slot = 0): Promise<number> => {
|
|
668
|
+
// The disjoint SAB sub-view this render owns. At slots=1, slotView === the
|
|
669
|
+
// whole view (offset 0, full length) so the K=1 path is byte-identical.
|
|
670
|
+
// The request always arrives as an INLINE JSON string (SAB-request is closed
|
|
671
|
+
// — see the dispatch module doc); the SAB is used only for the RESPONSE.
|
|
672
|
+
const slotView = slots === 1 ? view : view.subarray(slot * sub, slot * sub + sub)
|
|
655
673
|
const call = JSON.parse(envelopeJson) as RouteCall
|
|
656
674
|
const wid = opts.getWorkerId?.() ?? 0
|
|
657
675
|
const workerId = BigInt(wid)
|
|
@@ -660,11 +678,18 @@ export function makeRenderer(
|
|
|
660
678
|
const flat = byRouteId.get(call.route_id)
|
|
661
679
|
if (!flat) {
|
|
662
680
|
console.error(`[brust] unknown route_id=${call.route_id} for path=${call.path}`)
|
|
663
|
-
await emitSingleChunkResponse(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
681
|
+
await emitSingleChunkResponse(
|
|
682
|
+
slotView,
|
|
683
|
+
napi,
|
|
684
|
+
workerId,
|
|
685
|
+
encoder,
|
|
686
|
+
{
|
|
687
|
+
status: 404,
|
|
688
|
+
contentType: 'text/plain; charset=utf-8',
|
|
689
|
+
body: 'not found',
|
|
690
|
+
},
|
|
691
|
+
slot,
|
|
692
|
+
)
|
|
668
693
|
return 0
|
|
669
694
|
}
|
|
670
695
|
// Inject the permanently-unaborted signal — non-SSE routes don't
|
|
@@ -695,7 +720,7 @@ export function makeRenderer(
|
|
|
695
720
|
// FAST LANE: single-chunk error. Works for both React (big dispatch
|
|
696
721
|
// reads the SAB via its fast-lane arm) and native routes (which take
|
|
697
722
|
// the channel-free dispatch_single_chunk).
|
|
698
|
-
return packSingleChunkResponse(
|
|
723
|
+
return packSingleChunkResponse(slotView, encoder, {
|
|
699
724
|
status: 500,
|
|
700
725
|
contentType: 'text/html; charset=utf-8',
|
|
701
726
|
body: 'internal error',
|
|
@@ -705,7 +730,7 @@ export function makeRenderer(
|
|
|
705
730
|
if (verdict._brustStream !== STREAM_MARKER) {
|
|
706
731
|
// Middleware short-circuited with a concrete response. FAST LANE —
|
|
707
732
|
// single-chunk; same dual-dispatch safety as the error path above.
|
|
708
|
-
return packSingleChunkResponse(
|
|
733
|
+
return packSingleChunkResponse(slotView, encoder, {
|
|
709
734
|
status: verdict.status,
|
|
710
735
|
contentType: verdict.contentType ?? 'text/html; charset=utf-8',
|
|
711
736
|
body: verdict.body,
|
|
@@ -750,7 +775,7 @@ export function makeRenderer(
|
|
|
750
775
|
console.error(`[brust] loader failed for native route ${flat.fullPath}:`, err)
|
|
751
776
|
// FAST LANE: native routes take dispatch_single_chunk (no chunk
|
|
752
777
|
// channel), so every native fallback MUST pack + return a length.
|
|
753
|
-
return packSingleChunkResponse(
|
|
778
|
+
return packSingleChunkResponse(slotView, encoder, {
|
|
754
779
|
status: 500,
|
|
755
780
|
contentType: 'text/html; charset=utf-8',
|
|
756
781
|
body: 'internal error',
|
|
@@ -762,7 +787,7 @@ export function makeRenderer(
|
|
|
762
787
|
const verdict = chainResult.verdict
|
|
763
788
|
if (!verdict.render) {
|
|
764
789
|
// redirect — no template render; fast-lane packed response with Location.
|
|
765
|
-
return packSingleChunkResponse(
|
|
790
|
+
return packSingleChunkResponse(slotView, encoder, {
|
|
766
791
|
status: verdict.status,
|
|
767
792
|
contentType: 'text/html; charset=utf-8',
|
|
768
793
|
body: '',
|
|
@@ -792,6 +817,21 @@ export function makeRenderer(
|
|
|
792
817
|
)
|
|
793
818
|
}
|
|
794
819
|
;(data as Record<string, unknown>).__brust_store__ = buildStoreScripts(storeSnapshot)
|
|
820
|
+
// Component-CSS SSR. Fill the framework-owned
|
|
821
|
+
// `{{ __brust_component_css__ | safe }}` head slot with the route's
|
|
822
|
+
// component-CSS chunk <link>s (from the component-CSS manifest, seeded
|
|
823
|
+
// per-route in the worker). Empty string when the route imports no
|
|
824
|
+
// .module.css / co-located .css, so the slot always resolves.
|
|
825
|
+
if (process.env.BRUST_DEV === '1' && '__brust_component_css__' in data) {
|
|
826
|
+
console.warn(
|
|
827
|
+
`[brust] native loader for "${flat.nativeTemplate}" returned a reserved "__brust_component_css__" key — overwritten by the component-CSS links`,
|
|
828
|
+
)
|
|
829
|
+
}
|
|
830
|
+
;(data as Record<string, unknown>).__brust_component_css__ = getCssHrefsForRoute(
|
|
831
|
+
flat.fullPath,
|
|
832
|
+
)
|
|
833
|
+
.map((href) => `<link rel="stylesheet" href="${href}"/>`)
|
|
834
|
+
.join('')
|
|
795
835
|
}
|
|
796
836
|
const json = JSON.stringify(data)
|
|
797
837
|
// napiRenderJinja has no headers param — any Set-Cookie staged by a
|
|
@@ -828,24 +868,25 @@ export function makeRenderer(
|
|
|
828
868
|
const finalBytes = encoder.encode(JSON.stringify(ctx))
|
|
829
869
|
// The original size check guarded the pre-island bytes; the merged
|
|
830
870
|
// context (with island props + component html) can be larger. Re-check on finalBytes.
|
|
831
|
-
if (finalBytes.length >
|
|
832
|
-
return packSingleChunkResponse(
|
|
871
|
+
if (finalBytes.length > slotView.length) {
|
|
872
|
+
return packSingleChunkResponse(slotView, encoder, {
|
|
833
873
|
status: 413,
|
|
834
874
|
contentType: 'text/plain; charset=utf-8',
|
|
835
875
|
body: 'loader data too large for SAB',
|
|
836
876
|
})
|
|
837
877
|
}
|
|
838
|
-
|
|
878
|
+
slotView.set(finalBytes, 0)
|
|
839
879
|
try {
|
|
840
880
|
return (native as any).napiRenderJinja(
|
|
841
881
|
Number(workerId),
|
|
882
|
+
slot,
|
|
842
883
|
finalBytes.length,
|
|
843
884
|
flat.nativeTemplate,
|
|
844
885
|
renderStatus,
|
|
845
886
|
)
|
|
846
887
|
} catch (err) {
|
|
847
888
|
console.error(`[brust] napiRenderJinja failed for "${flat.nativeTemplate}":`, err)
|
|
848
|
-
return packSingleChunkResponse(
|
|
889
|
+
return packSingleChunkResponse(slotView, encoder, {
|
|
849
890
|
status: 500,
|
|
850
891
|
contentType: 'text/html; charset=utf-8',
|
|
851
892
|
body: 'internal error',
|
|
@@ -853,14 +894,14 @@ export function makeRenderer(
|
|
|
853
894
|
}
|
|
854
895
|
}
|
|
855
896
|
const dataBytes = encoder.encode(json)
|
|
856
|
-
if (dataBytes.length >
|
|
857
|
-
return packSingleChunkResponse(
|
|
897
|
+
if (dataBytes.length > slotView.length) {
|
|
898
|
+
return packSingleChunkResponse(slotView, encoder, {
|
|
858
899
|
status: 413,
|
|
859
900
|
contentType: 'text/plain; charset=utf-8',
|
|
860
901
|
body: 'loader data too large for SAB',
|
|
861
902
|
})
|
|
862
903
|
}
|
|
863
|
-
|
|
904
|
+
slotView.set(dataBytes, 0)
|
|
864
905
|
try {
|
|
865
906
|
// FAST LANE: napiRenderJinja is a SYNC napi call — renders Rust-side,
|
|
866
907
|
// writes the framed response into the SAB, and returns its length
|
|
@@ -868,13 +909,14 @@ export function makeRenderer(
|
|
|
868
909
|
// fast-lane arm reads the SAB directly (no chunk channel).
|
|
869
910
|
return (native as any).napiRenderJinja(
|
|
870
911
|
Number(workerId),
|
|
912
|
+
slot,
|
|
871
913
|
dataBytes.length,
|
|
872
914
|
flat.nativeTemplate,
|
|
873
915
|
renderStatus,
|
|
874
916
|
)
|
|
875
917
|
} catch (err) {
|
|
876
918
|
console.error(`[brust] napiRenderJinja failed for "${flat.nativeTemplate}":`, err)
|
|
877
|
-
return packSingleChunkResponse(
|
|
919
|
+
return packSingleChunkResponse(slotView, encoder, {
|
|
878
920
|
status: 500,
|
|
879
921
|
contentType: 'text/html; charset=utf-8',
|
|
880
922
|
body: 'internal error',
|
|
@@ -900,16 +942,24 @@ export function makeRenderer(
|
|
|
900
942
|
// bind throw. Shape matches the legacy "internal error" path so
|
|
901
943
|
// existing integration tests stay green.
|
|
902
944
|
console.error(`[brust] render setup failed:`, err)
|
|
903
|
-
return await emitSingleChunkResponse(
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
945
|
+
return await emitSingleChunkResponse(
|
|
946
|
+
slotView,
|
|
947
|
+
napi,
|
|
948
|
+
workerId,
|
|
949
|
+
encoder,
|
|
950
|
+
{
|
|
951
|
+
status: 500,
|
|
952
|
+
contentType: 'text/html; charset=utf-8',
|
|
953
|
+
body: 'internal error',
|
|
954
|
+
},
|
|
955
|
+
slot,
|
|
956
|
+
)
|
|
908
957
|
}
|
|
909
958
|
const storeSnapshot = collectSnapshot()
|
|
910
959
|
await renderBranchStreaming({
|
|
911
960
|
element,
|
|
912
|
-
view,
|
|
961
|
+
view: slotView,
|
|
962
|
+
slot,
|
|
913
963
|
workerId,
|
|
914
964
|
napi,
|
|
915
965
|
errorBoundary,
|
|
@@ -923,22 +973,22 @@ export function makeRenderer(
|
|
|
923
973
|
})
|
|
924
974
|
}
|
|
925
975
|
if (call.kind === 'navigation') {
|
|
926
|
-
await navigationBranch(call, byRouteId,
|
|
976
|
+
await navigationBranch(call, byRouteId, slotView, encoder, opts.getWorkerId, slot)
|
|
927
977
|
return 0
|
|
928
978
|
}
|
|
929
979
|
if (call.kind === 'action') {
|
|
930
980
|
// FAST LANE: pack the framed response into the SAB and return its length.
|
|
931
981
|
// Rust reads it directly after the Promise settles — no chunk channel.
|
|
932
982
|
const resp = await dispatchAction(call, byActionId)
|
|
933
|
-
return packSingleChunkResponse(
|
|
983
|
+
return packSingleChunkResponse(slotView, encoder, resp)
|
|
934
984
|
}
|
|
935
985
|
if (call.kind === 'mcp') {
|
|
936
986
|
const resp = await mcpBranchToResponse(call, opts.mcp)
|
|
937
|
-
return await emitSingleChunkResponse(
|
|
987
|
+
return await emitSingleChunkResponse(slotView, napi, workerId, encoder, resp, slot)
|
|
938
988
|
}
|
|
939
989
|
if (call.kind === 'sse') {
|
|
940
990
|
try {
|
|
941
|
-
await sseBranch(call,
|
|
991
|
+
await sseBranch(call, slotView, encoder, routes)
|
|
942
992
|
} catch (err) {
|
|
943
993
|
// Setup-time errors only (BigInt coerce, dynamic import resolve,
|
|
944
994
|
// napi shim build). Once handleSseStream has started streaming,
|
|
@@ -951,7 +1001,7 @@ export function makeRenderer(
|
|
|
951
1001
|
}
|
|
952
1002
|
if (call.kind === 'ws') {
|
|
953
1003
|
try {
|
|
954
|
-
await wsBranch(call,
|
|
1004
|
+
await wsBranch(call, slotView, encoder, routes)
|
|
955
1005
|
} catch (err) {
|
|
956
1006
|
// Setup-time errors only — same reasoning as sseBranch above.
|
|
957
1007
|
console.error('[brust] wsBranch uncaught:', err)
|
|
@@ -963,11 +1013,18 @@ export function makeRenderer(
|
|
|
963
1013
|
// Unknown kind — log and 500. Shouldn't happen unless Rust ships
|
|
964
1014
|
// something out of band.
|
|
965
1015
|
console.error(`[brust] unknown envelope kind in worker:`, (call as { kind?: string }).kind)
|
|
966
|
-
return await emitSingleChunkResponse(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1016
|
+
return await emitSingleChunkResponse(
|
|
1017
|
+
slotView,
|
|
1018
|
+
napi,
|
|
1019
|
+
workerId,
|
|
1020
|
+
encoder,
|
|
1021
|
+
{
|
|
1022
|
+
status: 500,
|
|
1023
|
+
contentType: 'text/plain; charset=utf-8',
|
|
1024
|
+
body: 'invalid envelope kind',
|
|
1025
|
+
},
|
|
1026
|
+
slot,
|
|
1027
|
+
)
|
|
971
1028
|
}
|
|
972
1029
|
}
|
|
973
1030
|
|
|
@@ -1013,24 +1070,37 @@ async function navigationBranch(
|
|
|
1013
1070
|
view: Uint8Array,
|
|
1014
1071
|
encoder: TextEncoder,
|
|
1015
1072
|
getWorkerId: (() => number | null) | undefined,
|
|
1073
|
+
slot = 0,
|
|
1016
1074
|
): Promise<void> {
|
|
1017
1075
|
const workerId = BigInt(getWorkerId?.() ?? 0)
|
|
1018
1076
|
const napi = {
|
|
1019
|
-
renderChunk: async (wid: bigint, len: number, _view: Uint8Array): Promise<void> => {
|
|
1020
|
-
await (native as any).napiRenderChunk(Number(wid), len)
|
|
1077
|
+
renderChunk: async (wid: bigint, s: number, len: number, _view: Uint8Array): Promise<void> => {
|
|
1078
|
+
await (native as any).napiRenderChunk(Number(wid), s, len)
|
|
1021
1079
|
},
|
|
1022
|
-
renderChunkFinal: async (
|
|
1023
|
-
|
|
1080
|
+
renderChunkFinal: async (
|
|
1081
|
+
wid: bigint,
|
|
1082
|
+
s: number,
|
|
1083
|
+
len: number,
|
|
1084
|
+
_view: Uint8Array,
|
|
1085
|
+
): Promise<void> => {
|
|
1086
|
+
await (native as any).napiRenderChunkFinal(Number(wid), s, len)
|
|
1024
1087
|
},
|
|
1025
1088
|
}
|
|
1026
1089
|
|
|
1027
1090
|
const flat = byRouteId.get(call.route_id)
|
|
1028
1091
|
if (!flat) {
|
|
1029
|
-
await emitSingleChunkResponse(
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1092
|
+
await emitSingleChunkResponse(
|
|
1093
|
+
view,
|
|
1094
|
+
napi,
|
|
1095
|
+
workerId,
|
|
1096
|
+
encoder,
|
|
1097
|
+
{
|
|
1098
|
+
status: 404,
|
|
1099
|
+
contentType: 'application/json; charset=utf-8',
|
|
1100
|
+
body: '{"error":"not found"}',
|
|
1101
|
+
},
|
|
1102
|
+
slot,
|
|
1103
|
+
)
|
|
1034
1104
|
return
|
|
1035
1105
|
}
|
|
1036
1106
|
|
|
@@ -1059,11 +1129,18 @@ async function navigationBranch(
|
|
|
1059
1129
|
navVerdict = (await navChain()) as NavMarkerResponse
|
|
1060
1130
|
} catch (err) {
|
|
1061
1131
|
console.error('[brust] navigation middleware threw:', err)
|
|
1062
|
-
await emitSingleChunkResponse(
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1132
|
+
await emitSingleChunkResponse(
|
|
1133
|
+
view,
|
|
1134
|
+
napi,
|
|
1135
|
+
workerId,
|
|
1136
|
+
encoder,
|
|
1137
|
+
{
|
|
1138
|
+
status: 500,
|
|
1139
|
+
contentType: 'application/json; charset=utf-8',
|
|
1140
|
+
body: '{"error":"middleware threw"}',
|
|
1141
|
+
},
|
|
1142
|
+
slot,
|
|
1143
|
+
)
|
|
1067
1144
|
return
|
|
1068
1145
|
}
|
|
1069
1146
|
|
|
@@ -1071,12 +1148,19 @@ async function navigationBranch(
|
|
|
1071
1148
|
// Middleware short-circuited — emit the verdict. Client's non-2xx check
|
|
1072
1149
|
// triggers the full-reload fallback so the user hits the real route and
|
|
1073
1150
|
// sees the middleware's challenge page.
|
|
1074
|
-
await emitSingleChunkResponse(
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1151
|
+
await emitSingleChunkResponse(
|
|
1152
|
+
view,
|
|
1153
|
+
napi,
|
|
1154
|
+
workerId,
|
|
1155
|
+
encoder,
|
|
1156
|
+
{
|
|
1157
|
+
status: navVerdict.status,
|
|
1158
|
+
contentType: navVerdict.contentType ?? 'application/json; charset=utf-8',
|
|
1159
|
+
body: navVerdict.body,
|
|
1160
|
+
headers: navVerdict.headers,
|
|
1161
|
+
},
|
|
1162
|
+
slot,
|
|
1163
|
+
)
|
|
1080
1164
|
return
|
|
1081
1165
|
}
|
|
1082
1166
|
|
|
@@ -1100,7 +1184,7 @@ async function navigationBranch(
|
|
|
1100
1184
|
// full-document native render path).
|
|
1101
1185
|
let navHeaders: Record<string, string> | undefined
|
|
1102
1186
|
if (flat.nativeTemplate !== undefined) {
|
|
1103
|
-
fullHtml = await renderNativeRouteToHtml(call, flat, view, encoder, workerId)
|
|
1187
|
+
fullHtml = await renderNativeRouteToHtml(call, flat, view, encoder, workerId, slot)
|
|
1104
1188
|
} else {
|
|
1105
1189
|
// Wrap loader run (inside buildRenderElement) + render in one store scope so
|
|
1106
1190
|
// store reads resolve the per-request instance; collect after render.
|
|
@@ -1136,19 +1220,33 @@ async function navigationBranch(
|
|
|
1136
1220
|
const title = titleMatch ? titleMatch[1].replace(/<!--.*?-->/g, '').trim() : ''
|
|
1137
1221
|
|
|
1138
1222
|
const body = JSON.stringify({ html: innerHtml, title, store })
|
|
1139
|
-
await emitSingleChunkResponse(
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1223
|
+
await emitSingleChunkResponse(
|
|
1224
|
+
view,
|
|
1225
|
+
napi,
|
|
1226
|
+
workerId,
|
|
1227
|
+
encoder,
|
|
1228
|
+
{
|
|
1229
|
+
status: 200,
|
|
1230
|
+
contentType: 'application/json; charset=utf-8',
|
|
1231
|
+
body,
|
|
1232
|
+
headers: navHeaders,
|
|
1233
|
+
},
|
|
1234
|
+
slot,
|
|
1235
|
+
)
|
|
1145
1236
|
} catch (err) {
|
|
1146
1237
|
console.error('[brust] navigation render failed:', err)
|
|
1147
|
-
await emitSingleChunkResponse(
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1238
|
+
await emitSingleChunkResponse(
|
|
1239
|
+
view,
|
|
1240
|
+
napi,
|
|
1241
|
+
workerId,
|
|
1242
|
+
encoder,
|
|
1243
|
+
{
|
|
1244
|
+
status: 500,
|
|
1245
|
+
contentType: 'application/json; charset=utf-8',
|
|
1246
|
+
body: '{"error":"render failed"}',
|
|
1247
|
+
},
|
|
1248
|
+
slot,
|
|
1249
|
+
)
|
|
1152
1250
|
}
|
|
1153
1251
|
}
|
|
1154
1252
|
|
|
@@ -1170,6 +1268,7 @@ async function renderNativeRouteToHtml(
|
|
|
1170
1268
|
view: Uint8Array,
|
|
1171
1269
|
encoder: TextEncoder,
|
|
1172
1270
|
workerId: bigint,
|
|
1271
|
+
slot = 0,
|
|
1173
1272
|
): Promise<string> {
|
|
1174
1273
|
const templateName = flat.nativeTemplate as string
|
|
1175
1274
|
|
|
@@ -1211,6 +1310,7 @@ async function renderNativeRouteToHtml(
|
|
|
1211
1310
|
|
|
1212
1311
|
const len = (native as any).napiRenderJinja(
|
|
1213
1312
|
Number(workerId),
|
|
1313
|
+
slot,
|
|
1214
1314
|
bytes.length,
|
|
1215
1315
|
templateName,
|
|
1216
1316
|
) as number
|
|
@@ -1331,8 +1431,8 @@ function packSingleChunkResponse(
|
|
|
1331
1431
|
async function emitSingleChunkResponse(
|
|
1332
1432
|
view: Uint8Array,
|
|
1333
1433
|
napi: {
|
|
1334
|
-
renderChunk: (w: bigint, len: number, view: Uint8Array) => Promise<void>
|
|
1335
|
-
renderChunkFinal: (w: bigint, len: number, view: Uint8Array) => Promise<void>
|
|
1434
|
+
renderChunk: (w: bigint, slot: number, len: number, view: Uint8Array) => Promise<void>
|
|
1435
|
+
renderChunkFinal: (w: bigint, slot: number, len: number, view: Uint8Array) => Promise<void>
|
|
1336
1436
|
},
|
|
1337
1437
|
workerId: bigint,
|
|
1338
1438
|
encoder: TextEncoder,
|
|
@@ -1342,6 +1442,7 @@ async function emitSingleChunkResponse(
|
|
|
1342
1442
|
body: string | Uint8Array
|
|
1343
1443
|
headers?: Record<string, string>
|
|
1344
1444
|
},
|
|
1445
|
+
slot = 0,
|
|
1345
1446
|
): Promise<number> {
|
|
1346
1447
|
const bodyBytes = typeof resp.body === 'string' ? encoder.encode(resp.body) : resp.body
|
|
1347
1448
|
const meta = JSON.stringify({
|
|
@@ -1372,14 +1473,14 @@ async function emitSingleChunkResponse(
|
|
|
1372
1473
|
view[1] = errMetaBytes.length & 0xff
|
|
1373
1474
|
view.set(errMetaBytes, 2)
|
|
1374
1475
|
view.set(errBody, 2 + errMetaBytes.length)
|
|
1375
|
-
await napi.renderChunkFinal(workerId, errTotal, view)
|
|
1476
|
+
await napi.renderChunkFinal(workerId, slot, errTotal, view)
|
|
1376
1477
|
return 0
|
|
1377
1478
|
}
|
|
1378
1479
|
view[0] = (metaBytes.length >> 8) & 0xff
|
|
1379
1480
|
view[1] = metaBytes.length & 0xff
|
|
1380
1481
|
view.set(metaBytes, 2)
|
|
1381
1482
|
view.set(bodyBytes, 2 + metaBytes.length)
|
|
1382
|
-
await napi.renderChunkFinal(workerId, total, view)
|
|
1483
|
+
await napi.renderChunkFinal(workerId, slot, total, view)
|
|
1383
1484
|
return 0
|
|
1384
1485
|
}
|
|
1385
1486
|
|