@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 +47 -15
- package/dist/adapters/react.d.ts +68 -8
- package/dist/adapters/vue.d.ts +57 -7
- 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/dist/vue.cjs +25 -9
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +23 -7
- package/dist/vue.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/react.ts +94 -12
- package/src/adapters/vue.ts +78 -14
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,
|
|
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
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
|
328
|
-
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
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
|
|
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/adapters/vue.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACA;ACJA;AACE;AACA;AACA;AACA;AACA;AAAA,0BAIK;AAMA,IAAM,mBAAA,kBAAoD,MAAA,CAAO,iBAAiB,CAAA;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
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.
|
package/src/adapters/vue.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
timer = setInterval(() => {
|
|
202
|
+
const timer = setInterval(() => {
|
|
139
203
|
connected.value = socket.connected;
|
|
140
204
|
tabRole.value = socket.tabRole;
|
|
141
205
|
}, 1000);
|