@gwakko/shared-websocket 0.2.0 → 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
@@ -350,12 +350,16 @@ useSocketCallback<Notification>('notification', (n) => {
350
350
 
351
351
  ### Vue Composables
352
352
 
353
- | Composable | Returns | Description |
354
- |-----------|---------|-------------|
355
- | `useSocketEvent<T>(event)` | `Ref<T>` | Latest event value |
356
- | `useSocketStream<T>(event)` | `Ref<T[]>` | Accumulated events |
357
- | `useSocketSync<T>(key, init)` | `Ref<T>` | Cross-tab synced state (two-way) |
358
- | `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 }` | — |
359
363
 
360
364
  ## How It Works
361
365
 
@@ -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/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.2.0",
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",
@@ -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);