@gwakko/shared-websocket 0.12.1 → 0.12.3
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 +1 -0
- package/dist/SharedSocket.d.ts +3 -0
- package/dist/TabSync.d.ts +14 -2
- package/dist/WorkerSocket.d.ts +7 -1
- package/dist/adapters/sync-react.d.ts +3 -1
- package/dist/adapters/sync-vue.d.ts +3 -1
- package/dist/{chunk-RM27CYKT.js → chunk-7WBM2C7H.js} +15 -2
- package/dist/chunk-7WBM2C7H.js.map +1 -0
- package/dist/{chunk-MP3K5IEI.cjs → chunk-OVKB2KLE.cjs} +69 -33
- package/dist/chunk-OVKB2KLE.cjs.map +1 -0
- package/dist/{chunk-ET3YHQ7V.cjs → chunk-RJKAFACH.cjs} +16 -3
- package/dist/chunk-RJKAFACH.cjs.map +1 -0
- package/dist/{chunk-INJYCCW7.js → chunk-SQZHBLWT.js} +47 -11
- package/dist/chunk-SQZHBLWT.js.map +1 -0
- package/dist/index.cjs +4 -4
- package/dist/index.js +2 -2
- package/dist/react.cjs +3 -6
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +3 -6
- package/dist/react.js.map +1 -1
- package/dist/sync-react.cjs +3 -3
- package/dist/sync-react.cjs.map +1 -1
- package/dist/sync-react.js +3 -3
- package/dist/sync-react.js.map +1 -1
- package/dist/sync-vue.cjs +3 -3
- package/dist/sync-vue.cjs.map +1 -1
- package/dist/sync-vue.js +3 -3
- package/dist/sync-vue.js.map +1 -1
- package/dist/sync.cjs +2 -2
- package/dist/sync.d.ts +1 -1
- package/dist/sync.js +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/vue.cjs +3 -3
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +2 -2
- package/dist/vue.js.map +1 -1
- package/package.json +1 -1
- package/src/SharedSocket.ts +13 -0
- package/src/SharedWebSocket.ts +6 -2
- package/src/TabSync.ts +23 -2
- package/src/WorkerSocket.ts +41 -7
- package/src/adapters/react.ts +2 -5
- package/src/adapters/sync-react.ts +4 -2
- package/src/adapters/sync-vue.ts +2 -2
- package/src/adapters/vue.ts +1 -1
- package/src/sync.ts +1 -1
- package/src/types.ts +2 -0
- package/dist/chunk-ET3YHQ7V.cjs.map +0 -1
- package/dist/chunk-INJYCCW7.js.map +0 -1
- package/dist/chunk-MP3K5IEI.cjs.map +0 -1
- package/dist/chunk-RM27CYKT.js.map +0 -1
package/dist/react.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SharedWebSocket
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SQZHBLWT.js";
|
|
4
4
|
import "./chunk-B2V5HX77.js";
|
|
5
5
|
|
|
6
6
|
// src/adapters/react.ts
|
|
@@ -15,12 +15,9 @@ import {
|
|
|
15
15
|
} from "react";
|
|
16
16
|
var SharedWSContext = createContext(null);
|
|
17
17
|
function SharedWebSocketProvider({ url, options, children }) {
|
|
18
|
-
const [socket] = useState(() =>
|
|
19
|
-
const ws = new SharedWebSocket(url, options);
|
|
20
|
-
ws.connect();
|
|
21
|
-
return ws;
|
|
22
|
-
});
|
|
18
|
+
const [socket] = useState(() => new SharedWebSocket(url, options));
|
|
23
19
|
useEffect(() => {
|
|
20
|
+
socket.connect();
|
|
24
21
|
return () => {
|
|
25
22
|
socket[Symbol.dispose]();
|
|
26
23
|
};
|
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 useRef,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers, EventHandler } 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/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs via BroadcastChannel.\n *\n * @example\n * function LoginPage() {\n * const { authenticate } = useSocketAuth();\n * const login = async (email: string, password: string) => {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * };\n * return <button onClick={() => login('user@test.com', 'pass')}>Login</button>;\n * }\n *\n * @example\n * function Header() {\n * const { isAuthenticated, deauthenticate } = useSocketAuth();\n * return isAuthenticated\n * ? <button onClick={deauthenticate}>Logout</button>\n * : <Link to=\"/login\">Login</Link>;\n * }\n */\nexport function useSocketAuth(): {\n isAuthenticated: boolean;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const [isAuthenticated, setIsAuthenticated] = useState(socket.isAuthenticated);\n\n const onAuthChange = useEffectEvent((authenticated: boolean) => {\n setIsAuthenticated(authenticated);\n });\n\n useEffect(() => {\n return socket.onAuthChange(onAuthChange);\n }, [socket]);\n\n const authenticate = useEffectEvent((token: string) => {\n socket.authenticate(token);\n });\n\n const deauthenticate = useEffectEvent(() => {\n socket.deauthenticate();\n });\n\n return { isAuthenticated, authenticate, deauthenticate };\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 as EventHandler);\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 as EventHandler);\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 as EventHandler);\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 isAuthenticated: boolean;\n} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n const [isAuthenticated, setIsAuthenticated] = useState(socket.isAuthenticated);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n setIsAuthenticated(socket.isAuthenticated);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole, isAuthenticated };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => console.log('Disconnected'),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n\n const onConnect = useEffectEvent(() => handlers.onConnect?.());\n const onDisconnect = useEffectEvent(() => handlers.onDisconnect?.());\n const onReconnecting = useEffectEvent(() => handlers.onReconnecting?.());\n const onLeaderChange = useEffectEvent((isLeader: boolean) => handlers.onLeaderChange?.(isLeader));\n const onError = useEffectEvent((error: unknown) => handlers.onError?.(error));\n const onActive = useEffectEvent(() => handlers.onActive?.());\n const onInactive = useEffectEvent(() => handlers.onInactive?.());\n const onVisibilityChange = useEffectEvent((isActive: boolean) => handlers.onVisibilityChange?.(isActive));\n const onAuthChange = useEffectEvent((authenticated: boolean) => handlers.onAuthChange?.(authenticated));\n\n useEffect(() => {\n const unsubs = [\n socket.onConnect(onConnect),\n socket.onDisconnect(onDisconnect),\n socket.onReconnecting(onReconnecting),\n socket.onLeaderChange(onLeaderChange),\n socket.onError(onError),\n socket.onActive(onActive),\n socket.onInactive(onInactive),\n socket.onVisibilityChange(onVisibilityChange),\n socket.onAuthChange(onAuthChange),\n ];\n return () => unsubs.forEach((u) => u());\n }, [socket]);\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * const message = useSocketEvent('chat:room_123:message');\n * chat.send('message', { text: 'Hello' });\n *\n * @example\n * // Tenant notifications\n * const notifications = useChannel(`tenant:${tenantId}:notifications`);\n * useSocketCallback(`tenant:${tenantId}:notifications:alert`, showToast);\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channelRef = useRef(socket.channel(name, options));\n\n useEffect(() => {\n channelRef.current = socket.channel(name, options);\n return () => channelRef.current.leave();\n }, [socket, name]);\n\n return channelRef.current;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n * useTopics([`user:${userId}:mentions`]);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n useEffect(() => {\n topics.forEach((t) => socket.subscribe(t, options));\n return () => topics.forEach((t) => socket.unsubscribe(t));\n }, [socket, topics.join(',')]);\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n *\n * @example\n * usePush('order.created', {\n * title: (order) => `New Order #${order.id}`,\n * body: (order) => `$${order.total}`,\n * onClick: (order) => navigate(`/orders/${order.id}`),\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n\n useEffect(() => {\n const unsub = socket.push<T>(event, config);\n return unsub;\n }, [socket, event]);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/adapters/react.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers, EventHandler } 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(() => new SharedWebSocket(url, options));\n\n useEffect(() => {\n socket.connect();\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/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs via BroadcastChannel.\n *\n * @example\n * function LoginPage() {\n * const { authenticate } = useSocketAuth();\n * const login = async (email: string, password: string) => {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * };\n * return <button onClick={() => login('user@test.com', 'pass')}>Login</button>;\n * }\n *\n * @example\n * function Header() {\n * const { isAuthenticated, deauthenticate } = useSocketAuth();\n * return isAuthenticated\n * ? <button onClick={deauthenticate}>Logout</button>\n * : <Link to=\"/login\">Login</Link>;\n * }\n */\nexport function useSocketAuth(): {\n isAuthenticated: boolean;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const [isAuthenticated, setIsAuthenticated] = useState(socket.isAuthenticated);\n\n const onAuthChange = useEffectEvent((authenticated: boolean) => {\n setIsAuthenticated(authenticated);\n });\n\n useEffect(() => {\n return socket.onAuthChange(onAuthChange);\n }, [socket]);\n\n const authenticate = useEffectEvent((token: string) => {\n socket.authenticate(token);\n });\n\n const deauthenticate = useEffectEvent(() => {\n socket.deauthenticate();\n });\n\n return { isAuthenticated, authenticate, deauthenticate };\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 as EventHandler);\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 as EventHandler);\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 as EventHandler);\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 isAuthenticated: boolean;\n} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n const [isAuthenticated, setIsAuthenticated] = useState(socket.isAuthenticated);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n setIsAuthenticated(socket.isAuthenticated);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole, isAuthenticated };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => console.log('Disconnected'),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n\n const onConnect = useEffectEvent(() => handlers.onConnect?.());\n const onDisconnect = useEffectEvent(() => handlers.onDisconnect?.());\n const onReconnecting = useEffectEvent(() => handlers.onReconnecting?.());\n const onLeaderChange = useEffectEvent((isLeader: boolean) => handlers.onLeaderChange?.(isLeader));\n const onError = useEffectEvent((error: unknown) => handlers.onError?.(error));\n const onActive = useEffectEvent(() => handlers.onActive?.());\n const onInactive = useEffectEvent(() => handlers.onInactive?.());\n const onVisibilityChange = useEffectEvent((isActive: boolean) => handlers.onVisibilityChange?.(isActive));\n const onAuthChange = useEffectEvent((authenticated: boolean) => handlers.onAuthChange?.(authenticated));\n\n useEffect(() => {\n const unsubs = [\n socket.onConnect(onConnect),\n socket.onDisconnect(onDisconnect),\n socket.onReconnecting(onReconnecting),\n socket.onLeaderChange(onLeaderChange),\n socket.onError(onError),\n socket.onActive(onActive),\n socket.onInactive(onInactive),\n socket.onVisibilityChange(onVisibilityChange),\n socket.onAuthChange(onAuthChange),\n ];\n return () => unsubs.forEach((u) => u());\n }, [socket]);\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * const message = useSocketEvent('chat:room_123:message');\n * chat.send('message', { text: 'Hello' });\n *\n * @example\n * // Tenant notifications\n * const notifications = useChannel(`tenant:${tenantId}:notifications`);\n * useSocketCallback(`tenant:${tenantId}:notifications:alert`, showToast);\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channelRef = useRef(socket.channel(name, options));\n\n useEffect(() => {\n channelRef.current = socket.channel(name, options);\n return () => channelRef.current.leave();\n }, [socket, name]);\n\n return channelRef.current;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n * useTopics([`user:${userId}:mentions`]);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n useEffect(() => {\n topics.forEach((t) => socket.subscribe(t, options));\n return () => topics.forEach((t) => socket.unsubscribe(t));\n }, [socket, topics.join(',')]);\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n *\n * @example\n * usePush('order.created', {\n * title: (order) => `New Order #${order.id}`,\n * body: (order) => `$${order.total}`,\n * onClick: (order) => navigate(`/orders/${order.id}`),\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n\n useEffect(() => {\n const unsub = socket.push<T>(event, config);\n return unsub;\n }, [socket, event]);\n}\n"],"mappings":";;;;;;AAAA;AAAA,EACE;AAAA,EACA;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,IAAI,gBAAgB,KAAK,OAAO,CAAC;AAEjE,YAAU,MAAM;AACd,WAAO,QAAQ;AACf,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;AAwBO,SAAS,gBAId;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAS,OAAO,eAAe;AAE7E,QAAM,eAAe,eAAe,CAAC,kBAA2B;AAC9D,uBAAmB,aAAa;AAAA,EAClC,CAAC;AAED,YAAU,MAAM;AACd,WAAO,OAAO,aAAa,YAAY;AAAA,EACzC,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,eAAe,eAAe,CAAC,UAAkB;AACrD,WAAO,aAAa,KAAK;AAAA,EAC3B,CAAC;AAED,QAAM,iBAAiB,eAAe,MAAM;AAC1C,WAAO,eAAe;AAAA,EACxB,CAAC;AAED,SAAO,EAAE,iBAAiB,cAAc,eAAe;AACzD;AA2BO,SAAS,eAAkB,OAAe,UAA6C;AAC5F,QAAM,SAAS,mBAAmB;AAClC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,MAAS;AAE3D,QAAM,UAAU,eAAe,CAAC,SAAY;AAC1C,QAAI,UAAU;AACZ,eAAS,IAAI;AAAA,IACf,OAAO;AACL,eAAS,IAAI;AAAA,IACf;AAAA,EACF,CAAC;AAED,YAAU,MAAM;AACd,UAAM,QAAQ,OAAO,GAAG,OAAO,OAAuB;AACtD,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO,WAAW,SAAY;AAChC;AAyBO,SAAS,gBAAmB,OAAe,UAAmC;AACnF,QAAM,SAAS,mBAAmB;AAClC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAc,CAAC,CAAC;AAE1C,QAAM,UAAU,eAAe,CAAC,SAAY;AAC1C,QAAI,UAAU;AACZ,eAAS,IAAI;AAAA,IACf,OAAO;AACL,eAAS,CAAC,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;AAAA,IACpC;AAAA,EACF,CAAC;AAED,YAAU,MAAM;AACd,QAAI,CAAC,SAAU,UAAS,CAAC,CAAC;AAC1B,UAAM,QAAQ,OAAO,GAAG,OAAO,OAAuB;AACtD,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO,WAAW,CAAC,IAAI;AACzB;AAkBO,SAAS,cACd,KACA,cACA,UACyB;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;AACf,eAAW,MAAM;AAAA,EACnB,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;AAqBO,SAAS,kBAAqB,OAAe,UAAmC;AACrF,QAAM,SAAS,mBAAmB;AAElC,QAAM,UAAU,eAAe,CAAC,SAAY;AAC1C,aAAS,IAAI;AAAA,EACf,CAAC;AAED,YAAU,MAAM;AACd,UAAM,QAAQ,OAAO,GAAG,OAAO,OAAuB;AACtD,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,KAAK,CAAC;AACpB;AASO,SAAS,kBAId;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,OAAO,SAAS;AAC3D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAkB,OAAO,OAAO;AAC9D,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAS,OAAO,eAAe;AAE7E,QAAM,OAAO,eAAe,MAAM;AAChC,iBAAa,OAAO,SAAS;AAC7B,eAAW,OAAO,OAAO;AACzB,uBAAmB,OAAO,eAAe;AAAA,EAC3C,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,SAAS,gBAAgB;AAC/C;AAcO,SAAS,mBAAmB,UAAyC;AAC1E,QAAM,SAAS,mBAAmB;AAElC,QAAM,YAAY,eAAe,MAAM,SAAS,YAAY,CAAC;AAC7D,QAAM,eAAe,eAAe,MAAM,SAAS,eAAe,CAAC;AACnE,QAAM,iBAAiB,eAAe,MAAM,SAAS,iBAAiB,CAAC;AACvE,QAAM,iBAAiB,eAAe,CAAC,aAAsB,SAAS,iBAAiB,QAAQ,CAAC;AAChG,QAAM,UAAU,eAAe,CAAC,UAAmB,SAAS,UAAU,KAAK,CAAC;AAC5E,QAAM,WAAW,eAAe,MAAM,SAAS,WAAW,CAAC;AAC3D,QAAM,aAAa,eAAe,MAAM,SAAS,aAAa,CAAC;AAC/D,QAAM,qBAAqB,eAAe,CAAC,aAAsB,SAAS,qBAAqB,QAAQ,CAAC;AACxG,QAAM,eAAe,eAAe,CAAC,kBAA2B,SAAS,eAAe,aAAa,CAAC;AAEtG,YAAU,MAAM;AACd,UAAM,SAAS;AAAA,MACb,OAAO,UAAU,SAAS;AAAA,MAC1B,OAAO,aAAa,YAAY;AAAA,MAChC,OAAO,eAAe,cAAc;AAAA,MACpC,OAAO,eAAe,cAAc;AAAA,MACpC,OAAO,QAAQ,OAAO;AAAA,MACtB,OAAO,SAAS,QAAQ;AAAA,MACxB,OAAO,WAAW,UAAU;AAAA,MAC5B,OAAO,mBAAmB,kBAAkB;AAAA,MAC5C,OAAO,aAAa,YAAY;AAAA,IAClC;AACA,WAAO,MAAM,OAAO,QAAQ,CAAC,MAAM,EAAE,CAAC;AAAA,EACxC,GAAG,CAAC,MAAM,CAAC;AACb;AAeO,SAAS,WAAW,MAAc,SAA8B;AACrE,QAAM,SAAS,mBAAmB;AAClC,QAAM,aAAa,OAAO,OAAO,QAAQ,MAAM,OAAO,CAAC;AAEvD,YAAU,MAAM;AACd,eAAW,UAAU,OAAO,QAAQ,MAAM,OAAO;AACjD,WAAO,MAAM,WAAW,QAAQ,MAAM;AAAA,EACxC,GAAG,CAAC,QAAQ,IAAI,CAAC;AAEjB,SAAO,WAAW;AACpB;AASO,SAAS,UAAU,QAAkB,SAAoC;AAC9E,QAAM,SAAS,mBAAmB;AAElC,YAAU,MAAM;AACd,WAAO,QAAQ,CAAC,MAAM,OAAO,UAAU,GAAG,OAAO,CAAC;AAClD,WAAO,MAAM,OAAO,QAAQ,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAAA,EAC1D,GAAG,CAAC,QAAQ,OAAO,KAAK,GAAG,CAAC,CAAC;AAC/B;AAmBO,SAAS,QACd,OACA,QASM;AACN,QAAM,SAAS,mBAAmB;AAElC,YAAU,MAAM;AACd,UAAM,QAAQ,OAAO,KAAQ,OAAO,MAAM;AAC1C,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,KAAK,CAAC;AACpB;","names":[]}
|
package/dist/sync-react.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
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
|
-
var
|
|
3
|
+
var _chunkRJKAFACHcjs = require('./chunk-RJKAFACH.cjs');
|
|
4
4
|
require('./chunk-PNQIHDJF.cjs');
|
|
5
5
|
|
|
6
6
|
// src/adapters/sync-react.ts
|
|
@@ -13,8 +13,8 @@ require('./chunk-PNQIHDJF.cjs');
|
|
|
13
13
|
|
|
14
14
|
var _react = require('react');
|
|
15
15
|
var TabSyncContext = _react.createContext.call(void 0, null);
|
|
16
|
-
function TabSyncProvider({ channel, children }) {
|
|
17
|
-
const [sync] = _react.useState.call(void 0, () => new (0,
|
|
16
|
+
function TabSyncProvider({ channel, debug, children }) {
|
|
17
|
+
const [sync] = _react.useState.call(void 0, () => new (0, _chunkRJKAFACHcjs.TabSync)(channel, { debug }));
|
|
18
18
|
_react.useEffect.call(void 0, () => {
|
|
19
19
|
return () => sync[Symbol.dispose]();
|
|
20
20
|
}, [sync]);
|
package/dist/sync-react.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/sync-react.cjs","../src/adapters/sync-react.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B,gCAA6B;AAC7B;AACA;ACLA;AACE;AACA;AACA;AACA;AACA;AAEA;AAAA,8BACK;AAKP,IAAM,eAAA,EAAiB,kCAAA,IAAkC,CAAA;
|
|
1
|
+
{"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/sync-react.cjs","../src/adapters/sync-react.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/sync-react.cjs","sourcesContent":[null,"import {\n createContext,\n useContext,\n useEffect,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { TabSync } from '../TabSync';\n\n// ─── Context ─────────────────────────────────────────────\n\nconst TabSyncContext = createContext<TabSync | null>(null);\n\n/**\n * Provider for TabSync — creates instance, auto-disposes on unmount.\n *\n * @example\n * function App() {\n * return (\n * <TabSyncProvider channel=\"my-app\">\n * <Dashboard />\n * </TabSyncProvider>\n * );\n * }\n */\nexport interface TabSyncProviderProps {\n /** BroadcastChannel name (default: \"tab-sync\"). */\n channel?: string;\n /** Enable debug logging. */\n debug?: boolean;\n children: ReactNode;\n}\n\nexport function TabSyncProvider({ channel, debug, children }: TabSyncProviderProps) {\n const [sync] = useState(() => new TabSync(channel, { debug }));\n\n useEffect(() => {\n return () => sync[Symbol.dispose]();\n }, [sync]);\n\n return createElement(TabSyncContext.Provider, { value: sync }, children);\n}\n\n/**\n * Access the TabSync instance from context.\n *\n * @example\n * const sync = useTabSyncContext();\n * sync.set('theme', 'dark');\n */\nexport function useTabSyncContext(): TabSync {\n const ctx = useContext(TabSyncContext);\n if (!ctx) {\n throw new Error('useTabSync must be used within <TabSyncProvider>');\n }\n return ctx;\n}\n\n// ─── Hooks ───────────────────────────────────────────────\n\n/**\n * Two-way state sync across browser tabs — like useState but shared.\n * - Without callback: returns [value, setter] synced across tabs.\n * - With callback: also calls your handler on every change (side effects).\n *\n * @example\n * // Shared state — updates propagate to all tabs\n * const [theme, setTheme] = useTabSync('theme', 'light');\n * <button onClick={() => setTheme('dark')}>Dark mode</button>\n *\n * @example\n * // With side effect callback\n * const [cart, setCart] = useTabSync('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * });\n *\n * @example\n * // Form state synced across tabs\n * const [draft, setDraft] = useTabSync('email-draft', '');\n * <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />\n */\nexport function useTabSync<T>(\n key: string,\n initialValue: T,\n callback?: (value: T) => void,\n): [T, (value: T) => void] {\n const sync = useTabSyncContext();\n const [value, setValue] = useState<T>(() => sync.get<T>(key) ?? initialValue);\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n callback?.(synced);\n });\n\n useEffect(() => {\n return sync.on<T>(key, onSync);\n }, [sync, key]);\n\n const setAndSync = useEffectEvent((newValue: T) => {\n setValue(newValue);\n sync.set(key, newValue);\n });\n\n return [value, setAndSync];\n}\n\n/**\n * Read-only subscription to a synced key. Returns undefined until first set.\n *\n * @example\n * const theme = useTabSyncValue<string>('theme');\n * <div className={theme === 'dark' ? 'dark' : 'light'} />\n */\nexport function useTabSyncValue<T>(key: string): T | undefined {\n const sync = useTabSyncContext();\n const [value, setValue] = useState<T | undefined>(() => sync.get<T>(key));\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n });\n\n useEffect(() => {\n return sync.on<T>(key, onSync);\n }, [sync, key]);\n\n return value;\n}\n\n/**\n * Fire-and-forget listener for a synced key. No state, no return value.\n *\n * @example\n * useTabSyncCallback<string>('theme', (theme) => {\n * document.documentElement.setAttribute('data-theme', theme);\n * });\n *\n * @example\n * useTabSyncCallback<Cart>('cart', (cart) => {\n * analytics.track('cart_updated', { count: cart.items.length });\n * });\n */\nexport function useTabSyncCallback<T>(key: string, callback: (value: T) => void): void {\n const sync = useTabSyncContext();\n\n const handler = useEffectEvent((value: T) => {\n callback(value);\n });\n\n useEffect(() => {\n return sync.on<T>(key, handler);\n }, [sync, key]);\n}\n"]}
|
package/dist/sync-react.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TabSync
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7WBM2C7H.js";
|
|
4
4
|
import "./chunk-B2V5HX77.js";
|
|
5
5
|
|
|
6
6
|
// src/adapters/sync-react.ts
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
createElement
|
|
14
14
|
} from "react";
|
|
15
15
|
var TabSyncContext = createContext(null);
|
|
16
|
-
function TabSyncProvider({ channel, children }) {
|
|
17
|
-
const [sync] = useState(() => new TabSync(channel));
|
|
16
|
+
function TabSyncProvider({ channel, debug, children }) {
|
|
17
|
+
const [sync] = useState(() => new TabSync(channel, { debug }));
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
return () => sync[Symbol.dispose]();
|
|
20
20
|
}, [sync]);
|
package/dist/sync-react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/sync-react.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { TabSync } from '../TabSync';\n\n// ─── Context ─────────────────────────────────────────────\n\nconst TabSyncContext = createContext<TabSync | null>(null);\n\n/**\n * Provider for TabSync — creates instance, auto-disposes on unmount.\n *\n * @example\n * function App() {\n * return (\n * <TabSyncProvider channel=\"my-app\">\n * <Dashboard />\n * </TabSyncProvider>\n * );\n * }\n */\nexport interface TabSyncProviderProps {\n /** BroadcastChannel name (default: \"tab-sync\"). */\n channel?: string;\n children: ReactNode;\n}\n\nexport function TabSyncProvider({ channel, children }: TabSyncProviderProps) {\n const [sync] = useState(() => new TabSync(channel));\n\n useEffect(() => {\n return () => sync[Symbol.dispose]();\n }, [sync]);\n\n return createElement(TabSyncContext.Provider, { value: sync }, children);\n}\n\n/**\n * Access the TabSync instance from context.\n *\n * @example\n * const sync = useTabSyncContext();\n * sync.set('theme', 'dark');\n */\nexport function useTabSyncContext(): TabSync {\n const ctx = useContext(TabSyncContext);\n if (!ctx) {\n throw new Error('useTabSync must be used within <TabSyncProvider>');\n }\n return ctx;\n}\n\n// ─── Hooks ───────────────────────────────────────────────\n\n/**\n * Two-way state sync across browser tabs — like useState but shared.\n * - Without callback: returns [value, setter] synced across tabs.\n * - With callback: also calls your handler on every change (side effects).\n *\n * @example\n * // Shared state — updates propagate to all tabs\n * const [theme, setTheme] = useTabSync('theme', 'light');\n * <button onClick={() => setTheme('dark')}>Dark mode</button>\n *\n * @example\n * // With side effect callback\n * const [cart, setCart] = useTabSync('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * });\n *\n * @example\n * // Form state synced across tabs\n * const [draft, setDraft] = useTabSync('email-draft', '');\n * <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />\n */\nexport function useTabSync<T>(\n key: string,\n initialValue: T,\n callback?: (value: T) => void,\n): [T, (value: T) => void] {\n const sync = useTabSyncContext();\n const [value, setValue] = useState<T>(() => sync.get<T>(key) ?? initialValue);\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n callback?.(synced);\n });\n\n useEffect(() => {\n return sync.on<T>(key, onSync);\n }, [sync, key]);\n\n const setAndSync = useEffectEvent((newValue: T) => {\n setValue(newValue);\n sync.set(key, newValue);\n });\n\n return [value, setAndSync];\n}\n\n/**\n * Read-only subscription to a synced key. Returns undefined until first set.\n *\n * @example\n * const theme = useTabSyncValue<string>('theme');\n * <div className={theme === 'dark' ? 'dark' : 'light'} />\n */\nexport function useTabSyncValue<T>(key: string): T | undefined {\n const sync = useTabSyncContext();\n const [value, setValue] = useState<T | undefined>(() => sync.get<T>(key));\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n });\n\n useEffect(() => {\n return sync.on<T>(key, onSync);\n }, [sync, key]);\n\n return value;\n}\n\n/**\n * Fire-and-forget listener for a synced key. No state, no return value.\n *\n * @example\n * useTabSyncCallback<string>('theme', (theme) => {\n * document.documentElement.setAttribute('data-theme', theme);\n * });\n *\n * @example\n * useTabSyncCallback<Cart>('cart', (cart) => {\n * analytics.track('cart_updated', { count: cart.items.length });\n * });\n */\nexport function useTabSyncCallback<T>(key: string, callback: (value: T) => void): void {\n const sync = useTabSyncContext();\n\n const handler = useEffectEvent((value: T) => {\n callback(value);\n });\n\n useEffect(() => {\n return sync.on<T>(key, handler);\n }, [sync, key]);\n}\n"],"mappings":";;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AAKP,IAAM,iBAAiB,cAA8B,IAAI;
|
|
1
|
+
{"version":3,"sources":["../src/adapters/sync-react.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { TabSync } from '../TabSync';\n\n// ─── Context ─────────────────────────────────────────────\n\nconst TabSyncContext = createContext<TabSync | null>(null);\n\n/**\n * Provider for TabSync — creates instance, auto-disposes on unmount.\n *\n * @example\n * function App() {\n * return (\n * <TabSyncProvider channel=\"my-app\">\n * <Dashboard />\n * </TabSyncProvider>\n * );\n * }\n */\nexport interface TabSyncProviderProps {\n /** BroadcastChannel name (default: \"tab-sync\"). */\n channel?: string;\n /** Enable debug logging. */\n debug?: boolean;\n children: ReactNode;\n}\n\nexport function TabSyncProvider({ channel, debug, children }: TabSyncProviderProps) {\n const [sync] = useState(() => new TabSync(channel, { debug }));\n\n useEffect(() => {\n return () => sync[Symbol.dispose]();\n }, [sync]);\n\n return createElement(TabSyncContext.Provider, { value: sync }, children);\n}\n\n/**\n * Access the TabSync instance from context.\n *\n * @example\n * const sync = useTabSyncContext();\n * sync.set('theme', 'dark');\n */\nexport function useTabSyncContext(): TabSync {\n const ctx = useContext(TabSyncContext);\n if (!ctx) {\n throw new Error('useTabSync must be used within <TabSyncProvider>');\n }\n return ctx;\n}\n\n// ─── Hooks ───────────────────────────────────────────────\n\n/**\n * Two-way state sync across browser tabs — like useState but shared.\n * - Without callback: returns [value, setter] synced across tabs.\n * - With callback: also calls your handler on every change (side effects).\n *\n * @example\n * // Shared state — updates propagate to all tabs\n * const [theme, setTheme] = useTabSync('theme', 'light');\n * <button onClick={() => setTheme('dark')}>Dark mode</button>\n *\n * @example\n * // With side effect callback\n * const [cart, setCart] = useTabSync('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * });\n *\n * @example\n * // Form state synced across tabs\n * const [draft, setDraft] = useTabSync('email-draft', '');\n * <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />\n */\nexport function useTabSync<T>(\n key: string,\n initialValue: T,\n callback?: (value: T) => void,\n): [T, (value: T) => void] {\n const sync = useTabSyncContext();\n const [value, setValue] = useState<T>(() => sync.get<T>(key) ?? initialValue);\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n callback?.(synced);\n });\n\n useEffect(() => {\n return sync.on<T>(key, onSync);\n }, [sync, key]);\n\n const setAndSync = useEffectEvent((newValue: T) => {\n setValue(newValue);\n sync.set(key, newValue);\n });\n\n return [value, setAndSync];\n}\n\n/**\n * Read-only subscription to a synced key. Returns undefined until first set.\n *\n * @example\n * const theme = useTabSyncValue<string>('theme');\n * <div className={theme === 'dark' ? 'dark' : 'light'} />\n */\nexport function useTabSyncValue<T>(key: string): T | undefined {\n const sync = useTabSyncContext();\n const [value, setValue] = useState<T | undefined>(() => sync.get<T>(key));\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n });\n\n useEffect(() => {\n return sync.on<T>(key, onSync);\n }, [sync, key]);\n\n return value;\n}\n\n/**\n * Fire-and-forget listener for a synced key. No state, no return value.\n *\n * @example\n * useTabSyncCallback<string>('theme', (theme) => {\n * document.documentElement.setAttribute('data-theme', theme);\n * });\n *\n * @example\n * useTabSyncCallback<Cart>('cart', (cart) => {\n * analytics.track('cart_updated', { count: cart.items.length });\n * });\n */\nexport function useTabSyncCallback<T>(key: string, callback: (value: T) => void): void {\n const sync = useTabSyncContext();\n\n const handler = useEffectEvent((value: T) => {\n callback(value);\n });\n\n useEffect(() => {\n return sync.on<T>(key, handler);\n }, [sync, key]);\n}\n"],"mappings":";;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AAKP,IAAM,iBAAiB,cAA8B,IAAI;AAsBlD,SAAS,gBAAgB,EAAE,SAAS,OAAO,SAAS,GAAyB;AAClF,QAAM,CAAC,IAAI,IAAI,SAAS,MAAM,IAAI,QAAQ,SAAS,EAAE,MAAM,CAAC,CAAC;AAE7D,YAAU,MAAM;AACd,WAAO,MAAM,KAAK,OAAO,OAAO,EAAE;AAAA,EACpC,GAAG,CAAC,IAAI,CAAC;AAET,SAAO,cAAc,eAAe,UAAU,EAAE,OAAO,KAAK,GAAG,QAAQ;AACzE;AASO,SAAS,oBAA6B;AAC3C,QAAM,MAAM,WAAW,cAAc;AACrC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,SAAO;AACT;AAyBO,SAAS,WACd,KACA,cACA,UACyB;AACzB,QAAM,OAAO,kBAAkB;AAC/B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAY,MAAM,KAAK,IAAO,GAAG,KAAK,YAAY;AAE5E,QAAM,SAAS,eAAe,CAAC,WAAc;AAC3C,aAAS,MAAM;AACf,eAAW,MAAM;AAAA,EACnB,CAAC;AAED,YAAU,MAAM;AACd,WAAO,KAAK,GAAM,KAAK,MAAM;AAAA,EAC/B,GAAG,CAAC,MAAM,GAAG,CAAC;AAEd,QAAM,aAAa,eAAe,CAAC,aAAgB;AACjD,aAAS,QAAQ;AACjB,SAAK,IAAI,KAAK,QAAQ;AAAA,EACxB,CAAC;AAED,SAAO,CAAC,OAAO,UAAU;AAC3B;AASO,SAAS,gBAAmB,KAA4B;AAC7D,QAAM,OAAO,kBAAkB;AAC/B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,MAAM,KAAK,IAAO,GAAG,CAAC;AAExE,QAAM,SAAS,eAAe,CAAC,WAAc;AAC3C,aAAS,MAAM;AAAA,EACjB,CAAC;AAED,YAAU,MAAM;AACd,WAAO,KAAK,GAAM,KAAK,MAAM;AAAA,EAC/B,GAAG,CAAC,MAAM,GAAG,CAAC;AAEd,SAAO;AACT;AAeO,SAAS,mBAAsB,KAAa,UAAoC;AACrF,QAAM,OAAO,kBAAkB;AAE/B,QAAM,UAAU,eAAe,CAAC,UAAa;AAC3C,aAAS,KAAK;AAAA,EAChB,CAAC;AAED,YAAU,MAAM;AACd,WAAO,KAAK,GAAM,KAAK,OAAO;AAAA,EAChC,GAAG,CAAC,MAAM,GAAG,CAAC;AAChB;","names":[]}
|
package/dist/sync-vue.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
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
|
-
var
|
|
3
|
+
var _chunkRJKAFACHcjs = require('./chunk-RJKAFACH.cjs');
|
|
4
4
|
require('./chunk-PNQIHDJF.cjs');
|
|
5
5
|
|
|
6
6
|
// src/adapters/sync-vue.ts
|
|
@@ -12,10 +12,10 @@ require('./chunk-PNQIHDJF.cjs');
|
|
|
12
12
|
|
|
13
13
|
var _vue = require('vue');
|
|
14
14
|
var TabSyncKey = /* @__PURE__ */ Symbol("TabSync");
|
|
15
|
-
function createTabSyncPlugin(channel) {
|
|
15
|
+
function createTabSyncPlugin(channel, options) {
|
|
16
16
|
return {
|
|
17
17
|
install(app) {
|
|
18
|
-
const sync = new (0,
|
|
18
|
+
const sync = new (0, _chunkRJKAFACHcjs.TabSync)(channel, options);
|
|
19
19
|
app.provide(TabSyncKey, sync);
|
|
20
20
|
const originalUnmount = app.unmount.bind(app);
|
|
21
21
|
app.unmount = () => {
|
package/dist/sync-vue.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/sync-vue.cjs","../src/adapters/sync-vue.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B,gCAA6B;AAC7B;AACA;ACLA;AACE;AACA;AACA;AACA;AACA;AAAA,0BAIK;AAKA,IAAM,WAAA,kBAAoC,MAAA,CAAO,SAAS,CAAA;AAS1D,SAAS,mBAAA,CAAoB,OAAA,EAAkB;
|
|
1
|
+
{"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/sync-vue.cjs","../src/adapters/sync-vue.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B,gCAA6B;AAC7B;AACA;ACLA;AACE;AACA;AACA;AACA;AACA;AAAA,0BAIK;AAKA,IAAM,WAAA,kBAAoC,MAAA,CAAO,SAAS,CAAA;AAS1D,SAAS,mBAAA,CAAoB,OAAA,EAAkB,OAAA,EAA+B;AACnF,EAAA,OAAO;AAAA,IACL,OAAA,CAAQ,GAAA,EAAU;AAChB,MAAA,MAAM,KAAA,EAAO,IAAI,8BAAA,CAAQ,OAAA,EAAS,OAAO,CAAA;AACzC,MAAA,GAAA,CAAI,OAAA,CAAQ,UAAA,EAAY,IAAI,CAAA;AAE5B,MAAA,MAAM,gBAAA,EAAkB,GAAA,CAAI,OAAA,CAAQ,IAAA,CAAK,GAAG,CAAA;AAC5C,MAAA,GAAA,CAAI,QAAA,EAAU,CAAA,EAAA,GAAM;AAClB,QAAA,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA,CAAE,CAAA;AACrB,QAAA,eAAA,CAAgB,CAAA;AAAA,MAClB,CAAA;AAAA,IACF;AAAA,EACF,CAAA;AACF;AASO,SAAS,iBAAA,CAAA,EAA6B;AAC3C,EAAA,MAAM,KAAA,EAAO,yBAAA,UAAiB,CAAA;AAC9B,EAAA,GAAA,CAAI,CAAC,IAAA,EAAM;AACT,IAAA,MAAM,IAAI,KAAA,CAAM,+DAA+D,CAAA;AAAA,EACjF;AACA,EAAA,OAAO,IAAA;AACT;AAwBO,SAAS,UAAA,CAAc,GAAA,EAAa,YAAA,EAAiB,QAAA,EAAuC;AACjG,EAAA,MAAM,KAAA,EAAO,iBAAA,CAAkB,CAAA;AAC/B,EAAA,MAAM,MAAA,EAAQ,sBAAA,iBAAO,IAAA,CAAK,GAAA,CAAO,GAAG,CAAA,UAAK,cAAY,CAAA;AAErD,EAAA,MAAM,MAAA,EAAQ,IAAA,CAAK,EAAA,CAAM,GAAA,EAAK,CAAC,CAAA,EAAA,GAAM;AACnC,IAAA,KAAA,CAAM,MAAA,EAAQ,CAAA;AACd,oBAAA,QAAA,wBAAA,CAAW,CAAC,GAAA;AAAA,EACd,CAAC,CAAA;AAED,EAAA,wBAAA;AAAA,IACE,KAAA;AAAA,IACA,CAAC,MAAA,EAAA,GAAW;AACV,MAAA,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,MAAM,CAAA;AAAA,IACtB,CAAA;AAAA,IACA,EAAE,IAAA,EAAM,KAAK;AAAA,EACf,CAAA;AAEA,EAAA,8BAAA,KAAiB,CAAA;AACjB,EAAA,OAAO,KAAA;AACT;AASO,SAAS,eAAA,CAAmB,GAAA,EAAiC;AAClE,EAAA,MAAM,KAAA,EAAO,iBAAA,CAAkB,CAAA;AAC/B,EAAA,MAAM,MAAA,EAAQ,sBAAA,IAAmB,CAAK,GAAA,CAAO,GAAG,CAAC,CAAA;AAEjD,EAAA,MAAM,MAAA,EAAQ,IAAA,CAAK,EAAA,CAAM,GAAA,EAAK,CAAC,CAAA,EAAA,GAAM;AACnC,IAAA,KAAA,CAAM,MAAA,EAAQ,CAAA;AAAA,EAChB,CAAC,CAAA;AAED,EAAA,8BAAA,KAAiB,CAAA;AACjB,EAAA,OAAO,2BAAA,KAAc,CAAA;AACvB;AAUO,SAAS,kBAAA,CAAsB,GAAA,EAAa,QAAA,EAAoC;AACrF,EAAA,MAAM,KAAA,EAAO,iBAAA,CAAkB,CAAA;AAC/B,EAAA,MAAM,MAAA,EAAQ,IAAA,CAAK,EAAA,CAAM,GAAA,EAAK,QAAQ,CAAA;AACtC,EAAA,8BAAA,KAAiB,CAAA;AACnB;AD9DA;AACE;AACA;AACA;AACA;AACA;AACA;AACF,+PAAC","file":"/Users/gwakko/Projects/shared-websocket/dist/sync-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 { TabSync } from '../TabSync';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const TabSyncKey: InjectionKey<TabSync> = Symbol('TabSync');\n\n/**\n * Vue 3 plugin for TabSync.\n *\n * @example\n * const app = createApp(App);\n * app.use(createTabSyncPlugin('my-app'));\n */\nexport function createTabSyncPlugin(channel?: string, options?: { debug?: boolean }) {\n return {\n install(app: App) {\n const sync = new TabSync(channel, options);\n app.provide(TabSyncKey, sync);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n sync[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the TabSync instance from provided context.\n *\n * @example\n * const sync = useTabSyncContext();\n * sync.set('theme', 'dark');\n */\nexport function useTabSyncContext(): TabSync {\n const sync = inject(TabSyncKey);\n if (!sync) {\n throw new Error('useTabSync: TabSync not provided. Did you install the plugin?');\n }\n return sync;\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Two-way reactive state synced across tabs.\n * Mutating the ref broadcasts the change; changes from other tabs update the ref.\n *\n * @example\n * const theme = useTabSync('theme', 'light');\n * theme.value = 'dark'; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useTabSync('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * });\n * cart.value = { items: [...cart.value.items, newItem] };\n *\n * @example\n * // Form draft synced across tabs\n * const draft = useTabSync('email-draft', '');\n * // <textarea v-model=\"draft\" />\n */\nexport function useTabSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const sync = useTabSyncContext();\n const value = ref<T>(sync.get<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = sync.on<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n sync.set(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Read-only subscription to a synced key. Returns undefined until first set.\n *\n * @example\n * const theme = useTabSyncValue<string>('theme');\n * // <div :class=\"theme === 'dark' ? 'dark' : 'light'\" />\n */\nexport function useTabSyncValue<T>(key: string): Ref<T | undefined> {\n const sync = useTabSyncContext();\n const value = ref<T | undefined>(sync.get<T>(key)) as Ref<T | undefined>;\n\n const unsub = sync.on<T>(key, (v) => {\n value.value = v;\n });\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Fire-and-forget listener for a synced key. No ref, no return value.\n *\n * @example\n * useTabSyncCallback<string>('theme', (theme) => {\n * document.documentElement.setAttribute('data-theme', theme);\n * });\n */\nexport function useTabSyncCallback<T>(key: string, callback: (value: T) => void): void {\n const sync = useTabSyncContext();\n const unsub = sync.on<T>(key, callback);\n onUnmounted(unsub);\n}\n"]}
|
package/dist/sync-vue.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TabSync
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7WBM2C7H.js";
|
|
4
4
|
import "./chunk-B2V5HX77.js";
|
|
5
5
|
|
|
6
6
|
// src/adapters/sync-vue.ts
|
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
watch
|
|
13
13
|
} from "vue";
|
|
14
14
|
var TabSyncKey = /* @__PURE__ */ Symbol("TabSync");
|
|
15
|
-
function createTabSyncPlugin(channel) {
|
|
15
|
+
function createTabSyncPlugin(channel, options) {
|
|
16
16
|
return {
|
|
17
17
|
install(app) {
|
|
18
|
-
const sync = new TabSync(channel);
|
|
18
|
+
const sync = new TabSync(channel, options);
|
|
19
19
|
app.provide(TabSyncKey, sync);
|
|
20
20
|
const originalUnmount = app.unmount.bind(app);
|
|
21
21
|
app.unmount = () => {
|
package/dist/sync-vue.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/sync-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 { TabSync } from '../TabSync';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const TabSyncKey: InjectionKey<TabSync> = Symbol('TabSync');\n\n/**\n * Vue 3 plugin for TabSync.\n *\n * @example\n * const app = createApp(App);\n * app.use(createTabSyncPlugin('my-app'));\n */\nexport function createTabSyncPlugin(channel?: string) {\n return {\n install(app: App) {\n const sync = new TabSync(channel);\n app.provide(TabSyncKey, sync);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n sync[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the TabSync instance from provided context.\n *\n * @example\n * const sync = useTabSyncContext();\n * sync.set('theme', 'dark');\n */\nexport function useTabSyncContext(): TabSync {\n const sync = inject(TabSyncKey);\n if (!sync) {\n throw new Error('useTabSync: TabSync not provided. Did you install the plugin?');\n }\n return sync;\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Two-way reactive state synced across tabs.\n * Mutating the ref broadcasts the change; changes from other tabs update the ref.\n *\n * @example\n * const theme = useTabSync('theme', 'light');\n * theme.value = 'dark'; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useTabSync('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * });\n * cart.value = { items: [...cart.value.items, newItem] };\n *\n * @example\n * // Form draft synced across tabs\n * const draft = useTabSync('email-draft', '');\n * // <textarea v-model=\"draft\" />\n */\nexport function useTabSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const sync = useTabSyncContext();\n const value = ref<T>(sync.get<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = sync.on<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n sync.set(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Read-only subscription to a synced key. Returns undefined until first set.\n *\n * @example\n * const theme = useTabSyncValue<string>('theme');\n * // <div :class=\"theme === 'dark' ? 'dark' : 'light'\" />\n */\nexport function useTabSyncValue<T>(key: string): Ref<T | undefined> {\n const sync = useTabSyncContext();\n const value = ref<T | undefined>(sync.get<T>(key)) as Ref<T | undefined>;\n\n const unsub = sync.on<T>(key, (v) => {\n value.value = v;\n });\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Fire-and-forget listener for a synced key. No ref, no return value.\n *\n * @example\n * useTabSyncCallback<string>('theme', (theme) => {\n * document.documentElement.setAttribute('data-theme', theme);\n * });\n */\nexport function useTabSyncCallback<T>(key: string, callback: (value: T) => void): void {\n const sync = useTabSyncContext();\n const unsub = sync.on<T>(key, callback);\n onUnmounted(unsub);\n}\n"],"mappings":";;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAKA,IAAM,aAAoC,uBAAO,SAAS;AAS1D,SAAS,oBAAoB,SAAkB;
|
|
1
|
+
{"version":3,"sources":["../src/adapters/sync-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 { TabSync } from '../TabSync';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const TabSyncKey: InjectionKey<TabSync> = Symbol('TabSync');\n\n/**\n * Vue 3 plugin for TabSync.\n *\n * @example\n * const app = createApp(App);\n * app.use(createTabSyncPlugin('my-app'));\n */\nexport function createTabSyncPlugin(channel?: string, options?: { debug?: boolean }) {\n return {\n install(app: App) {\n const sync = new TabSync(channel, options);\n app.provide(TabSyncKey, sync);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n sync[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the TabSync instance from provided context.\n *\n * @example\n * const sync = useTabSyncContext();\n * sync.set('theme', 'dark');\n */\nexport function useTabSyncContext(): TabSync {\n const sync = inject(TabSyncKey);\n if (!sync) {\n throw new Error('useTabSync: TabSync not provided. Did you install the plugin?');\n }\n return sync;\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Two-way reactive state synced across tabs.\n * Mutating the ref broadcasts the change; changes from other tabs update the ref.\n *\n * @example\n * const theme = useTabSync('theme', 'light');\n * theme.value = 'dark'; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useTabSync('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * });\n * cart.value = { items: [...cart.value.items, newItem] };\n *\n * @example\n * // Form draft synced across tabs\n * const draft = useTabSync('email-draft', '');\n * // <textarea v-model=\"draft\" />\n */\nexport function useTabSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const sync = useTabSyncContext();\n const value = ref<T>(sync.get<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = sync.on<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n sync.set(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Read-only subscription to a synced key. Returns undefined until first set.\n *\n * @example\n * const theme = useTabSyncValue<string>('theme');\n * // <div :class=\"theme === 'dark' ? 'dark' : 'light'\" />\n */\nexport function useTabSyncValue<T>(key: string): Ref<T | undefined> {\n const sync = useTabSyncContext();\n const value = ref<T | undefined>(sync.get<T>(key)) as Ref<T | undefined>;\n\n const unsub = sync.on<T>(key, (v) => {\n value.value = v;\n });\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Fire-and-forget listener for a synced key. No ref, no return value.\n *\n * @example\n * useTabSyncCallback<string>('theme', (theme) => {\n * document.documentElement.setAttribute('data-theme', theme);\n * });\n */\nexport function useTabSyncCallback<T>(key: string, callback: (value: T) => void): void {\n const sync = useTabSyncContext();\n const unsub = sync.on<T>(key, callback);\n onUnmounted(unsub);\n}\n"],"mappings":";;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAKA,IAAM,aAAoC,uBAAO,SAAS;AAS1D,SAAS,oBAAoB,SAAkB,SAA+B;AACnF,SAAO;AAAA,IACL,QAAQ,KAAU;AAChB,YAAM,OAAO,IAAI,QAAQ,SAAS,OAAO;AACzC,UAAI,QAAQ,YAAY,IAAI;AAE5B,YAAM,kBAAkB,IAAI,QAAQ,KAAK,GAAG;AAC5C,UAAI,UAAU,MAAM;AAClB,aAAK,OAAO,OAAO,EAAE;AACrB,wBAAgB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACF;AASO,SAAS,oBAA6B;AAC3C,QAAM,OAAO,OAAO,UAAU;AAC9B,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,MAAM,+DAA+D;AAAA,EACjF;AACA,SAAO;AACT;AAwBO,SAAS,WAAc,KAAa,cAAiB,UAAuC;AACjG,QAAM,OAAO,kBAAkB;AAC/B,QAAM,QAAQ,IAAO,KAAK,IAAO,GAAG,KAAK,YAAY;AAErD,QAAM,QAAQ,KAAK,GAAM,KAAK,CAAC,MAAM;AACnC,UAAM,QAAQ;AACd,eAAW,CAAC;AAAA,EACd,CAAC;AAED;AAAA,IACE;AAAA,IACA,CAAC,WAAW;AACV,WAAK,IAAI,KAAK,MAAM;AAAA,IACtB;AAAA,IACA,EAAE,MAAM,KAAK;AAAA,EACf;AAEA,cAAY,KAAK;AACjB,SAAO;AACT;AASO,SAAS,gBAAmB,KAAiC;AAClE,QAAM,OAAO,kBAAkB;AAC/B,QAAM,QAAQ,IAAmB,KAAK,IAAO,GAAG,CAAC;AAEjD,QAAM,QAAQ,KAAK,GAAM,KAAK,CAAC,MAAM;AACnC,UAAM,QAAQ;AAAA,EAChB,CAAC;AAED,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AAUO,SAAS,mBAAsB,KAAa,UAAoC;AACrF,QAAM,OAAO,kBAAkB;AAC/B,QAAM,QAAQ,KAAK,GAAM,KAAK,QAAQ;AACtC,cAAY,KAAK;AACnB;","names":[]}
|
package/dist/sync.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkRJKAFACHcjs = require('./chunk-RJKAFACH.cjs');
|
|
4
4
|
require('./chunk-PNQIHDJF.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
exports.TabSync =
|
|
7
|
+
exports.TabSync = _chunkRJKAFACHcjs.TabSync;
|
|
8
8
|
//# sourceMappingURL=sync.cjs.map
|
package/dist/sync.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { TabSync } from './TabSync';
|
|
2
|
-
export type { Unsubscribe } from './types';
|
|
2
|
+
export type { Unsubscribe, Logger } from './types';
|
package/dist/sync.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface SharedWebSocketOptions<TEvents extends EventMap = EventMap> {
|
|
|
47
47
|
protocols?: string[];
|
|
48
48
|
reconnect?: boolean;
|
|
49
49
|
reconnectMaxDelay?: number;
|
|
50
|
+
/** Max reconnect attempts before giving up (default: Infinity — retry forever). */
|
|
51
|
+
reconnectMaxRetries?: number;
|
|
50
52
|
heartbeatInterval?: number;
|
|
51
53
|
electionTimeout?: number;
|
|
52
54
|
leaderHeartbeat?: number;
|
package/dist/vue.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
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
|
-
var
|
|
3
|
+
var _chunkOVKB2KLEcjs = require('./chunk-OVKB2KLE.cjs');
|
|
4
4
|
require('./chunk-PNQIHDJF.cjs');
|
|
5
5
|
|
|
6
6
|
// src/adapters/vue.ts
|
|
@@ -15,8 +15,8 @@ var SharedWebSocketKey = /* @__PURE__ */ Symbol("SharedWebSocket");
|
|
|
15
15
|
function createSharedWebSocketPlugin(url, options) {
|
|
16
16
|
return {
|
|
17
17
|
install(app) {
|
|
18
|
-
const socket = new (0,
|
|
19
|
-
socket.connect();
|
|
18
|
+
const socket = new (0, _chunkOVKB2KLEcjs.SharedWebSocket)(url, options);
|
|
19
|
+
void socket.connect();
|
|
20
20
|
app.provide(SharedWebSocketKey, socket);
|
|
21
21
|
const originalUnmount = app.unmount.bind(app);
|
|
22
22
|
app.unmount = () => {
|
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, SocketLifecycleHandlers } 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/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs.\n *\n * @example\n * const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();\n *\n * async function login(email: string, password: string) {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * }\n *\n * @example\n * // In template: <button v-if=\"isAuthenticated\" @click=\"deauthenticate\">Logout</button>\n */\nexport function useSocketAuth(): {\n isAuthenticated: Ref<boolean>;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const unsub = socket.onAuthChange((authenticated: boolean) => {\n isAuthenticated.value = authenticated;\n });\n\n onUnmounted(unsub);\n\n return {\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n authenticate: (token: string) => socket.authenticate(token),\n deauthenticate: () => socket.deauthenticate(),\n };\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 handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n value.value = typed;\n }\n };\n const unsub = socket.on(event, handler);\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 handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n items.value = [...items.value, typed];\n }\n };\n const unsub = socket.on(event, handler);\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: unknown) => {\n callback(data as T);\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 isAuthenticated: Ref<boolean>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n isAuthenticated.value = socket.isAuthenticated;\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 isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => showOfflineBanner(),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n const unsubs: (() => void)[] = [];\n\n if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));\n if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));\n if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));\n if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));\n if (handlers.onError) unsubs.push(socket.onError(handlers.onError));\n if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));\n if (handlers.onInactive) unsubs.push(socket.onInactive(handlers.onInactive));\n if (handlers.onVisibilityChange) unsubs.push(socket.onVisibilityChange(handlers.onVisibilityChange));\n if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * // Listen via useSocketEvent('chat:room_123:message')\n * // Send via chat.send('message', { text: 'Hello' })\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name, options);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n topics.forEach((t) => socket.subscribe(t, options));\n\n onUnmounted(() => {\n topics.forEach((t) => socket.unsubscribe(t));\n });\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n const unsub = socket.push<T>(event, config);\n\n onUnmounted(unsub);\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, SocketLifecycleHandlers } 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 void 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/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs.\n *\n * @example\n * const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();\n *\n * async function login(email: string, password: string) {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * }\n *\n * @example\n * // In template: <button v-if=\"isAuthenticated\" @click=\"deauthenticate\">Logout</button>\n */\nexport function useSocketAuth(): {\n isAuthenticated: Ref<boolean>;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const unsub = socket.onAuthChange((authenticated: boolean) => {\n isAuthenticated.value = authenticated;\n });\n\n onUnmounted(unsub);\n\n return {\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n authenticate: (token: string) => socket.authenticate(token),\n deauthenticate: () => socket.deauthenticate(),\n };\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 handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n value.value = typed;\n }\n };\n const unsub = socket.on(event, handler);\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 handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n items.value = [...items.value, typed];\n }\n };\n const unsub = socket.on(event, handler);\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: unknown) => {\n callback(data as T);\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 isAuthenticated: Ref<boolean>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n isAuthenticated.value = socket.isAuthenticated;\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 isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => showOfflineBanner(),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n const unsubs: (() => void)[] = [];\n\n if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));\n if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));\n if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));\n if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));\n if (handlers.onError) unsubs.push(socket.onError(handlers.onError));\n if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));\n if (handlers.onInactive) unsubs.push(socket.onInactive(handlers.onInactive));\n if (handlers.onVisibilityChange) unsubs.push(socket.onVisibilityChange(handlers.onVisibilityChange));\n if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * // Listen via useSocketEvent('chat:room_123:message')\n * // Send via chat.send('message', { text: 'Hello' })\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name, options);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n topics.forEach((t) => socket.subscribe(t, options));\n\n onUnmounted(() => {\n topics.forEach((t) => socket.unsubscribe(t));\n });\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n const unsub = socket.push<T>(event, config);\n\n onUnmounted(unsub);\n}\n"]}
|
package/dist/vue.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SharedWebSocket
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SQZHBLWT.js";
|
|
4
4
|
import "./chunk-B2V5HX77.js";
|
|
5
5
|
|
|
6
6
|
// src/adapters/vue.ts
|
|
@@ -16,7 +16,7 @@ function createSharedWebSocketPlugin(url, options) {
|
|
|
16
16
|
return {
|
|
17
17
|
install(app) {
|
|
18
18
|
const socket = new SharedWebSocket(url, options);
|
|
19
|
-
socket.connect();
|
|
19
|
+
void socket.connect();
|
|
20
20
|
app.provide(SharedWebSocketKey, socket);
|
|
21
21
|
const originalUnmount = app.unmount.bind(app);
|
|
22
22
|
app.unmount = () => {
|
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, SocketLifecycleHandlers } 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/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs.\n *\n * @example\n * const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();\n *\n * async function login(email: string, password: string) {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * }\n *\n * @example\n * // In template: <button v-if=\"isAuthenticated\" @click=\"deauthenticate\">Logout</button>\n */\nexport function useSocketAuth(): {\n isAuthenticated: Ref<boolean>;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const unsub = socket.onAuthChange((authenticated: boolean) => {\n isAuthenticated.value = authenticated;\n });\n\n onUnmounted(unsub);\n\n return {\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n authenticate: (token: string) => socket.authenticate(token),\n deauthenticate: () => socket.deauthenticate(),\n };\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 handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n value.value = typed;\n }\n };\n const unsub = socket.on(event, handler);\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 handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n items.value = [...items.value, typed];\n }\n };\n const unsub = socket.on(event, handler);\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: unknown) => {\n callback(data as T);\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 isAuthenticated: Ref<boolean>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n isAuthenticated.value = socket.isAuthenticated;\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 isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => showOfflineBanner(),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n const unsubs: (() => void)[] = [];\n\n if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));\n if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));\n if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));\n if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));\n if (handlers.onError) unsubs.push(socket.onError(handlers.onError));\n if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));\n if (handlers.onInactive) unsubs.push(socket.onInactive(handlers.onInactive));\n if (handlers.onVisibilityChange) unsubs.push(socket.onVisibilityChange(handlers.onVisibilityChange));\n if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * // Listen via useSocketEvent('chat:room_123:message')\n * // Send via chat.send('message', { text: 'Hello' })\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name, options);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n topics.forEach((t) => socket.subscribe(t, options));\n\n onUnmounted(() => {\n topics.forEach((t) => socket.unsubscribe(t));\n });\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n const unsub = socket.push<T>(event, config);\n\n onUnmounted(unsub);\n}\n"],"mappings":"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","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, SocketLifecycleHandlers } 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 void 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/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs.\n *\n * @example\n * const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();\n *\n * async function login(email: string, password: string) {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * }\n *\n * @example\n * // In template: <button v-if=\"isAuthenticated\" @click=\"deauthenticate\">Logout</button>\n */\nexport function useSocketAuth(): {\n isAuthenticated: Ref<boolean>;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const unsub = socket.onAuthChange((authenticated: boolean) => {\n isAuthenticated.value = authenticated;\n });\n\n onUnmounted(unsub);\n\n return {\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n authenticate: (token: string) => socket.authenticate(token),\n deauthenticate: () => socket.deauthenticate(),\n };\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 handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n value.value = typed;\n }\n };\n const unsub = socket.on(event, handler);\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 handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n items.value = [...items.value, typed];\n }\n };\n const unsub = socket.on(event, handler);\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: unknown) => {\n callback(data as T);\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 isAuthenticated: Ref<boolean>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n isAuthenticated.value = socket.isAuthenticated;\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 isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => showOfflineBanner(),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n const unsubs: (() => void)[] = [];\n\n if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));\n if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));\n if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));\n if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));\n if (handlers.onError) unsubs.push(socket.onError(handlers.onError));\n if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));\n if (handlers.onInactive) unsubs.push(socket.onInactive(handlers.onInactive));\n if (handlers.onVisibilityChange) unsubs.push(socket.onVisibilityChange(handlers.onVisibilityChange));\n if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * // Listen via useSocketEvent('chat:room_123:message')\n * // Send via chat.send('message', { text: 'Hello' })\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name, options);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n topics.forEach((t) => socket.subscribe(t, options));\n\n onUnmounted(() => {\n topics.forEach((t) => socket.unsubscribe(t));\n });\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n const unsub = socket.push<T>(event, config);\n\n onUnmounted(unsub);\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,WAAK,OAAO,QAAQ;AACpB,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;AAiBO,SAAS,gBAId;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,kBAAkB,IAAI,OAAO,eAAe;AAElD,QAAM,QAAQ,OAAO,aAAa,CAAC,kBAA2B;AAC5D,oBAAgB,QAAQ;AAAA,EAC1B,CAAC;AAED,cAAY,KAAK;AAEjB,SAAO;AAAA,IACL,iBAAiB,SAAS,eAAe;AAAA,IACzC,cAAc,CAAC,UAAkB,OAAO,aAAa,KAAK;AAAA,IAC1D,gBAAgB,MAAM,OAAO,eAAe;AAAA,EAC9C;AACF;AAoBO,SAAS,eAAkB,OAAe,UAAkD;AACjG,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAmB,MAAS;AAE1C,QAAM,UAAU,CAAC,SAAkB;AACjC,UAAM,QAAQ;AACd,QAAI,UAAU;AACZ,eAAS,KAAK;AAAA,IAChB,OAAO;AACL,YAAM,QAAQ;AAAA,IAChB;AAAA,EACF;AACA,QAAM,QAAQ,OAAO,GAAG,OAAO,OAAO;AAEtC,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AAyBO,SAAS,gBAAmB,OAAe,UAAwC;AACxF,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAS,CAAC,CAAC;AAEzB,QAAM,UAAU,CAAC,SAAkB;AACjC,UAAM,QAAQ;AACd,QAAI,UAAU;AACZ,eAAS,KAAK;AAAA,IAChB,OAAO;AACL,YAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,KAAK;AAAA,IACtC;AAAA,EACF;AACA,QAAM,QAAQ,OAAO,GAAG,OAAO,OAAO;AAEtC,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,SAAkB;AAChD,aAAS,IAAS;AAAA,EACpB,CAAC;AAED,cAAY,KAAK;AACnB;AAQO,SAAS,kBAId;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,YAAY,IAAI,OAAO,SAAS;AACtC,QAAM,UAAU,IAAa,OAAO,OAAO;AAC3C,QAAM,kBAAkB,IAAI,OAAO,eAAe;AAElD,QAAM,QAAQ,YAAY,MAAM;AAC9B,cAAU,QAAQ,OAAO;AACzB,YAAQ,QAAQ,OAAO;AACvB,oBAAgB,QAAQ,OAAO;AAAA,EACjC,GAAG,GAAI;AAEP,cAAY,MAAM,cAAc,KAAK,CAAC;AAEtC,SAAO;AAAA,IACL,WAAW,SAAS,SAAS;AAAA,IAC7B,SAAS,SAAS,OAAO;AAAA,IACzB,iBAAiB,SAAS,eAAe;AAAA,EAC3C;AACF;AAcO,SAAS,mBAAmB,UAAyC;AAC1E,QAAM,SAAS,mBAAmB;AAClC,QAAM,SAAyB,CAAC;AAEhC,MAAI,SAAS,UAAW,QAAO,KAAK,OAAO,UAAU,SAAS,SAAS,CAAC;AACxE,MAAI,SAAS,aAAc,QAAO,KAAK,OAAO,aAAa,SAAS,YAAY,CAAC;AACjF,MAAI,SAAS,eAAgB,QAAO,KAAK,OAAO,eAAe,SAAS,cAAc,CAAC;AACvF,MAAI,SAAS,eAAgB,QAAO,KAAK,OAAO,eAAe,SAAS,cAAc,CAAC;AACvF,MAAI,SAAS,QAAS,QAAO,KAAK,OAAO,QAAQ,SAAS,OAAO,CAAC;AAClE,MAAI,SAAS,SAAU,QAAO,KAAK,OAAO,SAAS,SAAS,QAAQ,CAAC;AACrE,MAAI,SAAS,WAAY,QAAO,KAAK,OAAO,WAAW,SAAS,UAAU,CAAC;AAC3E,MAAI,SAAS,mBAAoB,QAAO,KAAK,OAAO,mBAAmB,SAAS,kBAAkB,CAAC;AACnG,MAAI,SAAS,aAAc,QAAO,KAAK,OAAO,aAAa,SAAS,YAAY,CAAC;AAEjF,cAAY,MAAM,OAAO,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;AAC9C;AAUO,SAAS,WAAW,MAAc,SAA8B;AACrE,QAAM,SAAS,mBAAmB;AAClC,QAAM,UAAU,OAAO,QAAQ,MAAM,OAAO;AAE5C,cAAY,MAAM,QAAQ,MAAM,CAAC;AAEjC,SAAO;AACT;AAQO,SAAS,UAAU,QAAkB,SAAoC;AAC9E,QAAM,SAAS,mBAAmB;AAElC,SAAO,QAAQ,CAAC,MAAM,OAAO,UAAU,GAAG,OAAO,CAAC;AAElD,cAAY,MAAM;AAChB,WAAO,QAAQ,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAAA,EAC7C,CAAC;AACH;AAYO,SAAS,QACd,OACA,QASM;AACN,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,OAAO,KAAQ,OAAO,MAAM;AAE1C,cAAY,KAAK;AACnB;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gwakko/shared-websocket",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
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",
|