@gwakko/shared-websocket 0.3.0 → 0.6.2

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/dist/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 _chunkQ4OKSJX7cjs = require('./chunk-Q4OKSJX7.cjs');
3
+ var _chunkUEOFAFLVcjs = require('./chunk-UEOFAFLV.cjs');
4
4
 
5
5
  // src/adapters/react.ts
6
6
 
@@ -10,11 +10,12 @@ var _chunkQ4OKSJX7cjs = require('./chunk-Q4OKSJX7.cjs');
10
10
 
11
11
 
12
12
 
13
+
13
14
  var _react = require('react');
14
15
  var SharedWSContext = _react.createContext.call(void 0, null);
15
16
  function SharedWebSocketProvider({ url, options, children }) {
16
17
  const [socket] = _react.useState.call(void 0, () => {
17
- const ws = new (0, _chunkQ4OKSJX7cjs.SharedWebSocket)(url, options);
18
+ const ws = new (0, _chunkUEOFAFLVcjs.SharedWebSocket)(url, options);
18
19
  ws.connect();
19
20
  return ws;
20
21
  });
@@ -108,6 +109,35 @@ function useSocketStatus() {
108
109
  }, [socket]);
109
110
  return { connected, tabRole };
110
111
  }
112
+ function useSocketLifecycle(handlers) {
113
+ const socket = useSharedWebSocket();
114
+ const onConnect = _react.useEffectEvent.call(void 0, () => _optionalChain([handlers, 'access', _2 => _2.onConnect, 'optionalCall', _3 => _3()]));
115
+ const onDisconnect = _react.useEffectEvent.call(void 0, () => _optionalChain([handlers, 'access', _4 => _4.onDisconnect, 'optionalCall', _5 => _5()]));
116
+ const onReconnecting = _react.useEffectEvent.call(void 0, () => _optionalChain([handlers, 'access', _6 => _6.onReconnecting, 'optionalCall', _7 => _7()]));
117
+ const onLeaderChange = _react.useEffectEvent.call(void 0, (isLeader) => _optionalChain([handlers, 'access', _8 => _8.onLeaderChange, 'optionalCall', _9 => _9(isLeader)]));
118
+ const onError = _react.useEffectEvent.call(void 0, (error) => _optionalChain([handlers, 'access', _10 => _10.onError, 'optionalCall', _11 => _11(error)]));
119
+ _react.useEffect.call(void 0, () => {
120
+ const unsubs = [
121
+ socket.onConnect(onConnect),
122
+ socket.onDisconnect(onDisconnect),
123
+ socket.onReconnecting(onReconnecting),
124
+ socket.onLeaderChange(onLeaderChange),
125
+ socket.onError(onError)
126
+ ];
127
+ return () => unsubs.forEach((u) => u());
128
+ }, [socket]);
129
+ }
130
+ function useChannel(name) {
131
+ const socket = useSharedWebSocket();
132
+ const channelRef = _react.useRef.call(void 0, socket.channel(name));
133
+ _react.useEffect.call(void 0, () => {
134
+ channelRef.current = socket.channel(name);
135
+ return () => channelRef.current.leave();
136
+ }, [socket, name]);
137
+ return channelRef.current;
138
+ }
139
+
140
+
111
141
 
112
142
 
113
143
 
