@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 +10 -6
- package/dist/adapters/vue.d.ts +57 -7
- 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/vue.ts +78 -14
package/README.md
CHANGED
|
@@ -350,12 +350,16 @@ useSocketCallback<Notification>('notification', (n) => {
|
|
|
350
350
|
|
|
351
351
|
### Vue Composables
|
|
352
352
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
|
356
|
-
|
|
357
|
-
| `
|
|
358
|
-
| `
|
|
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
|
|
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/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/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);
|