@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.
Files changed (160) hide show
  1. package/README.md +238 -0
  2. package/dist/src/core/BaseSseClient.cjs.js +97 -0
  3. package/dist/src/core/BaseSseClient.cjs.js.map +1 -0
  4. package/dist/src/core/BaseSseClient.d.ts +33 -0
  5. package/dist/src/core/BaseSseClient.d.ts.map +1 -0
  6. package/dist/src/core/BaseSseClient.esm.js +89 -0
  7. package/dist/src/core/BaseSseClient.esm.js.map +1 -0
  8. package/dist/src/core/SseClient.cjs.js +382 -0
  9. package/dist/src/core/SseClient.cjs.js.map +1 -0
  10. package/dist/src/core/SseClient.d.ts +24 -0
  11. package/dist/src/core/SseClient.d.ts.map +1 -0
  12. package/dist/src/core/SseClient.esm.js +370 -0
  13. package/dist/src/core/SseClient.esm.js.map +1 -0
  14. package/dist/src/core/backoff.cjs.js +30 -0
  15. package/dist/src/core/backoff.cjs.js.map +1 -0
  16. package/dist/src/core/backoff.d.ts +12 -0
  17. package/dist/src/core/backoff.d.ts.map +1 -0
  18. package/dist/src/core/backoff.esm.js +28 -0
  19. package/dist/src/core/backoff.esm.js.map +1 -0
  20. package/dist/src/core/emitter.cjs.js +57 -0
  21. package/dist/src/core/emitter.cjs.js.map +1 -0
  22. package/dist/src/core/emitter.d.ts +14 -0
  23. package/dist/src/core/emitter.d.ts.map +1 -0
  24. package/dist/src/core/emitter.esm.js +51 -0
  25. package/dist/src/core/emitter.esm.js.map +1 -0
  26. package/dist/src/core/eventNames.cjs.js +64 -0
  27. package/dist/src/core/eventNames.cjs.js.map +1 -0
  28. package/dist/src/core/eventNames.d.ts +31 -0
  29. package/dist/src/core/eventNames.d.ts.map +1 -0
  30. package/dist/src/core/eventNames.esm.js +54 -0
  31. package/dist/src/core/eventNames.esm.js.map +1 -0
  32. package/dist/src/core/retryWait.cjs.js +20 -0
  33. package/dist/src/core/retryWait.cjs.js.map +1 -0
  34. package/dist/src/core/retryWait.d.ts +6 -0
  35. package/dist/src/core/retryWait.d.ts.map +1 -0
  36. package/dist/src/core/retryWait.esm.js +18 -0
  37. package/dist/src/core/retryWait.esm.js.map +1 -0
  38. package/dist/src/core/statusMachine.cjs.js +56 -0
  39. package/dist/src/core/statusMachine.cjs.js.map +1 -0
  40. package/dist/src/core/statusMachine.d.ts +9 -0
  41. package/dist/src/core/statusMachine.d.ts.map +1 -0
  42. package/dist/src/core/statusMachine.esm.js +54 -0
  43. package/dist/src/core/statusMachine.esm.js.map +1 -0
  44. package/dist/src/core/stream.cjs.js +136 -0
  45. package/dist/src/core/stream.cjs.js.map +1 -0
  46. package/dist/src/core/stream.d.ts +42 -0
  47. package/dist/src/core/stream.d.ts.map +1 -0
  48. package/dist/src/core/stream.esm.js +128 -0
  49. package/dist/src/core/stream.esm.js.map +1 -0
  50. package/dist/src/core/ticket.cjs.js +237 -0
  51. package/dist/src/core/ticket.cjs.js.map +1 -0
  52. package/dist/src/core/ticket.d.ts +31 -0
  53. package/dist/src/core/ticket.d.ts.map +1 -0
  54. package/dist/src/core/ticket.esm.js +220 -0
  55. package/dist/src/core/ticket.esm.js.map +1 -0
  56. package/dist/src/core/types.cjs.js +54 -0
  57. package/dist/src/core/types.cjs.js.map +1 -0
  58. package/dist/src/core/types.d.ts +131 -0
  59. package/dist/src/core/types.d.ts.map +1 -0
  60. package/dist/src/core/types.esm.js +50 -0
  61. package/dist/src/core/types.esm.js.map +1 -0
  62. package/dist/src/core/wire.cjs.js +29 -0
  63. package/dist/src/core/wire.cjs.js.map +1 -0
  64. package/dist/src/core/wire.d.ts +7 -0
  65. package/dist/src/core/wire.d.ts.map +1 -0
  66. package/dist/src/core/wire.esm.js +23 -0
  67. package/dist/src/core/wire.esm.js.map +1 -0
  68. package/dist/src/createSseClient.cjs.js +30 -0
  69. package/dist/src/createSseClient.cjs.js.map +1 -0
  70. package/dist/src/createSseClient.d.ts +8 -0
  71. package/dist/src/createSseClient.d.ts.map +1 -0
  72. package/dist/src/createSseClient.esm.js +28 -0
  73. package/dist/src/createSseClient.esm.js.map +1 -0
  74. package/dist/src/index.cjs.js +10 -0
  75. package/dist/src/index.cjs.js.map +1 -0
  76. package/dist/src/index.d.ts +6 -0
  77. package/dist/src/index.d.ts.map +1 -0
  78. package/dist/src/index.esm.js +3 -0
  79. package/dist/src/index.esm.js.map +1 -0
  80. package/dist/src/react/createSseContext.cjs.js +130 -0
  81. package/dist/src/react/createSseContext.cjs.js.map +1 -0
  82. package/dist/src/react/createSseContext.d.ts +32 -0
  83. package/dist/src/react/createSseContext.d.ts.map +1 -0
  84. package/dist/src/react/createSseContext.esm.js +122 -0
  85. package/dist/src/react/createSseContext.esm.js.map +1 -0
  86. package/dist/src/react/index.cjs.js +8 -0
  87. package/dist/src/react/index.cjs.js.map +1 -0
  88. package/dist/src/react/index.d.ts +3 -0
  89. package/dist/src/react/index.d.ts.map +1 -0
  90. package/dist/src/react/index.esm.js +2 -0
  91. package/dist/src/react/index.esm.js.map +1 -0
  92. package/dist/src/testing/doubles.d.ts +50 -0
  93. package/dist/src/testing/doubles.d.ts.map +1 -0
  94. package/dist/src/testing/harness.d.ts +43 -0
  95. package/dist/src/testing/harness.d.ts.map +1 -0
  96. package/dist/src/testing/index.d.ts +3 -0
  97. package/dist/src/testing/index.d.ts.map +1 -0
  98. package/dist/src/worker/WorkerSseClient.cjs.js +240 -0
  99. package/dist/src/worker/WorkerSseClient.cjs.js.map +1 -0
  100. package/dist/src/worker/WorkerSseClient.d.ts +23 -0
  101. package/dist/src/worker/WorkerSseClient.d.ts.map +1 -0
  102. package/dist/src/worker/WorkerSseClient.esm.js +228 -0
  103. package/dist/src/worker/WorkerSseClient.esm.js.map +1 -0
  104. package/dist/src/worker/hub.d.ts +19 -0
  105. package/dist/src/worker/hub.d.ts.map +1 -0
  106. package/dist/src/worker/portRegistry.d.ts +41 -0
  107. package/dist/src/worker/portRegistry.d.ts.map +1 -0
  108. package/dist/src/worker/protocol.d.ts +63 -0
  109. package/dist/src/worker/protocol.d.ts.map +1 -0
  110. package/dist/src/worker/sse.worker.d.ts +2 -0
  111. package/dist/src/worker/sse.worker.d.ts.map +1 -0
  112. package/dist/src/worker/ticketDelegate.d.ts +16 -0
  113. package/dist/src/worker/ticketDelegate.d.ts.map +1 -0
  114. package/dist/src/worker/workerConnection.d.ts +28 -0
  115. package/dist/src/worker/workerConnection.d.ts.map +1 -0
  116. package/dist/worker/sse.worker.js +1865 -0
  117. package/dist/worker/sse.worker.js.map +1 -0
  118. package/package.json +87 -0
  119. package/src/core/BaseSseClient.test.ts +142 -0
  120. package/src/core/BaseSseClient.ts +103 -0
  121. package/src/core/SseClient.connection.test.ts +863 -0
  122. package/src/core/SseClient.scenarios.test.ts +164 -0
  123. package/src/core/SseClient.test.ts +63 -0
  124. package/src/core/SseClient.ts +310 -0
  125. package/src/core/backoff.test.ts +43 -0
  126. package/src/core/backoff.ts +39 -0
  127. package/src/core/emitter.ts +56 -0
  128. package/src/core/eventNames.ts +53 -0
  129. package/src/core/retryWait.ts +18 -0
  130. package/src/core/statusMachine.ts +56 -0
  131. package/src/core/stream.ts +141 -0
  132. package/src/core/ticket.ts +146 -0
  133. package/src/core/types.ts +182 -0
  134. package/src/core/wire.ts +23 -0
  135. package/src/createSseClient.test.ts +82 -0
  136. package/src/createSseClient.ts +34 -0
  137. package/src/index.ts +21 -0
  138. package/src/react/SseProvider.test.tsx +129 -0
  139. package/src/react/createSseContext.test.ts +47 -0
  140. package/src/react/createSseContext.tsx +140 -0
  141. package/src/react/index.ts +2 -0
  142. package/src/react/useSseEvent.test.tsx +71 -0
  143. package/src/react/useSseStatus.test.tsx +40 -0
  144. package/src/testing/doubles.ts +128 -0
  145. package/src/testing/harness.ts +82 -0
  146. package/src/testing/index.ts +3 -0
  147. package/src/worker/SseClientLike.contract.test.ts +270 -0
  148. package/src/worker/WorkerSseClient.scenarios.test.ts +179 -0
  149. package/src/worker/WorkerSseClient.test.ts +377 -0
  150. package/src/worker/WorkerSseClient.ts +190 -0
  151. package/src/worker/hub.connection.test.ts +1023 -0
  152. package/src/worker/hub.test.ts +213 -0
  153. package/src/worker/hub.ts +120 -0
  154. package/src/worker/portRegistry.test.ts +59 -0
  155. package/src/worker/portRegistry.ts +86 -0
  156. package/src/worker/protocol.ts +80 -0
  157. package/src/worker/sse.worker.ts +19 -0
  158. package/src/worker/ticketDelegate.test.ts +99 -0
  159. package/src/worker/ticketDelegate.ts +80 -0
  160. package/src/worker/workerConnection.ts +134 -0
