@remember-web/sse 0.1.0-alpha.1
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 +238 -0
- package/dist/src/core/BaseSseClient.cjs.js +97 -0
- package/dist/src/core/BaseSseClient.cjs.js.map +1 -0
- package/dist/src/core/BaseSseClient.d.ts +33 -0
- package/dist/src/core/BaseSseClient.d.ts.map +1 -0
- package/dist/src/core/BaseSseClient.esm.js +89 -0
- package/dist/src/core/BaseSseClient.esm.js.map +1 -0
- package/dist/src/core/SseClient.cjs.js +382 -0
- package/dist/src/core/SseClient.cjs.js.map +1 -0
- package/dist/src/core/SseClient.d.ts +24 -0
- package/dist/src/core/SseClient.d.ts.map +1 -0
- package/dist/src/core/SseClient.esm.js +370 -0
- package/dist/src/core/SseClient.esm.js.map +1 -0
- package/dist/src/core/backoff.cjs.js +30 -0
- package/dist/src/core/backoff.cjs.js.map +1 -0
- package/dist/src/core/backoff.d.ts +12 -0
- package/dist/src/core/backoff.d.ts.map +1 -0
- package/dist/src/core/backoff.esm.js +28 -0
- package/dist/src/core/backoff.esm.js.map +1 -0
- package/dist/src/core/emitter.cjs.js +57 -0
- package/dist/src/core/emitter.cjs.js.map +1 -0
- package/dist/src/core/emitter.d.ts +14 -0
- package/dist/src/core/emitter.d.ts.map +1 -0
- package/dist/src/core/emitter.esm.js +51 -0
- package/dist/src/core/emitter.esm.js.map +1 -0
- package/dist/src/core/eventNames.cjs.js +64 -0
- package/dist/src/core/eventNames.cjs.js.map +1 -0
- package/dist/src/core/eventNames.d.ts +31 -0
- package/dist/src/core/eventNames.d.ts.map +1 -0
- package/dist/src/core/eventNames.esm.js +54 -0
- package/dist/src/core/eventNames.esm.js.map +1 -0
- package/dist/src/core/retryWait.cjs.js +20 -0
- package/dist/src/core/retryWait.cjs.js.map +1 -0
- package/dist/src/core/retryWait.d.ts +6 -0
- package/dist/src/core/retryWait.d.ts.map +1 -0
- package/dist/src/core/retryWait.esm.js +18 -0
- package/dist/src/core/retryWait.esm.js.map +1 -0
- package/dist/src/core/statusMachine.cjs.js +56 -0
- package/dist/src/core/statusMachine.cjs.js.map +1 -0
- package/dist/src/core/statusMachine.d.ts +9 -0
- package/dist/src/core/statusMachine.d.ts.map +1 -0
- package/dist/src/core/statusMachine.esm.js +54 -0
- package/dist/src/core/statusMachine.esm.js.map +1 -0
- package/dist/src/core/stream.cjs.js +136 -0
- package/dist/src/core/stream.cjs.js.map +1 -0
- package/dist/src/core/stream.d.ts +42 -0
- package/dist/src/core/stream.d.ts.map +1 -0
- package/dist/src/core/stream.esm.js +128 -0
- package/dist/src/core/stream.esm.js.map +1 -0
- package/dist/src/core/ticket.cjs.js +237 -0
- package/dist/src/core/ticket.cjs.js.map +1 -0
- package/dist/src/core/ticket.d.ts +31 -0
- package/dist/src/core/ticket.d.ts.map +1 -0
- package/dist/src/core/ticket.esm.js +220 -0
- package/dist/src/core/ticket.esm.js.map +1 -0
- package/dist/src/core/types.cjs.js +54 -0
- package/dist/src/core/types.cjs.js.map +1 -0
- package/dist/src/core/types.d.ts +131 -0
- package/dist/src/core/types.d.ts.map +1 -0
- package/dist/src/core/types.esm.js +50 -0
- package/dist/src/core/types.esm.js.map +1 -0
- package/dist/src/core/wire.cjs.js +29 -0
- package/dist/src/core/wire.cjs.js.map +1 -0
- package/dist/src/core/wire.d.ts +7 -0
- package/dist/src/core/wire.d.ts.map +1 -0
- package/dist/src/core/wire.esm.js +23 -0
- package/dist/src/core/wire.esm.js.map +1 -0
- package/dist/src/createSseClient.cjs.js +30 -0
- package/dist/src/createSseClient.cjs.js.map +1 -0
- package/dist/src/createSseClient.d.ts +8 -0
- package/dist/src/createSseClient.d.ts.map +1 -0
- package/dist/src/createSseClient.esm.js +28 -0
- package/dist/src/createSseClient.esm.js.map +1 -0
- package/dist/src/index.cjs.js +10 -0
- package/dist/src/index.cjs.js.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.esm.js +3 -0
- package/dist/src/index.esm.js.map +1 -0
- package/dist/src/react/createSseContext.cjs.js +130 -0
- package/dist/src/react/createSseContext.cjs.js.map +1 -0
- package/dist/src/react/createSseContext.d.ts +32 -0
- package/dist/src/react/createSseContext.d.ts.map +1 -0
- package/dist/src/react/createSseContext.esm.js +122 -0
- package/dist/src/react/createSseContext.esm.js.map +1 -0
- package/dist/src/react/index.cjs.js +8 -0
- package/dist/src/react/index.cjs.js.map +1 -0
- package/dist/src/react/index.d.ts +3 -0
- package/dist/src/react/index.d.ts.map +1 -0
- package/dist/src/react/index.esm.js +2 -0
- package/dist/src/react/index.esm.js.map +1 -0
- package/dist/src/testing/doubles.d.ts +50 -0
- package/dist/src/testing/doubles.d.ts.map +1 -0
- package/dist/src/testing/harness.d.ts +43 -0
- package/dist/src/testing/harness.d.ts.map +1 -0
- package/dist/src/testing/index.d.ts +3 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/worker/WorkerSseClient.cjs.js +240 -0
- package/dist/src/worker/WorkerSseClient.cjs.js.map +1 -0
- package/dist/src/worker/WorkerSseClient.d.ts +23 -0
- package/dist/src/worker/WorkerSseClient.d.ts.map +1 -0
- package/dist/src/worker/WorkerSseClient.esm.js +228 -0
- package/dist/src/worker/WorkerSseClient.esm.js.map +1 -0
- package/dist/src/worker/hub.d.ts +19 -0
- package/dist/src/worker/hub.d.ts.map +1 -0
- package/dist/src/worker/portRegistry.d.ts +41 -0
- package/dist/src/worker/portRegistry.d.ts.map +1 -0
- package/dist/src/worker/protocol.d.ts +63 -0
- package/dist/src/worker/protocol.d.ts.map +1 -0
- package/dist/src/worker/sse.worker.d.ts +2 -0
- package/dist/src/worker/sse.worker.d.ts.map +1 -0
- package/dist/src/worker/ticketDelegate.d.ts +16 -0
- package/dist/src/worker/ticketDelegate.d.ts.map +1 -0
- package/dist/src/worker/workerConnection.d.ts +28 -0
- package/dist/src/worker/workerConnection.d.ts.map +1 -0
- package/dist/worker/sse.worker.js +1865 -0
- package/dist/worker/sse.worker.js.map +1 -0
- package/package.json +87 -0
- package/src/core/BaseSseClient.test.ts +142 -0
- package/src/core/BaseSseClient.ts +103 -0
- package/src/core/SseClient.connection.test.ts +863 -0
- package/src/core/SseClient.scenarios.test.ts +164 -0
- package/src/core/SseClient.test.ts +63 -0
- package/src/core/SseClient.ts +310 -0
- package/src/core/backoff.test.ts +43 -0
- package/src/core/backoff.ts +39 -0
- package/src/core/emitter.ts +56 -0
- package/src/core/eventNames.ts +53 -0
- package/src/core/retryWait.ts +18 -0
- package/src/core/statusMachine.ts +56 -0
- package/src/core/stream.ts +141 -0
- package/src/core/ticket.ts +146 -0
- package/src/core/types.ts +182 -0
- package/src/core/wire.ts +23 -0
- package/src/createSseClient.test.ts +82 -0
- package/src/createSseClient.ts +34 -0
- package/src/index.ts +21 -0
- package/src/react/SseProvider.test.tsx +129 -0
- package/src/react/createSseContext.test.ts +47 -0
- package/src/react/createSseContext.tsx +140 -0
- package/src/react/index.ts +2 -0
- package/src/react/useSseEvent.test.tsx +71 -0
- package/src/react/useSseStatus.test.tsx +40 -0
- package/src/testing/doubles.ts +128 -0
- package/src/testing/harness.ts +82 -0
- package/src/testing/index.ts +3 -0
- package/src/worker/SseClientLike.contract.test.ts +270 -0
- package/src/worker/WorkerSseClient.scenarios.test.ts +179 -0
- package/src/worker/WorkerSseClient.test.ts +377 -0
- package/src/worker/WorkerSseClient.ts +190 -0
- package/src/worker/hub.connection.test.ts +1023 -0
- package/src/worker/hub.test.ts +213 -0
- package/src/worker/hub.ts +120 -0
- package/src/worker/portRegistry.test.ts +59 -0
- package/src/worker/portRegistry.ts +86 -0
- package/src/worker/protocol.ts +80 -0
- package/src/worker/sse.worker.ts +19 -0
- package/src/worker/ticketDelegate.test.ts +99 -0
- package/src/worker/ticketDelegate.ts +80 -0
- package/src/worker/workerConnection.ts +134 -0
package/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# @remember-web/sse
|
|
2
|
+
|
|
3
|
+
티켓 기반 SSE(Server-Sent Events) 수신 클라이언트 SDK입니다.
|
|
4
|
+
|
|
5
|
+
- **연결은 하나** — SharedWorker로 여러 탭이 SSE 연결 1개를 공유합니다 (미지원 환경은 탭별 연결로 자동 폴백)
|
|
6
|
+
- **알아서 복구** — 끊기면 지수 백오프로 재연결하고, 복구되면 `resync` 신호로 "놓친 구간을 다시 조회하라"고 알립니다
|
|
7
|
+
- **타입은 소비처 소유** — 이벤트 이름·페이로드 타입은 앱이 정의한 맵에서 추론됩니다. SDK는 어떤 도메인 이벤트가 오는지 모릅니다
|
|
8
|
+
|
|
9
|
+
## 설치
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @remember-web/sse
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 빠른 시작 (React)
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// sse.ts — 앱당 1회, 모듈 스코프에서 호출합니다 (렌더 중 호출 금지)
|
|
19
|
+
import { createSseContext } from '@remember-web/sse/react';
|
|
20
|
+
|
|
21
|
+
// 이벤트 이름·페이로드는 서버와 앱 사이의 계약입니다 — 여기에 그 계약을 적습니다
|
|
22
|
+
interface AppEventMap {
|
|
23
|
+
'notification.created': { id: string; title: string };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const { SseProvider, useSseEvent, useSseStatus } =
|
|
27
|
+
createSseContext<AppEventMap>();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
// App.tsx
|
|
32
|
+
<SseProvider
|
|
33
|
+
baseUrl="https://api.example.com"
|
|
34
|
+
getAuthToken={() => auth.accessToken}
|
|
35
|
+
enabled={isLoggedIn}
|
|
36
|
+
sharedWorker={() =>
|
|
37
|
+
new SharedWorker(new URL('@remember-web/sse/worker', import.meta.url), {
|
|
38
|
+
name: 'app-sse',
|
|
39
|
+
type: 'module',
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
<App />
|
|
44
|
+
</SseProvider>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// 화면 — 이벤트명만 적으면 페이로드 타입이 추론됩니다
|
|
49
|
+
useSseEvent('notification.created', (event) => showToast(event.title));
|
|
50
|
+
|
|
51
|
+
// 재수립 신호 — 끊겨 있던 사이의 공백은 REST 재조회로 메웁니다
|
|
52
|
+
useSseEvent('resync', () => queryClient.invalidateQueries(...));
|
|
53
|
+
|
|
54
|
+
const status = useSseStatus(); // 'reconnecting'이면 "연결 중…" 표시 등
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
이게 전부입니다. 연결·해제는 `SseProvider`가 수명주기에 맞춰 처리하고, 탭이 다시 보일 때의 재개(`attachAutoResume`)도 자동으로 배선됩니다.
|
|
58
|
+
|
|
59
|
+
## 구조 — 무엇이 어디에 있나
|
|
60
|
+
|
|
61
|
+
```mermaid
|
|
62
|
+
flowchart TD
|
|
63
|
+
subgraph TAB ["각 탭"]
|
|
64
|
+
A["화면 컴포넌트"] --> B["useSseEvent · useSseStatus"]
|
|
65
|
+
B --> C["SseProvider"]
|
|
66
|
+
C --> D{"createSseClient()"}
|
|
67
|
+
D -->|"sharedWorker 미제공<br/>또는 미지원 브라우저"| E["SseClient<br/>(탭별 직접 연결)"]
|
|
68
|
+
D -->|"sharedWorker 제공"| F["WorkerSseClient<br/>(워커 프록시)"]
|
|
69
|
+
end
|
|
70
|
+
subgraph WORKER ["SharedWorker — 모든 탭이 공유"]
|
|
71
|
+
G["허브<br/>탭 장부·구독 취합·티켓 위임"] --> H["워커 연결<br/>(SseClient 재사용)"]
|
|
72
|
+
end
|
|
73
|
+
F -.->|"MessagePort"| G
|
|
74
|
+
E --> S[("SSE 서버")]
|
|
75
|
+
H --> S
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| 모듈 | 역할 | 소비처가 아는가 |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `createSseContext` (react) | 앱 이벤트 맵으로 Provider·훅 세트를 생성 — 타입 추론의 진입점 | ✅ 주 표면 |
|
|
81
|
+
| `createSseClient` (core) | 환경을 보고 두 구현 중 하나를 골라 만드는 단일 생성 진입점 | React 없이 쓸 때만 |
|
|
82
|
+
| `SseClient` | 탭별 직접 연결 — 티켓 발급 → 스트림 유지 → 재연결 루프의 원본 구현 | ❌ 내부 |
|
|
83
|
+
| `WorkerSseClient` | 같은 인터페이스의 탭 측 프록시 — 연결은 워커가 소유, 탭은 메시지로 중계받음 | ❌ 내부 |
|
|
84
|
+
| 워커 허브 | 탭 장부(누가 살아 있나)·구독 취합·티켓 발급 위임(인증 토큰은 탭에만 있음) | ❌ 내부 |
|
|
85
|
+
|
|
86
|
+
두 구현(`SseClient`·`WorkerSseClient`)은 같은 인터페이스(`SseClientLike`)를 지키므로, **소비처 코드는 어느 경로로 연결됐는지 몰라도 동일하게 동작합니다.**
|
|
87
|
+
|
|
88
|
+
## 연결의 수명
|
|
89
|
+
|
|
90
|
+
```mermaid
|
|
91
|
+
sequenceDiagram
|
|
92
|
+
participant C as 클라이언트
|
|
93
|
+
participant S as 서버
|
|
94
|
+
C->>S: POST /v1/events/ticket (인증 토큰)
|
|
95
|
+
S-->>C: { data: { ticket } } — 수명이 짧아 즉시 사용합니다
|
|
96
|
+
C->>S: GET /v1/events?ticket=… (EventSource)
|
|
97
|
+
S-->>C: event: hello
|
|
98
|
+
Note over C: 이때부터 open — 재수립이면 resync 발화
|
|
99
|
+
loop 수신 중
|
|
100
|
+
S-->>C: event: notification.created …
|
|
101
|
+
end
|
|
102
|
+
Note over C,S: 끊김 → 백오프 대기 → 티켓부터 다시 (무한 재시도)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`hello`를 받아야 `open`입니다 — HTTP 응답이 열렸다는 것과 이벤트 파이프라인이 연결됐다는 것을 구분하기 위한 서버의 수립 완료 신호입니다.
|
|
106
|
+
|
|
107
|
+
### 상태
|
|
108
|
+
|
|
109
|
+
```mermaid
|
|
110
|
+
stateDiagram-v2
|
|
111
|
+
[*] --> idle
|
|
112
|
+
idle --> connecting: connect()
|
|
113
|
+
connecting --> open: hello 수신
|
|
114
|
+
connecting --> reconnecting: 네트워크성 실패
|
|
115
|
+
connecting --> fatal: 티켓 거부 · 생성 실패
|
|
116
|
+
open --> reconnecting: 끊김 · goaway(retry)
|
|
117
|
+
open --> fatal: 예상 못 한 예외
|
|
118
|
+
open --> closed: goaway(close)
|
|
119
|
+
reconnecting --> connecting: 백오프 만료
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
여기에 더해 **어느 상태에서든 `disconnect()`는 `closed`로** 갑니다. `closed`·`fatal`에서 다시 받으려면 `connect()`를 부릅니다(`fatal`은 `disconnect()`로 정리한 뒤).
|
|
123
|
+
|
|
124
|
+
## 실패와 회복 — 소비처 계약
|
|
125
|
+
|
|
126
|
+
`connect()`는 **던지지 않고 Promise도 돌려주지 않습니다.** 실패는 전부 상태(`onStatus`)와 이벤트로 옵니다.
|
|
127
|
+
|
|
128
|
+
연결에 문제가 생기면 세 이벤트로 알립니다. 구분축은 **"누가 결정했고, 재시도는 어떻게 되나"**입니다.
|
|
129
|
+
|
|
130
|
+
| | `stalled` | `fatal` | `goaway` |
|
|
131
|
+
|---|---|---|---|
|
|
132
|
+
| 한 줄로 | 고전 중이라는 신고 | SDK의 포기 선언 | 서버의 작별 인사 |
|
|
133
|
+
| 출처 | SDK 합성 — 와이어에 없음 | SDK 합성 — 와이어에 없음 | **서버 발신** — 와이어의 `event: goaway` |
|
|
134
|
+
| 결정 주체 | SDK — 응답 없는 실패 연속 5회 관찰 | SDK — 재시도 무의미 판정 | 서버 — 산 연결 위에서 예고 |
|
|
135
|
+
| 재시도 | **계속되는 중** (회복되면 자동으로 이어짐) | 없음 — 멈춤 | `action: 'retry'`면 SDK가 즉시 재수립, `'close'`면 종료 |
|
|
136
|
+
| 몇 번 오나 | 미연결 구간마다 1회 — 회복(open) 후 다시 끊기면 또 옴 | 세션당 1회 — 멈추므로 반복 없음 (`disconnect()` → `connect()`로 재개한 뒤에만 다시 올 수 있음) | 서버가 보낼 때마다 — `retry`는 재수립 후 또 올 수 있고, `close`는 그 연결의 마지막 메시지 |
|
|
137
|
+
| 성격 | 항상 이상 | 항상 이상 (`cause`로 사유 구분) | 정상 절차일 수 있음 — 배포·로그아웃·점검 |
|
|
138
|
+
| 소비처가 할 일 | 진행형 안내 | 사유별 안내, 해소 후 `disconnect()` → `connect()` | `action: 'close'`일 때만 `reason`별 안내 |
|
|
139
|
+
|
|
140
|
+
(`resync`는 에러가 아니라 재수립 후의 데이터 복구 신호라 이 표 밖입니다 — 아래 표의 "재연결 성공" 행 참조.)
|
|
141
|
+
|
|
142
|
+
상황별 상세 계약:
|
|
143
|
+
|
|
144
|
+
| 상황 | SDK가 하는 일 | 소비처가 받는 것 | 소비처가 할 일 |
|
|
145
|
+
|---|---|---|---|
|
|
146
|
+
| 네트워크 끊김·서버 5xx·응답 타임아웃 | 지수 백오프(1s→최대 30s, jitter)로 **무한 자동 재연결** | status `reconnecting` | (선택) "연결 중…" 표시 |
|
|
147
|
+
| 네트워크 에러 지속 (오프라인·CORS 차단·접속 거부 등 응답 없는 실패가 연속 5회) | **재시도를 계속하면서** 한 번 알림 | **`stalled` 이벤트** | 네트워크·보안 정책 안내 (fatal이 아닙니다 — 회복되면 자동으로 이어집니다) |
|
|
148
|
+
| 재연결 성공 | 수신 재개 | **`resync` 이벤트** | 끊긴 사이의 공백을 REST 재조회로 메움 |
|
|
149
|
+
| 티켓 발급 401/403 | 멈춤 (자동 재시도 없음) | status `fatal` + **`fatal` 이벤트** `{ cause: 'auth', status }` | 재로그인 유도 |
|
|
150
|
+
| 티켓 발급 429 | 멈춤 | `fatal` 이벤트 `{ cause: 'rate_limit', status: 429 }` | 한도 안내, 잠시 후 재시도 |
|
|
151
|
+
| `getAuthToken` 콜백이 예외를 던짐 | 멈춤 | `fatal` 이벤트 `{ cause: 'auth_callback' }` | 소비처 코드 문제 — 콜백은 실패 시 throw 대신 `null`을 반환해야 합니다 |
|
|
152
|
+
| SharedWorker 생성 차단 (CSP 등) | 멈춤 | `fatal` 이벤트 `{ cause: 'blocked' }` | 환경 안내 |
|
|
153
|
+
| EventSource 생성 실패 (잘못된 `baseUrl` 등) | 멈춤 | `fatal` 이벤트 `{ cause: 'config' }` | 설정 오류 — 재시도해도 되살아나지 않으니 재시도 안내를 하지 마세요 |
|
|
154
|
+
| 그 외 예상 못 한 오류 | 멈춤 | `fatal` 이벤트 `{ cause: 'unknown' }` | 일반 오류 안내 |
|
|
155
|
+
| 서버가 재연결 지시 (배포 등) | 대기 없이 즉시 재수립 | `goaway` 이벤트 (`action: 'retry'`) | 없음 |
|
|
156
|
+
| 서버가 종료 지시 | 재연결하지 않고 닫음 | status `closed` + `goaway` 이벤트 (`action: 'close'`, `reason` 포함) | `reason`별 안내 (로그아웃·점검 등) |
|
|
157
|
+
|
|
158
|
+
- **`fatal`의 회복 계약**: 사유를 해소한 뒤 `disconnect()` → `connect()` — **회복은 `fatal` 이벤트 핸들러에서 시작하세요.** status 리스너에서 `'fatal'`만 보고 끊으면 워커 경로에선 뒤따라오던 사유가 유실될 수 있습니다. 자동 재개(`attachAutoResume`)는 `fatal`·`closed`를 되살리지 않습니다 — 끊은 의도를 존중합니다.
|
|
159
|
+
- **사유는 발화 순간의 구독자에게만 갑니다** — 재생이 없으므로 `fatal`(과 `goaway`·`stalled`) 핸들러는 화면 단위가 아니라 앱 루트에 고정하세요.
|
|
160
|
+
- **`stalled`는 진행형 통지입니다** — 응답 status가 없는 네트워크성 실패가 hello에 닿지 못한 채 연속 5회 이어지는 순간 1회 발화하고, 그 뒤에도 재시도는 계속됩니다. 브라우저는 오프라인·CORS 차단·접속 거부를 구분해주지 않으므로 사유는 싣지 않고, 서버가 응답한 실패(5xx, 본문이 계약과 다른 200)는 네트워크 문제가 아니라 세지 않고, 연속 판정을 끊습니다. 회복은 별도 이벤트 없이 status가 `open`으로 바뀌는 것으로 알 수 있고, 그때 판정이 리셋되어 다음 구간에 다시 발화합니다.
|
|
161
|
+
- **`resync`가 데이터 복구의 전부입니다.** SDK는 이벤트를 저장·재전송하지 않습니다. 실시간 이벤트는 "지금 일어난 일"의 알림이고, 정합성의 원천은 REST API입니다.
|
|
162
|
+
|
|
163
|
+
## 이벤트 계약
|
|
164
|
+
|
|
165
|
+
- **이벤트 이름·페이로드 타입은 소비처 소유입니다.** 서버와 앱 사이의 계약을 `createSseContext<M>`의 맵에 적으면 SDK는 그대로 흘려보냅니다.
|
|
166
|
+
- **예약 이벤트명 5개는 도메인 맵에 쓰지 마세요**: `hello`(SDK가 소비), `resync`·`goaway`·`fatal`·`stalled`(SDK가 발화해 소비처에 배달). 맵에 같은 이름을 정의하면 타입이 충돌합니다.
|
|
167
|
+
- **모르는 이벤트는 무시됩니다** — 서버가 이벤트를 추가해도 구버전 앱은 깨지지 않습니다.
|
|
168
|
+
- **JSON이 아닌 페이로드는 원문 문자열 그대로 배달됩니다** — SDK가 임의로 버리지 않습니다.
|
|
169
|
+
- **핸들러 예외는 격리됩니다** — 한 구독자가 던져도 다른 구독자와 연결에는 영향이 없고, `logger.warn`으로 보고됩니다.
|
|
170
|
+
|
|
171
|
+
## 탭 간 연결 공유 (SharedWorker)
|
|
172
|
+
|
|
173
|
+
`sharedWorker` 옵션을 주면 여러 탭이 연결 1개를 공유합니다. 서버 동시 연결 슬롯과 브라우저 연결 수 제한을 아낍니다.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
sharedWorker: () =>
|
|
177
|
+
new SharedWorker(new URL('@remember-web/sse/worker', import.meta.url), {
|
|
178
|
+
name: 'app-sse',
|
|
179
|
+
type: 'module',
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- **URL은 번들러가 해석해야 하므로** 인스턴스가 아닌 팩토리를 받습니다. `new URL(…, import.meta.url)` 패턴은 Vite·webpack 5가 지원합니다.
|
|
184
|
+
- **미지원 브라우저는 자동으로 탭별 직접 연결로 폴백**합니다 (`logger.warn`으로 알림).
|
|
185
|
+
- 인증 토큰은 워커로 넘어가지 않습니다 — 티켓 발급은 살아 있는 탭 하나에 위임됩니다.
|
|
186
|
+
- 백그라운드 탭이 얼어붙어도(frozen) 밀린 이벤트는 깨어날 때 그대로 배달됩니다. 전 탭이 얼어붙으면 연결을 접고, 깨어난 탭의 `connect()`가 다시 세웁니다 — `SseProvider`가 이 재개를 자동 배선합니다.
|
|
187
|
+
- **배포 주의**: 워커 스크립트가 캐시된 구버전이면 신버전 탭과 잠시 섞일 수 있습니다. 미지의 메시지는 양쪽 다 무시하도록 설계돼 있어 안전하지만, 구버전 워커는 새 이벤트(예: `fatal`)를 중계하지 못할 수 있으니 **치명 실패 처리에는 `onStatus`의 `'fatal'`을 폴백으로** 함께 보세요.
|
|
188
|
+
|
|
189
|
+
## SSR
|
|
190
|
+
|
|
191
|
+
생성은 어느 환경에서든 안전합니다 — 브라우저 전역이 없으면 `connect()`는 조용히 no-op이고, 첫 렌더의 status는 `idle`입니다. 훅은 `SseProvider` 안에서만 호출하세요.
|
|
192
|
+
|
|
193
|
+
## React 없이 쓰기
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { createSseClient, wire } from '@remember-web/sse';
|
|
197
|
+
|
|
198
|
+
const client = createSseClient<AppEventMap>({
|
|
199
|
+
baseUrl: 'https://api.example.com',
|
|
200
|
+
getAuthToken: () => auth.accessToken,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 핸들러 맵 일괄 등록 — 반환 함수 하나로 전부 해제
|
|
204
|
+
const unwire = wire(client, {
|
|
205
|
+
'notification.created': (event) => badge.increment(),
|
|
206
|
+
resync: () => badge.refetch(),
|
|
207
|
+
fatal: ({ cause }) => handleFatal(cause),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
client.connect();
|
|
211
|
+
const detach = client.attachAutoResume(); // 탭 복귀 시 자동 재개 (선택)
|
|
212
|
+
|
|
213
|
+
// 정리
|
|
214
|
+
detach();
|
|
215
|
+
unwire();
|
|
216
|
+
client.disconnect();
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`on('fatal')` 같은 구독은 `connect()`보다 먼저 걸어두세요 — 생성 단계에서 동기 발화되는 실패(`blocked`)를 놓치지 않습니다.
|
|
220
|
+
|
|
221
|
+
## 옵션
|
|
222
|
+
|
|
223
|
+
| 옵션 | 필수 | 설명 |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| `baseUrl` | ✅ | SSE 서버 오리진. `POST {baseUrl}/v1/events/ticket`, `GET {baseUrl}/v1/events`를 사용합니다 |
|
|
226
|
+
| `getAuthToken` | ✅ | 티켓 발급 요청의 인증 토큰. 호출 시점마다 불리므로 최신 토큰을 돌려주면 됩니다. `null`이면 인증 헤더 없이 요청. **실패 시 throw 대신 `null`을 반환하세요** — 예외가 새어 나오면 재시도 없이 `fatal { cause: 'auth_callback' }`로 멈춥니다 |
|
|
227
|
+
| `sharedWorker` | | SharedWorker 팩토리 — 제공하면 탭 간 연결 공유, 미제공이면 탭별 직접 연결 |
|
|
228
|
+
| `backoff` | | 재연결 대기 조정: `initialDelayMs`(1000) · `maxDelayMs`(30000) · `multiplier`(2) |
|
|
229
|
+
| `logger` | | `warn`·`error`를 받는 로거. 연결 실패 사유·설정 실수가 여기로 보고됩니다 |
|
|
230
|
+
| `fetch` / `eventSource` | | 주입점 (테스트·특수 환경용). `eventSource`는 워커 경로에는 적용되지 않습니다 |
|
|
231
|
+
|
|
232
|
+
## 서버 요구 사항
|
|
233
|
+
|
|
234
|
+
이 SDK는 다음 계약의 서버를 전제합니다.
|
|
235
|
+
|
|
236
|
+
- `POST /v1/events/ticket` — 인증 토큰을 받고 짧은 수명의 일회용 티켓을 발급 (`{ data: { ticket } }` — 사내 기본 응답 스키마)
|
|
237
|
+
- `GET /v1/events?ticket=…` — `text/event-stream`. 수립 직후 `hello` 이벤트를 1회 전송
|
|
238
|
+
- (선택) `goaway` 이벤트 — `{ action: 'retry' | 'close', reason?: string }`으로 클라이언트에 재연결·종료를 지시
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var _classCallCheck = require('@babel/runtime/helpers/classCallCheck');
|
|
4
|
+
var _createClass = require('@babel/runtime/helpers/createClass');
|
|
5
|
+
var _defineProperty = require('@babel/runtime/helpers/defineProperty');
|
|
6
|
+
var emitter = require('./emitter.cjs.js');
|
|
7
|
+
var eventNames = require('./eventNames.cjs.js');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var _classCallCheck__default = /*#__PURE__*/_interopDefault(_classCallCheck);
|
|
12
|
+
var _createClass__default = /*#__PURE__*/_interopDefault(_createClass);
|
|
13
|
+
var _defineProperty__default = /*#__PURE__*/_interopDefault(_defineProperty);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @description 두 구현이 공유하는 구독 표면 — 구독 장부(emitter)를 소유하고 on·onStatus를 확정합니다.
|
|
17
|
+
* 연결을 어떻게 다루는지는 모릅니다: 구현마다 다른 것만 추상 멤버로 남깁니다.
|
|
18
|
+
* 같아야 할 것을 한 벌로 두기 위한 클래스입니다 — 두 구현이 각자 배선하다 갈린 이력이 있습니다.
|
|
19
|
+
*/
|
|
20
|
+
var BaseSseClient = /*#__PURE__*/function () {
|
|
21
|
+
function BaseSseClient(logger) {
|
|
22
|
+
_classCallCheck__default.default(this, BaseSseClient);
|
|
23
|
+
_defineProperty__default.default(this, "emitter", void 0);
|
|
24
|
+
this.logger = logger;
|
|
25
|
+
this.emitter = emitter.createEmitter(logger);
|
|
26
|
+
}
|
|
27
|
+
return _createClass__default.default(BaseSseClient, [{
|
|
28
|
+
key: "on",
|
|
29
|
+
value: function on(type, handler) {
|
|
30
|
+
if (eventNames.isUndeliverableEvent(type)) {
|
|
31
|
+
var _this$logger;
|
|
32
|
+
(_this$logger = this.logger) === null || _this$logger === void 0 || _this$logger.warn("sse: '".concat(type, "'\uB294 SDK\uAC00 \uC18C\uBE44\uD558\uB294 \uC608\uC57D \uC774\uBCA4\uD2B8\uB77C \uAD6C\uB3C5\uC774 \uBC30\uB2EC\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4"));
|
|
33
|
+
}
|
|
34
|
+
this.registerSubscription(type);
|
|
35
|
+
return this.emitter.on(type, handler);
|
|
36
|
+
}
|
|
37
|
+
}, {
|
|
38
|
+
key: "onStatus",
|
|
39
|
+
value: function onStatus(listener) {
|
|
40
|
+
return this.emitter.onStatus(listener);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @description 탭이 돌아올 때 connect()를 다시 부릅니다 — 전원이 얼어붙어 접힌 연결(휴면)은
|
|
45
|
+
* 깨어난 탭의 이 호출만이 다시 세울 수 있습니다. 연결이 살아 있으면 아무 일도 없습니다.
|
|
46
|
+
* bfcache 복귀는 visibilitychange가 아니라 pageshow로만 알 수 있어 둘 다 봅니다.
|
|
47
|
+
* SSR에는 붙일 곳이 없어 아무 일도 하지 않습니다.
|
|
48
|
+
*
|
|
49
|
+
* @returns 리스너를 떼는 함수 — 클라이언트를 버릴 때 함께 부르세요
|
|
50
|
+
*/
|
|
51
|
+
}, {
|
|
52
|
+
key: "attachAutoResume",
|
|
53
|
+
value: function attachAutoResume() {
|
|
54
|
+
var _this = this;
|
|
55
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
|
56
|
+
return function () {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 되살릴 대상은 idle입니다 — 전원 침묵으로 접힌 휴면이 이 상태입니다(workerConnection.close).
|
|
60
|
+
// closed(소비처 disconnect·서버 goaway close)는 끊은 의도를 되살리지 않고,
|
|
61
|
+
// fatal의 회복 계약은 disconnect 후 connect라 자동 재개가 대신할 수 없습니다
|
|
62
|
+
var notifyAlive = function notifyAlive() {
|
|
63
|
+
if (_this.status === 'closed' || _this.status === 'fatal') {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
_this.connect();
|
|
67
|
+
};
|
|
68
|
+
var onVisible = function onVisible() {
|
|
69
|
+
if (document.visibilityState === 'visible') {
|
|
70
|
+
notifyAlive();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// persisted = bfcache 복귀, 직결 경로용 — Chrome은 SharedWorker 페이지를 bfcache에 넣지 않고,
|
|
75
|
+
// 넣는 브라우저라면 포트가 이미 죽어 이 connect가 닿지 않습니다(알려진 한계)
|
|
76
|
+
var onPageShow = function onPageShow(event) {
|
|
77
|
+
if (event.persisted) {
|
|
78
|
+
notifyAlive();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
82
|
+
window.addEventListener('pageshow', onPageShow);
|
|
83
|
+
return function () {
|
|
84
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
85
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @description 구독이 등록될 때 각 구현이 할 일 — 열린 소스에 리스너를 붙이거나, 워커에 알리거나.
|
|
91
|
+
* 연결 전 구독도 여기로 오므로, 아직 붙일 곳이 없으면 조용히 넘어가야 합니다.
|
|
92
|
+
*/
|
|
93
|
+
}]);
|
|
94
|
+
}();
|
|
95
|
+
|
|
96
|
+
exports.BaseSseClient = BaseSseClient;
|
|
97
|
+
//# sourceMappingURL=BaseSseClient.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseSseClient.cjs.js","sources":["../../../src/core/BaseSseClient.ts"],"sourcesContent":["import type { Emitter } from './emitter';\nimport { createEmitter } from './emitter';\nimport { isUndeliverableEvent } from './eventNames';\nimport type {\n PayloadOf,\n SseClientLike,\n SseEventHandler,\n SseEventMap,\n SseEventName,\n SseLogger,\n SseStatus,\n Unsubscribe,\n} from './types';\n\n/**\n * @description 두 구현이 공유하는 구독 표면 — 구독 장부(emitter)를 소유하고 on·onStatus를 확정합니다.\n * 연결을 어떻게 다루는지는 모릅니다: 구현마다 다른 것만 추상 멤버로 남깁니다.\n * 같아야 할 것을 한 벌로 두기 위한 클래스입니다 — 두 구현이 각자 배선하다 갈린 이력이 있습니다.\n */\nexport abstract class BaseSseClient<\n M extends object = SseEventMap,\n> implements SseClientLike<M> {\n declare readonly __eventMap?: M;\n\n protected readonly emitter: Emitter;\n\n constructor(protected readonly logger?: SseLogger) {\n this.emitter = createEmitter(logger);\n }\n\n on<K extends SseEventName<M>>(\n type: K,\n handler: SseEventHandler<PayloadOf<M, K>>\n ): Unsubscribe {\n if (isUndeliverableEvent(type)) {\n this.logger?.warn(\n `sse: '${type}'는 SDK가 소비하는 예약 이벤트라 구독이 배달되지 않습니다`\n );\n }\n this.registerSubscription(type);\n return this.emitter.on(type, handler as SseEventHandler<unknown>);\n }\n\n onStatus(listener: (status: SseStatus) => void): Unsubscribe {\n return this.emitter.onStatus(listener);\n }\n\n /**\n * @description 탭이 돌아올 때 connect()를 다시 부릅니다 — 전원이 얼어붙어 접힌 연결(휴면)은\n * 깨어난 탭의 이 호출만이 다시 세울 수 있습니다. 연결이 살아 있으면 아무 일도 없습니다.\n * bfcache 복귀는 visibilitychange가 아니라 pageshow로만 알 수 있어 둘 다 봅니다.\n * SSR에는 붙일 곳이 없어 아무 일도 하지 않습니다.\n *\n * @returns 리스너를 떼는 함수 — 클라이언트를 버릴 때 함께 부르세요\n */\n attachAutoResume(): Unsubscribe {\n if (typeof document === 'undefined' || typeof window === 'undefined') {\n return () => {};\n }\n\n // 되살릴 대상은 idle입니다 — 전원 침묵으로 접힌 휴면이 이 상태입니다(workerConnection.close).\n // closed(소비처 disconnect·서버 goaway close)는 끊은 의도를 되살리지 않고,\n // fatal의 회복 계약은 disconnect 후 connect라 자동 재개가 대신할 수 없습니다\n const notifyAlive = (): void => {\n if (this.status === 'closed' || this.status === 'fatal') {\n return;\n }\n this.connect();\n };\n\n const onVisible = (): void => {\n if (document.visibilityState === 'visible') {\n notifyAlive();\n }\n };\n\n // persisted = bfcache 복귀, 직결 경로용 — Chrome은 SharedWorker 페이지를 bfcache에 넣지 않고,\n // 넣는 브라우저라면 포트가 이미 죽어 이 connect가 닿지 않습니다(알려진 한계)\n const onPageShow = (event: PageTransitionEvent): void => {\n if (event.persisted) {\n notifyAlive();\n }\n };\n\n document.addEventListener('visibilitychange', onVisible);\n window.addEventListener('pageshow', onPageShow);\n\n return () => {\n document.removeEventListener('visibilitychange', onVisible);\n window.removeEventListener('pageshow', onPageShow);\n };\n }\n\n /**\n * @description 구독이 등록될 때 각 구현이 할 일 — 열린 소스에 리스너를 붙이거나, 워커에 알리거나.\n * 연결 전 구독도 여기로 오므로, 아직 붙일 곳이 없으면 조용히 넘어가야 합니다.\n */\n protected abstract registerSubscription(type: string): void;\n\n abstract get status(): SseStatus;\n abstract connect(): void;\n abstract disconnect(): void;\n}\n"],"names":["BaseSseClient","logger","_classCallCheck","_defineProperty","emitter","createEmitter","_createClass","key","value","on","type","handler","isUndeliverableEvent","_this$logger","warn","concat","registerSubscription","onStatus","listener","attachAutoResume","_this","document","window","notifyAlive","status","connect","onVisible","visibilityState","onPageShow","event","persisted","addEventListener","removeEventListener"],"mappings":";;;;;;;;;;;;;;AAcA;AACA;AACA;AACA;AACA;AACA,IAAsBA,aAAa,gBAAA,YAAA;EAOjC,SAAAA,aAAAA,CAA+BC,MAAkB,EAAE;AAAAC,IAAAA,gCAAA,OAAAF,aAAA,CAAA;IAAAG,gCAAA,CAAA,IAAA,EAAA,SAAA,EAAA,MAAA,CAAA;IAAA,IAAA,CAApBF,MAAkB,GAAlBA,MAAkB;AAC/C,IAAA,IAAI,CAACG,OAAO,GAAGC,qBAAa,CAACJ,MAAM,CAAC;AACtC;EAAC,OAAAK,6BAAA,CAAAN,aAAA,EAAA,CAAA;IAAAO,GAAA,EAAA,IAAA;AAAAC,IAAAA,KAAA,EAED,SAAAC,EAAAA,CACEC,IAAO,EACPC,OAAyC,EAC5B;AACb,MAAA,IAAIC,+BAAoB,CAACF,IAAI,CAAC,EAAE;AAAA,QAAA,IAAAG,YAAA;AAC9B,QAAA,CAAAA,YAAA,GAAA,IAAI,CAACZ,MAAM,cAAAY,YAAA,KAAA,MAAA,IAAXA,YAAA,CAAaC,IAAI,CAAA,QAAA,CAAAC,MAAA,CACNL,IAAI,0JACf,CAAC;AACH;AACA,MAAA,IAAI,CAACM,oBAAoB,CAACN,IAAI,CAAC;MAC/B,OAAO,IAAI,CAACN,OAAO,CAACK,EAAE,CAACC,IAAI,EAAEC,OAAmC,CAAC;AACnE;AAAC,GAAA,EAAA;IAAAJ,GAAA,EAAA,UAAA;AAAAC,IAAAA,KAAA,EAED,SAAAS,QAAAA,CAASC,QAAqC,EAAe;AAC3D,MAAA,OAAO,IAAI,CAACd,OAAO,CAACa,QAAQ,CAACC,QAAQ,CAAC;AACxC;;AAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AAPE,GAAA,EAAA;IAAAX,GAAA,EAAA,kBAAA;IAAAC,KAAA,EAQA,SAAAW,gBAAAA,GAAgC;AAAA,MAAA,IAAAC,KAAA,GAAA,IAAA;MAC9B,IAAI,OAAOC,QAAQ,KAAK,WAAW,IAAI,OAAOC,MAAM,KAAK,WAAW,EAAE;QACpE,OAAO,YAAM,EAAE;AACjB;;AAEA;AACA;AACA;AACA,MAAA,IAAMC,WAAW,GAAG,SAAdA,WAAWA,GAAe;QAC9B,IAAIH,KAAI,CAACI,MAAM,KAAK,QAAQ,IAAIJ,KAAI,CAACI,MAAM,KAAK,OAAO,EAAE;AACvD,UAAA;AACF;QACAJ,KAAI,CAACK,OAAO,EAAE;OACf;AAED,MAAA,IAAMC,SAAS,GAAG,SAAZA,SAASA,GAAe;AAC5B,QAAA,IAAIL,QAAQ,CAACM,eAAe,KAAK,SAAS,EAAE;AAC1CJ,UAAAA,WAAW,EAAE;AACf;OACD;;AAED;AACA;AACA,MAAA,IAAMK,UAAU,GAAG,SAAbA,UAAUA,CAAIC,KAA0B,EAAW;QACvD,IAAIA,KAAK,CAACC,SAAS,EAAE;AACnBP,UAAAA,WAAW,EAAE;AACf;OACD;AAEDF,MAAAA,QAAQ,CAACU,gBAAgB,CAAC,kBAAkB,EAAEL,SAAS,CAAC;AACxDJ,MAAAA,MAAM,CAACS,gBAAgB,CAAC,UAAU,EAAEH,UAAU,CAAC;AAE/C,MAAA,OAAO,YAAM;AACXP,QAAAA,QAAQ,CAACW,mBAAmB,CAAC,kBAAkB,EAAEN,SAAS,CAAC;AAC3DJ,QAAAA,MAAM,CAACU,mBAAmB,CAAC,UAAU,EAAEJ,UAAU,CAAC;OACnD;AACH;;AAEA;AACF;AACA;AACA;AAHE,GAAA,CAAA,CAAA;AAAA,CAAA;;;;"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Emitter } from './emitter';
|
|
2
|
+
import type { PayloadOf, SseClientLike, SseEventHandler, SseEventMap, SseEventName, SseLogger, SseStatus, Unsubscribe } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* @description 두 구현이 공유하는 구독 표면 — 구독 장부(emitter)를 소유하고 on·onStatus를 확정합니다.
|
|
5
|
+
* 연결을 어떻게 다루는지는 모릅니다: 구현마다 다른 것만 추상 멤버로 남깁니다.
|
|
6
|
+
* 같아야 할 것을 한 벌로 두기 위한 클래스입니다 — 두 구현이 각자 배선하다 갈린 이력이 있습니다.
|
|
7
|
+
*/
|
|
8
|
+
export declare abstract class BaseSseClient<M extends object = SseEventMap> implements SseClientLike<M> {
|
|
9
|
+
protected readonly logger?: SseLogger | undefined;
|
|
10
|
+
readonly __eventMap?: M;
|
|
11
|
+
protected readonly emitter: Emitter;
|
|
12
|
+
constructor(logger?: SseLogger | undefined);
|
|
13
|
+
on<K extends SseEventName<M>>(type: K, handler: SseEventHandler<PayloadOf<M, K>>): Unsubscribe;
|
|
14
|
+
onStatus(listener: (status: SseStatus) => void): Unsubscribe;
|
|
15
|
+
/**
|
|
16
|
+
* @description 탭이 돌아올 때 connect()를 다시 부릅니다 — 전원이 얼어붙어 접힌 연결(휴면)은
|
|
17
|
+
* 깨어난 탭의 이 호출만이 다시 세울 수 있습니다. 연결이 살아 있으면 아무 일도 없습니다.
|
|
18
|
+
* bfcache 복귀는 visibilitychange가 아니라 pageshow로만 알 수 있어 둘 다 봅니다.
|
|
19
|
+
* SSR에는 붙일 곳이 없어 아무 일도 하지 않습니다.
|
|
20
|
+
*
|
|
21
|
+
* @returns 리스너를 떼는 함수 — 클라이언트를 버릴 때 함께 부르세요
|
|
22
|
+
*/
|
|
23
|
+
attachAutoResume(): Unsubscribe;
|
|
24
|
+
/**
|
|
25
|
+
* @description 구독이 등록될 때 각 구현이 할 일 — 열린 소스에 리스너를 붙이거나, 워커에 알리거나.
|
|
26
|
+
* 연결 전 구독도 여기로 오므로, 아직 붙일 곳이 없으면 조용히 넘어가야 합니다.
|
|
27
|
+
*/
|
|
28
|
+
protected abstract registerSubscription(type: string): void;
|
|
29
|
+
abstract get status(): SseStatus;
|
|
30
|
+
abstract connect(): void;
|
|
31
|
+
abstract disconnect(): void;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=BaseSseClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseSseClient.d.ts","sourceRoot":"","sources":["../../../src/core/BaseSseClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,eAAe,EACf,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACT,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB;;;;GAIG;AACH,8BAAsB,aAAa,CACjC,CAAC,SAAS,MAAM,GAAG,WAAW,CAC9B,YAAW,aAAa,CAAC,CAAC,CAAC;IAKf,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC;IAJtC,SAAiB,UAAU,CAAC,EAAE,CAAC,CAAC;IAEhC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;gBAEL,MAAM,CAAC,uBAAW;IAIjD,EAAE,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,EAC1B,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GACxC,WAAW;IAUd,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,WAAW;IAI5D;;;;;;;OAOG;IACH,gBAAgB,IAAI,WAAW;IAsC/B;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAE3D,QAAQ,KAAK,MAAM,IAAI,SAAS,CAAC;IACjC,QAAQ,CAAC,OAAO,IAAI,IAAI;IACxB,QAAQ,CAAC,UAAU,IAAI,IAAI;CAC5B"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import _classCallCheck from '@babel/runtime/helpers/classCallCheck';
|
|
2
|
+
import _createClass from '@babel/runtime/helpers/createClass';
|
|
3
|
+
import _defineProperty from '@babel/runtime/helpers/defineProperty';
|
|
4
|
+
import { createEmitter } from './emitter.esm.js';
|
|
5
|
+
import { isUndeliverableEvent } from './eventNames.esm.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @description 두 구현이 공유하는 구독 표면 — 구독 장부(emitter)를 소유하고 on·onStatus를 확정합니다.
|
|
9
|
+
* 연결을 어떻게 다루는지는 모릅니다: 구현마다 다른 것만 추상 멤버로 남깁니다.
|
|
10
|
+
* 같아야 할 것을 한 벌로 두기 위한 클래스입니다 — 두 구현이 각자 배선하다 갈린 이력이 있습니다.
|
|
11
|
+
*/
|
|
12
|
+
var BaseSseClient = /*#__PURE__*/function () {
|
|
13
|
+
function BaseSseClient(logger) {
|
|
14
|
+
_classCallCheck(this, BaseSseClient);
|
|
15
|
+
_defineProperty(this, "emitter", void 0);
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
this.emitter = createEmitter(logger);
|
|
18
|
+
}
|
|
19
|
+
return _createClass(BaseSseClient, [{
|
|
20
|
+
key: "on",
|
|
21
|
+
value: function on(type, handler) {
|
|
22
|
+
if (isUndeliverableEvent(type)) {
|
|
23
|
+
var _this$logger;
|
|
24
|
+
(_this$logger = this.logger) === null || _this$logger === void 0 || _this$logger.warn("sse: '".concat(type, "'\uB294 SDK\uAC00 \uC18C\uBE44\uD558\uB294 \uC608\uC57D \uC774\uBCA4\uD2B8\uB77C \uAD6C\uB3C5\uC774 \uBC30\uB2EC\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4"));
|
|
25
|
+
}
|
|
26
|
+
this.registerSubscription(type);
|
|
27
|
+
return this.emitter.on(type, handler);
|
|
28
|
+
}
|
|
29
|
+
}, {
|
|
30
|
+
key: "onStatus",
|
|
31
|
+
value: function onStatus(listener) {
|
|
32
|
+
return this.emitter.onStatus(listener);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @description 탭이 돌아올 때 connect()를 다시 부릅니다 — 전원이 얼어붙어 접힌 연결(휴면)은
|
|
37
|
+
* 깨어난 탭의 이 호출만이 다시 세울 수 있습니다. 연결이 살아 있으면 아무 일도 없습니다.
|
|
38
|
+
* bfcache 복귀는 visibilitychange가 아니라 pageshow로만 알 수 있어 둘 다 봅니다.
|
|
39
|
+
* SSR에는 붙일 곳이 없어 아무 일도 하지 않습니다.
|
|
40
|
+
*
|
|
41
|
+
* @returns 리스너를 떼는 함수 — 클라이언트를 버릴 때 함께 부르세요
|
|
42
|
+
*/
|
|
43
|
+
}, {
|
|
44
|
+
key: "attachAutoResume",
|
|
45
|
+
value: function attachAutoResume() {
|
|
46
|
+
var _this = this;
|
|
47
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
|
48
|
+
return function () {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 되살릴 대상은 idle입니다 — 전원 침묵으로 접힌 휴면이 이 상태입니다(workerConnection.close).
|
|
52
|
+
// closed(소비처 disconnect·서버 goaway close)는 끊은 의도를 되살리지 않고,
|
|
53
|
+
// fatal의 회복 계약은 disconnect 후 connect라 자동 재개가 대신할 수 없습니다
|
|
54
|
+
var notifyAlive = function notifyAlive() {
|
|
55
|
+
if (_this.status === 'closed' || _this.status === 'fatal') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
_this.connect();
|
|
59
|
+
};
|
|
60
|
+
var onVisible = function onVisible() {
|
|
61
|
+
if (document.visibilityState === 'visible') {
|
|
62
|
+
notifyAlive();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// persisted = bfcache 복귀, 직결 경로용 — Chrome은 SharedWorker 페이지를 bfcache에 넣지 않고,
|
|
67
|
+
// 넣는 브라우저라면 포트가 이미 죽어 이 connect가 닿지 않습니다(알려진 한계)
|
|
68
|
+
var onPageShow = function onPageShow(event) {
|
|
69
|
+
if (event.persisted) {
|
|
70
|
+
notifyAlive();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
74
|
+
window.addEventListener('pageshow', onPageShow);
|
|
75
|
+
return function () {
|
|
76
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
77
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @description 구독이 등록될 때 각 구현이 할 일 — 열린 소스에 리스너를 붙이거나, 워커에 알리거나.
|
|
83
|
+
* 연결 전 구독도 여기로 오므로, 아직 붙일 곳이 없으면 조용히 넘어가야 합니다.
|
|
84
|
+
*/
|
|
85
|
+
}]);
|
|
86
|
+
}();
|
|
87
|
+
|
|
88
|
+
export { BaseSseClient };
|
|
89
|
+
//# sourceMappingURL=BaseSseClient.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseSseClient.esm.js","sources":["../../../src/core/BaseSseClient.ts"],"sourcesContent":["import type { Emitter } from './emitter';\nimport { createEmitter } from './emitter';\nimport { isUndeliverableEvent } from './eventNames';\nimport type {\n PayloadOf,\n SseClientLike,\n SseEventHandler,\n SseEventMap,\n SseEventName,\n SseLogger,\n SseStatus,\n Unsubscribe,\n} from './types';\n\n/**\n * @description 두 구현이 공유하는 구독 표면 — 구독 장부(emitter)를 소유하고 on·onStatus를 확정합니다.\n * 연결을 어떻게 다루는지는 모릅니다: 구현마다 다른 것만 추상 멤버로 남깁니다.\n * 같아야 할 것을 한 벌로 두기 위한 클래스입니다 — 두 구현이 각자 배선하다 갈린 이력이 있습니다.\n */\nexport abstract class BaseSseClient<\n M extends object = SseEventMap,\n> implements SseClientLike<M> {\n declare readonly __eventMap?: M;\n\n protected readonly emitter: Emitter;\n\n constructor(protected readonly logger?: SseLogger) {\n this.emitter = createEmitter(logger);\n }\n\n on<K extends SseEventName<M>>(\n type: K,\n handler: SseEventHandler<PayloadOf<M, K>>\n ): Unsubscribe {\n if (isUndeliverableEvent(type)) {\n this.logger?.warn(\n `sse: '${type}'는 SDK가 소비하는 예약 이벤트라 구독이 배달되지 않습니다`\n );\n }\n this.registerSubscription(type);\n return this.emitter.on(type, handler as SseEventHandler<unknown>);\n }\n\n onStatus(listener: (status: SseStatus) => void): Unsubscribe {\n return this.emitter.onStatus(listener);\n }\n\n /**\n * @description 탭이 돌아올 때 connect()를 다시 부릅니다 — 전원이 얼어붙어 접힌 연결(휴면)은\n * 깨어난 탭의 이 호출만이 다시 세울 수 있습니다. 연결이 살아 있으면 아무 일도 없습니다.\n * bfcache 복귀는 visibilitychange가 아니라 pageshow로만 알 수 있어 둘 다 봅니다.\n * SSR에는 붙일 곳이 없어 아무 일도 하지 않습니다.\n *\n * @returns 리스너를 떼는 함수 — 클라이언트를 버릴 때 함께 부르세요\n */\n attachAutoResume(): Unsubscribe {\n if (typeof document === 'undefined' || typeof window === 'undefined') {\n return () => {};\n }\n\n // 되살릴 대상은 idle입니다 — 전원 침묵으로 접힌 휴면이 이 상태입니다(workerConnection.close).\n // closed(소비처 disconnect·서버 goaway close)는 끊은 의도를 되살리지 않고,\n // fatal의 회복 계약은 disconnect 후 connect라 자동 재개가 대신할 수 없습니다\n const notifyAlive = (): void => {\n if (this.status === 'closed' || this.status === 'fatal') {\n return;\n }\n this.connect();\n };\n\n const onVisible = (): void => {\n if (document.visibilityState === 'visible') {\n notifyAlive();\n }\n };\n\n // persisted = bfcache 복귀, 직결 경로용 — Chrome은 SharedWorker 페이지를 bfcache에 넣지 않고,\n // 넣는 브라우저라면 포트가 이미 죽어 이 connect가 닿지 않습니다(알려진 한계)\n const onPageShow = (event: PageTransitionEvent): void => {\n if (event.persisted) {\n notifyAlive();\n }\n };\n\n document.addEventListener('visibilitychange', onVisible);\n window.addEventListener('pageshow', onPageShow);\n\n return () => {\n document.removeEventListener('visibilitychange', onVisible);\n window.removeEventListener('pageshow', onPageShow);\n };\n }\n\n /**\n * @description 구독이 등록될 때 각 구현이 할 일 — 열린 소스에 리스너를 붙이거나, 워커에 알리거나.\n * 연결 전 구독도 여기로 오므로, 아직 붙일 곳이 없으면 조용히 넘어가야 합니다.\n */\n protected abstract registerSubscription(type: string): void;\n\n abstract get status(): SseStatus;\n abstract connect(): void;\n abstract disconnect(): void;\n}\n"],"names":["BaseSseClient","logger","_classCallCheck","_defineProperty","emitter","createEmitter","_createClass","key","value","on","type","handler","isUndeliverableEvent","_this$logger","warn","concat","registerSubscription","onStatus","listener","attachAutoResume","_this","document","window","notifyAlive","status","connect","onVisible","visibilityState","onPageShow","event","persisted","addEventListener","removeEventListener"],"mappings":";;;;;;AAcA;AACA;AACA;AACA;AACA;AACA,IAAsBA,aAAa,gBAAA,YAAA;EAOjC,SAAAA,aAAAA,CAA+BC,MAAkB,EAAE;AAAAC,IAAAA,eAAA,OAAAF,aAAA,CAAA;IAAAG,eAAA,CAAA,IAAA,EAAA,SAAA,EAAA,MAAA,CAAA;IAAA,IAAA,CAApBF,MAAkB,GAAlBA,MAAkB;AAC/C,IAAA,IAAI,CAACG,OAAO,GAAGC,aAAa,CAACJ,MAAM,CAAC;AACtC;EAAC,OAAAK,YAAA,CAAAN,aAAA,EAAA,CAAA;IAAAO,GAAA,EAAA,IAAA;AAAAC,IAAAA,KAAA,EAED,SAAAC,EAAAA,CACEC,IAAO,EACPC,OAAyC,EAC5B;AACb,MAAA,IAAIC,oBAAoB,CAACF,IAAI,CAAC,EAAE;AAAA,QAAA,IAAAG,YAAA;AAC9B,QAAA,CAAAA,YAAA,GAAA,IAAI,CAACZ,MAAM,cAAAY,YAAA,KAAA,MAAA,IAAXA,YAAA,CAAaC,IAAI,CAAA,QAAA,CAAAC,MAAA,CACNL,IAAI,0JACf,CAAC;AACH;AACA,MAAA,IAAI,CAACM,oBAAoB,CAACN,IAAI,CAAC;MAC/B,OAAO,IAAI,CAACN,OAAO,CAACK,EAAE,CAACC,IAAI,EAAEC,OAAmC,CAAC;AACnE;AAAC,GAAA,EAAA;IAAAJ,GAAA,EAAA,UAAA;AAAAC,IAAAA,KAAA,EAED,SAAAS,QAAAA,CAASC,QAAqC,EAAe;AAC3D,MAAA,OAAO,IAAI,CAACd,OAAO,CAACa,QAAQ,CAACC,QAAQ,CAAC;AACxC;;AAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AAPE,GAAA,EAAA;IAAAX,GAAA,EAAA,kBAAA;IAAAC,KAAA,EAQA,SAAAW,gBAAAA,GAAgC;AAAA,MAAA,IAAAC,KAAA,GAAA,IAAA;MAC9B,IAAI,OAAOC,QAAQ,KAAK,WAAW,IAAI,OAAOC,MAAM,KAAK,WAAW,EAAE;QACpE,OAAO,YAAM,EAAE;AACjB;;AAEA;AACA;AACA;AACA,MAAA,IAAMC,WAAW,GAAG,SAAdA,WAAWA,GAAe;QAC9B,IAAIH,KAAI,CAACI,MAAM,KAAK,QAAQ,IAAIJ,KAAI,CAACI,MAAM,KAAK,OAAO,EAAE;AACvD,UAAA;AACF;QACAJ,KAAI,CAACK,OAAO,EAAE;OACf;AAED,MAAA,IAAMC,SAAS,GAAG,SAAZA,SAASA,GAAe;AAC5B,QAAA,IAAIL,QAAQ,CAACM,eAAe,KAAK,SAAS,EAAE;AAC1CJ,UAAAA,WAAW,EAAE;AACf;OACD;;AAED;AACA;AACA,MAAA,IAAMK,UAAU,GAAG,SAAbA,UAAUA,CAAIC,KAA0B,EAAW;QACvD,IAAIA,KAAK,CAACC,SAAS,EAAE;AACnBP,UAAAA,WAAW,EAAE;AACf;OACD;AAEDF,MAAAA,QAAQ,CAACU,gBAAgB,CAAC,kBAAkB,EAAEL,SAAS,CAAC;AACxDJ,MAAAA,MAAM,CAACS,gBAAgB,CAAC,UAAU,EAAEH,UAAU,CAAC;AAE/C,MAAA,OAAO,YAAM;AACXP,QAAAA,QAAQ,CAACW,mBAAmB,CAAC,kBAAkB,EAAEN,SAAS,CAAC;AAC3DJ,QAAAA,MAAM,CAACU,mBAAmB,CAAC,UAAU,EAAEJ,UAAU,CAAC;OACnD;AACH;;AAEA;AACF;AACA;AACA;AAHE,GAAA,CAAA,CAAA;AAAA,CAAA;;;;"}
|