react-ws-context 0.2.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 +30 -0
- package/README.md +38 -11
- package/README.zh-TW.md +38 -11
- package/dist/index.d.mts +17 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +68 -54
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ 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
|
+
|
|
32
|
+
## [0.3.0] - 2026-08-29
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `WsPhase` type — provider connection intent and reconnect strategy: `idle` | `connecting` | `open` | `reconnecting` | `stopped`
|
|
37
|
+
- `WsState.phase` — subscribable provider lifecycle phase, orthogonal to `status` (WebSocket readyState mapping); use with `reconnectAttempt` / `reconnectExhausted` for reconnect UI
|
|
38
|
+
- Exported `WsPhase` from the package entry
|
|
39
|
+
|
|
10
40
|
## [0.2.0] - 2026-08-28
|
|
11
41
|
|
|
12
42
|
### Added
|
package/README.md
CHANGED
|
@@ -93,7 +93,8 @@ createWsContext(options)
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
- **Call `createWsContext` multiple times** for independent connections (e.g. app WS + notification WS).
|
|
96
|
-
-
|
|
96
|
+
- **Provider instances** — `useWsStoreApi` (store), `useWsEventsApi` (emitter); actions are assembled in `WsProvider` via `useMemo` and passed through Context.
|
|
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`.
|
|
99
100
|
|
|
@@ -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
|
|
|
@@ -182,6 +184,8 @@ useWsStore<T>(selector: (state: WsState) => T): T
|
|
|
182
184
|
```ts
|
|
183
185
|
interface WsState {
|
|
184
186
|
status: WsStatus;
|
|
187
|
+
/** Provider connection intent and reconnect strategy; orthogonal to `status` */
|
|
188
|
+
phase: WsPhase;
|
|
185
189
|
/** Reconnects scheduled this cycle (+1 on unintentional close, not on success) */
|
|
186
190
|
reconnectAttempt: number;
|
|
187
191
|
/** `true` when `reconnectMax` is hit and the final attempt failed; cleared by `connect()` / `disconnect()` */
|
|
@@ -191,9 +195,9 @@ interface WsState {
|
|
|
191
195
|
}
|
|
192
196
|
```
|
|
193
197
|
|
|
194
|
-
| Belongs in store
|
|
195
|
-
|
|
|
196
|
-
| `status`, reconnect progress (`reconnectAttempt` / `reconnectExhausted`), pending queue size, liveness / stall summaries | `lastMessage`, message history, app payloads |
|
|
198
|
+
| Belongs in store | Does not belong |
|
|
199
|
+
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
|
200
|
+
| `status`, `phase`, reconnect progress (`reconnectAttempt` / `reconnectExhausted`), pending queue size, liveness / stall summaries | `lastMessage`, message history, app payloads |
|
|
197
201
|
|
|
198
202
|
`CreateWsContextOptions` (e.g. `url`, `reconnectMax`) are frozen at `createWsContext` and are **not** in `WsState`. For UI like `n/max`, keep the config alongside the store fields you subscribe to.
|
|
199
203
|
|
|
@@ -206,10 +210,31 @@ interface WsState {
|
|
|
206
210
|
| `open` | Connected |
|
|
207
211
|
| `closed` | Disconnected |
|
|
208
212
|
|
|
213
|
+
Maps to the current WebSocket connection state (similar to readyState). Does **not** express provider intent such as “in an auto-reconnect cycle” or “user disconnected” — use `phase` for that.
|
|
214
|
+
|
|
215
|
+
#### `WsPhase`
|
|
216
|
+
|
|
217
|
+
| Value | Meaning |
|
|
218
|
+
| --------------- | ------------------------------------------------------------------------------------------------ |
|
|
219
|
+
| `idle` | Not connected, no reconnect scheduled (initial or manual `disconnect()`) |
|
|
220
|
+
| `connecting` | First connect or manual `connect()` in progress |
|
|
221
|
+
| `open` | Connected |
|
|
222
|
+
| `reconnecting` | Auto-reconnect cycle (waiting for timer or connecting); pair with `status`, `reconnectAttempt` |
|
|
223
|
+
| `stopped` | Will not auto-reconnect; use `reconnectExhausted` to distinguish max retries vs reconnect disabled |
|
|
224
|
+
|
|
225
|
+
`status` and `phase` often change together but mean different things. For example, `phase === "reconnecting"` with `status === "closed"` means waiting for the reconnect timer; `status === "connecting"` means the timer fired and a connect attempt is in progress.
|
|
226
|
+
|
|
209
227
|
**Tip:** use a selector to subscribe to only the fields you need.
|
|
210
228
|
|
|
211
229
|
```tsx
|
|
230
|
+
const phase = useWsStore((s) => s.phase);
|
|
212
231
|
const status = useWsStore((s) => s.status);
|
|
232
|
+
|
|
233
|
+
// Manual connect: only when idle or stopped
|
|
234
|
+
const canConnect = phase === "idle" || phase === "stopped";
|
|
235
|
+
// Intentional disconnect: while connected or in a connect/reconnect attempt
|
|
236
|
+
const canDisconnect =
|
|
237
|
+
phase === "open" || phase === "connecting" || phase === "reconnecting";
|
|
213
238
|
```
|
|
214
239
|
|
|
215
240
|
---
|
|
@@ -229,6 +254,7 @@ Must be used inside the matching `WsProvider`. Registers in `useEffect` and unsu
|
|
|
229
254
|
|
|
230
255
|
- Handler is kept in a ref — changing the callback does **not** re-subscribe
|
|
231
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
|
|
232
258
|
- For multiple events, call `useWsEvents` multiple times
|
|
233
259
|
|
|
234
260
|
---
|
|
@@ -292,7 +318,8 @@ From the main `react-ws-context` entry:
|
|
|
292
318
|
| `CreateWsContextOptions` | Options for `createWsContext` |
|
|
293
319
|
| `WsContextValue` | Return type of `useWsActions()` |
|
|
294
320
|
| `WsEvents` | Event name → handler map |
|
|
295
|
-
| `WsStatus` |
|
|
321
|
+
| `WsStatus` | WebSocket connection state (`WsState`) |
|
|
322
|
+
| `WsPhase` | Provider connection intent / reconnect phase (`WsState`) |
|
|
296
323
|
| `WsState` | Subscribable store shape (health / queue / reconnect) |
|
|
297
324
|
|
|
298
325
|
---
|
|
@@ -330,7 +357,7 @@ import {
|
|
|
330
357
|
| Topic | Notes |
|
|
331
358
|
| ---------------- | ------------------------------------------------------------------------------ |
|
|
332
359
|
| Immutable config | `url`, `reconnectMs`, etc. are fixed at create time |
|
|
333
|
-
| Reconnect | Fixed interval only; no exponential backoff
|
|
360
|
+
| Reconnect | Fixed interval only; no exponential backoff; optional cap via `reconnectMax` |
|
|
334
361
|
| SSR | No `WebSocket` on the server; `connect()` is a no-op without `window` |
|
|
335
362
|
| Error status | No `"error"` in `WsStatus`; use `useWsEvents("error")` |
|
|
336
363
|
| `WsState` scope | Health / queue / reconnect only — not messages or app data |
|
|
@@ -361,5 +388,5 @@ This package does **not** list zustand or nanoevents as npm dependencies. It inl
|
|
|
361
388
|
|
|
362
389
|
- **Author:** [Andrey Sitnik](https://github.com/ai) (`ai`)
|
|
363
390
|
- **License:** [MIT](https://github.com/ai/nanoevents/blob/main/LICENSE)
|
|
364
|
-
- **Adapted from:** [`createNanoEvents`](https://github.com/ai/nanoevents/blob/main/index.js); `
|
|
365
|
-
- **
|
|
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,7 +93,8 @@ createWsContext(options)
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
- **同一應用可多次呼叫 `createWsContext`**,每次產生一組互不共用的 Provider 與 hooks(例如同時連業務 WS 與通知 WS)。
|
|
96
|
-
-
|
|
96
|
+
- **Provider 內部 instance** — `useWsStoreApi`(store)、`useWsEventsApi`(emitter);actions 在 `WsProvider` 內以 `useMemo` 組裝後注入 Context。
|
|
97
|
+
- **`WsState` 只放連線層、低頻欄位** — 連線健康(`status`、`phase`)、outbound 佇列(如未來 `pendingCount`)、重連(`reconnectAttempt`)。**不放**訊息 payload 或業務資料。
|
|
97
98
|
- **訊息與錯誤事件** — 請用 `useWsEvents`;訊息歷史請自行寫入 state、cache 或外部 store。
|
|
98
99
|
- **連線錯誤不反映在 `WsStatus`** — 請用 `useWsEvents("error", …)` 處理;原生 `error` 事件後通常緊接 `close`。
|
|
99
100
|
|
|
@@ -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
|
|
|
@@ -182,6 +184,8 @@ useWsStore<T>(selector: (state: WsState) => T): T
|
|
|
182
184
|
```ts
|
|
183
185
|
interface WsState {
|
|
184
186
|
status: WsStatus;
|
|
187
|
+
/** Provider 連線意圖與重連策略階段;與 `status` 正交 */
|
|
188
|
+
phase: WsPhase;
|
|
185
189
|
/** 本輪已排程的自動重連次數(意外斷線當下 +1,非重連成功才 +1) */
|
|
186
190
|
reconnectAttempt: number;
|
|
187
191
|
/** 本輪自動重連已達 `reconnectMax` 且最後一次也失敗;`connect()` / `disconnect()` 歸 `false` */
|
|
@@ -191,9 +195,9 @@ interface WsState {
|
|
|
191
195
|
}
|
|
192
196
|
```
|
|
193
197
|
|
|
194
|
-
| 適合放進 store
|
|
195
|
-
|
|
|
196
|
-
| `status`、重連進度(`reconnectAttempt` / `reconnectExhausted`)、待送佇列長度、探活/stall 等連線健康摘要 | `lastMessage`、訊息歷史、業務 payload |
|
|
198
|
+
| 適合放進 store | 不適合 |
|
|
199
|
+
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
200
|
+
| `status`、`phase`、重連進度(`reconnectAttempt` / `reconnectExhausted`)、待送佇列長度、探活/stall 等連線健康摘要 | `lastMessage`、訊息歷史、業務 payload |
|
|
197
201
|
|
|
198
202
|
`CreateWsContextOptions`(如 `url`、`reconnectMax`)在 `createWsContext` 時凍結,**不在** `WsState`;UI 若需顯示 `n/max` 請自行保存設定值,或訂閱時與 store 欄位組合。
|
|
199
203
|
|
|
@@ -206,10 +210,31 @@ interface WsState {
|
|
|
206
210
|
| `open` | 已連線 |
|
|
207
211
|
| `closed` | 已斷線 |
|
|
208
212
|
|
|
213
|
+
反映 WebSocket 當下的連線狀態(類似 readyState 映射)。**不含**「是否在自動重連週期」「是否為使用者主動斷線」等 provider 意圖——請搭配 `phase`。
|
|
214
|
+
|
|
215
|
+
#### `WsPhase`
|
|
216
|
+
|
|
217
|
+
| 值 | 意義 |
|
|
218
|
+
| --------------- | -------------------------------------------------------------------- |
|
|
219
|
+
| `idle` | 未連線、未排程重連(初始或手動 `disconnect()`) |
|
|
220
|
+
| `connecting` | 首次或手動 `connect()` 連線中 |
|
|
221
|
+
| `open` | 已連線 |
|
|
222
|
+
| `reconnecting` | 自動重連週期(等待計時器或連線中);搭配 `status`、`reconnectAttempt` |
|
|
223
|
+
| `stopped` | 不會再自動重連;`reconnectExhausted` 區分達上限或未啟用重連 |
|
|
224
|
+
|
|
225
|
+
`status` 與 `phase` 常同時變化,但語意不同。例如 `phase === "reconnecting"` 且 `status === "closed"` 表示正在等待重連計時器;`status === "connecting"` 則表示計時器已觸發、正在嘗試連線。
|
|
226
|
+
|
|
209
227
|
**建議:** 以 selector 只訂閱需要的欄位;state 擴充後可避免不必要的重繪。
|
|
210
228
|
|
|
211
229
|
```tsx
|
|
230
|
+
const phase = useWsStore((s) => s.phase);
|
|
212
231
|
const status = useWsStore((s) => s.status);
|
|
232
|
+
|
|
233
|
+
// 手動連線:閒置或已停止時才可點
|
|
234
|
+
const canConnect = phase === "idle" || phase === "stopped";
|
|
235
|
+
// 主動斷線:連線中或重連週期內才可點
|
|
236
|
+
const canDisconnect =
|
|
237
|
+
phase === "open" || phase === "connecting" || phase === "reconnecting";
|
|
213
238
|
```
|
|
214
239
|
|
|
215
240
|
---
|
|
@@ -229,6 +254,7 @@ const status = useWsStore((s) => s.status);
|
|
|
229
254
|
|
|
230
255
|
- `handler` 以 ref 保存最新引用,callback 重建**不會**導致重新訂閱
|
|
231
256
|
- `type` 變更**會**重新訂閱
|
|
257
|
+
- 意外斷線時,`close` handler 觸發前 store 已更新為 `status: "closed"` 及對應 `phase`(`reconnecting` / `stopped` 等)
|
|
232
258
|
- 需監聽多種事件時,分別呼叫多次 `useWsEvents`
|
|
233
259
|
|
|
234
260
|
---
|
|
@@ -292,7 +318,8 @@ createWsContext({
|
|
|
292
318
|
| `CreateWsContextOptions` | `createWsContext` 的選項 |
|
|
293
319
|
| `WsContextValue` | `useWsActions()` 回傳型別 |
|
|
294
320
|
| `WsEvents` | 事件名稱與 handler 的型別對應 |
|
|
295
|
-
| `WsStatus` |
|
|
321
|
+
| `WsStatus` | WebSocket 連線狀態(`WsState` 的一環) |
|
|
322
|
+
| `WsPhase` | Provider 連線意圖與重連策略階段(`WsState` 的一環) |
|
|
296
323
|
| `WsState` | 可訂閱 store 的 state 形狀(連線健康/佇列/重連) |
|
|
297
324
|
|
|
298
325
|
---
|
|
@@ -343,7 +370,7 @@ sendJson(createStallMessage("stall"));
|
|
|
343
370
|
| 項目 | 說明 |
|
|
344
371
|
| -------------- | ------------------------------------------------------------------------------------------ |
|
|
345
372
|
| 設定不可變 | `url`、`reconnectMs` 等建立後固定;需換 URL 請另建 context 或手動 `disconnect` + `connect` |
|
|
346
|
-
| 重連策略 | 固定間隔,無 exponential backoff
|
|
373
|
+
| 重連策略 | 固定間隔,無 exponential backoff;`reconnectMax > 0` 可限制次數 |
|
|
347
374
|
| SSR | 不在 server 建立 `WebSocket`;`connect()` 在 `window` 不存在時為 no-op |
|
|
348
375
|
| 錯誤狀態 | 不設 `"error"` status;請監聽 `useWsEvents("error")` |
|
|
349
376
|
| `WsState` 範圍 | 只含連線健康/佇列/重連;訊息與業務資料不走 store |
|
|
@@ -376,5 +403,5 @@ sendJson(createStallMessage("stall"));
|
|
|
376
403
|
- **授權:** [MIT](https://github.com/ai/nanoevents/blob/main/LICENSE)
|
|
377
404
|
- **借鑑範圍:**
|
|
378
405
|
- Typed event emitter — 執行期邏輯幾乎對齊 [`createNanoEvents`](https://github.com/ai/nanoevents/blob/main/index.js);型別為本套件收斂版
|
|
379
|
-
- `
|
|
380
|
-
- **對應原始碼:** `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
|
@@ -11,6 +11,18 @@ import { PropsWithChildren } from "react";
|
|
|
11
11
|
* 錯誤用 `useWsEvents("error")`;不另設 error status。
|
|
12
12
|
*/
|
|
13
13
|
type WsStatus = "idle" | "connecting" | "open" | "closed";
|
|
14
|
+
/**
|
|
15
|
+
* Provider 連線意圖與重連策略階段(`WsState` 的一環)。
|
|
16
|
+
*
|
|
17
|
+
* 與 `status`(WebSocket readyState 映射)正交,補足 UI 無法單靠 `status` 判斷的情境。
|
|
18
|
+
*
|
|
19
|
+
* - `idle` — 未連線、未排程重連(初始或手動 `disconnect()`)
|
|
20
|
+
* - `connecting` — 首次或手動 `connect()` 連線中
|
|
21
|
+
* - `open` — 已連線
|
|
22
|
+
* - `reconnecting` — 自動重連週期(等待計時器或連線中);細節搭配 `status`、`reconnectAttempt`
|
|
23
|
+
* - `stopped` — 不會再自動重連;`reconnectExhausted` 區分達上限或未啟用重連
|
|
24
|
+
*/
|
|
25
|
+
type WsPhase = "idle" | "connecting" | "open" | "reconnecting" | "stopped";
|
|
14
26
|
/**
|
|
15
27
|
* 可訂閱的連線層 state(低頻更新)。
|
|
16
28
|
*
|
|
@@ -23,6 +35,8 @@ type WsStatus = "idle" | "connecting" | "open" | "closed";
|
|
|
23
35
|
type WsState = {
|
|
24
36
|
/** 連線生命週期狀態 */
|
|
25
37
|
status: WsStatus;
|
|
38
|
+
/** Provider 連線意圖與重連策略階段 */
|
|
39
|
+
phase: WsPhase;
|
|
26
40
|
/**
|
|
27
41
|
* 本輪已排程的自動重連次數(意外斷線當下 +1,非重連成功才 +1)。
|
|
28
42
|
*
|
|
@@ -62,7 +76,7 @@ interface CreateWsContextOptions {
|
|
|
62
76
|
/**
|
|
63
77
|
* 是否自動重新連線。
|
|
64
78
|
*
|
|
65
|
-
*
|
|
79
|
+
* @default true
|
|
66
80
|
*/
|
|
67
81
|
autoConnect?: boolean;
|
|
68
82
|
/**
|
|
@@ -123,7 +137,7 @@ interface WsContextValue {
|
|
|
123
137
|
* 傳送 JSON 資料
|
|
124
138
|
*
|
|
125
139
|
* @param data 資料
|
|
126
|
-
* @returns
|
|
140
|
+
* @returns 是否已送出或已入隊;`JSON.stringify` 失敗(如 circular reference)回傳 `false`
|
|
127
141
|
*/
|
|
128
142
|
sendJson: (data: unknown) => boolean;
|
|
129
143
|
/** 建立連線 */
|
|
@@ -152,5 +166,5 @@ declare function createWsContext(options: CreateWsContextOptions): {
|
|
|
152
166
|
useWsEvents: <E extends keyof WsEvents>(type: E, handler: WsEvents[E]) => void;
|
|
153
167
|
};
|
|
154
168
|
//#endregion
|
|
155
|
-
export { type CreateWsContextOptions, type WsContextValue, type WsEvents, type WsState, type WsStatus, createWsContext };
|
|
169
|
+
export { type CreateWsContextOptions, type WsContextValue, type WsEvents, type WsPhase, type WsState, type WsStatus, createWsContext };
|
|
156
170
|
//# sourceMappingURL=index.d.mts.map
|
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;;;;;;;;;;
|
|
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,18 +192,19 @@ function useStore(store, selector) {
|
|
|
183
192
|
function createWsStore(init = "idle") {
|
|
184
193
|
return createStore({
|
|
185
194
|
status: init,
|
|
195
|
+
phase: init === "open" || init === "idle" ? init : "connecting",
|
|
186
196
|
reconnectAttempt: 0,
|
|
187
197
|
reconnectExhausted: false
|
|
188
198
|
});
|
|
189
199
|
}
|
|
200
|
+
function createWsStoreContext() {
|
|
201
|
+
return createContext(null);
|
|
202
|
+
}
|
|
190
203
|
/** 每個 `WsProvider` 各有一份 {@link WsState} store */
|
|
191
204
|
function useWsStoreApi() {
|
|
192
205
|
const [store] = useState(() => createWsStore());
|
|
193
206
|
return store;
|
|
194
207
|
}
|
|
195
|
-
function createWsStoreContext() {
|
|
196
|
-
return createContext(null);
|
|
197
|
-
}
|
|
198
208
|
/** 訂閱 {@link WsState};建議以 selector 只取需要的連線層欄位。 */
|
|
199
209
|
function createUseWsStore(StoreCtx) {
|
|
200
210
|
function useWsStore(selector) {
|
|
@@ -206,6 +216,7 @@ function createUseWsStore(StoreCtx) {
|
|
|
206
216
|
}
|
|
207
217
|
//#endregion
|
|
208
218
|
//#region src/ws-context/ws-actions.ts
|
|
219
|
+
/** actions 無獨立 instance;由 WsProvider 以 useMemo 組裝後注入 Context */
|
|
209
220
|
function createWsActionsContext() {
|
|
210
221
|
return createContext(null);
|
|
211
222
|
}
|
|
@@ -238,8 +249,10 @@ function createReconnect(reconnectMs, reconnectMax, callbacks) {
|
|
|
238
249
|
onConnectBegin() {
|
|
239
250
|
clearTimer();
|
|
240
251
|
intentionalClose = false;
|
|
252
|
+
const reconnecting = fromTimer;
|
|
241
253
|
if (!fromTimer) resetCycle();
|
|
242
254
|
fromTimer = false;
|
|
255
|
+
return reconnecting;
|
|
243
256
|
},
|
|
244
257
|
onOpen() {
|
|
245
258
|
resetCycle();
|
|
@@ -308,16 +321,16 @@ function defaultParse(data) {
|
|
|
308
321
|
*/
|
|
309
322
|
function createWsContext(options) {
|
|
310
323
|
const { url, protocols, autoConnect = true, reconnectMs = 0, reconnectMax = 0, outgoingQueueMax = 0, parse = defaultParse, liveness } = options;
|
|
311
|
-
const ActionsCtx = createWsActionsContext();
|
|
312
|
-
const useWsActions = createUseWsActions(ActionsCtx);
|
|
313
324
|
const StoreCtx = createWsStoreContext();
|
|
314
325
|
const useWsStore = createUseWsStore(StoreCtx);
|
|
315
|
-
const
|
|
316
|
-
const
|
|
326
|
+
const ActionsCtx = createWsActionsContext();
|
|
327
|
+
const useWsActions = createUseWsActions(ActionsCtx);
|
|
328
|
+
const EventsCtx = createWsEventsContext();
|
|
329
|
+
const useWsEvents = createUseWsEvents(EventsCtx);
|
|
317
330
|
function WsProvider({ children }) {
|
|
318
331
|
const wsRef = useRef(null);
|
|
319
332
|
const store = useWsStoreApi();
|
|
320
|
-
const emitter =
|
|
333
|
+
const emitter = useWsEventsApi();
|
|
321
334
|
const reconnect = useReconnect(reconnectMs, reconnectMax, {
|
|
322
335
|
getAttempt: () => store.getState().reconnectAttempt,
|
|
323
336
|
setAttempt: (reconnectAttempt) => store.setState({ reconnectAttempt }),
|
|
@@ -325,31 +338,33 @@ function createWsContext(options) {
|
|
|
325
338
|
});
|
|
326
339
|
const outgoingQueue = useOutgoingQueue(outgoingQueueMax);
|
|
327
340
|
const livenessSession = useLiveness(liveness, () => wsRef.current);
|
|
328
|
-
const setStatus = useCallback((status) => {
|
|
329
|
-
store.setState({ status });
|
|
330
|
-
}, [store]);
|
|
331
341
|
const getStatus = useCallback(() => store.getState().status, [store]);
|
|
332
|
-
|
|
342
|
+
/** 主動斷線與 Provider unmount 共用;`reason` 區分 `"client disconnect"` / `"provider unmount"` */
|
|
343
|
+
const teardown = useCallback((reason) => {
|
|
333
344
|
reconnect.cancel();
|
|
334
345
|
livenessSession.stop();
|
|
335
346
|
outgoingQueue.clear();
|
|
347
|
+
store.setState({
|
|
348
|
+
phase: "idle",
|
|
349
|
+
status: "closed"
|
|
350
|
+
});
|
|
336
351
|
const ws = wsRef.current;
|
|
337
352
|
wsRef.current = null;
|
|
338
353
|
if (ws) {
|
|
339
354
|
detachAndClose(ws);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
} else setStatus("closed");
|
|
355
|
+
emitter.emit("close", clientCloseEvent(reason));
|
|
356
|
+
}
|
|
343
357
|
}, [
|
|
344
|
-
|
|
358
|
+
store,
|
|
345
359
|
emitter,
|
|
346
360
|
outgoingQueue,
|
|
347
361
|
livenessSession,
|
|
348
362
|
reconnect
|
|
349
363
|
]);
|
|
364
|
+
const disconnect = useCallback(() => teardown("client disconnect"), [teardown]);
|
|
350
365
|
const connect = useCallback(() => {
|
|
351
366
|
if (typeof window === "undefined") return;
|
|
352
|
-
reconnect.onConnectBegin();
|
|
367
|
+
const fromReconnect = reconnect.onConnectBegin();
|
|
353
368
|
livenessSession.stop();
|
|
354
369
|
const prev = wsRef.current;
|
|
355
370
|
if (prev) {
|
|
@@ -357,15 +372,21 @@ function createWsContext(options) {
|
|
|
357
372
|
detachAndClose(prev);
|
|
358
373
|
emitter.emit("close", clientCloseEvent("reconnect"));
|
|
359
374
|
}
|
|
360
|
-
|
|
375
|
+
store.setState({
|
|
376
|
+
status: "connecting",
|
|
377
|
+
phase: fromReconnect ? "reconnecting" : "connecting"
|
|
378
|
+
});
|
|
361
379
|
const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);
|
|
362
380
|
wsRef.current = ws;
|
|
363
381
|
ws.onopen = (event) => {
|
|
364
382
|
if (wsRef.current !== ws) return;
|
|
365
383
|
reconnect.onOpen();
|
|
366
|
-
|
|
384
|
+
store.setState({
|
|
385
|
+
status: "open",
|
|
386
|
+
phase: "open"
|
|
387
|
+
});
|
|
367
388
|
outgoingQueue.flush((data) => ws.send(data));
|
|
368
|
-
livenessSession.start(
|
|
389
|
+
livenessSession.start();
|
|
369
390
|
emitter.emit("open", event);
|
|
370
391
|
};
|
|
371
392
|
ws.onmessage = (event) => {
|
|
@@ -381,12 +402,15 @@ function createWsContext(options) {
|
|
|
381
402
|
ws.onclose = (event) => {
|
|
382
403
|
if (wsRef.current === ws) wsRef.current = null;
|
|
383
404
|
livenessSession.stop();
|
|
384
|
-
|
|
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);
|
|
385
410
|
emitter.emit("close", event);
|
|
386
|
-
reconnect.scheduleAfterClose();
|
|
387
411
|
};
|
|
388
412
|
}, [
|
|
389
|
-
|
|
413
|
+
store,
|
|
390
414
|
emitter,
|
|
391
415
|
outgoingQueue,
|
|
392
416
|
livenessSession,
|
|
@@ -395,24 +419,8 @@ function createWsContext(options) {
|
|
|
395
419
|
reconnect.bindOnReconnect(connect);
|
|
396
420
|
useEffect(() => {
|
|
397
421
|
if (autoConnect) connect();
|
|
398
|
-
return () =>
|
|
399
|
-
|
|
400
|
-
livenessSession.stop();
|
|
401
|
-
outgoingQueue.clear();
|
|
402
|
-
const ws = wsRef.current;
|
|
403
|
-
wsRef.current = null;
|
|
404
|
-
if (ws) {
|
|
405
|
-
detachAndClose(ws);
|
|
406
|
-
emitter.emit("close", clientCloseEvent("provider unmount"));
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
}, [
|
|
410
|
-
connect,
|
|
411
|
-
emitter,
|
|
412
|
-
outgoingQueue,
|
|
413
|
-
livenessSession,
|
|
414
|
-
reconnect
|
|
415
|
-
]);
|
|
422
|
+
return () => teardown("provider unmount");
|
|
423
|
+
}, [connect, teardown]);
|
|
416
424
|
const send = useCallback((data) => {
|
|
417
425
|
const ws = wsRef.current;
|
|
418
426
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
@@ -421,7 +429,13 @@ function createWsContext(options) {
|
|
|
421
429
|
}
|
|
422
430
|
return outgoingQueue.enqueue(data);
|
|
423
431
|
}, [outgoingQueue]);
|
|
424
|
-
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]);
|
|
425
439
|
const actions = useMemo(() => ({
|
|
426
440
|
send,
|
|
427
441
|
sendJson,
|
|
@@ -439,7 +453,7 @@ function createWsContext(options) {
|
|
|
439
453
|
value: actions,
|
|
440
454
|
children: /* @__PURE__ */ jsx(StoreCtx.Provider, {
|
|
441
455
|
value: store,
|
|
442
|
-
children: /* @__PURE__ */ jsx(
|
|
456
|
+
children: /* @__PURE__ */ jsx(EventsCtx.Provider, {
|
|
443
457
|
value: emitter,
|
|
444
458
|
children
|
|
445
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 * 可訂閱的連線層 state(低頻更新)。\n *\n * 只放:**連線健康**、**outbound 佇列**、**重連** 等連線生命週期資訊。\n *\n * 不放:訊息 payload、訊息歷史、業務資料(請用 `useWsEvents` 或自行管理 state)。\n *\n * 未來可能擴充例如 `pendingCount`;新增欄位時請維持低頻、可 selector 訂閱。\n */\nexport type WsState = {\n /** 連線生命週期狀態 */\n status: WsStatus;\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 return createStore<WsState>({\n status: init,\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 /** 開始連線時呼叫 */\n onConnectBegin: () => void;\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 if (!fromTimer) resetCycle();\n fromTimer = false;\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 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 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 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, emitter, outgoingQueue, livenessSession, reconnect]);\n\n const connect = useCallback<WsContextValue[\"connect\"]>(() => {\n if (typeof window === \"undefined\") return;\n\n 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\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 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 reconnect.scheduleAfterClose();\n };\n }, [setStatus, emitter, outgoingQueue, livenessSession, reconnect]);\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