@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.9.23",
3
+ "version": "0.9.24",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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
- let timeoutId: ReturnType<typeof setTimeout> | null = null;
673
- if (streamTimeout && streamTimeout > 0) {
674
- timeoutId = setTimeout(() => {
675
- streamTimedOut = true;
676
- if (isDev) {
677
- console.warn(`[Mandu Streaming] Stream timeout after ${streamTimeout}ms`);
678
- }
679
- }, streamTimeout);
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
- if (streamTimedOut) {
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
- reader.cancel();
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 } = await reader.read();
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
- if (timeoutId) clearTimeout(timeoutId);
773
- reader.cancel();
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 remainingTime = Math.max(0, deferredTimeout - (Date.now() - startTime));
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,