hadars 0.1.6 → 0.1.8
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/dist/cli.js +117 -53
- package/dist/ssr-render-worker.js +119 -24
- package/package.json +1 -1
- package/src/build.ts +129 -75
- package/src/ssr-render-worker.ts +157 -51
package/dist/cli.js
CHANGED
|
@@ -770,11 +770,16 @@ var RenderWorkerPool = class {
|
|
|
770
770
|
const w = new Worker(workerPath, { workerData: { ssrBundlePath } });
|
|
771
771
|
this.workerPending.set(w, /* @__PURE__ */ new Set());
|
|
772
772
|
w.on("message", (msg) => {
|
|
773
|
-
const { id, type, html, error, chunk } = msg;
|
|
773
|
+
const { id, type, html, headHtml, status, error, chunk } = msg;
|
|
774
774
|
const p = this.pending.get(id);
|
|
775
775
|
if (!p)
|
|
776
776
|
return;
|
|
777
|
-
if (p.kind === "
|
|
777
|
+
if (p.kind === "renderFullStream") {
|
|
778
|
+
if (type === "head") {
|
|
779
|
+
p.headSettled = true;
|
|
780
|
+
p.headResolve({ headHtml, status });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
778
783
|
if (type === "chunk") {
|
|
779
784
|
p.controller.enqueue(chunk);
|
|
780
785
|
return;
|
|
@@ -783,8 +788,12 @@ var RenderWorkerPool = class {
|
|
|
783
788
|
this.workerPending.get(w)?.delete(id);
|
|
784
789
|
if (type === "done")
|
|
785
790
|
p.controller.close();
|
|
786
|
-
else
|
|
787
|
-
|
|
791
|
+
else {
|
|
792
|
+
const err = new Error(error ?? "Stream error");
|
|
793
|
+
if (!p.headSettled)
|
|
794
|
+
p.headReject(err);
|
|
795
|
+
p.controller.error(err);
|
|
796
|
+
}
|
|
788
797
|
return;
|
|
789
798
|
}
|
|
790
799
|
this.pending.delete(id);
|
|
@@ -792,7 +801,7 @@ var RenderWorkerPool = class {
|
|
|
792
801
|
if (error)
|
|
793
802
|
p.reject(new Error(error));
|
|
794
803
|
else
|
|
795
|
-
p.resolve(html);
|
|
804
|
+
p.resolve({ html, headHtml, status });
|
|
796
805
|
});
|
|
797
806
|
w.on("error", (err) => {
|
|
798
807
|
console.error("[hadars] Render worker error:", err);
|
|
@@ -820,10 +829,13 @@ var RenderWorkerPool = class {
|
|
|
820
829
|
const p = this.pending.get(id);
|
|
821
830
|
if (p) {
|
|
822
831
|
this.pending.delete(id);
|
|
823
|
-
if (p.kind === "
|
|
832
|
+
if (p.kind === "renderFull")
|
|
824
833
|
p.reject(err);
|
|
825
|
-
else
|
|
834
|
+
else {
|
|
835
|
+
if (!p.headSettled)
|
|
836
|
+
p.headReject(err);
|
|
826
837
|
p.controller.error(err);
|
|
838
|
+
}
|
|
827
839
|
}
|
|
828
840
|
}
|
|
829
841
|
this.workerPending.delete(w);
|
|
@@ -836,8 +848,8 @@ var RenderWorkerPool = class {
|
|
|
836
848
|
this.rrIndex++;
|
|
837
849
|
return w;
|
|
838
850
|
}
|
|
839
|
-
/**
|
|
840
|
-
|
|
851
|
+
/** Run the full SSR lifecycle in a worker thread. Returns html, headHtml, status. */
|
|
852
|
+
renderFull(req) {
|
|
841
853
|
return new Promise((resolve2, reject) => {
|
|
842
854
|
const w = this.nextWorker();
|
|
843
855
|
if (!w) {
|
|
@@ -845,10 +857,10 @@ var RenderWorkerPool = class {
|
|
|
845
857
|
return;
|
|
846
858
|
}
|
|
847
859
|
const id = this.nextId++;
|
|
848
|
-
this.pending.set(id, { kind: "
|
|
860
|
+
this.pending.set(id, { kind: "renderFull", resolve: resolve2, reject });
|
|
849
861
|
this.workerPending.get(w)?.add(id);
|
|
850
862
|
try {
|
|
851
|
-
w.postMessage({ id, type: "
|
|
863
|
+
w.postMessage({ id, type: "renderFull", streaming: false, request: req });
|
|
852
864
|
} catch (err) {
|
|
853
865
|
this.pending.delete(id);
|
|
854
866
|
this.workerPending.get(w)?.delete(id);
|
|
@@ -856,52 +868,56 @@ var RenderWorkerPool = class {
|
|
|
856
868
|
}
|
|
857
869
|
});
|
|
858
870
|
}
|
|
859
|
-
/**
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
let
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
871
|
+
/** Run the full SSR lifecycle in a worker thread, streaming the response. */
|
|
872
|
+
renderFullStream(req) {
|
|
873
|
+
let headResolve;
|
|
874
|
+
let headReject;
|
|
875
|
+
const head = new Promise((res, rej) => {
|
|
876
|
+
headResolve = res;
|
|
877
|
+
headReject = rej;
|
|
867
878
|
});
|
|
879
|
+
let controller;
|
|
880
|
+
const stream = new ReadableStream({ start: (ctrl) => {
|
|
881
|
+
controller = ctrl;
|
|
882
|
+
} });
|
|
868
883
|
const w = this.nextWorker();
|
|
869
884
|
if (!w) {
|
|
870
|
-
queueMicrotask(() =>
|
|
871
|
-
|
|
885
|
+
queueMicrotask(() => {
|
|
886
|
+
headReject(new Error("[hadars] No render workers available"));
|
|
887
|
+
controller.error(new Error("[hadars] No render workers available"));
|
|
888
|
+
});
|
|
889
|
+
return { head, stream };
|
|
872
890
|
}
|
|
873
891
|
const id = this.nextId++;
|
|
874
|
-
this.pending.set(id, { kind: "
|
|
892
|
+
this.pending.set(id, { kind: "renderFullStream", headSettled: false, headResolve, headReject, controller });
|
|
875
893
|
this.workerPending.get(w)?.add(id);
|
|
876
894
|
try {
|
|
877
|
-
w.postMessage({ id, type: "
|
|
895
|
+
w.postMessage({ id, type: "renderFull", streaming: true, request: req });
|
|
878
896
|
} catch (err) {
|
|
879
897
|
this.pending.delete(id);
|
|
880
898
|
this.workerPending.get(w)?.delete(id);
|
|
881
|
-
queueMicrotask(() =>
|
|
899
|
+
queueMicrotask(() => {
|
|
900
|
+
headReject(err);
|
|
901
|
+
controller.error(err);
|
|
902
|
+
});
|
|
882
903
|
}
|
|
883
|
-
return stream;
|
|
904
|
+
return { head, stream };
|
|
884
905
|
}
|
|
885
906
|
async terminate() {
|
|
886
907
|
await Promise.all(this.workers.map((w) => w.terminate()));
|
|
887
908
|
}
|
|
888
909
|
};
|
|
889
|
-
async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, streaming,
|
|
890
|
-
const renderToString = !streaming
|
|
891
|
-
const renderReadableStream = streaming
|
|
892
|
-
const unsuspendForRender = renderPayload?.appProps?.context?._unsuspend ?? null;
|
|
910
|
+
async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, streaming, unsuspendForRender) {
|
|
911
|
+
const renderToString = !streaming ? await getRenderToString() : null;
|
|
912
|
+
const renderReadableStream = streaming ? await getReadableStreamRenderer() : null;
|
|
893
913
|
if (!streaming) {
|
|
894
914
|
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
895
915
|
let bodyHtml;
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
bodyHtml = renderToString(ReactPage);
|
|
902
|
-
} finally {
|
|
903
|
-
globalThis.__hadarsUnsuspend = null;
|
|
904
|
-
}
|
|
916
|
+
try {
|
|
917
|
+
globalThis.__hadarsUnsuspend = unsuspendForRender;
|
|
918
|
+
bodyHtml = renderToString(ReactPage);
|
|
919
|
+
} finally {
|
|
920
|
+
globalThis.__hadarsUnsuspend = null;
|
|
905
921
|
}
|
|
906
922
|
return new Response(precontentHtml + bodyHtml + postContent, {
|
|
907
923
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
@@ -912,19 +928,14 @@ async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml,
|
|
|
912
928
|
async start(controller) {
|
|
913
929
|
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
914
930
|
controller.enqueue(encoder.encode(precontentHtml));
|
|
915
|
-
let
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
globalThis.__hadarsUnsuspend = unsuspendForRender;
|
|
922
|
-
streamPromise = renderReadableStream(ReactPage);
|
|
923
|
-
} finally {
|
|
924
|
-
globalThis.__hadarsUnsuspend = null;
|
|
925
|
-
}
|
|
926
|
-
bodyStream = await streamPromise;
|
|
931
|
+
let streamPromise;
|
|
932
|
+
try {
|
|
933
|
+
globalThis.__hadarsUnsuspend = unsuspendForRender;
|
|
934
|
+
streamPromise = renderReadableStream(ReactPage);
|
|
935
|
+
} finally {
|
|
936
|
+
globalThis.__hadarsUnsuspend = null;
|
|
927
937
|
}
|
|
938
|
+
const bodyStream = await streamPromise;
|
|
928
939
|
const reader = bodyStream.getReader();
|
|
929
940
|
while (true) {
|
|
930
941
|
const { done, value } = await reader.read();
|
|
@@ -941,6 +952,24 @@ async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml,
|
|
|
941
952
|
status
|
|
942
953
|
});
|
|
943
954
|
}
|
|
955
|
+
async function serializeRequest(req) {
|
|
956
|
+
const isGetOrHead = ["GET", "HEAD"].includes(req.method ?? "GET");
|
|
957
|
+
const body = isGetOrHead ? null : new Uint8Array(await req.clone().arrayBuffer());
|
|
958
|
+
const headers = {};
|
|
959
|
+
req.headers.forEach((v, k) => {
|
|
960
|
+
headers[k] = v;
|
|
961
|
+
});
|
|
962
|
+
return {
|
|
963
|
+
url: req.url,
|
|
964
|
+
method: req.method ?? "GET",
|
|
965
|
+
headers,
|
|
966
|
+
body,
|
|
967
|
+
pathname: req.pathname,
|
|
968
|
+
search: req.search,
|
|
969
|
+
location: req.location,
|
|
970
|
+
cookies: req.cookies
|
|
971
|
+
};
|
|
972
|
+
}
|
|
944
973
|
var makePrecontentHtmlGetter = (htmlFilePromise) => {
|
|
945
974
|
let preHead = null;
|
|
946
975
|
let postHead = null;
|
|
@@ -1194,7 +1223,7 @@ var dev = async (options) => {
|
|
|
1194
1223
|
getAfterRenderProps,
|
|
1195
1224
|
getFinalProps
|
|
1196
1225
|
} = await import(importPath);
|
|
1197
|
-
const { ReactPage, status, headHtml } = await getReactResponse(request, {
|
|
1226
|
+
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1198
1227
|
document: {
|
|
1199
1228
|
body: Component,
|
|
1200
1229
|
lang: "en",
|
|
@@ -1203,7 +1232,8 @@ var dev = async (options) => {
|
|
|
1203
1232
|
getFinalProps
|
|
1204
1233
|
}
|
|
1205
1234
|
});
|
|
1206
|
-
|
|
1235
|
+
const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
|
|
1236
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, options.streaming === true, unsuspend);
|
|
1207
1237
|
}, options.websocket);
|
|
1208
1238
|
};
|
|
1209
1239
|
var build = async (options) => {
|
|
@@ -1332,6 +1362,39 @@ var run = async (options) => {
|
|
|
1332
1362
|
getAfterRenderProps,
|
|
1333
1363
|
getFinalProps
|
|
1334
1364
|
} = await import(componentPath);
|
|
1365
|
+
if (renderPool) {
|
|
1366
|
+
const serialReq = await serializeRequest(request);
|
|
1367
|
+
if (options.streaming) {
|
|
1368
|
+
const { head, stream } = renderPool.renderFullStream(serialReq);
|
|
1369
|
+
const { headHtml: wHead, status: wStatus } = await head;
|
|
1370
|
+
const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
|
|
1371
|
+
const responseStream = new ReadableStream({
|
|
1372
|
+
async start(controller) {
|
|
1373
|
+
controller.enqueue(encoder.encode(precontentHtml));
|
|
1374
|
+
const reader = stream.getReader();
|
|
1375
|
+
while (true) {
|
|
1376
|
+
const { done, value } = await reader.read();
|
|
1377
|
+
if (done)
|
|
1378
|
+
break;
|
|
1379
|
+
controller.enqueue(value);
|
|
1380
|
+
}
|
|
1381
|
+
controller.enqueue(encoder.encode(postContent));
|
|
1382
|
+
controller.close();
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
return new Response(responseStream, {
|
|
1386
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1387
|
+
status: wStatus
|
|
1388
|
+
});
|
|
1389
|
+
} else {
|
|
1390
|
+
const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
|
|
1391
|
+
const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
|
|
1392
|
+
return new Response(precontentHtml + html + postContent, {
|
|
1393
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1394
|
+
status: wStatus
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1335
1398
|
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1336
1399
|
document: {
|
|
1337
1400
|
body: Component,
|
|
@@ -1341,7 +1404,8 @@ var run = async (options) => {
|
|
|
1341
1404
|
getFinalProps
|
|
1342
1405
|
}
|
|
1343
1406
|
});
|
|
1344
|
-
|
|
1407
|
+
const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
|
|
1408
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, options.streaming === true, unsuspend);
|
|
1345
1409
|
}, options.websocket);
|
|
1346
1410
|
};
|
|
1347
1411
|
|
|
@@ -8,9 +8,9 @@ var _React = null;
|
|
|
8
8
|
var _renderToStaticMarkup = null;
|
|
9
9
|
var _renderToString = null;
|
|
10
10
|
var _renderToReadableStream = null;
|
|
11
|
-
var
|
|
11
|
+
var _ssrMod = null;
|
|
12
12
|
async function init() {
|
|
13
|
-
if (_React &&
|
|
13
|
+
if (_React && _ssrMod)
|
|
14
14
|
return;
|
|
15
15
|
const req = createRequire(pathMod.resolve(process.cwd(), "__ninety_fake__.js"));
|
|
16
16
|
if (!_React) {
|
|
@@ -29,19 +29,51 @@ async function init() {
|
|
|
29
29
|
const browserMod = await import(browserPath);
|
|
30
30
|
_renderToReadableStream = browserMod.renderToReadableStream;
|
|
31
31
|
}
|
|
32
|
-
if (!
|
|
33
|
-
|
|
34
|
-
_Component = ssrMod.default;
|
|
32
|
+
if (!_ssrMod) {
|
|
33
|
+
_ssrMod = await import(pathToFileURL(ssrBundlePath).href);
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
|
-
function
|
|
36
|
+
function deserializeRequest(s) {
|
|
37
|
+
const init2 = { method: s.method, headers: new Headers(s.headers) };
|
|
38
|
+
if (s.body)
|
|
39
|
+
init2.body = s.body;
|
|
40
|
+
const req = new Request(s.url, init2);
|
|
41
|
+
Object.assign(req, {
|
|
42
|
+
pathname: s.pathname,
|
|
43
|
+
search: s.search,
|
|
44
|
+
location: s.location,
|
|
45
|
+
cookies: s.cookies
|
|
46
|
+
});
|
|
47
|
+
return req;
|
|
48
|
+
}
|
|
49
|
+
function buildHeadHtml(head) {
|
|
50
|
+
const R = _React;
|
|
51
|
+
const metaEntries = Object.entries(head.meta ?? {});
|
|
52
|
+
const linkEntries = Object.entries(head.link ?? {});
|
|
53
|
+
const styleEntries = Object.entries(head.style ?? {});
|
|
54
|
+
const scriptEntries = Object.entries(head.script ?? {});
|
|
55
|
+
return _renderToStaticMarkup(
|
|
56
|
+
R.createElement(
|
|
57
|
+
R.Fragment,
|
|
58
|
+
null,
|
|
59
|
+
R.createElement("title", null, head.title),
|
|
60
|
+
...metaEntries.map(([id, opts]) => R.createElement("meta", { key: id, id, ...opts })),
|
|
61
|
+
...linkEntries.map(([id, opts]) => R.createElement("link", { key: id, id, ...opts })),
|
|
62
|
+
...styleEntries.map(([id, opts]) => R.createElement("style", { key: id, id, ...opts })),
|
|
63
|
+
...scriptEntries.map(([id, opts]) => R.createElement("script", { key: id, id, ...opts }))
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function buildReactPage(appProps, clientProps) {
|
|
68
|
+
const R = _React;
|
|
69
|
+
const Component = _ssrMod.default;
|
|
38
70
|
return R.createElement(
|
|
39
71
|
R.Fragment,
|
|
40
72
|
null,
|
|
41
73
|
R.createElement(
|
|
42
74
|
"div",
|
|
43
75
|
{ id: "app" },
|
|
44
|
-
R.createElement(
|
|
76
|
+
R.createElement(Component, appProps)
|
|
45
77
|
),
|
|
46
78
|
R.createElement("script", {
|
|
47
79
|
id: "hadars",
|
|
@@ -52,18 +84,79 @@ function buildReactPage(R, appProps, clientProps) {
|
|
|
52
84
|
})
|
|
53
85
|
);
|
|
54
86
|
}
|
|
87
|
+
async function runFullLifecycle(serialReq) {
|
|
88
|
+
const R = _React;
|
|
89
|
+
const Component = _ssrMod.default;
|
|
90
|
+
const { getInitProps, getAfterRenderProps, getFinalProps } = _ssrMod;
|
|
91
|
+
const parsedReq = deserializeRequest(serialReq);
|
|
92
|
+
const unsuspend = { cache: /* @__PURE__ */ new Map(), hasPending: false };
|
|
93
|
+
const context = {
|
|
94
|
+
head: { title: "Hadars App", meta: {}, link: {}, style: {}, script: {}, status: 200 },
|
|
95
|
+
_unsuspend: unsuspend
|
|
96
|
+
};
|
|
97
|
+
let props = {
|
|
98
|
+
...getInitProps ? await getInitProps(parsedReq) : {},
|
|
99
|
+
location: serialReq.location,
|
|
100
|
+
context
|
|
101
|
+
};
|
|
102
|
+
let html = "";
|
|
103
|
+
let iters = 0;
|
|
104
|
+
do {
|
|
105
|
+
unsuspend.hasPending = false;
|
|
106
|
+
try {
|
|
107
|
+
globalThis.__hadarsUnsuspend = unsuspend;
|
|
108
|
+
html = _renderToStaticMarkup(R.createElement(Component, props));
|
|
109
|
+
} finally {
|
|
110
|
+
globalThis.__hadarsUnsuspend = null;
|
|
111
|
+
}
|
|
112
|
+
if (unsuspend.hasPending) {
|
|
113
|
+
const pending = [...unsuspend.cache.values()].filter((e) => e.status === "pending").map((e) => e.promise);
|
|
114
|
+
await Promise.all(pending);
|
|
115
|
+
}
|
|
116
|
+
} while (unsuspend.hasPending && ++iters < 25);
|
|
117
|
+
props = getAfterRenderProps ? await getAfterRenderProps(props, html) : props;
|
|
118
|
+
try {
|
|
119
|
+
globalThis.__hadarsUnsuspend = unsuspend;
|
|
120
|
+
_renderToStaticMarkup(R.createElement(Component, { ...props, location: serialReq.location, context }));
|
|
121
|
+
} finally {
|
|
122
|
+
globalThis.__hadarsUnsuspend = null;
|
|
123
|
+
}
|
|
124
|
+
const serverData = {};
|
|
125
|
+
for (const [k, v] of unsuspend.cache) {
|
|
126
|
+
if (v.status === "fulfilled")
|
|
127
|
+
serverData[k] = v.value;
|
|
128
|
+
if (v.status === "suspense-cached")
|
|
129
|
+
serverData[k] = v.value;
|
|
130
|
+
}
|
|
131
|
+
const { context: _ctx, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
|
|
132
|
+
const clientProps = {
|
|
133
|
+
...restProps,
|
|
134
|
+
location: serialReq.location,
|
|
135
|
+
...Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}
|
|
136
|
+
};
|
|
137
|
+
const headHtml = buildHeadHtml(context.head);
|
|
138
|
+
const status = context.head.status ?? 200;
|
|
139
|
+
const finalAppProps = { ...props, location: serialReq.location, context };
|
|
140
|
+
return { finalAppProps, clientProps, headHtml, status, unsuspend };
|
|
141
|
+
}
|
|
55
142
|
parentPort.on("message", async (msg) => {
|
|
56
|
-
const { id, type } = msg;
|
|
143
|
+
const { id, type, request, streaming } = msg;
|
|
57
144
|
try {
|
|
58
145
|
await init();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
146
|
+
if (type !== "renderFull")
|
|
147
|
+
return;
|
|
148
|
+
const { finalAppProps, clientProps, headHtml, status, unsuspend } = await runFullLifecycle(request);
|
|
149
|
+
const ReactPage = buildReactPage(finalAppProps, clientProps);
|
|
150
|
+
if (streaming) {
|
|
151
|
+
parentPort.postMessage({ id, type: "head", headHtml, status });
|
|
152
|
+
let streamPromise;
|
|
153
|
+
try {
|
|
154
|
+
globalThis.__hadarsUnsuspend = unsuspend;
|
|
155
|
+
streamPromise = _renderToReadableStream(ReactPage);
|
|
156
|
+
} finally {
|
|
157
|
+
globalThis.__hadarsUnsuspend = null;
|
|
158
|
+
}
|
|
159
|
+
const stream = await streamPromise;
|
|
67
160
|
const reader = stream.getReader();
|
|
68
161
|
while (true) {
|
|
69
162
|
const { done, value } = await reader.read();
|
|
@@ -72,18 +165,20 @@ parentPort.on("message", async (msg) => {
|
|
|
72
165
|
parentPort.postMessage({ id, type: "chunk", chunk: value }, [value.buffer]);
|
|
73
166
|
}
|
|
74
167
|
parentPort.postMessage({ id, type: "done" });
|
|
75
|
-
|
|
168
|
+
} else {
|
|
169
|
+
let html;
|
|
170
|
+
try {
|
|
171
|
+
globalThis.__hadarsUnsuspend = unsuspend;
|
|
172
|
+
html = _renderToString(ReactPage);
|
|
173
|
+
} finally {
|
|
174
|
+
globalThis.__hadarsUnsuspend = null;
|
|
175
|
+
}
|
|
176
|
+
parentPort.postMessage({ id, html, headHtml, status });
|
|
76
177
|
}
|
|
77
|
-
const { appProps, clientProps } = msg;
|
|
78
|
-
const ReactPage = buildReactPage(R, appProps, clientProps);
|
|
79
|
-
globalThis.__hadarsUnsuspend = unsuspend;
|
|
80
|
-
const html = _renderToString(ReactPage);
|
|
81
|
-
globalThis.__hadarsUnsuspend = null;
|
|
82
|
-
parentPort.postMessage({ id, html });
|
|
83
178
|
} catch (err) {
|
|
84
179
|
globalThis.__hadarsUnsuspend = null;
|
|
85
180
|
const errMsg = err?.message ?? String(err);
|
|
86
|
-
if (
|
|
181
|
+
if (streaming) {
|
|
87
182
|
parentPort.postMessage({ id, type: "error", error: errMsg });
|
|
88
183
|
} else {
|
|
89
184
|
parentPort.postMessage({ id, error: errMsg });
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -58,16 +58,21 @@ async function getRenderToString(): Promise<(element: any) => string> {
|
|
|
58
58
|
// renderString — renderToString for non-streaming responses
|
|
59
59
|
// renderStream — renderToReadableStream chunks forwarded as a ReadableStream
|
|
60
60
|
|
|
61
|
-
type
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
import type { SerializableRequest } from './ssr-render-worker';
|
|
62
|
+
|
|
63
|
+
type PendingRenderFull = {
|
|
64
|
+
kind: 'renderFull';
|
|
65
|
+
resolve: (result: { html: string; headHtml: string; status: number }) => void;
|
|
64
66
|
reject: (err: Error) => void;
|
|
65
67
|
};
|
|
66
|
-
type
|
|
67
|
-
kind: '
|
|
68
|
+
type PendingRenderFullStream = {
|
|
69
|
+
kind: 'renderFullStream';
|
|
70
|
+
headSettled: boolean;
|
|
71
|
+
headResolve: (result: { headHtml: string; status: number }) => void;
|
|
72
|
+
headReject: (err: Error) => void;
|
|
68
73
|
controller: ReadableStreamDefaultController<Uint8Array>;
|
|
69
74
|
};
|
|
70
|
-
type PendingEntry =
|
|
75
|
+
type PendingEntry = PendingRenderFull | PendingRenderFullStream;
|
|
71
76
|
|
|
72
77
|
class RenderWorkerPool {
|
|
73
78
|
private workers: any[] = [];
|
|
@@ -90,27 +95,36 @@ class RenderWorkerPool {
|
|
|
90
95
|
const w = new Worker(workerPath, { workerData: { ssrBundlePath } });
|
|
91
96
|
this.workerPending.set(w, new Set());
|
|
92
97
|
w.on('message', (msg: any) => {
|
|
93
|
-
const { id, type, html, error, chunk } = msg;
|
|
98
|
+
const { id, type, html, headHtml, status, error, chunk } = msg;
|
|
94
99
|
const p = this.pending.get(id);
|
|
95
100
|
if (!p) return;
|
|
96
101
|
|
|
97
|
-
if (p.kind === '
|
|
102
|
+
if (p.kind === 'renderFullStream') {
|
|
103
|
+
if (type === 'head') {
|
|
104
|
+
p.headSettled = true;
|
|
105
|
+
p.headResolve({ headHtml, status });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
98
108
|
if (type === 'chunk') {
|
|
99
109
|
p.controller.enqueue(chunk as Uint8Array);
|
|
100
|
-
return;
|
|
110
|
+
return;
|
|
101
111
|
}
|
|
102
112
|
this.pending.delete(id);
|
|
103
113
|
this.workerPending.get(w)?.delete(id);
|
|
104
114
|
if (type === 'done') p.controller.close();
|
|
105
|
-
else
|
|
115
|
+
else {
|
|
116
|
+
const err = new Error(error ?? 'Stream error');
|
|
117
|
+
if (!p.headSettled) p.headReject(err);
|
|
118
|
+
p.controller.error(err);
|
|
119
|
+
}
|
|
106
120
|
return;
|
|
107
121
|
}
|
|
108
122
|
|
|
109
|
-
//
|
|
123
|
+
// renderFull (non-streaming)
|
|
110
124
|
this.pending.delete(id);
|
|
111
125
|
this.workerPending.get(w)?.delete(id);
|
|
112
126
|
if (error) p.reject(new Error(error));
|
|
113
|
-
else p.resolve(html);
|
|
127
|
+
else p.resolve({ html, headHtml, status });
|
|
114
128
|
});
|
|
115
129
|
w.on('error', (err: Error) => {
|
|
116
130
|
console.error('[hadars] Render worker error:', err);
|
|
@@ -130,19 +144,20 @@ class RenderWorkerPool {
|
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
private _handleWorkerDeath(w: any, err: Error) {
|
|
133
|
-
// Remove the dead worker from the rotation so it is never selected again.
|
|
134
147
|
const idx = this.workers.indexOf(w);
|
|
135
148
|
if (idx !== -1) this.workers.splice(idx, 1);
|
|
136
149
|
|
|
137
|
-
// Reject every in-flight request that was sent to this worker.
|
|
138
150
|
const ids = this.workerPending.get(w);
|
|
139
151
|
if (ids) {
|
|
140
152
|
for (const id of ids) {
|
|
141
153
|
const p = this.pending.get(id);
|
|
142
154
|
if (p) {
|
|
143
155
|
this.pending.delete(id);
|
|
144
|
-
if (p.kind === '
|
|
145
|
-
else
|
|
156
|
+
if (p.kind === 'renderFull') p.reject(err);
|
|
157
|
+
else {
|
|
158
|
+
if (!p.headSettled) p.headReject(err);
|
|
159
|
+
p.controller.error(err);
|
|
160
|
+
}
|
|
146
161
|
}
|
|
147
162
|
}
|
|
148
163
|
this.workerPending.delete(w);
|
|
@@ -156,19 +171,16 @@ class RenderWorkerPool {
|
|
|
156
171
|
return w;
|
|
157
172
|
}
|
|
158
173
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
174
|
+
/** Run the full SSR lifecycle in a worker thread. Returns html, headHtml, status. */
|
|
175
|
+
renderFull(req: SerializableRequest): Promise<{ html: string; headHtml: string; status: number }> {
|
|
161
176
|
return new Promise((resolve, reject) => {
|
|
162
177
|
const w = this.nextWorker();
|
|
163
|
-
if (!w) {
|
|
164
|
-
reject(new Error('[hadars] No render workers available'));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
178
|
+
if (!w) { reject(new Error('[hadars] No render workers available')); return; }
|
|
167
179
|
const id = this.nextId++;
|
|
168
|
-
this.pending.set(id, { kind: '
|
|
180
|
+
this.pending.set(id, { kind: 'renderFull', resolve, reject });
|
|
169
181
|
this.workerPending.get(w)?.add(id);
|
|
170
182
|
try {
|
|
171
|
-
w.postMessage({ id, type: '
|
|
183
|
+
w.postMessage({ id, type: 'renderFull', streaming: false, request: req });
|
|
172
184
|
} catch (err) {
|
|
173
185
|
this.pending.delete(id);
|
|
174
186
|
this.workerPending.get(w)?.delete(id);
|
|
@@ -177,32 +189,34 @@ class RenderWorkerPool {
|
|
|
177
189
|
});
|
|
178
190
|
}
|
|
179
191
|
|
|
180
|
-
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
let
|
|
184
|
-
const
|
|
185
|
-
|
|
192
|
+
/** Run the full SSR lifecycle in a worker thread, streaming the response. */
|
|
193
|
+
renderFullStream(req: SerializableRequest): { head: Promise<{ headHtml: string; status: number }>; stream: ReadableStream<Uint8Array> } {
|
|
194
|
+
let headResolve!: (r: { headHtml: string; status: number }) => void;
|
|
195
|
+
let headReject!: (err: Error) => void;
|
|
196
|
+
const head = new Promise<{ headHtml: string; status: number }>((res, rej) => {
|
|
197
|
+
headResolve = res; headReject = rej;
|
|
186
198
|
});
|
|
199
|
+
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
|
200
|
+
const stream = new ReadableStream<Uint8Array>({ start: (ctrl) => { controller = ctrl; } });
|
|
187
201
|
const w = this.nextWorker();
|
|
188
202
|
if (!w) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
queueMicrotask(() => {
|
|
204
|
+
headReject(new Error('[hadars] No render workers available'));
|
|
205
|
+
controller.error(new Error('[hadars] No render workers available'));
|
|
206
|
+
});
|
|
207
|
+
return { head, stream };
|
|
192
208
|
}
|
|
193
209
|
const id = this.nextId++;
|
|
194
|
-
|
|
195
|
-
// the first chunk arrives.
|
|
196
|
-
this.pending.set(id, { kind: 'renderStream', controller });
|
|
210
|
+
this.pending.set(id, { kind: 'renderFullStream', headSettled: false, headResolve, headReject, controller });
|
|
197
211
|
this.workerPending.get(w)?.add(id);
|
|
198
212
|
try {
|
|
199
|
-
w.postMessage({ id, type: '
|
|
213
|
+
w.postMessage({ id, type: 'renderFull', streaming: true, request: req });
|
|
200
214
|
} catch (err) {
|
|
201
215
|
this.pending.delete(id);
|
|
202
216
|
this.workerPending.get(w)?.delete(id);
|
|
203
|
-
queueMicrotask(() => controller.error(err));
|
|
217
|
+
queueMicrotask(() => { headReject(err as Error); controller.error(err); });
|
|
204
218
|
}
|
|
205
|
-
return stream;
|
|
219
|
+
return { head, stream };
|
|
206
220
|
}
|
|
207
221
|
|
|
208
222
|
async terminate(): Promise<void> {
|
|
@@ -216,33 +230,24 @@ async function buildSsrResponse(
|
|
|
216
230
|
status: number,
|
|
217
231
|
getPrecontentHtml: (headHtml: string) => Promise<[string, string]>,
|
|
218
232
|
streaming: boolean,
|
|
219
|
-
|
|
220
|
-
renderPayload?: { appProps: Record<string, unknown>; clientProps: Record<string, unknown> },
|
|
233
|
+
unsuspendForRender: any,
|
|
221
234
|
): Promise<Response> {
|
|
222
235
|
// Resolve renderers before touching globalThis.__hadarsUnsuspend.
|
|
223
236
|
// Any await after setting the global would create a window where a concurrent
|
|
224
237
|
// request on the same thread could overwrite it before the render call runs.
|
|
225
238
|
// Loading the renderer first ensures the set→call→clear sequence is synchronous.
|
|
226
|
-
const renderToString =
|
|
227
|
-
const renderReadableStream =
|
|
228
|
-
|
|
229
|
-
// Extract the unsuspend cache from renderPayload so non-pool renders can expose
|
|
230
|
-
// resolved useServerData() values to the SSR bundle via globalThis.__hadarsUnsuspend.
|
|
231
|
-
const unsuspendForRender = (renderPayload?.appProps?.context as any)?._unsuspend ?? null;
|
|
239
|
+
const renderToString = !streaming ? await getRenderToString() : null;
|
|
240
|
+
const renderReadableStream = streaming ? await getReadableStreamRenderer() : null;
|
|
232
241
|
|
|
233
242
|
if (!streaming) {
|
|
234
243
|
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
235
244
|
let bodyHtml: string;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
bodyHtml = renderToString!(ReactPage);
|
|
243
|
-
} finally {
|
|
244
|
-
(globalThis as any).__hadarsUnsuspend = null;
|
|
245
|
-
}
|
|
245
|
+
// set → call (synchronous) → clear: no await in between, safe under concurrency
|
|
246
|
+
try {
|
|
247
|
+
(globalThis as any).__hadarsUnsuspend = unsuspendForRender;
|
|
248
|
+
bodyHtml = renderToString!(ReactPage);
|
|
249
|
+
} finally {
|
|
250
|
+
(globalThis as any).__hadarsUnsuspend = null;
|
|
246
251
|
}
|
|
247
252
|
return new Response(precontentHtml + bodyHtml + postContent, {
|
|
248
253
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
@@ -255,22 +260,17 @@ async function buildSsrResponse(
|
|
|
255
260
|
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
256
261
|
controller.enqueue(encoder.encode(precontentHtml));
|
|
257
262
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
(globalThis as any).__hadarsUnsuspend = unsuspendForRender;
|
|
268
|
-
streamPromise = renderReadableStream!(ReactPage);
|
|
269
|
-
} finally {
|
|
270
|
-
(globalThis as any).__hadarsUnsuspend = null;
|
|
271
|
-
}
|
|
272
|
-
bodyStream = await streamPromise;
|
|
263
|
+
// React's renderToReadableStream starts rendering synchronously on call,
|
|
264
|
+
// so hooks fire before the returned Promise settles. Set the global
|
|
265
|
+
// immediately before the call and clear right after — no await in between.
|
|
266
|
+
let streamPromise: Promise<ReadableStream<Uint8Array>>;
|
|
267
|
+
try {
|
|
268
|
+
(globalThis as any).__hadarsUnsuspend = unsuspendForRender;
|
|
269
|
+
streamPromise = renderReadableStream!(ReactPage);
|
|
270
|
+
} finally {
|
|
271
|
+
(globalThis as any).__hadarsUnsuspend = null;
|
|
273
272
|
}
|
|
273
|
+
const bodyStream = await streamPromise;
|
|
274
274
|
const reader = bodyStream.getReader();
|
|
275
275
|
while (true) {
|
|
276
276
|
const { done, value } = await reader.read();
|
|
@@ -289,6 +289,24 @@ async function buildSsrResponse(
|
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
/** Serialize a HadarsRequest into a structure-clonable object for postMessage. */
|
|
293
|
+
async function serializeRequest(req: any): Promise<SerializableRequest> {
|
|
294
|
+
const isGetOrHead = ['GET', 'HEAD'].includes(req.method ?? 'GET');
|
|
295
|
+
const body = isGetOrHead ? null : new Uint8Array(await req.clone().arrayBuffer());
|
|
296
|
+
const headers: Record<string, string> = {};
|
|
297
|
+
(req.headers as Headers).forEach((v: string, k: string) => { headers[k] = v; });
|
|
298
|
+
return {
|
|
299
|
+
url: req.url,
|
|
300
|
+
method: req.method ?? 'GET',
|
|
301
|
+
headers,
|
|
302
|
+
body,
|
|
303
|
+
pathname: req.pathname,
|
|
304
|
+
search: req.search,
|
|
305
|
+
location: req.location,
|
|
306
|
+
cookies: req.cookies,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
292
310
|
/**
|
|
293
311
|
* Returns a function that parses `out.html` into pre-head, post-head, and
|
|
294
312
|
* post-content segments and caches the result. Call the returned function with
|
|
@@ -593,7 +611,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
593
611
|
getFinalProps,
|
|
594
612
|
} = (await import(importPath)) as HadarsEntryModule<any>;
|
|
595
613
|
|
|
596
|
-
const { ReactPage, status, headHtml } = await getReactResponse(request, {
|
|
614
|
+
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
597
615
|
document: {
|
|
598
616
|
body: Component as React.FC<HadarsProps<object>>,
|
|
599
617
|
lang: 'en',
|
|
@@ -603,7 +621,8 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
603
621
|
},
|
|
604
622
|
});
|
|
605
623
|
|
|
606
|
-
|
|
624
|
+
const unsuspend = (renderPayload.appProps.context as any)?._unsuspend ?? null;
|
|
625
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, options.streaming === true, unsuspend);
|
|
607
626
|
}, options.websocket);
|
|
608
627
|
};
|
|
609
628
|
|
|
@@ -762,6 +781,40 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
762
781
|
getFinalProps,
|
|
763
782
|
} = (await import(componentPath)) as HadarsEntryModule<any>;
|
|
764
783
|
|
|
784
|
+
if (renderPool) {
|
|
785
|
+
// Worker runs the full lifecycle — no non-serializable objects cross the thread boundary.
|
|
786
|
+
const serialReq = await serializeRequest(request);
|
|
787
|
+
if (options.streaming) {
|
|
788
|
+
const { head, stream } = renderPool.renderFullStream(serialReq);
|
|
789
|
+
const { headHtml: wHead, status: wStatus } = await head;
|
|
790
|
+
const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
|
|
791
|
+
const responseStream = new ReadableStream({
|
|
792
|
+
async start(controller) {
|
|
793
|
+
controller.enqueue(encoder.encode(precontentHtml));
|
|
794
|
+
const reader = stream.getReader();
|
|
795
|
+
while (true) {
|
|
796
|
+
const { done, value } = await reader.read();
|
|
797
|
+
if (done) break;
|
|
798
|
+
controller.enqueue(value);
|
|
799
|
+
}
|
|
800
|
+
controller.enqueue(encoder.encode(postContent));
|
|
801
|
+
controller.close();
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
return new Response(responseStream, {
|
|
805
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
806
|
+
status: wStatus,
|
|
807
|
+
});
|
|
808
|
+
} else {
|
|
809
|
+
const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
|
|
810
|
+
const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
|
|
811
|
+
return new Response(precontentHtml + html + postContent, {
|
|
812
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
813
|
+
status: wStatus,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
765
818
|
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
766
819
|
document: {
|
|
767
820
|
body: Component as React.FC<HadarsProps<object>>,
|
|
@@ -772,6 +825,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
772
825
|
},
|
|
773
826
|
});
|
|
774
827
|
|
|
775
|
-
|
|
828
|
+
const unsuspend = (renderPayload.appProps.context as any)?._unsuspend ?? null;
|
|
829
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, options.streaming === true, unsuspend);
|
|
776
830
|
}, options.websocket);
|
|
777
831
|
};
|
package/src/ssr-render-worker.ts
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SSR render worker — runs in a node:worker_threads thread.
|
|
3
3
|
*
|
|
4
|
-
* Handles
|
|
4
|
+
* Handles one message type sent by RenderWorkerPool in build.ts:
|
|
5
5
|
*
|
|
6
|
-
* { type: '
|
|
7
|
-
* →
|
|
8
|
-
* → postMessage({ id, type: 'staticMarkup', html, context: props.context })
|
|
9
|
-
*
|
|
10
|
-
* { type: 'renderString', id, appProps, clientProps }
|
|
6
|
+
* { type: 'renderFull', id, streaming: false, request: SerializableRequest }
|
|
7
|
+
* → runs full lifecycle (getInitProps → render loop → getAfterRenderProps → getFinalProps)
|
|
11
8
|
* → renderToString(ReactPage)
|
|
12
|
-
* → postMessage({ id, html })
|
|
9
|
+
* → postMessage({ id, html, headHtml, status })
|
|
13
10
|
*
|
|
14
|
-
* { type: '
|
|
15
|
-
* →
|
|
11
|
+
* { type: 'renderFull', id, streaming: true, request: SerializableRequest }
|
|
12
|
+
* → runs full lifecycle
|
|
13
|
+
* → renderToReadableStream(ReactPage), streams chunks back
|
|
14
|
+
* → postMessage({ id, type: 'head', headHtml, status })
|
|
16
15
|
* → postMessage({ id, type: 'chunk', chunk }) × N
|
|
17
16
|
* → postMessage({ id, type: 'done' })
|
|
18
17
|
*
|
|
19
18
|
* The SSR bundle path is passed once via workerData at thread creation time so
|
|
20
|
-
* the
|
|
19
|
+
* the SSR module is only imported once per worker lifetime.
|
|
21
20
|
*/
|
|
22
21
|
|
|
23
22
|
import { workerData, parentPort } from 'node:worker_threads';
|
|
@@ -33,10 +32,11 @@ let _React: any = null;
|
|
|
33
32
|
let _renderToStaticMarkup: ((element: any) => string) | null = null;
|
|
34
33
|
let _renderToString: ((element: any) => string) | null = null;
|
|
35
34
|
let _renderToReadableStream: ((element: any, options?: any) => Promise<ReadableStream<Uint8Array>>) | null = null;
|
|
36
|
-
|
|
35
|
+
// Full SSR module — includes default (App component) + lifecycle exports.
|
|
36
|
+
let _ssrMod: any = null;
|
|
37
37
|
|
|
38
38
|
async function init() {
|
|
39
|
-
if (_React &&
|
|
39
|
+
if (_React && _ssrMod) return;
|
|
40
40
|
|
|
41
41
|
const req = createRequire(pathMod.resolve(process.cwd(), '__ninety_fake__.js'));
|
|
42
42
|
|
|
@@ -59,19 +59,59 @@ async function init() {
|
|
|
59
59
|
_renderToReadableStream = browserMod.renderToReadableStream;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
if (!
|
|
63
|
-
|
|
64
|
-
_Component = ssrMod.default;
|
|
62
|
+
if (!_ssrMod) {
|
|
63
|
+
_ssrMod = await import(pathToFileURL(ssrBundlePath).href);
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
export type SerializableRequest = {
|
|
68
|
+
url: string;
|
|
69
|
+
method: string;
|
|
70
|
+
headers: Record<string, string>;
|
|
71
|
+
body: Uint8Array | null;
|
|
72
|
+
pathname: string;
|
|
73
|
+
search: string;
|
|
74
|
+
location: string;
|
|
75
|
+
cookies: Record<string, string>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function deserializeRequest(s: SerializableRequest): any {
|
|
79
|
+
const init: RequestInit = { method: s.method, headers: new Headers(s.headers) };
|
|
80
|
+
if (s.body) init.body = s.body;
|
|
81
|
+
const req = new Request(s.url, init);
|
|
82
|
+
Object.assign(req, {
|
|
83
|
+
pathname: s.pathname,
|
|
84
|
+
search: s.search,
|
|
85
|
+
location: s.location,
|
|
86
|
+
cookies: s.cookies,
|
|
87
|
+
});
|
|
88
|
+
return req;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildHeadHtml(head: any): string {
|
|
92
|
+
const R = _React;
|
|
93
|
+
const metaEntries = Object.entries(head.meta ?? {});
|
|
94
|
+
const linkEntries = Object.entries(head.link ?? {});
|
|
95
|
+
const styleEntries = Object.entries(head.style ?? {});
|
|
96
|
+
const scriptEntries = Object.entries(head.script ?? {});
|
|
97
|
+
return _renderToStaticMarkup!(
|
|
98
|
+
R.createElement(R.Fragment, null,
|
|
99
|
+
R.createElement('title', null, head.title),
|
|
100
|
+
...metaEntries.map(([id, opts]) => R.createElement('meta', { key: id, id, ...(opts as any) })),
|
|
101
|
+
...linkEntries.map(([id, opts]) => R.createElement('link', { key: id, id, ...(opts as any) })),
|
|
102
|
+
...styleEntries.map(([id, opts]) => R.createElement('style', { key: id, id, ...(opts as any) })),
|
|
103
|
+
...scriptEntries.map(([id, opts]) => R.createElement('script', { key: id, id, ...(opts as any) })),
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildReactPage(appProps: any, clientProps: any) {
|
|
109
|
+
const R = _React;
|
|
110
|
+
const Component = _ssrMod.default;
|
|
71
111
|
return R.createElement(
|
|
72
112
|
R.Fragment, null,
|
|
73
113
|
R.createElement('div', { id: 'app' },
|
|
74
|
-
R.createElement(
|
|
114
|
+
R.createElement(Component, appProps),
|
|
75
115
|
),
|
|
76
116
|
R.createElement('script', {
|
|
77
117
|
id: 'hadars',
|
|
@@ -83,53 +123,119 @@ function buildReactPage(R: any, appProps: Record<string, unknown>, clientProps:
|
|
|
83
123
|
);
|
|
84
124
|
}
|
|
85
125
|
|
|
126
|
+
async function runFullLifecycle(serialReq: SerializableRequest) {
|
|
127
|
+
const R = _React;
|
|
128
|
+
const Component = _ssrMod.default;
|
|
129
|
+
const { getInitProps, getAfterRenderProps, getFinalProps } = _ssrMod;
|
|
130
|
+
|
|
131
|
+
const parsedReq = deserializeRequest(serialReq);
|
|
132
|
+
|
|
133
|
+
const unsuspend: any = { cache: new Map(), hasPending: false };
|
|
134
|
+
const context: any = {
|
|
135
|
+
head: { title: 'Hadars App', meta: {}, link: {}, style: {}, script: {}, status: 200 },
|
|
136
|
+
_unsuspend: unsuspend,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
let props: any = {
|
|
140
|
+
...(getInitProps ? await getInitProps(parsedReq) : {}),
|
|
141
|
+
location: serialReq.location,
|
|
142
|
+
context,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// useServerData render loop — same logic as getReactResponse on main thread.
|
|
146
|
+
let html = '';
|
|
147
|
+
let iters = 0;
|
|
148
|
+
do {
|
|
149
|
+
unsuspend.hasPending = false;
|
|
150
|
+
try {
|
|
151
|
+
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
152
|
+
html = _renderToStaticMarkup!(R.createElement(Component, props));
|
|
153
|
+
} finally {
|
|
154
|
+
(globalThis as any).__hadarsUnsuspend = null;
|
|
155
|
+
}
|
|
156
|
+
if (unsuspend.hasPending) {
|
|
157
|
+
const pending = [...unsuspend.cache.values()]
|
|
158
|
+
.filter((e: any) => e.status === 'pending')
|
|
159
|
+
.map((e: any) => e.promise);
|
|
160
|
+
await Promise.all(pending);
|
|
161
|
+
}
|
|
162
|
+
} while (unsuspend.hasPending && ++iters < 25);
|
|
163
|
+
|
|
164
|
+
props = getAfterRenderProps ? await getAfterRenderProps(props, html) : props;
|
|
165
|
+
|
|
166
|
+
// Re-render to capture head changes from getAfterRenderProps.
|
|
167
|
+
try {
|
|
168
|
+
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
169
|
+
_renderToStaticMarkup!(R.createElement(Component, { ...props, location: serialReq.location, context }));
|
|
170
|
+
} finally {
|
|
171
|
+
(globalThis as any).__hadarsUnsuspend = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Collect resolved useServerData values for client hydration.
|
|
175
|
+
const serverData: Record<string, unknown> = {};
|
|
176
|
+
for (const [k, v] of unsuspend.cache) {
|
|
177
|
+
if ((v as any).status === 'fulfilled') serverData[k] = (v as any).value;
|
|
178
|
+
if ((v as any).status === 'suspense-cached') serverData[k] = (v as any).value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { context: _ctx, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
|
|
182
|
+
const clientProps = {
|
|
183
|
+
...restProps,
|
|
184
|
+
location: serialReq.location,
|
|
185
|
+
...(Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const headHtml = buildHeadHtml(context.head);
|
|
189
|
+
const status: number = context.head.status ?? 200;
|
|
190
|
+
const finalAppProps = { ...props, location: serialReq.location, context };
|
|
191
|
+
|
|
192
|
+
return { finalAppProps, clientProps, headHtml, status, unsuspend };
|
|
193
|
+
}
|
|
194
|
+
|
|
86
195
|
parentPort!.on('message', async (msg: any) => {
|
|
87
|
-
const { id, type } = msg;
|
|
196
|
+
const { id, type, request, streaming } = msg;
|
|
88
197
|
try {
|
|
89
198
|
await init();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
199
|
+
|
|
200
|
+
if (type !== 'renderFull') return;
|
|
201
|
+
|
|
202
|
+
const { finalAppProps, clientProps, headHtml, status, unsuspend } =
|
|
203
|
+
await runFullLifecycle(request as SerializableRequest);
|
|
204
|
+
|
|
205
|
+
const ReactPage = buildReactPage(finalAppProps, clientProps);
|
|
206
|
+
|
|
207
|
+
if (streaming) {
|
|
208
|
+
parentPort!.postMessage({ id, type: 'head', headHtml, status });
|
|
209
|
+
let streamPromise: Promise<ReadableStream<Uint8Array>>;
|
|
210
|
+
try {
|
|
211
|
+
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
212
|
+
streamPromise = _renderToReadableStream!(ReactPage);
|
|
213
|
+
} finally {
|
|
214
|
+
(globalThis as any).__hadarsUnsuspend = null;
|
|
215
|
+
}
|
|
216
|
+
const stream = await streamPromise;
|
|
107
217
|
const reader = stream.getReader();
|
|
108
218
|
while (true) {
|
|
109
219
|
const { done, value } = await reader.read();
|
|
110
220
|
if (done) break;
|
|
111
|
-
// Transfer the underlying ArrayBuffer to avoid a copy across threads.
|
|
112
221
|
parentPort!.postMessage({ id, type: 'chunk', chunk: value }, [value.buffer as ArrayBuffer]);
|
|
113
222
|
}
|
|
114
223
|
parentPort!.postMessage({ id, type: 'done' });
|
|
115
|
-
|
|
224
|
+
} else {
|
|
225
|
+
let html: string;
|
|
226
|
+
try {
|
|
227
|
+
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
228
|
+
html = _renderToString!(ReactPage);
|
|
229
|
+
} finally {
|
|
230
|
+
(globalThis as any).__hadarsUnsuspend = null;
|
|
231
|
+
}
|
|
232
|
+
parentPort!.postMessage({ id, html, headHtml, status });
|
|
116
233
|
}
|
|
117
234
|
|
|
118
|
-
// ── renderString (type === 'renderString' or legacy messages) ────────────
|
|
119
|
-
const { appProps, clientProps } = msg as {
|
|
120
|
-
appProps: Record<string, unknown>;
|
|
121
|
-
clientProps: Record<string, unknown>;
|
|
122
|
-
};
|
|
123
|
-
const ReactPage = buildReactPage(R, appProps, clientProps);
|
|
124
|
-
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
125
|
-
const html = _renderToString!(ReactPage);
|
|
126
|
-
(globalThis as any).__hadarsUnsuspend = null;
|
|
127
|
-
parentPort!.postMessage({ id, html });
|
|
128
|
-
|
|
129
235
|
} catch (err: any) {
|
|
130
236
|
(globalThis as any).__hadarsUnsuspend = null;
|
|
131
237
|
const errMsg = err?.message ?? String(err);
|
|
132
|
-
if (
|
|
238
|
+
if (streaming) {
|
|
133
239
|
parentPort!.postMessage({ id, type: 'error', error: errMsg });
|
|
134
240
|
} else {
|
|
135
241
|
parentPort!.postMessage({ id, error: errMsg });
|