@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 @@
|
|
|
1
|
+
{"version":3,"file":"sse.worker.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@remember-web/sse",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Remember Web SSE Client SDK",
|
|
5
|
+
"homepage": "https://dramancompany.github.io/rwp",
|
|
6
|
+
"author": "Remember",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/dramancompany/rwp.git"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"registry": "https://registry.npmjs.org/"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "rollup -c && rollup -c rollup.config.worker.mjs && yarn fix:dts-alias",
|
|
18
|
+
"fix:dts-alias": "yarn tsc-alias -p tsconfig.alias.json",
|
|
19
|
+
"type-check": "tsc -p ./tsconfig.cli.json",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest watch"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"source": "./src/index.ts",
|
|
28
|
+
"main": "./dist/src/index.cjs.js",
|
|
29
|
+
"module": "./dist/src/index.esm.js",
|
|
30
|
+
"types": "./dist/src/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/src/index.d.ts",
|
|
34
|
+
"import": "./dist/src/index.esm.js",
|
|
35
|
+
"require": "./dist/src/index.cjs.js"
|
|
36
|
+
},
|
|
37
|
+
"./react": {
|
|
38
|
+
"types": "./dist/src/react/index.d.ts",
|
|
39
|
+
"import": "./dist/src/react/index.esm.js",
|
|
40
|
+
"require": "./dist/src/react/index.cjs.js"
|
|
41
|
+
},
|
|
42
|
+
"./worker": "./dist/worker/sse.worker.js"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@babel/runtime": "^7.24.4",
|
|
46
|
+
"@remember-web/shared": "^0.1.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@babel/core": "^7.24.4",
|
|
50
|
+
"@babel/plugin-transform-runtime": "^7.24.3",
|
|
51
|
+
"@babel/preset-env": "^7.24.4",
|
|
52
|
+
"@babel/preset-react": "^7.24.1",
|
|
53
|
+
"@babel/preset-typescript": "^7.24.1",
|
|
54
|
+
"@rollup/plugin-babel": "^6.0.4",
|
|
55
|
+
"@rollup/plugin-commonjs": "^28.0.6",
|
|
56
|
+
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
57
|
+
"@testing-library/dom": "^10.4.0",
|
|
58
|
+
"@testing-library/react": "^16.3.0",
|
|
59
|
+
"@types/react": "^18.2.77",
|
|
60
|
+
"@types/react-dom": "^18.2.25",
|
|
61
|
+
"jsdom": "^26.1.0",
|
|
62
|
+
"react": "^18.2.0",
|
|
63
|
+
"react-dom": "^18.2.0",
|
|
64
|
+
"rollup": "^4.44.1",
|
|
65
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
66
|
+
"rollup-plugin-preserve-directives": "^0.4.0",
|
|
67
|
+
"rollup-plugin-typescript2": "^0.36.0",
|
|
68
|
+
"tsc-alias": "^1.8.8",
|
|
69
|
+
"tsconfig": "0.0.2",
|
|
70
|
+
"tslib": "^2.6.2",
|
|
71
|
+
"typescript": "^5.4.5",
|
|
72
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
73
|
+
"vitest": "^3.2.4"
|
|
74
|
+
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
77
|
+
"typescript": ">=5.4"
|
|
78
|
+
},
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"react": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"typescript": {
|
|
84
|
+
"optional": true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// @vitest-environment jsdom — attachAutoResume가 document·window에 배선하는 유일한 표면이라 이 파일만 jsdom입니다.
|
|
2
|
+
// 나머지 공유 표면(on·onStatus)은 SseClientLike.contract.test.ts가 두 구현으로 고정합니다
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createSseClient } from '../createSseClient';
|
|
6
|
+
import {
|
|
7
|
+
createHarness,
|
|
8
|
+
FakeEventSource,
|
|
9
|
+
lastSource,
|
|
10
|
+
openConnection,
|
|
11
|
+
setupFakeTimers,
|
|
12
|
+
} from '../testing';
|
|
13
|
+
|
|
14
|
+
setupFakeTimers();
|
|
15
|
+
|
|
16
|
+
const createClientSpy = () => {
|
|
17
|
+
let connects = 0;
|
|
18
|
+
const client = createSseClient({
|
|
19
|
+
baseUrl: 'https://sse.example.com',
|
|
20
|
+
getAuthToken: () => 'token-1',
|
|
21
|
+
});
|
|
22
|
+
// connect 자체를 재우고 호출만 셉니다 — 여기서 볼 것은 "언제 불리는가"뿐입니다
|
|
23
|
+
client.connect = () => {
|
|
24
|
+
connects += 1;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return { client, connects: () => connects };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const setVisibility = (state: DocumentVisibilityState): void => {
|
|
31
|
+
Object.defineProperty(document, 'visibilityState', {
|
|
32
|
+
value: state,
|
|
33
|
+
configurable: true,
|
|
34
|
+
});
|
|
35
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const showPage = (persisted: boolean): void => {
|
|
39
|
+
window.dispatchEvent(new PageTransitionEvent('pageshow', { persisted }));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe('attachAutoResume — 탭 복귀 배선', () => {
|
|
43
|
+
it('탭이 다시 보이면 connect를 부릅니다 — 얼어붙은 사이 수거됐어도 돌아옵니다', () => {
|
|
44
|
+
const { client, connects } = createClientSpy();
|
|
45
|
+
client.attachAutoResume();
|
|
46
|
+
|
|
47
|
+
setVisibility('visible');
|
|
48
|
+
|
|
49
|
+
expect(connects()).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('탭이 숨을 때는 부르지 않습니다', () => {
|
|
53
|
+
const { client, connects } = createClientSpy();
|
|
54
|
+
client.attachAutoResume();
|
|
55
|
+
|
|
56
|
+
setVisibility('hidden');
|
|
57
|
+
|
|
58
|
+
expect(connects()).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// bfcache는 SharedWorker 포트를 evict하므로 visibilitychange로는 알 수 없습니다
|
|
62
|
+
it('bfcache에서 돌아오면 connect를 부릅니다', () => {
|
|
63
|
+
const { client, connects } = createClientSpy();
|
|
64
|
+
client.attachAutoResume();
|
|
65
|
+
|
|
66
|
+
showPage(true);
|
|
67
|
+
|
|
68
|
+
expect(connects()).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('bfcache가 아닌 첫 로드에는 부르지 않습니다 — connect는 소비처가 이미 불렀습니다', () => {
|
|
72
|
+
const { client, connects } = createClientSpy();
|
|
73
|
+
client.attachAutoResume();
|
|
74
|
+
|
|
75
|
+
showPage(false);
|
|
76
|
+
|
|
77
|
+
expect(connects()).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('떼면 더 부르지 않습니다 — 클라이언트를 버릴 때 누수가 남지 않게', () => {
|
|
81
|
+
const { client, connects } = createClientSpy();
|
|
82
|
+
const detach = client.attachAutoResume();
|
|
83
|
+
|
|
84
|
+
detach();
|
|
85
|
+
setVisibility('visible');
|
|
86
|
+
showPage(true);
|
|
87
|
+
|
|
88
|
+
expect(connects()).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('attachAutoResume — 끊은 쪽의 의도 존중', () => {
|
|
93
|
+
it('disconnect한 뒤에는 탭이 돌아와도 되살리지 않습니다 — 끊은 건 소비처의 판단입니다', () => {
|
|
94
|
+
const { client, connects } = createClientSpy();
|
|
95
|
+
client.attachAutoResume();
|
|
96
|
+
client.disconnect();
|
|
97
|
+
|
|
98
|
+
setVisibility('visible');
|
|
99
|
+
|
|
100
|
+
expect(connects()).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('서버가 goaway(action: close)로 끊은 뒤에는 탭이 돌아와도 되살리지 않습니다', async () => {
|
|
104
|
+
const { client } = createHarness();
|
|
105
|
+
await openConnection(client);
|
|
106
|
+
client.attachAutoResume();
|
|
107
|
+
|
|
108
|
+
lastSource().dispatch(
|
|
109
|
+
'goaway',
|
|
110
|
+
JSON.stringify({ reason: 'user_logout', action: 'close' })
|
|
111
|
+
);
|
|
112
|
+
expect(client.status).toBe('closed');
|
|
113
|
+
|
|
114
|
+
setVisibility('visible');
|
|
115
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
116
|
+
|
|
117
|
+
// 자동 재개가 서버의 close를 뒤집으면 재연결 티켓과 새 소스가 생깁니다
|
|
118
|
+
expect(FakeEventSource.instances).toHaveLength(1);
|
|
119
|
+
expect(client.status).toBe('closed');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// fatal의 회복 계약은 disconnect 후 connect라 자동 재개가 대신할 수 없습니다 — 시도하면 탭 전환마다 warn만 쌓입니다
|
|
123
|
+
it('fatal 상태에서는 탭이 돌아와도 connect를 시도하지 않습니다', async () => {
|
|
124
|
+
const warnings: string[] = [];
|
|
125
|
+
const { client, failTicketsWith } = createHarness({
|
|
126
|
+
logger: { warn: (message) => warnings.push(message), error: () => {} },
|
|
127
|
+
});
|
|
128
|
+
failTicketsWith(401);
|
|
129
|
+
client.connect();
|
|
130
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
131
|
+
expect(client.status).toBe('fatal');
|
|
132
|
+
client.attachAutoResume();
|
|
133
|
+
const warningsBefore = warnings.length;
|
|
134
|
+
|
|
135
|
+
setVisibility('visible');
|
|
136
|
+
setVisibility('visible');
|
|
137
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
138
|
+
|
|
139
|
+
expect(warnings).toHaveLength(warningsBefore);
|
|
140
|
+
expect(client.status).toBe('fatal');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Emitter } from './emitter';
|
|
2
|
+
import { createEmitter } from './emitter';
|
|
3
|
+
import { isUndeliverableEvent } from './eventNames';
|
|
4
|
+
import type {
|
|
5
|
+
PayloadOf,
|
|
6
|
+
SseClientLike,
|
|
7
|
+
SseEventHandler,
|
|
8
|
+
SseEventMap,
|
|
9
|
+
SseEventName,
|
|
10
|
+
SseLogger,
|
|
11
|
+
SseStatus,
|
|
12
|
+
Unsubscribe,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @description 두 구현이 공유하는 구독 표면 — 구독 장부(emitter)를 소유하고 on·onStatus를 확정합니다.
|
|
17
|
+
* 연결을 어떻게 다루는지는 모릅니다: 구현마다 다른 것만 추상 멤버로 남깁니다.
|
|
18
|
+
* 같아야 할 것을 한 벌로 두기 위한 클래스입니다 — 두 구현이 각자 배선하다 갈린 이력이 있습니다.
|
|
19
|
+
*/
|
|
20
|
+
export abstract class BaseSseClient<
|
|
21
|
+
M extends object = SseEventMap,
|
|
22
|
+
> implements SseClientLike<M> {
|
|
23
|
+
declare readonly __eventMap?: M;
|
|
24
|
+
|
|
25
|
+
protected readonly emitter: Emitter;
|
|
26
|
+
|
|
27
|
+
constructor(protected readonly logger?: SseLogger) {
|
|
28
|
+
this.emitter = createEmitter(logger);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
on<K extends SseEventName<M>>(
|
|
32
|
+
type: K,
|
|
33
|
+
handler: SseEventHandler<PayloadOf<M, K>>
|
|
34
|
+
): Unsubscribe {
|
|
35
|
+
if (isUndeliverableEvent(type)) {
|
|
36
|
+
this.logger?.warn(
|
|
37
|
+
`sse: '${type}'는 SDK가 소비하는 예약 이벤트라 구독이 배달되지 않습니다`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
this.registerSubscription(type);
|
|
41
|
+
return this.emitter.on(type, handler as SseEventHandler<unknown>);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onStatus(listener: (status: SseStatus) => void): Unsubscribe {
|
|
45
|
+
return this.emitter.onStatus(listener);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @description 탭이 돌아올 때 connect()를 다시 부릅니다 — 전원이 얼어붙어 접힌 연결(휴면)은
|
|
50
|
+
* 깨어난 탭의 이 호출만이 다시 세울 수 있습니다. 연결이 살아 있으면 아무 일도 없습니다.
|
|
51
|
+
* bfcache 복귀는 visibilitychange가 아니라 pageshow로만 알 수 있어 둘 다 봅니다.
|
|
52
|
+
* SSR에는 붙일 곳이 없어 아무 일도 하지 않습니다.
|
|
53
|
+
*
|
|
54
|
+
* @returns 리스너를 떼는 함수 — 클라이언트를 버릴 때 함께 부르세요
|
|
55
|
+
*/
|
|
56
|
+
attachAutoResume(): Unsubscribe {
|
|
57
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
|
58
|
+
return () => {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 되살릴 대상은 idle입니다 — 전원 침묵으로 접힌 휴면이 이 상태입니다(workerConnection.close).
|
|
62
|
+
// closed(소비처 disconnect·서버 goaway close)는 끊은 의도를 되살리지 않고,
|
|
63
|
+
// fatal의 회복 계약은 disconnect 후 connect라 자동 재개가 대신할 수 없습니다
|
|
64
|
+
const notifyAlive = (): void => {
|
|
65
|
+
if (this.status === 'closed' || this.status === 'fatal') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.connect();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const onVisible = (): void => {
|
|
72
|
+
if (document.visibilityState === 'visible') {
|
|
73
|
+
notifyAlive();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// persisted = bfcache 복귀, 직결 경로용 — Chrome은 SharedWorker 페이지를 bfcache에 넣지 않고,
|
|
78
|
+
// 넣는 브라우저라면 포트가 이미 죽어 이 connect가 닿지 않습니다(알려진 한계)
|
|
79
|
+
const onPageShow = (event: PageTransitionEvent): void => {
|
|
80
|
+
if (event.persisted) {
|
|
81
|
+
notifyAlive();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
86
|
+
window.addEventListener('pageshow', onPageShow);
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
90
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @description 구독이 등록될 때 각 구현이 할 일 — 열린 소스에 리스너를 붙이거나, 워커에 알리거나.
|
|
96
|
+
* 연결 전 구독도 여기로 오므로, 아직 붙일 곳이 없으면 조용히 넘어가야 합니다.
|
|
97
|
+
*/
|
|
98
|
+
protected abstract registerSubscription(type: string): void;
|
|
99
|
+
|
|
100
|
+
abstract get status(): SseStatus;
|
|
101
|
+
abstract connect(): void;
|
|
102
|
+
abstract disconnect(): void;
|
|
103
|
+
}
|