react-ws-context 0.3.0 → 0.4.0
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/CHANGELOG.md +22 -0
- package/README.md +8 -5
- package/README.zh-TW.md +8 -5
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +66 -65
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Store `setState` skips listener notification when partial values are unchanged (shallow compare on updated keys)
|
|
15
|
+
- Provider unmount syncs store to `status: "closed"` and `phase: "idle"` (consistent with `disconnect()`)
|
|
16
|
+
- Liveness ping and timeout both use `getActiveSocket()` for the active socket
|
|
17
|
+
- `sendJson` returns `false` when `JSON.stringify` fails (e.g. circular reference) instead of throwing
|
|
18
|
+
- `useWsEvents` handler ref sync moved into `useEffect` (complies with `react-hooks/refs`)
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Shared `teardown()` for `disconnect()` and provider unmount cleanup
|
|
23
|
+
- On unintentional close, store updates before `close` is emitted so handlers see consistent `status` / `phase`
|
|
24
|
+
- Batch `status` and `phase` store updates where both change together
|
|
25
|
+
- `Liveness.start()` takes no socket argument; uses `getActiveSocket()` at runtime
|
|
26
|
+
- Internal layout: `useWsEventsApi` in `ws-events.ts` (`emitter.ts` is React-free); aligned Context module structure (`ws-store`, `ws-events`, `ws-actions`)
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Store smoke test for `setState` deduplication
|
|
31
|
+
|
|
10
32
|
## [0.3.0] - 2026-08-29
|
|
11
33
|
|
|
12
34
|
### Added
|
package/README.md
CHANGED
|
@@ -93,6 +93,7 @@ createWsContext(options)
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
- **Call `createWsContext` multiple times** for independent connections (e.g. app WS + notification WS).
|
|
96
|
+
- **Provider instances** — `useWsStoreApi` (store), `useWsEventsApi` (emitter); actions are assembled in `WsProvider` via `useMemo` and passed through Context.
|
|
96
97
|
- **`WsState` holds low-frequency connection data only** — health (`status`, `phase`), outbound queue (e.g. future `pendingCount`), reconnect (`reconnectAttempt`). **Not** message payloads or app data.
|
|
97
98
|
- **Messages and errors** — use `useWsEvents`; keep message history in your own state, cache, or store.
|
|
98
99
|
- **Connection errors are not a `WsStatus`** — use `useWsEvents("error")`; native `error` is usually followed by `close`.
|
|
@@ -141,7 +142,8 @@ Creates, owns, and tears down the native `WebSocket`.
|
|
|
141
142
|
| Behavior | Description |
|
|
142
143
|
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
144
|
| mount + `autoConnect: true` | Calls `connect()` |
|
|
144
|
-
| unmount |
|
|
145
|
+
| unmount | Cancels reconnect, stops liveness, clears outbound queue; syncs store to `status: "closed"`, `phase: "idle"`; closes socket and emits `close` (reason: `"provider unmount"`) |
|
|
146
|
+
| `disconnect()` | Same store reset as unmount (`phase: "idle"`, `status: "closed"`), no auto-reconnect; emits `close` (reason: `"client disconnect"`) |
|
|
145
147
|
| reconnect | Fixed interval when `reconnectMs > 0` and close was not intentional (no exponential backoff); stops after `reconnectMax` if `> 0` |
|
|
146
148
|
| before reconnect | Closes existing socket and emits `close` (reason: `"reconnect"`) |
|
|
147
149
|
|
|
@@ -156,13 +158,13 @@ Must be used inside the matching `WsProvider`. Return value is memoized and **do
|
|
|
156
158
|
| `send` | `(data) => boolean` | Send raw data. Sends immediately when OPEN; otherwise enqueues if configured |
|
|
157
159
|
| `sendJson` | `(data: unknown) => boolean` | `JSON.stringify` then `send` |
|
|
158
160
|
| `connect` | `() => void` | Open connection; closes any existing socket first |
|
|
159
|
-
| `disconnect` | `() => void` | Intentional close; no auto-reconnect; clears outbound queue
|
|
161
|
+
| `disconnect` | `() => void` | Intentional close; sets store to `phase: "idle"`, `status: "closed"`; no auto-reconnect; clears outbound queue |
|
|
160
162
|
| `getStatus` | `() => WsStatus` | Read current status; no subscription, no re-render |
|
|
161
163
|
|
|
162
164
|
**`send` / `sendJson` return value:**
|
|
163
165
|
|
|
164
166
|
- `true` — sent or enqueued
|
|
165
|
-
- `false` — not OPEN and queue full (`outgoingQueueMax > 0`),
|
|
167
|
+
- `false` — not OPEN and queue full (`outgoingQueueMax > 0`), queue disabled (`outgoingQueueMax === 0`), or `sendJson` failed to `JSON.stringify` (e.g. circular reference)
|
|
166
168
|
|
|
167
169
|
---
|
|
168
170
|
|
|
@@ -252,6 +254,7 @@ Must be used inside the matching `WsProvider`. Registers in `useEffect` and unsu
|
|
|
252
254
|
|
|
253
255
|
- Handler is kept in a ref — changing the callback does **not** re-subscribe
|
|
254
256
|
- Changing `type` **does** re-subscribe
|
|
257
|
+
- On unintentional close, the store is updated to `status: "closed"` and the appropriate `phase` before the `close` handler runs
|
|
255
258
|
- For multiple events, call `useWsEvents` multiple times
|
|
256
259
|
|
|
257
260
|
---
|
|
@@ -385,5 +388,5 @@ This package does **not** list zustand or nanoevents as npm dependencies. It inl
|
|
|
385
388
|
|
|
386
389
|
- **Author:** [Andrey Sitnik](https://github.com/ai) (`ai`)
|
|
387
390
|
- **License:** [MIT](https://github.com/ai/nanoevents/blob/main/LICENSE)
|
|
388
|
-
- **Adapted from:** [`createNanoEvents`](https://github.com/ai/nanoevents/blob/main/index.js); `
|
|
389
|
-
- **
|
|
391
|
+
- **Adapted from:** [`createNanoEvents`](https://github.com/ai/nanoevents/blob/main/index.js); `useWsEventsApi` added by this package (`ws-events.ts`)
|
|
392
|
+
- **Files:** `src/ws-context/emitter.ts`, `src/ws-context/ws-events.ts`
|
package/README.zh-TW.md
CHANGED
|
@@ -93,6 +93,7 @@ createWsContext(options)
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
- **同一應用可多次呼叫 `createWsContext`**,每次產生一組互不共用的 Provider 與 hooks(例如同時連業務 WS 與通知 WS)。
|
|
96
|
+
- **Provider 內部 instance** — `useWsStoreApi`(store)、`useWsEventsApi`(emitter);actions 在 `WsProvider` 內以 `useMemo` 組裝後注入 Context。
|
|
96
97
|
- **`WsState` 只放連線層、低頻欄位** — 連線健康(`status`、`phase`)、outbound 佇列(如未來 `pendingCount`)、重連(`reconnectAttempt`)。**不放**訊息 payload 或業務資料。
|
|
97
98
|
- **訊息與錯誤事件** — 請用 `useWsEvents`;訊息歷史請自行寫入 state、cache 或外部 store。
|
|
98
99
|
- **連線錯誤不反映在 `WsStatus`** — 請用 `useWsEvents("error", …)` 處理;原生 `error` 事件後通常緊接 `close`。
|
|
@@ -141,7 +142,8 @@ createWsContext(options)
|
|
|
141
142
|
| 行為 | 說明 |
|
|
142
143
|
| --------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
143
144
|
| mount + `autoConnect: true` | 自動 `connect()` |
|
|
144
|
-
| unmount |
|
|
145
|
+
| unmount | 取消重連、停止探活、清空 outbound 佇列;store 同步為 `status: "closed"`、`phase: "idle"`;關閉 socket 並 emit `close`(reason: `"provider unmount"`) |
|
|
146
|
+
| `disconnect()` | 同 unmount 的 store 重置(`phase: "idle"`、`status: "closed"`),但不觸發自動重連;emit `close`(reason: `"client disconnect"`) |
|
|
145
147
|
| 重連 | 非主動斷線且 `reconnectMs > 0` 時,以固定間隔重試(無 exponential backoff);`reconnectMax > 0` 時超過次數即停止 |
|
|
146
148
|
| 重連前 | 若已有舊 socket,先關閉並 emit `close`(reason: `"reconnect"`) |
|
|
147
149
|
|
|
@@ -156,13 +158,13 @@ createWsContext(options)
|
|
|
156
158
|
| `send` | `(data) => boolean` | 傳送原始資料(`string`、`ArrayBuffer`、`Blob` 等)。已 OPEN 則立即送出;否則視佇列設定入隊 |
|
|
157
159
|
| `sendJson` | `(data: unknown) => boolean` | `JSON.stringify` 後呼叫 `send` |
|
|
158
160
|
| `connect` | `() => void` | 建立連線;若已有連線會先關閉舊 socket |
|
|
159
|
-
| `disconnect` | `() => void` |
|
|
161
|
+
| `disconnect` | `() => void` | 主動斷線;store 設為 `phase: "idle"`、`status: "closed"`,**不**觸發自動重連;清空 outbound 佇列 |
|
|
160
162
|
| `getStatus` | `() => WsStatus` | 讀取當下 status;不訂閱、不觸發渲染 |
|
|
161
163
|
|
|
162
164
|
**`send` / `sendJson` 回傳值:**
|
|
163
165
|
|
|
164
166
|
- `true` — 已送出,或已成功入隊
|
|
165
|
-
- `false` — 未 OPEN 且佇列已滿(`outgoingQueueMax > 0` 且達上限),或佇列關閉(`outgoingQueueMax === 0
|
|
167
|
+
- `false` — 未 OPEN 且佇列已滿(`outgoingQueueMax > 0` 且達上限),或佇列關閉(`outgoingQueueMax === 0`);`sendJson` 另含 `JSON.stringify` 失敗(如 circular reference)
|
|
166
168
|
|
|
167
169
|
---
|
|
168
170
|
|
|
@@ -252,6 +254,7 @@ const canDisconnect =
|
|
|
252
254
|
|
|
253
255
|
- `handler` 以 ref 保存最新引用,callback 重建**不會**導致重新訂閱
|
|
254
256
|
- `type` 變更**會**重新訂閱
|
|
257
|
+
- 意外斷線時,`close` handler 觸發前 store 已更新為 `status: "closed"` 及對應 `phase`(`reconnecting` / `stopped` 等)
|
|
255
258
|
- 需監聽多種事件時,分別呼叫多次 `useWsEvents`
|
|
256
259
|
|
|
257
260
|
---
|
|
@@ -400,5 +403,5 @@ sendJson(createStallMessage("stall"));
|
|
|
400
403
|
- **授權:** [MIT](https://github.com/ai/nanoevents/blob/main/LICENSE)
|
|
401
404
|
- **借鑑範圍:**
|
|
402
405
|
- Typed event emitter — 執行期邏輯幾乎對齊 [`createNanoEvents`](https://github.com/ai/nanoevents/blob/main/index.js);型別為本套件收斂版
|
|
403
|
-
- `
|
|
404
|
-
- **對應原始碼:** `src/ws-context/emitter.ts`
|
|
406
|
+
- `useWsEventsApi` 為本套件自行新增(React `useState` 包裝,見 `ws-events.ts`)
|
|
407
|
+
- **對應原始碼:** `src/ws-context/emitter.ts`、`src/ws-context/ws-events.ts`
|
package/dist/index.d.mts
CHANGED
|
@@ -76,7 +76,7 @@ interface CreateWsContextOptions {
|
|
|
76
76
|
/**
|
|
77
77
|
* 是否自動重新連線。
|
|
78
78
|
*
|
|
79
|
-
*
|
|
79
|
+
* @default true
|
|
80
80
|
*/
|
|
81
81
|
autoConnect?: boolean;
|
|
82
82
|
/**
|
|
@@ -137,7 +137,7 @@ interface WsContextValue {
|
|
|
137
137
|
* 傳送 JSON 資料
|
|
138
138
|
*
|
|
139
139
|
* @param data 資料
|
|
140
|
-
* @returns
|
|
140
|
+
* @returns 是否已送出或已入隊;`JSON.stringify` 失敗(如 circular reference)回傳 `false`
|
|
141
141
|
*/
|
|
142
142
|
sendJson: (data: unknown) => boolean;
|
|
143
143
|
/** 建立連線 */
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/ws-context/ws-store.ts","../src/ws-context/liveness/types.ts","../src/ws-context/types.ts","../src/ws-context/index.tsx"],"mappings":";;;;;;;;;;;;KAcY;;;;;;;;;;;;KAaA;;;;;;;;;;KAgBA;;EAEV,QAAQ;;EAER,OAAO;;;;;;;;EAQP;;;;;;EAMA;;;;;UC5De;;EAEf;;EAEA;;EAEA;;EAEA,SAAS;;;;;UCLM;;EAEf;;EAEA;;;;;;EAMA;;;;;;EAMA;;;;;;;;;EASA;;;;;;EAMA;;;;;;EAMA,SAAS,MAAM;;EAEf,WAAW;;UAGI;;;;;;;EAOf,UAAU,eAAe,OAAO;;EAEhC,OAAO,OAAO;;EAEd,QAAQ,OAAO;;EAEf,QAAQ,OAAO;;;UAIA;;;;;;;EAOf,OAAO,MAAM,WAAW;;;;;;;EAOxB,WAAW;;EAEX;;EAEA;;EAEA,iBAAiB;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/ws-context/ws-store.ts","../src/ws-context/liveness/types.ts","../src/ws-context/types.ts","../src/ws-context/index.tsx"],"mappings":";;;;;;;;;;;;KAcY;;;;;;;;;;;;KAaA;;;;;;;;;;KAgBA;;EAEV,QAAQ;;EAER,OAAO;;;;;;;;EAQP;;;;;;EAMA;;;;;UC5De;;EAEf;;EAEA;;EAEA;;EAEA,SAAS;;;;;UCLM;;EAEf;;EAEA;;;;;;EAMA;;;;;;EAMA;;;;;;;;;EASA;;;;;;EAMA;;;;;;EAMA,SAAS,MAAM;;EAEf,WAAW;;UAGI;;;;;;;EAOf,UAAU,eAAe,OAAO;;EAEhC,OAAO,OAAO;;EAEd,QAAQ,OAAO;;EAEf,QAAQ,OAAO;;;UAIA;;;;;;;EAOf,OAAO,MAAM,WAAW;;;;;;;EAOxB,WAAW;;EAEX;;EAEA;;EAEA,iBAAiB;;;;;;;;;;;iBC5CH,gBAAgB,SAAS;EAmBL,eAAA,YAAA,sCAAiB,IAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -17,21 +17,24 @@ function createEmitter() {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
function useEmitter() {
|
|
21
|
-
const [emitter] = useState(() => createEmitter());
|
|
22
|
-
return emitter;
|
|
23
|
-
}
|
|
24
20
|
//#endregion
|
|
25
21
|
//#region src/ws-context/ws-events.ts
|
|
26
22
|
function createWsEventsContext() {
|
|
27
23
|
return createContext(null);
|
|
28
24
|
}
|
|
29
|
-
|
|
25
|
+
/** 每個 `WsProvider` 各有一份 event emitter */
|
|
26
|
+
function useWsEventsApi() {
|
|
27
|
+
const [emitter] = useState(() => createEmitter());
|
|
28
|
+
return emitter;
|
|
29
|
+
}
|
|
30
|
+
function createUseWsEvents(EventsCtx) {
|
|
30
31
|
function useWsEvents(type, handler) {
|
|
31
|
-
const emitter = useContext(
|
|
32
|
+
const emitter = useContext(EventsCtx);
|
|
32
33
|
if (!emitter) throw new Error("useWsEvents 必須包在對應的 WsProvider 內");
|
|
33
34
|
const handlerRef = useRef(handler);
|
|
34
|
-
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
handlerRef.current = handler;
|
|
37
|
+
});
|
|
35
38
|
useEffect(() => {
|
|
36
39
|
return emitter.on(type, ((...args) => {
|
|
37
40
|
handlerRef.current(...args);
|
|
@@ -100,15 +103,16 @@ const DISABLED_LIVENESS = {
|
|
|
100
103
|
function createLiveness(options, getActiveSocket) {
|
|
101
104
|
let controller = null;
|
|
102
105
|
return {
|
|
103
|
-
start(
|
|
106
|
+
start() {
|
|
104
107
|
controller?.stop();
|
|
105
108
|
controller = createLivenessController(options, () => {
|
|
106
109
|
const current = getActiveSocket();
|
|
107
110
|
if (current?.readyState === WebSocket.OPEN) current.close();
|
|
108
111
|
});
|
|
109
112
|
const sendPing = createPingSender(options.ping, (data) => {
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const current = getActiveSocket();
|
|
114
|
+
if (!current || current.readyState !== WebSocket.OPEN) return false;
|
|
115
|
+
current.send(JSON.stringify(data));
|
|
112
116
|
return true;
|
|
113
117
|
});
|
|
114
118
|
controller.start(sendPing);
|
|
@@ -160,7 +164,7 @@ function createStore(initialState) {
|
|
|
160
164
|
getInitialState: () => initialState,
|
|
161
165
|
setState: (partial) => {
|
|
162
166
|
const nextPartial = typeof partial === "function" ? partial(state) : partial;
|
|
163
|
-
if (
|
|
167
|
+
if (!hasPartialChanged(state, nextPartial)) return;
|
|
164
168
|
const prev = state;
|
|
165
169
|
state = Object.assign({}, state, nextPartial);
|
|
166
170
|
for (const listener of listeners) listener(state, prev);
|
|
@@ -173,6 +177,11 @@ function createStore(initialState) {
|
|
|
173
177
|
}
|
|
174
178
|
};
|
|
175
179
|
}
|
|
180
|
+
/** partial 內所有 key 值與 state 相同則視為無變更,不通知訂閱者 */
|
|
181
|
+
function hasPartialChanged(state, partial) {
|
|
182
|
+
for (const [key, value] of Object.entries(partial)) if (!Object.is(state[key], value)) return true;
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
176
185
|
//#endregion
|
|
177
186
|
//#region src/ws-context/use-store.ts
|
|
178
187
|
function useStore(store, selector) {
|
|
@@ -183,19 +192,19 @@ function useStore(store, selector) {
|
|
|
183
192
|
function createWsStore(init = "idle") {
|
|
184
193
|
return createStore({
|
|
185
194
|
status: init,
|
|
186
|
-
phase: init === "open"
|
|
195
|
+
phase: init === "open" || init === "idle" ? init : "connecting",
|
|
187
196
|
reconnectAttempt: 0,
|
|
188
197
|
reconnectExhausted: false
|
|
189
198
|
});
|
|
190
199
|
}
|
|
200
|
+
function createWsStoreContext() {
|
|
201
|
+
return createContext(null);
|
|
202
|
+
}
|
|
191
203
|
/** 每個 `WsProvider` 各有一份 {@link WsState} store */
|
|
192
204
|
function useWsStoreApi() {
|
|
193
205
|
const [store] = useState(() => createWsStore());
|
|
194
206
|
return store;
|
|
195
207
|
}
|
|
196
|
-
function createWsStoreContext() {
|
|
197
|
-
return createContext(null);
|
|
198
|
-
}
|
|
199
208
|
/** 訂閱 {@link WsState};建議以 selector 只取需要的連線層欄位。 */
|
|
200
209
|
function createUseWsStore(StoreCtx) {
|
|
201
210
|
function useWsStore(selector) {
|
|
@@ -207,6 +216,7 @@ function createUseWsStore(StoreCtx) {
|
|
|
207
216
|
}
|
|
208
217
|
//#endregion
|
|
209
218
|
//#region src/ws-context/ws-actions.ts
|
|
219
|
+
/** actions 無獨立 instance;由 WsProvider 以 useMemo 組裝後注入 Context */
|
|
210
220
|
function createWsActionsContext() {
|
|
211
221
|
return createContext(null);
|
|
212
222
|
}
|
|
@@ -311,16 +321,16 @@ function defaultParse(data) {
|
|
|
311
321
|
*/
|
|
312
322
|
function createWsContext(options) {
|
|
313
323
|
const { url, protocols, autoConnect = true, reconnectMs = 0, reconnectMax = 0, outgoingQueueMax = 0, parse = defaultParse, liveness } = options;
|
|
314
|
-
const ActionsCtx = createWsActionsContext();
|
|
315
|
-
const useWsActions = createUseWsActions(ActionsCtx);
|
|
316
324
|
const StoreCtx = createWsStoreContext();
|
|
317
325
|
const useWsStore = createUseWsStore(StoreCtx);
|
|
318
|
-
const
|
|
319
|
-
const
|
|
326
|
+
const ActionsCtx = createWsActionsContext();
|
|
327
|
+
const useWsActions = createUseWsActions(ActionsCtx);
|
|
328
|
+
const EventsCtx = createWsEventsContext();
|
|
329
|
+
const useWsEvents = createUseWsEvents(EventsCtx);
|
|
320
330
|
function WsProvider({ children }) {
|
|
321
331
|
const wsRef = useRef(null);
|
|
322
332
|
const store = useWsStoreApi();
|
|
323
|
-
const emitter =
|
|
333
|
+
const emitter = useWsEventsApi();
|
|
324
334
|
const reconnect = useReconnect(reconnectMs, reconnectMax, {
|
|
325
335
|
getAttempt: () => store.getState().reconnectAttempt,
|
|
326
336
|
setAttempt: (reconnectAttempt) => store.setState({ reconnectAttempt }),
|
|
@@ -328,33 +338,30 @@ function createWsContext(options) {
|
|
|
328
338
|
});
|
|
329
339
|
const outgoingQueue = useOutgoingQueue(outgoingQueueMax);
|
|
330
340
|
const livenessSession = useLiveness(liveness, () => wsRef.current);
|
|
331
|
-
const setStatus = useCallback((status) => {
|
|
332
|
-
store.setState({ status });
|
|
333
|
-
}, [store]);
|
|
334
|
-
const setPhase = useCallback((phase) => {
|
|
335
|
-
store.setState({ phase });
|
|
336
|
-
}, [store]);
|
|
337
341
|
const getStatus = useCallback(() => store.getState().status, [store]);
|
|
338
|
-
|
|
342
|
+
/** 主動斷線與 Provider unmount 共用;`reason` 區分 `"client disconnect"` / `"provider unmount"` */
|
|
343
|
+
const teardown = useCallback((reason) => {
|
|
339
344
|
reconnect.cancel();
|
|
340
345
|
livenessSession.stop();
|
|
341
346
|
outgoingQueue.clear();
|
|
342
|
-
|
|
347
|
+
store.setState({
|
|
348
|
+
phase: "idle",
|
|
349
|
+
status: "closed"
|
|
350
|
+
});
|
|
343
351
|
const ws = wsRef.current;
|
|
344
352
|
wsRef.current = null;
|
|
345
353
|
if (ws) {
|
|
346
354
|
detachAndClose(ws);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
} else setStatus("closed");
|
|
355
|
+
emitter.emit("close", clientCloseEvent(reason));
|
|
356
|
+
}
|
|
350
357
|
}, [
|
|
351
|
-
|
|
352
|
-
setPhase,
|
|
358
|
+
store,
|
|
353
359
|
emitter,
|
|
354
360
|
outgoingQueue,
|
|
355
361
|
livenessSession,
|
|
356
362
|
reconnect
|
|
357
363
|
]);
|
|
364
|
+
const disconnect = useCallback(() => teardown("client disconnect"), [teardown]);
|
|
358
365
|
const connect = useCallback(() => {
|
|
359
366
|
if (typeof window === "undefined") return;
|
|
360
367
|
const fromReconnect = reconnect.onConnectBegin();
|
|
@@ -365,17 +372,21 @@ function createWsContext(options) {
|
|
|
365
372
|
detachAndClose(prev);
|
|
366
373
|
emitter.emit("close", clientCloseEvent("reconnect"));
|
|
367
374
|
}
|
|
368
|
-
|
|
369
|
-
|
|
375
|
+
store.setState({
|
|
376
|
+
status: "connecting",
|
|
377
|
+
phase: fromReconnect ? "reconnecting" : "connecting"
|
|
378
|
+
});
|
|
370
379
|
const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);
|
|
371
380
|
wsRef.current = ws;
|
|
372
381
|
ws.onopen = (event) => {
|
|
373
382
|
if (wsRef.current !== ws) return;
|
|
374
383
|
reconnect.onOpen();
|
|
375
|
-
|
|
376
|
-
|
|
384
|
+
store.setState({
|
|
385
|
+
status: "open",
|
|
386
|
+
phase: "open"
|
|
387
|
+
});
|
|
377
388
|
outgoingQueue.flush((data) => ws.send(data));
|
|
378
|
-
livenessSession.start(
|
|
389
|
+
livenessSession.start();
|
|
379
390
|
emitter.emit("open", event);
|
|
380
391
|
};
|
|
381
392
|
ws.onmessage = (event) => {
|
|
@@ -391,41 +402,25 @@ function createWsContext(options) {
|
|
|
391
402
|
ws.onclose = (event) => {
|
|
392
403
|
if (wsRef.current === ws) wsRef.current = null;
|
|
393
404
|
livenessSession.stop();
|
|
394
|
-
|
|
405
|
+
const scheduled = reconnect.scheduleAfterClose();
|
|
406
|
+
const patch = { status: "closed" };
|
|
407
|
+
if (scheduled) patch.phase = "reconnecting";
|
|
408
|
+
else if (store.getState().phase !== "idle") patch.phase = "stopped";
|
|
409
|
+
store.setState(patch);
|
|
395
410
|
emitter.emit("close", event);
|
|
396
|
-
if (reconnect.scheduleAfterClose()) setPhase("reconnecting");
|
|
397
|
-
else if (store.getState().phase !== "idle") setPhase("stopped");
|
|
398
411
|
};
|
|
399
412
|
}, [
|
|
400
|
-
|
|
401
|
-
setPhase,
|
|
413
|
+
store,
|
|
402
414
|
emitter,
|
|
403
415
|
outgoingQueue,
|
|
404
416
|
livenessSession,
|
|
405
|
-
reconnect
|
|
406
|
-
store
|
|
417
|
+
reconnect
|
|
407
418
|
]);
|
|
408
419
|
reconnect.bindOnReconnect(connect);
|
|
409
420
|
useEffect(() => {
|
|
410
421
|
if (autoConnect) connect();
|
|
411
|
-
return () =>
|
|
412
|
-
|
|
413
|
-
livenessSession.stop();
|
|
414
|
-
outgoingQueue.clear();
|
|
415
|
-
const ws = wsRef.current;
|
|
416
|
-
wsRef.current = null;
|
|
417
|
-
if (ws) {
|
|
418
|
-
detachAndClose(ws);
|
|
419
|
-
emitter.emit("close", clientCloseEvent("provider unmount"));
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
}, [
|
|
423
|
-
connect,
|
|
424
|
-
emitter,
|
|
425
|
-
outgoingQueue,
|
|
426
|
-
livenessSession,
|
|
427
|
-
reconnect
|
|
428
|
-
]);
|
|
422
|
+
return () => teardown("provider unmount");
|
|
423
|
+
}, [connect, teardown]);
|
|
429
424
|
const send = useCallback((data) => {
|
|
430
425
|
const ws = wsRef.current;
|
|
431
426
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
@@ -434,7 +429,13 @@ function createWsContext(options) {
|
|
|
434
429
|
}
|
|
435
430
|
return outgoingQueue.enqueue(data);
|
|
436
431
|
}, [outgoingQueue]);
|
|
437
|
-
const sendJson = useCallback((data) =>
|
|
432
|
+
const sendJson = useCallback((data) => {
|
|
433
|
+
try {
|
|
434
|
+
return send(JSON.stringify(data));
|
|
435
|
+
} catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}, [send]);
|
|
438
439
|
const actions = useMemo(() => ({
|
|
439
440
|
send,
|
|
440
441
|
sendJson,
|
|
@@ -452,7 +453,7 @@ function createWsContext(options) {
|
|
|
452
453
|
value: actions,
|
|
453
454
|
children: /* @__PURE__ */ jsx(StoreCtx.Provider, {
|
|
454
455
|
value: store,
|
|
455
|
-
children: /* @__PURE__ */ jsx(
|
|
456
|
+
children: /* @__PURE__ */ jsx(EventsCtx.Provider, {
|
|
456
457
|
value: emitter,
|
|
457
458
|
children
|
|
458
459
|
})
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/ws-context/emitter.ts","../src/ws-context/ws-events.ts","../src/ws-context/liveness/resolve-ping.ts","../src/ws-context/liveness/controller.ts","../src/ws-context/liveness/liveness.ts","../src/ws-context/outgoing-queue.ts","../src/ws-context/store.ts","../src/ws-context/use-store.ts","../src/ws-context/ws-store.ts","../src/ws-context/ws-actions.ts","../src/ws-context/reconnect.ts","../src/ws-context/socket.ts","../src/ws-context/index.tsx"],"sourcesContent":["// Typed event emitter。\n// 執行期邏輯對齊 nanoevents 的 createNanoEvents(幾乎逐行相同);型別為本套件收斂版。\n// Project: nanoevents — https://github.com/ai/nanoevents\n// Author: Andrey Sitnik — https://github.com/ai\n// License: MIT — https://github.com/ai/nanoevents/blob/main/LICENSE\n// Source:\n// - https://github.com/ai/nanoevents/blob/main/index.js\n// - https://github.com/ai/nanoevents/blob/main/index.d.ts\n// Modifications: 內嵌以達成零 runtime 依賴;新增 useEmitter(React useState 包裝)。\n\nimport { useState } from \"react\";\n\nexport interface Emitter<\n Events extends { [E in keyof Events]: (...args: never[]) => void },\n> {\n events: { [E in keyof Events]?: Array<Events[E]> };\n emit<E extends keyof Events>(event: E, ...args: Parameters<Events[E]>): void;\n on<E extends keyof Events>(event: E, cb: Events[E]): () => void;\n}\n\nexport function createEmitter<\n Events extends { [E in keyof Events]: (...args: never[]) => void },\n>(): Emitter<Events> {\n return {\n events: {},\n emit(event, ...args) {\n const callbacks = this.events[event] || [];\n for (let i = 0, len = callbacks.length; i < len; i++) {\n callbacks[i]!(...args);\n }\n },\n on(event, cb) {\n (this.events[event] ||= []).push(cb);\n return () => {\n this.events[event] = this.events[event]?.filter((fn) => fn !== cb);\n };\n },\n };\n}\n\nexport function useEmitter<\n Events extends { [E in keyof Events]: (...args: never[]) => void },\n>(): Emitter<Events> {\n const [emitter] = useState(() => createEmitter<Events>());\n return emitter;\n}\n","import {\n createContext,\n useContext,\n useEffect,\n useRef,\n type Context,\n} from \"react\";\nimport type { Emitter } from \"./emitter\";\nimport type { WsEvents } from \"./types\";\n\nexport function createWsEventsContext() {\n return createContext<Emitter<WsEvents> | null>(null);\n}\n\nexport function createUseWsEvents(\n EmitterCtx: Context<Emitter<WsEvents> | null>,\n) {\n function useWsEvents<E extends keyof WsEvents>(\n type: E,\n handler: WsEvents[E],\n ): void {\n const emitter = useContext(EmitterCtx);\n if (!emitter) {\n throw new Error(\"useWsEvents 必須包在對應的 WsProvider 內\");\n }\n\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n\n useEffect(() => {\n return emitter.on(type, ((...args: never[]) => {\n (handlerRef.current as (...a: never[]) => void)(...args);\n }) as WsEvents[E]);\n }, [type, emitter]);\n }\n\n return useWsEvents;\n}\n","import type { LivenessOptions } from \"./types\";\n\nexport function resolvePingPayload(ping: LivenessOptions[\"ping\"]): unknown {\n return typeof ping === \"function\" ? ping() : ping;\n}\n","import { resolvePingPayload } from \"./resolve-ping\";\nimport type { LivenessOptions } from \"./types\";\n\nexport interface LivenessController {\n /** 開始探活 */\n start: (sendPing: () => void) => void;\n /** 停止探活 */\n stop: () => void;\n /** 收到訊息 */\n onMessage: (data: unknown) => void;\n}\n\nexport function createLivenessController(\n options: LivenessOptions,\n onTimeout: () => void,\n): LivenessController {\n const { intervalMs, timeoutMs, isPong } = options;\n\n let intervalId: ReturnType<typeof setInterval> | null = null;\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let sendPingRef: (() => void) | null = null;\n\n function clearTimeoutTimer(): void {\n if (timeoutId != null) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n }\n\n function armTimeout(): void {\n clearTimeoutTimer();\n timeoutId = setTimeout(onTimeout, timeoutMs);\n }\n\n function tick(): void {\n sendPingRef?.();\n armTimeout();\n }\n\n return {\n start(sendPing) {\n sendPingRef = sendPing;\n tick();\n intervalId = setInterval(tick, intervalMs);\n },\n\n stop() {\n if (intervalId != null) {\n clearInterval(intervalId);\n intervalId = null;\n }\n clearTimeoutTimer();\n sendPingRef = null;\n },\n\n onMessage(data) {\n if (isPong(data)) clearTimeoutTimer();\n },\n };\n}\n\nexport function createPingSender(\n ping: LivenessOptions[\"ping\"],\n sendJson: (data: unknown) => boolean,\n): () => void {\n return () => {\n sendJson(resolvePingPayload(ping));\n };\n}\n","import { useState } from \"react\";\nimport {\n createLivenessController,\n createPingSender,\n type LivenessController,\n} from \"./controller\";\nimport type { LivenessOptions } from \"./types\";\n\nexport interface Liveness {\n /** 開始探活 */\n start: (ws: WebSocket) => void;\n /** 停止探活 */\n stop: () => void;\n /** 收到訊息 */\n onMessage: (data: unknown) => void;\n}\n\nconst DISABLED_LIVENESS: Liveness = {\n start() {},\n stop() {},\n onMessage() {},\n};\n\nexport function createLiveness(\n options: LivenessOptions,\n getActiveSocket: () => WebSocket | null,\n): Liveness {\n let controller: LivenessController | null = null;\n\n return {\n start(ws) {\n controller?.stop();\n controller = createLivenessController(options, () => {\n const current = getActiveSocket();\n if (current?.readyState === WebSocket.OPEN) current.close();\n });\n const sendPing = createPingSender(options.ping, (data) => {\n if (ws.readyState !== WebSocket.OPEN) return false;\n ws.send(JSON.stringify(data));\n return true;\n });\n controller.start(sendPing);\n },\n\n stop() {\n controller?.stop();\n controller = null;\n },\n\n onMessage(data) {\n controller?.onMessage(data);\n },\n };\n}\n\nexport function useLiveness(\n options: LivenessOptions | undefined,\n getActiveSocket: () => WebSocket | null,\n): Liveness {\n const [session] = useState(() =>\n options ? createLiveness(options, getActiveSocket) : DISABLED_LIVENESS,\n );\n return session;\n}\n","import { useState } from \"react\";\n\nexport type OutgoingData = Parameters<WebSocket[\"send\"]>[0];\n\nexport interface OutgoingQueue {\n /** 列隊 */\n enqueue: (data: OutgoingData) => boolean;\n /** 清空 */\n clear: () => void;\n /** 依序送出後清空 */\n flush: (send: (data: OutgoingData) => void) => void;\n}\n\nexport function createOutgoingQueue(max: number): OutgoingQueue {\n let items: OutgoingData[] = [];\n\n return {\n enqueue(data) {\n if (max <= 0 || items.length >= max) return false;\n items.push(data);\n return true;\n },\n clear() {\n items = [];\n },\n flush(send) {\n const queued = items;\n items = [];\n for (const data of queued) send(data);\n },\n };\n}\n\nexport function useOutgoingQueue(max: number): OutgoingQueue {\n const [queue] = useState(() => createOutgoingQueue(max));\n return queue;\n}\n","// 精簡外部 store:只保留本套件需要的 getState / setState / subscribe / getInitialState。\n// 靈感與行為對齊 zustand/vanilla(非完整搬移;無 middleware、無 replace、無 initializer factory)。\n// Project: zustand — https://github.com/pmndrs/zustand\n// Author: pmndrs (Poimandres) — https://github.com/pmndrs\n// License: MIT — https://github.com/pmndrs/zustand/blob/main/LICENSE\n// Source: https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts\n// Modifications: 內嵌子集以達成零 runtime 依賴。\n\nexport type StoreApi<State> = {\n getState: () => State;\n getInitialState: () => State;\n setState: (\n partial: Partial<State> | ((state: State) => Partial<State>),\n ) => void;\n subscribe: (listener: (state: State, prev: State) => void) => () => void;\n};\n\nexport function createStore<State extends object>(\n initialState: State,\n): StoreApi<State> {\n let state = initialState;\n const listeners = new Set<(state: State, prev: State) => void>();\n\n return {\n getState: () => state,\n getInitialState: () => initialState,\n setState: (partial) => {\n const nextPartial =\n typeof partial === \"function\" ? partial(state) : partial;\n if (Object.is(nextPartial, state)) return;\n const prev = state;\n state = Object.assign({}, state, nextPartial);\n for (const listener of listeners) listener(state, prev);\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n","// 訂閱外部 store(selector + useSyncExternalStore)。\n// 靈感來自 zustand/react 的 useStore(非完整搬移;selector 必填、無 useDebugValue)。\n// Project: zustand — https://github.com/pmndrs/zustand\n// Author: pmndrs (Poimandres) — https://github.com/pmndrs\n// License: MIT — https://github.com/pmndrs/zustand/blob/main/LICENSE\n// Source: https://github.com/pmndrs/zustand/blob/main/src/react.ts\n// Modifications: 內嵌以達成零 runtime 依賴。\n// 用途:訂閱 WsState(連線健康/佇列/重連);訊息不走此 store。\n\nimport { useSyncExternalStore } from \"react\";\nimport type { StoreApi } from \"./store\";\n\nexport function useStore<State, Selected>(\n store: StoreApi<State>,\n selector: (state: State) => Selected,\n): Selected {\n return useSyncExternalStore(\n store.subscribe,\n () => selector(store.getState()),\n () => selector(store.getInitialState()),\n );\n}\n","import { createContext, useContext, useState, type Context } from \"react\";\nimport { createStore, type StoreApi } from \"./store\";\nimport { useStore } from \"./use-store\";\n\n/**\n * 連線生命週期狀態(`WsState` 的一環)。\n *\n * - `idle` — 尚未連線\n * - `connecting` — 連線中\n * - `open` — 已連線\n * - `closed` — 已斷線\n *\n * 錯誤用 `useWsEvents(\"error\")`;不另設 error status。\n */\nexport type WsStatus = \"idle\" | \"connecting\" | \"open\" | \"closed\";\n\n/**\n * Provider 連線意圖與重連策略階段(`WsState` 的一環)。\n *\n * 與 `status`(WebSocket readyState 映射)正交,補足 UI 無法單靠 `status` 判斷的情境。\n *\n * - `idle` — 未連線、未排程重連(初始或手動 `disconnect()`)\n * - `connecting` — 首次或手動 `connect()` 連線中\n * - `open` — 已連線\n * - `reconnecting` — 自動重連週期(等待計時器或連線中);細節搭配 `status`、`reconnectAttempt`\n * - `stopped` — 不會再自動重連;`reconnectExhausted` 區分達上限或未啟用重連\n */\nexport type WsPhase =\n | \"idle\"\n | \"connecting\"\n | \"open\"\n | \"reconnecting\"\n | \"stopped\";\n\n/**\n * 可訂閱的連線層 state(低頻更新)。\n *\n * 只放:**連線健康**、**outbound 佇列**、**重連** 等連線生命週期資訊。\n *\n * 不放:訊息 payload、訊息歷史、業務資料(請用 `useWsEvents` 或自行管理 state)。\n *\n * 未來可能擴充例如 `pendingCount`;新增欄位時請維持低頻、可 selector 訂閱。\n */\nexport type WsState = {\n /** 連線生命週期狀態 */\n status: WsStatus;\n /** Provider 連線意圖與重連策略階段 */\n phase: WsPhase;\n /**\n * 本輪已排程的自動重連次數(意外斷線當下 +1,非重連成功才 +1)。\n *\n * 顯示為 `n` 時,代表第 `n` 次重連已排程或進行中。\n *\n * 成功 `open`、手動 `connect()` 或主動 `disconnect()` 歸零。\n */\n reconnectAttempt: number;\n /**\n * 本輪自動重連已達 `reconnectMax` 且最後一次也失敗。\n *\n * 手動 `connect()` 或 `disconnect()` 設定為 `false`\n */\n reconnectExhausted: boolean;\n};\n\nexport type WsStoreApi = StoreApi<WsState>;\n\nexport function createWsStore(init: WsStatus = \"idle\"): WsStoreApi {\n const phase: WsPhase = init === \"open\" ? \"open\" : init === \"idle\" ? \"idle\" : \"connecting\";\n return createStore<WsState>({\n status: init,\n phase,\n reconnectAttempt: 0,\n reconnectExhausted: false,\n });\n}\n\n/** 每個 `WsProvider` 各有一份 {@link WsState} store */\nexport function useWsStoreApi(): WsStoreApi {\n const [store] = useState(() => createWsStore());\n return store;\n}\n\nexport function createWsStoreContext() {\n return createContext<WsStoreApi | null>(null);\n}\n\n/** 訂閱 {@link WsState};建議以 selector 只取需要的連線層欄位。 */\nexport function createUseWsStore(StoreCtx: Context<WsStoreApi | null>) {\n function useWsStore(): WsState;\n function useWsStore<T>(selector: (state: WsState) => T): T;\n function useWsStore<T>(selector?: (state: WsState) => T): T {\n const store = useContext(StoreCtx);\n if (!store) {\n throw new Error(\"useWsStore 必須包在對應的 WsProvider 內\");\n }\n const select = selector ?? ((state: WsState) => state as T);\n return useStore(store, select);\n }\n\n return useWsStore;\n}\n","import { createContext, useContext, type Context } from \"react\";\nimport type { WsContextValue } from \"./types\";\n\nexport function createWsActionsContext() {\n return createContext<WsContextValue | null>(null);\n}\n\nexport function createUseWsActions(ActionsCtx: Context<WsContextValue | null>) {\n function useWsActions(): WsContextValue {\n const value = useContext(ActionsCtx);\n if (!value) {\n throw new Error(\"useWsActions 必須包在對應的 WsProvider 內\");\n }\n return value;\n }\n\n return useWsActions;\n}\n","import { useState } from \"react\";\n\nexport interface ReconnectCallbacks {\n /** 讀取 store 的 `reconnectAttempt` */\n getAttempt: () => number;\n /** 寫入 store 的 `reconnectAttempt` */\n setAttempt: (attempt: number) => void;\n /** 寫入 store 的 `reconnectExhausted` */\n setExhausted: (exhausted: boolean) => void;\n}\n\nexport interface Reconnect {\n /** 開始連線時呼叫;回傳 `true` 表示由重連計時器觸發 */\n onConnectBegin: () => boolean;\n /** 連線成功時呼叫 */\n onOpen: () => void;\n /** 意外斷線後嘗試重連;有排重連回 `true` */\n scheduleAfterClose: () => boolean;\n /** 主動斷線或元件卸載時呼叫 */\n cancel: () => void;\n /** 設定重連時要執行的 connect */\n bindOnReconnect: (fn: () => void) => void;\n}\n\nexport function createReconnect(\n reconnectMs: number,\n reconnectMax: number,\n callbacks: ReconnectCallbacks,\n): Reconnect {\n let intentionalClose = false;\n let fromTimer = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let onReconnect = () => {};\n\n const clearTimer = () => {\n if (timer != null) {\n clearTimeout(timer);\n timer = null;\n }\n };\n\n const resetCycle = () => {\n if (callbacks.getAttempt() !== 0) callbacks.setAttempt(0);\n callbacks.setExhausted(false);\n };\n\n return {\n onConnectBegin() {\n clearTimer();\n intentionalClose = false;\n const reconnecting = fromTimer;\n if (!fromTimer) resetCycle();\n fromTimer = false;\n return reconnecting;\n },\n\n onOpen() {\n resetCycle();\n },\n\n scheduleAfterClose() {\n if (intentionalClose || reconnectMs <= 0) return false;\n const attempt = callbacks.getAttempt();\n if (reconnectMax > 0 && attempt >= reconnectMax) {\n callbacks.setExhausted(true);\n return false;\n }\n callbacks.setAttempt(attempt + 1);\n fromTimer = true;\n // 固定間隔重連,無 backoff;之後可換成指數退避\n timer = setTimeout(() => {\n timer = null;\n onReconnect();\n }, reconnectMs);\n return true;\n },\n\n cancel() {\n intentionalClose = true;\n clearTimer();\n resetCycle();\n },\n\n bindOnReconnect(fn) {\n onReconnect = fn;\n },\n };\n}\n\nexport function useReconnect(\n reconnectMs: number,\n reconnectMax: number,\n callbacks: ReconnectCallbacks,\n): Reconnect {\n const [session] = useState(() =>\n createReconnect(reconnectMs, reconnectMax, callbacks),\n );\n return session;\n}\n","export function detachAndClose(ws: WebSocket): void {\n ws.onopen = null;\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n if (ws.readyState < WebSocket.CLOSING) ws.close();\n}\n\nexport function clientCloseEvent(reason: string): CloseEvent {\n return new CloseEvent(\"close\", {\n code: 1000,\n reason,\n wasClean: true,\n });\n}\n","import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n type PropsWithChildren,\n} from \"react\";\nimport { useEmitter } from \"./emitter\";\nimport { createUseWsEvents, createWsEventsContext } from \"./ws-events\";\nimport type { CreateWsContextOptions, WsContextValue, WsEvents } from \"./types\";\nimport { useLiveness } from \"./liveness/liveness\";\nimport { useOutgoingQueue } from \"./outgoing-queue\";\nimport {\n createUseWsStore,\n createWsStoreContext,\n useWsStoreApi,\n type WsPhase,\n type WsStatus,\n} from \"./ws-store\";\nimport { createUseWsActions, createWsActionsContext } from \"./ws-actions\";\nimport { useReconnect } from \"./reconnect\";\nimport { clientCloseEvent, detachAndClose } from \"./socket\";\n\nexport type { CreateWsContextOptions, WsContextValue, WsEvents } from \"./types\";\n\nfunction defaultParse(data: MessageEvent[\"data\"]): unknown {\n if (typeof data !== \"string\") return data;\n try {\n return JSON.parse(data) as unknown;\n } catch {\n return data;\n }\n}\n\n/**\n * @example\n * ```ts\n * export const { WsProvider, useWsActions, useWsStore, useWsEvents } =\n * createWsContext({ url: \"ws://localhost:8080\" });\n * ```\n */\nexport function createWsContext(options: CreateWsContextOptions) {\n const {\n url,\n protocols,\n autoConnect = true,\n reconnectMs = 0,\n reconnectMax = 0,\n outgoingQueueMax = 0,\n parse = defaultParse,\n liveness,\n } = options;\n\n const ActionsCtx = createWsActionsContext();\n const useWsActions = createUseWsActions(ActionsCtx);\n const StoreCtx = createWsStoreContext();\n const useWsStore = createUseWsStore(StoreCtx);\n const EmitterCtx = createWsEventsContext();\n const useWsEvents = createUseWsEvents(EmitterCtx);\n\n function WsProvider({ children }: PropsWithChildren) {\n const wsRef = useRef<WebSocket | null>(null);\n const store = useWsStoreApi();\n const emitter = useEmitter<WsEvents>();\n const reconnect = useReconnect(reconnectMs, reconnectMax, {\n getAttempt: () => store.getState().reconnectAttempt,\n setAttempt: (reconnectAttempt) => store.setState({ reconnectAttempt }),\n setExhausted: (reconnectExhausted) =>\n store.setState({ reconnectExhausted }),\n });\n const outgoingQueue = useOutgoingQueue(outgoingQueueMax);\n const livenessSession = useLiveness(liveness, () => wsRef.current);\n\n const setStatus = useCallback(\n (status: WsStatus) => {\n store.setState({ status });\n },\n [store],\n );\n\n const setPhase = useCallback(\n (phase: WsPhase) => {\n store.setState({ phase });\n },\n [store],\n );\n\n const getStatus = useCallback<WsContextValue[\"getStatus\"]>(\n () => store.getState().status,\n [store],\n );\n\n const disconnect = useCallback<WsContextValue[\"disconnect\"]>(() => {\n reconnect.cancel();\n livenessSession.stop();\n outgoingQueue.clear();\n setPhase(\"idle\");\n const ws = wsRef.current;\n wsRef.current = null;\n if (ws) {\n detachAndClose(ws);\n setStatus(\"closed\");\n emitter.emit(\"close\", clientCloseEvent(\"client disconnect\"));\n } else {\n setStatus(\"closed\");\n }\n }, [setStatus, setPhase, emitter, outgoingQueue, livenessSession, reconnect]);\n\n const connect = useCallback<WsContextValue[\"connect\"]>(() => {\n if (typeof window === \"undefined\") return;\n\n const fromReconnect = reconnect.onConnectBegin();\n livenessSession.stop();\n\n const prev = wsRef.current;\n if (prev) {\n wsRef.current = null;\n detachAndClose(prev);\n emitter.emit(\"close\", clientCloseEvent(\"reconnect\"));\n }\n\n setStatus(\"connecting\");\n setPhase(fromReconnect ? \"reconnecting\" : \"connecting\");\n\n const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);\n wsRef.current = ws;\n\n ws.onopen = (event) => {\n if (wsRef.current !== ws) return;\n reconnect.onOpen();\n setStatus(\"open\");\n setPhase(\"open\");\n outgoingQueue.flush((data) => ws.send(data));\n livenessSession.start(ws);\n emitter.emit(\"open\", event);\n };\n\n ws.onmessage = (event) => {\n if (wsRef.current !== ws) return;\n const data = parse(event.data);\n livenessSession.onMessage(data);\n emitter.emit(\"message\", data, event);\n };\n\n ws.onerror = (event) => {\n if (wsRef.current !== ws) return;\n emitter.emit(\"error\", event);\n };\n\n ws.onclose = (event) => {\n if (wsRef.current === ws) wsRef.current = null;\n livenessSession.stop();\n setStatus(\"closed\");\n emitter.emit(\"close\", event);\n const scheduled = reconnect.scheduleAfterClose();\n if (scheduled) {\n setPhase(\"reconnecting\");\n } else if (store.getState().phase !== \"idle\") {\n setPhase(\"stopped\");\n }\n };\n }, [setStatus, setPhase, emitter, outgoingQueue, livenessSession, reconnect, store]);\n\n reconnect.bindOnReconnect(connect);\n\n useEffect(() => {\n if (autoConnect) connect();\n return () => {\n reconnect.cancel();\n livenessSession.stop();\n outgoingQueue.clear();\n const ws = wsRef.current;\n wsRef.current = null;\n if (ws) {\n detachAndClose(ws);\n emitter.emit(\"close\", clientCloseEvent(\"provider unmount\"));\n }\n };\n }, [connect, emitter, outgoingQueue, livenessSession, reconnect]);\n\n const send = useCallback<WsContextValue[\"send\"]>(\n (data) => {\n const ws = wsRef.current;\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(data);\n return true;\n }\n return outgoingQueue.enqueue(data);\n },\n [outgoingQueue],\n );\n\n const sendJson = useCallback<WsContextValue[\"sendJson\"]>(\n (data) => send(JSON.stringify(data)),\n [send],\n );\n\n const actions = useMemo<WsContextValue>(\n () => ({\n send,\n sendJson,\n connect,\n disconnect,\n getStatus,\n }),\n [send, sendJson, connect, disconnect, getStatus],\n );\n\n return (\n <ActionsCtx.Provider value={actions}>\n <StoreCtx.Provider value={store}>\n <EmitterCtx.Provider value={emitter}>{children}</EmitterCtx.Provider>\n </StoreCtx.Provider>\n </ActionsCtx.Provider>\n );\n }\n\n return {\n WsProvider,\n useWsActions,\n useWsStore,\n useWsEvents,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/ws-context/emitter.ts","../src/ws-context/ws-events.ts","../src/ws-context/liveness/resolve-ping.ts","../src/ws-context/liveness/controller.ts","../src/ws-context/liveness/liveness.ts","../src/ws-context/outgoing-queue.ts","../src/ws-context/store.ts","../src/ws-context/use-store.ts","../src/ws-context/ws-store.ts","../src/ws-context/ws-actions.ts","../src/ws-context/reconnect.ts","../src/ws-context/socket.ts","../src/ws-context/index.tsx"],"sourcesContent":["// Typed event emitter。\n// 執行期邏輯對齊 nanoevents 的 createNanoEvents(幾乎逐行相同);型別為本套件收斂版。\n// Project: nanoevents — https://github.com/ai/nanoevents\n// Author: Andrey Sitnik — https://github.com/ai\n// License: MIT — https://github.com/ai/nanoevents/blob/main/LICENSE\n// Source:\n// - https://github.com/ai/nanoevents/blob/main/index.js\n// - https://github.com/ai/nanoevents/blob/main/index.d.ts\n// Modifications: 內嵌以達成零 runtime 依賴。\n\nexport interface Emitter<\n Events extends { [E in keyof Events]: (...args: never[]) => void },\n> {\n events: { [E in keyof Events]?: Array<Events[E]> };\n emit<E extends keyof Events>(event: E, ...args: Parameters<Events[E]>): void;\n on<E extends keyof Events>(event: E, cb: Events[E]): () => void;\n}\n\nexport function createEmitter<\n Events extends { [E in keyof Events]: (...args: never[]) => void },\n>(): Emitter<Events> {\n return {\n events: {},\n emit(event, ...args) {\n const callbacks = this.events[event] || [];\n for (let i = 0, len = callbacks.length; i < len; i++) {\n callbacks[i]!(...args);\n }\n },\n on(event, cb) {\n (this.events[event] ||= []).push(cb);\n return () => {\n this.events[event] = this.events[event]?.filter((fn) => fn !== cb);\n };\n },\n };\n}\n","import {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n type Context,\n} from \"react\";\nimport { createEmitter, type Emitter } from \"./emitter\";\nimport type { WsEvents } from \"./types\";\n\nexport type WsEventsEmitter = Emitter<WsEvents>;\n\nexport function createWsEventsContext() {\n return createContext<WsEventsEmitter | null>(null);\n}\n\n/** 每個 `WsProvider` 各有一份 event emitter */\nexport function useWsEventsApi(): WsEventsEmitter {\n const [emitter] = useState(() => createEmitter<WsEvents>());\n return emitter;\n}\n\nexport function createUseWsEvents(EventsCtx: Context<WsEventsEmitter | null>) {\n function useWsEvents<E extends keyof WsEvents>(\n type: E,\n handler: WsEvents[E],\n ): void {\n const emitter = useContext(EventsCtx);\n if (!emitter) {\n throw new Error(\"useWsEvents 必須包在對應的 WsProvider 內\");\n }\n\n const handlerRef = useRef(handler);\n\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n return emitter.on(type, ((...args: never[]) => {\n (handlerRef.current as (...a: never[]) => void)(...args);\n }) as WsEvents[E]);\n }, [type, emitter]);\n }\n\n return useWsEvents;\n}\n","import type { LivenessOptions } from \"./types\";\n\nexport function resolvePingPayload(ping: LivenessOptions[\"ping\"]): unknown {\n return typeof ping === \"function\" ? ping() : ping;\n}\n","import { resolvePingPayload } from \"./resolve-ping\";\nimport type { LivenessOptions } from \"./types\";\n\nexport interface LivenessController {\n /** 開始探活 */\n start: (sendPing: () => void) => void;\n /** 停止探活 */\n stop: () => void;\n /** 收到訊息 */\n onMessage: (data: unknown) => void;\n}\n\nexport function createLivenessController(\n options: LivenessOptions,\n onTimeout: () => void,\n): LivenessController {\n const { intervalMs, timeoutMs, isPong } = options;\n\n let intervalId: ReturnType<typeof setInterval> | null = null;\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let sendPingRef: (() => void) | null = null;\n\n function clearTimeoutTimer(): void {\n if (timeoutId != null) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n }\n\n function armTimeout(): void {\n clearTimeoutTimer();\n timeoutId = setTimeout(onTimeout, timeoutMs);\n }\n\n function tick(): void {\n sendPingRef?.();\n armTimeout();\n }\n\n return {\n start(sendPing) {\n sendPingRef = sendPing;\n tick();\n intervalId = setInterval(tick, intervalMs);\n },\n\n stop() {\n if (intervalId != null) {\n clearInterval(intervalId);\n intervalId = null;\n }\n clearTimeoutTimer();\n sendPingRef = null;\n },\n\n onMessage(data) {\n if (isPong(data)) clearTimeoutTimer();\n },\n };\n}\n\nexport function createPingSender(\n ping: LivenessOptions[\"ping\"],\n sendJson: (data: unknown) => boolean,\n): () => void {\n return () => {\n sendJson(resolvePingPayload(ping));\n };\n}\n","import { useState } from \"react\";\nimport {\n createLivenessController,\n createPingSender,\n type LivenessController,\n} from \"./controller\";\nimport type { LivenessOptions } from \"./types\";\n\nexport interface Liveness {\n /** 開始探活(socket 由 {@link createLiveness} 的 `getActiveSocket` 取得) */\n start: () => void;\n /** 停止探活 */\n stop: () => void;\n /** 收到訊息 */\n onMessage: (data: unknown) => void;\n}\n\nconst DISABLED_LIVENESS: Liveness = {\n start() {},\n stop() {},\n onMessage() {},\n};\n\nexport function createLiveness(\n options: LivenessOptions,\n getActiveSocket: () => WebSocket | null,\n): Liveness {\n let controller: LivenessController | null = null;\n\n return {\n start() {\n controller?.stop();\n controller = createLivenessController(options, () => {\n const current = getActiveSocket();\n if (current?.readyState === WebSocket.OPEN) current.close();\n });\n const sendPing = createPingSender(options.ping, (data) => {\n const current = getActiveSocket();\n if (!current || current.readyState !== WebSocket.OPEN) return false;\n current.send(JSON.stringify(data));\n return true;\n });\n controller.start(sendPing);\n },\n\n stop() {\n controller?.stop();\n controller = null;\n },\n\n onMessage(data) {\n controller?.onMessage(data);\n },\n };\n}\n\nexport function useLiveness(\n options: LivenessOptions | undefined,\n getActiveSocket: () => WebSocket | null,\n): Liveness {\n const [session] = useState(() =>\n options ? createLiveness(options, getActiveSocket) : DISABLED_LIVENESS,\n );\n return session;\n}\n","import { useState } from \"react\";\n\nexport type OutgoingData = Parameters<WebSocket[\"send\"]>[0];\n\nexport interface OutgoingQueue {\n /** 列隊 */\n enqueue: (data: OutgoingData) => boolean;\n /** 清空 */\n clear: () => void;\n /** 依序送出後清空 */\n flush: (send: (data: OutgoingData) => void) => void;\n}\n\nexport function createOutgoingQueue(max: number): OutgoingQueue {\n let items: OutgoingData[] = [];\n\n return {\n enqueue(data) {\n if (max <= 0 || items.length >= max) return false;\n items.push(data);\n return true;\n },\n clear() {\n items = [];\n },\n flush(send) {\n const queued = items;\n items = [];\n for (const data of queued) send(data);\n },\n };\n}\n\nexport function useOutgoingQueue(max: number): OutgoingQueue {\n const [queue] = useState(() => createOutgoingQueue(max));\n return queue;\n}\n","// 精簡外部 store:只保留本套件需要的 getState / setState / subscribe / getInitialState。\n// 靈感與行為對齊 zustand/vanilla(非完整搬移;無 middleware、無 replace、無 initializer factory)。\n// Project: zustand — https://github.com/pmndrs/zustand\n// Author: pmndrs (Poimandres) — https://github.com/pmndrs\n// License: MIT — https://github.com/pmndrs/zustand/blob/main/LICENSE\n// Source: https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts\n// Modifications: 內嵌子集以達成零 runtime 依賴。\n\nexport type StoreApi<State> = {\n getState: () => State;\n getInitialState: () => State;\n setState: (\n partial: Partial<State> | ((state: State) => Partial<State>),\n ) => void;\n subscribe: (listener: (state: State, prev: State) => void) => () => void;\n};\n\nexport function createStore<State extends object>(\n initialState: State,\n): StoreApi<State> {\n let state = initialState;\n const listeners = new Set<(state: State, prev: State) => void>();\n\n return {\n getState: () => state,\n getInitialState: () => initialState,\n setState: (partial) => {\n const nextPartial =\n typeof partial === \"function\" ? partial(state) : partial;\n if (!hasPartialChanged(state, nextPartial)) return;\n const prev = state;\n state = Object.assign({}, state, nextPartial);\n for (const listener of listeners) listener(state, prev);\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** partial 內所有 key 值與 state 相同則視為無變更,不通知訂閱者 */\nfunction hasPartialChanged<State extends object>(\n state: State,\n partial: Partial<State>,\n): boolean {\n for (const [key, value] of Object.entries(partial)) {\n if (!Object.is(state[key as keyof State], value)) {\n return true;\n }\n }\n return false;\n}\n","// 訂閱外部 store(selector + useSyncExternalStore)。\n// 靈感來自 zustand/react 的 useStore(非完整搬移;selector 必填、無 useDebugValue)。\n// Project: zustand — https://github.com/pmndrs/zustand\n// Author: pmndrs (Poimandres) — https://github.com/pmndrs\n// License: MIT — https://github.com/pmndrs/zustand/blob/main/LICENSE\n// Source: https://github.com/pmndrs/zustand/blob/main/src/react.ts\n// Modifications: 內嵌以達成零 runtime 依賴。\n// 用途:訂閱 WsState(連線健康/佇列/重連);訊息不走此 store。\n\nimport { useSyncExternalStore } from \"react\";\nimport type { StoreApi } from \"./store\";\n\nexport function useStore<State, Selected>(\n store: StoreApi<State>,\n selector: (state: State) => Selected,\n): Selected {\n return useSyncExternalStore(\n store.subscribe,\n () => selector(store.getState()),\n () => selector(store.getInitialState()),\n );\n}\n","import { createContext, useContext, useState, type Context } from \"react\";\nimport { createStore, type StoreApi } from \"./store\";\nimport { useStore } from \"./use-store\";\n\n/**\n * 連線生命週期狀態(`WsState` 的一環)。\n *\n * - `idle` — 尚未連線\n * - `connecting` — 連線中\n * - `open` — 已連線\n * - `closed` — 已斷線\n *\n * 錯誤用 `useWsEvents(\"error\")`;不另設 error status。\n */\nexport type WsStatus = \"idle\" | \"connecting\" | \"open\" | \"closed\";\n\n/**\n * Provider 連線意圖與重連策略階段(`WsState` 的一環)。\n *\n * 與 `status`(WebSocket readyState 映射)正交,補足 UI 無法單靠 `status` 判斷的情境。\n *\n * - `idle` — 未連線、未排程重連(初始或手動 `disconnect()`)\n * - `connecting` — 首次或手動 `connect()` 連線中\n * - `open` — 已連線\n * - `reconnecting` — 自動重連週期(等待計時器或連線中);細節搭配 `status`、`reconnectAttempt`\n * - `stopped` — 不會再自動重連;`reconnectExhausted` 區分達上限或未啟用重連\n */\nexport type WsPhase =\n | \"idle\"\n | \"connecting\"\n | \"open\"\n | \"reconnecting\"\n | \"stopped\";\n\n/**\n * 可訂閱的連線層 state(低頻更新)。\n *\n * 只放:**連線健康**、**outbound 佇列**、**重連** 等連線生命週期資訊。\n *\n * 不放:訊息 payload、訊息歷史、業務資料(請用 `useWsEvents` 或自行管理 state)。\n *\n * 未來可能擴充例如 `pendingCount`;新增欄位時請維持低頻、可 selector 訂閱。\n */\nexport type WsState = {\n /** 連線生命週期狀態 */\n status: WsStatus;\n /** Provider 連線意圖與重連策略階段 */\n phase: WsPhase;\n /**\n * 本輪已排程的自動重連次數(意外斷線當下 +1,非重連成功才 +1)。\n *\n * 顯示為 `n` 時,代表第 `n` 次重連已排程或進行中。\n *\n * 成功 `open`、手動 `connect()` 或主動 `disconnect()` 歸零。\n */\n reconnectAttempt: number;\n /**\n * 本輪自動重連已達 `reconnectMax` 且最後一次也失敗。\n *\n * 手動 `connect()` 或 `disconnect()` 設定為 `false`\n */\n reconnectExhausted: boolean;\n};\n\nexport type WsStoreApi = StoreApi<WsState>;\n\nexport function createWsStore(init: WsStatus = \"idle\"): WsStoreApi {\n const phase: WsPhase =\n init === \"open\" || init === \"idle\" ? init : \"connecting\";\n return createStore<WsState>({\n status: init,\n phase,\n reconnectAttempt: 0,\n reconnectExhausted: false,\n });\n}\n\nexport function createWsStoreContext() {\n return createContext<WsStoreApi | null>(null);\n}\n\n/** 每個 `WsProvider` 各有一份 {@link WsState} store */\nexport function useWsStoreApi(): WsStoreApi {\n const [store] = useState(() => createWsStore());\n return store;\n}\n\n/** 訂閱 {@link WsState};建議以 selector 只取需要的連線層欄位。 */\nexport function createUseWsStore(StoreCtx: Context<WsStoreApi | null>) {\n function useWsStore(): WsState;\n function useWsStore<T>(selector: (state: WsState) => T): T;\n function useWsStore<T>(selector?: (state: WsState) => T): T {\n const store = useContext(StoreCtx);\n if (!store) {\n throw new Error(\"useWsStore 必須包在對應的 WsProvider 內\");\n }\n const select = selector ?? ((state: WsState) => state as T);\n return useStore(store, select);\n }\n\n return useWsStore;\n}\n","import { createContext, useContext, type Context } from \"react\";\nimport type { WsContextValue } from \"./types\";\n\n/** actions 無獨立 instance;由 WsProvider 以 useMemo 組裝後注入 Context */\nexport function createWsActionsContext() {\n return createContext<WsContextValue | null>(null);\n}\n\nexport function createUseWsActions(ActionsCtx: Context<WsContextValue | null>) {\n function useWsActions(): WsContextValue {\n const value = useContext(ActionsCtx);\n if (!value) {\n throw new Error(\"useWsActions 必須包在對應的 WsProvider 內\");\n }\n return value;\n }\n\n return useWsActions;\n}\n","import { useState } from \"react\";\n\nexport interface ReconnectCallbacks {\n /** 讀取 store 的 `reconnectAttempt` */\n getAttempt: () => number;\n /** 寫入 store 的 `reconnectAttempt` */\n setAttempt: (attempt: number) => void;\n /** 寫入 store 的 `reconnectExhausted` */\n setExhausted: (exhausted: boolean) => void;\n}\n\nexport interface Reconnect {\n /** 開始連線時呼叫;回傳 `true` 表示由重連計時器觸發 */\n onConnectBegin: () => boolean;\n /** 連線成功時呼叫 */\n onOpen: () => void;\n /** 意外斷線後嘗試重連;有排重連回 `true` */\n scheduleAfterClose: () => boolean;\n /** 主動斷線或元件卸載時呼叫 */\n cancel: () => void;\n /** 設定重連時要執行的 connect */\n bindOnReconnect: (fn: () => void) => void;\n}\n\nexport function createReconnect(\n reconnectMs: number,\n reconnectMax: number,\n callbacks: ReconnectCallbacks,\n): Reconnect {\n let intentionalClose = false;\n let fromTimer = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let onReconnect = () => {};\n\n const clearTimer = () => {\n if (timer != null) {\n clearTimeout(timer);\n timer = null;\n }\n };\n\n const resetCycle = () => {\n if (callbacks.getAttempt() !== 0) callbacks.setAttempt(0);\n callbacks.setExhausted(false);\n };\n\n return {\n onConnectBegin() {\n clearTimer();\n intentionalClose = false;\n const reconnecting = fromTimer;\n if (!fromTimer) resetCycle();\n fromTimer = false;\n return reconnecting;\n },\n\n onOpen() {\n resetCycle();\n },\n\n scheduleAfterClose() {\n if (intentionalClose || reconnectMs <= 0) return false;\n const attempt = callbacks.getAttempt();\n if (reconnectMax > 0 && attempt >= reconnectMax) {\n callbacks.setExhausted(true);\n return false;\n }\n callbacks.setAttempt(attempt + 1);\n fromTimer = true;\n // 固定間隔重連,無 backoff;之後可換成指數退避\n timer = setTimeout(() => {\n timer = null;\n onReconnect();\n }, reconnectMs);\n return true;\n },\n\n cancel() {\n intentionalClose = true;\n clearTimer();\n resetCycle();\n },\n\n bindOnReconnect(fn) {\n onReconnect = fn;\n },\n };\n}\n\nexport function useReconnect(\n reconnectMs: number,\n reconnectMax: number,\n callbacks: ReconnectCallbacks,\n): Reconnect {\n const [session] = useState(() =>\n createReconnect(reconnectMs, reconnectMax, callbacks),\n );\n return session;\n}\n","export function detachAndClose(ws: WebSocket): void {\n ws.onopen = null;\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n if (ws.readyState < WebSocket.CLOSING) ws.close();\n}\n\nexport function clientCloseEvent(reason: string): CloseEvent {\n return new CloseEvent(\"close\", {\n code: 1000,\n reason,\n wasClean: true,\n });\n}\n","import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n type PropsWithChildren,\n} from \"react\";\nimport { createUseWsEvents, createWsEventsContext, useWsEventsApi } from \"./ws-events\";\nimport type { CreateWsContextOptions, WsContextValue } from \"./types\";\nimport { useLiveness } from \"./liveness/liveness\";\nimport { useOutgoingQueue } from \"./outgoing-queue\";\nimport {\n createUseWsStore,\n createWsStoreContext,\n useWsStoreApi,\n type WsPhase,\n} from \"./ws-store\";\nimport { createUseWsActions, createWsActionsContext } from \"./ws-actions\";\nimport { useReconnect } from \"./reconnect\";\nimport { clientCloseEvent, detachAndClose } from \"./socket\";\n\nexport type { CreateWsContextOptions, WsContextValue, WsEvents } from \"./types\";\n\nfunction defaultParse(data: MessageEvent[\"data\"]): unknown {\n if (typeof data !== \"string\") return data;\n try {\n return JSON.parse(data) as unknown;\n } catch {\n return data;\n }\n}\n\n/**\n * @example\n * ```ts\n * export const { WsProvider, useWsActions, useWsStore, useWsEvents } =\n * createWsContext({ url: \"ws://localhost:8080\" });\n * ```\n */\nexport function createWsContext(options: CreateWsContextOptions) {\n const {\n url,\n protocols,\n autoConnect = true,\n reconnectMs = 0,\n reconnectMax = 0,\n outgoingQueueMax = 0,\n parse = defaultParse,\n liveness,\n } = options;\n\n const StoreCtx = createWsStoreContext();\n const useWsStore = createUseWsStore(StoreCtx);\n const ActionsCtx = createWsActionsContext();\n const useWsActions = createUseWsActions(ActionsCtx);\n const EventsCtx = createWsEventsContext();\n const useWsEvents = createUseWsEvents(EventsCtx);\n\n function WsProvider({ children }: PropsWithChildren) {\n const wsRef = useRef<WebSocket | null>(null);\n const store = useWsStoreApi();\n const emitter = useWsEventsApi();\n const reconnect = useReconnect(reconnectMs, reconnectMax, {\n getAttempt: () => store.getState().reconnectAttempt,\n setAttempt: (reconnectAttempt) => store.setState({ reconnectAttempt }),\n setExhausted: (reconnectExhausted) =>\n store.setState({ reconnectExhausted }),\n });\n const outgoingQueue = useOutgoingQueue(outgoingQueueMax);\n const livenessSession = useLiveness(liveness, () => wsRef.current);\n\n const getStatus = useCallback<WsContextValue[\"getStatus\"]>(\n () => store.getState().status,\n [store],\n );\n\n /** 主動斷線與 Provider unmount 共用;`reason` 區分 `\"client disconnect\"` / `\"provider unmount\"` */\n const teardown = useCallback(\n (reason: string) => {\n reconnect.cancel();\n livenessSession.stop();\n outgoingQueue.clear();\n store.setState({ phase: \"idle\", status: \"closed\" });\n const ws = wsRef.current;\n wsRef.current = null;\n if (ws) {\n detachAndClose(ws);\n emitter.emit(\"close\", clientCloseEvent(reason));\n }\n },\n [store, emitter, outgoingQueue, livenessSession, reconnect],\n );\n\n const disconnect = useCallback<WsContextValue[\"disconnect\"]>(\n () => teardown(\"client disconnect\"),\n [teardown],\n );\n\n const connect = useCallback<WsContextValue[\"connect\"]>(() => {\n if (typeof window === \"undefined\") return;\n\n const fromReconnect = reconnect.onConnectBegin();\n livenessSession.stop();\n\n const prev = wsRef.current;\n if (prev) {\n wsRef.current = null;\n detachAndClose(prev);\n emitter.emit(\"close\", clientCloseEvent(\"reconnect\"));\n }\n\n store.setState({\n status: \"connecting\",\n phase: fromReconnect ? \"reconnecting\" : \"connecting\",\n });\n\n const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);\n wsRef.current = ws;\n\n ws.onopen = (event) => {\n if (wsRef.current !== ws) return;\n reconnect.onOpen();\n store.setState({ status: \"open\", phase: \"open\" });\n outgoingQueue.flush((data) => ws.send(data));\n livenessSession.start();\n emitter.emit(\"open\", event);\n };\n\n ws.onmessage = (event) => {\n if (wsRef.current !== ws) return;\n const data = parse(event.data);\n livenessSession.onMessage(data);\n emitter.emit(\"message\", data, event);\n };\n\n ws.onerror = (event) => {\n if (wsRef.current !== ws) return;\n emitter.emit(\"error\", event);\n };\n\n ws.onclose = (event) => {\n if (wsRef.current === ws) wsRef.current = null;\n livenessSession.stop();\n const scheduled = reconnect.scheduleAfterClose();\n // 意外斷線:先更新 store,再 emit close(handler 可讀到一致的 status / phase)\n const patch: { status: \"closed\"; phase?: WsPhase } = { status: \"closed\" };\n if (scheduled) {\n patch.phase = \"reconnecting\";\n } else if (store.getState().phase !== \"idle\") {\n patch.phase = \"stopped\";\n }\n store.setState(patch);\n emitter.emit(\"close\", event);\n };\n }, [store, emitter, outgoingQueue, livenessSession, reconnect]);\n\n reconnect.bindOnReconnect(connect);\n\n useEffect(() => {\n if (autoConnect) connect();\n return () => teardown(\"provider unmount\");\n }, [connect, teardown]);\n\n const send = useCallback<WsContextValue[\"send\"]>(\n (data) => {\n const ws = wsRef.current;\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(data);\n return true;\n }\n return outgoingQueue.enqueue(data);\n },\n [outgoingQueue],\n );\n\n const sendJson = useCallback<WsContextValue[\"sendJson\"]>(\n (data) => {\n try {\n return send(JSON.stringify(data));\n } catch {\n return false;\n }\n },\n [send],\n );\n\n const actions = useMemo<WsContextValue>(\n () => ({ send, sendJson, connect, disconnect, getStatus }),\n [send, sendJson, connect, disconnect, getStatus],\n );\n\n return (\n <ActionsCtx.Provider value={actions}>\n <StoreCtx.Provider value={store}>\n <EventsCtx.Provider value={emitter}>{children}</EventsCtx.Provider>\n </StoreCtx.Provider>\n </ActionsCtx.Provider>\n );\n }\n\n return {\n WsProvider,\n useWsActions,\n useWsStore,\n useWsEvents,\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED