@gwakko/shared-websocket 0.12.2 → 0.13.0

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.
Files changed (56) hide show
  1. package/README.md +2 -1
  2. package/dist/SharedSocket.d.ts +8 -1
  3. package/dist/SharedWebSocket.d.ts +22 -0
  4. package/dist/TabSync.d.ts +14 -2
  5. package/dist/WorkerSocket.d.ts +9 -1
  6. package/dist/adapters/react.d.ts +23 -0
  7. package/dist/adapters/sync-react.d.ts +3 -1
  8. package/dist/adapters/sync-vue.d.ts +3 -1
  9. package/dist/adapters/vue.d.ts +23 -0
  10. package/dist/{chunk-RM27CYKT.js → chunk-7WBM2C7H.js} +15 -2
  11. package/dist/chunk-7WBM2C7H.js.map +1 -0
  12. package/dist/{chunk-FZIIMO67.js → chunk-IK4HLA3K.js} +119 -14
  13. package/dist/chunk-IK4HLA3K.js.map +1 -0
  14. package/dist/{chunk-ET3YHQ7V.cjs → chunk-RJKAFACH.cjs} +16 -3
  15. package/dist/chunk-RJKAFACH.cjs.map +1 -0
  16. package/dist/{chunk-ADGLL3J2.cjs → chunk-RKVYLJTQ.cjs} +133 -28
  17. package/dist/chunk-RKVYLJTQ.cjs.map +1 -0
  18. package/dist/index.cjs +4 -4
  19. package/dist/index.js +2 -2
  20. package/dist/react.cjs +31 -13
  21. package/dist/react.cjs.map +1 -1
  22. package/dist/react.js +24 -6
  23. package/dist/react.js.map +1 -1
  24. package/dist/sync-react.cjs +3 -3
  25. package/dist/sync-react.cjs.map +1 -1
  26. package/dist/sync-react.js +3 -3
  27. package/dist/sync-react.js.map +1 -1
  28. package/dist/sync-vue.cjs +3 -3
  29. package/dist/sync-vue.cjs.map +1 -1
  30. package/dist/sync-vue.js +3 -3
  31. package/dist/sync-vue.js.map +1 -1
  32. package/dist/sync.cjs +2 -2
  33. package/dist/sync.d.ts +1 -1
  34. package/dist/sync.js +1 -1
  35. package/dist/types.d.ts +3 -1
  36. package/dist/vue.cjs +26 -4
  37. package/dist/vue.cjs.map +1 -1
  38. package/dist/vue.js +24 -2
  39. package/dist/vue.js.map +1 -1
  40. package/dist/worker/socket.worker.d.ts +6 -2
  41. package/package.json +1 -1
  42. package/src/SharedSocket.ts +27 -3
  43. package/src/SharedWebSocket.ts +56 -3
  44. package/src/TabSync.ts +23 -2
  45. package/src/WorkerSocket.ts +54 -7
  46. package/src/adapters/react.ts +49 -5
  47. package/src/adapters/sync-react.ts +4 -2
  48. package/src/adapters/sync-vue.ts +2 -2
  49. package/src/adapters/vue.ts +48 -1
  50. package/src/sync.ts +1 -1
  51. package/src/types.ts +3 -1
  52. package/src/worker/socket.worker.ts +37 -2
  53. package/dist/chunk-ADGLL3J2.cjs.map +0 -1
  54. package/dist/chunk-ET3YHQ7V.cjs.map +0 -1
  55. package/dist/chunk-FZIIMO67.js.map +0 -1
  56. package/dist/chunk-RM27CYKT.js.map +0 -1
