@lucaismyname/ginger 0.0.30 → 0.0.32

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 (102) hide show
  1. package/README.md +140 -8
  2. package/dist/analyzer/liveAudioGraph.d.ts +14 -1
  3. package/dist/analyzer/liveAudioGraph.d.ts.map +1 -1
  4. package/dist/analyzer/useGingerLiveAnalyzer.d.ts +6 -0
  5. package/dist/analyzer/useGingerLiveAnalyzer.d.ts.map +1 -1
  6. package/dist/client.cjs +1 -1
  7. package/dist/client.js +40 -32
  8. package/dist/client.js.map +1 -1
  9. package/dist/context/GingerProvider.d.ts +1 -1
  10. package/dist/context/GingerProvider.d.ts.map +1 -1
  11. package/dist/core/playbackReducer.d.ts.map +1 -1
  12. package/dist/equalizer/index.cjs +2 -0
  13. package/dist/equalizer/index.cjs.map +1 -0
  14. package/dist/equalizer/index.d.ts +3 -0
  15. package/dist/equalizer/index.d.ts.map +1 -0
  16. package/dist/equalizer/index.js +52 -0
  17. package/dist/equalizer/index.js.map +1 -0
  18. package/dist/equalizer/useGingerEqualizer.d.ts +41 -0
  19. package/dist/equalizer/useGingerEqualizer.d.ts.map +1 -0
  20. package/dist/ginger-L2ZFgzH4.js +2223 -0
  21. package/dist/ginger-L2ZFgzH4.js.map +1 -0
  22. package/dist/ginger-NEcOSSJD.cjs +2 -0
  23. package/dist/ginger-NEcOSSJD.cjs.map +1 -0
  24. package/dist/hooks/useGingerChapterProgress.d.ts +14 -0
  25. package/dist/hooks/useGingerChapterProgress.d.ts.map +1 -0
  26. package/dist/hooks/useGingerKeyboardShortcuts.d.ts +6 -0
  27. package/dist/hooks/useGingerKeyboardShortcuts.d.ts.map +1 -1
  28. package/dist/hooks/useGingerPlaybackHistory.d.ts +26 -0
  29. package/dist/hooks/useGingerPlaybackHistory.d.ts.map +1 -0
  30. package/dist/hooks/useGingerSleepTimer.d.ts.map +1 -1
  31. package/dist/hooks/useGingerVolumeFade.d.ts +22 -0
  32. package/dist/hooks/useGingerVolumeFade.d.ts.map +1 -0
  33. package/dist/index.cjs +1 -1
  34. package/dist/index.d.ts +18 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +40 -32
  37. package/dist/index.js.map +1 -1
  38. package/dist/liveAudioGraph-0cpHD_Ic.cjs +2 -0
  39. package/dist/liveAudioGraph-0cpHD_Ic.cjs.map +1 -0
  40. package/dist/liveAudioGraph-DvPaxBCP.js +105 -0
  41. package/dist/liveAudioGraph-DvPaxBCP.js.map +1 -0
  42. package/dist/remote/index.cjs +2 -0
  43. package/dist/remote/index.cjs.map +1 -0
  44. package/dist/remote/index.d.ts +5 -0
  45. package/dist/remote/index.d.ts.map +1 -0
  46. package/dist/remote/index.js +149 -0
  47. package/dist/remote/index.js.map +1 -0
  48. package/dist/remote/remoteProtocol.d.ts +28 -0
  49. package/dist/remote/remoteProtocol.d.ts.map +1 -0
  50. package/dist/remote/useGingerRemote.d.ts +35 -0
  51. package/dist/remote/useGingerRemote.d.ts.map +1 -0
  52. package/dist/selectors-BalBCc7X.js +127 -0
  53. package/dist/selectors-BalBCc7X.js.map +1 -0
  54. package/dist/selectors-YXnP8Y8g.cjs +2 -0
  55. package/dist/selectors-YXnP8Y8g.cjs.map +1 -0
  56. package/dist/spatial/index.cjs +2 -0
  57. package/dist/spatial/index.cjs.map +1 -0
  58. package/dist/spatial/index.d.ts +3 -0
  59. package/dist/spatial/index.d.ts.map +1 -0
  60. package/dist/spatial/index.js +59 -0
  61. package/dist/spatial/index.js.map +1 -0
  62. package/dist/spatial/useGingerSpatialAudio.d.ts +34 -0
  63. package/dist/spatial/useGingerSpatialAudio.d.ts.map +1 -0
  64. package/dist/spatial/useGingerSpatialAudio.test.d.ts +2 -0
  65. package/dist/spatial/useGingerSpatialAudio.test.d.ts.map +1 -0
  66. package/dist/store.d.ts +46 -0
  67. package/dist/store.d.ts.map +1 -0
  68. package/dist/testing/index.cjs +1 -1
  69. package/dist/testing/index.js +1 -1
  70. package/dist/testing/mockWebAudio.d.ts +29 -1
  71. package/dist/testing/mockWebAudio.d.ts.map +1 -1
  72. package/dist/transcript/index.cjs +8 -0
  73. package/dist/transcript/index.cjs.map +1 -0
  74. package/dist/transcript/index.d.ts +5 -0
  75. package/dist/transcript/index.d.ts.map +1 -0
  76. package/dist/transcript/index.js +99 -0
  77. package/dist/transcript/index.js.map +1 -0
  78. package/dist/transcript/parseTranscript.d.ts +27 -0
  79. package/dist/transcript/parseTranscript.d.ts.map +1 -0
  80. package/dist/transcript/parseTranscript.test.d.ts +2 -0
  81. package/dist/transcript/parseTranscript.test.d.ts.map +1 -0
  82. package/dist/transcript/useGingerTranscriptSync.d.ts +23 -0
  83. package/dist/transcript/useGingerTranscriptSync.d.ts.map +1 -0
  84. package/dist/types.d.ts +30 -8
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/useGinger-BXgia32v.cjs +2 -0
  87. package/dist/useGinger-BXgia32v.cjs.map +1 -0
  88. package/dist/useGinger-hpp2pAGY.js +48 -0
  89. package/dist/useGinger-hpp2pAGY.js.map +1 -0
  90. package/dist/useGingerChapterProgress-BdaalJvX.cjs +2 -0
  91. package/dist/useGingerChapterProgress-BdaalJvX.cjs.map +1 -0
  92. package/dist/useGingerChapterProgress-CZdv-HiI.js +322 -0
  93. package/dist/useGingerChapterProgress-CZdv-HiI.js.map +1 -0
  94. package/package.json +23 -3
  95. package/dist/ginger-DYlsaFGA.js +0 -2283
  96. package/dist/ginger-DYlsaFGA.js.map +0 -1
  97. package/dist/ginger-YDZhJqet.cjs +0 -2
  98. package/dist/ginger-YDZhJqet.cjs.map +0 -1
  99. package/dist/useNextTrackPrefetch-BZdly__j.js +0 -265
  100. package/dist/useNextTrackPrefetch-BZdly__j.js.map +0 -1
  101. package/dist/useNextTrackPrefetch-pBFiWDyx.cjs +0 -2
  102. package/dist/useNextTrackPrefetch-pBFiWDyx.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,43 +1,51 @@