@@ -116,5 +146,5 @@ function useSocketStatus() {
116
146
 
117
147
 
118
148
 
119
- exports.SharedWebSocketProvider = SharedWebSocketProvider; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketCallback = useSocketCallback; exports.useSocketEvent = useSocketEvent; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync;
149
+ exports.SharedWebSocketProvider = SharedWebSocketProvider; exports.useChannel = useChannel; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketCallback = useSocketCallback; exports.useSocketEvent = useSocketEvent; exports.useSocketLifecycle = useSocketLifecycle; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync;
120
150
  //# sourceMappingURL=react.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/react.cjs","../src/adapters/react.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/react.cjs","sourcesContent":[null,"import {\n createContext,\n useContext,\n useEffect,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Context ─────────────────────────────────────────────\n\nconst SharedWSContext = createContext<SharedWebSocket | null>(null);\n\n/**\n * Provider props — pass URL and options as props for flexibility.\n *\n * @example\n * <SharedWebSocketProvider url=\"wss://api.example.com/ws\" options={{ auth: getToken }}>\n * <App />\n * </SharedWebSocketProvider>\n */\nexport interface SharedWebSocketProviderProps {\n url: string;\n options?: SharedWebSocketOptions;\n children: ReactNode;\n}\n\n/**\n * Provider component — creates SharedWebSocket from props, auto-disposes on unmount.\n *\n * @example\n * function App() {\n * return (\n * <SharedWebSocketProvider\n * url=\"wss://api.example.com/ws\"\n * options={{\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }}\n * >\n * <Dashboard />\n * </SharedWebSocketProvider>\n * );\n * }\n */\nexport function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps) {\n const [socket] = useState(() => {\n const ws = new SharedWebSocket(url, options);\n ws.connect();\n return ws;\n });\n\n useEffect(() => {\n return () => {\n socket[Symbol.dispose]();\n };\n }, [socket]);\n\n return createElement(SharedWSContext.Provider, { value: socket }, children);\n}\n\n/**\n * Access the SharedWebSocket instance from context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const ctx = useContext(SharedWSContext);\n if (!ctx) {\n throw new Error('useSharedWebSocket must be used within <SharedWebSocketProvider>');\n }\n return ctx;\n}\n\n// ─── Hooks ───────────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns the latest received value (reactive state).\n * - With callback: calls your handler on each event (stable ref via useEffectEvent).\n *\n * @example\n * // Reactive state — returns latest value\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback — full control, no state\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n *\n * @example\n * // Custom callback with transform — store in your own state\n * const [orders, setOrders] = useState<Order[]>([]);\n * useSocketEvent<Order>('order.created', (order) => {\n * setOrders(prev => [order, ...prev].slice(0, 50)); // keep last 50\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): T | undefined {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T | undefined>(undefined);\n\n const onEvent = useEffectEvent((data: T) => {\n if (callback) {\n callback(data);\n } else {\n setValue(data);\n }\n });\n\n useEffect(() => {\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return callback ? undefined : value;\n}\n\n/**\n * Accumulate WebSocket events into an array.\n * - Without callback: returns accumulated array (reactive state).\n * - With callback: calls your handler on each event, you manage your own state.\n *\n * @example\n * // Default — accumulates all events\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom callback — keep only last 50, transform, filter, etc.\n * const [messages, setMessages] = useState<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * setMessages(prev => [msg, ...prev].slice(0, 50));\n * });\n *\n * @example\n * // Custom callback — filter by type\n * const [errors, setErrors] = useState<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') setErrors(prev => [...prev, entry]);\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): T[] {\n const socket = useSharedWebSocket();\n const [items, setItems] = useState<T[]>([]);\n\n const onEvent = useEffectEvent((data: T) => {\n if (callback) {\n callback(data);\n } else {\n setItems((prev) => [...prev, data]);\n }\n });\n\n useEffect(() => {\n if (!callback) setItems([]);\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return callback ? [] : items;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: returns [value, setter] (like useState but synced).\n * - With callback: calls your handler when any tab updates this key.\n *\n * @example\n * // Default — reactive synced state\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] });\n *\n * @example\n * // Custom callback — side effects on sync\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated', { count: cart.items.length });\n * });\n */\nexport function useSocketSync<T>(\n key: string,\n initialValue: T,\n callback?: (value: T) => void,\n): [T, (value: T) => void] {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T>(() => {\n return socket.getSync<T>(key) ?? initialValue;\n });\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n callback?.(synced);\n });\n\n useEffect(() => {\n const unsub = socket.onSync<T>(key, onSync);\n return unsub;\n }, [socket, key]);\n\n const setAndSync = useEffectEvent((newValue: T) => {\n setValue(newValue);\n socket.sync(key, newValue);\n });\n\n return [value, setAndSync];\n}\n\n/**\n * Subscribe to a WebSocket event with just a callback — no state, no return value.\n * Fire-and-forget: side effects, logging, analytics, sounds, browser notifications.\n * Stable ref via useEffectEvent — callback always sees latest closure values.\n *\n * @example\n * useSocketCallback<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', { id: order.id });\n * });\n *\n * @example\n * // Browser notification only from leader tab\n * useSocketCallback<Notification>('notification', (notif) => {\n * if (ws.tabRole === 'leader' && document.hidden) {\n * new Notification(notif.title, { body: notif.body });\n * }\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const handler = useEffectEvent((data: T) => {\n callback(data);\n });\n\n useEffect(() => {\n const unsub = socket.on(event, handler);\n return unsub;\n }, [socket, event]);\n}\n\n/**\n * Reactive connection status.\n * Uses useEffectEvent to avoid re-creating interval on state change.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: boolean;\n tabRole: TabRole;\n} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole };\n}\n"]}
1
+ {"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/react.cjs","../src/adapters/react.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACA;ACJA;AACE;AACA;AACA;AACA;AACA;AACA;AAEA;AAAA,8BACK;AAMP,IAAM,gBAAA,EAAkB,kCAAA,IAA0C,CAAA;AAkC3D,SAAS,uBAAA,CAAwB,EAAE,GAAA,EAAK,OAAA,EAAS,SAAS,CAAA,EAAiC;AAChG,EAAA,MAAM,CAAC,MAAM,EAAA,EAAI,6BAAA,CAAS,EAAA,GAAM;AAC9B,IAAA,MAAM,GAAA,EAAK,IAAI,sCAAA,CAAgB,GAAA,EAAK,OAAO,CAAA;AAC3C,IAAA,EAAA,CAAG,OAAA,CAAQ,CAAA;AACX,IAAA,OAAO,EAAA;AAAA,EACT,CAAC,CAAA;AAED,EAAA,8BAAA,CAAU,EAAA,GAAM;AACd,IAAA,OAAO,CAAA,EAAA,GAAM;AACX,MAAA,MAAA,CAAO,MAAA,CAAO,OAAO,CAAA,CAAE,CAAA;AAAA,IACzB,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,OAAO,kCAAA,eAAc,CAAgB,QAAA,EAAU,EAAE,KAAA,EAAO,OAAO,CAAA,EAAG,QAAQ,CAAA;AAC5E;AASO,SAAS,kBAAA,CAAA,EAAsC;AACpD,EAAA,MAAM,IAAA,EAAM,+BAAA,eAA0B,CAAA;AACtC,EAAA,GAAA,CAAI,CAAC,GAAA,EAAK;AACR,IAAA,MAAM,IAAI,KAAA,CAAM,kEAAkE,CAAA;AAAA,EACpF;AACA,EAAA,OAAO,GAAA;AACT;AA2BO,SAAS,cAAA,CAAkB,KAAA,EAAe,QAAA,EAA6C;AAC5F,EAAA,MAAM,OAAA,EAAS,kBAAA,CAAmB,CAAA;AAClC,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,EAAA,EAAI,6BAAA,KAAwB,CAAS,CAAA;AAE3D,EAAA,MAAM,QAAA,EAAU,mCAAA,CAAgB,IAAA,EAAA,GAAY;AAC1C,IAAA,GAAA,CAAI,QAAA,EAAU;AACZ,MAAA,QAAA,CAAS,IAAI,CAAA;AAAA,IACf,EAAA,KAAO;AACL,MAAA,QAAA,CAAS,IAAI,CAAA;AAAA,IACf;AAAA,EACF,CAAC,CAAA;AAED,EAAA,8BAAA,CAAU,EAAA,GAAM;AACd,IAAA,MAAM,MAAA,EAAQ,MAAA,CAAO,EAAA,CAAG,KAAA,EAAO,OAAuB,CAAA;AACtD,IAAA,OAAO,KAAA;AAAA,EACT,CAAA,EAAG,CAAC,MAAA,EAAQ,KAAK,CAAC,CAAA;AAElB,EAAA,OAAO,SAAA,EAAW,KAAA,EAAA,EAAY,KAAA;AAChC;AAyBO,SAAS,eAAA,CAAmB,KAAA,EAAe,QAAA,EAAmC;AACnF,EAAA,MAAM,OAAA,EAAS,kBAAA,CAAmB,CAAA;AAClC,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,EAAA,EAAI,6BAAA,CAAe,CAAC,CAAA;AAE1C,EAAA,MAAM,QAAA,EAAU,mCAAA,CAAgB,IAAA,EAAA,GAAY;AAC1C,IAAA,GAAA,CAAI,QAAA,EAAU;AACZ,MAAA,QAAA,CAAS,IAAI,CAAA;AAAA,IACf,EAAA,KAAO;AACL,MAAA,QAAA,CAAS,CAAC,IAAA,EAAA,GAAS,CAAC,GAAG,IAAA,EAAM,IAAI,CAAC,CAAA;AAAA,IACpC;AAAA,EACF,CAAC,CAAA;AAED,EAAA,8BAAA,CAAU,EAAA,GAAM;AACd,IAAA,GAAA,CAAI,CAAC,QAAA,EAAU,QAAA,CAAS,CAAC,CAAC,CAAA;AAC1B,IAAA,MAAM,MAAA,EAAQ,MAAA,CAAO,EAAA,CAAG,KAAA,EAAO,OAAuB,CAAA;AACtD,IAAA,OAAO,KAAA;AAAA,EACT,CAAA,EAAG,CAAC,MAAA,EAAQ,KAAK,CAAC,CAAA;AAElB,EAAA,OAAO,SAAA,EAAW,CAAC,EAAA,EAAI,KAAA;AACzB;AAkBO,SAAS,aAAA,CACd,GAAA,EACA,YAAA,EACA,QAAA,EACyB;AACzB,EAAA,MAAM,OAAA,EAAS,kBAAA,CAAmB,CAAA;AAClC,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,EAAA,EAAI,6BAAA,CAAY,EAAA,GAAM;AAC1C,IAAA,wBAAO,MAAA,CAAO,OAAA,CAAW,GAAG,CAAA,UAAK,cAAA;AAAA,EACnC,CAAC,CAAA;AAED,EAAA,MAAM,OAAA,EAAS,mCAAA,CAAgB,MAAA,EAAA,GAAc;AAC3C,IAAA,QAAA,CAAS,MAAM,CAAA;AACf,oBAAA,QAAA,wBAAA,CAAW,MAAM,GAAA;AAAA,EACnB,CAAC,CAAA;AAED,EAAA,8BAAA,CAAU,EAAA,GAAM;AACd,IAAA,MAAM,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAU,GAAA,EAAK,MAAM,CAAA;AAC1C,IAAA,OAAO,KAAA;AAAA,EACT,CAAA,EAAG,CAAC,MAAA,EAAQ,GAAG,CAAC,CAAA;AAEhB,EAAA,MAAM,WAAA,EAAa,mCAAA,CAAgB,QAAA,EAAA,GAAgB;AACjD,IAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,IAAA,MAAA,CAAO,IAAA,CAAK,GAAA,EAAK,QAAQ,CAAA;AAAA,EAC3B,CAAC,CAAA;AAED,EAAA,OAAO,CAAC,KAAA,EAAO,UAAU,CAAA;AAC3B;AAqBO,SAAS,iBAAA,CAAqB,KAAA,EAAe,QAAA,EAAmC;AACrF,EAAA,MAAM,OAAA,EAAS,kBAAA,CAAmB,CAAA;AAElC,EAAA,MAAM,QAAA,EAAU,mCAAA,CAAgB,IAAA,EAAA,GAAY;AAC1C,IAAA,QAAA,CAAS,IAAI,CAAA;AAAA,EACf,CAAC,CAAA;AAED,EAAA,8BAAA,CAAU,EAAA,GAAM;AACd,IAAA,MAAM,MAAA,EAAQ,MAAA,CAAO,EAAA,CAAG,KAAA,EAAO,OAAuB,CAAA;AACtD,IAAA,OAAO,KAAA;AAAA,EACT,CAAA,EAAG,CAAC,MAAA,EAAQ,KAAK,CAAC,CAAA;AACpB;AASO,SAAS,eAAA,CAAA,EAGd;AACA,EAAA,MAAM,OAAA,EAAS,kBAAA,CAAmB,CAAA;AAClC,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,EAAA,EAAI,6BAAA,MAAS,CAAO,SAAS,CAAA;AAC3D,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,EAAA,EAAI,6BAAA,MAAkB,CAAO,OAAO,CAAA;AAE9D,EAAA,MAAM,KAAA,EAAO,mCAAA,CAAe,EAAA,GAAM;AAChC,IAAA,YAAA,CAAa,MAAA,CAAO,SAAS,CAAA;AAC7B,IAAA,UAAA,CAAW,MAAA,CAAO,OAAO,CAAA;AAAA,EAC3B,CAAC,CAAA;AAED,EAAA,8BAAA,CAAU,EAAA,GAAM;AACd,IAAA,MAAM,SAAA,EAAW,WAAA,CAAY,IAAA,EAAM,GAAI,CAAA;AACvC,IAAA,OAAO,CAAA,EAAA,GAAM,aAAA,CAAc,QAAQ,CAAA;AAAA,EACrC,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,OAAO,EAAE,SAAA,EAAW,QAAQ,CAAA;AAC9B;AAcO,SAAS,kBAAA,CAAmB,QAAA,EAAyC;AAC1E,EAAA,MAAM,OAAA,EAAS,kBAAA,CAAmB,CAAA;AAElC,EAAA,MAAM,UAAA,EAAY,mCAAA,CAAe,EAAA,mBAAM,QAAA,qBAAS,SAAA,0BAAA,CAAY,GAAC,CAAA;AAC7D,EAAA,MAAM,aAAA,EAAe,mCAAA,CAAe,EAAA,mBAAM,QAAA,qBAAS,YAAA,0BAAA,CAAe,GAAC,CAAA;AACnE,EAAA,MAAM,eAAA,EAAiB,mCAAA,CAAe,EAAA,mBAAM,QAAA,qBAAS,cAAA,0BAAA,CAAiB,GAAC,CAAA;AACvE,EAAA,MAAM,eAAA,EAAiB,mCAAA,CAAgB,QAAA,EAAA,mBAAsB,QAAA,qBAAS,cAAA,0BAAA,CAAiB,QAAQ,GAAC,CAAA;AAChG,EAAA,MAAM,QAAA,EAAU,mCAAA,CAAgB,KAAA,EAAA,mBAAmB,QAAA,uBAAS,OAAA,4BAAA,CAAU,KAAK,GAAC,CAAA;AAE5E,EAAA,8BAAA,CAAU,EAAA,GAAM;AACd,IAAA,MAAM,OAAA,EAAS;AAAA,MACb,MAAA,CAAO,SAAA,CAAU,SAAS,CAAA;AAAA,MAC1B,MAAA,CAAO,YAAA,CAAa,YAAY,CAAA;AAAA,MAChC,MAAA,CAAO,cAAA,CAAe,cAAc,CAAA;AAAA,MACpC,MAAA,CAAO,cAAA,CAAe,cAAc,CAAA;AAAA,MACpC,MAAA,CAAO,OAAA,CAAQ,OAAO;AAAA,IACxB,CAAA;AACA,IAAA,OAAO,CAAA,EAAA,GAAM,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA,EAAA,GAAM,CAAA,CAAE,CAAC,CAAA;AAAA,EACxC,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AACb;AAeO,SAAS,UAAA,CAAW,IAAA,EAAc;AACvC,EAAA,MAAM,OAAA,EAAS,kBAAA,CAAmB,CAAA;AAClC,EAAA,MAAM,WAAA,EAAa,2BAAA,MAAO,CAAO,OAAA,CAAQ,IAAI,CAAC,CAAA;AAE9C,EAAA,8BAAA,CAAU,EAAA,GAAM;AACd,IAAA,UAAA,CAAW,QAAA,EAAU,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA;AACxC,IAAA,OAAO,CAAA,EAAA,GAAM,UAAA,CAAW,OAAA,CAAQ,KAAA,CAAM,CAAA;AAAA,EACxC,CAAA,EAAG,CAAC,MAAA,EAAQ,IAAI,CAAC,CAAA;AAEjB,EAAA,OAAO,UAAA,CAAW,OAAA;AACpB;AD/LA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,kZAAC","file":"/Users/gwakko/Projects/shared-websocket/dist/react.cjs","sourcesContent":[null,"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// ─── 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} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole };\n}\n\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\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 ];\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) {\n const socket = useSharedWebSocket();\n const channelRef = useRef(socket.channel(name));\n\n useEffect(() => {\n channelRef.current = socket.channel(name);\n return () => channelRef.current.leave();\n }, [socket, name]);\n\n return channelRef.current;\n}\n"]}
package/dist/react.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  SharedWebSocket
3
- } from "./chunk-JJTAPRPG.js";
3
+ } from "./chunk-4D2ZDCA6.js";
4
4
 
