@rscc/common-react 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeonghyeon-Ryu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @rscc/common-react
2
+
3
+ RSCC 공통 React 훅 — `useDebounce`, `useSse`. [@rscc/common-core](https://www.npmjs.com/package/@rscc/common-core) 의 SSE 파서(`readSseStream`)를 React 상태로 래핑한다.
4
+
5
+ ## 설치
6
+
7
+ ```bash
8
+ npm i @rscc/common-react
9
+ ```
10
+
11
+ - **peerDependencies: `react >= 18`** — 소비 앱이 직접 설치
12
+ - `@rscc/common-core` 는 `^` 범위 의존으로 함께 설치된다 (두 패키지는 락스텝 버전)
13
+ - **Node >= 18**, ESM + CJS 듀얼 빌드, 타입 선언 동봉
14
+
15
+ ## 훅
16
+
17
+ | 훅 | 시그니처 | 설명 |
18
+ |---|---|---|
19
+ | `useDebounce` | `useDebounce<T>(value, delayMs = 200): T` | 마지막 안정 값만 통과 — 자동완성 API 호출 절감 |
20
+ | `useSse` | `useSse(options?)` → `{ start, cancel, text, sources, conversationId, error, isStreaming }` | SSE 구독 상태 훅 — 언마운트 시 자동 중단 |
21
+
22
+ ## 사용 예시
23
+
24
+ ### useDebounce
25
+
26
+ ```ts
27
+ import { useDebounce } from "@rscc/common-react";
28
+
29
+ const debounced = useDebounce(keyword, 200);
30
+ // debounced 가 바뀔 때만 자동완성 API 호출
31
+ ```
32
+
33
+ ### useSse
34
+
35
+ ```ts
36
+ import { useSse } from "@rscc/common-react";
37
+
38
+ const { start, cancel, text, sources, conversationId, error, isStreaming } = useSse();
39
+
40
+ await start("/api/v1/chat/stream", { method: "POST", headers, body });
41
+ // text: delta 누적 텍스트, sources: RAG 근거, error: in-band 오류 메시지
42
+ // cancel(): 중단 — 그때까지의 text 는 보존되고 error 는 남지 않는다
43
+ ```
44
+
45
+ - 구독 전 검증 실패(`!res.ok`)는 응답 봉투의 `message` 를 `error` 로 노출한다.
46
+ - `start` 재호출 시 이전 스트림을 중단하고 상태를 초기화한다.
47
+ - 한계: `[DONE]` 정상 종료와 끊긴 스트림을 구분하는 신호가 없다 — 둘 다 `isStreaming=false`, `error=null` 로 끝난다.
48
+
49
+ ## 문서 / 저장소
50
+
51
+ - 상세 문서: [js/README.md](https://github.com/Jeonghyeon-Ryu/r-common/blob/master/js/README.md)
52
+ - SSE 프레임 계약: [contracts/sse-frames.md](https://github.com/Jeonghyeon-Ryu/r-common/blob/master/contracts/sse-frames.md)
53
+ - 저장소: [Jeonghyeon-Ryu/r-common](https://github.com/Jeonghyeon-Ryu/r-common) · MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ useDebounce: () => useDebounce,
24
+ useSse: () => useSse
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/useDebounce.ts
29
+ var import_react = require("react");
30
+ function useDebounce(value, delayMs = 200) {
31
+ const [debounced, setDebounced] = (0, import_react.useState)(value);
32
+ (0, import_react.useEffect)(() => {
33
+ const t = setTimeout(() => setDebounced(value), delayMs);
34
+ return () => clearTimeout(t);
35
+ }, [value, delayMs]);
36
+ return debounced;
37
+ }
38
+
39
+ // src/useSse.ts
40
+ var import_react2 = require("react");
41
+ var import_common_core = require("@rscc/common-core");
42
+ var RECONNECT_MARKER = new Error("rscc-sse-reconnect");
43
+ function useSse(options = {}) {
44
+ const { fetchImpl, reconnect } = options;
45
+ const [text, setText] = (0, import_react2.useState)("");
46
+ const [sources, setSources] = (0, import_react2.useState)(null);
47
+ const [conversationId, setConversationId] = (0, import_react2.useState)(null);
48
+ const [error, setError] = (0, import_react2.useState)(null);
49
+ const [isStreaming, setIsStreaming] = (0, import_react2.useState)(false);
50
+ const [finishReason, setFinishReason] = (0, import_react2.useState)(null);
51
+ const abortRef = (0, import_react2.useRef)(null);
52
+ const cancel = (0, import_react2.useCallback)(() => {
53
+ abortRef.current?.abort();
54
+ }, []);
55
+ const start = (0, import_react2.useCallback)(
56
+ async (url, init) => {
57
+ abortRef.current?.abort();
58
+ const controller = new AbortController();
59
+ abortRef.current = controller;
60
+ const resetStreamState = () => {
61
+ setText("");
62
+ setSources(null);
63
+ setConversationId(null);
64
+ setError(null);
65
+ };
66
+ resetStreamState();
67
+ setFinishReason(null);
68
+ setIsStreaming(true);
69
+ const fetchFn = fetchImpl ?? globalThis.fetch;
70
+ let reason = "interrupted";
71
+ const attemptStream = async (attempt) => {
72
+ if (attempt > 0) resetStreamState();
73
+ let done = false;
74
+ let sawErrorFrame = false;
75
+ let streamStarted = false;
76
+ try {
77
+ const res = await fetchFn(url, { ...init, signal: controller.signal });
78
+ if (!res.ok) {
79
+ const json = await res.json().catch(() => null);
80
+ setError(typeof json?.message === "string" ? json.message : `HTTP ${res.status}`);
81
+ return;
82
+ }
83
+ streamStarted = true;
84
+ await (0, import_common_core.readSseStream)(res, {
85
+ onConversationId: setConversationId,
86
+ onSources: setSources,
87
+ onDelta: (d) => setText((prev) => prev + d),
88
+ onError: (m) => {
89
+ sawErrorFrame = true;
90
+ setError(m);
91
+ },
92
+ onDone: () => {
93
+ done = true;
94
+ }
95
+ });
96
+ } catch (err) {
97
+ if (controller.signal.aborted) {
98
+ reason = "aborted";
99
+ return;
100
+ }
101
+ setError(err instanceof Error ? err.message : String(err));
102
+ if (streamStarted && !sawErrorFrame && reconnect) throw RECONNECT_MARKER;
103
+ return;
104
+ }
105
+ if (controller.signal.aborted) {
106
+ reason = "aborted";
107
+ return;
108
+ }
109
+ if (done) {
110
+ reason = "done";
111
+ return;
112
+ }
113
+ if (!sawErrorFrame && reconnect) throw RECONNECT_MARKER;
114
+ };
115
+ try {
116
+ if (reconnect) {
117
+ await (0, import_common_core.retry)(attemptStream, {
118
+ retries: reconnect.retries ?? 3,
119
+ minDelayMs: reconnect.minDelayMs ?? 1e3,
120
+ maxDelayMs: reconnect.maxDelayMs ?? 15e3,
121
+ factor: reconnect.factor ?? 2,
122
+ jitter: reconnect.jitter ?? true,
123
+ shouldRetry: (e) => e === RECONNECT_MARKER,
124
+ signal: controller.signal
125
+ });
126
+ } else {
127
+ await attemptStream(0);
128
+ }
129
+ } catch (err) {
130
+ if (controller.signal.aborted) {
131
+ reason = "aborted";
132
+ } else if (err !== RECONNECT_MARKER) {
133
+ setError(err instanceof Error ? err.message : String(err));
134
+ }
135
+ } finally {
136
+ if (abortRef.current === controller) {
137
+ abortRef.current = null;
138
+ setIsStreaming(false);
139
+ setFinishReason(reason);
140
+ }
141
+ }
142
+ },
143
+ [fetchImpl, reconnect]
144
+ );
145
+ (0, import_react2.useEffect)(() => () => abortRef.current?.abort(), []);
146
+ return { start, cancel, text, sources, conversationId, error, isStreaming, finishReason };
147
+ }
148
+ // Annotate the CommonJS export names for ESM import in node:
149
+ 0 && (module.exports = {
150
+ useDebounce,
151
+ useSse
152
+ });
@@ -0,0 +1,82 @@
1
+ import { SseSource } from '@rscc/common-core';
2
+
3
+ /**
4
+ * value 가 delayMs(ms) 동안 더 이상 바뀌지 않으면 그 값을 반환한다.
5
+ * 빠른 연속 입력(타이핑)에서 마지막 안정된 값만 통과시켜 자동완성 호출 수를 줄인다.
6
+ */
7
+ declare function useDebounce<T>(value: T, delayMs?: number): T;
8
+
9
+ /**
10
+ * 직전 스트림의 종료 사유.
11
+ *
12
+ * - `'done'`: [DONE] 종료 마커 수신 (in-band error 프레임 후의 [DONE] 도 포함 —
13
+ * error 상태와 공존).
14
+ * - `'aborted'`: cancel() / 새 start() 대체 / 언마운트로 인한 중단.
15
+ * - `'interrupted'`: [DONE] 없이 끝난 모든 비정상 종료 — 무언 EOF, 전송 오류,
16
+ * 구독 전 검증 실패(!res.ok) 포함.
17
+ */
18
+ type SseFinishReason = "done" | "aborted" | "interrupted";
19
+ /**
20
+ * 자동 재연결 옵션 (core retry() 의 백오프 파라미터 부분집합).
21
+ *
22
+ * 재연결 트리거는 "스트림 시작(res.ok) 후 [DONE] 없이 끊긴 경우"뿐이다 —
23
+ * 'done'/'aborted'/구독 전 실패(!res.ok)/in-band error 프레임 종료는 서버 의도로
24
+ * 보고 재연결하지 않는다. 각 시도는 **전체 재요청**이며 직전 시도의
25
+ * text/sources/conversationId/error 상태를 초기화한다 (중복 누적 방지).
26
+ * 이어받기(Last-Event-ID)는 비범위 — sse-frames.md 가 `event:`/`id:`/`retry:`
27
+ * 필드를 사용하지 않는다고 명시하므로 재개 지점이 존재하지 않는다.
28
+ */
29
+ interface SseReconnectOptions {
30
+ /** 최대 재연결 시도 횟수 (총 연결 = retries + 1). 기본 3. */
31
+ retries?: number;
32
+ /** 백오프 base(ms). 기본 1000. */
33
+ minDelayMs?: number;
34
+ /** 회당 대기 상한(ms). 기본 15000. */
35
+ maxDelayMs?: number;
36
+ /** 지수 배수. 기본 2. */
37
+ factor?: number;
38
+ /** full jitter 적용 여부. 기본 true. */
39
+ jitter?: boolean;
40
+ }
41
+ interface UseSseOptions {
42
+ /** fetch 구현체 주입 (테스트용). 기본 globalThis.fetch. */
43
+ fetchImpl?: typeof fetch;
44
+ /**
45
+ * 자동 재연결 — 기본 undefined = off (옵트인. 비멱등 POST 스트림은 전체 재요청이
46
+ * 서버 측 중복 생성으로 이어질 수 있으므로 멱등성이 보장될 때만 켤 것).
47
+ */
48
+ reconnect?: SseReconnectOptions;
49
+ }
50
+ interface UseSseReturn {
51
+ /**
52
+ * SSE 스트림 구독 시작. 이전 스트림이 진행 중이면 중단 후 상태를 초기화한다.
53
+ * 반환 Promise 는 스트림 종료([DONE]/중단/오류) 시 resolve — 오류는 error 상태로 노출되며 reject 하지 않는다.
54
+ */
55
+ start: (url: string, init?: RequestInit) => Promise<void>;
56
+ /** 진행 중인 스트림 중단. */
57
+ cancel: () => void;
58
+ /** delta 프레임 누적 텍스트. */
59
+ text: string;
60
+ /** sources 프레임 (RAG 근거). 미수신 시 null. */
61
+ sources: SseSource[] | null;
62
+ /** conversationId 프레임 값. 미수신 시 null. */
63
+ conversationId: number | null;
64
+ /** in-band error 프레임 메시지 또는 구독 전/전송 오류 메시지. */
65
+ error: string | null;
66
+ isStreaming: boolean;
67
+ /** 직전 스트림의 종료 사유. 스트리밍 중/시작 전엔 null. */
68
+ finishReason: SseFinishReason | null;
69
+ }
70
+ /**
71
+ * @rscc/common-core 의 readSseStream 을 감싼 상태 훅 (contracts/sse-frames.md).
72
+ *
73
+ * - 구독 전 검증 실패(!res.ok — CommonResponse JSON 에러 경로)는 봉투의 message 를 error 로 노출.
74
+ * - 스트림 시작 후 오류는 in-band error 프레임으로 수신되어 동일하게 error 로 노출.
75
+ * - 언마운트 시 진행 중 스트림을 자동 중단한다.
76
+ * - 종료 사유는 finishReason 으로 구분한다 — 'done'([DONE] 수신) / 'aborted'(중단) /
77
+ * 'interrupted'([DONE] 없이 끊김). reconnect 옵트인 시 'interrupted' 성 종료 중
78
+ * "스트림 시작 후 끊김"만 지수 백오프로 전체 재요청한다 ({@link SseReconnectOptions}).
79
+ */
80
+ declare function useSse(options?: UseSseOptions): UseSseReturn;
81
+
82
+ export { type SseFinishReason, type SseReconnectOptions, type UseSseOptions, type UseSseReturn, useDebounce, useSse };
@@ -0,0 +1,82 @@
1
+ import { SseSource } from '@rscc/common-core';
2
+
3
+ /**
4
+ * value 가 delayMs(ms) 동안 더 이상 바뀌지 않으면 그 값을 반환한다.
5
+ * 빠른 연속 입력(타이핑)에서 마지막 안정된 값만 통과시켜 자동완성 호출 수를 줄인다.
6
+ */
7
+ declare function useDebounce<T>(value: T, delayMs?: number): T;
8
+
9
+ /**
10
+ * 직전 스트림의 종료 사유.
11
+ *
12
+ * - `'done'`: [DONE] 종료 마커 수신 (in-band error 프레임 후의 [DONE] 도 포함 —
13
+ * error 상태와 공존).
14
+ * - `'aborted'`: cancel() / 새 start() 대체 / 언마운트로 인한 중단.
15
+ * - `'interrupted'`: [DONE] 없이 끝난 모든 비정상 종료 — 무언 EOF, 전송 오류,
16
+ * 구독 전 검증 실패(!res.ok) 포함.
17
+ */
18
+ type SseFinishReason = "done" | "aborted" | "interrupted";
19
+ /**
20
+ * 자동 재연결 옵션 (core retry() 의 백오프 파라미터 부분집합).
21
+ *
22
+ * 재연결 트리거는 "스트림 시작(res.ok) 후 [DONE] 없이 끊긴 경우"뿐이다 —
23
+ * 'done'/'aborted'/구독 전 실패(!res.ok)/in-band error 프레임 종료는 서버 의도로
24
+ * 보고 재연결하지 않는다. 각 시도는 **전체 재요청**이며 직전 시도의
25
+ * text/sources/conversationId/error 상태를 초기화한다 (중복 누적 방지).
26
+ * 이어받기(Last-Event-ID)는 비범위 — sse-frames.md 가 `event:`/`id:`/`retry:`
27
+ * 필드를 사용하지 않는다고 명시하므로 재개 지점이 존재하지 않는다.
28
+ */
29
+ interface SseReconnectOptions {
30
+ /** 최대 재연결 시도 횟수 (총 연결 = retries + 1). 기본 3. */
31
+ retries?: number;
32
+ /** 백오프 base(ms). 기본 1000. */
33
+ minDelayMs?: number;
34
+ /** 회당 대기 상한(ms). 기본 15000. */
35
+ maxDelayMs?: number;
36
+ /** 지수 배수. 기본 2. */
37
+ factor?: number;
38
+ /** full jitter 적용 여부. 기본 true. */
39
+ jitter?: boolean;
40
+ }
41
+ interface UseSseOptions {
42
+ /** fetch 구현체 주입 (테스트용). 기본 globalThis.fetch. */
43
+ fetchImpl?: typeof fetch;
44
+ /**
45
+ * 자동 재연결 — 기본 undefined = off (옵트인. 비멱등 POST 스트림은 전체 재요청이
46
+ * 서버 측 중복 생성으로 이어질 수 있으므로 멱등성이 보장될 때만 켤 것).
47
+ */
48
+ reconnect?: SseReconnectOptions;
49
+ }
50
+ interface UseSseReturn {
51
+ /**
52
+ * SSE 스트림 구독 시작. 이전 스트림이 진행 중이면 중단 후 상태를 초기화한다.
53
+ * 반환 Promise 는 스트림 종료([DONE]/중단/오류) 시 resolve — 오류는 error 상태로 노출되며 reject 하지 않는다.
54
+ */
55
+ start: (url: string, init?: RequestInit) => Promise<void>;
56
+ /** 진행 중인 스트림 중단. */
57
+ cancel: () => void;
58
+ /** delta 프레임 누적 텍스트. */
59
+ text: string;
60
+ /** sources 프레임 (RAG 근거). 미수신 시 null. */
61
+ sources: SseSource[] | null;
62
+ /** conversationId 프레임 값. 미수신 시 null. */
63
+ conversationId: number | null;
64
+ /** in-band error 프레임 메시지 또는 구독 전/전송 오류 메시지. */
65
+ error: string | null;
66
+ isStreaming: boolean;
67
+ /** 직전 스트림의 종료 사유. 스트리밍 중/시작 전엔 null. */
68
+ finishReason: SseFinishReason | null;
69
+ }
70
+ /**
71
+ * @rscc/common-core 의 readSseStream 을 감싼 상태 훅 (contracts/sse-frames.md).
72
+ *
73
+ * - 구독 전 검증 실패(!res.ok — CommonResponse JSON 에러 경로)는 봉투의 message 를 error 로 노출.
74
+ * - 스트림 시작 후 오류는 in-band error 프레임으로 수신되어 동일하게 error 로 노출.
75
+ * - 언마운트 시 진행 중 스트림을 자동 중단한다.
76
+ * - 종료 사유는 finishReason 으로 구분한다 — 'done'([DONE] 수신) / 'aborted'(중단) /
77
+ * 'interrupted'([DONE] 없이 끊김). reconnect 옵트인 시 'interrupted' 성 종료 중
78
+ * "스트림 시작 후 끊김"만 지수 백오프로 전체 재요청한다 ({@link SseReconnectOptions}).
79
+ */
80
+ declare function useSse(options?: UseSseOptions): UseSseReturn;
81
+
82
+ export { type SseFinishReason, type SseReconnectOptions, type UseSseOptions, type UseSseReturn, useDebounce, useSse };
package/dist/index.js ADDED
@@ -0,0 +1,124 @@
1
+ // src/useDebounce.ts
2
+ import { useEffect, useState } from "react";
3
+ function useDebounce(value, delayMs = 200) {
4
+ const [debounced, setDebounced] = useState(value);
5
+ useEffect(() => {
6
+ const t = setTimeout(() => setDebounced(value), delayMs);
7
+ return () => clearTimeout(t);
8
+ }, [value, delayMs]);
9
+ return debounced;
10
+ }
11
+
12
+ // src/useSse.ts
13
+ import { useCallback, useEffect as useEffect2, useRef, useState as useState2 } from "react";
14
+ import { readSseStream, retry } from "@rscc/common-core";
15
+ var RECONNECT_MARKER = new Error("rscc-sse-reconnect");
16
+ function useSse(options = {}) {
17
+ const { fetchImpl, reconnect } = options;
18
+ const [text, setText] = useState2("");
19
+ const [sources, setSources] = useState2(null);
20
+ const [conversationId, setConversationId] = useState2(null);
21
+ const [error, setError] = useState2(null);
22
+ const [isStreaming, setIsStreaming] = useState2(false);
23
+ const [finishReason, setFinishReason] = useState2(null);
24
+ const abortRef = useRef(null);
25
+ const cancel = useCallback(() => {
26
+ abortRef.current?.abort();
27
+ }, []);
28
+ const start = useCallback(
29
+ async (url, init) => {
30
+ abortRef.current?.abort();
31
+ const controller = new AbortController();
32
+ abortRef.current = controller;
33
+ const resetStreamState = () => {
34
+ setText("");
35
+ setSources(null);
36
+ setConversationId(null);
37
+ setError(null);
38
+ };
39
+ resetStreamState();
40
+ setFinishReason(null);
41
+ setIsStreaming(true);
42
+ const fetchFn = fetchImpl ?? globalThis.fetch;
43
+ let reason = "interrupted";
44
+ const attemptStream = async (attempt) => {
45
+ if (attempt > 0) resetStreamState();
46
+ let done = false;
47
+ let sawErrorFrame = false;
48
+ let streamStarted = false;
49
+ try {
50
+ const res = await fetchFn(url, { ...init, signal: controller.signal });
51
+ if (!res.ok) {
52
+ const json = await res.json().catch(() => null);
53
+ setError(typeof json?.message === "string" ? json.message : `HTTP ${res.status}`);
54
+ return;
55
+ }
56
+ streamStarted = true;
57
+ await readSseStream(res, {
58
+ onConversationId: setConversationId,
59
+ onSources: setSources,
60
+ onDelta: (d) => setText((prev) => prev + d),
61
+ onError: (m) => {
62
+ sawErrorFrame = true;
63
+ setError(m);
64
+ },
65
+ onDone: () => {
66
+ done = true;
67
+ }
68
+ });
69
+ } catch (err) {
70
+ if (controller.signal.aborted) {
71
+ reason = "aborted";
72
+ return;
73
+ }
74
+ setError(err instanceof Error ? err.message : String(err));
75
+ if (streamStarted && !sawErrorFrame && reconnect) throw RECONNECT_MARKER;
76
+ return;
77
+ }
78
+ if (controller.signal.aborted) {
79
+ reason = "aborted";
80
+ return;
81
+ }
82
+ if (done) {
83
+ reason = "done";
84
+ return;
85
+ }
86
+ if (!sawErrorFrame && reconnect) throw RECONNECT_MARKER;
87
+ };
88
+ try {
89
+ if (reconnect) {
90
+ await retry(attemptStream, {
91
+ retries: reconnect.retries ?? 3,
92
+ minDelayMs: reconnect.minDelayMs ?? 1e3,
93
+ maxDelayMs: reconnect.maxDelayMs ?? 15e3,
94
+ factor: reconnect.factor ?? 2,
95
+ jitter: reconnect.jitter ?? true,
96
+ shouldRetry: (e) => e === RECONNECT_MARKER,
97
+ signal: controller.signal
98
+ });
99
+ } else {
100
+ await attemptStream(0);
101
+ }
102
+ } catch (err) {
103
+ if (controller.signal.aborted) {
104
+ reason = "aborted";
105
+ } else if (err !== RECONNECT_MARKER) {
106
+ setError(err instanceof Error ? err.message : String(err));
107
+ }
108
+ } finally {
109
+ if (abortRef.current === controller) {
110
+ abortRef.current = null;
111
+ setIsStreaming(false);
112
+ setFinishReason(reason);
113
+ }
114
+ }
115
+ },
116
+ [fetchImpl, reconnect]
117
+ );
118
+ useEffect2(() => () => abortRef.current?.abort(), []);
119
+ return { start, cancel, text, sources, conversationId, error, isStreaming, finishReason };
120
+ }
121
+ export {
122
+ useDebounce,
123
+ useSse
124
+ };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@rscc/common-react",
3
+ "version": "0.1.2",
4
+ "description": "RSCC 공통 React 훅 — useDebounce, useSse (@rscc/common-core 의 SSE 파서 래핑).",
5
+ "keywords": [
6
+ "rscc",
7
+ "react",
8
+ "hooks",
9
+ "sse",
10
+ "use-sse",
11
+ "use-debounce",
12
+ "typescript"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Jeonghyeon-Ryu",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Jeonghyeon-Ryu/r-common.git",
19
+ "directory": "js/packages/common-react"
20
+ },
21
+ "homepage": "https://github.com/Jeonghyeon-Ryu/r-common/tree/master/js/packages/common-react",
22
+ "bugs": {
23
+ "url": "https://github.com/Jeonghyeon-Ryu/r-common/issues"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "type": "module",
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "import": {
38
+ "types": "./dist/index.d.ts",
39
+ "default": "./dist/index.js"
40
+ },
41
+ "require": {
42
+ "types": "./dist/index.d.cts",
43
+ "default": "./dist/index.cjs"
44
+ }
45
+ }
46
+ },
47
+ "files": [
48
+ "dist"
49
+ ],
50
+ "sideEffects": false,
51
+ "scripts": {
52
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
53
+ "test": "vitest run",
54
+ "prepublishOnly": "npm run build"
55
+ },
56
+ "dependencies": {
57
+ "@rscc/common-core": "^0.1.2"
58
+ },
59
+ "peerDependencies": {
60
+ "react": ">=18"
61
+ },
62
+ "devDependencies": {
63
+ "@testing-library/dom": "^10.4.0",
64
+ "@testing-library/react": "^16.3.0",
65
+ "@types/react": "^19.1.0",
66
+ "jsdom": "^26.1.0",
67
+ "react": "^19.1.0",
68
+ "react-dom": "^19.1.0",
69
+ "tsup": "^8.4.0",
70
+ "typescript": "^5.8.3",
71
+ "vitest": "^3.1.4"
72
+ }
73
+ }