1
- import { C as s, G as r, L as i, P as u, a as n, R as t, S as g, b as l, c, V as o, d as p, W as G, e as S, f as y, g as m, h as d, p as h, u as k, i as L, j as f, k as P, l as b, m as x } from "./ginger-DYlsaFGA.js";
2
- import { a as C, d as v, u as B, b as A, c as F, e as R, f as D, g as T, h as W } from "./useNextTrackPrefetch-BZdly__j.js";
3
- import { g as w, a as z, u as I, b as K, c as M } from "./GingerSplitContexts-BzBExb95.js";
1
+ import { C as s, G as r, L as i, P as t, a as u, R as n, S as o, b as g, c, V as l, d as p, W as G, e as m, f as y, g as S, p as d, u as f, h, i as P, j as k, k as L, l as b } from "./ginger-L2ZFgzH4.js";
2
+ import { u as C } from "./useGinger-hpp2pAGY.js";
3
+ import { c as v, u as B, a as F, b as A, d as R, e as D, f as T, g as W, h as j, i as w } from "./useGingerChapterProgress-CZdv-HiI.js";
4
+ import { a as H, d as I, s as K } from "./liveAudioGraph-DvPaxBCP.js";
5
+ import { d as N } from "./selectors-BalBCc7X.js";
6
+ import { g as X, a as q, u as E, b as J, c as O } from "./GingerSplitContexts-BzBExb95.js";
4
7
  export {
5
8
  s as Chapters,
6
9
  r as Ginger,
7
10
  i as LyricsSynced,
8
- u as Pause,
9
- n as Play,
10
- t as RepeatGlyph,
11
- g as ShuffleIcon,
12
- l as SkipBack,
11
+ t as Pause,
12
+ u as Play,
13
+ n as RepeatGlyph,
14
+ o as ShuffleIcon,
15
+ g as SkipBack,
13
16
  c as SkipForward,
14
- o as Volume2,
17
+ l as Volume2,
15
18
  p as VolumeX,
16
19
  G as Wrapper,
17
- C as attachLiveAnalyser,
18
- S as clampPlaybackRate,
20
+ H as attachLiveAnalyser,
21
+ m as clampPlaybackRate,
19
22
  y as clampVolume,
20
- m as defaultGingerLocale,
21
- d as derivePlaybackUiState,
22
- v as detachLiveAnalyser,
23
- w as gingerStateFromContextValues,
24
- z as gingerStateFromContexts,
25
- h as parseLrc,
26
- B as useGinger,
27
- k as useGingerChapters,
28
- A as useGingerDebugLog,
29
- F as useGingerKeyboardShortcuts,
23
+ v as createGingerStore,
24
+ S as defaultGingerLocale,
25
+ N as derivePlaybackUiState,
26
+ I as detachLiveAnalyser,
27
+ X as gingerStateFromContextValues,
28
+ q as gingerStateFromContexts,
29
+ d as parseLrc,
30
+ K as setProcessingChain,
31
+ C as useGinger,
32
+ B as useGingerChapterProgress,
33
+ f as useGingerChapters,
34
+ F as useGingerDebugLog,
35
+ A as useGingerKeyboardShortcuts,
30
36
  R as useGingerLiveAnalyzer,
31
- L as useGingerLocale,
32
- f as useGingerLyricsSync,
33
- I as useGingerMedia,
34
- K as useGingerPlayback,
35
- D as useGingerSleepTimer,
36
- M as useGingerState,
37
- T as useNextTrackPrefetch,
38
- P as usePlayPauseBinding,
39
- b as useSeekBarBinding,
40
- W as useSeekDrag,
41
- x as useVolumeSlider
37
+ h as useGingerLocale,
38
+ P as useGingerLyricsSync,
39
+ E as useGingerMedia,
40
+ J as useGingerPlayback,
41
+ D as useGingerPlaybackHistory,
42
+ T as useGingerSleepTimer,
43
+ O as useGingerState,
44
+ W as useGingerVolumeFade,
45
+ j as useNextTrackPrefetch,
46
+ k as usePlayPauseBinding,
47
+ L as useSeekBarBinding,
48
+ w as useSeekDrag,
49
+ b as useVolumeSlider
42
50
  };
