@gwakko/shared-websocket 0.1.1 → 0.2.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/README.md +37 -9
- package/dist/adapters/react.d.ts +68 -8
- package/dist/react.cjs +30 -10
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +28 -8
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/react.ts +94 -12
package/README.md
CHANGED
|
@@ -308,17 +308,45 @@ Callback receives `{ ws, signal }` — destructure what you need. Signal aborts
|
|
|
308
308
|
| `connected` | `boolean` | Connection status |
|
|
309
309
|
| `tabRole` | `'leader' \| 'follower'` | Current tab's role |
|
|
310
310
|
|
|
311
|
-
### React Hooks (React 19,
|
|
311
|
+
### React Hooks (React 19, `useEffectEvent` for stable refs)
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
|------|---------|-------------|
|
|
315
|
-
| `useSharedWebSocket()` | `SharedWebSocket` | Access instance from context |
|
|
316
|
-
| `useSocketEvent<T>(event)` | `T \| undefined` | Latest event value |
|
|
317
|
-
| `useSocketStream<T>(event)` | `T[]` | Accumulated events |
|
|
318
|
-
| `useSocketSync<T>(key, init)` | `[T, setter]` | Cross-tab synced state |
|
|
319
|
-
| `useSocketStatus()` | `{ connected, tabRole }` | Connection status |
|
|
313
|
+
All hooks use context internally — no need to pass `ws`. Every hook accepts an **optional callback** for custom handling.
|
|
320
314
|
|
|
321
|
-
|
|
315
|
+
| Hook | Without callback | With callback |
|
|
316
|
+
|------|-----------------|---------------|
|
|
317
|
+
| `useSharedWebSocket()` | `SharedWebSocket` | — |
|
|
318
|
+
| `useSocketEvent<T>(event, cb?)` | Returns `T \| undefined` | `cb(data)` on each event |
|
|
319
|
+
| `useSocketStream<T>(event, cb?)` | Returns `T[]` (accumulated) | `cb(data)` — manage your own state |
|
|
320
|
+
| `useSocketSync<T>(key, init, cb?)` | Returns `[T, setter]` | `cb(value)` — side effects on sync |
|
|
321
|
+
| `useSocketCallback<T>(event, cb)` | — | Fire-and-forget (no state) |
|
|
322
|
+
| `useSocketStatus()` | `{ connected, tabRole }` | — |
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
// Without callback — reactive state
|
|
326
|
+
const order = useSocketEvent<Order>('order.created');
|
|
327
|
+
|
|
328
|
+
// With callback — custom logic, stable ref
|
|
329
|
+
useSocketEvent<Order>('order.created', (order) => {
|
|
330
|
+
playSound('new-order');
|
|
331
|
+
analytics.track('order_received', order);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Stream with limit
|
|
335
|
+
const [msgs, setMsgs] = useState<Message[]>([]);
|
|
336
|
+
useSocketStream<Message>('chat.message', (msg) => {
|
|
337
|
+
setMsgs(prev => [msg, ...prev].slice(0, 50));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Sync with side effect
|
|
341
|
+
const [cart, setCart] = useSocketSync('cart', { items: [] }, (cart) => {
|
|
342
|
+
document.title = `Cart (${cart.items.length})`;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Fire-and-forget
|
|
346
|
+
useSocketCallback<Notification>('notification', (n) => {
|
|
347
|
+
if (ws.tabRole === 'leader') new Notification(n.title);
|
|
348
|
+
});
|
|
349
|
+
```
|
|
322
350
|
|
|
323
351
|
### Vue Composables
|
|
324
352
|
|
package/dist/adapters/react.d.ts
CHANGED
|
@@ -42,30 +42,90 @@ export declare function SharedWebSocketProvider({ url, options, children }: Shar
|
|
|
42
42
|
*/
|
|
43
43
|
export declare function useSharedWebSocket(): SharedWebSocket;
|
|
44
44
|
/**
|
|
45
|
-
* Subscribe to a WebSocket event.
|
|
46
|
-
*
|
|
45
|
+
* Subscribe to a WebSocket event.
|
|
46
|
+
* - Without callback: returns the latest received value (reactive state).
|
|
47
|
+
* - With callback: calls your handler on each event (stable ref via useEffectEvent).
|
|
47
48
|
*
|
|
48
49
|
* @example
|
|
50
|
+
* // Reactive state — returns latest value
|
|
49
51
|
* const order = useSocketEvent<Order>('order.created');
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // Custom callback — full control, no state
|
|
55
|
+
* useSocketEvent<Order>('order.created', (order) => {
|
|
56
|
+
* playSound('new-order');
|
|
57
|
+
* analytics.track('order_received', order);
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Custom callback with transform — store in your own state
|
|
62
|
+
* const [orders, setOrders] = useState<Order[]>([]);
|
|
63
|
+
* useSocketEvent<Order>('order.created', (order) => {
|
|
64
|
+
* setOrders(prev => [order, ...prev].slice(0, 50)); // keep last 50
|
|
65
|
+
* });
|
|
50
66
|
*/
|
|
51
|
-
export declare function useSocketEvent<T>(event: string): T | undefined;
|
|
67
|
+
export declare function useSocketEvent<T>(event: string, callback?: (data: T) => void): T | undefined;
|
|
52
68
|
/**
|
|
53
69
|
* Accumulate WebSocket events into an array.
|
|
54
|
-
*
|
|
70
|
+
* - Without callback: returns accumulated array (reactive state).
|
|
71
|
+
* - With callback: calls your handler on each event, you manage your own state.
|
|
55
72
|
*
|
|
56
73
|
* @example
|
|
74
|
+
* // Default — accumulates all events
|
|
57
75
|
* const messages = useSocketStream<ChatMessage>('chat.message');
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Custom callback — keep only last 50, transform, filter, etc.
|
|
79
|
+
* const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
80
|
+
* useSocketStream<ChatMessage>('chat.message', (msg) => {
|
|
81
|
+
* setMessages(prev => [msg, ...prev].slice(0, 50));
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // Custom callback — filter by type
|
|
86
|
+
* const [errors, setErrors] = useState<LogEntry[]>([]);
|
|
87
|
+
* useSocketStream<LogEntry>('log.entry', (entry) => {
|
|
88
|
+
* if (entry.level === 'error') setErrors(prev => [...prev, entry]);
|
|
89
|
+
* });
|
|
58
90
|
*/
|
|
59
|
-
export declare function useSocketStream<T>(event: string): T[];
|
|
91
|
+
export declare function useSocketStream<T>(event: string, callback?: (data: T) => void): T[];
|
|
60
92
|
/**
|
|
61
93
|
* Two-way state sync across browser tabs.
|
|
62
|
-
*
|
|
94
|
+
* - Without callback: returns [value, setter] (like useState but synced).
|
|
95
|
+
* - With callback: calls your handler when any tab updates this key.
|
|
63
96
|
*
|
|
64
97
|
* @example
|
|
98
|
+
* // Default — reactive synced state
|
|
65
99
|
* const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] });
|
|
66
|
-
*
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* // Custom callback — side effects on sync
|
|
103
|
+
* const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] }, (cart) => {
|
|
104
|
+
* document.title = `Cart (${cart.items.length})`;
|
|
105
|
+
* analytics.track('cart_updated', { count: cart.items.length });
|
|
106
|
+
* });
|
|
107
|
+
*/
|
|
108
|
+
export declare function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): [T, (value: T) => void];
|
|
109
|
+
/**
|
|
110
|
+
* Subscribe to a WebSocket event with just a callback — no state, no return value.
|
|
111
|
+
* Fire-and-forget: side effects, logging, analytics, sounds, browser notifications.
|
|
112
|
+
* Stable ref via useEffectEvent — callback always sees latest closure values.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* useSocketCallback<Order>('order.created', (order) => {
|
|
116
|
+
* playSound('new-order');
|
|
117
|
+
* analytics.track('order_received', { id: order.id });
|
|
118
|
+
* });
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Browser notification only from leader tab
|
|
122
|
+
* useSocketCallback<Notification>('notification', (notif) => {
|
|
123
|
+
* if (ws.tabRole === 'leader' && document.hidden) {
|
|
124
|
+
* new Notification(notif.title, { body: notif.body });
|
|
125
|
+
* }
|
|
126
|
+
* });
|
|
67
127
|
*/
|
|
68
|
-
export declare function
|
|
128
|
+
export declare function useSocketCallback<T>(event: string, callback: (data: T) => void): void;
|
|
69
129
|
/**
|
|
70
130
|
* Reactive connection status.
|
|
71
131
|
* Uses useEffectEvent to avoid re-creating interval on state change.
|
package/dist/react.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
3
|
var _chunkSMH3X34Ncjs = require('./chunk-SMH3X34N.cjs');
|
|
4
4
|
|
|
@@ -32,38 +32,47 @@ function useSharedWebSocket() {
|
|
|
32
32
|
}
|
|
33
33
|
return ctx;
|
|
34
34
|
}
|
|
35
|
-
function useSocketEvent(event) {
|
|
35
|
+
function useSocketEvent(event, callback) {
|
|
36
36
|
const socket = useSharedWebSocket();
|
|
37
37
|
const [value, setValue] = _react.useState.call(void 0, void 0);
|
|
38
38
|
const onEvent = _react.useEffectEvent.call(void 0, (data) => {
|
|
39
|
-
|
|
39
|
+
if (callback) {
|
|
40
|
+
callback(data);
|
|
41
|
+
} else {
|
|
42
|
+
setValue(data);
|
|
43
|
+
}
|
|
40
44
|
});
|
|
41
45
|
_react.useEffect.call(void 0, () => {
|
|
42
46
|
const unsub = socket.on(event, onEvent);
|
|
43
47
|
return unsub;
|
|
44
48
|
}, [socket, event]);
|
|
45
|
-
return value;
|
|
49
|
+
return callback ? void 0 : value;
|
|
46
50
|
}
|
|
47
|
-
function useSocketStream(event) {
|
|
51
|
+
function useSocketStream(event, callback) {
|
|
48
52
|
const socket = useSharedWebSocket();
|
|
49
53
|
const [items, setItems] = _react.useState.call(void 0, []);
|
|
50
54
|
const onEvent = _react.useEffectEvent.call(void 0, (data) => {
|
|
51
|
-
|
|
55
|
+
if (callback) {
|
|
56
|
+
callback(data);
|
|
57
|
+
} else {
|
|
58
|
+
setItems((prev) => [...prev, data]);
|
|
59
|
+
}
|
|
52
60
|
});
|
|
53
61
|
_react.useEffect.call(void 0, () => {
|
|
54
|
-
setItems([]);
|
|
62
|
+
if (!callback) setItems([]);
|
|
55
63
|
const unsub = socket.on(event, onEvent);
|
|
56
64
|
return unsub;
|
|
57
65
|
}, [socket, event]);
|
|
58
|
-
return items;
|
|
66
|
+
return callback ? [] : items;
|
|
59
67
|
}
|
|
60
|
-
function useSocketSync(key, initialValue) {
|
|
68
|
+
function useSocketSync(key, initialValue, callback) {
|
|
61
69
|
const socket = useSharedWebSocket();
|
|
62
70
|
const [value, setValue] = _react.useState.call(void 0, () => {
|
|
63
71
|
return _nullishCoalesce(socket.getSync(key), () => ( initialValue));
|
|
64
72
|
});
|
|
65
73
|
const onSync = _react.useEffectEvent.call(void 0, (synced) => {
|
|
66
74
|
setValue(synced);
|
|
75
|
+
_optionalChain([callback, 'optionalCall', _ => _(synced)]);
|
|
67
76
|
});
|
|
68
77
|
_react.useEffect.call(void 0, () => {
|
|
69
78
|
const unsub = socket.onSync(key, onSync);
|
|
@@ -75,6 +84,16 @@ function useSocketSync(key, initialValue) {
|
|
|
75
84
|
});
|
|
76
85
|
return [value, setAndSync];
|
|
77
86
|
}
|
|
87
|
+
function useSocketCallback(event, callback) {
|
|
88
|
+
const socket = useSharedWebSocket();
|
|
89
|
+
const handler = _react.useEffectEvent.call(void 0, (data) => {
|
|
90
|
+
callback(data);
|
|
91
|
+
});
|
|
92
|
+
_react.useEffect.call(void 0, () => {
|
|
93
|
+
const unsub = socket.on(event, handler);
|
|
94
|
+
return unsub;
|
|
95
|
+
}, [socket, event]);
|
|
96
|
+
}
|
|
78
97
|
function useSocketStatus() {
|
|
79
98
|
const socket = useSharedWebSocket();
|
|
80
99
|
const [connected, setConnected] = _react.useState.call(void 0, socket.connected);
|
|
@@ -96,5 +115,6 @@ function useSocketStatus() {
|
|
|
96
115
|
|
|
97
116
|
|
|
98
117
|
|
|
99
|
-
|
|
118
|
+
|
|
119
|
+
exports.SharedWebSocketProvider = SharedWebSocketProvider; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketCallback = useSocketCallback; exports.useSocketEvent = useSocketEvent; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync;
|
|
100
120
|
//# sourceMappingURL=react.cjs.map
|
package/dist/react.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/react.cjs","../src/adapters/react.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/react.cjs","sourcesContent":[null,"import {\n createContext,\n useContext,\n useEffect,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Context ─────────────────────────────────────────────\n\nconst SharedWSContext = createContext<SharedWebSocket | null>(null);\n\n/**\n * Provider props — pass URL and options as props for flexibility.\n *\n * @example\n * <SharedWebSocketProvider url=\"wss://api.example.com/ws\" options={{ auth: getToken }}>\n * <App />\n * </SharedWebSocketProvider>\n */\nexport interface SharedWebSocketProviderProps {\n url: string;\n options?: SharedWebSocketOptions;\n children: ReactNode;\n}\n\n/**\n * Provider component — creates SharedWebSocket from props, auto-disposes on unmount.\n *\n * @example\n * function App() {\n * return (\n * <SharedWebSocketProvider\n * url=\"wss://api.example.com/ws\"\n * options={{\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }}\n * >\n * <Dashboard />\n * </SharedWebSocketProvider>\n * );\n * }\n */\nexport function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps) {\n const [socket] = useState(() => {\n const ws = new SharedWebSocket(url, options);\n ws.connect();\n return ws;\n });\n\n useEffect(() => {\n return () => {\n socket[Symbol.dispose]();\n };\n }, [socket]);\n\n return createElement(SharedWSContext.Provider, { value: socket }, children);\n}\n\n/**\n * Access the SharedWebSocket instance from context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const ctx = useContext(SharedWSContext);\n if (!ctx) {\n throw new Error('useSharedWebSocket must be used within <SharedWebSocketProvider>');\n }\n return ctx;\n}\n\n// ─── Hooks ───────────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event. Returns the latest received value.\n * Uses useEffectEvent for a stable callback ref — no stale closures.\n *\n * @example\n * const order = useSocketEvent<Order>('order.created');\n */\nexport function useSocketEvent<T>(event: string): T | undefined {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T | undefined>(undefined);\n\n const onEvent = useEffectEvent((data: T) => {\n setValue(data);\n });\n\n useEffect(() => {\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return value;\n}\n\n/**\n * Accumulate WebSocket events into an array.\n * Uses useEffectEvent — handler always sees latest state without re-subscribing.\n *\n * @example\n * const messages = useSocketStream<ChatMessage>('chat.message');\n */\nexport function useSocketStream<T>(event: string): T[] {\n const socket = useSharedWebSocket();\n const [items, setItems] = useState<T[]>([]);\n\n const onEvent = useEffectEvent((data: T) => {\n setItems((prev) => [...prev, data]);\n });\n\n useEffect(() => {\n setItems([]);\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return items;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * Uses useEffectEvent for stable sync callback.\n *\n * @example\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] });\n * // setCart in one tab → updates all tabs instantly\n */\nexport function useSocketSync<T>(\n key: string,\n initialValue: T,\n): [T, (value: T) => void] {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T>(() => {\n return socket.getSync<T>(key) ?? initialValue;\n });\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n });\n\n useEffect(() => {\n const unsub = socket.onSync<T>(key, onSync);\n return unsub;\n }, [socket, key]);\n\n const setAndSync = useEffectEvent((newValue: T) => {\n setValue(newValue);\n socket.sync(key, newValue);\n });\n\n return [value, setAndSync];\n}\n\n/**\n * Reactive connection status.\n * Uses useEffectEvent to avoid re-creating interval on state change.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: boolean;\n tabRole: TabRole;\n} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/react.cjs","../src/adapters/react.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/react.cjs","sourcesContent":[null,"import {\n createContext,\n useContext,\n useEffect,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Context ─────────────────────────────────────────────\n\nconst SharedWSContext = createContext<SharedWebSocket | null>(null);\n\n/**\n * Provider props — pass URL and options as props for flexibility.\n *\n * @example\n * <SharedWebSocketProvider url=\"wss://api.example.com/ws\" options={{ auth: getToken }}>\n * <App />\n * </SharedWebSocketProvider>\n */\nexport interface SharedWebSocketProviderProps {\n url: string;\n options?: SharedWebSocketOptions;\n children: ReactNode;\n}\n\n/**\n * Provider component — creates SharedWebSocket from props, auto-disposes on unmount.\n *\n * @example\n * function App() {\n * return (\n * <SharedWebSocketProvider\n * url=\"wss://api.example.com/ws\"\n * options={{\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }}\n * >\n * <Dashboard />\n * </SharedWebSocketProvider>\n * );\n * }\n */\nexport function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps) {\n const [socket] = useState(() => {\n const ws = new SharedWebSocket(url, options);\n ws.connect();\n return ws;\n });\n\n useEffect(() => {\n return () => {\n socket[Symbol.dispose]();\n };\n }, [socket]);\n\n return createElement(SharedWSContext.Provider, { value: socket }, children);\n}\n\n/**\n * Access the SharedWebSocket instance from context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const ctx = useContext(SharedWSContext);\n if (!ctx) {\n throw new Error('useSharedWebSocket must be used within <SharedWebSocketProvider>');\n }\n return ctx;\n}\n\n// ─── Hooks ───────────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns the latest received value (reactive state).\n * - With callback: calls your handler on each event (stable ref via useEffectEvent).\n *\n * @example\n * // Reactive state — returns latest value\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback — full control, no state\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n *\n * @example\n * // Custom callback with transform — store in your own state\n * const [orders, setOrders] = useState<Order[]>([]);\n * useSocketEvent<Order>('order.created', (order) => {\n * setOrders(prev => [order, ...prev].slice(0, 50)); // keep last 50\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): T | undefined {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T | undefined>(undefined);\n\n const onEvent = useEffectEvent((data: T) => {\n if (callback) {\n callback(data);\n } else {\n setValue(data);\n }\n });\n\n useEffect(() => {\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return callback ? undefined : value;\n}\n\n/**\n * Accumulate WebSocket events into an array.\n * - Without callback: returns accumulated array (reactive state).\n * - With callback: calls your handler on each event, you manage your own state.\n *\n * @example\n * // Default — accumulates all events\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom callback — keep only last 50, transform, filter, etc.\n * const [messages, setMessages] = useState<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * setMessages(prev => [msg, ...prev].slice(0, 50));\n * });\n *\n * @example\n * // Custom callback — filter by type\n * const [errors, setErrors] = useState<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') setErrors(prev => [...prev, entry]);\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): T[] {\n const socket = useSharedWebSocket();\n const [items, setItems] = useState<T[]>([]);\n\n const onEvent = useEffectEvent((data: T) => {\n if (callback) {\n callback(data);\n } else {\n setItems((prev) => [...prev, data]);\n }\n });\n\n useEffect(() => {\n if (!callback) setItems([]);\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return callback ? [] : items;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: returns [value, setter] (like useState but synced).\n * - With callback: calls your handler when any tab updates this key.\n *\n * @example\n * // Default — reactive synced state\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] });\n *\n * @example\n * // Custom callback — side effects on sync\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated', { count: cart.items.length });\n * });\n */\nexport function useSocketSync<T>(\n key: string,\n initialValue: T,\n callback?: (value: T) => void,\n): [T, (value: T) => void] {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T>(() => {\n return socket.getSync<T>(key) ?? initialValue;\n });\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n callback?.(synced);\n });\n\n useEffect(() => {\n const unsub = socket.onSync<T>(key, onSync);\n return unsub;\n }, [socket, key]);\n\n const setAndSync = useEffectEvent((newValue: T) => {\n setValue(newValue);\n socket.sync(key, newValue);\n });\n\n return [value, setAndSync];\n}\n\n/**\n * Subscribe to a WebSocket event with just a callback — no state, no return value.\n * Fire-and-forget: side effects, logging, analytics, sounds, browser notifications.\n * Stable ref via useEffectEvent — callback always sees latest closure values.\n *\n * @example\n * useSocketCallback<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', { id: order.id });\n * });\n *\n * @example\n * // Browser notification only from leader tab\n * useSocketCallback<Notification>('notification', (notif) => {\n * if (ws.tabRole === 'leader' && document.hidden) {\n * new Notification(notif.title, { body: notif.body });\n * }\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const handler = useEffectEvent((data: T) => {\n callback(data);\n });\n\n useEffect(() => {\n const unsub = socket.on(event, handler);\n return unsub;\n }, [socket, event]);\n}\n\n/**\n * Reactive connection status.\n * Uses useEffectEvent to avoid re-creating interval on state change.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: boolean;\n tabRole: TabRole;\n} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole };\n}\n"]}
|
package/dist/react.js
CHANGED
|
@@ -32,38 +32,47 @@ function useSharedWebSocket() {
|
|
|
32
32
|
}
|
|
33
33
|
return ctx;
|
|
34
34
|
}
|
|
35
|
-
function useSocketEvent(event) {
|
|
35
|
+
function useSocketEvent(event, callback) {
|
|
36
36
|
const socket = useSharedWebSocket();
|
|
37
37
|
const [value, setValue] = useState(void 0);
|
|
38
38
|
const onEvent = useEffectEvent((data) => {
|
|
39
|
-
|
|
39
|
+
if (callback) {
|
|
40
|
+
callback(data);
|
|
41
|
+
} else {
|
|
42
|
+
setValue(data);
|
|
43
|
+
}
|
|
40
44
|
});
|
|
41
45
|
useEffect(() => {
|
|
42
46
|
const unsub = socket.on(event, onEvent);
|
|
43
47
|
return unsub;
|
|
44
48
|
}, [socket, event]);
|
|
45
|
-
return value;
|
|
49
|
+
return callback ? void 0 : value;
|
|
46
50
|
}
|
|
47
|
-
function useSocketStream(event) {
|
|
51
|
+
function useSocketStream(event, callback) {
|
|
48
52
|
const socket = useSharedWebSocket();
|
|
49
53
|
const [items, setItems] = useState([]);
|
|
50
54
|
const onEvent = useEffectEvent((data) => {
|
|
51
|
-
|
|
55
|
+
if (callback) {
|
|
56
|
+
callback(data);
|
|
57
|
+
} else {
|
|
58
|
+
setItems((prev) => [...prev, data]);
|
|
59
|
+
}
|
|
52
60
|
});
|
|
53
61
|
useEffect(() => {
|
|
54
|
-
setItems([]);
|
|
62
|
+
if (!callback) setItems([]);
|
|
55
63
|
const unsub = socket.on(event, onEvent);
|
|
56
64
|
return unsub;
|
|
57
65
|
}, [socket, event]);
|
|
58
|
-
return items;
|
|
66
|
+
return callback ? [] : items;
|
|
59
67
|
}
|
|
60
|
-
function useSocketSync(key, initialValue) {
|
|
68
|
+
function useSocketSync(key, initialValue, callback) {
|
|
61
69
|
const socket = useSharedWebSocket();
|
|
62
70
|
const [value, setValue] = useState(() => {
|
|
63
71
|
return socket.getSync(key) ?? initialValue;
|
|
64
72
|
});
|
|
65
73
|
const onSync = useEffectEvent((synced) => {
|
|
66
74
|
setValue(synced);
|
|
75
|
+
callback?.(synced);
|
|
67
76
|
});
|
|
68
77
|
useEffect(() => {
|
|
69
78
|
const unsub = socket.onSync(key, onSync);
|
|
@@ -75,6 +84,16 @@ function useSocketSync(key, initialValue) {
|
|
|
75
84
|
});
|
|
76
85
|
return [value, setAndSync];
|
|
77
86
|
}
|
|
87
|
+
function useSocketCallback(event, callback) {
|
|
88
|
+
const socket = useSharedWebSocket();
|
|
89
|
+
const handler = useEffectEvent((data) => {
|
|
90
|
+
callback(data);
|
|
91
|
+
});
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const unsub = socket.on(event, handler);
|
|
94
|
+
return unsub;
|
|
95
|
+
}, [socket, event]);
|
|
96
|
+
}
|
|
78
97
|
function useSocketStatus() {
|
|
79
98
|
const socket = useSharedWebSocket();
|
|
80
99
|
const [connected, setConnected] = useState(socket.connected);
|
|
@@ -92,6 +111,7 @@ function useSocketStatus() {
|
|
|
92
111
|
export {
|
|
93
112
|
SharedWebSocketProvider,
|
|
94
113
|
useSharedWebSocket,
|
|
114
|
+
useSocketCallback,
|
|
95
115
|
useSocketEvent,
|
|
96
116
|
useSocketStatus,
|
|
97
117
|
useSocketStream,
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/react.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Context ─────────────────────────────────────────────\n\nconst SharedWSContext = createContext<SharedWebSocket | null>(null);\n\n/**\n * Provider props — pass URL and options as props for flexibility.\n *\n * @example\n * <SharedWebSocketProvider url=\"wss://api.example.com/ws\" options={{ auth: getToken }}>\n * <App />\n * </SharedWebSocketProvider>\n */\nexport interface SharedWebSocketProviderProps {\n url: string;\n options?: SharedWebSocketOptions;\n children: ReactNode;\n}\n\n/**\n * Provider component — creates SharedWebSocket from props, auto-disposes on unmount.\n *\n * @example\n * function App() {\n * return (\n * <SharedWebSocketProvider\n * url=\"wss://api.example.com/ws\"\n * options={{\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }}\n * >\n * <Dashboard />\n * </SharedWebSocketProvider>\n * );\n * }\n */\nexport function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps) {\n const [socket] = useState(() => {\n const ws = new SharedWebSocket(url, options);\n ws.connect();\n return ws;\n });\n\n useEffect(() => {\n return () => {\n socket[Symbol.dispose]();\n };\n }, [socket]);\n\n return createElement(SharedWSContext.Provider, { value: socket }, children);\n}\n\n/**\n * Access the SharedWebSocket instance from context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const ctx = useContext(SharedWSContext);\n if (!ctx) {\n throw new Error('useSharedWebSocket must be used within <SharedWebSocketProvider>');\n }\n return ctx;\n}\n\n// ─── Hooks ───────────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event. Returns the latest received value.\n * Uses useEffectEvent for a stable callback ref — no stale closures.\n *\n * @example\n * const order = useSocketEvent<Order>('order.created');\n */\nexport function useSocketEvent<T>(event: string): T | undefined {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T | undefined>(undefined);\n\n const onEvent = useEffectEvent((data: T) => {\n setValue(data);\n });\n\n useEffect(() => {\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return value;\n}\n\n/**\n * Accumulate WebSocket events into an array.\n * Uses useEffectEvent — handler always sees latest state without re-subscribing.\n *\n * @example\n * const messages = useSocketStream<ChatMessage>('chat.message');\n */\nexport function useSocketStream<T>(event: string): T[] {\n const socket = useSharedWebSocket();\n const [items, setItems] = useState<T[]>([]);\n\n const onEvent = useEffectEvent((data: T) => {\n setItems((prev) => [...prev, data]);\n });\n\n useEffect(() => {\n setItems([]);\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return items;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * Uses useEffectEvent for stable sync callback.\n *\n * @example\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] });\n * // setCart in one tab → updates all tabs instantly\n */\nexport function useSocketSync<T>(\n key: string,\n initialValue: T,\n): [T, (value: T) => void] {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T>(() => {\n return socket.getSync<T>(key) ?? initialValue;\n });\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n });\n\n useEffect(() => {\n const unsub = socket.onSync<T>(key, onSync);\n return unsub;\n }, [socket, key]);\n\n const setAndSync = useEffectEvent((newValue: T) => {\n setValue(newValue);\n socket.sync(key, newValue);\n });\n\n return [value, setAndSync];\n}\n\n/**\n * Reactive connection status.\n * Uses useEffectEvent to avoid re-creating interval on state change.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: boolean;\n tabRole: TabRole;\n} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole };\n}\n"],"mappings":";;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AAMP,IAAM,kBAAkB,cAAsC,IAAI;AAkC3D,SAAS,wBAAwB,EAAE,KAAK,SAAS,SAAS,GAAiC;AAChG,QAAM,CAAC,MAAM,IAAI,SAAS,MAAM;AAC9B,UAAM,KAAK,IAAI,gBAAgB,KAAK,OAAO;AAC3C,OAAG,QAAQ;AACX,WAAO;AAAA,EACT,CAAC;AAED,YAAU,MAAM;AACd,WAAO,MAAM;AACX,aAAO,OAAO,OAAO,EAAE;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,SAAO,cAAc,gBAAgB,UAAU,EAAE,OAAO,OAAO,GAAG,QAAQ;AAC5E;AASO,SAAS,qBAAsC;AACpD,QAAM,MAAM,WAAW,eAAe;AACtC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,kEAAkE;AAAA,EACpF;AACA,SAAO;AACT;AAWO,SAAS,eAAkB,OAA8B;AAC9D,QAAM,SAAS,mBAAmB;AAClC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,MAAS;AAE3D,QAAM,UAAU,eAAe,CAAC,SAAY;AAC1C,aAAS,IAAI;AAAA,EACf,CAAC;AAED,YAAU,MAAM;AACd,UAAM,QAAQ,OAAO,GAAG,OAAO,OAAO;AACtC,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO;AACT;AASO,SAAS,gBAAmB,OAAoB;AACrD,QAAM,SAAS,mBAAmB;AAClC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAc,CAAC,CAAC;AAE1C,QAAM,UAAU,eAAe,CAAC,SAAY;AAC1C,aAAS,CAAC,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;AAAA,EACpC,CAAC;AAED,YAAU,MAAM;AACd,aAAS,CAAC,CAAC;AACX,UAAM,QAAQ,OAAO,GAAG,OAAO,OAAO;AACtC,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO;AACT;AAUO,SAAS,cACd,KACA,cACyB;AACzB,QAAM,SAAS,mBAAmB;AAClC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAY,MAAM;AAC1C,WAAO,OAAO,QAAW,GAAG,KAAK;AAAA,EACnC,CAAC;AAED,QAAM,SAAS,eAAe,CAAC,WAAc;AAC3C,aAAS,MAAM;AAAA,EACjB,CAAC;AAED,YAAU,MAAM;AACd,UAAM,QAAQ,OAAO,OAAU,KAAK,MAAM;AAC1C,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,GAAG,CAAC;AAEhB,QAAM,aAAa,eAAe,CAAC,aAAgB;AACjD,aAAS,QAAQ;AACjB,WAAO,KAAK,KAAK,QAAQ;AAAA,EAC3B,CAAC;AAED,SAAO,CAAC,OAAO,UAAU;AAC3B;AASO,SAAS,kBAGd;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,OAAO,SAAS;AAC3D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAkB,OAAO,OAAO;AAE9D,QAAM,OAAO,eAAe,MAAM;AAChC,iBAAa,OAAO,SAAS;AAC7B,eAAW,OAAO,OAAO;AAAA,EAC3B,CAAC;AAED,YAAU,MAAM;AACd,UAAM,WAAW,YAAY,MAAM,GAAI;AACvC,WAAO,MAAM,cAAc,QAAQ;AAAA,EACrC,GAAG,CAAC,MAAM,CAAC;AAEX,SAAO,EAAE,WAAW,QAAQ;AAC9B;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/adapters/react.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Context ─────────────────────────────────────────────\n\nconst SharedWSContext = createContext<SharedWebSocket | null>(null);\n\n/**\n * Provider props — pass URL and options as props for flexibility.\n *\n * @example\n * <SharedWebSocketProvider url=\"wss://api.example.com/ws\" options={{ auth: getToken }}>\n * <App />\n * </SharedWebSocketProvider>\n */\nexport interface SharedWebSocketProviderProps {\n url: string;\n options?: SharedWebSocketOptions;\n children: ReactNode;\n}\n\n/**\n * Provider component — creates SharedWebSocket from props, auto-disposes on unmount.\n *\n * @example\n * function App() {\n * return (\n * <SharedWebSocketProvider\n * url=\"wss://api.example.com/ws\"\n * options={{\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }}\n * >\n * <Dashboard />\n * </SharedWebSocketProvider>\n * );\n * }\n */\nexport function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps) {\n const [socket] = useState(() => {\n const ws = new SharedWebSocket(url, options);\n ws.connect();\n return ws;\n });\n\n useEffect(() => {\n return () => {\n socket[Symbol.dispose]();\n };\n }, [socket]);\n\n return createElement(SharedWSContext.Provider, { value: socket }, children);\n}\n\n/**\n * Access the SharedWebSocket instance from context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const ctx = useContext(SharedWSContext);\n if (!ctx) {\n throw new Error('useSharedWebSocket must be used within <SharedWebSocketProvider>');\n }\n return ctx;\n}\n\n// ─── Hooks ───────────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns the latest received value (reactive state).\n * - With callback: calls your handler on each event (stable ref via useEffectEvent).\n *\n * @example\n * // Reactive state — returns latest value\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback — full control, no state\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n *\n * @example\n * // Custom callback with transform — store in your own state\n * const [orders, setOrders] = useState<Order[]>([]);\n * useSocketEvent<Order>('order.created', (order) => {\n * setOrders(prev => [order, ...prev].slice(0, 50)); // keep last 50\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): T | undefined {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T | undefined>(undefined);\n\n const onEvent = useEffectEvent((data: T) => {\n if (callback) {\n callback(data);\n } else {\n setValue(data);\n }\n });\n\n useEffect(() => {\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return callback ? undefined : value;\n}\n\n/**\n * Accumulate WebSocket events into an array.\n * - Without callback: returns accumulated array (reactive state).\n * - With callback: calls your handler on each event, you manage your own state.\n *\n * @example\n * // Default — accumulates all events\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom callback — keep only last 50, transform, filter, etc.\n * const [messages, setMessages] = useState<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * setMessages(prev => [msg, ...prev].slice(0, 50));\n * });\n *\n * @example\n * // Custom callback — filter by type\n * const [errors, setErrors] = useState<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') setErrors(prev => [...prev, entry]);\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): T[] {\n const socket = useSharedWebSocket();\n const [items, setItems] = useState<T[]>([]);\n\n const onEvent = useEffectEvent((data: T) => {\n if (callback) {\n callback(data);\n } else {\n setItems((prev) => [...prev, data]);\n }\n });\n\n useEffect(() => {\n if (!callback) setItems([]);\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return callback ? [] : items;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: returns [value, setter] (like useState but synced).\n * - With callback: calls your handler when any tab updates this key.\n *\n * @example\n * // Default — reactive synced state\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] });\n *\n * @example\n * // Custom callback — side effects on sync\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated', { count: cart.items.length });\n * });\n */\nexport function useSocketSync<T>(\n key: string,\n initialValue: T,\n callback?: (value: T) => void,\n): [T, (value: T) => void] {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T>(() => {\n return socket.getSync<T>(key) ?? initialValue;\n });\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n callback?.(synced);\n });\n\n useEffect(() => {\n const unsub = socket.onSync<T>(key, onSync);\n return unsub;\n }, [socket, key]);\n\n const setAndSync = useEffectEvent((newValue: T) => {\n setValue(newValue);\n socket.sync(key, newValue);\n });\n\n return [value, setAndSync];\n}\n\n/**\n * Subscribe to a WebSocket event with just a callback — no state, no return value.\n * Fire-and-forget: side effects, logging, analytics, sounds, browser notifications.\n * Stable ref via useEffectEvent — callback always sees latest closure values.\n *\n * @example\n * useSocketCallback<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', { id: order.id });\n * });\n *\n * @example\n * // Browser notification only from leader tab\n * useSocketCallback<Notification>('notification', (notif) => {\n * if (ws.tabRole === 'leader' && document.hidden) {\n * new Notification(notif.title, { body: notif.body });\n * }\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const handler = useEffectEvent((data: T) => {\n callback(data);\n });\n\n useEffect(() => {\n const unsub = socket.on(event, handler);\n return unsub;\n }, [socket, event]);\n}\n\n/**\n * Reactive connection status.\n * Uses useEffectEvent to avoid re-creating interval on state change.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: boolean;\n tabRole: TabRole;\n} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/src/adapters/react.ts
CHANGED
|
@@ -80,18 +80,38 @@ export function useSharedWebSocket(): SharedWebSocket {
|
|
|
80
80
|
// ─── Hooks ───────────────────────────────────────────────
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* Subscribe to a WebSocket event.
|
|
84
|
-
*
|
|
83
|
+
* Subscribe to a WebSocket event.
|
|
84
|
+
* - Without callback: returns the latest received value (reactive state).
|
|
85
|
+
* - With callback: calls your handler on each event (stable ref via useEffectEvent).
|
|
85
86
|
*
|
|
86
87
|
* @example
|
|
88
|
+
* // Reactive state — returns latest value
|
|
87
89
|
* const order = useSocketEvent<Order>('order.created');
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // Custom callback — full control, no state
|
|
93
|
+
* useSocketEvent<Order>('order.created', (order) => {
|
|
94
|
+
* playSound('new-order');
|
|
95
|
+
* analytics.track('order_received', order);
|
|
96
|
+
* });
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* // Custom callback with transform — store in your own state
|
|
100
|
+
* const [orders, setOrders] = useState<Order[]>([]);
|
|
101
|
+
* useSocketEvent<Order>('order.created', (order) => {
|
|
102
|
+
* setOrders(prev => [order, ...prev].slice(0, 50)); // keep last 50
|
|
103
|
+
* });
|
|
88
104
|
*/
|
|
89
|
-
export function useSocketEvent<T>(event: string): T | undefined {
|
|
105
|
+
export function useSocketEvent<T>(event: string, callback?: (data: T) => void): T | undefined {
|
|
90
106
|
const socket = useSharedWebSocket();
|
|
91
107
|
const [value, setValue] = useState<T | undefined>(undefined);
|
|
92
108
|
|
|
93
109
|
const onEvent = useEffectEvent((data: T) => {
|
|
94
|
-
|
|
110
|
+
if (callback) {
|
|
111
|
+
callback(data);
|
|
112
|
+
} else {
|
|
113
|
+
setValue(data);
|
|
114
|
+
}
|
|
95
115
|
});
|
|
96
116
|
|
|
97
117
|
useEffect(() => {
|
|
@@ -99,44 +119,73 @@ export function useSocketEvent<T>(event: string): T | undefined {
|
|
|
99
119
|
return unsub;
|
|
100
120
|
}, [socket, event]);
|
|
101
121
|
|
|
102
|
-
return value;
|
|
122
|
+
return callback ? undefined : value;
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
/**
|
|
106
126
|
* Accumulate WebSocket events into an array.
|
|
107
|
-
*
|
|
127
|
+
* - Without callback: returns accumulated array (reactive state).
|
|
128
|
+
* - With callback: calls your handler on each event, you manage your own state.
|
|
108
129
|
*
|
|
109
130
|
* @example
|
|
131
|
+
* // Default — accumulates all events
|
|
110
132
|
* const messages = useSocketStream<ChatMessage>('chat.message');
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* // Custom callback — keep only last 50, transform, filter, etc.
|
|
136
|
+
* const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
137
|
+
* useSocketStream<ChatMessage>('chat.message', (msg) => {
|
|
138
|
+
* setMessages(prev => [msg, ...prev].slice(0, 50));
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* // Custom callback — filter by type
|
|
143
|
+
* const [errors, setErrors] = useState<LogEntry[]>([]);
|
|
144
|
+
* useSocketStream<LogEntry>('log.entry', (entry) => {
|
|
145
|
+
* if (entry.level === 'error') setErrors(prev => [...prev, entry]);
|
|
146
|
+
* });
|
|
111
147
|
*/
|
|
112
|
-
export function useSocketStream<T>(event: string): T[] {
|
|
148
|
+
export function useSocketStream<T>(event: string, callback?: (data: T) => void): T[] {
|
|
113
149
|
const socket = useSharedWebSocket();
|
|
114
150
|
const [items, setItems] = useState<T[]>([]);
|
|
115
151
|
|
|
116
152
|
const onEvent = useEffectEvent((data: T) => {
|
|
117
|
-
|
|
153
|
+
if (callback) {
|
|
154
|
+
callback(data);
|
|
155
|
+
} else {
|
|
156
|
+
setItems((prev) => [...prev, data]);
|
|
157
|
+
}
|
|
118
158
|
});
|
|
119
159
|
|
|
120
160
|
useEffect(() => {
|
|
121
|
-
setItems([]);
|
|
161
|
+
if (!callback) setItems([]);
|
|
122
162
|
const unsub = socket.on(event, onEvent);
|
|
123
163
|
return unsub;
|
|
124
164
|
}, [socket, event]);
|
|
125
165
|
|
|
126
|
-
return items;
|
|
166
|
+
return callback ? [] : items;
|
|
127
167
|
}
|
|
128
168
|
|
|
129
169
|
/**
|
|
130
170
|
* Two-way state sync across browser tabs.
|
|
131
|
-
*
|
|
171
|
+
* - Without callback: returns [value, setter] (like useState but synced).
|
|
172
|
+
* - With callback: calls your handler when any tab updates this key.
|
|
132
173
|
*
|
|
133
174
|
* @example
|
|
175
|
+
* // Default — reactive synced state
|
|
134
176
|
* const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] });
|
|
135
|
-
*
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* // Custom callback — side effects on sync
|
|
180
|
+
* const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] }, (cart) => {
|
|
181
|
+
* document.title = `Cart (${cart.items.length})`;
|
|
182
|
+
* analytics.track('cart_updated', { count: cart.items.length });
|
|
183
|
+
* });
|
|
136
184
|
*/
|
|
137
185
|
export function useSocketSync<T>(
|
|
138
186
|
key: string,
|
|
139
187
|
initialValue: T,
|
|
188
|
+
callback?: (value: T) => void,
|
|
140
189
|
): [T, (value: T) => void] {
|
|
141
190
|
const socket = useSharedWebSocket();
|
|
142
191
|
const [value, setValue] = useState<T>(() => {
|
|
@@ -145,6 +194,7 @@ export function useSocketSync<T>(
|
|
|
145
194
|
|
|
146
195
|
const onSync = useEffectEvent((synced: T) => {
|
|
147
196
|
setValue(synced);
|
|
197
|
+
callback?.(synced);
|
|
148
198
|
});
|
|
149
199
|
|
|
150
200
|
useEffect(() => {
|
|
@@ -160,6 +210,38 @@ export function useSocketSync<T>(
|
|
|
160
210
|
return [value, setAndSync];
|
|
161
211
|
}
|
|
162
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Subscribe to a WebSocket event with just a callback — no state, no return value.
|
|
215
|
+
* Fire-and-forget: side effects, logging, analytics, sounds, browser notifications.
|
|
216
|
+
* Stable ref via useEffectEvent — callback always sees latest closure values.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* useSocketCallback<Order>('order.created', (order) => {
|
|
220
|
+
* playSound('new-order');
|
|
221
|
+
* analytics.track('order_received', { id: order.id });
|
|
222
|
+
* });
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* // Browser notification only from leader tab
|
|
226
|
+
* useSocketCallback<Notification>('notification', (notif) => {
|
|
227
|
+
* if (ws.tabRole === 'leader' && document.hidden) {
|
|
228
|
+
* new Notification(notif.title, { body: notif.body });
|
|
229
|
+
* }
|
|
230
|
+
* });
|
|
231
|
+
*/
|
|
232
|
+
export function useSocketCallback<T>(event: string, callback: (data: T) => void): void {
|
|
233
|
+
const socket = useSharedWebSocket();
|
|
234
|
+
|
|
235
|
+
const handler = useEffectEvent((data: T) => {
|
|
236
|
+
callback(data);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const unsub = socket.on(event, handler);
|
|
241
|
+
return unsub;
|
|
242
|
+
}, [socket, event]);
|
|
243
|
+
}
|
|
244
|
+
|
|
163
245
|
/**
|
|
164
246
|
* Reactive connection status.
|
|
165
247
|
* Uses useEffectEvent to avoid re-creating interval on state change.
|