@mandujs/core 0.9.23 → 0.9.24
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/package.json +1 -1
- package/src/runtime/streaming-ssr.ts +69 -24
package/package.json
CHANGED
|
@@ -96,7 +96,7 @@ export interface StreamingSSROptions {
|
|
|
96
96
|
hmrPort?: number;
|
|
97
97
|
/** Client-side Router 활성화 */
|
|
98
98
|
enableClientRouter?: boolean;
|
|
99
|
-
/** Streaming 타임아웃 (ms) */
|
|
99
|
+
/** Streaming 타임아웃 (ms) - 전체 스트림 최대 시간 */
|
|
100
100
|
streamTimeout?: number;
|
|
101
101
|
/** Shell 렌더링 후 콜백 (TTFB 측정 시점) */
|
|
102
102
|
onShellReady?: () => void;
|
|
@@ -626,11 +626,14 @@ export async function renderToStream(
|
|
|
626
626
|
: generateHTMLTail(options);
|
|
627
627
|
|
|
628
628
|
let shellSent = false;
|
|
629
|
+
let timedOut = false;
|
|
629
630
|
|
|
630
631
|
// React renderToReadableStream 호출
|
|
631
632
|
// 실패 시 throw → renderStreamingResponse에서 500 처리
|
|
632
633
|
const reactStream = await renderToReadableStream(element, {
|
|
633
634
|
onError: (error: Error) => {
|
|
635
|
+
if (timedOut) return;
|
|
636
|
+
|
|
634
637
|
metrics.hasError = true;
|
|
635
638
|
const streamingError: StreamingError = {
|
|
636
639
|
error,
|
|
@@ -665,18 +668,44 @@ export async function renderToStream(
|
|
|
665
668
|
|
|
666
669
|
// Custom stream으로 래핑 (Shell + React Content + Tail)
|
|
667
670
|
let tailSent = false;
|
|
668
|
-
let streamTimedOut = false;
|
|
669
671
|
const reader = reactStream.getReader();
|
|
672
|
+
const deadline = streamTimeout && streamTimeout > 0
|
|
673
|
+
? metrics.startTime + streamTimeout
|
|
674
|
+
: null;
|
|
670
675
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
676
|
+
async function readWithTimeout(): Promise<ReadableStreamReadResult<Uint8Array> | null> {
|
|
677
|
+
if (!deadline) {
|
|
678
|
+
return reader.read();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const remaining = deadline - Date.now();
|
|
682
|
+
if (remaining <= 0) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
687
|
+
const timeoutPromise = new Promise<{ kind: "timeout" }>((resolve) => {
|
|
688
|
+
timeoutId = setTimeout(() => resolve({ kind: "timeout" }), remaining);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const readPromise = reader
|
|
692
|
+
.read()
|
|
693
|
+
.then((result) => ({ kind: "read" as const, result }))
|
|
694
|
+
.catch((error) => ({ kind: "error" as const, error }));
|
|
695
|
+
|
|
696
|
+
const result = await Promise.race([readPromise, timeoutPromise]);
|
|
697
|
+
|
|
698
|
+
if (result.kind === "timeout") {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
703
|
+
|
|
704
|
+
if (result.kind === "error") {
|
|
705
|
+
throw result.error;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return result.result;
|
|
680
709
|
}
|
|
681
710
|
|
|
682
711
|
return new ReadableStream<Uint8Array>({
|
|
@@ -690,10 +719,16 @@ export async function renderToStream(
|
|
|
690
719
|
|
|
691
720
|
async pull(controller) {
|
|
692
721
|
try {
|
|
693
|
-
|
|
694
|
-
|
|
722
|
+
const readResult = await readWithTimeout();
|
|
723
|
+
|
|
724
|
+
// 타임아웃 발생
|
|
725
|
+
if (!readResult) {
|
|
695
726
|
const timeoutError = new Error(`Stream timeout: exceeded ${streamTimeout}ms`);
|
|
696
727
|
metrics.hasError = true;
|
|
728
|
+
timedOut = true;
|
|
729
|
+
if (isDev) {
|
|
730
|
+
console.warn(`[Mandu Streaming] Stream timeout after ${streamTimeout}ms`);
|
|
731
|
+
}
|
|
697
732
|
|
|
698
733
|
const streamingError: StreamingError = {
|
|
699
734
|
error: timeoutError,
|
|
@@ -712,16 +747,18 @@ export async function renderToStream(
|
|
|
712
747
|
onMetrics?.(metrics);
|
|
713
748
|
}
|
|
714
749
|
controller.close();
|
|
715
|
-
|
|
750
|
+
try {
|
|
751
|
+
const cancelPromise = reader.cancel();
|
|
752
|
+
if (cancelPromise) {
|
|
753
|
+
cancelPromise.catch(() => {});
|
|
754
|
+
}
|
|
755
|
+
} catch {}
|
|
716
756
|
return;
|
|
717
757
|
}
|
|
718
758
|
|
|
719
|
-
const { done, value } =
|
|
759
|
+
const { done, value } = readResult;
|
|
720
760
|
|
|
721
761
|
if (done) {
|
|
722
|
-
// 타이머 정리
|
|
723
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
724
|
-
|
|
725
762
|
if (!tailSent) {
|
|
726
763
|
controller.enqueue(encoder.encode(htmlTail));
|
|
727
764
|
tailSent = true;
|
|
@@ -739,9 +776,6 @@ export async function renderToStream(
|
|
|
739
776
|
// React 컨텐츠를 그대로 스트리밍
|
|
740
777
|
controller.enqueue(value);
|
|
741
778
|
} catch (error) {
|
|
742
|
-
// 타이머 정리
|
|
743
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
744
|
-
|
|
745
779
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
746
780
|
metrics.hasError = true;
|
|
747
781
|
|
|
@@ -769,8 +803,12 @@ export async function renderToStream(
|
|
|
769
803
|
},
|
|
770
804
|
|
|
771
805
|
cancel() {
|
|
772
|
-
|
|
773
|
-
|
|
806
|
+
try {
|
|
807
|
+
const cancelPromise = reader.cancel();
|
|
808
|
+
if (cancelPromise) {
|
|
809
|
+
cancelPromise.catch(() => {});
|
|
810
|
+
}
|
|
811
|
+
} catch {}
|
|
774
812
|
},
|
|
775
813
|
});
|
|
776
814
|
}
|
|
@@ -879,6 +917,7 @@ export async function renderWithDeferredData(
|
|
|
879
917
|
isDev = false,
|
|
880
918
|
...restOptions
|
|
881
919
|
} = options;
|
|
920
|
+
const streamTimeout = options.streamTimeout;
|
|
882
921
|
|
|
883
922
|
const encoder = new TextEncoder();
|
|
884
923
|
const startTime = Date.now();
|
|
@@ -940,7 +979,13 @@ export async function renderWithDeferredData(
|
|
|
940
979
|
// base stream 완료 후, deferred가 아직 안 끝났으면 잠시 대기
|
|
941
980
|
// (단, deferredTimeout 내에서만)
|
|
942
981
|
if (!allDeferredSettled) {
|
|
943
|
-
const
|
|
982
|
+
const elapsed = Date.now() - startTime;
|
|
983
|
+
let remainingTime = deferredTimeout - elapsed;
|
|
984
|
+
if (streamTimeout && streamTimeout > 0) {
|
|
985
|
+
const remainingStream = streamTimeout - elapsed;
|
|
986
|
+
remainingTime = Math.min(remainingTime, remainingStream);
|
|
987
|
+
}
|
|
988
|
+
remainingTime = Math.max(0, remainingTime);
|
|
944
989
|
if (remainingTime > 0) {
|
|
945
990
|
await Promise.race([
|
|
946
991
|
deferredSettledPromise,
|