43
51
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
@@ -0,0 +1,2 @@
1
+ "use strict";const r=new WeakMap;function h(t){const e=2**Math.round(Math.log2(t));return Math.min(32768,Math.max(32,e))}function d(t){const{processingChain:e}=t;return e.length>0?e[e.length-1]:t.source}function a(t){const{source:e,processingChain:n,consumers:s,context:c}=t;try{e.disconnect()}catch{}for(const i of n)try{i.disconnect()}catch{}for(const{analyser:i}of s.values())try{i.disconnect(c.destination)}catch{}if(n.length>0){e.connect(n[0]);for(let i=0;i<n.length-1;i++)n[i].connect(n[i+1])}const o=d(t);if(s.size===0)o.connect(c.destination);else{let i=!0;for(const{analyser:u}of s.values())o.connect(u),i&&(u.connect(c.destination),i=!1)}}function f(t){let e=r.get(t);if(!e){const n=window.AudioContext??window.webkitAudioContext;if(!n)throw new Error("Web Audio API is not available");const s=new n,c=s.createMediaElementSource(t);e={context:s,source:c,consumers:new Map,nextId:0,processingChain:[],processingActive:!1},r.set(t,e)}return e}function l(t,e){if(e.consumers.size===0&&!e.processingActive){try{e.source.disconnect()}catch{}e.context.close(),r.delete(t)}}function g(t,e){const n=f(t),{context:s}=n,c=s.createAnalyser();c.fftSize=h(e.fftSize),c.smoothingTimeConstant=e.smoothingTimeConstant,c.minDecibels=e.minDecibels,c.maxDecibels=e.maxDecibels;const o=n.nextId;return n.nextId+=1,n.consumers.set(o,{analyser:c}),a(n),{id:o,context:s,analyser:c}}function y(t,e){const n=r.get(t);if(!n)return;const s=n.consumers.get(e);if(s){try{s.analyser.disconnect()}catch{}if(n.consumers.delete(e),n.consumers.size===0&&!n.processingActive){l(t,n);return}a(n)}}function m(t,e){if(typeof window>"u")return;if(e.length===0){const s=r.get(t);if(!s)return;s.processingChain=[],s.processingActive=!1,s.consumers.size===0?l(t,s):a(s);return}const n=f(t);n.processingChain=e,n.processingActive=!0,a(n)}exports.attachLiveAnalyser=g;exports.detachLiveAnalyser=y;exports.setProcessingChain=m;
2
+ //# sourceMappingURL=liveAudioGraph-0cpHD_Ic.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"liveAudioGraph-0cpHD_Ic.cjs","sources":["../src/analyzer/liveAudioGraph.ts"],"sourcesContent":["/**\n * One MediaElementAudioSourceNode per HTMLAudioElement; multiple AnalyserNodes may tap the source.\n * An optional processing chain (e.g. EQ filters) can be inserted between the source and the\n * analysers via `setProcessingChain`.\n */\n\nexport type LiveAnalyserOptions = {\n fftSize: number;\n smoothingTimeConstant: number;\n minDecibels: number;\n maxDecibels: number;\n};\n\ntype Consumer = {\n analyser: AnalyserNode;\n};\n\ntype ElementEntry = {\n context: AudioContext;\n source: MediaElementAudioSourceNode;\n consumers: Map<number, Consumer>;\n nextId: number;\n /** Ordered processing nodes (e.g. BiquadFilterNode[]) inserted between source and analysers. */\n processingChain: AudioNode[];\n /** True while a processing chain is installed; prevents context teardown when no consumers exist. */\n processingActive: boolean;\n};\n\nconst entries = new WeakMap<HTMLAudioElement, ElementEntry>();\n\nfunction clampFftSize(n: number): number {\n const p = 2 ** Math.round(Math.log2(n));\n return Math.min(32768, Math.max(32, p));\n}\n\nfunction getChainTail(entry: ElementEntry): AudioNode {\n const { processingChain } = entry;\n return processingChain.length > 0 ? processingChain[processingChain.length - 1]! : entry.source;\n}\n\n/**\n * Rebuild all graph connections from scratch.\n * Call after any structural change (add/remove consumer, change processing chain).\n */\nfunction rebuildGraph(entry: ElementEntry): void {\n const { source, processingChain, consumers, context } = entry;\n\n // Disconnect source outputs\n try {\n source.disconnect();\n } catch {\n // ignore\n }\n\n // Disconnect processing chain node outputs\n for (const node of processingChain) {\n try {\n node.disconnect();\n } catch {\n // ignore\n }\n }\n\n // Disconnect all analysers from destination (we will reconnect selectively below)\n for (const { analyser } of consumers.values()) {\n try {\n analyser.disconnect(context.destination);\n } catch {\n // ignore\n }\n }\n\n // Build processing chain: source → node[0] → ... → node[N]\n if (processingChain.length > 0) {\n source.connect(processingChain[0]!);\n for (let i = 0; i < processingChain.length - 1; i++) {\n processingChain[i]!.connect(processingChain[i + 1]!);\n }\n }\n\n const tail = getChainTail(entry);\n\n if (consumers.size === 0) {\n // No analyser consumers: route tail directly to destination so audio is audible\n tail.connect(context.destination);\n } else {\n // Connect tail to all analysers; first one routes to destination (playback sink)\n let isFirst = true;\n for (const { analyser } of consumers.values()) {\n tail.connect(analyser);\n if (isFirst) {\n analyser.connect(context.destination);\n isFirst = false;\n }\n }\n }\n}\n\nfunction getOrCreateEntry(element: HTMLAudioElement): ElementEntry {\n let entry = entries.get(element);\n if (!entry) {\n const Context =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const context = new Context();\n const source = context.createMediaElementSource(element);\n entry = {\n context,\n source,\n consumers: new Map(),\n nextId: 0,\n processingChain: [],\n processingActive: false,\n };\n entries.set(element, entry);\n }\n return entry;\n}\n\nfunction maybeCloseEntry(element: HTMLAudioElement, entry: ElementEntry): void {\n if (entry.consumers.size === 0 && !entry.processingActive) {\n try {\n entry.source.disconnect();\n } catch {\n // ignore\n }\n void entry.context.close();\n entries.delete(element);\n }\n}\n\nexport function attachLiveAnalyser(\n element: HTMLAudioElement,\n options: LiveAnalyserOptions,\n): { id: number; context: AudioContext; analyser: AnalyserNode } {\n const entry = getOrCreateEntry(element);\n const { context } = entry;\n\n const analyser = context.createAnalyser();\n analyser.fftSize = clampFftSize(options.fftSize);\n analyser.smoothingTimeConstant = options.smoothingTimeConstant;\n analyser.minDecibels = options.minDecibels;\n analyser.maxDecibels = options.maxDecibels;\n\n const id = entry.nextId;\n entry.nextId += 1;\n entry.consumers.set(id, { analyser });\n\n rebuildGraph(entry);\n\n return { id, context, analyser };\n}\n\nexport function detachLiveAnalyser(element: HTMLAudioElement, id: number): void {\n const entry = entries.get(element);\n if (!entry) return;\n\n const consumer = entry.consumers.get(id);\n if (!consumer) return;\n\n try {\n consumer.analyser.disconnect();\n } catch {\n // ignore\n }\n entry.consumers.delete(id);\n\n if (entry.consumers.size === 0 && !entry.processingActive) {\n maybeCloseEntry(element, entry);\n return;\n }\n\n rebuildGraph(entry);\n}\n\n/**\n * Insert an ordered processing chain (e.g. BiquadFilterNode[]) between the audio source and the\n * analyser consumers. Pass an empty array to clear the chain.\n *\n * Safe to call while analyser consumers are active; the graph is rebuilt immediately.\n * Note: because `createMediaElementSource` can only be called once per element, the EQ and live\n * analyser share the same AudioContext. Calling both for the same element is supported.\n */\nexport function setProcessingChain(element: HTMLAudioElement, nodes: AudioNode[]): void {\n if (typeof window === \"undefined\") return;\n\n if (nodes.length === 0) {\n const entry = entries.get(element);\n if (!entry) return;\n entry.processingChain = [];\n entry.processingActive = false;\n if (entry.consumers.size === 0) {\n maybeCloseEntry(element, entry);\n } else {\n rebuildGraph(entry);\n }\n return;\n }\n\n const entry = getOrCreateEntry(element);\n entry.processingChain = nodes;\n entry.processingActive = true;\n rebuildGraph(entry);\n}\n"],"names":["entries","clampFftSize","n","p","getChainTail","entry","processingChain","rebuildGraph","source","consumers","context","node","analyser","tail","isFirst","getOrCreateEntry","element","Context","maybeCloseEntry","attachLiveAnalyser","options","id","detachLiveAnalyser","consumer","setProcessingChain","nodes"],"mappings":"aA4BA,MAAMA,MAAc,QAEpB,SAASC,EAAaC,EAAmB,CACvC,MAAMC,EAAI,GAAK,KAAK,MAAM,KAAK,KAAKD,CAAC,CAAC,EACtC,OAAO,KAAK,IAAI,MAAO,KAAK,IAAI,GAAIC,CAAC,CAAC,CACxC,CAEA,SAASC,EAAaC,EAAgC,CACpD,KAAM,CAAE,gBAAAC,GAAoBD,EAC5B,OAAOC,EAAgB,OAAS,EAAIA,EAAgBA,EAAgB,OAAS,CAAC,EAAKD,EAAM,MAC3F,CAMA,SAASE,EAAaF,EAA2B,CAC/C,KAAM,CAAE,OAAAG,EAAQ,gBAAAF,EAAiB,UAAAG,EAAW,QAAAC,GAAYL,EAGxD,GAAI,CACFG,EAAO,WAAA,CACT,MAAQ,CAER,CAGA,UAAWG,KAAQL,EACjB,GAAI,CACFK,EAAK,WAAA,CACP,MAAQ,CAER,CAIF,SAAW,CAAE,SAAAC,CAAA,IAAcH,EAAU,SACnC,GAAI,CACFG,EAAS,WAAWF,EAAQ,WAAW,CACzC,MAAQ,CAER,CAIF,GAAIJ,EAAgB,OAAS,EAAG,CAC9BE,EAAO,QAAQF,EAAgB,CAAC,CAAE,EAClC,QAAS,EAAI,EAAG,EAAIA,EAAgB,OAAS,EAAG,IAC9CA,EAAgB,CAAC,EAAG,QAAQA,EAAgB,EAAI,CAAC,CAAE,CAEvD,CAEA,MAAMO,EAAOT,EAAaC,CAAK,EAE/B,GAAII,EAAU,OAAS,EAErBI,EAAK,QAAQH,EAAQ,WAAW,MAC3B,CAEL,IAAII,EAAU,GACd,SAAW,CAAE,SAAAF,CAAA,IAAcH,EAAU,SACnCI,EAAK,QAAQD,CAAQ,EACjBE,IACFF,EAAS,QAAQF,EAAQ,WAAW,EACpCI,EAAU,GAGhB,CACF,CAEA,SAASC,EAAiBC,EAAyC,CACjE,IAAIX,EAAQL,EAAQ,IAAIgB,CAAO,EAC/B,GAAI,CAACX,EAAO,CACV,MAAMY,EACJ,OAAO,cACN,OAAmE,mBACtE,GAAI,CAACA,EACH,MAAM,IAAI,MAAM,gCAAgC,EAElD,MAAMP,EAAU,IAAIO,EACdT,EAASE,EAAQ,yBAAyBM,CAAO,EACvDX,EAAQ,CACN,QAAAK,EACA,OAAAF,EACA,cAAe,IACf,OAAQ,EACR,gBAAiB,CAAA,EACjB,iBAAkB,EAAA,EAEpBR,EAAQ,IAAIgB,EAASX,CAAK,CAC5B,CACA,OAAOA,CACT,CAEA,SAASa,EAAgBF,EAA2BX,EAA2B,CAC7E,GAAIA,EAAM,UAAU,OAAS,GAAK,CAACA,EAAM,iBAAkB,CACzD,GAAI,CACFA,EAAM,OAAO,WAAA,CACf,MAAQ,CAER,CACKA,EAAM,QAAQ,MAAA,EACnBL,EAAQ,OAAOgB,CAAO,CACxB,CACF,CAEO,SAASG,EACdH,EACAI,EAC+D,CAC/D,MAAMf,EAAQU,EAAiBC,CAAO,EAChC,CAAE,QAAAN,GAAYL,EAEdO,EAAWF,EAAQ,eAAA,EACzBE,EAAS,QAAUX,EAAamB,EAAQ,OAAO,EAC/CR,EAAS,sBAAwBQ,EAAQ,sBACzCR,EAAS,YAAcQ,EAAQ,YAC/BR,EAAS,YAAcQ,EAAQ,YAE/B,MAAMC,EAAKhB,EAAM,OACjB,OAAAA,EAAM,QAAU,EAChBA,EAAM,UAAU,IAAIgB,EAAI,CAAE,SAAAT,EAAU,EAEpCL,EAAaF,CAAK,EAEX,CAAE,GAAAgB,EAAI,QAAAX,EAAS,SAAAE,CAAA,CACxB,CAEO,SAASU,EAAmBN,EAA2BK,EAAkB,CAC9E,MAAMhB,EAAQL,EAAQ,IAAIgB,CAAO,EACjC,GAAI,CAACX,EAAO,OAEZ,MAAMkB,EAAWlB,EAAM,UAAU,IAAIgB,CAAE,EACvC,GAAKE,EAEL,IAAI,CACFA,EAAS,SAAS,WAAA,CACpB,MAAQ,CAER,CAGA,GAFAlB,EAAM,UAAU,OAAOgB,CAAE,EAErBhB,EAAM,UAAU,OAAS,GAAK,CAACA,EAAM,iBAAkB,CACzDa,EAAgBF,EAASX,CAAK,EAC9B,MACF,CAEAE,EAAaF,CAAK,EACpB,CAUO,SAASmB,EAAmBR,EAA2BS,EAA0B,CACtF,GAAI,OAAO,OAAW,IAAa,OAEnC,GAAIA,EAAM,SAAW,EAAG,CACtB,MAAMpB,EAAQL,EAAQ,IAAIgB,CAAO,EACjC,GAAI,CAACX,EAAO,OACZA,EAAM,gBAAkB,CAAA,EACxBA,EAAM,iBAAmB,GACrBA,EAAM,UAAU,OAAS,EAC3Ba,EAAgBF,EAASX,CAAK,EAE9BE,EAAaF,CAAK,EAEpB,MACF,CAEA,MAAMA,EAAQU,EAAiBC,CAAO,EACtCX,EAAM,gBAAkBoB,EACxBpB,EAAM,iBAAmB,GACzBE,EAAaF,CAAK,CACpB"}
@@ -0,0 +1,105 @@
1
+ const r = /* @__PURE__ */ new WeakMap();
2
+ function d(t) {
3
+ const e = 2 ** Math.round(Math.log2(t));
4
+ return Math.min(32768, Math.max(32, e));
5
+ }
6
+ function h(t) {
7
+ const { processingChain: e } = t;
8
+ return e.length > 0 ? e[e.length - 1] : t.source;
9
+ }
10
+ function a(t) {
11
+ const { source: e, processingChain: n, consumers: s, context: c } = t;
12
+ try {
13
+ e.disconnect();
14
+ } catch {
15
+ }
16
+ for (const o of n)
17
+ try {
18
+ o.disconnect();
19
+ } catch {
20
+ }
21
+ for (const { analyser: o } of s.values())
22
+ try {
23
+ o.disconnect(c.destination);
24
+ } catch {
25
+ }
26
+ if (n.length > 0) {
27
+ e.connect(n[0]);
28
+ for (let o = 0; o < n.length - 1; o++)
29
+ n[o].connect(n[o + 1]);
30
+ }
31
+ const i = h(t);
32
+ if (s.size === 0)
33
+ i.connect(c.destination);
34
+ else {
35
+ let o = !0;
36
+ for (const { analyser: u } of s.values())
37
+ i.connect(u), o && (u.connect(c.destination), o = !1);
38
+ }
39
+ }
40
+ function f(t) {
41
+ let e = r.get(t);
42
+ if (!e) {
43
+ const n = window.AudioContext ?? window.webkitAudioContext;
44
+ if (!n)
45
+ throw new Error("Web Audio API is not available");
46
+ const s = new n(), c = s.createMediaElementSource(t);
47
+ e = {
48
+ context: s,
49
+ source: c,
50
+ consumers: /* @__PURE__ */ new Map(),
51
+ nextId: 0,
52
+ processingChain: [],
53
+ processingActive: !1
54
+ }, r.set(t, e);
55
+ }
56
+ return e;
57
+ }
58
+ function l(t, e) {
59
+ if (e.consumers.size === 0 && !e.processingActive) {
60
+ try {
61
+ e.source.disconnect();
62
+ } catch {
63
+ }
64
+ e.context.close(), r.delete(t);
65
+ }
66
+ }
67
+ function g(t, e) {
68
+ const n = f(t), { context: s } = n, c = s.createAnalyser();
69
+ c.fftSize = d(e.fftSize), c.smoothingTimeConstant = e.smoothingTimeConstant, c.minDecibels = e.minDecibels, c.maxDecibels = e.maxDecibels;
70
+ const i = n.nextId;
71
+ return n.nextId += 1, n.consumers.set(i, { analyser: c }), a(n), { id: i, context: s, analyser: c };
72
+ }
73
+ function m(t, e) {
74
+ const n = r.get(t);
75
+ if (!n) return;
76
+ const s = n.consumers.get(e);
77
+ if (s) {
78
+ try {
79
+ s.analyser.disconnect();
80
+ } catch {
81
+ }
82
+ if (n.consumers.delete(e), n.consumers.size === 0 && !n.processingActive) {
83
+ l(t, n);
84
+ return;
85
+ }
86
+ a(n);
87
+ }
88
+ }
89
+ function y(t, e) {
90
+ if (typeof window > "u") return;
91
+ if (e.length === 0) {
92
+ const s = r.get(t);
93
+ if (!s) return;
94
+ s.processingChain = [], s.processingActive = !1, s.consumers.size === 0 ? l(t, s) : a(s);
95
+ return;
96
+ }
97
+ const n = f(t);
98
+ n.processingChain = e, n.processingActive = !0, a(n);
99
+ }
100
+ export {
101
+ g as a,
102
+ m as d,
103
+ y as s
104
+ };
105
+ //# sourceMappingURL=liveAudioGraph-DvPaxBCP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"liveAudioGraph-DvPaxBCP.js","sources":["../src/analyzer/liveAudioGraph.ts"],"sourcesContent":["/**\n * One MediaElementAudioSourceNode per HTMLAudioElement; multiple AnalyserNodes may tap the source.\n * An optional processing chain (e.g. EQ filters) can be inserted between the source and the\n * analysers via `setProcessingChain`.\n */\n\nexport type LiveAnalyserOptions = {\n fftSize: number;\n smoothingTimeConstant: number;\n minDecibels: number;\n maxDecibels: number;\n};\n\ntype Consumer = {\n analyser: AnalyserNode;\n};\n\ntype ElementEntry = {\n context: AudioContext;\n source: MediaElementAudioSourceNode;\n consumers: Map<number, Consumer>;\n nextId: number;\n /** Ordered processing nodes (e.g. BiquadFilterNode[]) inserted between source and analysers. */\n processingChain: AudioNode[];\n /** True while a processing chain is installed; prevents context teardown when no consumers exist. */\n processingActive: boolean;\n};\n\nconst entries = new WeakMap<HTMLAudioElement, ElementEntry>();\n\nfunction clampFftSize(n: number): number {\n const p = 2 ** Math.round(Math.log2(n));\n return Math.min(32768, Math.max(32, p));\n}\n\nfunction getChainTail(entry: ElementEntry): AudioNode {\n const { processingChain } = entry;\n return processingChain.length > 0 ? processingChain[processingChain.length - 1]! : entry.source;\n}\n\n/**\n * Rebuild all graph connections from scratch.\n * Call after any structural change (add/remove consumer, change processing chain).\n */\nfunction rebuildGraph(entry: ElementEntry): void {\n const { source, processingChain, consumers, context } = entry;\n\n // Disconnect source outputs\n try {\n source.disconnect();\n } catch {\n // ignore\n }\n\n // Disconnect processing chain node outputs\n for (const node of processingChain) {\n try {\n node.disconnect();\n } catch {\n // ignore\n }\n }\n\n // Disconnect all analysers from destination (we will reconnect selectively below)\n for (const { analyser } of consumers.values()) {\n try {\n analyser.disconnect(context.destination);\n } catch {\n // ignore\n }\n }\n\n // Build processing chain: source → node[0] → ... → node[N]\n if (processingChain.length > 0) {\n source.connect(processingChain[0]!);\n for (let i = 0; i < processingChain.length - 1; i++) {\n processingChain[i]!.connect(processingChain[i + 1]!);\n }\n }\n\n const tail = getChainTail(entry);\n\n if (consumers.size === 0) {\n // No analyser consumers: route tail directly to destination so audio is audible\n tail.connect(context.destination);\n } else {\n // Connect tail to all analysers; first one routes to destination (playback sink)\n let isFirst = true;\n for (const { analyser } of consumers.values()) {\n tail.connect(analyser);\n if (isFirst) {\n analyser.connect(context.destination);\n isFirst = false;\n }\n }\n }\n}\n\nfunction getOrCreateEntry(element: HTMLAudioElement): ElementEntry {\n let entry = entries.get(element);\n if (!entry) {\n const Context =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const context = new Context();\n const source = context.createMediaElementSource(element);\n entry = {\n context,\n source,\n consumers: new Map(),\n nextId: 0,\n processingChain: [],\n processingActive: false,\n };\n entries.set(element, entry);\n }\n return entry;\n}\n\nfunction maybeCloseEntry(element: HTMLAudioElement, entry: ElementEntry): void {\n if (entry.consumers.size === 0 && !entry.processingActive) {\n try {\n entry.source.disconnect();\n } catch {\n // ignore\n }\n void entry.context.close();\n entries.delete(element);\n }\n}\n\nexport function attachLiveAnalyser(\n element: HTMLAudioElement,\n options: LiveAnalyserOptions,\n): { id: number; context: AudioContext; analyser: AnalyserNode } {\n const entry = getOrCreateEntry(element);\n const { context } = entry;\n\n const analyser = context.createAnalyser();\n analyser.fftSize = clampFftSize(options.fftSize);\n analyser.smoothingTimeConstant = options.smoothingTimeConstant;\n analyser.minDecibels = options.minDecibels;\n analyser.maxDecibels = options.maxDecibels;\n\n const id = entry.nextId;\n entry.nextId += 1;\n entry.consumers.set(id, { analyser });\n\n rebuildGraph(entry);\n\n return { id, context, analyser };\n}\n\nexport function detachLiveAnalyser(element: HTMLAudioElement, id: number): void {\n const entry = entries.get(element);\n if (!entry) return;\n\n const consumer = entry.consumers.get(id);\n if (!consumer) return;\n\n try {\n consumer.analyser.disconnect();\n } catch {\n // ignore\n }\n entry.consumers.delete(id);\n\n if (entry.consumers.size === 0 && !entry.processingActive) {\n maybeCloseEntry(element, entry);\n return;\n }\n\n rebuildGraph(entry);\n}\n\n/**\n * Insert an ordered processing chain (e.g. BiquadFilterNode[]) between the audio source and the\n * analyser consumers. Pass an empty array to clear the chain.\n *\n * Safe to call while analyser consumers are active; the graph is rebuilt immediately.\n * Note: because `createMediaElementSource` can only be called once per element, the EQ and live\n * analyser share the same AudioContext. Calling both for the same element is supported.\n */\nexport function setProcessingChain(element: HTMLAudioElement, nodes: AudioNode[]): void {\n if (typeof window === \"undefined\") return;\n\n if (nodes.length === 0) {\n const entry = entries.get(element);\n if (!entry) return;\n entry.processingChain = [];\n entry.processingActive = false;\n if (entry.consumers.size === 0) {\n maybeCloseEntry(element, entry);\n } else {\n rebuildGraph(entry);\n }\n return;\n }\n\n const entry = getOrCreateEntry(element);\n entry.processingChain = nodes;\n entry.processingActive = true;\n rebuildGraph(entry);\n}\n"],"names":["entries","clampFftSize","n","p","getChainTail","entry","processingChain","rebuildGraph","source","consumers","context","node","analyser","i","tail","isFirst","getOrCreateEntry","element","Context","maybeCloseEntry","attachLiveAnalyser","options","id","detachLiveAnalyser","consumer","setProcessingChain","nodes"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("react"),U=require("../useGinger-BXgia32v.cjs"),S="ginger-remote";function B(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`ginger-tab-${Math.random().toString(36).slice(2)}`}function F(_={}){const{channelName:w=S,heartbeatMs:b=2e3,electionTimeoutMs:N=300}=_,{state:r,init:k}=U.useGinger(),e=a.useRef("");e.current===""&&(e.current=B());const[l,i]=a.useState("pending"),[P,g]=a.useState(0),[G,M]=a.useState(null),t=a.useRef(l);t.current=l;const L=a.useRef(k);L.current=k;const T=a.useRef(null),o=a.useRef(new Set),E=a.useRef(Date.now()),I=a.useRef(null),s=a.useRef(null),m=a.useRef(null),R=a.useCallback(u=>{var c;(c=T.current)==null||c.postMessage(u)},[]),f=a.useCallback(()=>{I.current&&(clearTimeout(I.current),I.current=null)},[]),y=a.useCallback(()=>{s.current&&(clearInterval(s.current),s.current=null)},[]);a.useEffect(()=>{if(typeof window>"u"||typeof BroadcastChannel>"u"){M("BroadcastChannel is not available in this environment");return}M(null);const u=new BroadcastChannel(w);T.current=u,o.current=new Set([e.current]);const c=p=>{u.postMessage(p)},A=()=>{s.current&&clearInterval(s.current),s.current=setInterval(()=>{const p=e.current;o.current.add(p);const n=o.current.size;g(n),c({type:"HEARTBEAT",tabId:p,connectedCount:n})},b)},h=()=>{f(),y(),i("follower"),t.current="follower"},H=()=>{f(),i("leader"),t.current="leader",o.current.add(e.current),c({type:"LEADER_ANNOUNCE",tabId:e.current}),A()},v=()=>{f(),I.current=setTimeout(()=>{t.current==="pending"&&(i("leader"),t.current="leader",o.current.add(e.current),c({type:"LEADER_ANNOUNCE",tabId:e.current}),A())},N)},C=p=>{const n=p.data;if(!n||typeof n!="object"||!("type"in n))return;const d=e.current;switch(n.type){case"PING":{o.current.add(n.tabId),t.current==="leader"&&c({type:"PONG",tabId:d,leaderTabId:d});break}case"PONG":{n.leaderTabId&&n.leaderTabId!==d&&(h(),E.current=Date.now());break}case"LEADER_ANNOUNCE":{if(n.tabId===d)break;o.current.add(n.tabId),n.tabId<d?(h(),E.current=Date.now()):n.tabId>d&&(t.current==="pending"||t.current==="leader")&&H();break}case"LEADER_RESIGN":{if(n.tabId===d)break;E.current=Date.now(),t.current==="follower"&&(i("pending"),t.current="pending",c({type:"PING",tabId:d}),v());break}case"HEARTBEAT":{t.current==="follower"&&(E.current=Date.now(),g(n.connectedCount));break}case"STATE_SNAPSHOT":{t.current==="follower"&&n.tabId!==d&&L.current(n.snapshot);break}}};u.addEventListener("message",C),c({type:"PING",tabId:e.current}),I.current=setTimeout(()=>{t.current==="pending"&&(i("leader"),t.current="leader",o.current.add(e.current),c({type:"LEADER_ANNOUNCE",tabId:e.current}),A())},N),m.current=setInterval(()=>{t.current==="follower"&&Date.now()-E.current>b*2&&(i("pending"),t.current="pending",c({type:"PING",tabId:e.current}),v())},b);const D=()=>{t.current==="leader"&&c({type:"LEADER_RESIGN",tabId:e.current})};return window.addEventListener("pagehide",D),()=>{window.removeEventListener("pagehide",D),f(),y(),m.current&&(clearInterval(m.current),m.current=null),u.removeEventListener("message",C),t.current==="leader"&&c({type:"LEADER_RESIGN",tabId:e.current}),u.close(),T.current=null}},[w,f,N,b,y]),a.useEffect(()=>{if(l!=="leader")return;const u={tracks:r.tracks,currentIndex:r.currentIndex,playlistMeta:r.playlistMeta,isPaused:r.isPaused,isShuffled:!1,repeatMode:r.repeatMode,playbackMode:r.playbackMode,volume:r.volume,muted:r.muted,playbackRate:r.playbackRate};R({type:"STATE_SNAPSHOT",tabId:e.current,snapshot:u})},[l,r.tracks,r.currentIndex,r.isPaused,r.repeatMode,r.playbackMode,r.playlistMeta,r.volume,r.muted,r.playbackRate,R]);const O=a.useCallback(()=>{f(),y(),i("leader"),t.current="leader",o.current.add(e.current),R({type:"LEADER_ANNOUNCE",tabId:e.current}),s.current&&clearInterval(s.current),s.current=setInterval(()=>{const u=e.current;o.current.add(u);const c=o.current.size;g(c),R({type:"HEARTBEAT",tabId:u,connectedCount:c})},b)},[f,b,R,y]);return{isLeader:l==="leader",isFollower:l==="follower",isPending:l==="pending",connectedTabs:P,claimLeadership:O,error:G}}exports.DEFAULT_REMOTE_CHANNEL_NAME=S;exports.useGingerRemote=F;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../../src/remote/remoteProtocol.ts","../../src/remote/useGingerRemote.ts"],"sourcesContent":["import type { GingerInitPayload } from \"../types\";\n\nexport const DEFAULT_REMOTE_CHANNEL_NAME = \"ginger-remote\";\n\n/**\n * Cross-tab messages for {@link useGingerRemote}.\n */\nexport type RemoteMessage =\n | { type: \"PING\"; tabId: string }\n | { type: \"PONG\"; tabId: string; leaderTabId: string }\n | { type: \"LEADER_ANNOUNCE\"; tabId: string }\n | { type: \"LEADER_RESIGN\"; tabId: string }\n | { type: \"HEARTBEAT\"; tabId: string; connectedCount: number }\n | { type: \"STATE_SNAPSHOT\"; tabId: string; snapshot: GingerInitPayload };\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport type { GingerInitPayload } from \"../types\";\nimport { DEFAULT_REMOTE_CHANNEL_NAME, type RemoteMessage } from \"./remoteProtocol\";\n\nexport type UseGingerRemoteOptions = {\n /** BroadcastChannel name. Default: `\"ginger-remote\"`. */\n channelName?: string;\n /** Leader heartbeat interval in ms. Default: `2000`. */\n heartbeatMs?: number;\n /** Time to wait for an existing leader before claiming leadership. Default: `300`. */\n electionTimeoutMs?: number;\n};\n\nexport type UseGingerRemoteResult = {\n isLeader: boolean;\n isFollower: boolean;\n /** True until a leader is elected or this tab becomes leader. */\n isPending: boolean;\n connectedTabs: number;\n /** Request leadership (other tabs may win if their `tabId` is lexicographically smaller). */\n claimLeadership: () => void;\n error: string | null;\n};\n\nfunction makeTabId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `ginger-tab-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Multi-tab coordination via `BroadcastChannel`: elects a single leader tab and syncs\n * playback state to followers with `INIT` snapshots.\n *\n * Mount `Ginger.Player` only on the leader tab so one `<audio>` element plays:\n *\n * ```tsx\n * const { isLeader } = useGingerRemote();\n * return <>{isLeader && <Ginger.Player />}</>;\n * ```\n *\n * ```ts\n * import { useGingerRemote } from \"@lucaismyname/ginger/remote\";\n * ```\n */\nexport function useGingerRemote(options: UseGingerRemoteOptions = {}): UseGingerRemoteResult {\n const {\n channelName = DEFAULT_REMOTE_CHANNEL_NAME,\n heartbeatMs = 2000,\n electionTimeoutMs = 300,\n } = options;\n\n const { state, init } = useGinger();\n\n const tabIdRef = useRef<string>(\"\");\n if (tabIdRef.current === \"\") {\n tabIdRef.current = makeTabId();\n }\n\n const [role, setRole] = useState<\"pending\" | \"leader\" | \"follower\">(\"pending\");\n const [connectedTabs, setConnectedTabs] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const roleRef = useRef(role);\n roleRef.current = role;\n\n const initRef = useRef(init);\n initRef.current = init;\n\n const channelRef = useRef<BroadcastChannel | null>(null);\n const knownTabsRef = useRef<Set<string>>(new Set());\n const lastHeartbeatAtRef = useRef<number>(Date.now());\n const electionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const heartbeatTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n const leaderWatchRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const post = useCallback((msg: RemoteMessage) => {\n channelRef.current?.postMessage(msg);\n }, []);\n\n const clearElectionTimer = useCallback(() => {\n if (electionTimerRef.current) {\n clearTimeout(electionTimerRef.current);\n electionTimerRef.current = null;\n }\n }, []);\n\n const stopHeartbeat = useCallback(() => {\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n heartbeatTimerRef.current = null;\n }\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n setError(\"BroadcastChannel is not available in this environment\");\n return;\n }\n\n setError(null);\n const ch = new BroadcastChannel(channelName);\n channelRef.current = ch;\n knownTabsRef.current = new Set([tabIdRef.current]);\n\n const postMsg = (msg: RemoteMessage) => {\n ch.postMessage(msg);\n };\n\n const startLeaderHeartbeat = () => {\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n }\n heartbeatTimerRef.current = setInterval(() => {\n const leaderId = tabIdRef.current;\n knownTabsRef.current.add(leaderId);\n const count = knownTabsRef.current.size;\n setConnectedTabs(count);\n postMsg({ type: \"HEARTBEAT\", tabId: leaderId, connectedCount: count });\n }, heartbeatMs);\n };\n\n const becomeFollower = () => {\n clearElectionTimer();\n stopHeartbeat();\n setRole(\"follower\");\n roleRef.current = \"follower\";\n };\n\n const becomeLeaderFromRemote = () => {\n clearElectionTimer();\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n };\n\n const scheduleElection = () => {\n clearElectionTimer();\n electionTimerRef.current = setTimeout(() => {\n if (roleRef.current === \"pending\") {\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n }\n }, electionTimeoutMs);\n };\n\n const onMessage = (ev: MessageEvent<RemoteMessage>) => {\n const msg = ev.data;\n if (!msg || typeof msg !== \"object\" || !(\"type\" in msg)) return;\n\n const myId = tabIdRef.current;\n\n switch (msg.type) {\n case \"PING\": {\n knownTabsRef.current.add(msg.tabId);\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"PONG\", tabId: myId, leaderTabId: myId });\n }\n break;\n }\n case \"PONG\": {\n if (msg.leaderTabId && msg.leaderTabId !== myId) {\n becomeFollower();\n lastHeartbeatAtRef.current = Date.now();\n }\n break;\n }\n case \"LEADER_ANNOUNCE\": {\n if (msg.tabId === myId) break;\n knownTabsRef.current.add(msg.tabId);\n if (msg.tabId < myId) {\n becomeFollower();\n lastHeartbeatAtRef.current = Date.now();\n } else if (\n msg.tabId > myId &&\n (roleRef.current === \"pending\" || roleRef.current === \"leader\")\n ) {\n becomeLeaderFromRemote();\n }\n break;\n }\n case \"LEADER_RESIGN\": {\n if (msg.tabId === myId) break;\n lastHeartbeatAtRef.current = Date.now();\n if (roleRef.current === \"follower\") {\n setRole(\"pending\");\n roleRef.current = \"pending\";\n postMsg({ type: \"PING\", tabId: myId });\n scheduleElection();\n }\n break;\n }\n case \"HEARTBEAT\": {\n if (roleRef.current === \"follower\") {\n lastHeartbeatAtRef.current = Date.now();\n setConnectedTabs(msg.connectedCount);\n }\n break;\n }\n case \"STATE_SNAPSHOT\": {\n if (roleRef.current === \"follower\" && msg.tabId !== myId) {\n initRef.current(msg.snapshot);\n }\n break;\n }\n default:\n break;\n }\n };\n\n ch.addEventListener(\"message\", onMessage);\n\n postMsg({ type: \"PING\", tabId: tabIdRef.current });\n\n electionTimerRef.current = setTimeout(() => {\n if (roleRef.current === \"pending\") {\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n }\n }, electionTimeoutMs);\n\n leaderWatchRef.current = setInterval(() => {\n if (roleRef.current !== \"follower\") return;\n if (Date.now() - lastHeartbeatAtRef.current > heartbeatMs * 2) {\n setRole(\"pending\");\n roleRef.current = \"pending\";\n postMsg({ type: \"PING\", tabId: tabIdRef.current });\n scheduleElection();\n }\n }, heartbeatMs);\n\n const onPageHide = () => {\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"LEADER_RESIGN\", tabId: tabIdRef.current });\n }\n };\n window.addEventListener(\"pagehide\", onPageHide);\n\n return () => {\n window.removeEventListener(\"pagehide\", onPageHide);\n clearElectionTimer();\n stopHeartbeat();\n if (leaderWatchRef.current) {\n clearInterval(leaderWatchRef.current);\n leaderWatchRef.current = null;\n }\n ch.removeEventListener(\"message\", onMessage);\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"LEADER_RESIGN\", tabId: tabIdRef.current });\n }\n ch.close();\n channelRef.current = null;\n };\n }, [channelName, clearElectionTimer, electionTimeoutMs, heartbeatMs, stopHeartbeat]);\n\n useEffect(() => {\n if (role !== \"leader\") return;\n const snapshot: GingerInitPayload = {\n tracks: state.tracks,\n currentIndex: state.currentIndex,\n playlistMeta: state.playlistMeta,\n isPaused: state.isPaused,\n /** Avoid `createInitialState` re-shuffling on followers; queue order is already canonical. */\n isShuffled: false,\n repeatMode: state.repeatMode,\n playbackMode: state.playbackMode,\n volume: state.volume,\n muted: state.muted,\n playbackRate: state.playbackRate,\n };\n post({\n type: \"STATE_SNAPSHOT\",\n tabId: tabIdRef.current,\n snapshot,\n });\n }, [\n role,\n state.tracks,\n state.currentIndex,\n state.isPaused,\n state.repeatMode,\n state.playbackMode,\n state.playlistMeta,\n state.volume,\n state.muted,\n state.playbackRate,\n post,\n ]);\n\n const claimLeadership = useCallback(() => {\n clearElectionTimer();\n stopHeartbeat();\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n post({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n }\n heartbeatTimerRef.current = setInterval(() => {\n const leaderId = tabIdRef.current;\n knownTabsRef.current.add(leaderId);\n const count = knownTabsRef.current.size;\n setConnectedTabs(count);\n post({ type: \"HEARTBEAT\", tabId: leaderId, connectedCount: count });\n }, heartbeatMs);\n }, [clearElectionTimer, heartbeatMs, post, stopHeartbeat]);\n\n return {\n isLeader: role === \"leader\",\n isFollower: role === \"follower\",\n isPending: role === \"pending\",\n connectedTabs,\n claimLeadership,\n error,\n };\n}\n"],"names":["DEFAULT_REMOTE_CHANNEL_NAME","makeTabId","useGingerRemote","options","channelName","heartbeatMs","electionTimeoutMs","state","init","useGinger","tabIdRef","useRef","role","setRole","useState","connectedTabs","setConnectedTabs","error","setError","roleRef","initRef","channelRef","knownTabsRef","lastHeartbeatAtRef","electionTimerRef","heartbeatTimerRef","leaderWatchRef","post","useCallback","msg","_a","clearElectionTimer","stopHeartbeat","useEffect","ch","postMsg","startLeaderHeartbeat","leaderId","count","becomeFollower","becomeLeaderFromRemote","scheduleElection","onMessage","ev","myId","onPageHide","snapshot","claimLeadership"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_REMOTE_CHANNEL_NAME } from './remoteProtocol';
2
+ export type { RemoteMessage } from './remoteProtocol';
3
+ export { useGingerRemote } from './useGingerRemote';
4
+ export type { UseGingerRemoteOptions, UseGingerRemoteResult } from './useGingerRemote';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/remote/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,149 @@
1
+ import { useRef as s, useState as k, useCallback as N, useEffect as _ } from "react";
2
+ import { u as x } from "../useGinger-hpp2pAGY.js";
3
+ const F = "ginger-remote";
4
+ function z() {
5
+ return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `ginger-tab-${Math.random().toString(36).slice(2)}`;
6
+ }
7
+ function $(G = {}) {
8
+ const {
9
+ channelName: h = F,
10
+ heartbeatMs: p = 2e3,
11
+ electionTimeoutMs: T = 300
12
+ } = G, { state: r, init: v } = x(), e = s("");
13
+ e.current === "" && (e.current = z());
14
+ const [l, i] = k("pending"), [H, g] = k(0), [O, L] = k(null), t = s(l);
15
+ t.current = l;
16
+ const M = s(v);
17
+ M.current = v;
18
+ const A = s(null), o = s(/* @__PURE__ */ new Set()), I = s(Date.now()), E = s(null), u = s(null), R = s(null), m = N((c) => {
19
+ var a;
20
+ (a = A.current) == null || a.postMessage(c);
21
+ }, []), f = N(() => {
22
+ E.current && (clearTimeout(E.current), E.current = null);
23
+ }, []), y = N(() => {
24
+ u.current && (clearInterval(u.current), u.current = null);
25
+ }, []);
26
+ _(() => {
27
+ if (typeof window > "u" || typeof BroadcastChannel > "u") {
28
+ L("BroadcastChannel is not available in this environment");
29
+ return;
30
+ }
31
+ L(null);
32
+ const c = new BroadcastChannel(h);
33
+ A.current = c, o.current = /* @__PURE__ */ new Set([e.current]);
34
+ const a = (b) => {
35
+ c.postMessage(b);
36
+ }, w = () => {
37
+ u.current && clearInterval(u.current), u.current = setInterval(() => {
38
+ const b = e.current;
39
+ o.current.add(b);
40
+ const n = o.current.size;
41
+ g(n), a({ type: "HEARTBEAT", tabId: b, connectedCount: n });
42
+ }, p);
43
+ }, D = () => {
44
+ f(), y(), i("follower"), t.current = "follower";
45
+ }, B = () => {
46
+ f(), i("leader"), t.current = "leader", o.current.add(e.current), a({ type: "LEADER_ANNOUNCE", tabId: e.current }), w();
47
+ }, C = () => {
48
+ f(), E.current = setTimeout(() => {
49
+ t.current === "pending" && (i("leader"), t.current = "leader", o.current.add(e.current), a({ type: "LEADER_ANNOUNCE", tabId: e.current }), w());
50
+ }, T);
51
+ }, S = (b) => {
52
+ const n = b.data;
53
+ if (!n || typeof n != "object" || !("type" in n)) return;
54
+ const d = e.current;
55
+ switch (n.type) {
56
+ case "PING": {
57
+ o.current.add(n.tabId), t.current === "leader" && a({ type: "PONG", tabId: d, leaderTabId: d });
58
+ break;
59
+ }
60
+ case "PONG": {
61
+ n.leaderTabId && n.leaderTabId !== d && (D(), I.current = Date.now());
62
+ break;
63
+ }
64
+ case "LEADER_ANNOUNCE": {
65
+ if (n.tabId === d) break;
66
+ o.current.add(n.tabId), n.tabId < d ? (D(), I.current = Date.now()) : n.tabId > d && (t.current === "pending" || t.current === "leader") && B();
67
+ break;
68
+ }
69
+ case "LEADER_RESIGN": {
70
+ if (n.tabId === d) break;
71
+ I.current = Date.now(), t.current === "follower" && (i("pending"), t.current = "pending", a({ type: "PING", tabId: d }), C());
72
+ break;
73
+ }
74
+ case "HEARTBEAT": {
75
+ t.current === "follower" && (I.current = Date.now(), g(n.connectedCount));
76
+ break;
77
+ }
78
+ case "STATE_SNAPSHOT": {
79
+ t.current === "follower" && n.tabId !== d && M.current(n.snapshot);
80
+ break;
81
+ }
82
+ }
83
+ };
84
+ c.addEventListener("message", S), a({ type: "PING", tabId: e.current }), E.current = setTimeout(() => {
85
+ t.current === "pending" && (i("leader"), t.current = "leader", o.current.add(e.current), a({ type: "LEADER_ANNOUNCE", tabId: e.current }), w());
86
+ }, T), R.current = setInterval(() => {
87
+ t.current === "follower" && Date.now() - I.current > p * 2 && (i("pending"), t.current = "pending", a({ type: "PING", tabId: e.current }), C());
88
+ }, p);
89
+ const P = () => {
90
+ t.current === "leader" && a({ type: "LEADER_RESIGN", tabId: e.current });
91
+ };
92
+ return window.addEventListener("pagehide", P), () => {
93
+ window.removeEventListener("pagehide", P), f(), y(), R.current && (clearInterval(R.current), R.current = null), c.removeEventListener("message", S), t.current === "leader" && a({ type: "LEADER_RESIGN", tabId: e.current }), c.close(), A.current = null;
94
+ };
95
+ }, [h, f, T, p, y]), _(() => {
96
+ if (l !== "leader") return;
97
+ const c = {
98
+ tracks: r.tracks,
99
+ currentIndex: r.currentIndex,
100
+ playlistMeta: r.playlistMeta,
101
+ isPaused: r.isPaused,
102
+ /** Avoid `createInitialState` re-shuffling on followers; queue order is already canonical. */
103
+ isShuffled: !1,
104
+ repeatMode: r.repeatMode,
105
+ playbackMode: r.playbackMode,
106
+ volume: r.volume,
107
+ muted: r.muted,
108
+ playbackRate: r.playbackRate
109
+ };
110
+ m({
111
+ type: "STATE_SNAPSHOT",
112
+ tabId: e.current,
113
+ snapshot: c
114
+ });
115
+ }, [
116
+ l,
117
+ r.tracks,
118
+ r.currentIndex,
119
+ r.isPaused,
120
+ r.repeatMode,
121
+ r.playbackMode,
122
+ r.playlistMeta,
123
+ r.volume,
124
+ r.muted,
125
+ r.playbackRate,
126
+ m
127
+ ]);
128
+ const U = N(() => {
129
+ f(), y(), i("leader"), t.current = "leader", o.current.add(e.current), m({ type: "LEADER_ANNOUNCE", tabId: e.current }), u.current && clearInterval(u.current), u.current = setInterval(() => {
130
+ const c = e.current;
131
+ o.current.add(c);
132
+ const a = o.current.size;
133
+ g(a), m({ type: "HEARTBEAT", tabId: c, connectedCount: a });
134
+ }, p);
135
+ }, [f, p, m, y]);
136
+ return {
137
+ isLeader: l === "leader",
138
+ isFollower: l === "follower",
139
+ isPending: l === "pending",
140
+ connectedTabs: H,
141
+ claimLeadership: U,
142
+ error: O
143
+ };
144
+ }
145
+ export {
146
+ F as DEFAULT_REMOTE_CHANNEL_NAME,
147
+ $ as useGingerRemote
148
+ };
149
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/remote/remoteProtocol.ts","../../src/remote/useGingerRemote.ts"],"sourcesContent":["import type { GingerInitPayload } from \"../types\";\n\nexport const DEFAULT_REMOTE_CHANNEL_NAME = \"ginger-remote\";\n\n/**\n * Cross-tab messages for {@link useGingerRemote}.\n */\nexport type RemoteMessage =\n | { type: \"PING\"; tabId: string }\n | { type: \"PONG\"; tabId: string; leaderTabId: string }\n | { type: \"LEADER_ANNOUNCE\"; tabId: string }\n | { type: \"LEADER_RESIGN\"; tabId: string }\n | { type: \"HEARTBEAT\"; tabId: string; connectedCount: number }\n | { type: \"STATE_SNAPSHOT\"; tabId: string; snapshot: GingerInitPayload };\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport type { GingerInitPayload } from \"../types\";\nimport { DEFAULT_REMOTE_CHANNEL_NAME, type RemoteMessage } from \"./remoteProtocol\";\n\nexport type UseGingerRemoteOptions = {\n /** BroadcastChannel name. Default: `\"ginger-remote\"`. */\n channelName?: string;\n /** Leader heartbeat interval in ms. Default: `2000`. */\n heartbeatMs?: number;\n /** Time to wait for an existing leader before claiming leadership. Default: `300`. */\n electionTimeoutMs?: number;\n};\n\nexport type UseGingerRemoteResult = {\n isLeader: boolean;\n isFollower: boolean;\n /** True until a leader is elected or this tab becomes leader. */\n isPending: boolean;\n connectedTabs: number;\n /** Request leadership (other tabs may win if their `tabId` is lexicographically smaller). */\n claimLeadership: () => void;\n error: string | null;\n};\n\nfunction makeTabId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `ginger-tab-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Multi-tab coordination via `BroadcastChannel`: elects a single leader tab and syncs\n * playback state to followers with `INIT` snapshots.\n *\n * Mount `Ginger.Player` only on the leader tab so one `<audio>` element plays:\n *\n * ```tsx\n * const { isLeader } = useGingerRemote();\n * return <>{isLeader && <Ginger.Player />}</>;\n * ```\n *\n * ```ts\n * import { useGingerRemote } from \"@lucaismyname/ginger/remote\";\n * ```\n */\nexport function useGingerRemote(options: UseGingerRemoteOptions = {}): UseGingerRemoteResult {\n const {\n channelName = DEFAULT_REMOTE_CHANNEL_NAME,\n heartbeatMs = 2000,\n electionTimeoutMs = 300,\n } = options;\n\n const { state, init } = useGinger();\n\n const tabIdRef = useRef<string>(\"\");\n if (tabIdRef.current === \"\") {\n tabIdRef.current = makeTabId();\n }\n\n const [role, setRole] = useState<\"pending\" | \"leader\" | \"follower\">(\"pending\");\n const [connectedTabs, setConnectedTabs] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const roleRef = useRef(role);\n roleRef.current = role;\n\n const initRef = useRef(init);\n initRef.current = init;\n\n const channelRef = useRef<BroadcastChannel | null>(null);\n const knownTabsRef = useRef<Set<string>>(new Set());\n const lastHeartbeatAtRef = useRef<number>(Date.now());\n const electionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const heartbeatTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n const leaderWatchRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const post = useCallback((msg: RemoteMessage) => {\n channelRef.current?.postMessage(msg);\n }, []);\n\n const clearElectionTimer = useCallback(() => {\n if (electionTimerRef.current) {\n clearTimeout(electionTimerRef.current);\n electionTimerRef.current = null;\n }\n }, []);\n\n const stopHeartbeat = useCallback(() => {\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n heartbeatTimerRef.current = null;\n }\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n setError(\"BroadcastChannel is not available in this environment\");\n return;\n }\n\n setError(null);\n const ch = new BroadcastChannel(channelName);\n channelRef.current = ch;\n knownTabsRef.current = new Set([tabIdRef.current]);\n\n const postMsg = (msg: RemoteMessage) => {\n ch.postMessage(msg);\n };\n\n const startLeaderHeartbeat = () => {\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n }\n heartbeatTimerRef.current = setInterval(() => {\n const leaderId = tabIdRef.current;\n knownTabsRef.current.add(leaderId);\n const count = knownTabsRef.current.size;\n setConnectedTabs(count);\n postMsg({ type: \"HEARTBEAT\", tabId: leaderId, connectedCount: count });\n }, heartbeatMs);\n };\n\n const becomeFollower = () => {\n clearElectionTimer();\n stopHeartbeat();\n setRole(\"follower\");\n roleRef.current = \"follower\";\n };\n\n const becomeLeaderFromRemote = () => {\n clearElectionTimer();\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n };\n\n const scheduleElection = () => {\n clearElectionTimer();\n electionTimerRef.current = setTimeout(() => {\n if (roleRef.current === \"pending\") {\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n }\n }, electionTimeoutMs);\n };\n\n const onMessage = (ev: MessageEvent<RemoteMessage>) => {\n const msg = ev.data;\n if (!msg || typeof msg !== \"object\" || !(\"type\" in msg)) return;\n\n const myId = tabIdRef.current;\n\n switch (msg.type) {\n case \"PING\": {\n knownTabsRef.current.add(msg.tabId);\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"PONG\", tabId: myId, leaderTabId: myId });\n }\n break;\n }\n case \"PONG\": {\n if (msg.leaderTabId && msg.leaderTabId !== myId) {\n becomeFollower();\n lastHeartbeatAtRef.current = Date.now();\n }\n break;\n }\n case \"LEADER_ANNOUNCE\": {\n if (msg.tabId === myId) break;\n knownTabsRef.current.add(msg.tabId);\n if (msg.tabId < myId) {\n becomeFollower();\n lastHeartbeatAtRef.current = Date.now();\n } else if (\n msg.tabId > myId &&\n (roleRef.current === \"pending\" || roleRef.current === \"leader\")\n ) {\n becomeLeaderFromRemote();\n }\n break;\n }\n case \"LEADER_RESIGN\": {\n if (msg.tabId === myId) break;\n lastHeartbeatAtRef.current = Date.now();\n if (roleRef.current === \"follower\") {\n setRole(\"pending\");\n roleRef.current = \"pending\";\n postMsg({ type: \"PING\", tabId: myId });\n scheduleElection();\n }\n break;\n }\n case \"HEARTBEAT\": {\n if (roleRef.current === \"follower\") {\n lastHeartbeatAtRef.current = Date.now();\n setConnectedTabs(msg.connectedCount);\n }\n break;\n }\n case \"STATE_SNAPSHOT\": {\n if (roleRef.current === \"follower\" && msg.tabId !== myId) {\n initRef.current(msg.snapshot);\n }\n break;\n }\n default:\n break;\n }\n };\n\n ch.addEventListener(\"message\", onMessage);\n\n postMsg({ type: \"PING\", tabId: tabIdRef.current });\n\n electionTimerRef.current = setTimeout(() => {\n if (roleRef.current === \"pending\") {\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n }\n }, electionTimeoutMs);\n\n leaderWatchRef.current = setInterval(() => {\n if (roleRef.current !== \"follower\") return;\n if (Date.now() - lastHeartbeatAtRef.current > heartbeatMs * 2) {\n setRole(\"pending\");\n roleRef.current = \"pending\";\n postMsg({ type: \"PING\", tabId: tabIdRef.current });\n scheduleElection();\n }\n }, heartbeatMs);\n\n const onPageHide = () => {\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"LEADER_RESIGN\", tabId: tabIdRef.current });\n }\n };\n window.addEventListener(\"pagehide\", onPageHide);\n\n return () => {\n window.removeEventListener(\"pagehide\", onPageHide);\n clearElectionTimer();\n stopHeartbeat();\n if (leaderWatchRef.current) {\n clearInterval(leaderWatchRef.current);\n leaderWatchRef.current = null;\n }\n ch.removeEventListener(\"message\", onMessage);\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"LEADER_RESIGN\", tabId: tabIdRef.current });\n }\n ch.close();\n channelRef.current = null;\n };\n }, [channelName, clearElectionTimer, electionTimeoutMs, heartbeatMs, stopHeartbeat]);\n\n useEffect(() => {\n if (role !== \"leader\") return;\n const snapshot: GingerInitPayload = {\n tracks: state.tracks,\n currentIndex: state.currentIndex,\n playlistMeta: state.playlistMeta,\n isPaused: state.isPaused,\n /** Avoid `createInitialState` re-shuffling on followers; queue order is already canonical. */\n isShuffled: false,\n repeatMode: state.repeatMode,\n playbackMode: state.playbackMode,\n volume: state.volume,\n muted: state.muted,\n playbackRate: state.playbackRate,\n };\n post({\n type: \"STATE_SNAPSHOT\",\n tabId: tabIdRef.current,\n snapshot,\n });\n }, [\n role,\n state.tracks,\n state.currentIndex,\n state.isPaused,\n state.repeatMode,\n state.playbackMode,\n state.playlistMeta,\n state.volume,\n state.muted,\n state.playbackRate,\n post,\n ]);\n\n const claimLeadership = useCallback(() => {\n clearElectionTimer();\n stopHeartbeat();\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n post({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n }\n heartbeatTimerRef.current = setInterval(() => {\n const leaderId = tabIdRef.current;\n knownTabsRef.current.add(leaderId);\n const count = knownTabsRef.current.size;\n setConnectedTabs(count);\n post({ type: \"HEARTBEAT\", tabId: leaderId, connectedCount: count });\n }, heartbeatMs);\n }, [clearElectionTimer, heartbeatMs, post, stopHeartbeat]);\n\n return {\n isLeader: role === \"leader\",\n isFollower: role === \"follower\",\n isPending: role === \"pending\",\n connectedTabs,\n claimLeadership,\n error,\n };\n}\n"],"names":["DEFAULT_REMOTE_CHANNEL_NAME","makeTabId","useGingerRemote","options","channelName","heartbeatMs","electionTimeoutMs","state","init","useGinger","tabIdRef","useRef","role","setRole","useState","connectedTabs","setConnectedTabs","error","setError","roleRef","initRef","channelRef","knownTabsRef","lastHeartbeatAtRef","electionTimerRef","heartbeatTimerRef","leaderWatchRef","post","useCallback","msg","_a","clearElectionTimer","stopHeartbeat","useEffect","ch","postMsg","startLeaderHeartbeat","leaderId","count","becomeFollower","becomeLeaderFromRemote","scheduleElection","onMessage","ev","myId","onPageHide","snapshot","claimLeadership"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { GingerInitPayload } from '../types';
2
+ export declare const DEFAULT_REMOTE_CHANNEL_NAME = "ginger-remote";
3
+ /**
4
+ * Cross-tab messages for {@link useGingerRemote}.
5
+ */
6
+ export type RemoteMessage = {
7
+ type: "PING";
8
+ tabId: string;
9
+ } | {
10
+ type: "PONG";
11
+ tabId: string;
12
+ leaderTabId: string;
13
+ } | {
14
+ type: "LEADER_ANNOUNCE";
15
+ tabId: string;
16
+ } | {
17
+ type: "LEADER_RESIGN";
18
+ tabId: string;
19
+ } | {
20
+ type: "HEARTBEAT";
21
+ tabId: string;
22
+ connectedCount: number;
23
+ } | {
24
+ type: "STATE_SNAPSHOT";
25
+ tabId: string;
26
+ snapshot: GingerInitPayload;
27
+ };
28
+ //# sourceMappingURL=remoteProtocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remoteProtocol.d.ts","sourceRoot":"","sources":["../../src/remote/remoteProtocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,eAAO,MAAM,2BAA2B,kBAAkB,CAAC;AAE3D;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,CAAC"}
@@ -0,0 +1,35 @@
1
+ export type UseGingerRemoteOptions = {
2
+ /** BroadcastChannel name. Default: `"ginger-remote"`. */
3
+ channelName?: string;
4
+ /** Leader heartbeat interval in ms. Default: `2000`. */
5
+ heartbeatMs?: number;
6
+ /** Time to wait for an existing leader before claiming leadership. Default: `300`. */
7
+ electionTimeoutMs?: number;
8
+ };
9
+ export type UseGingerRemoteResult = {
10
+ isLeader: boolean;
11
+ isFollower: boolean;
12
+ /** True until a leader is elected or this tab becomes leader. */
13
+ isPending: boolean;
14
+ connectedTabs: number;
15
+ /** Request leadership (other tabs may win if their `tabId` is lexicographically smaller). */
16
+ claimLeadership: () => void;
17
+ error: string | null;
18
+ };
19
+ /**
20
+ * Multi-tab coordination via `BroadcastChannel`: elects a single leader tab and syncs
21
+ * playback state to followers with `INIT` snapshots.
22
+ *
23
+ * Mount `Ginger.Player` only on the leader tab so one `<audio>` element plays:
24
+ *
25
+ * ```tsx
26
+ * const { isLeader } = useGingerRemote();
27
+ * return <>{isLeader && <Ginger.Player />}</>;
28
+ * ```
29
+ *
30
+ * ```ts
31
+ * import { useGingerRemote } from "@lucaismyname/ginger/remote";
32
+ * ```
33
+ */
34
+ export declare function useGingerRemote(options?: UseGingerRemoteOptions): UseGingerRemoteResult;
35
+ //# sourceMappingURL=useGingerRemote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGingerRemote.d.ts","sourceRoot":"","sources":["../../src/remote/useGingerRemote.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,sBAAsB,GAAG;IACnC,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,iEAAiE;IACjE,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AASF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,qBAAqB,CAuR3F"}