@gwakko/shared-websocket 0.12.2 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/TabSync.d.ts +14 -2
  2. package/dist/WorkerSocket.d.ts +7 -1
  3. package/dist/adapters/sync-react.d.ts +3 -1
  4. package/dist/adapters/sync-vue.d.ts +3 -1
  5. package/dist/{chunk-RM27CYKT.js → chunk-7WBM2C7H.js} +15 -2
  6. package/dist/chunk-7WBM2C7H.js.map +1 -0
  7. package/dist/{chunk-ADGLL3J2.cjs → chunk-OVKB2KLE.cjs} +38 -11
  8. package/dist/chunk-OVKB2KLE.cjs.map +1 -0
  9. package/dist/{chunk-ET3YHQ7V.cjs → chunk-RJKAFACH.cjs} +16 -3
  10. package/dist/chunk-RJKAFACH.cjs.map +1 -0
  11. package/dist/{chunk-FZIIMO67.js → chunk-SQZHBLWT.js} +38 -11
  12. package/dist/chunk-SQZHBLWT.js.map +1 -0
  13. package/dist/index.cjs +4 -4
  14. package/dist/index.js +2 -2
  15. package/dist/react.cjs +3 -6
  16. package/dist/react.cjs.map +1 -1
  17. package/dist/react.js +3 -6
  18. package/dist/react.js.map +1 -1
  19. package/dist/sync-react.cjs +3 -3
  20. package/dist/sync-react.cjs.map +1 -1
  21. package/dist/sync-react.js +3 -3
  22. package/dist/sync-react.js.map +1 -1
  23. package/dist/sync-vue.cjs +3 -3
  24. package/dist/sync-vue.cjs.map +1 -1
  25. package/dist/sync-vue.js +3 -3
  26. package/dist/sync-vue.js.map +1 -1
  27. package/dist/sync.cjs +2 -2
  28. package/dist/sync.d.ts +1 -1
  29. package/dist/sync.js +1 -1
  30. package/dist/vue.cjs +3 -3
  31. package/dist/vue.cjs.map +1 -1
  32. package/dist/vue.js +2 -2
  33. package/dist/vue.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/SharedWebSocket.ts +5 -2
  36. package/src/TabSync.ts +23 -2
  37. package/src/WorkerSocket.ts +41 -7
  38. package/src/adapters/react.ts +2 -5
  39. package/src/adapters/sync-react.ts +4 -2
  40. package/src/adapters/sync-vue.ts +2 -2
  41. package/src/adapters/vue.ts +1 -1
  42. package/src/sync.ts +1 -1
  43. package/dist/chunk-ADGLL3J2.cjs.map +0 -1
  44. package/dist/chunk-ET3YHQ7V.cjs.map +0 -1
  45. package/dist/chunk-FZIIMO67.js.map +0 -1
  46. package/dist/chunk-RM27CYKT.js.map +0 -1
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/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 _chunkOVKB2KLEcjs = require('./chunk-OVKB2KLE.cjs');
4
4
  require('./chunk-PNQIHDJF.cjs');
5
5
 
6
6
  // src/adapters/vue.ts