@@ -0,0 +1,82 @@
1
+ // @vitest-environment node
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { SseClient } from './core/SseClient';
5
+ import type { SseClientOptions } from './core/types';
6
+ import { createSseClient } from './createSseClient';
7
+ import { WorkerSseClient } from './worker/WorkerSseClient';
8
+
9
+ const createOptions = (
10
+ overrides?: Partial<SseClientOptions>
11
+ ): SseClientOptions & { warnings: string[] } => {
12
+ const warnings: string[] = [];
13
+ return {
14
+ baseUrl: 'https://sse.example.com',
15
+ getAuthToken: () => 'token-1',
16
+ logger: { warn: (message) => warnings.push(message), error: () => {} },
17
+ warnings,
18
+ ...overrides,
19
+ };
20
+ };
21
+
22
+ afterEach(() => {
23
+ vi.unstubAllGlobals();
24
+ });
25
+
26
+ describe('createSseClient โ€” ๊ตฌํ˜„ ์„ ํƒ', () => {
27
+ it('sharedWorker ํŒฉํ† ๋ฆฌ๋ฅผ ์ œ๊ณตํ•˜๊ณ  ๋ธŒ๋ผ์šฐ์ €๊ฐ€ SharedWorker๋ฅผ ์ง€์›ํ•˜๋ฉด ํƒญ ๊ฐ„ ๊ณต์œ  ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค', () => {
28
+ vi.stubGlobal('SharedWorker', class {});
29
+ const options = createOptions({
30
+ sharedWorker: () => ({}) as SharedWorker,
31
+ });
32
+
33
+ const client = createSseClient(options);
34
+
35
+ expect(client).toBeInstanceOf(WorkerSseClient);
36
+ expect(options.warnings).toHaveLength(0);
37
+ });
38
+
39
+ it('sharedWorker ํŒฉํ† ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ํƒญ๋ณ„ ์ง์ ‘ ์—ฐ๊ฒฐ ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค', () => {
40
+ const client = createSseClient(createOptions());
41
+
42
+ expect(client).toBeInstanceOf(SseClient);
43
+ });
44
+
45
+ it('sharedWorker๋ฅผ ์ œ๊ณตํ•ด๋„ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ SharedWorker๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋ฉด ์ง์ ‘ ์—ฐ๊ฒฐ๋กœ ๋Œ€์ฒดํ•˜๊ณ  logger.warn์œผ๋กœ ์•Œ๋ฆฝ๋‹ˆ๋‹ค', () => {
46
+ vi.stubGlobal('document', {});
47
+ const options = createOptions({
48
+ sharedWorker: () => ({}) as SharedWorker,
49
+ });
50
+
51
+ const client = createSseClient(options);
52
+
53
+ expect(client).toBeInstanceOf(SseClient);
54
+ expect(options.warnings).toHaveLength(1);
55
+ expect(options.warnings[0]).toContain('SharedWorker');
56
+ });
57
+
58
+ it('SSRยท์›Œ์ปค ์Šค์ฝ”ํ”„(document ์—†์Œ)์—์„œ๋Š” ์ง์ ‘ ์—ฐ๊ฒฐ๋กœ ๋Œ€์ฒดํ•˜๋˜ ๊ฒฝ๊ณ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค', () => {
59
+ const options = createOptions({
60
+ sharedWorker: () => ({}) as SharedWorker,
61
+ });
62
+
63
+ const client = createSseClient(options);
64
+
65
+ expect(client).toBeInstanceOf(SseClient);
66
+ expect(options.warnings).toHaveLength(0);
67
+ });
68
+
69
+ it('์›Œ์ปค ๊ฒฝ๋กœ์— eventSource๋ฅผ ํ•จ๊ป˜ ์ฃผ๋ฉด ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค๊ณ  logger.warn์œผ๋กœ ์•Œ๋ฆฝ๋‹ˆ๋‹ค', () => {
70
+ vi.stubGlobal('SharedWorker', class {});
71
+ const options = createOptions({
72
+ sharedWorker: () => ({}) as SharedWorker,
73
+ eventSource: class {} as unknown as typeof EventSource,
74
+ });
75
+
76
+ const client = createSseClient(options);
77
+
78
+ expect(client).toBeInstanceOf(WorkerSseClient);
79
+ expect(options.warnings).toHaveLength(1);
80
+ expect(options.warnings[0]).toContain('eventSource');
81
+ });
82
+ });
@@ -0,0 +1,34 @@
1
+ import { SseClient } from './core/SseClient';
2
+ import type {
3
+ SseClientLike,
4
+ SseClientOptions,
5
+ SseEventMap,
6
+ } from './core/types';
7
+ import { WorkerSseClient } from './worker/WorkerSseClient';
8
+
9
+ /**
10
+ * @description ํ™˜๊ฒฝ์— ๋งž๋Š” SseClientLike ๊ตฌํ˜„์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค โ€” sharedWorker ํŒฉํ† ๋ฆฌ๋ฅผ ์ œ๊ณตํ•˜๋ฉด
11
+ * ํƒญ ๊ฐ„ ์—ฐ๊ฒฐ ๊ณต์œ (WorkerSseClient), ๋ฏธ์ œ๊ณต์ด๊ฑฐ๋‚˜ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ SharedWorker๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋ฉด
12
+ * ํƒญ๋ณ„ ์ง์ ‘ ์—ฐ๊ฒฐ(SseClient)์ž…๋‹ˆ๋‹ค. ์ƒ์„ฑ ์‹œ์ ์—๋Š” ๋Šฅ๋ ฅ ๊ฒ€์‚ฌ๋งŒ ํ•˜๋ฏ€๋กœ SSRยท์›Œ์ปค ์Šค์ฝ”ํ”„์—์„œ๋„ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.
13
+ */
14
+ export const createSseClient = <M extends object = SseEventMap>(
15
+ options: SseClientOptions
16
+ ): SseClientLike<M> => {
17
+ if (options.sharedWorker !== undefined) {
18
+ if (options.eventSource !== undefined) {
19
+ options.logger?.warn(
20
+ 'sse: ์›Œ์ปค ๊ฒฝ๋กœ์—์„œ๋Š” eventSource ์ฃผ์ž…์ด ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค โ€” ์ŠคํŠธ๋ฆผ์€ ์›Œ์ปค ์Šค์ฝ”ํ”„์—์„œ ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค'
21
+ );
22
+ }
23
+ if (typeof SharedWorker !== 'undefined') {
24
+ return new WorkerSseClient<M>(options);
25
+ }
26
+ // document๋Š” ๋ธŒ๋ผ์šฐ์ € ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—๋งŒ ์žˆ์Šต๋‹ˆ๋‹ค โ€” SSRยท์›Œ์ปค ์Šค์ฝ”ํ”„์˜ ๋ฌด์˜๋ฏธํ•œ ๊ฒฝ๊ณ ๋ฅผ ํ•จ๊ป˜ ๊ฑฐ๋ฆ…๋‹ˆ๋‹ค
27
+ if (typeof document !== 'undefined') {
28
+ options.logger?.warn(
29
+ 'sse: ์ด ๋ธŒ๋ผ์šฐ์ €๋Š” SharedWorker๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค โ€” ํƒญ๋ณ„ ์ง์ ‘ ์—ฐ๊ฒฐ๋กœ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค'
30
+ );
31
+ }
32
+ }
33
+ return new SseClient<M>(options);
34
+ };
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ์€ createSseClient ๋‹จ์ผ ์ง„์ž…์ž…๋‹ˆ๋‹ค โ€” ๊ตฌํ˜„ ํด๋ž˜์Šค๋ฅผ ์ง์ ‘ ๋…ธ์ถœํ•˜๋ฉด ์ฃผ์ž…์ (sharedWorker ์„ ํƒ)์„ ์šฐํšŒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค
2
+ export { createSseClient } from './createSseClient';
3
+ export type { BackoffOptions } from './core/backoff';
4
+ export { wire } from './core/wire';
5
+ export type { WireHandlers } from './core/wire';
6
+ export type {
7
+ FatalCause,
8
+ FatalPayload,
9
+ GoawayAction,
10
+ GoawayPayload,
11
+ GoawayReason,
12
+ PayloadOf,
13
+ SseClientLike,
14
+ SseClientOptions,
15
+ SseEventHandler,
16
+ SseEventMap,
17
+ SseEventName,
18
+ SseLogger,
19
+ SseStatus,
20
+ Unsubscribe,
21
+ } from './core/types';
@@ -0,0 +1,129 @@
1
+ import { render } from '@testing-library/react';
2
+ import { StrictMode } from 'react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { createSseContext } from './createSseContext';
6
+
7
+ import { SseClient } from '../core/SseClient';
8
+
9
+ const { SseProvider } = createSseContext();
10
+
11
+ const options = {
12
+ baseUrl: 'https://sse.example.com',
13
+ getAuthToken: () => 'token',
14
+ };
15
+
16
+ function trackLifecycleCalls() {
17
+ const calls: string[] = [];
18
+ const instances: unknown[] = [];
19
+ vi.spyOn(SseClient.prototype, 'connect').mockImplementation(
20
+ function (this: unknown) {
21
+ calls.push('connect');
22
+ instances.push(this);
23
+ }
24
+ );
25
+ vi.spyOn(SseClient.prototype, 'disconnect').mockImplementation(
26
+ function (this: unknown) {
27
+ calls.push('disconnect');
28
+ instances.push(this);
29
+ }
30
+ );
31
+ return { calls, instances };
32
+ }
33
+
34
+ describe('SseProvider ์—ฐ๊ฒฐ ์ˆ˜๋ช…์ฃผ๊ธฐ', () => {
35
+ afterEach(() => {
36
+ vi.restoreAllMocks();
37
+ });
38
+
39
+ it('StrictMode ์ด์ค‘ ๋งˆ์šดํŠธ์—์„œ ๊ฐ™์€ ์ธ์Šคํ„ด์Šค๋กœ connect โ†’ disconnect โ†’ connect๊ฐ€ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค', () => {
40
+ const { calls, instances } = trackLifecycleCalls();
41
+ const { unmount } = render(
42
+ <StrictMode>
43
+ <SseProvider {...options}>{null}</SseProvider>
44
+ </StrictMode>
45
+ );
46
+ expect(calls).toEqual(['connect', 'disconnect', 'connect']);
47
+ expect(new Set(instances).size).toBe(1);
48
+ unmount();
49
+ expect(calls).toEqual(['connect', 'disconnect', 'connect', 'disconnect']);
50
+ });
51
+
52
+ it('enabled=false๋กœ ๋งˆ์šดํŠธํ•˜๋ฉด ์—ฐ๊ฒฐํ•œ ์  ์—†๋Š” ํด๋ผ์ด์–ธํŠธ์— disconnect๊ฐ€ ๋‚˜๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค', () => {
53
+ const { calls } = trackLifecycleCalls();
54
+ const { unmount } = render(
55
+ <StrictMode>
56
+ <SseProvider {...options} enabled={false}>
57
+ {null}
58
+ </SseProvider>
59
+ </StrictMode>
60
+ );
61
+ unmount();
62
+ expect(calls).toEqual([]);
63
+ });
64
+
65
+ it('enabled๋ฅผ ๋„๋ฉด ์ด์ „ effect์˜ cleanup์ด ํ•œ ๋ฒˆ๋งŒ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค', () => {
66
+ const { calls } = trackLifecycleCalls();
67
+ const { rerender } = render(<SseProvider {...options}>{null}</SseProvider>);
68
+ rerender(
69
+ <SseProvider {...options} enabled={false}>
70
+ {null}
71
+ </SseProvider>
72
+ );
73
+ expect(calls).toEqual(['connect', 'disconnect']);
74
+ });
75
+
76
+ it('enabled๋ฅผ ๋‹ค์‹œ ์ผœ๋ฉด ์žฌ์—ฐ๊ฒฐํ•˜๊ณ , ๊ทธ ์—ฐ๊ฒฐ์€ ์–ธ๋งˆ์šดํŠธ ๋•Œ ํ•ด์ œ๋ฉ๋‹ˆ๋‹ค', () => {
77
+ const { calls } = trackLifecycleCalls();
78
+ const { rerender, unmount } = render(
79
+ <SseProvider {...options} enabled={false}>
80
+ {null}
81
+ </SseProvider>
82
+ );
83
+ rerender(<SseProvider {...options}>{null}</SseProvider>);
84
+ expect(calls).toEqual(['connect']);
85
+ unmount();
86
+ expect(calls).toEqual(['connect', 'disconnect']);
87
+ });
88
+ });
89
+
90
+ // ์ด ๋ฐฐ์„ ์ด ์—†์œผ๋ฉด ํœด๋ฉด์œผ๋กœ ์ ‘ํžŒ ์—ฐ๊ฒฐ์ด ๊นจ์–ด๋‚˜๋„ ์„œ์ง€ ๋ชปํ•ด, ๋ฐฉ์น˜ํ•œ ํƒญ์˜ ์ˆ˜์‹ ์ด ์ƒˆ๋กœ๊ณ ์นจ ์ „๊นŒ์ง€ ์ฃฝ์–ด ์žˆ์Šต๋‹ˆ๋‹ค
91
+ describe('SseProvider ์ž๋™ ์žฌ๊ฐœ', () => {
92
+ afterEach(() => {
93
+ vi.restoreAllMocks();
94
+ });
95
+
96
+ it('ํƒญ์ด ๋‹ค์‹œ ๋ณด์ด๋ฉด connect๋ฅผ ๋‹ค์‹œ ๋ถ€๋ฆ…๋‹ˆ๋‹ค', () => {
97
+ const { calls } = trackLifecycleCalls();
98
+ render(<SseProvider {...options}>{null}</SseProvider>);
99
+ expect(calls).toEqual(['connect']);
100
+
101
+ document.dispatchEvent(new Event('visibilitychange'));
102
+
103
+ expect(calls).toEqual(['connect', 'connect']);
104
+ });
105
+
106
+ it('์–ธ๋งˆ์šดํŠธํ•œ ๋’ค์—๋Š” ํƒญ์ด ๋‹ค์‹œ ๋ณด์—ฌ๋„ connect๊ฐ€ ๋‚˜๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค', () => {
107
+ const { calls } = trackLifecycleCalls();
108
+ const { unmount } = render(<SseProvider {...options}>{null}</SseProvider>);
109
+ unmount();
110
+ const before = [...calls];
111
+
112
+ document.dispatchEvent(new Event('visibilitychange'));
113
+
114
+ expect(calls).toEqual(before);
115
+ });
116
+
117
+ it('enabled=false๋ฉด ์ž๋™ ์žฌ๊ฐœ๋„ ๋ฐฐ์„ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค', () => {
118
+ const { calls } = trackLifecycleCalls();
119
+ render(
120
+ <SseProvider {...options} enabled={false}>
121
+ {null}
122
+ </SseProvider>
123
+ );
124
+
125
+ document.dispatchEvent(new Event('visibilitychange'));
126
+
127
+ expect(calls).toEqual([]);
128
+ });
129
+ });
@@ -0,0 +1,47 @@
1
+ // @vitest-environment node
2
+ import { describe, expectTypeOf, it } from 'vitest';
3
+
4
+ import { createSseContext } from './createSseContext';
5
+
6
+ import type { FatalPayload } from '../core/types';
7
+
8
+ // ์ด ํŒŒ์ผ์˜ ๊ฒ€์ฆ ๋Œ€์ƒ์€ ํƒ€์ž… ์ถ”๋ก ์ž…๋‹ˆ๋‹ค โ€” ํ›… ํ˜ธ์ถœ ์—†์ด ์ธ์Šคํ„ด์Šคํ™” ํ‘œํ˜„์‹์œผ๋กœ๋งŒ ํ™•์ธํ•˜๋ฏ€๋กœ Provider๊ฐ€ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค
9
+ // ๊ฒŒ์ดํŠธ๋Š” tsc(type-check)์ž…๋‹ˆ๋‹ค โ€” vitest run์€ ํƒ€์ž… ๋‹จ์–ธ์„ ๊ฒ€์ฆํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
10
+ interface TestEventMap {
11
+ 'demo.created': { id: string };
12
+ }
13
+
14
+ describe('createSseContext ํƒ€์ž… ์ถ”๋ก ', () => {
15
+ const hooks = createSseContext<TestEventMap>();
16
+
17
+ it('๋งต์— ์žˆ๋Š” ์ด๋ฒคํŠธ๋ช…์ด๋ฉด ํŽ˜์ด๋กœ๋“œ๊ฐ€ ๋งต์—์„œ ์ถ”๋ก ๋ฉ๋‹ˆ๋‹ค', () => {
18
+ expectTypeOf(hooks.useSseEvent<'demo.created'>)
19
+ .parameter(1)
20
+ .toEqualTypeOf<(payload: { id: string }) => void>();
21
+ });
22
+
23
+ it('๋งต์— ์—†๋Š” ์ด๋ฒคํŠธ๋ช…์€ unknown ํŽ˜์ด๋กœ๋“œ๋กœ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค', () => {
24
+ expectTypeOf(hooks.useSseEvent<'not.in.map'>)
25
+ .parameter(1)
26
+ .toEqualTypeOf<(payload: unknown) => void>();
27
+ });
28
+
29
+ it('SDK ์˜ˆ์•ฝ ์ด๋ฒคํŠธ resync์˜ ํŽ˜์ด๋กœ๋“œ๋Š” void์ž…๋‹ˆ๋‹ค', () => {
30
+ expectTypeOf(hooks.useSseEvent<'resync'>)
31
+ .parameter(1)
32
+ .toEqualTypeOf<(payload: void) => void>();
33
+ });
34
+
35
+ it('SDK ์˜ˆ์•ฝ ์ด๋ฒคํŠธ fatal์˜ ํŽ˜์ด๋กœ๋“œ๋Š” ๋ฉˆ์ถ˜ ์‚ฌ์œ ๋ฅผ ๋‹ด์Šต๋‹ˆ๋‹ค', () => {
36
+ expectTypeOf(hooks.useSseEvent<'fatal'>)
37
+ .parameter(1)
38
+ .toEqualTypeOf<(payload: FatalPayload) => void>();
39
+ });
40
+
41
+ it('๋งต๊ณผ ๋‹ค๋ฅธ ํŽ˜์ด๋กœ๋“œ ํƒ€์ž…์˜ ํ•ธ๋“ค๋Ÿฌ๋Š” ์ปดํŒŒ์ผ์—์„œ ๊ฑฐ๋ถ€๋ฉ๋‹ˆ๋‹ค', () => {
42
+ type DemoHandler = Parameters<typeof hooks.useSseEvent<'demo.created'>>[1];
43
+ // @ts-expect-error 'demo.created'์˜ ํŽ˜์ด๋กœ๋“œ๋Š” { id: string }์ž…๋‹ˆ๋‹ค
44
+ const wrong: DemoHandler = (e: { wrong: number }) => e.wrong;
45
+ expectTypeOf(wrong).toBeFunction();
46
+ });
47
+ });
@@ -0,0 +1,140 @@
1
+ import type { ReactNode } from 'react';
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useInsertionEffect,
8
+ useRef,
9
+ useState,
10
+ useSyncExternalStore,
11
+ } from 'react';
12
+
13
+ import type {
14
+ PayloadOf,
15
+ SseClientLike,
16
+ SseClientOptions,
17
+ SseEventHandler,
18
+ SseEventMap,
19
+ SseEventName,
20
+ SseStatus,
21
+ } from '../core/types';
22
+ import { createSseClient } from '../createSseClient';
23
+
24
+ // useEffectEvent(React 19.2+)์™€ ๊ฐ™์€ ๋ชฉ์ ์˜ latest-ref ํŒจํ„ด โ€” insertion effect๋Š” ์ปค๋ฐ‹๋œ ๋ Œ๋”์—์„œ๋งŒ ์‹คํ–‰๋˜๊ณ  paint ์ „์— ๋™๊ธฐ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค
25
+ const useLatestRef = <T,>(value: T) => {
26
+ const ref = useRef(value);
27
+ useInsertionEffect(() => {
28
+ ref.current = value;
29
+ });
30
+ return ref;
31
+ };
32
+
33
+ /**
34
+ * @description ์—ฐ๊ฒฐ ์˜ต์…˜์€ ๋งˆ์šดํŠธ ์‹œ์  ๊ฐ’์œผ๋กœ ๊ณ ์ •๋ฉ๋‹ˆ๋‹ค โ€” ์ดํ›„ prop ๋ณ€๊ฒฝ์€ ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค.
35
+ * ๋‹จ `getAuthToken`์€ ๋งˆ์ง€๋ง‰ ์ปค๋ฐ‹๋œ ๋ Œ๋”์˜ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ ์ธ๋ผ์ธ ํด๋กœ์ €๋ฅผ ๋„˜๊ฒจ๋„ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.
36
+ */
37
+ export interface SseProviderProps extends SseClientOptions {
38
+ /** @description false๋ฉด ์—ฐ๊ฒฐํ•˜์ง€ ์•Š๊ณ , ์—ฐ๊ฒฐ ์ค‘์ด๋ฉด ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค (๋กœ๊ทธ์•„์›ƒ ๋“ฑ) */
39
+ enabled?: boolean;
40
+ children: ReactNode;
41
+ }
42
+
43
+ /**
44
+ * @description ์•ฑ์˜ ์ด๋ฒคํŠธ ๋งต์œผ๋กœ Provider์™€ ํ›…์„ ํ•œ ๋ฒˆ์— ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค โ€” ํ˜ธ์ถœ๋ถ€๋Š” ์ด๋ฒคํŠธ๋ช…๋งŒ์œผ๋กœ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ์ถ”๋ก ๋ฉ๋‹ˆ๋‹ค.
45
+ * ์ปจํ…์ŠคํŠธ๋ฅผ ํŒฉํ† ๋ฆฌ ์•ˆ์—์„œ ๋งŒ๋“ค์–ด Provider์™€ ํ›…์ด ํƒ€์ž…์œผ๋กœ ๊ฒฐํ•ฉ๋˜๋ฉฐ,
46
+ * ํ˜ธ์ถœ๋งˆ๋‹ค ์ œ๋„ค๋ฆญ์„ ๋ช…์‹œํ•˜๋Š” ๋ฐฉ์‹์€ TS์˜ ๋ถ€๋ถ„ ํƒ€์ž… ์ธ์ž ์ถ”๋ก  ๋ถ€์žฌ๋กœ ํŽ˜์ด๋กœ๋“œ๊ฐ€ unknown์ด ๋˜์–ด ํŒฉํ† ๋ฆฌ๋กœ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค.
47
+ * ์ปดํฌ๋„ŒํŠธ ๋ฐ– ๋ชจ๋“ˆ ์Šค์ฝ”ํ”„์—์„œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœํ•˜์„ธ์š” โ€” ๋ Œ๋” ์ค‘ ํ˜ธ์ถœํ•˜๋ฉด ์ปจํ…์ŠคํŠธ๊ฐ€ ๋งค๋ฒˆ ์žฌ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
48
+ *
49
+ * @example
50
+ * // ์•ฑ๋‹น 1ํšŒ, ๋ชจ๋“ˆ ์Šค์ฝ”ํ”„
51
+ * export const { SseProvider, useSseEvent, useSseStatus } =
52
+ * createSseContext<AppEventMap>();
53
+ *
54
+ * // ํ˜ธ์ถœ๋ถ€ โ€” ์ด๋ฒคํŠธ๋ช…๋งŒ์œผ๋กœ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ์ถ”๋ก ๋ฉ๋‹ˆ๋‹ค
55
+ * useSseEvent('demo.created', (event) => showToast(event.id));
56
+ */
57
+ export function createSseContext<M extends object = SseEventMap>() {
58
+ const SseClientContext = createContext<SseClientLike<M> | null>(null);
59
+
60
+ const SseProvider = ({
61
+ enabled = true,
62
+ children,
63
+ ...options
64
+ }: Readonly<SseProviderProps>) => {
65
+ const getAuthTokenRef = useLatestRef(options.getAuthToken);
66
+
67
+ const [client] = useState(() =>
68
+ createSseClient<M>({
69
+ ...options,
70
+ getAuthToken: () => getAuthTokenRef.current(),
71
+ })
72
+ );
73
+
74
+ useEffect(() => {
75
+ if (!enabled) {
76
+ return;
77
+ }
78
+ client.connect();
79
+ // ํœด๋ฉด์œผ๋กœ ์ ‘ํžŒ ์—ฐ๊ฒฐ์€ ๊นจ์–ด๋‚œ ํƒญ์˜ connect๋งŒ์ด ๋‹ค์‹œ ์„ธ์›๋‹ˆ๋‹ค โ€” ์†Œ๋น„์ฒ˜๊ฐ€ ๋ชฐ๋ผ๋„ ๋ณต๊ตฌ๋˜๊ฒŒ ์—ฌ๊ธฐ์„œ ๋ถ™์ž…๋‹ˆ๋‹ค
80
+ const detachAutoResume = client.attachAutoResume();
81
+ return () => {
82
+ detachAutoResume();
83
+ client.disconnect();
84
+ };
85
+ }, [client, enabled]);
86
+
87
+ return (
88
+ <SseClientContext.Provider value={client}>
89
+ {children}
90
+ </SseClientContext.Provider>
91
+ );
92
+ };
93
+
94
+ const useSseClient = (): SseClientLike<M> => {
95
+ const client = useContext(SseClientContext);
96
+ if (!client) {
97
+ throw new Error('SSE ํ›…์€ SseProvider ์•ˆ์—์„œ๋งŒ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค');
98
+ }
99
+ return client;
100
+ };
101
+
102
+ /**
103
+ * @description ์ด๋ฒคํŠธ ๊ตฌ๋… ํ›… โ€” ๋งˆ์šดํŠธ ๋™์•ˆ ๊ตฌ๋…ํ•˜๊ณ  ์–ธ๋งˆ์šดํŠธ์— ํ•ด์ œํ•˜๋ฉฐ, ํ•ธ๋“ค๋Ÿฌ๋Š” ํ•ญ์ƒ ์ตœ์‹  ๋ Œ๋”์˜ ํด๋กœ์ €๋ฅผ ๋ด…๋‹ˆ๋‹ค.
104
+ * @param type ์ด๋ฒคํŠธ ์ด๋ฆ„. ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ์™ธ ์˜ˆ์•ฝ 4์ข… โ€”
105
+ * - `fatal`(SDK์˜ ํฌ๊ธฐ ์„ ์–ธ โ€” ์žฌ์‹œ๋„ ์—†์ด ๋ฉˆ์ถค, SDK ํ•ฉ์„ฑ) ยท
106
+ * - `stalled`(์žฌ์‹œ๋„๊ฐ€ ๊ณ„์†๋˜๋Š” ์ง„ํ–‰ํ˜• ํ†ต์ง€, SDK ํ•ฉ์„ฑ) ยท
107
+ * - `goaway`(์„œ๋ฒ„์˜ ์˜ˆ๊ณ ๋œ ๋Š์Œ โ€” `action: 'close'`๋งŒ ์•ˆ๋‚ด ๋Œ€์ƒ, ์„œ๋ฒ„ ๋ฐœ์‹ ) ยท
108
+ * - `resync`(์žฌ์ˆ˜๋ฆฝ ํ›„ ๋†“์นœ ๊ตฌ๊ฐ„์„ REST๋กœ ์žฌ์กฐํšŒํ•˜๋ผ๋Š” ์‹ ํ˜ธ, SDK ํ•ฉ์„ฑ). ์ƒ์„ธ๋Š” `WithLifecycle` ๋ฌธ์„œ ์ฐธ์กฐ.
109
+ * @param handler ์ด๋ฒคํŠธ ํŽ˜์ด๋กœ๋“œ๋ฅผ ๋ฐ›๋Š” ์ฝœ๋ฐฑ โ€” ๋ฐœํ™” ์ˆœ๊ฐ„์˜ ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ ๋ฐฐ๋‹ฌ๋˜๋ฏ€๋กœ ์ˆ˜๋ช…์ฃผ๊ธฐ ํ•ธ๋“ค๋Ÿฌ๋Š” ์•ฑ ๋ฃจํŠธ์— ๋‘์„ธ์š”
110
+ */
111
+ const useSseEvent = <K extends SseEventName<M>>(
112
+ type: K,
113
+ handler: SseEventHandler<PayloadOf<M, K>>
114
+ ): void => {
115
+ const client = useSseClient();
116
+ const handlerRef = useLatestRef(handler);
117
+
118
+ useEffect(
119
+ () => client.on(type, (payload) => handlerRef.current(payload)),
120
+ // handlerRef๋Š” useRef ์ •์ฒด์„ฑ์ด๋ผ ์žฌ๊ตฌ๋…์„ ์œ ๋ฐœํ•˜์ง€ ์•Š์ง€๋งŒ, exhaustive-deps๊ฐ€ ์ปค์Šคํ…€ ํ›… ๋ฐ˜ํ™˜์„ ์•ˆ์ •์œผ๋กœ ๋ณผ ์ˆ˜ ์—†์–ด ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค
121
+ [client, type, handlerRef]
122
+ );
123
+ };
124
+
125
+ const useSseStatus = (): SseStatus => {
126
+ const client = useSseClient();
127
+ const subscribe = useCallback(
128
+ (onStoreChange: () => void) => client.onStatus(onStoreChange),
129
+ [client]
130
+ );
131
+ return useSyncExternalStore(
132
+ subscribe,
133
+ () => client.status,
134
+ // ์„œ๋ฒ„/hydration ์ฒซ ๋ Œ๋”๋Š” connect() ์ด์ „์ด๋ผ ํ•ญ์ƒ ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐ ์ƒํƒœ์ž…๋‹ˆ๋‹ค
135
+ () => client.status
136
+ );
137
+ };
138
+
139
+ return { SseProvider, useSseClient, useSseEvent, useSseStatus };
140
+ }
@@ -0,0 +1,2 @@
1
+ export { createSseContext } from './createSseContext';
2
+ export type { SseProviderProps } from './createSseContext';
@@ -0,0 +1,71 @@
1
+ import { render } from '@testing-library/react';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { createSseContext } from './createSseContext';
5
+
6
+ import { SseClient } from '../core/SseClient';
7
+
8
+ interface TestEventMap {
9
+ 'demo.created': { id: string };
10
+ }
11
+
12
+ const { SseProvider, useSseEvent } = createSseContext<TestEventMap>();
13
+
14
+ const options = {
15
+ baseUrl: 'https://sse.example.com',
16
+ getAuthToken: () => 'token',
17
+ };
18
+
19
+ function EventProbe({ onCreated }: { onCreated: (e: { id: string }) => void }) {
20
+ useSseEvent('demo.created', onCreated);
21
+ return null;
22
+ }
23
+
24
+ describe('useSseEvent', () => {
25
+ afterEach(() => {
26
+ vi.restoreAllMocks();
27
+ });
28
+
29
+ it('๋ฆฌ๋ Œ๋” ์‹œ ์žฌ๊ตฌ๋… ์—†์ด, ์ด๋ฒคํŠธ๊ฐ€ ์˜ค๋ฉด ์ตœ์‹  ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค', () => {
30
+ let captured: ((payload: unknown) => void) | undefined;
31
+ const on = vi
32
+ .spyOn(SseClient.prototype, 'on')
33
+ .mockImplementation((_type, handler) => {
34
+ captured = handler as (payload: unknown) => void;
35
+ return () => {};
36
+ });
37
+ const staleHandler = vi.fn();
38
+ const latestHandler = vi.fn();
39
+ const { rerender } = render(
40
+ <SseProvider {...options}>
41
+ <EventProbe onCreated={staleHandler} />
42
+ </SseProvider>
43
+ );
44
+ rerender(
45
+ <SseProvider {...options}>
46
+ <EventProbe onCreated={latestHandler} />
47
+ </SseProvider>
48
+ );
49
+
50
+ captured?.({ id: 'a' });
51
+
52
+ expect(on).toHaveBeenCalledTimes(1);
53
+ expect(staleHandler).not.toHaveBeenCalled();
54
+ expect(latestHandler).toHaveBeenCalledWith({ id: 'a' });
55
+ });
56
+
57
+ it('์–ธ๋งˆ์šดํŠธํ•˜๋ฉด ๊ตฌ๋…์ด ํ•ด์ œ๋ฉ๋‹ˆ๋‹ค', () => {
58
+ const unsubscribe = vi.fn();
59
+ vi.spyOn(SseClient.prototype, 'on').mockImplementation(() => unsubscribe);
60
+ const { unmount } = render(
61
+ <SseProvider {...options}>
62
+ <EventProbe onCreated={() => {}} />
63
+ </SseProvider>
64
+ );
65
+ expect(unsubscribe).not.toHaveBeenCalled();
66
+
67
+ unmount();
68
+
69
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
70
+ });
71
+ });
@@ -0,0 +1,40 @@
1
+ import { render } from '@testing-library/react';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { createSseContext } from './createSseContext';
5
+
6
+ import { SseClient } from '../core/SseClient';
7
+
8
+ const { SseProvider, useSseStatus } = createSseContext();
9
+
10
+ const options = {
11
+ baseUrl: 'https://sse.example.com',
12
+ getAuthToken: () => 'token',
13
+ };
14
+
15
+ function StatusProbe() {
16
+ useSseStatus();
17
+ return null;
18
+ }
19
+
20
+ describe('useSseStatus', () => {
21
+ afterEach(() => {
22
+ vi.restoreAllMocks();
23
+ });
24
+
25
+ it('๋ฆฌ๋ Œ๋” ์‹œ ์žฌ๊ตฌ๋…ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค', () => {
26
+ const onStatus = vi.spyOn(SseClient.prototype, 'onStatus');
27
+ const { rerender, unmount } = render(
28
+ <SseProvider {...options}>
29
+ <StatusProbe />
30
+ </SseProvider>
31
+ );
32
+ rerender(
33
+ <SseProvider {...options}>
34
+ <StatusProbe />
35
+ </SseProvider>
36
+ );
37
+ expect(onStatus).toHaveBeenCalledTimes(1);
38
+ unmount();
39
+ });
40
+ });