5
5
  // src/adapters/react.ts
6
6
  import {
7
7
  createContext,
8
8
  useContext,
9
9
  useEffect,
10
+ useRef,
10
11
  useState,
11
12
  useEffectEvent,
12
13
  createElement
@@ -108,11 +109,40 @@ function useSocketStatus() {
108
109
  }, [socket]);
109
110
  return { connected, tabRole };
110
111
  }
112
+ function useSocketLifecycle(handlers) {
113
+ const socket = useSharedWebSocket();
114
+ const onConnect = useEffectEvent(() => handlers.onConnect?.());
115
+ const onDisconnect = useEffectEvent(() => handlers.onDisconnect?.());
116
+ const onReconnecting = useEffectEvent(() => handlers.onReconnecting?.());
117
+ const onLeaderChange = useEffectEvent((isLeader) => handlers.onLeaderChange?.(isLeader));
118
+ const onError = useEffectEvent((error) => handlers.onError?.(error));
119
+ useEffect(() => {
120
+ const unsubs = [
121
+ socket.onConnect(onConnect),
122
+ socket.onDisconnect(onDisconnect),
123
+ socket.onReconnecting(onReconnecting),
124
+ socket.onLeaderChange(onLeaderChange),
125
+ socket.onError(onError)
126
+ ];
127
+ return () => unsubs.forEach((u) => u());
128
+ }, [socket]);
129
+ }
130
+ function useChannel(name) {
131
+ const socket = useSharedWebSocket();
132
+ const channelRef = useRef(socket.channel(name));
133
+ useEffect(() => {
134
+ channelRef.current = socket.channel(name);
135
+ return () => channelRef.current.leave();
136
+ }, [socket, name]);
137
+ return channelRef.current;
138
+ }
111
139
  export {
112
140
  SharedWebSocketProvider,
141
+ useChannel,
113
142
  useSharedWebSocket,
114
143
  useSocketCallback,
115
144
  useSocketEvent,
145
+ useSocketLifecycle,
116
146
  useSocketStatus,
117
147
  useSocketStream,
118
148
  useSocketSync
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapters/react.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useState,\n useEffectEvent,\n type ReactNode,\n createElement,\n} from 'react';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Context ─────────────────────────────────────────────\n\nconst SharedWSContext = createContext<SharedWebSocket | null>(null);\n\n/**\n * Provider props — pass URL and options as props for flexibility.\n *\n * @example\n * <SharedWebSocketProvider url=\"wss://api.example.com/ws\" options={{ auth: getToken }}>\n * <App />\n * </SharedWebSocketProvider>\n */\nexport interface SharedWebSocketProviderProps {\n url: string;\n options?: SharedWebSocketOptions;\n children: ReactNode;\n}\n\n/**\n * Provider component — creates SharedWebSocket from props, auto-disposes on unmount.\n *\n * @example\n * function App() {\n * return (\n * <SharedWebSocketProvider\n * url=\"wss://api.example.com/ws\"\n * options={{\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }}\n * >\n * <Dashboard />\n * </SharedWebSocketProvider>\n * );\n * }\n */\nexport function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps) {\n const [socket] = useState(() => {\n const ws = new SharedWebSocket(url, options);\n ws.connect();\n return ws;\n });\n\n useEffect(() => {\n return () => {\n socket[Symbol.dispose]();\n };\n }, [socket]);\n\n return createElement(SharedWSContext.Provider, { value: socket }, children);\n}\n\n/**\n * Access the SharedWebSocket instance from context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const ctx = useContext(SharedWSContext);\n if (!ctx) {\n throw new Error('useSharedWebSocket must be used within <SharedWebSocketProvider>');\n }\n return ctx;\n}\n\n// ─── Hooks ───────────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns the latest received value (reactive state).\n * - With callback: calls your handler on each event (stable ref via useEffectEvent).\n *\n * @example\n * // Reactive state — returns latest value\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback — full control, no state\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n *\n * @example\n * // Custom callback with transform — store in your own state\n * const [orders, setOrders] = useState<Order[]>([]);\n * useSocketEvent<Order>('order.created', (order) => {\n * setOrders(prev => [order, ...prev].slice(0, 50)); // keep last 50\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): T | undefined {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T | undefined>(undefined);\n\n const onEvent = useEffectEvent((data: T) => {\n if (callback) {\n callback(data);\n } else {\n setValue(data);\n }\n });\n\n useEffect(() => {\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return callback ? undefined : value;\n}\n\n/**\n * Accumulate WebSocket events into an array.\n * - Without callback: returns accumulated array (reactive state).\n * - With callback: calls your handler on each event, you manage your own state.\n *\n * @example\n * // Default — accumulates all events\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom callback — keep only last 50, transform, filter, etc.\n * const [messages, setMessages] = useState<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * setMessages(prev => [msg, ...prev].slice(0, 50));\n * });\n *\n * @example\n * // Custom callback — filter by type\n * const [errors, setErrors] = useState<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') setErrors(prev => [...prev, entry]);\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): T[] {\n const socket = useSharedWebSocket();\n const [items, setItems] = useState<T[]>([]);\n\n const onEvent = useEffectEvent((data: T) => {\n if (callback) {\n callback(data);\n } else {\n setItems((prev) => [...prev, data]);\n }\n });\n\n useEffect(() => {\n if (!callback) setItems([]);\n const unsub = socket.on(event, onEvent);\n return unsub;\n }, [socket, event]);\n\n return callback ? [] : items;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: returns [value, setter] (like useState but synced).\n * - With callback: calls your handler when any tab updates this key.\n *\n * @example\n * // Default — reactive synced state\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] });\n *\n * @example\n * // Custom callback — side effects on sync\n * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated', { count: cart.items.length });\n * });\n */\nexport function useSocketSync<T>(\n key: string,\n initialValue: T,\n callback?: (value: T) => void,\n): [T, (value: T) => void] {\n const socket = useSharedWebSocket();\n const [value, setValue] = useState<T>(() => {\n return socket.getSync<T>(key) ?? initialValue;\n });\n\n const onSync = useEffectEvent((synced: T) => {\n setValue(synced);\n callback?.(synced);\n });\n\n useEffect(() => {\n const unsub = socket.onSync<T>(key, onSync);\n return unsub;\n }, [socket, key]);\n\n const setAndSync = useEffectEvent((newValue: T) => {\n setValue(newValue);\n socket.sync(key, newValue);\n });\n\n return [value, setAndSync];\n}\n\n/**\n * Subscribe to a WebSocket event with just a callback — no state, no return value.\n * Fire-and-forget: side effects, logging, analytics, sounds, browser notifications.\n * Stable ref via useEffectEvent — callback always sees latest closure values.\n *\n * @example\n * useSocketCallback<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', { id: order.id });\n * });\n *\n * @example\n * // Browser notification only from leader tab\n * useSocketCallback<Notification>('notification', (notif) => {\n * if (ws.tabRole === 'leader' && document.hidden) {\n * new Notification(notif.title, { body: notif.body });\n * }\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const handler = useEffectEvent((data: T) => {\n callback(data);\n });\n\n useEffect(() => {\n const unsub = socket.on(event, handler);\n return unsub;\n }, [socket, event]);\n}\n\n/**\n * Reactive connection status.\n * Uses useEffectEvent to avoid re-creating interval on state change.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: boolean;\n tabRole: TabRole;\n} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole };\n}\n"],"mappings":"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","names":[]}
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// ─── 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} {\n const socket = useSharedWebSocket();\n const [connected, setConnected] = useState(socket.connected);\n const [tabRole, setTabRole] = useState<TabRole>(socket.tabRole);\n\n const tick = useEffectEvent(() => {\n setConnected(socket.connected);\n setTabRole(socket.tabRole);\n });\n\n useEffect(() => {\n const interval = setInterval(tick, 1000);\n return () => clearInterval(interval);\n }, [socket]);\n\n return { connected, tabRole };\n}\n\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\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 ];\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) {\n const socket = useSharedWebSocket();\n const channelRef = useRef(socket.channel(name));\n\n useEffect(() => {\n channelRef.current = socket.channel(name);\n return () => channelRef.current.leave();\n }, [socket, name]);\n\n return channelRef.current;\n}\n"],"mappings":"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","names":[]}
package/dist/types.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export type SocketState = 'connecting' | 'connected' | 'reconnecting' | 'closed';
2
2
  export type TabRole = 'leader' | 'follower';
3
3
  export type Unsubscribe = () => void;
4
- export type EventHandler = (data: any) => void;
4
+ export type EventHandler<T = unknown> = (data: T) => void;
5
+ /** Type-safe event map. Keys are event names, values are payload types. */
6
+ export type EventMap = Record<string, unknown>;
5
7
  export interface BusMessage {
6
8
  id: string;
7
9
  source: string;
@@ -10,7 +12,38 @@ export interface BusMessage {
10
12
  data: unknown;
11
13
  timestamp: number;
12
14
  }
13
- export interface SharedWebSocketOptions {
15
+ /**
16
+ * Logger interface — inject your own logger (console, pino, winston, Sentry).
17
+ *
18
+ * @example
19
+ * // Default: console
20
+ * { logger: console }
21
+ *
22
+ * @example
23
+ * // Sentry breadcrumbs
24
+ * {
25
+ * logger: {
26
+ * debug: (msg, ...args) => Sentry.addBreadcrumb({ message: msg, data: args, level: 'debug' }),
27
+ * info: (msg, ...args) => Sentry.addBreadcrumb({ message: msg, data: args, level: 'info' }),
28
+ * warn: (msg, ...args) => Sentry.addBreadcrumb({ message: msg, data: args, level: 'warning' }),
29
+ * error: (msg, ...args) => Sentry.captureException(args[0] ?? new Error(msg)),
30
+ * }
31
+ * }
32
+ *
33
+ * @example
34
+ * // Pino
35
+ * import pino from 'pino';
36
+ * { logger: pino({ name: 'shared-ws' }) }
37
+ */
38
+ export interface Logger {
39
+ debug(message: string, ...args: unknown[]): void;
40
+ info(message: string, ...args: unknown[]): void;
41
+ warn(message: string, ...args: unknown[]): void;
42
+ error(message: string, ...args: unknown[]): void;
43
+ }
44
+ /** Middleware function — transform or inspect messages. Return null to drop. */
45
+ export type Middleware<T = unknown> = (message: T) => T | null;
46
+ export interface SharedWebSocketOptions<TEvents extends EventMap = EventMap> {
14
47
  protocols?: string[];
15
48
  reconnect?: boolean;
16
49
  reconnectMaxDelay?: number;
@@ -29,4 +62,42 @@ export interface SharedWebSocketOptions {
29
62
  useWorker?: boolean;
30
63
  /** Custom worker URL (if useWorker is true and you want to provide your own worker file). */
31
64
  workerUrl?: string | URL;
65
+ /** Override event/field names for server protocol compatibility. */
66
+ events?: Partial<EventProtocol>;
67
+ /** Enable debug logging (default: false). */
68
+ debug?: boolean;
69
+ /** Custom logger (default: console). Supports any logger with debug/info/warn/error. */
70
+ logger?: Logger;
71
+ }
72
+ /** Configurable event names and field mappings for server protocol. */
73
+ export interface EventProtocol {
74
+ /** Field name for event type in messages (default: "event"). */
75
+ eventField: string;
76
+ /** Field name for payload in messages (default: "data"). */
77
+ dataField: string;
78
+ /** Event name sent when joining a channel (default: "$channel:join"). */
79
+ channelJoin: string;
80
+ /** Event name sent when leaving a channel (default: "$channel:leave"). */
81
+ channelLeave: string;
82
+ /** Heartbeat payload sent to keep connection alive (default: { type: "ping" }). */
83
+ ping: unknown;
84
+ /** Fallback event name when message has no event field (default: "message"). */
85
+ defaultEvent: string;
86
+ }
87
+ /** Lifecycle event handlers for useSocketLifecycle / onConnect / etc. */
88
+ export interface SocketLifecycleHandlers {
89
+ onConnect?: () => void;
90
+ onDisconnect?: () => void;
91
+ onReconnecting?: () => void;
92
+ onLeaderChange?: (isLeader: boolean) => void;
93
+ onError?: (error: unknown) => void;
94
+ }
95
+ /** Scoped channel handle for private/topic-based subscriptions. */
96
+ export interface Channel {
97
+ readonly name: string;
98
+ on(event: string, handler: EventHandler): Unsubscribe;
99
+ once(event: string, handler: EventHandler): Unsubscribe;
100
+ send(event: string, data: unknown): void;
101
+ stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown>;
102
+ leave(): void;
32
103
  }
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 _chunkQ4OKSJX7cjs = require('./chunk-Q4OKSJX7.cjs');
3
+ var _chunkUEOFAFLVcjs = require('./chunk-UEOFAFLV.cjs');
4
4
 
5
5
  // src/adapters/vue.ts
6
6
 
@@ -14,7 +14,7 @@ var SharedWebSocketKey = /* @__PURE__ */ Symbol("SharedWebSocket");
14
14
  function createSharedWebSocketPlugin(url, options) {
15
15
  return {
16
16
  install(app) {
17
- const socket = new (0, _chunkQ4OKSJX7cjs.SharedWebSocket)(url, options);
17
+ const socket = new (0, _chunkUEOFAFLVcjs.SharedWebSocket)(url, options);
18
18
  socket.connect();
19
19
  app.provide(SharedWebSocketKey, socket);
20
20
  const originalUnmount = app.unmount.bind(app);
@@ -35,26 +35,30 @@ function useSharedWebSocket() {
35
35
  function useSocketEvent(event, callback) {
36
36
  const socket = useSharedWebSocket();
37
37
  const value = _vue.ref.call(void 0, void 0);
38
- const unsub = socket.on(event, (data) => {
38
+ const handler = (data) => {
39
+ const typed = data;
39
40
  if (callback) {
40
- callback(data);
41
+ callback(typed);
41
42
  } else {
42
- value.value = data;
43
+ value.value = typed;
43
44
  }
44
- });
45
+ };
46
+ const unsub = socket.on(event, handler);
45
47
  _vue.onUnmounted.call(void 0, unsub);
46
48
  return _vue.readonly.call(void 0, value);
47
49
  }
48
50
  function useSocketStream(event, callback) {
49
51
  const socket = useSharedWebSocket();
50
52
  const items = _vue.ref.call(void 0, []);
51
- const unsub = socket.on(event, (data) => {
53
+ const handler = (data) => {
54
+ const typed = data;
52
55
  if (callback) {
53
- callback(data);
56
+ callback(typed);
54
57
  } else {
55
- items.value = [...items.value, data];
58
+ items.value = [...items.value, typed];
56
59
  }
57
- });
60
+ };
61
+ const unsub = socket.on(event, handler);
58
62
  _vue.onUnmounted.call(void 0, unsub);
59
63
  return _vue.readonly.call(void 0, items);
60
64
  }
@@ -96,6 +100,24 @@ function useSocketStatus() {
96
100
  tabRole: _vue.readonly.call(void 0, tabRole)
97
101
  };
98
102
  }
103
+ function useSocketLifecycle(handlers) {
104
+ const socket = useSharedWebSocket();
105
+ const unsubs = [];
106
+ if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));
107
+ if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));
108
+ if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));
109
+ if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));
110
+ if (handlers.onError) unsubs.push(socket.onError(handlers.onError));
111
+ _vue.onUnmounted.call(void 0, () => unsubs.forEach((u) => u()));
112
+ }
113
+ function useChannel(name) {
114
+ const socket = useSharedWebSocket();
115
+ const channel = socket.channel(name);
116
+ _vue.onUnmounted.call(void 0, () => channel.leave());
117
+ return channel;
118
+ }
119
+
120
+
99
121
 
