@gwakko/shared-websocket 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -308,26 +308,58 @@ 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, uses `useEffectEvent` for stable refs)
311
+ ### React Hooks (React 19, `useEffectEvent` for stable refs)
312
312
 
313
- | Hook | Returns | Description |
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
- All hooks use context internally no need to pass `ws` as argument.
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
 
325
- | Composable | Returns | Description |
326
- |-----------|---------|-------------|
327
- | `useSocketEvent<T>(event)` | `Ref<T>` | Latest event value |
328
- | `useSocketStream<T>(event)` | `Ref<T[]>` | Accumulated events |
329
- | `useSocketSync<T>(key, init)` | `Ref<T>` | Cross-tab synced state (two-way) |
330
- | `useSocketStatus()` | `{ connected, tabRole }` | Reactive connection status |
353
+ All composables accept an **optional callback** — same pattern as React hooks.
354
+
355
+ | Composable | Without callback | With callback |
356
+ |-----------|-----------------|---------------|
357
+ | `useSharedWebSocket()` | `SharedWebSocket` | |
358
+ | `useSocketEvent<T>(event, cb?)` | `Ref<T>` | `cb(data)` on each event |
359
+ | `useSocketStream<T>(event, cb?)` | `Ref<T[]>` | `cb(data)` — manage your own ref |
360
+ | `useSocketSync<T>(key, init, cb?)` | `Ref<T>` (two-way) | `cb(value)` — side effects on sync |
361
+ | `useSocketCallback<T>(event, cb)` | — | Fire-and-forget |
362
+ | `useSocketStatus()` | `{ connected, tabRole }` | — |
331
363
 
332
364
  ## How It Works
333
365
 
@@ -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. Returns the latest received value.
46
- * Uses useEffectEvent for a stable callback ref no stale closures.
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
- * Uses useEffectEvent handler always sees latest state without re-subscribing.
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
- * Uses useEffectEvent for stable sync callback.
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
- * // setCart in one tab → updates all tabs instantly
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 useSocketSync<T>(key: string, initialValue: T): [T, (value: T) => void];
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.
@@ -7,7 +7,10 @@ export declare const SharedWebSocketKey: InjectionKey<SharedWebSocket>;
7
7
  *
8
8
  * @example
9
9
  * const app = createApp(App);
10
- * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws'));
10
+ * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {
11
+ * auth: () => localStorage.getItem('token')!,
12
+ * useWorker: true,
13
+ * }));
11
14
  */