@@ -15,8 +15,8 @@ var SharedWebSocketKey = /* @__PURE__ */ Symbol("SharedWebSocket");
15
15
  function createSharedWebSocketPlugin(url, options) {
16
16
  return {
17
17
  install(app) {
18
- const socket = new (0, _chunkADGLL3J2cjs.SharedWebSocket)(url, options);
19
- socket.connect();
18
+ const socket = new (0, _chunkOVKB2KLEcjs.SharedWebSocket)(url, options);
19
+ void socket.connect();
20
20
  app.provide(SharedWebSocketKey, socket);
21
21
  const originalUnmount = app.unmount.bind(app);
22
22
  app.unmount = () => {
package/dist/vue.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","../src/adapters/vue.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","sourcesContent":[null,"import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs.\n *\n * @example\n * const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();\n *\n * async function login(email: string, password: string) {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * }\n *\n * @example\n * // In template: <button v-if=\"isAuthenticated\" @click=\"deauthenticate\">Logout</button>\n */\nexport function useSocketAuth(): {\n isAuthenticated: Ref<boolean>;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const unsub = socket.onAuthChange((authenticated: boolean) => {\n isAuthenticated.value = authenticated;\n });\n\n onUnmounted(unsub);\n\n return {\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n authenticate: (token: string) => socket.authenticate(token),\n deauthenticate: () => socket.deauthenticate(),\n };\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns reactive ref with latest value.\n * - With callback: calls your handler on each event.\n *\n * @example\n * // Reactive state\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n value.value = typed;\n }\n };\n const unsub = socket.on(event, handler);\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events.\n * - Without callback: returns reactive array.\n * - With callback: calls your handler — manage your own state.\n *\n * @example\n * // Default accumulation\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom — keep last 50\n * const messages = ref<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * messages.value = [msg, ...messages.value].slice(0, 50);\n * });\n *\n * @example\n * // Custom — filter by type\n * const errors = ref<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') errors.value = [...errors.value, entry];\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n items.value = [...items.value, typed];\n }\n };\n const unsub = socket.on(event, handler);\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: reactive ref synced across tabs.\n * - With callback: called when any tab updates this key — side effects.\n *\n * @example\n * // Reactive two-way sync\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated');\n * });\n */\nexport function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Fire-and-forget event handler — no state, no ref.\n *\n * @example\n * useSocketCallback<Notification>('notification', (n) => {\n * showToast(n.title);\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const unsub = socket.on(event, (data: unknown) => {\n callback(data as T);\n });\n\n onUnmounted(unsub);\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n isAuthenticated: Ref<boolean>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n isAuthenticated.value = socket.isAuthenticated;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => showOfflineBanner(),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n const unsubs: (() => void)[] = [];\n\n if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));\n if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));\n if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));\n if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));\n if (handlers.onError) unsubs.push(socket.onError(handlers.onError));\n if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));\n if (handlers.onInactive) unsubs.push(socket.onInactive(handlers.onInactive));\n if (handlers.onVisibilityChange) unsubs.push(socket.onVisibilityChange(handlers.onVisibilityChange));\n if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * // Listen via useSocketEvent('chat:room_123:message')\n * // Send via chat.send('message', { text: 'Hello' })\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name, options);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n topics.forEach((t) => socket.subscribe(t, options));\n\n onUnmounted(() => {\n topics.forEach((t) => socket.unsubscribe(t));\n });\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n const unsub = socket.push<T>(event, config);\n\n onUnmounted(unsub);\n}\n"]}
1
+ {"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","../src/adapters/vue.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/vue.cjs","sourcesContent":[null,"import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n void socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs.\n *\n * @example\n * const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();\n *\n * async function login(email: string, password: string) {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * }\n *\n * @example\n * // In template: <button v-if=\"isAuthenticated\" @click=\"deauthenticate\">Logout</button>\n */\nexport function useSocketAuth(): {\n isAuthenticated: Ref<boolean>;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const unsub = socket.onAuthChange((authenticated: boolean) => {\n isAuthenticated.value = authenticated;\n });\n\n onUnmounted(unsub);\n\n return {\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n authenticate: (token: string) => socket.authenticate(token),\n deauthenticate: () => socket.deauthenticate(),\n };\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns reactive ref with latest value.\n * - With callback: calls your handler on each event.\n *\n * @example\n * // Reactive state\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n value.value = typed;\n }\n };\n const unsub = socket.on(event, handler);\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events.\n * - Without callback: returns reactive array.\n * - With callback: calls your handler — manage your own state.\n *\n * @example\n * // Default accumulation\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom — keep last 50\n * const messages = ref<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * messages.value = [msg, ...messages.value].slice(0, 50);\n * });\n *\n * @example\n * // Custom — filter by type\n * const errors = ref<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') errors.value = [...errors.value, entry];\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n items.value = [...items.value, typed];\n }\n };\n const unsub = socket.on(event, handler);\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: reactive ref synced across tabs.\n * - With callback: called when any tab updates this key — side effects.\n *\n * @example\n * // Reactive two-way sync\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated');\n * });\n */\nexport function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Fire-and-forget event handler — no state, no ref.\n *\n * @example\n * useSocketCallback<Notification>('notification', (n) => {\n * showToast(n.title);\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const unsub = socket.on(event, (data: unknown) => {\n callback(data as T);\n });\n\n onUnmounted(unsub);\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n isAuthenticated: Ref<boolean>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n isAuthenticated.value = socket.isAuthenticated;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => showOfflineBanner(),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n const unsubs: (() => void)[] = [];\n\n if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));\n if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));\n if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));\n if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));\n if (handlers.onError) unsubs.push(socket.onError(handlers.onError));\n if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));\n if (handlers.onInactive) unsubs.push(socket.onInactive(handlers.onInactive));\n if (handlers.onVisibilityChange) unsubs.push(socket.onVisibilityChange(handlers.onVisibilityChange));\n if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * // Listen via useSocketEvent('chat:room_123:message')\n * // Send via chat.send('message', { text: 'Hello' })\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name, options);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n topics.forEach((t) => socket.subscribe(t, options));\n\n onUnmounted(() => {\n topics.forEach((t) => socket.unsubscribe(t));\n });\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n const unsub = socket.push<T>(event, config);\n\n onUnmounted(unsub);\n}\n"]}
package/dist/vue.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SharedWebSocket
3
- } from "./chunk-FZIIMO67.js";
3
+ } from "./chunk-SQZHBLWT.js";
4
4
  import "./chunk-B2V5HX77.js";
5
5
 
6
6
  // src/adapters/vue.ts
@@ -16,7 +16,7 @@ function createSharedWebSocketPlugin(url, options) {
16
16
  return {
17
17
  install(app) {
18
18
  const socket = new SharedWebSocket(url, options);
19
- socket.connect();
19
+ void socket.connect();
20
20
  app.provide(SharedWebSocketKey, socket);
21
21
  const originalUnmount = app.unmount.bind(app);
22
22
  app.unmount = () => {
package/dist/vue.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapters/vue.ts"],"sourcesContent":["import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs.\n *\n * @example\n * const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();\n *\n * async function login(email: string, password: string) {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * }\n *\n * @example\n * // In template: <button v-if=\"isAuthenticated\" @click=\"deauthenticate\">Logout</button>\n */\nexport function useSocketAuth(): {\n isAuthenticated: Ref<boolean>;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const unsub = socket.onAuthChange((authenticated: boolean) => {\n isAuthenticated.value = authenticated;\n });\n\n onUnmounted(unsub);\n\n return {\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n authenticate: (token: string) => socket.authenticate(token),\n deauthenticate: () => socket.deauthenticate(),\n };\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns reactive ref with latest value.\n * - With callback: calls your handler on each event.\n *\n * @example\n * // Reactive state\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n value.value = typed;\n }\n };\n const unsub = socket.on(event, handler);\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events.\n * - Without callback: returns reactive array.\n * - With callback: calls your handler — manage your own state.\n *\n * @example\n * // Default accumulation\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom — keep last 50\n * const messages = ref<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * messages.value = [msg, ...messages.value].slice(0, 50);\n * });\n *\n * @example\n * // Custom — filter by type\n * const errors = ref<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') errors.value = [...errors.value, entry];\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n items.value = [...items.value, typed];\n }\n };\n const unsub = socket.on(event, handler);\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: reactive ref synced across tabs.\n * - With callback: called when any tab updates this key — side effects.\n *\n * @example\n * // Reactive two-way sync\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated');\n * });\n */\nexport function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Fire-and-forget event handler — no state, no ref.\n *\n * @example\n * useSocketCallback<Notification>('notification', (n) => {\n * showToast(n.title);\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const unsub = socket.on(event, (data: unknown) => {\n callback(data as T);\n });\n\n onUnmounted(unsub);\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n isAuthenticated: Ref<boolean>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n isAuthenticated.value = socket.isAuthenticated;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => showOfflineBanner(),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n const unsubs: (() => void)[] = [];\n\n if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));\n if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));\n if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));\n if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));\n if (handlers.onError) unsubs.push(socket.onError(handlers.onError));\n if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));\n if (handlers.onInactive) unsubs.push(socket.onInactive(handlers.onInactive));\n if (handlers.onVisibilityChange) unsubs.push(socket.onVisibilityChange(handlers.onVisibilityChange));\n if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * // Listen via useSocketEvent('chat:room_123:message')\n * // Send via chat.send('message', { text: 'Hello' })\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name, options);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n topics.forEach((t) => socket.subscribe(t, options));\n\n onUnmounted(() => {\n topics.forEach((t) => socket.unsubscribe(t));\n });\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n const unsub = socket.push<T>(event, config);\n\n onUnmounted(unsub);\n}\n"],"mappings":";;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAMA,IAAM,qBAAoD,uBAAO,iBAAiB;AAYlF,SAAS,4BAA4B,KAAa,SAAkC;AACzF,SAAO;AAAA,IACL,QAAQ,KAAU;AAChB,YAAM,SAAS,IAAI,gBAAgB,KAAK,OAAO;AAC/C,aAAO,QAAQ;AACf,UAAI,QAAQ,oBAAoB,MAAM;AAEtC,YAAM,kBAAkB,IAAI,QAAQ,KAAK,GAAG;AAC5C,UAAI,UAAU,MAAM;AAClB,eAAO,OAAO,OAAO,EAAE;AACvB,wBAAgB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACF;AASO,SAAS,qBAAsC;AACpD,QAAM,SAAS,OAAO,kBAAkB;AACxC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,+EAA+E;AAAA,EACjG;AACA,SAAO;AACT;AAiBO,SAAS,gBAId;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,kBAAkB,IAAI,OAAO,eAAe;AAElD,QAAM,QAAQ,OAAO,aAAa,CAAC,kBAA2B;AAC5D,oBAAgB,QAAQ;AAAA,EAC1B,CAAC;AAED,cAAY,KAAK;AAEjB,SAAO;AAAA,IACL,iBAAiB,SAAS,eAAe;AAAA,IACzC,cAAc,CAAC,UAAkB,OAAO,aAAa,KAAK;AAAA,IAC1D,gBAAgB,MAAM,OAAO,eAAe;AAAA,EAC9C;AACF;AAoBO,SAAS,eAAkB,OAAe,UAAkD;AACjG,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAmB,MAAS;AAE1C,QAAM,UAAU,CAAC,SAAkB;AACjC,UAAM,QAAQ;AACd,QAAI,UAAU;AACZ,eAAS,KAAK;AAAA,IAChB,OAAO;AACL,YAAM,QAAQ;AAAA,IAChB;AAAA,EACF;AACA,QAAM,QAAQ,OAAO,GAAG,OAAO,OAAO;AAEtC,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AAyBO,SAAS,gBAAmB,OAAe,UAAwC;AACxF,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAS,CAAC,CAAC;AAEzB,QAAM,UAAU,CAAC,SAAkB;AACjC,UAAM,QAAQ;AACd,QAAI,UAAU;AACZ,eAAS,KAAK;AAAA,IAChB,OAAO;AACL,YAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,KAAK;AAAA,IACtC;AAAA,EACF;AACA,QAAM,QAAQ,OAAO,GAAG,OAAO,OAAO;AAEtC,cAAY,KAAK;AACjB,SAAO,SAAS,KAAK;AACvB;AAmBO,SAAS,cAAiB,KAAa,cAAiB,UAAuC;AACpG,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,IAAO,OAAO,QAAW,GAAG,KAAK,YAAY;AAE3D,QAAM,QAAQ,OAAO,OAAU,KAAK,CAAC,MAAM;AACzC,UAAM,QAAQ;AACd,eAAW,CAAC;AAAA,EACd,CAAC;AAED;AAAA,IACE;AAAA,IACA,CAAC,WAAW;AACV,aAAO,KAAK,KAAK,MAAM;AAAA,IACzB;AAAA,IACA,EAAE,MAAM,KAAK;AAAA,EACf;AAEA,cAAY,KAAK;AACjB,SAAO;AACT;AAUO,SAAS,kBAAqB,OAAe,UAAmC;AACrF,QAAM,SAAS,mBAAmB;AAElC,QAAM,QAAQ,OAAO,GAAG,OAAO,CAAC,SAAkB;AAChD,aAAS,IAAS;AAAA,EACpB,CAAC;AAED,cAAY,KAAK;AACnB;AAQO,SAAS,kBAId;AACA,QAAM,SAAS,mBAAmB;AAClC,QAAM,YAAY,IAAI,OAAO,SAAS;AACtC,QAAM,UAAU,IAAa,OAAO,OAAO;AAC3C,QAAM,kBAAkB,IAAI,OAAO,eAAe;AAElD,QAAM,QAAQ,YAAY,MAAM;AAC9B,cAAU,QAAQ,OAAO;AACzB,YAAQ,QAAQ,OAAO;AACvB,oBAAgB,QAAQ,OAAO;AAAA,EACjC,GAAG,GAAI;AAEP,cAAY,MAAM,cAAc,KAAK,CAAC;AAEtC,SAAO;AAAA,IACL,WAAW,SAAS,SAAS;AAAA,IAC7B,SAAS,SAAS,OAAO;AAAA,IACzB,iBAAiB,SAAS,eAAe;AAAA,EAC3C;AACF;AAcO,SAAS,mBAAmB,UAAyC;AAC1E,QAAM,SAAS,mBAAmB;AAClC,QAAM,SAAyB,CAAC;AAEhC,MAAI,SAAS,UAAW,QAAO,KAAK,OAAO,UAAU,SAAS,SAAS,CAAC;AACxE,MAAI,SAAS,aAAc,QAAO,KAAK,OAAO,aAAa,SAAS,YAAY,CAAC;AACjF,MAAI,SAAS,eAAgB,QAAO,KAAK,OAAO,eAAe,SAAS,cAAc,CAAC;AACvF,MAAI,SAAS,eAAgB,QAAO,KAAK,OAAO,eAAe,SAAS,cAAc,CAAC;AACvF,MAAI,SAAS,QAAS,QAAO,KAAK,OAAO,QAAQ,SAAS,OAAO,CAAC;AAClE,MAAI,SAAS,SAAU,QAAO,KAAK,OAAO,SAAS,SAAS,QAAQ,CAAC;AACrE,MAAI,SAAS,WAAY,QAAO,KAAK,OAAO,WAAW,SAAS,UAAU,CAAC;AAC3E,MAAI,SAAS,mBAAoB,QAAO,KAAK,OAAO,mBAAmB,SAAS,kBAAkB,CAAC;AACnG,MAAI,SAAS,aAAc,QAAO,KAAK,OAAO,aAAa,SAAS,YAAY,CAAC;AAEjF,cAAY,MAAM,OAAO,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;AAC9C;AAUO,SAAS,WAAW,MAAc,SAA8B;AACrE,QAAM,SAAS,mBAAmB;AAClC,QAAM,UAAU,OAAO,QAAQ,MAAM,OAAO;AAE5C,cAAY,MAAM,QAAQ,MAAM,CAAC;AAEjC,SAAO;AACT;AAQO,SAAS,UAAU,QAAkB,SAAoC;AAC9E,QAAM,SAAS,mBAAmB;AAElC,SAAO,QAAQ,CAAC,MAAM,OAAO,UAAU,GAAG,OAAO,CAAC;AAElD,cAAY,MAAM;AAChB,WAAO,QAAQ,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAAA,EAC7C,CAAC;AACH;AAYO,SAAS,QACd,OACA,QASM;AACN,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ,OAAO,KAAQ,OAAO,MAAM;AAE1C,cAAY,KAAK;AACnB;","names":[]}
1
+ {"version":3,"sources":["../src/adapters/vue.ts"],"sourcesContent":["import {\n ref,\n onUnmounted,\n inject,\n readonly,\n watch,\n type Ref,\n type InjectionKey,\n type App,\n} from 'vue';\nimport { SharedWebSocket } from '../SharedWebSocket';\nimport type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers } from '../types';\n\n// ─── Plugin ──────────────────────────────────────────────\n\nexport const SharedWebSocketKey: InjectionKey<SharedWebSocket> = Symbol('SharedWebSocket');\n\n/**\n * Vue 3 plugin for SharedWebSocket.\n *\n * @example\n * const app = createApp(App);\n * app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {\n * auth: () => localStorage.getItem('token')!,\n * useWorker: true,\n * }));\n */\nexport function createSharedWebSocketPlugin(url: string, options?: SharedWebSocketOptions) {\n return {\n install(app: App) {\n const socket = new SharedWebSocket(url, options);\n void socket.connect();\n app.provide(SharedWebSocketKey, socket);\n\n const originalUnmount = app.unmount.bind(app);\n app.unmount = () => {\n socket[Symbol.dispose]();\n originalUnmount();\n };\n },\n };\n}\n\n/**\n * Access the SharedWebSocket instance from provided context.\n *\n * @example\n * const ws = useSharedWebSocket();\n * ws.send('chat.message', { text: 'Hello' });\n */\nexport function useSharedWebSocket(): SharedWebSocket {\n const socket = inject(SharedWebSocketKey);\n if (!socket) {\n throw new Error('useSharedWebSocket: SharedWebSocket not provided. Did you install the plugin?');\n }\n return socket;\n}\n\n/**\n * Reactive auth state with authenticate/deauthenticate actions.\n * Syncs across all tabs.\n *\n * @example\n * const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();\n *\n * async function login(email: string, password: string) {\n * const { token } = await api.login(email, password);\n * authenticate(token);\n * }\n *\n * @example\n * // In template: <button v-if=\"isAuthenticated\" @click=\"deauthenticate\">Logout</button>\n */\nexport function useSocketAuth(): {\n isAuthenticated: Ref<boolean>;\n authenticate: (token: string) => void;\n deauthenticate: () => void;\n} {\n const socket = useSharedWebSocket();\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const unsub = socket.onAuthChange((authenticated: boolean) => {\n isAuthenticated.value = authenticated;\n });\n\n onUnmounted(unsub);\n\n return {\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n authenticate: (token: string) => socket.authenticate(token),\n deauthenticate: () => socket.deauthenticate(),\n };\n}\n\n// ─── Composables ─────────────────────────────────────────\n\n/**\n * Subscribe to a WebSocket event.\n * - Without callback: returns reactive ref with latest value.\n * - With callback: calls your handler on each event.\n *\n * @example\n * // Reactive state\n * const order = useSocketEvent<Order>('order.created');\n *\n * @example\n * // Custom callback\n * useSocketEvent<Order>('order.created', (order) => {\n * playSound('new-order');\n * analytics.track('order_received', order);\n * });\n */\nexport function useSocketEvent<T>(event: string, callback?: (data: T) => void): Ref<T | undefined> {\n const socket = useSharedWebSocket();\n const value = ref<T | undefined>(undefined) as Ref<T | undefined>;\n\n const handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n value.value = typed;\n }\n };\n const unsub = socket.on(event, handler);\n\n onUnmounted(unsub);\n return readonly(value) as Ref<T | undefined>;\n}\n\n/**\n * Accumulate WebSocket events.\n * - Without callback: returns reactive array.\n * - With callback: calls your handler — manage your own state.\n *\n * @example\n * // Default accumulation\n * const messages = useSocketStream<ChatMessage>('chat.message');\n *\n * @example\n * // Custom — keep last 50\n * const messages = ref<ChatMessage[]>([]);\n * useSocketStream<ChatMessage>('chat.message', (msg) => {\n * messages.value = [msg, ...messages.value].slice(0, 50);\n * });\n *\n * @example\n * // Custom — filter by type\n * const errors = ref<LogEntry[]>([]);\n * useSocketStream<LogEntry>('log.entry', (entry) => {\n * if (entry.level === 'error') errors.value = [...errors.value, entry];\n * });\n */\nexport function useSocketStream<T>(event: string, callback?: (data: T) => void): Ref<T[]> {\n const socket = useSharedWebSocket();\n const items = ref<T[]>([]) as Ref<T[]>;\n\n const handler = (data: unknown) => {\n const typed = data as T;\n if (callback) {\n callback(typed);\n } else {\n items.value = [...items.value, typed];\n }\n };\n const unsub = socket.on(event, handler);\n\n onUnmounted(unsub);\n return readonly(items) as Ref<T[]>;\n}\n\n/**\n * Two-way state sync across browser tabs.\n * - Without callback: reactive ref synced across tabs.\n * - With callback: called when any tab updates this key — side effects.\n *\n * @example\n * // Reactive two-way sync\n * const cart = useSocketSync<Cart>('cart', { items: [] });\n * cart.value = { items: [1, 2, 3] }; // syncs to all tabs\n *\n * @example\n * // With side effect callback\n * const cart = useSocketSync<Cart>('cart', { items: [] }, (cart) => {\n * document.title = `Cart (${cart.items.length})`;\n * analytics.track('cart_updated');\n * });\n */\nexport function useSocketSync<T>(key: string, initialValue: T, callback?: (value: T) => void): Ref<T> {\n const socket = useSharedWebSocket();\n const value = ref<T>(socket.getSync<T>(key) ?? initialValue) as Ref<T>;\n\n const unsub = socket.onSync<T>(key, (v) => {\n value.value = v;\n callback?.(v);\n });\n\n watch(\n value,\n (newVal) => {\n socket.sync(key, newVal);\n },\n { deep: true },\n );\n\n onUnmounted(unsub);\n return value;\n}\n\n/**\n * Fire-and-forget event handler — no state, no ref.\n *\n * @example\n * useSocketCallback<Notification>('notification', (n) => {\n * showToast(n.title);\n * });\n */\nexport function useSocketCallback<T>(event: string, callback: (data: T) => void): void {\n const socket = useSharedWebSocket();\n\n const unsub = socket.on(event, (data: unknown) => {\n callback(data as T);\n });\n\n onUnmounted(unsub);\n}\n\n/**\n * Reactive connection status.\n *\n * @example\n * const { connected, tabRole } = useSocketStatus();\n */\nexport function useSocketStatus(): {\n connected: Ref<boolean>;\n tabRole: Ref<TabRole>;\n isAuthenticated: Ref<boolean>;\n} {\n const socket = useSharedWebSocket();\n const connected = ref(socket.connected);\n const tabRole = ref<TabRole>(socket.tabRole);\n const isAuthenticated = ref(socket.isAuthenticated);\n\n const timer = setInterval(() => {\n connected.value = socket.connected;\n tabRole.value = socket.tabRole;\n isAuthenticated.value = socket.isAuthenticated;\n }, 1000);\n\n onUnmounted(() => clearInterval(timer));\n\n return {\n connected: readonly(connected) as Ref<boolean>,\n tabRole: readonly(tabRole) as Ref<TabRole>,\n isAuthenticated: readonly(isAuthenticated) as Ref<boolean>,\n };\n}\n\n/**\n * Lifecycle hooks — react to connection state changes.\n *\n * @example\n * useSocketLifecycle({\n * onConnect: () => console.log('Connected!'),\n * onDisconnect: () => showOfflineBanner(),\n * onReconnecting: () => showSpinner(),\n * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),\n * onError: (err) => reportError(err),\n * });\n */\nexport function useSocketLifecycle(handlers: SocketLifecycleHandlers): void {\n const socket = useSharedWebSocket();\n const unsubs: (() => void)[] = [];\n\n if (handlers.onConnect) unsubs.push(socket.onConnect(handlers.onConnect));\n if (handlers.onDisconnect) unsubs.push(socket.onDisconnect(handlers.onDisconnect));\n if (handlers.onReconnecting) unsubs.push(socket.onReconnecting(handlers.onReconnecting));\n if (handlers.onLeaderChange) unsubs.push(socket.onLeaderChange(handlers.onLeaderChange));\n if (handlers.onError) unsubs.push(socket.onError(handlers.onError));\n if (handlers.onActive) unsubs.push(socket.onActive(handlers.onActive));\n if (handlers.onInactive) unsubs.push(socket.onInactive(handlers.onInactive));\n if (handlers.onVisibilityChange) unsubs.push(socket.onVisibilityChange(handlers.onVisibilityChange));\n if (handlers.onAuthChange) unsubs.push(socket.onAuthChange(handlers.onAuthChange));\n\n onUnmounted(() => unsubs.forEach((u) => u()));\n}\n\n/**\n * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.\n *\n * @example\n * const chat = useChannel('chat:room_123');\n * // Listen via useSocketEvent('chat:room_123:message')\n * // Send via chat.send('message', { text: 'Hello' })\n */\nexport function useChannel(name: string, options?: { auth?: boolean }) {\n const socket = useSharedWebSocket();\n const channel = socket.channel(name, options);\n\n onUnmounted(() => channel.leave());\n\n return channel;\n}\n\n/**\n * Subscribe to server-side topics. Auto-unsubscribes on unmount.\n *\n * @example\n * useTopics(['notifications:orders', 'notifications:payments']);\n */\nexport function useTopics(topics: string[], options?: { auth?: boolean }): void {\n const socket = useSharedWebSocket();\n\n topics.forEach((t) => socket.subscribe(t, options));\n\n onUnmounted(() => {\n topics.forEach((t) => socket.unsubscribe(t));\n });\n}\n\n/**\n * Enable browser push notifications for an event. Auto-cleanup on unmount.\n *\n * @example\n * usePush('notification', {\n * title: (n) => n.title,\n * body: (n) => n.body,\n * icon: '/icon.png',\n * });\n */\nexport function usePush<T = unknown>(\n event: string,\n config: {\n title: string | ((data: T) => string);\n body?: string | ((data: T) => string);\n icon?: string;\n tag?: string | ((data: T) => string);\n leaderOnly?: boolean;\n onlyWhenHidden?: boolean;\n onClick?: (data: T) => void;\n },\n): void {\n const socket = useSharedWebSocket();\n const unsub = socket.push<T>(event, config);\n\n onUnmounted(unsub);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gwakko/shared-websocket",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Share ONE WebSocket connection across browser tabs — leader election, BroadcastChannel sync, optional Web Worker",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -31,7 +31,7 @@ const NOOP_LOGGER: Logger = {
31
31
  /** Common interface for both SharedSocket and WorkerSocket. */
32
32
  interface SocketAdapter {
33
33
  readonly state: string;
34
- connect(): void;
34
+ connect(): void | Promise<void>;
35
35
  send(data: unknown): void;
36
36
  disconnect(): void;
37
37
  onMessage(fn: EventHandler): Unsubscribe;
@@ -675,6 +675,9 @@ export class SharedWebSocket<TEvents extends EventMap = EventMap> implements Dis
675
675
  return new WorkerSocket(this.url, {
676
676
  ...socketOptions,
677
677
  workerUrl: this.options.workerUrl,
678
+ auth: this.options.auth,
679
+ authToken: this.options.authToken,
680
+ authParam: this.options.authParam,
678
681
  });
679
682
  }
680
683
 
@@ -748,7 +751,7 @@ export class SharedWebSocket<TEvents extends EventMap = EventMap> implements Dis
748
751
  }),
749
752
  );
750
753
 
751
- this.socket.connect();
754
+ void this.socket.connect();
752
755
  }
753
756
 
754
757
  private reAuthenticateOnReconnect(): void {
package/src/TabSync.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import './utils/disposable';
2
- import type { Unsubscribe } from './types';
2
+ import type { Unsubscribe, Logger } from './types';
3
3
 
4
4
  interface SyncMessage {
5
5
  key: string;
@@ -7,6 +7,15 @@ interface SyncMessage {
7
7
  deleted?: boolean;
8
8
  }
9
9
 
10
+ interface TabSyncOptions {
11
+ /** Enable debug logging (default: false). */
12
+ debug?: boolean;
13
+ /** Custom logger (default: console). */
14
+ logger?: Logger;
15
+ }
16
+
17
+ const NOOP_LOGGER: Logger = { debug() {}, info() {}, warn() {}, error() {} };
18
+
10
19
  /**
11
20
  * Cross-tab state synchronization via BroadcastChannel.
12
21
  * No WebSocket needed — works standalone for sharing state between browser tabs.
@@ -15,21 +24,30 @@ interface SyncMessage {
15
24
  * const sync = new TabSync('my-app');
16
25
  * sync.set('theme', 'dark');
17
26
  * sync.on('theme', (theme) => applyTheme(theme));
27
+ *
28
+ * @example
29
+ * // With debug logging
30
+ * const sync = new TabSync('my-app', { debug: true });
18
31
  */
19
32
  export class TabSync implements Disposable {
20
33
  private store = new Map<string, unknown>();
21
34
  private listeners = new Map<string, Set<(value: unknown) => void>>();
22
35
  private bc: BroadcastChannel;
23
36
  private disposed = false;
37
+ private readonly log: Logger;
24
38
 
25
- constructor(channel = 'tab-sync') {
39
+ constructor(channel = 'tab-sync', options?: TabSyncOptions) {
40
+ this.log = options?.debug ? (options.logger ?? console) : NOOP_LOGGER;
41
+ this.log.debug('[TabSync] init', { channel });
26
42
  this.bc = new BroadcastChannel(channel);
27
43
  this.bc.onmessage = (ev: MessageEvent<SyncMessage>) => {
28
44
  const { key, value, deleted } = ev.data;
29
45
  if (deleted) {
30
46
  this.store.delete(key);
47
+ this.log.debug('[TabSync] ← remote delete', key);
31
48
  } else {
32
49
  this.store.set(key, value);
50
+ this.log.debug('[TabSync] ← remote set', key, value);
33
51
  }
34
52
  this.emit(key, value);
35
53
  };
@@ -39,6 +57,7 @@ export class TabSync implements Disposable {
39
57
  set<T>(key: string, value: T): void {
40
58
  this.store.set(key, value);
41
59
  this.bc.postMessage({ key, value } satisfies SyncMessage);
60
+ this.log.debug('[TabSync] → set', key, value);
42
61
  this.emit(key, value);
43
62
  }
44
63
 
@@ -51,6 +70,7 @@ export class TabSync implements Disposable {
51
70
  delete(key: string): void {
52
71
  this.store.delete(key);
53
72
  this.bc.postMessage({ key, value: undefined, deleted: true } satisfies SyncMessage);
73
+ this.log.debug('[TabSync] → delete', key);
54
74
  this.emit(key, undefined);
55
75
  }
56
76
 
@@ -98,6 +118,7 @@ export class TabSync implements Disposable {
98
118
  /** Clear all keys and notify listeners. */
99
119
  clear(): void {
100
120
  const keys = [...this.store.keys()];
121
+ this.log.debug('[TabSync] → clear', keys);
101
122
  this.store.clear();
102
123
  for (const key of keys) {
103
124
  this.bc.postMessage({ key, value: undefined, deleted: true } satisfies SyncMessage);
@@ -30,9 +30,14 @@ export class WorkerSocket implements Disposable {
30
30
  protocols?: string[];
31
31
  reconnect?: boolean;
32
32
  reconnectMaxDelay?: number;
33
+ reconnectMaxRetries?: number;
33
34
  heartbeatInterval?: number;
34
35
  sendBuffer?: number;
35
36
  workerUrl?: string | URL;
37
+ auth?: () => string | Promise<string>;
38
+ authToken?: string;
39
+ authParam?: string;
40
+ pingPayload?: unknown;
36
41
  } = {},
37
42
  ) {}
38
43
 
@@ -40,7 +45,18 @@ export class WorkerSocket implements Disposable {
40
45
  return this._state;
41
46
  }
42
47
 
43
- connect(): void {
48
+ async connect(): Promise<void> {
49
+ // Resolve auth token before sending to worker (functions can't cross worker boundary)
50
+ let authToken: string | undefined;
51
+ if (this.options.auth) {
52
+ authToken = await this.options.auth();
53
+ } else if (this.options.authToken) {
54
+ authToken = this.options.authToken;
55
+ }
56
+
57
+ // Build URL with auth token
58
+ const connectUrl = authToken ? this.buildUrl(authToken) : this.url;
59
+
44
60
  // Create worker from inline blob if no workerUrl provided
45
61
  const workerUrl = this.options.workerUrl ?? this.createWorkerBlob();
46
62
 
@@ -74,15 +90,25 @@ export class WorkerSocket implements Disposable {
74
90
 
75
91
  this.worker.postMessage({
76
92
  type: 'connect',
77
- url: this.url,
93
+ url: connectUrl,
78
94
  protocols: this.options.protocols ?? [],
79
95
  reconnect: this.options.reconnect ?? true,
80
96
  reconnectMaxDelay: this.options.reconnectMaxDelay ?? 30_000,
97
+ reconnectMaxRetries: this.options.reconnectMaxRetries ?? Infinity,
81
98
  heartbeatInterval: this.options.heartbeatInterval ?? 30_000,
82
99
  bufferSize: this.options.sendBuffer ?? 100,
100
+ pingPayload: this.options.pingPayload,
83
101
  });
84
102
  }
85
103
 
104
+ private buildUrl(token: string): string {
105
+ const param = this.options.authParam ?? 'token';
106
+ const httpUrl = this.url.replace(/^ws(s?):\/\//, 'http$1://');
107
+ const parsed = new URL(httpUrl);
108
+ parsed.searchParams.set(param, token);
109
+ return parsed.toString().replace(/^http(s?):\/\//, 'ws$1://');
110
+ }
111
+
86
112
  send(data: unknown): void {
87
113
  this.worker?.postMessage({ type: 'send', data });
88
114
  }
@@ -113,26 +139,34 @@ export class WorkerSocket implements Disposable {
113
139
  let ws = null, state = 'closed', buffer = [], disposed = false;
114
140
  let heartbeatTimer = null, reconnectTimer = null;
115
141
  let url = '', protocols = [], shouldReconnect = true;
116
- let maxDelay = 30000, hbInterval = 30000, maxBuf = 100, delay = 1000;
142
+ let maxDelay = 30000, maxRetries = Infinity, hbInterval = 30000, maxBuf = 100;
143
+ let delay = 1000, attempts = 0, pingPayload = '{"type":"ping"}';
117
144
 
118
145
  function setState(s) { state = s; self.postMessage({ type: 'state', state: s }); }
119
146
  function connect() {
120
147
  if (disposed) return;
121
148
  setState('connecting');
122
149
  ws = new WebSocket(url, protocols);
123
- ws.onopen = () => { setState('connected'); delay = 1000; self.postMessage({ type: 'open' }); flush(); startHB(); };
150
+ ws.onopen = () => { attempts = 0; delay = 1000; setState('connected'); self.postMessage({ type: 'open' }); flush(); startHB(); };
124
151
  ws.onmessage = (e) => { let d; try { d = JSON.parse(e.data); } catch { d = e.data; } self.postMessage({ type: 'message', data: d }); };
125
152
  ws.onclose = (e) => { stopHB(); self.postMessage({ type: 'close', code: e.code, reason: e.reason }); if (!disposed && shouldReconnect && e.code !== 1000) reconnect(); else setState('closed'); };
126
153
  ws.onerror = () => { self.postMessage({ type: 'error', message: 'error' }); };
127
154
  }
128
155
  function send(d) { if (state === 'connected' && ws?.readyState === 1) ws.send(JSON.stringify(d)); else if (buffer.length < maxBuf) buffer.push(d); }
129
156
  function flush() { const p = buffer.splice(0); p.forEach(send); }
130
- function startHB() { stopHB(); heartbeatTimer = setInterval(() => { if (ws?.readyState === 1) ws.send('{"type":"ping"}'); }, hbInterval); }
157
+ function startHB() { stopHB(); heartbeatTimer = setInterval(() => { if (ws?.readyState === 1) ws.send(pingPayload); }, hbInterval); }
131
158
  function stopHB() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } }
132
- function reconnect() { setState('reconnecting'); const j = delay * 0.25 * (Math.random() * 2 - 1); reconnectTimer = setTimeout(() => { if (!disposed) connect(); }, Math.min(delay + j, maxDelay)); delay = Math.min(delay * 2, maxDelay); }
159
+ function reconnect() {
160
+ attempts++;
161
+ if (attempts > maxRetries) { setState('closed'); return; }
162
+ setState('reconnecting');
163
+ const j = delay * 0.25 * (Math.random() * 2 - 1);
164
+ reconnectTimer = setTimeout(() => { if (!disposed) connect(); }, Math.min(delay + j, maxDelay));
165
+ delay = Math.min(delay * 2, maxDelay);
166
+ }
133
167
  self.onmessage = (e) => {
134
168
  const c = e.data;
135
- if (c.type === 'connect') { url = c.url; protocols = c.protocols || []; shouldReconnect = c.reconnect ?? true; maxDelay = c.reconnectMaxDelay || 30000; hbInterval = c.heartbeatInterval || 30000; maxBuf = c.bufferSize || 100; connect(); }
169
+ if (c.type === 'connect') { url = c.url; protocols = c.protocols || []; shouldReconnect = c.reconnect ?? true; maxDelay = c.reconnectMaxDelay || 30000; maxRetries = c.reconnectMaxRetries ?? Infinity; hbInterval = c.heartbeatInterval || 30000; maxBuf = c.bufferSize || 100; if (c.pingPayload) pingPayload = JSON.stringify(c.pingPayload); connect(); }
136
170
  if (c.type === 'send') send(c.data);
137
171
  if (c.type === 'disconnect') { disposed = true; stopHB(); if (reconnectTimer) clearTimeout(reconnectTimer); if (ws) { ws.onclose = null; if (ws.readyState < 2) ws.close(1000); ws = null; } buffer = []; setState('closed'); }
138
172
  };
@@ -48,13 +48,10 @@ export interface SharedWebSocketProviderProps {
48
48
  * }
49
49
  */
50
50
  export function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps) {
51
- const [socket] = useState(() => {
52
- const ws = new SharedWebSocket(url, options);
53
- ws.connect();
54
- return ws;
55
- });
51
+ const [socket] = useState(() => new SharedWebSocket(url, options));
56
52
 
57
53
  useEffect(() => {
54
+ socket.connect();
58
55
  return () => {
59
56
  socket[Symbol.dispose]();
60
57
  };
@@ -28,11 +28,13 @@ const TabSyncContext = createContext<TabSync | null>(null);
28
28
  export interface TabSyncProviderProps {
29
29
  /** BroadcastChannel name (default: "tab-sync"). */
30
30
  channel?: string;
31
+ /** Enable debug logging. */
32
+ debug?: boolean;
31
33
  children: ReactNode;
32
34
  }
33
35
 
34
- export function TabSyncProvider({ channel, children }: TabSyncProviderProps) {
35
- const [sync] = useState(() => new TabSync(channel));
36
+ export function TabSyncProvider({ channel, debug, children }: TabSyncProviderProps) {
37
+ const [sync] = useState(() => new TabSync(channel, { debug }));
36
38
 
37
39
  useEffect(() => {
38
40
  return () => sync[Symbol.dispose]();
@@ -21,10 +21,10 @@ export const TabSyncKey: InjectionKey<TabSync> = Symbol('TabSync');
21
21
  * const app = createApp(App);
22
22
  * app.use(createTabSyncPlugin('my-app'));
23
23
  */
24
- export function createTabSyncPlugin(channel?: string) {
24
+ export function createTabSyncPlugin(channel?: string, options?: { debug?: boolean }) {
25
25
  return {
26
26
  install(app: App) {
27
- const sync = new TabSync(channel);
27
+ const sync = new TabSync(channel, options);
28
28
  app.provide(TabSyncKey, sync);
29
29
 
30
30
  const originalUnmount = app.unmount.bind(app);
@@ -29,7 +29,7 @@ export function createSharedWebSocketPlugin(url: string, options?: SharedWebSock
29
29
  return {
30
30
  install(app: App) {
31
31
  const socket = new SharedWebSocket(url, options);
32
- socket.connect();
32
+ void socket.connect();
33
33
  app.provide(SharedWebSocketKey, socket);
34
34
 
35
35
  const originalUnmount = app.unmount.bind(app);
package/src/sync.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';