@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { TicketResponseMessage } from './protocol';
|
|
2
|
+
|
|
3
|
+
import type { TicketResult } from '../core/ticket';
|
|
4
|
+
import { TICKET_NETWORK_FAILURE, TICKET_TIMEOUT_MS } from '../core/ticket';
|
|
5
|
+
|
|
6
|
+
// 탭↔워커 postMessage 왕복 여유분 — 실측 아닌 임의값입니다
|
|
7
|
+
const PORT_ROUNDTRIP_SLACK_MS = 2000;
|
|
8
|
+
|
|
9
|
+
// 탭의 발급 제한보다 늦게 끝나야 합니다 — 탭이 스스로 실패를 알려줄 기회를 먼저 줍니다
|
|
10
|
+
export const TICKET_RESPONSE_TIMEOUT_MS =
|
|
11
|
+
TICKET_TIMEOUT_MS + PORT_ROUNDTRIP_SLACK_MS;
|
|
12
|
+
|
|
13
|
+
type Resolve = (result: TicketResult) => void;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @description 티켓 발급을 탭에 위임합니다 — 토큰은 탭에 남고 워커는 발급 결과만 받습니다.
|
|
17
|
+
* 워커가 코어 연결 루프를 그대로 쓸 수 있도록, 요청·응답 왕복을 Promise 하나로 바꿉니다.
|
|
18
|
+
*/
|
|
19
|
+
export const createTicketDelegate = (requestFromTab: () => void) => {
|
|
20
|
+
// 왕복은 한 번에 하나 — 겹친 요청은 탭에 다시 묻지 않고 이 결과를 함께 받습니다
|
|
21
|
+
let pending: {
|
|
22
|
+
resolvers: Resolve[];
|
|
23
|
+
timer: ReturnType<typeof setTimeout>;
|
|
24
|
+
} | null = null;
|
|
25
|
+
|
|
26
|
+
const finish = (result: TicketResult): void => {
|
|
27
|
+
if (pending === null) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const { resolvers, timer } = pending;
|
|
31
|
+
pending = null;
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
resolvers.forEach((resolve) => resolve(result));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const startNewRequest = (resolve: Resolve): void => {
|
|
37
|
+
pending = {
|
|
38
|
+
resolvers: [resolve],
|
|
39
|
+
timer: setTimeout(
|
|
40
|
+
() => finish(TICKET_NETWORK_FAILURE),
|
|
41
|
+
TICKET_RESPONSE_TIMEOUT_MS
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
requestFromTab();
|
|
47
|
+
} catch {
|
|
48
|
+
finish(TICKET_NETWORK_FAILURE);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const joinInFlight = (resolve: Resolve): void => {
|
|
53
|
+
pending?.resolvers.push(resolve);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
request: (): Promise<TicketResult> =>
|
|
58
|
+
new Promise<TicketResult>((resolve) => {
|
|
59
|
+
if (pending === null) {
|
|
60
|
+
startNewRequest(resolve);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
joinInFlight(resolve);
|
|
64
|
+
}),
|
|
65
|
+
|
|
66
|
+
/** @description 다음 연결이 이 왕복에 합류해 응답 없이 기다리지 않게 끊습니다 */
|
|
67
|
+
cancel: (): void => finish(TICKET_NETWORK_FAILURE),
|
|
68
|
+
|
|
69
|
+
/** @description 시간 제한 뒤 도착한 응답은 pending이 비어 있어 무시됩니다 */
|
|
70
|
+
settle: (message: TicketResponseMessage): void => {
|
|
71
|
+
const result: TicketResult = message.ok
|
|
72
|
+
? { ok: true, ticket: message.ticket }
|
|
73
|
+
: { ok: false, error: message.error };
|
|
74
|
+
|
|
75
|
+
finish(result);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type TicketDelegate = ReturnType<typeof createTicketDelegate>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { TicketDelegate } from './ticketDelegate';
|
|
2
|
+
|
|
3
|
+
import { FORWARDED_EVENTS } from '../core/eventNames';
|
|
4
|
+
import { SseClient } from '../core/SseClient';
|
|
5
|
+
import type { SseClientOptions, SseStatus, Unsubscribe } from '../core/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @description 워커 스코프에는 토큰이 없어 getAuthToken을 받지 않습니다 — 티켓은 탭이 발급해 넘겨줍니다
|
|
9
|
+
*/
|
|
10
|
+
export type WorkerConnectionOptions = Pick<
|
|
11
|
+
SseClientOptions,
|
|
12
|
+
'fetch' | 'eventSource' | 'backoff' | 'logger'
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
interface WorkerConnectionDeps {
|
|
16
|
+
options: WorkerConnectionOptions;
|
|
17
|
+
ticketDelegate: TicketDelegate;
|
|
18
|
+
onStatus: (status: SseStatus) => void;
|
|
19
|
+
onEvent: (event: string, payload: unknown) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isDeadStatus = (status: SseStatus): boolean =>
|
|
23
|
+
status === 'fatal' || status === 'closed';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @description 모든 탭이 함께 쓰는 워커의 연결 하나 — 수립·정리와 구독 부착을 맡습니다.
|
|
27
|
+
* 탭 장부(hub)는 "탭이 남았는가"만 판단하고, 그 판단을 연결에 적용하는 방법은 여기서만 압니다.
|
|
28
|
+
*/
|
|
29
|
+
export const createWorkerConnection = ({
|
|
30
|
+
options,
|
|
31
|
+
ticketDelegate,
|
|
32
|
+
onStatus,
|
|
33
|
+
onEvent,
|
|
34
|
+
}: WorkerConnectionDeps) => {
|
|
35
|
+
const subscriptions = new Set<string>();
|
|
36
|
+
// 하나로 묶어 셋이 따로 노는 상태를 표현 불가능하게 만듭니다
|
|
37
|
+
let active: {
|
|
38
|
+
client: SseClient;
|
|
39
|
+
detachStatus: Unsubscribe;
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
} | null = null;
|
|
42
|
+
// 인스턴스보다 오래 삽니다 — 재수립 판정이 SseClient 수명에 묶이면 접힌 뒤 복구에서 resync가 사라집니다
|
|
43
|
+
let hasOpenedBefore = false;
|
|
44
|
+
|
|
45
|
+
const attach = (client: SseClient, event: string): void => {
|
|
46
|
+
client.on(event, (payload) => onEvent(event, payload));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const open = (baseUrl: string): void => {
|
|
50
|
+
// 워커 스코프에는 토큰이 없습니다 — 티켓은 issueTicket으로 탭에 위임하므로 이 값은 쓰이지 않습니다
|
|
51
|
+
const client = new SseClient(
|
|
52
|
+
{ ...options, baseUrl, getAuthToken: () => null },
|
|
53
|
+
{ issueTicket: ticketDelegate.request, hasOpenedBefore }
|
|
54
|
+
);
|
|
55
|
+
const detachStatus = client.onStatus((next) => {
|
|
56
|
+
// open에 닿았다는 건 hello를 받았다는 뜻입니다 — 인스턴스가 바뀌어도 이 사실은 남아야 합니다
|
|
57
|
+
if (next === 'open') {
|
|
58
|
+
hasOpenedBefore = true;
|
|
59
|
+
}
|
|
60
|
+
// 이 리스너에 닿는 closed는 goaway(action: close)뿐입니다(자체 close는 먼저 detach) —
|
|
61
|
+
// 서버가 세션을 끝냈으니 직결 경로(#endSession)와 같은 판정으로 다음 연결은 첫 연결입니다
|
|
62
|
+
if (next === 'closed') {
|
|
63
|
+
hasOpenedBefore = false;
|
|
64
|
+
}
|
|
65
|
+
onStatus(next);
|
|
66
|
+
});
|
|
67
|
+
active = { client, detachStatus, baseUrl };
|
|
68
|
+
|
|
69
|
+
FORWARDED_EVENTS.forEach((event) => attach(client, event));
|
|
70
|
+
subscriptions.forEach((event) => attach(client, event));
|
|
71
|
+
client.connect();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @description 연결을 접습니다. endSession이면 재수립 판정도 함께 되돌립니다.
|
|
76
|
+
* 탭이 모두 스스로 떠났으면 세션이 끝난 것이고, 다음 탭에게는 첫 연결입니다 —
|
|
77
|
+
* 이어서 두면 신규 탭이 놓친 구간도 없이 resync를 받습니다.
|
|
78
|
+
*/
|
|
79
|
+
const close = ({ endSession = false } = {}): void => {
|
|
80
|
+
if (endSession) {
|
|
81
|
+
hasOpenedBefore = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (active === null) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// 정리 중 나오는 closed는 워커 내부 사정입니다 — 탭에 전하기 전에 통지를 끊습니다
|
|
88
|
+
active.detachStatus();
|
|
89
|
+
active.client.disconnect();
|
|
90
|
+
active = null;
|
|
91
|
+
|
|
92
|
+
ticketDelegate.cancel();
|
|
93
|
+
// 연결이 없어졌다는 사실은 알려야 합니다 — 안 그러면 뒤에 합류한 탭이 낡은 open 스냅샷을 받습니다
|
|
94
|
+
onStatus('idle');
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ensureOpen: (baseUrl: string): void => {
|
|
99
|
+
if (active !== null && isDeadStatus(active.client.status)) {
|
|
100
|
+
close();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (active === null) {
|
|
104
|
+
open(baseUrl);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 선 연결을 그대로 씁니다 — 다른 baseUrl로 붙으려던 탭은 그 사실을 알 길이 없어 알립니다
|
|
109
|
+
if (baseUrl !== active.baseUrl) {
|
|
110
|
+
options.logger?.warn(
|
|
111
|
+
`sse: 이미 ${active.baseUrl}로 연결돼 있어 baseUrl(${baseUrl})을 무시합니다`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
close,
|
|
117
|
+
|
|
118
|
+
/** @description 선 연결에는 즉시, 이후 연결에는 수립 시점에 붙습니다 */
|
|
119
|
+
subscribe: (event: string): void => {
|
|
120
|
+
if (subscriptions.has(event)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
subscriptions.add(event);
|
|
125
|
+
if (active !== null) {
|
|
126
|
+
attach(active.client, event);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
subscribedEvents: (): string[] => [...subscriptions],
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type WorkerConnection = ReturnType<typeof createWorkerConnection>;
|