12
15
  export declare function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions): {
13
16
  install(app: App): void;
@@ -17,30 +20,77 @@ export declare function createSharedWebSocketPlugin(url: string, options?: Share
17
20
  *
18
21
  * @example
19
22
  * const ws = useSharedWebSocket();
23
+ * ws.send('chat.message', { text: 'Hello' });
20
24
  */
21
25
  export declare function useSharedWebSocket(): SharedWebSocket;
22
26
  /**
23
- * Subscribe to a WebSocket event. Returns reactive ref with latest value.
27
+ * Subscribe to a WebSocket event.
28
+ * - Without callback: returns reactive ref with latest value.
29
+ * - With callback: calls your handler on each event.
24
30
  *
25
31
  * @example
32
+ * // Reactive state
26
33
  * const order = useSocketEvent<Order>('order.created');
34
+ *
35
+ * @example
36
+ * // Custom callback
37
+ * useSocketEvent<Order>('order.created', (order) => {
38
+ * playSound('new-order');
39
+ * analytics.track('order_received', order);
40
+ * });
27
41
  */
28
- export declare function useSocketEvent<T>(event: string): Ref<T | undefined>;
42
+ export declare function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined>;
29
43
  /**
30
- * Accumulate WebSocket events into reactive array.
44
+ * Accumulate WebSocket events.
45
+ * - Without callback: returns reactive array.
46
+ * - With callback: calls your handler — manage your own state.
31
47
  *
32
48
  * @example
49
+ * // Default accumulation
33
50
  * const messages = useSocketStream<ChatMessage>('chat.message');
51
+ *
52
+ * @example
53
+ * // Custom — keep last 50
54
+ * const messages = ref<ChatMessage[]>([]);
55
+ * useSocketStream<ChatMessage>('chat.message', (msg) => {
56
+ * messages.value = [msg, ...messages.value].slice(0, 50);
57
+ * });
58
+ *
59
+ * @example
60
+ * // Custom — filter by type
61
+ * const errors = ref<LogEntry[]>([]);
62
+ * useSocketStream<LogEntry>('log.entry', (entry) => {
63
+ * if (entry.level === 'error') errors.value = [...errors.value, entry];
64
+ * });
34
65
  */
35
- export declare function useSocketStream<T>(event: string): Ref<T[]>;
66
+ export declare function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]>;
36
67
  /**
37
- * Two-way state sync across browser tabs via reactive ref.
68
+ * Two-way state sync across browser tabs.
69
+ * - Without callback: reactive ref synced across tabs.
70
+ * - With callback: called when any tab updates this key — side effects.
38
71
  *
39
72
  * @example
73
+ * // Reactive two-way sync
40
74
  * const cart = useSocketSync<Cart>('cart', { items: [] });
41
75
  * cart.value = { items: [1, 2, 3] }; // syncs to all tabs
76
+ *
77
+ * @example
78
+ * // With side effect callback
79
+ * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {
80
+ * document.title = `Cart (${cart.items.length})`;
81
+ * analytics.track('cart_updated');
82
+ * });
83
+ */
84
+ export declare function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T>;
85
+ /**
86
+ * Fire-and-forget event handler — no state, no ref.
87
+ *
88
+ * @example
89
+ * useSocketCallback<Notification>('notification', (n) => {
90
+ * showToast(n.title);
91
+ * });
42
92
  */
43
- export declare function useSocketSync<T>(key: string, initialValue: T): Ref<T>;
93
+ export declare function useSocketCallback<T>(event: string, callback: (data: T) => void): void;
44
94
  /**
45
95
  * Reactive connection status.
46
96
  *
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
- setValue(data);
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
- setItems((prev) => [...prev, data]);
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
- exports.SharedWebSocketProvider = SharedWebSocketProvider; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketEvent = useSocketEvent; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync;
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
@@ -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
- setValue(data);
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
- setItems((prev) => [...prev, data]);
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/dist/vue.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,29 +32,38 @@ function useSharedWebSocket() {
32
32
  }
33
33
  return socket;
34
34
  }
35
- function useSocketEvent(event) {
35
+ function useSocketEvent(event, callback) {
36
36
  const socket = useSharedWebSocket();
37
37
  const value = _vue.ref.call(void 0, void 0);
38
38
  const unsub = socket.on(event, (data) => {
39
- value.value = data;
39
+ if (callback) {
40
+ callback(data);
41
+ } else {
42
+ value.value = data;
43
+ }
40
44
  });
41
45
  _vue.onUnmounted.call(void 0, unsub);
42
46
  return _vue.readonly.call(void 0, value);
43
47
  }
44
- function useSocketStream(event) {
48
+ function useSocketStream(event, callback) {
45
49
  const socket = useSharedWebSocket();
46
50
  const items = _vue.ref.call(void 0, []);
47
51
  const unsub = socket.on(event, (data) => {
48
- items.value = [...items.value, data];
52
+ if (callback) {
53
+ callback(data);
54
+ } else {
55
+ items.value = [...items.value, data];
56
+ }
49
57
  });
50
58
  _vue.onUnmounted.call(void 0, unsub);
51
59
  return _vue.readonly.call(void 0, items);
52
60
  }
53
- function useSocketSync(key, initialValue) {
61
+ function useSocketSync(key, initialValue, callback) {
54
62
  const socket = useSharedWebSocket();
55
63
  const value = _vue.ref.call(void 0, _nullishCoalesce(socket.getSync(key), () => ( initialValue)));
56
64
  const unsub = socket.onSync(key, (v) => {
57
65
  value.value = v;
66
+ _optionalChain([callback, 'optionalCall', _ => _(v)]);
58
67
  });
59
68
  _vue.watch.call(void 0,
60
69
  value,
@@ -66,12 +75,18 @@ function useSocketSync(key, initialValue) {
66
75
  _vue.onUnmounted.call(void 0, unsub);
67
76
  return value;
68
77
  }
78
+ function useSocketCallback(event, callback) {
79
+ const socket = useSharedWebSocket();
80
+ const unsub = socket.on(event, (data) => {
81
+ callback(data);
82
+ });
83
+ _vue.onUnmounted.call(void 0, unsub);
84
+ }
69
85
  function useSocketStatus() {
70
86
  const socket = useSharedWebSocket();
71
87
  const connected = _vue.ref.call(void 0, socket.connected);
72
88
  const tabRole = _vue.ref.call(void 0, socket.tabRole);
73
- let timer;
74
- timer = setInterval(() => {
89
+ const timer = setInterval(() => {
75
90
  connected.value = socket.connected;
76
91
  tabRole.value = socket.tabRole;
77
92
  }, 1e3);
@@ -89,5 +104,6 @@ function useSocketStatus() {
89
104
 
90
105
 
91
106
 
92
- exports.SharedWebSocketKey = SharedWebSocketKey; exports.createSharedWebSocketPlugin = createSharedWebSocketPlugin; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketEvent = useSocketEvent; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync;
107
+
108
+ exports.SharedWebSocketKey = SharedWebSocketKey; exports.createSharedWebSocketPlugin = createSharedWebSocketPlugin; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketCallback = useSocketCallback; exports.useSocketEvent = useSocketEvent; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync;
93
109
  //# sourceMappingURL=vue.cjs.map
package/dist/vue.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","../src/adapters/vue.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","sourcesContent":[null,"import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws'));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n // Cleanup on app unmount\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event. Returns reactive ref with latest value.\n *\n * @example\n * const order = useSocketEvent<Order>('order.created');\n */\nexport function useSocketEvent<T>(event: string): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const unsub = socket.on(event, (data: T) => {\n value.value = data;\n });\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events into reactive array.\n *\n * @example\n * const messages = useSocketStream<ChatMessage>('chat.message');\n */\nexport function useSocketStream<T>(event: string): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const unsub = socket.on(event, (data: T) => {\n items.value = [...items.value, data];\n });\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs via reactive ref.\n *\n * @example\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n */\nexport function useSocketSync<T>(key: string, initialValue: T): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n });\n\n // Watch for local changes → sync to other tabs\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n\n let timer: ReturnType<typeof setInterval>;\n\n timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n };\n}\n"]}
1
+ {"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","../src/adapters/vue.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","sourcesContent":[null,"import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns reactive ref with latest value.\n * - With callback: calls your handler on each event.\n *\n * @example\n * // Reactive state\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const unsub = socket.on(event, (data: T) => {\n if (callback) {\n callback(data);\n } else {\n value.value = data;\n }\n });\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events.\n * - Without callback: returns reactive array.\n * - With callback: calls your handler — manage your own state.\n *\n * @example\n * // Default accumulation\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom — keep last 50\n * const messages = ref<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * messages.value = [msg, ...messages.value].slice(0, 50);\n * });\n *\n * @example\n * // Custom — filter by type\n * const errors = ref<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') errors.value = [...errors.value, entry];\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const unsub = socket.on(event, (data: T) => {\n if (callback) {\n callback(data);\n } else {\n items.value = [...items.value, data];\n }\n });\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: reactive ref synced across tabs.\n * - With callback: called when any tab updates this key — side effects.\n *\n * @example\n * // Reactive two-way sync\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated');\n * });\n */\nexport function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Fire-and-forget event handler — no state, no ref.\n *\n * @example\n * useSocketCallback<Notification>('notification', (n) => {\n * showToast(n.title);\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const unsub = socket.on(event, (data: T) => {\n callback(data);\n });\n\n onUnmounted(unsub);\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n };\n}\n"]}
package/dist/vue.js CHANGED
@@ -32,29 +32,38 @@ function useSharedWebSocket() {
32
32
  }