@@ -1 +1 @@
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 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"]}
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"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TabSync
3
- } from "./chunk-RM27CYKT.js";
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]);
@@ -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;AAoBlD,SAAS,gBAAgB,EAAE,SAAS,SAAS,GAAyB;AAC3E,QAAM,CAAC,IAAI,IAAI,SAAS,MAAM,IAAI,QAAQ,OAAO,CAAC;AAElD,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":[]}
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 _chunkET3YHQ7Vcjs = require('./chunk-ET3YHQ7V.cjs');
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, _chunkET3YHQ7Vcjs.TabSync)(channel);
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 = () => {
@@ -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;AACpD,EAAA,OAAO;AAAA,IACL,OAAA,CAAQ,GAAA,EAAU;AAChB,MAAA,MAAM,KAAA,EAAO,IAAI,8BAAA,CAAQ,OAAO,CAAA;AAChC,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) {\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"]}
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-RM27CYKT.js";
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 = () => {
@@ -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;AACpD,SAAO;AAAA,IACL,QAAQ,KAAU;AAChB,YAAM,OAAO,IAAI,QAAQ,OAAO;AAChC,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":[]}
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 _chunkET3YHQ7Vcjs = require('./chunk-ET3YHQ7V.cjs');
3
+ var _chunkRJKAFACHcjs = require('./chunk-RJKAFACH.cjs');
4
4
  require('./chunk-PNQIHDJF.cjs');
5
5
 
6
6
 
7
- exports.TabSync = _chunkET3YHQ7Vcjs.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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TabSync
3
- } from "./chunk-RM27CYKT.js";
3
+ } from "./chunk-7WBM2C7H.js";
4
4
  import "./chunk-B2V5HX77.js";
5
5
  export {
6
6
  TabSync
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type SocketState = 'connecting' | 'connected' | 'reconnecting' | 'closed';
1
+ export type SocketState = 'connecting' | 'connected' | 'reconnecting' | 'closed' | 'failed';
2
2
  export type TabRole = 'leader' | 'follower';
3
3
  export type Unsubscribe = () => void;
4
4
  export type EventHandler<T = unknown> = (data: T) => void;
@@ -127,6 +127,8 @@ export interface SocketLifecycleHandlers {
127
127
  onConnect?: () => void;
128
128
  onDisconnect?: () => void;
129
129
  onReconnecting?: () => void;
130
+ /** Called when auto-reconnect gives up after exhausting reconnectMaxRetries. */
131
+ onReconnectFailed?: () => void;
130
132
  onLeaderChange?: (isLeader: boolean) => void;
131
133
  onError?: (error: unknown) => void;
132
134
  /** Called when this tab becomes visible/focused. */
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 _chunkADGLL3J2cjs = require('./chunk-ADGLL3J2.cjs');
3
+ var _chunkRKVYLJTQcjs = require('./chunk-RKVYLJTQ.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, _chunkADGLL3J2cjs.SharedWebSocket)(url, options);
19
- socket.connect();
18
+ const socket = new (0, _chunkRKVYLJTQcjs.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 = () => {
@@ -123,6 +123,7 @@ function useSocketLifecycle(handlers) {
123
123
  if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));
124
124
  if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));
125
125
  if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));
126
+ if (handlers.onReconnectFailed) unsubs.push(socket.onReconnectFailed(handlers.onReconnectFailed));
126
127
  if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));
127
128
  if (handlers.onError) unsubs.push(socket.onError(handlers.onError));
128
129
  if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));
@@ -131,6 +132,26 @@ function useSocketLifecycle(handlers) {
131
132
  if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));
132
133
  _vue.onUnmounted.call(void 0, () => unsubs.forEach((u) => u()));
133
134
  }