100
122
 
101
123
 
@@ -105,5 +127,5 @@ function useSocketStatus() {
105
127
 
106
128
 
107
129
 
108
- exports.SharedWebSocketKey = SharedWebSocketKey; exports.createSharedWebSocketPlugin = createSharedWebSocketPlugin; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketCallback = useSocketCallback; exports.useSocketEvent = useSocketEvent; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync;
130
+ exports.SharedWebSocketKey = SharedWebSocketKey; exports.createSharedWebSocketPlugin = createSharedWebSocketPlugin; exports.useChannel = useChannel; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketCallback = useSocketCallback; exports.useSocketEvent = useSocketEvent; exports.useSocketLifecycle = useSocketLifecycle; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync;
109
131
  //# sourceMappingURL=vue.cjs.map
package/dist/vue.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","../src/adapters/vue.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","sourcesContent":[null,"import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns reactive ref with latest value.\n * - With callback: calls your handler on each event.\n *\n * @example\n * // Reactive state\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const unsub = socket.on(event, (data: T) => {\n if (callback) {\n callback(data);\n } else {\n value.value = data;\n }\n });\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events.\n * - Without callback: returns reactive array.\n * - With callback: calls your handler — manage your own state.\n *\n * @example\n * // Default accumulation\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom — keep last 50\n * const messages = ref<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * messages.value = [msg, ...messages.value].slice(0, 50);\n * });\n *\n * @example\n * // Custom — filter by type\n * const errors = ref<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') errors.value = [...errors.value, entry];\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const unsub = socket.on(event, (data: T) => {\n if (callback) {\n callback(data);\n } else {\n items.value = [...items.value, data];\n }\n });\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: reactive ref synced across tabs.\n * - With callback: called when any tab updates this key — side effects.\n *\n * @example\n * // Reactive two-way sync\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated');\n * });\n */\nexport function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Fire-and-forget event handler — no state, no ref.\n *\n * @example\n * useSocketCallback<Notification>('notification', (n) => {\n * showToast(n.title);\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const unsub = socket.on(event, (data: T) => {\n callback(data);\n });\n\n onUnmounted(unsub);\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n };\n}\n"]}
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// ─── 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} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n };\n}\n\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\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) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n"]}
package/dist/vue.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SharedWebSocket
3
- } from "./chunk-JJTAPRPG.js";
3
+ } from "./chunk-4D2ZDCA6.js";
4
4
 