33
33
  return socket;
34
34
  }
35
- function useSocketEvent(event) {
35
+ function useSocketEvent(event, callback) {
36
36
  const socket = useSharedWebSocket();
37
37
  const value = ref(void 0);
38
38
  const unsub = socket.on(event, (data) => {
39
- value.value = data;
39
+ if (callback) {
40
+ callback(data);
41
+ } else {
42
+ value.value = data;
43
+ }
40
44
  });
41
45
  onUnmounted(unsub);
42
46
  return readonly(value);
43
47
  }
44
- function useSocketStream(event) {
48
+ function useSocketStream(event, callback) {
45
49
  const socket = useSharedWebSocket();
46
50
  const items = ref([]);
47
51
  const unsub = socket.on(event, (data) => {
48
- items.value = [...items.value, data];
52
+ if (callback) {
53
+ callback(data);
54
+ } else {
55
+ items.value = [...items.value, data];
56
+ }
49
57
  });
50
58
  onUnmounted(unsub);
51
59
  return readonly(items);
52
60
  }
53
- function useSocketSync(key, initialValue) {
61
+ function useSocketSync(key, initialValue, callback) {
54
62
  const socket = useSharedWebSocket();
55
63
  const value = ref(socket.getSync(key) ?? initialValue);
56
64
  const unsub = socket.onSync(key, (v) => {
57
65
  value.value = v;
66
+ callback?.(v);
58
67
  });
59
68
  watch(
60
69
  value,
@@ -66,12 +75,18 @@ function useSocketSync(key, initialValue) {
66
75
  onUnmounted(unsub);
67
76
  return value;
68
77
  }
78
+ function useSocketCallback(event, callback) {
79
+ const socket = useSharedWebSocket();
80
+ const unsub = socket.on(event, (data) => {
81
+ callback(data);
82
+ });
83
+ onUnmounted(unsub);
84
+ }
69
85
  function useSocketStatus() {
70
86
  const socket = useSharedWebSocket();
71
87
  const connected = ref(socket.connected);
72
88
  const tabRole = ref(socket.tabRole);
73
- let timer;
74
- timer = setInterval(() => {
89
+ const timer = setInterval(() => {
75
90
  connected.value = socket.connected;
76
91
  tabRole.value = socket.tabRole;
77
92
  }, 1e3);
@@ -85,6 +100,7 @@ export {
85
100
  SharedWebSocketKey,
86
101
  createSharedWebSocketPlugin,
87
102
  useSharedWebSocket,
103
+ useSocketCallback,
88
104
  useSocketEvent,
89
105
  useSocketStatus,
90
106
  useSocketStream,
package/dist/vue.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapters/vue.ts"],"sourcesContent":["import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws'));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n // Cleanup on app unmount\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event. Returns reactive ref with latest value.\n *\n * @example\n * const order = useSocketEvent<Order>('order.created');\n */\nexport function useSocketEvent<T>(event: string): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const unsub = socket.on(event, (data: T) => {\n value.value = data;\n });\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events into reactive array.\n *\n * @example\n * const messages = useSocketStream<ChatMessage>('chat.message');\n */\nexport function useSocketStream<T>(event: string): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const unsub = socket.on(event, (data: T) => {\n items.value = [...items.value, data];\n });\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs via reactive ref.\n *\n * @example\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n */\nexport function useSocketSync<T>(key: string, initialValue: T): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n });\n\n // Watch for local changes → sync to other tabs\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n\n let timer: ReturnType<typeof setInterval>;\n\n timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n };\n}\n"],"mappings":";;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAMA,IAAM,qBAAoD,uBAAO,iBAAiB;AASlF,SAAS,4BAA4B,KAAa,SAAkC;AACzF,SAAO;AAAA,IACL,QAAQ,KAAU;AAChB,YAAM,SAAS,IAAI,gBAAgB,KAAK,OAAO;AAC/C,aAAO,QAAQ;AACf,UAAI,QAAQ,oBAAoB,MAAM;AAGtC,YAAM,kBAAkB,IAAI,QAAQ,KAAK,GAAG;AAC5C,UAAI,UAAU,MAAM;AAClB,eAAO,OAAO,OAAO,EAAE;AACvB,wBAAgB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACF;AAQO,SAAS,qBAAsC;AACpD,QAAM,SAAS,OAAO,kBAAkB;AACxC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,+EAA+E;AAAA,EACjG;AACA,SAAO;AACT;AAUO,SAAS,eAAkB,OAAmC;AACnE,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAmB,MAAS;AAE1C,QAAM,QAAQ,OAAO,GAAG,OAAO,CAAC,SAAY;AAC1C,UAAM,QAAQ;AAAA,EAChB,CAAC;AAED,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AAQO,SAAS,gBAAmB,OAAyB;AAC1D,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAS,CAAC,CAAC;AAEzB,QAAM,QAAQ,OAAO,GAAG,OAAO,CAAC,SAAY;AAC1C,UAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,IAAI;AAAA,EACrC,CAAC;AAED,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AASO,SAAS,cAAiB,KAAa,cAAyB;AACrE,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAO,OAAO,QAAW,GAAG,KAAK,YAAY;AAE3D,QAAM,QAAQ,OAAO,OAAU,KAAK,CAAC,MAAM;AACzC,UAAM,QAAQ;AAAA,EAChB,CAAC;AAGD;AAAA,IACE;AAAA,IACA,CAAC,WAAW;AACV,aAAO,KAAK,KAAK,MAAM;AAAA,IACzB;AAAA,IACA,EAAE,MAAM,KAAK;AAAA,EACf;AAEA,cAAY,KAAK;AACjB,SAAO;AACT;AAQO,SAAS,kBAGd;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,YAAY,IAAI,OAAO,SAAS;AACtC,QAAM,UAAU,IAAa,OAAO,OAAO;AAE3C,MAAI;AAEJ,UAAQ,YAAY,MAAM;AACxB,cAAU,QAAQ,OAAO;AACzB,YAAQ,QAAQ,OAAO;AAAA,EACzB,GAAG,GAAI;AAEP,cAAY,MAAM,cAAc,KAAK,CAAC;AAEtC,SAAO;AAAA,IACL,WAAW,SAAS,SAAS;AAAA,IAC7B,SAAS,SAAS,OAAO;AAAA,EAC3B;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/adapters/vue.ts"],"sourcesContent":["import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns reactive ref with latest value.\n * - With callback: calls your handler on each event.\n *\n * @example\n * // Reactive state\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const unsub = socket.on(event, (data: T) => {\n if (callback) {\n callback(data);\n } else {\n value.value = data;\n }\n });\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events.\n * - Without callback: returns reactive array.\n * - With callback: calls your handler — manage your own state.\n *\n * @example\n * // Default accumulation\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom — keep last 50\n * const messages = ref<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * messages.value = [msg, ...messages.value].slice(0, 50);\n * });\n *\n * @example\n * // Custom — filter by type\n * const errors = ref<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') errors.value = [...errors.value, entry];\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const unsub = socket.on(event, (data: T) => {\n if (callback) {\n callback(data);\n } else {\n items.value = [...items.value, data];\n }\n });\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: reactive ref synced across tabs.\n * - With callback: called when any tab updates this key — side effects.\n *\n * @example\n * // Reactive two-way sync\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated');\n * });\n */\nexport function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Fire-and-forget event handler — no state, no ref.\n *\n * @example\n * useSocketCallback<Notification>('notification', (n) => {\n * showToast(n.title);\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const unsub = socket.on(event, (data: T) => {\n callback(data);\n });\n\n onUnmounted(unsub);\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n };\n}\n"],"mappings":";;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAMA,IAAM,qBAAoD,uBAAO,iBAAiB;AAYlF,SAAS,4BAA4B,KAAa,SAAkC;AACzF,SAAO;AAAA,IACL,QAAQ,KAAU;AAChB,YAAM,SAAS,IAAI,gBAAgB,KAAK,OAAO;AAC/C,aAAO,QAAQ;AACf,UAAI,QAAQ,oBAAoB,MAAM;AAEtC,YAAM,kBAAkB,IAAI,QAAQ,KAAK,GAAG;AAC5C,UAAI,UAAU,MAAM;AAClB,eAAO,OAAO,OAAO,EAAE;AACvB,wBAAgB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACF;AASO,SAAS,qBAAsC;AACpD,QAAM,SAAS,OAAO,kBAAkB;AACxC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,+EAA+E;AAAA,EACjG;AACA,SAAO;AACT;AAoBO,SAAS,eAAkB,OAAe,UAAkD;AACjG,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAmB,MAAS;AAE1C,QAAM,QAAQ,OAAO,GAAG,OAAO,CAAC,SAAY;AAC1C,QAAI,UAAU;AACZ,eAAS,IAAI;AAAA,IACf,OAAO;AACL,YAAM,QAAQ;AAAA,IAChB;AAAA,EACF,CAAC;AAED,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AAyBO,SAAS,gBAAmB,OAAe,UAAwC;AACxF,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAS,CAAC,CAAC;AAEzB,QAAM,QAAQ,OAAO,GAAG,OAAO,CAAC,SAAY;AAC1C,QAAI,UAAU;AACZ,eAAS,IAAI;AAAA,IACf,OAAO;AACL,YAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,IAAI;AAAA,IACrC;AAAA,EACF,CAAC;AAED,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AAmBO,SAAS,cAAiB,KAAa,cAAiB,UAAuC;AACpG,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAO,OAAO,QAAW,GAAG,KAAK,YAAY;AAE3D,QAAM,QAAQ,OAAO,OAAU,KAAK,CAAC,MAAM;AACzC,UAAM,QAAQ;AACd,eAAW,CAAC;AAAA,EACd,CAAC;AAED;AAAA,IACE;AAAA,IACA,CAAC,WAAW;AACV,aAAO,KAAK,KAAK,MAAM;AAAA,IACzB;AAAA,IACA,EAAE,MAAM,KAAK;AAAA,EACf;AAEA,cAAY,KAAK;AACjB,SAAO;AACT;AAUO,SAAS,kBAAqB,OAAe,UAAmC;AACrF,QAAM,SAAS,mBAAmB;AAElC,QAAM,QAAQ,OAAO,GAAG,OAAO,CAAC,SAAY;AAC1C,aAAS,IAAI;AAAA,EACf,CAAC;AAED,cAAY,KAAK;AACnB;AAQO,SAAS,kBAGd;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,YAAY,IAAI,OAAO,SAAS;AACtC,QAAM,UAAU,IAAa,OAAO,OAAO;AAE3C,QAAM,QAAQ,YAAY,MAAM;AAC9B,cAAU,QAAQ,OAAO;AACzB,YAAQ,QAAQ,OAAO;AAAA,EACzB,GAAG,GAAI;AAEP,cAAY,MAAM,cAAc,KAAK,CAAC;AAEtC,SAAO;AAAA,IACL,WAAW,SAAS,SAAS;AAAA,IAC7B,SAAS,SAAS,OAAO;AAAA,EAC3B;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gwakko/shared-websocket",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Share ONE WebSocket connection across browser tabs — leader election, BroadcastChannel sync, optional Web Worker",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -80,18 +80,38 @@ export function useSharedWebSocket(): SharedWebSocket {
80
80
  // ─── Hooks ───────────────────────────────────────────────
81
81
 
82
82
  /**
83
- * Subscribe to a WebSocket event. Returns the latest received value.
84
- * Uses useEffectEvent for a stable callback ref no stale closures.
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
- setValue(data);
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
- * Uses useEffectEvent handler always sees latest state without re-subscribing.
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
- setItems((prev) => [...prev, data]);
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
- * Uses useEffectEvent for stable sync callback.
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
- * // setCart in one tab → updates all tabs instantly
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.
@@ -20,7 +20,10 @@ export const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedW
20
20
  *
21
21
  * @example
22
22
  * const app = createApp(App);
23
- * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws'));
23
+ * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {
24
+ * auth: () => localStorage.getItem('token')!,
25
+ * useWorker: true,
26
+ * }));
24
27
  */
25
28
  export function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {
26
29
  return {
@@ -29,7 +32,6 @@ export function createSharedWebSocketPlugin(url: string, options?: SharedWebSock
29
32
  socket.connect();
30
33
  app.provide(SharedWebSocketKey, socket);
31
34
 
32
- // Cleanup on app unmount
33
35
  const originalUnmount = app.unmount.bind(app);
34
36
  app.unmount = () => {
35
37
  socket[Symbol.dispose]();
@@ -44,6 +46,7 @@ export function createSharedWebSocketPlugin(url: string, options?: SharedWebSock
44
46
  *
45
47
  * @example
46
48
  * const ws = useSharedWebSocket();
49
+ * ws.send('chat.message', { text: 'Hello' });
47
50
  */
48
51
  export function useSharedWebSocket(): SharedWebSocket {
49
52
  const socket = inject(SharedWebSocketKey);
@@ -56,17 +59,31 @@ export function useSharedWebSocket(): SharedWebSocket {
56
59
  // ─── Composables ─────────────────────────────────────────
57
60
 
58
61
  /**
59
- * Subscribe to a WebSocket event. Returns reactive ref with latest value.
62
+ * Subscribe to a WebSocket event.
63
+ * - Without callback: returns reactive ref with latest value.
64
+ * - With callback: calls your handler on each event.
60
65
  *
61
66
  * @example
67
+ * // Reactive state
62
68
  * const order = useSocketEvent<Order>('order.created');
69
+ *
70
+ * @example
71
+ * // Custom callback
72
+ * useSocketEvent<Order>('order.created', (order) => {
73
+ * playSound('new-order');
74
+ * analytics.track('order_received', order);
75
+ * });
63
76
  */
64
- export function useSocketEvent<T>(event: string): Ref<T | undefined> {
77
+ export function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined> {
65
78
  const socket = useSharedWebSocket();
66
79
  const value = ref<T | undefined>(undefined) as Ref<T | undefined>;
67
80
 
68
81
  const unsub = socket.on(event, (data: T) => {
69
- value.value = data;
82
+ if (callback) {
83
+ callback(data);
84
+ } else {
85
+ value.value = data;
86
+ }
70
87
  });
71
88
 
72
89
  onUnmounted(unsub);
@@ -74,17 +91,38 @@ export function useSocketEvent<T>(event: string): Ref<T | undefined> {
74
91
  }
75
92
 
76
93
  /**
77
- * Accumulate WebSocket events into reactive array.
94
+ * Accumulate WebSocket events.
95
+ * - Without callback: returns reactive array.
96
+ * - With callback: calls your handler — manage your own state.
78
97
  *
79
98
  * @example
99
+ * // Default accumulation
80
100
  * const messages = useSocketStream<ChatMessage>('chat.message');
101
+ *
102
+ * @example
103
+ * // Custom — keep last 50
104
+ * const messages = ref<ChatMessage[]>([]);
105
+ * useSocketStream<ChatMessage>('chat.message', (msg) => {
106
+ * messages.value = [msg, ...messages.value].slice(0, 50);
107
+ * });
108
+ *
109
+ * @example
110
+ * // Custom — filter by type
111
+ * const errors = ref<LogEntry[]>([]);
112
+ * useSocketStream<LogEntry>('log.entry', (entry) => {
113
+ * if (entry.level === 'error') errors.value = [...errors.value, entry];
114
+ * });
81
115
  */
82
- export function useSocketStream<T>(event: string): Ref<T[]> {
116
+ export function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]> {
83
117
  const socket = useSharedWebSocket();
84
118
  const items = ref<T[]>([]) as Ref<T[]>;
85
119
 
86
120
  const unsub = socket.on(event, (data: T) => {
87
- items.value = [...items.value, data];
121
+ if (callback) {
122
+ callback(data);
123
+ } else {
124
+ items.value = [...items.value, data];
125
+ }
88
126
  });
89
127
 
90
128
  onUnmounted(unsub);
@@ -92,21 +130,31 @@ export function useSocketStream<T>(event: string): Ref<T[]> {
92
130
  }
93
131
 
94
132
  /**
95
- * Two-way state sync across browser tabs via reactive ref.
133
+ * Two-way state sync across browser tabs.
134
+ * - Without callback: reactive ref synced across tabs.
135
+ * - With callback: called when any tab updates this key — side effects.
96
136
  *
97
137
  * @example
138
+ * // Reactive two-way sync
98
139
  * const cart = useSocketSync<Cart>('cart', { items: [] });
99
140
  * cart.value = { items: [1, 2, 3] }; // syncs to all tabs
141
+ *
142
+ * @example
143
+ * // With side effect callback
144
+ * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {
145
+ * document.title = `Cart (${cart.items.length})`;
146
+ * analytics.track('cart_updated');
147
+ * });
100
148
  */
101
- export function useSocketSync<T>(key: string, initialValue: T): Ref<T> {
149
+ export function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {
102
150
  const socket = useSharedWebSocket();
103
151
  const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;
104
152
 
105
153
  const unsub = socket.onSync<T>(key, (v) => {
106
154
  value.value = v;
155
+ callback?.(v);
107
156
  });
108
157
 
109
- // Watch for local changes → sync to other tabs
110
158
  watch(
111
159
  value,
112
160
  (newVal) => {
@@ -119,6 +167,24 @@ export function useSocketSync<T>(key: string, initialValue: T): Ref<T> {
119
167
  return value;
120
168
  }
121
169
 
170
+ /**
171
+ * Fire-and-forget event handler — no state, no ref.
172
+ *
173
+ * @example
174
+ * useSocketCallback<Notification>('notification', (n) => {
175
+ * showToast(n.title);
176
+ * });
177
+ */
178
+ export function useSocketCallback<T>(event: string, callback: (data: T) => void): void {
179
+ const socket = useSharedWebSocket();
180
+
181
+ const unsub = socket.on(event, (data: T) => {
182
+ callback(data);
183
+ });
184
+
185
+ onUnmounted(unsub);
186
+ }
187
+
122
188
  /**
123
189
  * Reactive connection status.
124
190
  *
@@ -133,9 +199,7 @@ export function useSocketStatus(): {
133
199
  const connected = ref(socket.connected);
134
200
  const tabRole = ref<TabRole>(socket.tabRole);
135
201
 
136
- let timer: ReturnType<typeof setInterval>;
137
-
138
- timer = setInterval(() => {
202
+ const timer = setInterval(() => {
139
203
  connected.value = socket.connected;
140
204
  tabRole.value = socket.tabRole;
141
205
  }, 1000);