135
+ function useSocketReconnect() {
136
+ const socket = useSharedWebSocket();
137
+ const hasFailed = _vue.ref.call(void 0, false);
138
+ const unsubs = [
139
+ socket.onReconnectFailed(() => {
140
+ hasFailed.value = true;
141
+ }),
142
+ socket.onConnect(() => {
143
+ hasFailed.value = false;
144
+ })
145
+ ];
146
+ _vue.onUnmounted.call(void 0, () => unsubs.forEach((u) => u()));
147
+ return {
148
+ hasFailed: _vue.readonly.call(void 0, hasFailed),
149
+ reconnect: () => {
150
+ hasFailed.value = false;
151
+ socket.reconnect();
152
+ }
153
+ };
154
+ }
134
155
  function useChannel(name, options) {
135
156
  const socket = useSharedWebSocket();
136
157
  const channel = socket.channel(name, options);
@@ -163,5 +184,6 @@ function usePush(event, config) {
163
184
 
164
185
 
165
186
 
166
- exports.SharedWebSocketKey = SharedWebSocketKey; exports.createSharedWebSocketPlugin = createSharedWebSocketPlugin; exports.useChannel = useChannel; exports.usePush = usePush; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketAuth = useSocketAuth; exports.useSocketCallback = useSocketCallback; exports.useSocketEvent = useSocketEvent; exports.useSocketLifecycle = useSocketLifecycle; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync; exports.useTopics = useTopics;
187
+
188
+ exports.SharedWebSocketKey = SharedWebSocketKey; exports.createSharedWebSocketPlugin = createSharedWebSocketPlugin; exports.useChannel = useChannel; exports.usePush = usePush; exports.useSharedWebSocket = useSharedWebSocket; exports.useSocketAuth = useSocketAuth; exports.useSocketCallback = useSocketCallback; exports.useSocketEvent = useSocketEvent; exports.useSocketLifecycle = useSocketLifecycle; exports.useSocketReconnect = useSocketReconnect; exports.useSocketStatus = useSocketStatus; exports.useSocketStream = useSocketStream; exports.useSocketSync = useSocketSync; exports.useTopics = useTopics;
167
189
  //# 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, 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.onReconnectFailed) unsubs.push(socket.onReconnectFailed(handlers.onReconnectFailed));\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 * Reactive reconnect state with a manual `reconnect` action. Use this to\n * power a \"Reconnect\" snackbar/banner after auto-reconnect gives up.\n *\n * `hasFailed` flips to `true` once `reconnectMaxRetries` are exhausted, and\n * back to `false` once the connection succeeds or the user calls `reconnect()`.\n *\n * @example\n * <script setup>\n * const { hasFailed, reconnect } = useSocketReconnect();\n * </script>\n *\n * <template>\n * <div v-if=\"hasFailed\" class=\"snackbar\">\n * Connection lost.\n * <button @click=\"reconnect\">Reconnect</button>\n * </div>\n * </template>\n */\nexport function useSocketReconnect(): {\n hasFailed: Ref<boolean>;\n reconnect: () => void;\n} {\n const socket = useSharedWebSocket();\n const hasFailed = ref(false);\n\n const unsubs = [\n socket.onReconnectFailed(() => {\n hasFailed.value = true;\n }),\n socket.onConnect(() => {\n hasFailed.value = false;\n }),\n ];\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n\n return {\n hasFailed: readonly(hasFailed) as Ref<boolean>,\n reconnect: () => {\n hasFailed.value = false;\n socket.reconnect();\n },\n };\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-FZIIMO67.js";
3
+ } from "./chunk-IK4HLA3K.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 = () => {
@@ -123,6 +123,7 @@ function useSocketLifecycle(handlers) {
123
123
  if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));
124
124
  if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));
125
125
  if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));
126
+ if (handlers.onReconnectFailed) unsubs.push(socket.onReconnectFailed(handlers.onReconnectFailed));
126
127
  if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));
127
128
  if (handlers.onError) unsubs.push(socket.onError(handlers.onError));
128
129
  if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));
@@ -131,6 +132,26 @@ function useSocketLifecycle(handlers) {
131
132
  if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));
132
133
  onUnmounted(() => unsubs.forEach((u) => u()));
133
134
  }
135
+ function useSocketReconnect() {
136
+ const socket = useSharedWebSocket();
137
+ const hasFailed = ref(false);
138
+ const unsubs = [
139
+ socket.onReconnectFailed(() => {
140
+ hasFailed.value = true;
141
+ }),
142
+ socket.onConnect(() => {
143
+ hasFailed.value = false;
144
+ })
145
+ ];
146
+ onUnmounted(() => unsubs.forEach((u) => u()));
147
+ return {
148
+ hasFailed: readonly(hasFailed),
149
+ reconnect: () => {
150
+ hasFailed.value = false;
151
+ socket.reconnect();
152
+ }
153
+ };
154
+ }
134
155
  function useChannel(name, options) {
135
156
  const socket = useSharedWebSocket();
136
157
  const channel = socket.channel(name, options);
@@ -159,6 +180,7 @@ export {
159
180
  useSocketCallback,
160
181
  useSocketEvent,
161
182
  useSocketLifecycle,
183
+ useSocketReconnect,
162
184
  useSocketStatus,
163
185
  useSocketStream,
164
186
  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, 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.onReconnectFailed) unsubs.push(socket.onReconnectFailed(handlers.onReconnectFailed));\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 * Reactive reconnect state with a manual `reconnect` action. Use this to\n * power a \"Reconnect\" snackbar/banner after auto-reconnect gives up.\n *\n * `hasFailed` flips to `true` once `reconnectMaxRetries` are exhausted, and\n * back to `false` once the connection succeeds or the user calls `reconnect()`.\n *\n * @example\n * <script setup>\n * const { hasFailed, reconnect } = useSocketReconnect();\n * </script>\n *\n * <template>\n * <div v-if=\"hasFailed\" class=\"snackbar\">\n * Connection lost.\n * <button @click=\"reconnect\">Reconnect</button>\n * </div>\n * </template>\n */\nexport function useSocketReconnect(): {\n hasFailed: Ref<boolean>;\n reconnect: () => void;\n} {\n const socket = useSharedWebSocket();\n const hasFailed = ref(false);\n\n const unsubs = [\n socket.onReconnectFailed(() => {\n hasFailed.value = true;\n }),\n socket.onConnect(() => {\n hasFailed.value = false;\n }),\n ];\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n\n return {\n hasFailed: readonly(hasFailed) as Ref<boolean>,\n reconnect: () => {\n hasFailed.value = false;\n socket.reconnect();\n },\n };\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":[]}