5
5
  // src/adapters/vue.ts
6
6
  import {
@@ -35,26 +35,30 @@ function useSharedWebSocket() {
35
35
  function useSocketEvent(event, callback) {
36
36
  const socket = useSharedWebSocket();
37
37
  const value = ref(void 0);
38
- const unsub = socket.on(event, (data) => {
38
+ const handler = (data) => {
39
+ const typed = data;
39
40
  if (callback) {
40
- callback(data);
41
+ callback(typed);
41
42
  } else {
42
- value.value = data;
43
+ value.value = typed;
43
44
  }
44
- });
45
+ };
46
+ const unsub = socket.on(event, handler);
45
47
  onUnmounted(unsub);
46
48
  return readonly(value);
47
49
  }
48
50
  function useSocketStream(event, callback) {
49
51
  const socket = useSharedWebSocket();
50
52
  const items = ref([]);
51
- const unsub = socket.on(event, (data) => {
53
+ const handler = (data) => {
54
+ const typed = data;
52
55
  if (callback) {
53
- callback(data);
56
+ callback(typed);
54
57
  } else {
55
- items.value = [...items.value, data];
58
+ items.value = [...items.value, typed];
56
59
  }
57
- });
60
+ };
61
+ const unsub = socket.on(event, handler);
58
62
  onUnmounted(unsub);
59
63
  return readonly(items);
60
64
  }
@@ -96,12 +100,30 @@ function useSocketStatus() {
96
100
  tabRole: readonly(tabRole)
97
101
  };
98
102
  }
103
+ function useSocketLifecycle(handlers) {
104
+ const socket = useSharedWebSocket();
105
+ const unsubs = [];
106
+ if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));
107
+ if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));
108
+ if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));
109
+ if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));
110
+ if (handlers.onError) unsubs.push(socket.onError(handlers.onError));
111
+ onUnmounted(() => unsubs.forEach((u) => u()));
112
+ }
113
+ function useChannel(name) {
114
+ const socket = useSharedWebSocket();
115
+ const channel = socket.channel(name);
116
+ onUnmounted(() => channel.leave());
117
+ return channel;
118
+ }
99
119
  export {
100
120
  SharedWebSocketKey,
101
121
  createSharedWebSocketPlugin,
122
+ useChannel,
102
123
  useSharedWebSocket,
103
124
  useSocketCallback,
104
125
  useSocketEvent,
126
+ useSocketLifecycle,
105
127
  useSocketStatus,
106
128
  useSocketStream,
107
129
  useSocketSync
package/dist/vue.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapters/vue.ts"],"sourcesContent":["import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns reactive ref with latest value.\n * - With callback: calls your handler on each event.\n *\n * @example\n * // Reactive state\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const unsub = socket.on(event, (data: T) => {\n if (callback) {\n callback(data);\n } else {\n value.value = data;\n }\n });\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events.\n * - Without callback: returns reactive array.\n * - With callback: calls your handler — manage your own state.\n *\n * @example\n * // Default accumulation\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom — keep last 50\n * const messages = ref<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * messages.value = [msg, ...messages.value].slice(0, 50);\n * });\n *\n * @example\n * // Custom — filter by type\n * const errors = ref<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') errors.value = [...errors.value, entry];\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const unsub = socket.on(event, (data: T) => {\n if (callback) {\n callback(data);\n } else {\n items.value = [...items.value, data];\n }\n });\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: reactive ref synced across tabs.\n * - With callback: called when any tab updates this key — side effects.\n *\n * @example\n * // Reactive two-way sync\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated');\n * });\n */\nexport function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Fire-and-forget event handler — no state, no ref.\n *\n * @example\n * useSocketCallback<Notification>('notification', (n) => {\n * showToast(n.title);\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const unsub = socket.on(event, (data: T) => {\n callback(data);\n });\n\n onUnmounted(unsub);\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n };\n}\n"],"mappings":";;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAMA,IAAM,qBAAoD,uBAAO,iBAAiB;AAYlF,SAAS,4BAA4B,KAAa,SAAkC;AACzF,SAAO;AAAA,IACL,QAAQ,KAAU;AAChB,YAAM,SAAS,IAAI,gBAAgB,KAAK,OAAO;AAC/C,aAAO,QAAQ;AACf,UAAI,QAAQ,oBAAoB,MAAM;AAEtC,YAAM,kBAAkB,IAAI,QAAQ,KAAK,GAAG;AAC5C,UAAI,UAAU,MAAM;AAClB,eAAO,OAAO,OAAO,EAAE;AACvB,wBAAgB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACF;AASO,SAAS,qBAAsC;AACpD,QAAM,SAAS,OAAO,kBAAkB;AACxC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,+EAA+E;AAAA,EACjG;AACA,SAAO;AACT;AAoBO,SAAS,eAAkB,OAAe,UAAkD;AACjG,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAmB,MAAS;AAE1C,QAAM,QAAQ,OAAO,GAAG,OAAO,CAAC,SAAY;AAC1C,QAAI,UAAU;AACZ,eAAS,IAAI;AAAA,IACf,OAAO;AACL,YAAM,QAAQ;AAAA,IAChB;AAAA,EACF,CAAC;AAED,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AAyBO,SAAS,gBAAmB,OAAe,UAAwC;AACxF,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAS,CAAC,CAAC;AAEzB,QAAM,QAAQ,OAAO,GAAG,OAAO,CAAC,SAAY;AAC1C,QAAI,UAAU;AACZ,eAAS,IAAI;AAAA,IACf,OAAO;AACL,YAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,IAAI;AAAA,IACrC;AAAA,EACF,CAAC;AAED,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AAmBO,SAAS,cAAiB,KAAa,cAAiB,UAAuC;AACpG,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAO,OAAO,QAAW,GAAG,KAAK,YAAY;AAE3D,QAAM,QAAQ,OAAO,OAAU,KAAK,CAAC,MAAM;AACzC,UAAM,QAAQ;AACd,eAAW,CAAC;AAAA,EACd,CAAC;AAED;AAAA,IACE;AAAA,IACA,CAAC,WAAW;AACV,aAAO,KAAK,KAAK,MAAM;AAAA,IACzB;AAAA,IACA,EAAE,MAAM,KAAK;AAAA,EACf;AAEA,cAAY,KAAK;AACjB,SAAO;AACT;AAUO,SAAS,kBAAqB,OAAe,UAAmC;AACrF,QAAM,SAAS,mBAAmB;AAElC,QAAM,QAAQ,OAAO,GAAG,OAAO,CAAC,SAAY;AAC1C,aAAS,IAAI;AAAA,EACf,CAAC;AAED,cAAY,KAAK;AACnB;AAQO,SAAS,kBAGd;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,YAAY,IAAI,OAAO,SAAS;AACtC,QAAM,UAAU,IAAa,OAAO,OAAO;AAE3C,QAAM,QAAQ,YAAY,MAAM;AAC9B,cAAU,QAAQ,OAAO;AACzB,YAAQ,QAAQ,OAAO;AAAA,EACzB,GAAG,GAAI;AAEP,cAAY,MAAM,cAAc,KAAK,CAAC;AAEtC,SAAO;AAAA,IACL,WAAW,SAAS,SAAS;AAAA,IAC7B,SAAS,SAAS,OAAO;AAAA,EAC3B;AACF;","names":[]}
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// ─── 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} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n };\n}\n\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\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) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gwakko/shared-websocket",
3
- "version": "0.3.0",
3
+ "version": "0.6.2",
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",