@@ -15,14 +15,15 @@
15
15
  * { type: 'error', message: string }
16
16
  * { type: 'state', state: SocketState }
17
17
  */
18
- type SocketState = 'connecting' | 'connected' | 'reconnecting' | 'closed';
18
+ type SocketState = 'connecting' | 'connected' | 'reconnecting' | 'closed' | 'failed';
19
19
  interface WorkerCommand {
20
- type: 'connect' | 'send' | 'disconnect';
20
+ type: 'connect' | 'send' | 'disconnect' | 'reconnect';
21
21
  url?: string;
22
22
  protocols?: string[];
23
23
  data?: unknown;
24
24
  reconnect?: boolean;
25
25
  reconnectMaxDelay?: number;
26
+ reconnectMaxRetries?: number;
26
27
  heartbeatInterval?: number;
27
28
  bufferSize?: number;
28
29
  }
@@ -36,9 +37,11 @@ declare let currentUrl: string;
36
37
  declare let currentProtocols: string[];
37
38
  declare let shouldReconnect: boolean;
38
39
  declare let maxDelay: number;
40
+ declare let maxRetries: number;
39
41
  declare let heartbeatInterval: number;
40
42
  declare let maxBuffer: number;
41
43
  declare let backoffDelay: number;
44
+ declare let reconnectAttempts: number;
42
45
  declare function setState(s: SocketState): void;
43
46
  declare function connect(url: string, protocols: string[]): void;
44
47
  declare function doConnect(): void;
@@ -48,4 +51,5 @@ declare function flushBuffer(): void;
48
51
  declare function startHeartbeat(): void;
49
52
  declare function stopHeartbeat(): void;
50
53
  declare function scheduleReconnect(): void;
54
+ declare function manualReconnect(): void;
51
55
  declare function clearReconnect(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gwakko/shared-websocket",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
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",
@@ -99,7 +99,7 @@ export class SharedSocket implements Disposable {
99
99
  this.ws.onclose = () => {
100
100
  this.stopHeartbeat();
101
101
  if (!this.disposed && this.opts.reconnect) {
102
- this.reconnect();
102
+ this.scheduleReconnect();
103
103
  } else {
104
104
  this.setState('closed');
105
105
  }
@@ -148,11 +148,35 @@ export class SharedSocket implements Disposable {
148
148
  return () => this.onStateChangeFns.delete(fn);
149
149
  }
150
150
 
151
- private reconnect(): void {
151
+ /**
152
+ * Manually trigger a reconnect. Resets the retry counter and clears any
153
+ * scheduled backoff so the next attempt happens immediately. Use after
154
+ * `state === 'failed'` to let the user retry, or any time to force a
155
+ * fresh connection.
156
+ */
157
+ reconnect(): void {
158
+ if (this.disposed) return;
159
+ this.clearReconnect();
160
+ this.reconnectAttempts = 0;
161
+
162
+ if (this.ws) {
163
+ this.ws.onclose = null;
164
+ this.ws.onmessage = null;
165
+ this.ws.onerror = null;
166
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
167
+ this.ws.close(1000, 'manual reconnect');
168
+ }
169
+ this.ws = null;
170
+ }
171
+
172
+ void this.connect();
173
+ }
174
+
175
+ private scheduleReconnect(): void {
152
176
  this.reconnectAttempts++;
153
177
 
154
178
  if (this.reconnectAttempts > this.opts.reconnectMaxRetries) {
155
- this.setState('closed');
179
+ this.setState('failed');
156
180
  return;
157
181
  }
158
182