@coventia/softphone 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/{call-CaPDWVk3.d.cts → call-FqcvDg1t.d.cts} +168 -12
  2. package/dist/{call-CaPDWVk3.d.ts → call-FqcvDg1t.d.ts} +168 -12
  3. package/dist/chunk-5E5OEXS2.js +14 -0
  4. package/dist/chunk-5E5OEXS2.js.map +1 -0
  5. package/dist/{chunk-EYEDPD7Y.js → chunk-6RR6G35E.js} +92 -17
  6. package/dist/chunk-6RR6G35E.js.map +1 -0
  7. package/dist/{chunk-2ZSOGGPO.js → chunk-FRXHEHQA.js} +15 -2
  8. package/dist/chunk-FRXHEHQA.js.map +1 -0
  9. package/dist/{chunk-UHNLEBBO.js → chunk-I3RSC52R.js} +28 -3
  10. package/dist/chunk-I3RSC52R.js.map +1 -0
  11. package/dist/chunk-M76SAXXM.js +13 -0
  12. package/dist/chunk-M76SAXXM.js.map +1 -0
  13. package/dist/{chunk-7R6LOOAI.js → chunk-N4PLEYFJ.js} +4 -2
  14. package/dist/chunk-N4PLEYFJ.js.map +1 -0
  15. package/dist/element/index.d.ts +198 -6
  16. package/dist/element/index.js +1160 -489
  17. package/dist/element/index.js.map +1 -1
  18. package/dist/{engine-BgBsIcEa.d.ts → engine-CsNRD7B7.d.ts} +1 -1
  19. package/dist/{engine-CU2rEo3g.d.cts → engine-CzOCrfY9.d.cts} +1 -1
  20. package/dist/index.cjs +675 -53
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +16 -6
  23. package/dist/index.d.ts +16 -6
  24. package/dist/index.js +453 -37
  25. package/dist/index.js.map +1 -1
  26. package/dist/react/index.cjs +26 -0
  27. package/dist/react/index.cjs.map +1 -1
  28. package/dist/react/index.d.cts +24 -5
  29. package/dist/react/index.d.ts +24 -5
  30. package/dist/react/index.js +2 -2
  31. package/dist/{softphone-DEt0TtQw.d.ts → softphone-By9sizlV.d.ts} +44 -3
  32. package/dist/{softphone-dZMkW8V1.d.cts → softphone-JZvJdzU3.d.cts} +44 -3
  33. package/dist/softphone.css +254 -23
  34. package/dist/{sources-M3GYMS6V.js → sources-QL2LRZVH.js} +93 -8
  35. package/dist/sources-QL2LRZVH.js.map +1 -0
  36. package/dist/testing/index.cjs +13 -0
  37. package/dist/testing/index.cjs.map +1 -1
  38. package/dist/testing/index.d.cts +2 -2
  39. package/dist/testing/index.d.ts +2 -2
  40. package/dist/testing/index.js +1 -1
  41. package/dist/ui/index.cjs +670 -20
  42. package/dist/ui/index.cjs.map +1 -1
  43. package/dist/ui/index.d.cts +2 -2
  44. package/dist/ui/index.d.ts +2 -2
  45. package/dist/ui/index.js +642 -22
  46. package/dist/ui/index.js.map +1 -1
  47. package/dist/zadarma/index.cjs +111 -14
  48. package/dist/zadarma/index.cjs.map +1 -1
  49. package/dist/zadarma/index.d.cts +5 -6
  50. package/dist/zadarma/index.d.ts +5 -6
  51. package/dist/zadarma/index.js +3 -2
  52. package/dist/zadarma-engine-TIUE7PZ7.js +6 -0
  53. package/dist/{zadarma-engine-L5DEFXUS.js.map → zadarma-engine-TIUE7PZ7.js.map} +1 -1
  54. package/package.json +1 -1
  55. package/dist/chunk-2ZSOGGPO.js.map +0 -1
  56. package/dist/chunk-7R6LOOAI.js.map +0 -1
  57. package/dist/chunk-EYEDPD7Y.js.map +0 -1
  58. package/dist/chunk-UHNLEBBO.js.map +0 -1
  59. package/dist/sources-M3GYMS6V.js.map +0 -1
  60. package/dist/zadarma-engine-L5DEFXUS.js +0 -5
@@ -46,6 +46,31 @@ function useIsLeader() {
46
46
  react.useCallback(() => phone.isLeader, [phone])
47
47
  );
48
48
  }
49
+ function useLine() {
50
+ const phone = useSoftphone();
51
+ const cache = react.useRef(null);
52
+ return react.useSyncExternalStore(
53
+ react.useCallback(
54
+ (cb) => {
55
+ const offLine = phone.on("lineChanged" /* LineChanged */, () => cb());
56
+ const offClaim = phone.on("lineClaimChanged" /* LineClaimChanged */, () => cb());
57
+ return () => {
58
+ offLine();
59
+ offClaim();
60
+ };
61
+ },
62
+ [phone]
63
+ ),
64
+ react.useCallback(() => {
65
+ const { line, lineWhere: where, lineClaim: claim } = phone;
66
+ const cached = cache.current;
67
+ if (cached?.line === line && cached.where === where && cached.claim === claim) return cached;
68
+ const view = { line, where, claim };
69
+ cache.current = view;
70
+ return view;
71
+ }, [phone])
72
+ );
73
+ }
49
74
  function useActiveCall() {
50
75
  const phone = useSoftphone();
51
76
  return react.useSyncExternalStore(
@@ -148,6 +173,7 @@ exports.useCall = useCall;
148
173
  exports.useDevices = useDevices;
149
174
  exports.useIsLeader = useIsLeader;
150
175
  exports.useLastError = useLastError;
176
+ exports.useLine = useLine;
151
177
  exports.usePresence = usePresence;
152
178
  exports.useSoftphone = useSoftphone;
153
179
  exports.useSoftphoneState = useSoftphoneState;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/context.tsx","../../src/react/hooks.ts"],"names":["createContext","useContext","useSyncExternalStore","useCallback","useRef"],"mappings":";;;;;;AAYA,IAAM,gBAAA,GAAmBA,oBAAgC,IAAI,CAAA;AAOtD,SAAS,iBAAA,CAAkB,EAAE,SAAA,EAAW,QAAA,EAAS,EAA2B;AACjF,EAAA,sCAAQ,gBAAA,CAAiB,QAAA,EAAjB,EAA0B,KAAA,EAAO,WAAY,QAAA,EAAS,CAAA;AAChE;AAGO,SAAS,YAAA,GAA0B;AACxC,EAAA,MAAM,SAAA,GAAYC,iBAAW,gBAAgB,CAAA;AAC7C,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAM,IAAI,MAAM,2DAA2D,CAAA;AAAA,EAC7E;AACA,EAAA,OAAO,SAAA;AACT;ACTO,SAAS,YAAA,GAA2C;AACzD,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOC,0BAAA;AAAA,IACLC,iBAAA;AAAA,MACE,CAAC,EAAA,KAAmB;AAClB,QAAA,MAAM,QAAA,GAAW,KAAA,CAAM,EAAA,CAAA,OAAA,cAAyB,MAAM,IAAI,CAAA;AAC1D,QAAA,MAAM,QAAA,GAAW,KAAA,CAAM,EAAA,CAAA,cAAA,qBAAgC,MAAM,IAAI,CAAA;AACjE,QAAA,OAAO,MAAM;AACX,UAAA,QAAA,EAAS;AACT,UAAA,QAAA,EAAS;AAAA,QACX,CAAA;AAAA,MACF,CAAA;AAAA,MACA,CAAC,KAAK;AAAA,KACR;AAAA,IACAA,kBAAY,MAAM,KAAA,CAAM,SAAA,EAAW,CAAC,KAAK,CAAC;AAAA,GAC5C;AACF;AAEO,SAAS,iBAAA,GAAoC;AAClD,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOD,0BAAA;AAAA,IACLC,iBAAA,CAAY,CAAC,EAAA,KAAmB,KAAA,CAAM,EAAA,CAAA,cAAA,qBAAgC,MAAM,EAAA,EAAI,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAAA,IAC1FA,kBAAY,MAAM,KAAA,CAAM,KAAA,EAAO,CAAC,KAAK,CAAC;AAAA,GACxC;AACF;AAOO,SAAS,WAAA,GAAuB;AACrC,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOD,0BAAA;AAAA,IACLC,iBAAA,CAAY,CAAC,EAAA,KAAmB,KAAA,CAAM,EAAA,CAAA,eAAA,sBAAiC,MAAM,EAAA,EAAI,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAAA,IAC3FA,kBAAY,MAAM,KAAA,CAAM,QAAA,EAAU,CAAC,KAAK,CAAC;AAAA,GAC3C;AACF;AAOO,SAAS,aAAA,GAA6B;AAC3C,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOD,0BAAA;AAAA,IACLC,iBAAA,CAAY,CAAC,EAAA,KAAmB,KAAA,CAAM,EAAA,CAAA,mBAAA,0BAAqC,MAAM,EAAA,EAAI,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAAA,IAC/FA,kBAAY,MAAM,KAAA,CAAM,UAAA,EAAY,CAAC,KAAK,CAAC;AAAA,GAC7C;AACF;AAYO,SAAS,YAAY,MAAA,EAA2C;AACrE,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOD,0BAAA;AAAA,IACLC,iBAAA;AAAA,MACE,CAAC,EAAA,KAAmB;AAClB,QAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,MAAM;AAAA,QAAC,CAAA;AAC3B,QAAA,OAAO,KAAA,CAAM,EAAA,CAAA,iBAAA,wBAAmC,MAAM,EAAA,EAAI,CAAA;AAAA,MAC5D,CAAA;AAAA,MACA,CAAC,OAAO,MAAM;AAAA,KAChB;AAAA,IACAA,kBAAY,MAAM,KAAA,CAAM,QAAA,EAAU,CAAC,KAAK,CAAC;AAAA,GAC3C;AACF;AAYA,IAAM,WAAA,GAAoC;AAAA,EAAA,cAAA;AAAA,EAAA,OAAA;AAAA,EAAA,MAAA;AAAA,EAAA,QAAA;AAAA,EAAA,oBAAA;AAAA,EAAA,cAAA;AAAA,EAAA,OAAA;AAQ1C,CAAA;AASO,SAAS,QAAQ,IAAA,EAAoC;AAC1D,EAAA,MAAM,KAAA,GAAQC,aAA8C,IAAI,CAAA;AAEhE,EAAA,MAAM,SAAA,GAAYD,iBAAA;AAAA,IAChB,CAAC,EAAA,KAAmB;AAClB,MAAA,IAAI,CAAC,IAAA,EAAM,OAAO,MAAM;AAAA,MAAC,CAAA;AACzB,MAAA,MAAM,SAAS,WAAA,CAAY,GAAA;AAAA,QAAI,CAAC,KAAA,KAC9B,IAAA,CAAK,EAAA,CAAG,OAAO,MAAM;AACnB,UAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,UAAA,EAAA,EAAG;AAAA,QACL,CAAC;AAAA,OACH;AACA,MAAA,OAAO,MAAM;AACX,QAAA,KAAA,MAAW,GAAA,IAAO,QAAQ,GAAA,EAAI;AAAA,MAChC,CAAA;AAAA,IACF,CAAA;AAAA,IACA,CAAC,IAAI;AAAA,GACP;AAEA,EAAA,MAAM,WAAA,GAAcA,kBAAY,MAAuB;AACrD,IAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,IAAA,IAAI,MAAM,OAAA,EAAS,IAAA,KAAS,IAAA,EAAM,OAAO,MAAM,OAAA,CAAQ,IAAA;AACvD,IAAA,MAAM,IAAA,GAAiB;AAAA,MACrB,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,cAAc,IAAA,CAAK,YAAA;AAAA,MACnB,YAAY,IAAA,CAAK;AAAA,KACnB;AACA,IAAA,KAAA,CAAM,OAAA,GAAU,EAAE,IAAA,EAAM,IAAA,EAAK;AAC7B,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAET,EAAA,OAAOD,0BAAA,CAAqB,WAAW,WAAW,CAAA;AACpD;AAWO,SAAS,UAAA,GAA2B;AACzC,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,MAAM,KAAA,GAAQE,aAA2D,IAAI,CAAA;AAE7E,EAAA,MAAM,SAAA,GAAYD,iBAAA;AAAA,IAChB,CAAC,EAAA,KACC,KAAA,CAAM,EAAA,CAAA,cAAA,qBAAgC,MAAM;AAC1C,MAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,MAAA,EAAA,EAAG;AAAA,IACL,CAAC,CAAA;AAAA,IACH,CAAC,KAAK;AAAA,GACR;AAEA,EAAA,MAAM,WAAA,GAAcA,kBAAY,MAAoB;AAClD,IAAA,MAAM,QAAQ,KAAA,CAAM,KAAA;AACpB,IAAA,IAAI,MAAM,OAAA,EAAS,KAAA,KAAU,KAAA,EAAO,OAAO,MAAM,OAAA,CAAQ,IAAA;AACzD,IAAA,MAAM,IAAA,GAAqB;AAAA,MACzB,QAAQ,KAAA,CAAM,MAAA;AAAA,MACd,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,gBAAgB,KAAA,CAAM,cAAA;AAAA,MACtB,iBAAiB,KAAA,CAAM,eAAA;AAAA,MACvB,yBAAyB,KAAA,CAAM,uBAAA;AAAA,MAC/B,iBAAiB,KAAA,CAAM,eAAA;AAAA,MACvB,yBAAyB,KAAA,CAAM,uBAAA;AAAA,MAC/B,QAAA,EAAU,CAAC,QAAA,KAAa,KAAA,CAAM,SAAS,QAAQ,CAAA;AAAA,MAC/C,SAAA,EAAW,CAAC,QAAA,KAAa,KAAA,CAAM,UAAU,QAAQ,CAAA;AAAA,MACjD,iBAAA,EAAmB,CAAC,QAAA,KAAa,KAAA,CAAM,kBAAkB,QAAQ,CAAA;AAAA,MACjE,OAAA,EAAS,MAAM,KAAA,CAAM,OAAA;AAAQ,KAC/B;AACA,IAAA,KAAA,CAAM,OAAA,GAAU,EAAE,KAAA,EAAO,IAAA,EAAK;AAC9B,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,OAAOD,0BAAA,CAAqB,WAAW,WAAW,CAAA;AACpD","file":"index.cjs","sourcesContent":["import { createContext, useContext, type ReactNode } from 'react';\r\nimport type { Softphone } from '../core/softphone.js';\r\n\r\n/**\r\n * The provider takes an INSTANCE the host constructed — not config props.\r\n *\r\n * This is LiveKit's hard-won lesson (SPEC §5.1): they are deprecating their config-props\r\n * `<LiveKitRoom>` in favour of a provider that accepts an instance. Holding the `Softphone` above the\r\n * React tree lets the app own the connection, survive route changes and unmounts without dropping a\r\n * call, and unit-test the engine with no React at all. Config-props would rebuild the engine on\r\n * every prop change — catastrophic for something holding a live `RTCPeerConnection`.\r\n */\r\nconst SoftphoneContext = createContext<Softphone | null>(null);\r\n\r\nexport interface SoftphoneProviderProps {\r\n readonly softphone: Softphone;\r\n readonly children: ReactNode;\r\n}\r\n\r\nexport function SoftphoneProvider({ softphone, children }: SoftphoneProviderProps) {\r\n return <SoftphoneContext.Provider value={softphone}>{children}</SoftphoneContext.Provider>;\r\n}\r\n\r\n/** The `Softphone` instance from context. Stable — does not re-render on state changes. */\r\nexport function useSoftphone(): Softphone {\r\n const softphone = useContext(SoftphoneContext);\r\n if (!softphone) {\r\n throw new Error('useSoftphone() must be used within a <SoftphoneProvider>.');\r\n }\r\n return softphone;\r\n}\r\n","// Built-in, no shim: the peer floor is React 18 (SPEC §2.7), which ships useSyncExternalStore.\r\nimport { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useSoftphone } from './context.js';\r\nimport { SoftphoneEvent, CallEvent } from '../core/events.js';\r\nimport type { Call, CallState } from '../core/call.js';\r\nimport type { PresenceEntry, RemoteParty, SoftphoneState } from '../core/types.js';\r\nimport type { AudioDevices } from '../core/softphone.js';\r\nimport type { FatalSoftphoneError } from '../core/errors.js';\r\n\r\n/** Reactive softphone lifecycle state (`stopped` → `online` → …). */\r\n/**\r\n * The reason the phone cannot currently be used, or `null`.\r\n *\r\n * Reads `softphone.lastError`, which is STATE rather than only an event — the widget is usually\r\n * opened *after* something broke, and a listener mounted at that point would have missed the event\r\n * entirely and rendered a dialpad with no explanation.\r\n *\r\n * Re-reads on both channels because both can change it: `Error` sets it, and `StateChanged` clears it\r\n * on reaching `online`. Subscribing to only the first would leave a stale gate on screen after the\r\n * phone recovered.\r\n */\r\nexport function useLastError(): FatalSoftphoneError | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offError = phone.on(SoftphoneEvent.Error, () => cb());\r\n const offState = phone.on(SoftphoneEvent.StateChanged, () => cb());\r\n return () => {\r\n offError();\r\n offState();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback(() => phone.lastError, [phone]),\r\n );\r\n}\r\n\r\nexport function useSoftphoneState(): SoftphoneState {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.StateChanged, () => cb()), [phone]),\r\n useCallback(() => phone.state, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * Whether THIS tab is the one allowed to be reachable (`multi-tab-leader-election.md`). `false` by\r\n * default and while waiting for another open tab to release it — show your own \"active in another\r\n * tab\" state for that duration; the `/ui` widget already does.\r\n */\r\nexport function useIsLeader(): boolean {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.LeaderChanged, () => cb()), [phone]),\r\n useCallback(() => phone.isLeader, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.\r\n *\r\n * `phone.activeCall` returns a stable reference for a given call, so it is a safe snapshot.\r\n */\r\nexport function useActiveCall(): Call | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.ActiveCallChanged, () => cb()), [phone]),\r\n useCallback(() => phone.activeCall, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.\r\n * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket\r\n * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own\r\n * always-connected decision), so passing `false` here does not close it — it only stops this\r\n * particular component from re-rendering on every update while it has no reason to care, e.g. the\r\n * transfer step being closed (spec §4.5: \"subscribe when the step opens... unsubscribe when it\r\n * closes\"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`\r\n * never shows a stale snapshot.\r\n */\r\nexport function usePresence(active: boolean): readonly PresenceEntry[] {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n if (!active) return () => {};\r\n return phone.on(SoftphoneEvent.PresenceChanged, () => cb());\r\n },\r\n [phone, active],\r\n ),\r\n useCallback(() => phone.presence, [phone]),\r\n );\r\n}\r\n\r\n/** An immutable snapshot of a call's reactive fields. */\r\nexport interface CallView {\r\n readonly state: CallState;\r\n readonly isMuted: boolean;\r\n readonly isOnHold: boolean;\r\n readonly remote: RemoteParty;\r\n readonly remoteStream: MediaStream | null;\r\n readonly answeredAt: Date | null;\r\n}\r\n\r\nconst CALL_EVENTS: readonly CallEvent[] = [\r\n CallEvent.StateChanged,\r\n CallEvent.Muted,\r\n CallEvent.Held,\r\n CallEvent.Unheld,\r\n CallEvent.RemotePartyUpdated,\r\n CallEvent.RemoteStream,\r\n CallEvent.Ended,\r\n];\r\n\r\n/**\r\n * Reactive view of a specific call. Re-renders only when one of the call's own events fires — no\r\n * polling, no re-render storms.\r\n *\r\n * The snapshot is CACHED and invalidated only on an event, so `useSyncExternalStore` gets a stable\r\n * reference between changes (returning a fresh object every render would infinite-loop).\r\n */\r\nexport function useCall(call: Call | null): CallView | null {\r\n const cache = useRef<{ call: Call; view: CallView } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) => {\r\n if (!call) return () => {};\r\n const unsubs = CALL_EVENTS.map((event) =>\r\n call.on(event, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n );\r\n return () => {\r\n for (const off of unsubs) off();\r\n };\r\n },\r\n [call],\r\n );\r\n\r\n const getSnapshot = useCallback((): CallView | null => {\r\n if (!call) return null;\r\n if (cache.current?.call === call) return cache.current.view;\r\n const view: CallView = {\r\n state: call.state,\r\n isMuted: call.isMuted,\r\n isOnHold: call.isOnHold,\r\n remote: call.remote,\r\n remoteStream: call.remoteStream,\r\n answeredAt: call.answeredAt,\r\n };\r\n cache.current = { call, view };\r\n return view;\r\n }, [call]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n\r\n/**\r\n * The audio device surface (`phone.audio`) — reactive. Re-renders on `SoftphoneEvent.DeviceChange`: a\r\n * device plugged/unplugged, or a selection changed (SPEC §7 Phase 2).\r\n *\r\n * The snapshot is CACHED and invalidated only on that event, same reasoning as `useCall`'s own\r\n * `CallView`: `phone.audio` is ONE stable object whose fields mutate in place, so returning it\r\n * directly would never change by reference and `useSyncExternalStore` would never re-render. A fresh\r\n * object, rebuilt only when something actually changed, is what makes this reactive without polling.\r\n */\r\nexport function useDevices(): AudioDevices {\r\n const phone = useSoftphone();\r\n const cache = useRef<{ audio: AudioDevices; view: AudioDevices } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) =>\r\n phone.on(SoftphoneEvent.DeviceChange, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n [phone],\r\n );\r\n\r\n const getSnapshot = useCallback((): AudioDevices => {\r\n const audio = phone.audio;\r\n if (cache.current?.audio === audio) return cache.current.view;\r\n const view: AudioDevices = {\r\n inputs: audio.inputs,\r\n outputs: audio.outputs,\r\n currentInputId: audio.currentInputId,\r\n currentOutputId: audio.currentOutputId,\r\n currentRingtoneOutputId: audio.currentRingtoneOutputId,\r\n canSelectOutput: audio.canSelectOutput,\r\n canSelectRingtoneOutput: audio.canSelectRingtoneOutput,\r\n setInput: (deviceId) => audio.setInput(deviceId),\r\n setOutput: (deviceId) => audio.setOutput(deviceId),\r\n setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),\r\n refresh: () => audio.refresh(),\r\n };\r\n cache.current = { audio, view };\r\n return view;\r\n }, [phone]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n"]}
1
+ {"version":3,"sources":["../../src/react/context.tsx","../../src/react/hooks.ts"],"names":["createContext","useContext","useSyncExternalStore","useCallback","useRef"],"mappings":";;;;;;AAYA,IAAM,gBAAA,GAAmBA,oBAAgC,IAAI,CAAA;AAOtD,SAAS,iBAAA,CAAkB,EAAE,SAAA,EAAW,QAAA,EAAS,EAA2B;AACjF,EAAA,sCAAQ,gBAAA,CAAiB,QAAA,EAAjB,EAA0B,KAAA,EAAO,WAAY,QAAA,EAAS,CAAA;AAChE;AAGO,SAAS,YAAA,GAA0B;AACxC,EAAA,MAAM,SAAA,GAAYC,iBAAW,gBAAgB,CAAA;AAC7C,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAM,IAAI,MAAM,2DAA2D,CAAA;AAAA,EAC7E;AACA,EAAA,OAAO,SAAA;AACT;ACTO,SAAS,YAAA,GAA2C;AACzD,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOC,0BAAA;AAAA,IACLC,iBAAA;AAAA,MACE,CAAC,EAAA,KAAmB;AAClB,QAAA,MAAM,QAAA,GAAW,KAAA,CAAM,EAAA,CAAA,OAAA,cAAyB,MAAM,IAAI,CAAA;AAC1D,QAAA,MAAM,QAAA,GAAW,KAAA,CAAM,EAAA,CAAA,cAAA,qBAAgC,MAAM,IAAI,CAAA;AACjE,QAAA,OAAO,MAAM;AACX,UAAA,QAAA,EAAS;AACT,UAAA,QAAA,EAAS;AAAA,QACX,CAAA;AAAA,MACF,CAAA;AAAA,MACA,CAAC,KAAK;AAAA,KACR;AAAA,IACAA,kBAAY,MAAM,KAAA,CAAM,SAAA,EAAW,CAAC,KAAK,CAAC;AAAA,GAC5C;AACF;AAEO,SAAS,iBAAA,GAAoC;AAClD,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOD,0BAAA;AAAA,IACLC,iBAAA,CAAY,CAAC,EAAA,KAAmB,KAAA,CAAM,EAAA,CAAA,cAAA,qBAAgC,MAAM,EAAA,EAAI,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAAA,IAC1FA,kBAAY,MAAM,KAAA,CAAM,KAAA,EAAO,CAAC,KAAK,CAAC;AAAA,GACxC;AACF;AAOO,SAAS,WAAA,GAAuB;AACrC,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOD,0BAAA;AAAA,IACLC,iBAAA,CAAY,CAAC,EAAA,KAAmB,KAAA,CAAM,EAAA,CAAA,eAAA,sBAAiC,MAAM,EAAA,EAAI,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAAA,IAC3FA,kBAAY,MAAM,KAAA,CAAM,QAAA,EAAU,CAAC,KAAK,CAAC;AAAA,GAC3C;AACF;AAqBO,SAAS,OAAA,GAAoB;AAClC,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,MAAM,KAAA,GAAQC,aAAwB,IAAI,CAAA;AAC1C,EAAA,OAAOF,0BAAA;AAAA,IACLC,iBAAA;AAAA,MACE,CAAC,EAAA,KAAmB;AAClB,QAAA,MAAM,OAAA,GAAU,KAAA,CAAM,EAAA,CAAA,aAAA,oBAA+B,MAAM,IAAI,CAAA;AAC/D,QAAA,MAAM,QAAA,GAAW,KAAA,CAAM,EAAA,CAAA,kBAAA,yBAAoC,MAAM,IAAI,CAAA;AACrE,QAAA,OAAO,MAAM;AACX,UAAA,OAAA,EAAQ;AACR,UAAA,QAAA,EAAS;AAAA,QACX,CAAA;AAAA,MACF,CAAA;AAAA,MACA,CAAC,KAAK;AAAA,KACR;AAAA,IACAA,kBAAY,MAAgB;AAC1B,MAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,KAAA,EAAO,SAAA,EAAW,OAAM,GAAI,KAAA;AACrD,MAAA,MAAM,SAAS,KAAA,CAAM,OAAA;AACrB,MAAA,IAAI,MAAA,EAAQ,SAAS,IAAA,IAAQ,MAAA,CAAO,UAAU,KAAA,IAAS,MAAA,CAAO,KAAA,KAAU,KAAA,EAAO,OAAO,MAAA;AACtF,MAAA,MAAM,IAAA,GAAiB,EAAE,IAAA,EAAM,KAAA,EAAO,KAAA,EAAM;AAC5C,MAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,MAAA,OAAO,IAAA;AAAA,IACT,CAAA,EAAG,CAAC,KAAK,CAAC;AAAA,GACZ;AACF;AAOO,SAAS,aAAA,GAA6B;AAC3C,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOD,0BAAA;AAAA,IACLC,iBAAA,CAAY,CAAC,EAAA,KAAmB,KAAA,CAAM,EAAA,CAAA,mBAAA,0BAAqC,MAAM,EAAA,EAAI,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAAA,IAC/FA,kBAAY,MAAM,KAAA,CAAM,UAAA,EAAY,CAAC,KAAK,CAAC;AAAA,GAC7C;AACF;AAYO,SAAS,YAAY,MAAA,EAA2C;AACrE,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,OAAOD,0BAAA;AAAA,IACLC,iBAAA;AAAA,MACE,CAAC,EAAA,KAAmB;AAClB,QAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,MAAM;AAAA,QAAC,CAAA;AAC3B,QAAA,OAAO,KAAA,CAAM,EAAA,CAAA,iBAAA,wBAAmC,MAAM,EAAA,EAAI,CAAA;AAAA,MAC5D,CAAA;AAAA,MACA,CAAC,OAAO,MAAM;AAAA,KAChB;AAAA,IACAA,kBAAY,MAAM,KAAA,CAAM,QAAA,EAAU,CAAC,KAAK,CAAC;AAAA,GAC3C;AACF;AAYA,IAAM,WAAA,GAAoC;AAAA,EAAA,cAAA;AAAA,EAAA,OAAA;AAAA,EAAA,MAAA;AAAA,EAAA,QAAA;AAAA,EAAA,oBAAA;AAAA,EAAA,cAAA;AAAA,EAAA,OAAA;AAQ1C,CAAA;AASO,SAAS,QAAQ,IAAA,EAAoC;AAC1D,EAAA,MAAM,KAAA,GAAQC,aAA8C,IAAI,CAAA;AAEhE,EAAA,MAAM,SAAA,GAAYD,iBAAA;AAAA,IAChB,CAAC,EAAA,KAAmB;AAClB,MAAA,IAAI,CAAC,IAAA,EAAM,OAAO,MAAM;AAAA,MAAC,CAAA;AACzB,MAAA,MAAM,SAAS,WAAA,CAAY,GAAA;AAAA,QAAI,CAAC,KAAA,KAC9B,IAAA,CAAK,EAAA,CAAG,OAAO,MAAM;AACnB,UAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,UAAA,EAAA,EAAG;AAAA,QACL,CAAC;AAAA,OACH;AACA,MAAA,OAAO,MAAM;AACX,QAAA,KAAA,MAAW,GAAA,IAAO,QAAQ,GAAA,EAAI;AAAA,MAChC,CAAA;AAAA,IACF,CAAA;AAAA,IACA,CAAC,IAAI;AAAA,GACP;AAEA,EAAA,MAAM,WAAA,GAAcA,kBAAY,MAAuB;AACrD,IAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,IAAA,IAAI,MAAM,OAAA,EAAS,IAAA,KAAS,IAAA,EAAM,OAAO,MAAM,OAAA,CAAQ,IAAA;AACvD,IAAA,MAAM,IAAA,GAAiB;AAAA,MACrB,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,cAAc,IAAA,CAAK,YAAA;AAAA,MACnB,YAAY,IAAA,CAAK;AAAA,KACnB;AACA,IAAA,KAAA,CAAM,OAAA,GAAU,EAAE,IAAA,EAAM,IAAA,EAAK;AAC7B,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAET,EAAA,OAAOD,0BAAA,CAAqB,WAAW,WAAW,CAAA;AACpD;AAWO,SAAS,UAAA,GAA2B;AACzC,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,MAAM,KAAA,GAAQE,aAA2D,IAAI,CAAA;AAE7E,EAAA,MAAM,SAAA,GAAYD,iBAAA;AAAA,IAChB,CAAC,EAAA,KACC,KAAA,CAAM,EAAA,CAAA,cAAA,qBAAgC,MAAM;AAC1C,MAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,MAAA,EAAA,EAAG;AAAA,IACL,CAAC,CAAA;AAAA,IACH,CAAC,KAAK;AAAA,GACR;AAEA,EAAA,MAAM,WAAA,GAAcA,kBAAY,MAAoB;AAClD,IAAA,MAAM,QAAQ,KAAA,CAAM,KAAA;AACpB,IAAA,IAAI,MAAM,OAAA,EAAS,KAAA,KAAU,KAAA,EAAO,OAAO,MAAM,OAAA,CAAQ,IAAA;AACzD,IAAA,MAAM,IAAA,GAAqB;AAAA,MACzB,QAAQ,KAAA,CAAM,MAAA;AAAA,MACd,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,gBAAgB,KAAA,CAAM,cAAA;AAAA,MACtB,iBAAiB,KAAA,CAAM,eAAA;AAAA,MACvB,yBAAyB,KAAA,CAAM,uBAAA;AAAA,MAC/B,iBAAiB,KAAA,CAAM,eAAA;AAAA,MACvB,yBAAyB,KAAA,CAAM,uBAAA;AAAA,MAC/B,QAAA,EAAU,CAAC,QAAA,KAAa,KAAA,CAAM,SAAS,QAAQ,CAAA;AAAA,MAC/C,SAAA,EAAW,CAAC,QAAA,KAAa,KAAA,CAAM,UAAU,QAAQ,CAAA;AAAA,MACjD,iBAAA,EAAmB,CAAC,QAAA,KAAa,KAAA,CAAM,kBAAkB,QAAQ,CAAA;AAAA,MACjE,OAAA,EAAS,MAAM,KAAA,CAAM,OAAA;AAAQ,KAC/B;AACA,IAAA,KAAA,CAAM,OAAA,GAAU,EAAE,KAAA,EAAO,IAAA,EAAK;AAC9B,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,OAAOD,0BAAA,CAAqB,WAAW,WAAW,CAAA;AACpD","file":"index.cjs","sourcesContent":["import { createContext, useContext, type ReactNode } from 'react';\r\nimport type { Softphone } from '../core/softphone.js';\r\n\r\n/**\r\n * The provider takes an INSTANCE the host constructed — not config props.\r\n *\r\n * This is LiveKit's hard-won lesson (SPEC §5.1): they are deprecating their config-props\r\n * `<LiveKitRoom>` in favour of a provider that accepts an instance. Holding the `Softphone` above the\r\n * React tree lets the app own the connection, survive route changes and unmounts without dropping a\r\n * call, and unit-test the engine with no React at all. Config-props would rebuild the engine on\r\n * every prop change — catastrophic for something holding a live `RTCPeerConnection`.\r\n */\r\nconst SoftphoneContext = createContext<Softphone | null>(null);\r\n\r\nexport interface SoftphoneProviderProps {\r\n readonly softphone: Softphone;\r\n readonly children: ReactNode;\r\n}\r\n\r\nexport function SoftphoneProvider({ softphone, children }: SoftphoneProviderProps) {\r\n return <SoftphoneContext.Provider value={softphone}>{children}</SoftphoneContext.Provider>;\r\n}\r\n\r\n/** The `Softphone` instance from context. Stable — does not re-render on state changes. */\r\nexport function useSoftphone(): Softphone {\r\n const softphone = useContext(SoftphoneContext);\r\n if (!softphone) {\r\n throw new Error('useSoftphone() must be used within a <SoftphoneProvider>.');\r\n }\r\n return softphone;\r\n}\r\n","// Built-in, no shim: the peer floor is React 18 (SPEC §2.7), which ships useSyncExternalStore.\r\nimport { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useSoftphone } from './context.js';\r\nimport { SoftphoneEvent, CallEvent } from '../core/events.js';\r\nimport type { Call, CallState } from '../core/call.js';\r\nimport type { LineClaim, LineLocation, LineState, PresenceEntry, RemoteParty, SoftphoneState } from '../core/types.js';\r\nimport type { AudioDevices } from '../core/softphone.js';\r\nimport type { FatalSoftphoneError } from '../core/errors.js';\r\n\r\n/** Reactive softphone lifecycle state (`stopped` → `online` → …). */\r\n/**\r\n * The reason the phone cannot currently be used, or `null`.\r\n *\r\n * Reads `softphone.lastError`, which is STATE rather than only an event — the widget is usually\r\n * opened *after* something broke, and a listener mounted at that point would have missed the event\r\n * entirely and rendered a dialpad with no explanation.\r\n *\r\n * Re-reads on both channels because both can change it: `Error` sets it, and `StateChanged` clears it\r\n * on reaching `online`. Subscribing to only the first would leave a stale gate on screen after the\r\n * phone recovered.\r\n */\r\nexport function useLastError(): FatalSoftphoneError | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offError = phone.on(SoftphoneEvent.Error, () => cb());\r\n const offState = phone.on(SoftphoneEvent.StateChanged, () => cb());\r\n return () => {\r\n offError();\r\n offState();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback(() => phone.lastError, [phone]),\r\n );\r\n}\r\n\r\nexport function useSoftphoneState(): SoftphoneState {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.StateChanged, () => cb()), [phone]),\r\n useCallback(() => phone.state, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * Whether THIS tab is the one allowed to be reachable (`multi-tab-leader-election.md`). `false` by\r\n * default and while waiting for another open tab to release it — show your own \"active in another\r\n * tab\" state for that duration; the `/ui` widget already does.\r\n */\r\nexport function useIsLeader(): boolean {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.LeaderChanged, () => cb()), [phone]),\r\n useCallback(() => phone.isLeader, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * Where this extension's calls ring, who has them when it is not here, and how the last attempt to bring them\r\n * here went — see `useLine`.\r\n */\r\nexport interface LineView {\r\n readonly line: LineState;\r\n readonly where: LineLocation | null;\r\n readonly claim: LineClaim;\r\n}\r\n\r\n/**\r\n * Where this extension's incoming calls ring (`phone.line` / `phone.lineWhere` / `phone.lineClaim`,\r\n * `docs/specs/line-owner.md` §5) — reactive on `SoftphoneEvent.LineChanged` and `LineClaimChanged`.\r\n * `elsewhere` is what puts the softphone in `standby`; show your own \"your phone is active elsewhere\" state\r\n * with `takeLine()` behind it, or let the `/ui` widget's do it.\r\n *\r\n * The snapshot is the SAME object until a field changes — compared by value, not rebuilt per read, since\r\n * `useSyncExternalStore` re-renders forever on a fresh object every time.\r\n */\r\nexport function useLine(): LineView {\r\n const phone = useSoftphone();\r\n const cache = useRef<LineView | null>(null);\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offLine = phone.on(SoftphoneEvent.LineChanged, () => cb());\r\n const offClaim = phone.on(SoftphoneEvent.LineClaimChanged, () => cb());\r\n return () => {\r\n offLine();\r\n offClaim();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback((): LineView => {\r\n const { line, lineWhere: where, lineClaim: claim } = phone;\r\n const cached = cache.current;\r\n if (cached?.line === line && cached.where === where && cached.claim === claim) return cached;\r\n const view: LineView = { line, where, claim };\r\n cache.current = view;\r\n return view;\r\n }, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.\r\n *\r\n * `phone.activeCall` returns a stable reference for a given call, so it is a safe snapshot.\r\n */\r\nexport function useActiveCall(): Call | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.ActiveCallChanged, () => cb()), [phone]),\r\n useCallback(() => phone.activeCall, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.\r\n * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket\r\n * itself stays governed entirely by the softphone — open while it is started and this tab leads (spec's\r\n * own always-connected decision) — so passing `false` here does not close it — it only stops this\r\n * particular component from re-rendering on every update while it has no reason to care, e.g. the\r\n * transfer step being closed (spec §4.5: \"subscribe when the step opens... unsubscribe when it\r\n * closes\"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`\r\n * never shows a stale snapshot.\r\n */\r\nexport function usePresence(active: boolean): readonly PresenceEntry[] {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n if (!active) return () => {};\r\n return phone.on(SoftphoneEvent.PresenceChanged, () => cb());\r\n },\r\n [phone, active],\r\n ),\r\n useCallback(() => phone.presence, [phone]),\r\n );\r\n}\r\n\r\n/** An immutable snapshot of a call's reactive fields. */\r\nexport interface CallView {\r\n readonly state: CallState;\r\n readonly isMuted: boolean;\r\n readonly isOnHold: boolean;\r\n readonly remote: RemoteParty;\r\n readonly remoteStream: MediaStream | null;\r\n readonly answeredAt: Date | null;\r\n}\r\n\r\nconst CALL_EVENTS: readonly CallEvent[] = [\r\n CallEvent.StateChanged,\r\n CallEvent.Muted,\r\n CallEvent.Held,\r\n CallEvent.Unheld,\r\n CallEvent.RemotePartyUpdated,\r\n CallEvent.RemoteStream,\r\n CallEvent.Ended,\r\n];\r\n\r\n/**\r\n * Reactive view of a specific call. Re-renders only when one of the call's own events fires — no\r\n * polling, no re-render storms.\r\n *\r\n * The snapshot is CACHED and invalidated only on an event, so `useSyncExternalStore` gets a stable\r\n * reference between changes (returning a fresh object every render would infinite-loop).\r\n */\r\nexport function useCall(call: Call | null): CallView | null {\r\n const cache = useRef<{ call: Call; view: CallView } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) => {\r\n if (!call) return () => {};\r\n const unsubs = CALL_EVENTS.map((event) =>\r\n call.on(event, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n );\r\n return () => {\r\n for (const off of unsubs) off();\r\n };\r\n },\r\n [call],\r\n );\r\n\r\n const getSnapshot = useCallback((): CallView | null => {\r\n if (!call) return null;\r\n if (cache.current?.call === call) return cache.current.view;\r\n const view: CallView = {\r\n state: call.state,\r\n isMuted: call.isMuted,\r\n isOnHold: call.isOnHold,\r\n remote: call.remote,\r\n remoteStream: call.remoteStream,\r\n answeredAt: call.answeredAt,\r\n };\r\n cache.current = { call, view };\r\n return view;\r\n }, [call]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n\r\n/**\r\n * The audio device surface (`phone.audio`) — reactive. Re-renders on `SoftphoneEvent.DeviceChange`: a\r\n * device plugged/unplugged, or a selection changed (SPEC §7 Phase 2).\r\n *\r\n * The snapshot is CACHED and invalidated only on that event, same reasoning as `useCall`'s own\r\n * `CallView`: `phone.audio` is ONE stable object whose fields mutate in place, so returning it\r\n * directly would never change by reference and `useSyncExternalStore` would never re-render. A fresh\r\n * object, rebuilt only when something actually changed, is what makes this reactive without polling.\r\n */\r\nexport function useDevices(): AudioDevices {\r\n const phone = useSoftphone();\r\n const cache = useRef<{ audio: AudioDevices; view: AudioDevices } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) =>\r\n phone.on(SoftphoneEvent.DeviceChange, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n [phone],\r\n );\r\n\r\n const getSnapshot = useCallback((): AudioDevices => {\r\n const audio = phone.audio;\r\n if (cache.current?.audio === audio) return cache.current.view;\r\n const view: AudioDevices = {\r\n inputs: audio.inputs,\r\n outputs: audio.outputs,\r\n currentInputId: audio.currentInputId,\r\n currentOutputId: audio.currentOutputId,\r\n currentRingtoneOutputId: audio.currentRingtoneOutputId,\r\n canSelectOutput: audio.canSelectOutput,\r\n canSelectRingtoneOutput: audio.canSelectRingtoneOutput,\r\n setInput: (deviceId) => audio.setInput(deviceId),\r\n setOutput: (deviceId) => audio.setOutput(deviceId),\r\n setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),\r\n refresh: () => audio.refresh(),\r\n };\r\n cache.current = { audio, view };\r\n return view;\r\n }, [phone]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n"]}
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { S as Softphone, A as AudioDevices } from '../softphone-dZMkW8V1.cjs';
4
- import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, P as PresenceEntry, r as SoftphoneState } from '../call-CaPDWVk3.cjs';
3
+ import { S as Softphone, A as AudioDevices } from '../softphone-JZvJdzU3.cjs';
4
+ import { f as CallState, R as RemoteParty, k as LineState, j as LineLocation, L as LineClaim, a as Call, h as FatalSoftphoneError, P as PresenceEntry, z as SoftphoneState } from '../call-FqcvDg1t.cjs';
5
5
 
6
6
  interface SoftphoneProviderProps {
7
7
  readonly softphone: Softphone;
@@ -31,6 +31,25 @@ declare function useSoftphoneState(): SoftphoneState;
31
31
  * tab" state for that duration; the `/ui` widget already does.
32
32
  */
33
33
  declare function useIsLeader(): boolean;
34
+ /**
35
+ * Where this extension's calls ring, who has them when it is not here, and how the last attempt to bring them
36
+ * here went — see `useLine`.
37
+ */
38
+ interface LineView {
39
+ readonly line: LineState;
40
+ readonly where: LineLocation | null;
41
+ readonly claim: LineClaim;
42
+ }
43
+ /**
44
+ * Where this extension's incoming calls ring (`phone.line` / `phone.lineWhere` / `phone.lineClaim`,
45
+ * `docs/specs/line-owner.md` §5) — reactive on `SoftphoneEvent.LineChanged` and `LineClaimChanged`.
46
+ * `elsewhere` is what puts the softphone in `standby`; show your own "your phone is active elsewhere" state
47
+ * with `takeLine()` behind it, or let the `/ui` widget's do it.
48
+ *
49
+ * The snapshot is the SAME object until a field changes — compared by value, not rebuilt per read, since
50
+ * `useSyncExternalStore` re-renders forever on a fresh object every time.
51
+ */
52
+ declare function useLine(): LineView;
34
53
  /**
35
54
  * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.
36
55
  *
@@ -40,8 +59,8 @@ declare function useActiveCall(): Call | null;
40
59
  /**
41
60
  * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.
42
61
  * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket
43
- * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own
44
- * always-connected decision), so passing `false` here does not close it — it only stops this
62
+ * itself stays governed entirely by the softphone — open while it is started and this tab leads (spec's
63
+ * own always-connected decision) — so passing `false` here does not close it — it only stops this
45
64
  * particular component from re-rendering on every update while it has no reason to care, e.g. the
46
65
  * transfer step being closed (spec §4.5: "subscribe when the step opens... unsubscribe when it
47
66
  * closes"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`
@@ -76,4 +95,4 @@ declare function useCall(call: Call | null): CallView | null;
76
95
  */
77
96
  declare function useDevices(): AudioDevices;
78
97
 
79
- export { type CallView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState };
98
+ export { type CallView, type LineView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useLine, usePresence, useSoftphone, useSoftphoneState };
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { S as Softphone, A as AudioDevices } from '../softphone-DEt0TtQw.js';
4
- import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, P as PresenceEntry, r as SoftphoneState } from '../call-CaPDWVk3.js';
3
+ import { S as Softphone, A as AudioDevices } from '../softphone-By9sizlV.js';
4
+ import { f as CallState, R as RemoteParty, k as LineState, j as LineLocation, L as LineClaim, a as Call, h as FatalSoftphoneError, P as PresenceEntry, z as SoftphoneState } from '../call-FqcvDg1t.js';
5
5
 
6
6
  interface SoftphoneProviderProps {
7
7
  readonly softphone: Softphone;
@@ -31,6 +31,25 @@ declare function useSoftphoneState(): SoftphoneState;
31
31
  * tab" state for that duration; the `/ui` widget already does.
32
32
  */
33
33
  declare function useIsLeader(): boolean;
34
+ /**
35
+ * Where this extension's calls ring, who has them when it is not here, and how the last attempt to bring them
36
+ * here went — see `useLine`.
37
+ */
38
+ interface LineView {
39
+ readonly line: LineState;
40
+ readonly where: LineLocation | null;
41
+ readonly claim: LineClaim;
42
+ }
43
+ /**
44
+ * Where this extension's incoming calls ring (`phone.line` / `phone.lineWhere` / `phone.lineClaim`,
45
+ * `docs/specs/line-owner.md` §5) — reactive on `SoftphoneEvent.LineChanged` and `LineClaimChanged`.
46
+ * `elsewhere` is what puts the softphone in `standby`; show your own "your phone is active elsewhere" state
47
+ * with `takeLine()` behind it, or let the `/ui` widget's do it.
48
+ *
49
+ * The snapshot is the SAME object until a field changes — compared by value, not rebuilt per read, since
50
+ * `useSyncExternalStore` re-renders forever on a fresh object every time.
51
+ */
52
+ declare function useLine(): LineView;
34
53
  /**
35
54
  * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.
36
55
  *
@@ -40,8 +59,8 @@ declare function useActiveCall(): Call | null;
40
59
  /**
41
60
  * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.
42
61
  * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket
43
- * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own
44
- * always-connected decision), so passing `false` here does not close it — it only stops this
62
+ * itself stays governed entirely by the softphone — open while it is started and this tab leads (spec's
63
+ * own always-connected decision) — so passing `false` here does not close it — it only stops this
45
64
  * particular component from re-rendering on every update while it has no reason to care, e.g. the
46
65
  * transfer step being closed (spec §4.5: "subscribe when the step opens... unsubscribe when it
47
66
  * closes"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`
@@ -76,4 +95,4 @@ declare function useCall(call: Call | null): CallView | null;
76
95
  */
77
96
  declare function useDevices(): AudioDevices;
78
97
 
79
- export { type CallView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState };
98
+ export { type CallView, type LineView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useLine, usePresence, useSoftphone, useSoftphoneState };
@@ -1,4 +1,4 @@
1
- export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState } from '../chunk-UHNLEBBO.js';
2
- import '../chunk-7R6LOOAI.js';
1
+ export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useLine, usePresence, useSoftphone, useSoftphoneState } from '../chunk-I3RSC52R.js';
2
+ import '../chunk-N4PLEYFJ.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import { x as ContactsSource, y as HistorySource, g as Contact, A as AgentsDirectory, z as TransferOutcomesPage, B as PresenceSource, m as RingtoneSource, t as TypedEmitter, q as SoftphoneEventMap, r as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call, P as PresenceEntry } from './call-CaPDWVk3.js';
1
+ import { K as ContactsSource, M as HistorySource, g as Contact, Q as AgentsDirectory, U as TransferOutcomesPage, W as PresenceSource, u as RingtoneSource, r as OutboundNumbersSource, B as TypedEmitter, y as SoftphoneEventMap, z as SoftphoneState, h as FatalSoftphoneError, k as LineState, j as LineLocation, L as LineClaim, e as CallOptions, a as Call, P as PresenceEntry } from './call-FqcvDg1t.js';
2
2
 
3
3
  /** Contacts, history, caller ID, the agents directory, transfer outcomes, presence and ringtone
4
4
  * settings, all from one config. */
@@ -16,6 +16,8 @@ interface BackendSources {
16
16
  readonly presence: PresenceSource;
17
17
  /** The caller's ringtone settings (custom-ringtone.md §4.4). Always Coventia's — no override. */
18
18
  readonly ringtones: RingtoneSource;
19
+ /** The caller's outbound number (outbound-number-per-agent.md §5.5). Always Coventia's — no override. */
20
+ readonly outboundNumbers: OutboundNumbersSource;
19
21
  }
20
22
 
21
23
  /**
@@ -114,6 +116,39 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
114
116
  * "active in another tab" state for that duration (the `/ui` widget already does).
115
117
  */
116
118
  readonly isLeader: boolean;
119
+ /**
120
+ * Where this extension's incoming calls ring (`docs/specs/line-owner.md` §5) — the axis ACROSS browsers,
121
+ * devices and the desktop app that `isLeader` cannot see. Only one softphone of an extension listens:
122
+ * the first one started keeps the line, and the others wait in `standby` until it is free or the user
123
+ * takes it (`takeLine()`).
124
+ *
125
+ * `unknown` until the backend has said, and whenever it cannot (no presence source, a backend that
126
+ * predates the line owner, a presence socket that is down): the softphone then listens, exactly as
127
+ * before this existed — it never leaves nobody listening. Changes fire `SoftphoneEvent.LineChanged`.
128
+ */
129
+ readonly line: LineState;
130
+ /**
131
+ * Who has the line while it is `elsewhere` — the desktop app, or another softphone (a browser tab) —
132
+ * or `null`: the line is here, unknown, or the backend did not say.
133
+ */
134
+ readonly lineWhere: LineLocation | null;
135
+ /**
136
+ * How the last attempt to bring the line here went, whoever made it (`takeLine()`, or a `call()` from
137
+ * `standby`) — what the waiting screen shows under its title, so a refused click-to-call explains itself
138
+ * just like a refused "Recibir aquí". Changes fire `SoftphoneEvent.LineClaimChanged`.
139
+ */
140
+ readonly lineClaim: LineClaim;
141
+ /**
142
+ * "Recibir aquí": takes the line from whichever softphone has it, so incoming calls ring here. Resolves
143
+ * once the line IS here (at once when it already is); the softphone then leaves `standby` and starts
144
+ * listening on its own.
145
+ *
146
+ * Rejects with a non-fatal `SoftphoneError`: `line/claim-refused` while the user is on a call on the
147
+ * softphone that has the line (it never moves during a call — retry once it ends), or `line/unavailable`
148
+ * when nothing could decide (no presence connection, a backend without the line owner, no answer in
149
+ * time). Neither is emitted on `SoftphoneEvent.Error` — this promise is the answer.
150
+ */
151
+ takeLine(): Promise<void>;
117
152
  /**
118
153
  * Start.
119
154
  *
@@ -126,6 +161,11 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
126
161
  stop(): Promise<void>;
127
162
  /** Idempotent. After this the instance is dead — construct a new one. */
128
163
  destroy(): Promise<void>;
164
+ /**
165
+ * Dial. From `standby` this takes the line first (`takeLine()`), then dials — a call the host starts is
166
+ * the user asking for the phone here — and rejects with that method's `line/*` error if the line cannot
167
+ * be taken.
168
+ */
129
169
  call(destination: string, options?: CallOptions): Promise<Call>;
130
170
  /** The live call, or null. The consult leg of an attended transfer is reachable from it. */
131
171
  readonly activeCall: Call | null;
@@ -140,8 +180,9 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
140
180
  /**
141
181
  * The live agent-presence list (Phase B, `docs/specs/transfer-presence-picker.md`) — every org
142
182
  * member with a phone extension, whatever their state. Empty when there is no `PresenceSource`
143
- * (no Coventia backend, or a host that omitted one), or while this tab is not `online && isLeader`
144
- * — never an error, just nothing to show (spec §4.4's own success criterion). Updates fire
183
+ * (no Coventia backend, or a host that omitted one), or while this tab is not a started leader —
184
+ * `online`, or waiting in `standby` for the line — or its connection is down: never an error, just
185
+ * nothing to show (spec §4.4's own success criterion). Updates fire
145
186
  * `SoftphoneEvent.PresenceChanged`; `usePresence()` is the reactive read.
146
187
  */
147
188
  readonly presence: readonly PresenceEntry[];
@@ -1,4 +1,4 @@
1
- import { x as ContactsSource, y as HistorySource, g as Contact, A as AgentsDirectory, z as TransferOutcomesPage, B as PresenceSource, m as RingtoneSource, t as TypedEmitter, q as SoftphoneEventMap, r as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call, P as PresenceEntry } from './call-CaPDWVk3.cjs';
1
+ import { K as ContactsSource, M as HistorySource, g as Contact, Q as AgentsDirectory, U as TransferOutcomesPage, W as PresenceSource, u as RingtoneSource, r as OutboundNumbersSource, B as TypedEmitter, y as SoftphoneEventMap, z as SoftphoneState, h as FatalSoftphoneError, k as LineState, j as LineLocation, L as LineClaim, e as CallOptions, a as Call, P as PresenceEntry } from './call-FqcvDg1t.cjs';
2
2
 
3
3
  /** Contacts, history, caller ID, the agents directory, transfer outcomes, presence and ringtone
4
4
  * settings, all from one config. */
@@ -16,6 +16,8 @@ interface BackendSources {
16
16
  readonly presence: PresenceSource;
17
17
  /** The caller's ringtone settings (custom-ringtone.md §4.4). Always Coventia's — no override. */
18
18
  readonly ringtones: RingtoneSource;
19
+ /** The caller's outbound number (outbound-number-per-agent.md §5.5). Always Coventia's — no override. */
20
+ readonly outboundNumbers: OutboundNumbersSource;
19
21
  }
20
22
 
21
23
  /**
@@ -114,6 +116,39 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
114
116
  * "active in another tab" state for that duration (the `/ui` widget already does).
115
117
  */
116
118
  readonly isLeader: boolean;
119
+ /**
120
+ * Where this extension's incoming calls ring (`docs/specs/line-owner.md` §5) — the axis ACROSS browsers,
121
+ * devices and the desktop app that `isLeader` cannot see. Only one softphone of an extension listens:
122
+ * the first one started keeps the line, and the others wait in `standby` until it is free or the user
123
+ * takes it (`takeLine()`).
124
+ *
125
+ * `unknown` until the backend has said, and whenever it cannot (no presence source, a backend that
126
+ * predates the line owner, a presence socket that is down): the softphone then listens, exactly as
127
+ * before this existed — it never leaves nobody listening. Changes fire `SoftphoneEvent.LineChanged`.
128
+ */
129
+ readonly line: LineState;
130
+ /**
131
+ * Who has the line while it is `elsewhere` — the desktop app, or another softphone (a browser tab) —
132
+ * or `null`: the line is here, unknown, or the backend did not say.
133
+ */
134
+ readonly lineWhere: LineLocation | null;
135
+ /**
136
+ * How the last attempt to bring the line here went, whoever made it (`takeLine()`, or a `call()` from
137
+ * `standby`) — what the waiting screen shows under its title, so a refused click-to-call explains itself
138
+ * just like a refused "Recibir aquí". Changes fire `SoftphoneEvent.LineClaimChanged`.
139
+ */
140
+ readonly lineClaim: LineClaim;
141
+ /**
142
+ * "Recibir aquí": takes the line from whichever softphone has it, so incoming calls ring here. Resolves
143
+ * once the line IS here (at once when it already is); the softphone then leaves `standby` and starts
144
+ * listening on its own.
145
+ *
146
+ * Rejects with a non-fatal `SoftphoneError`: `line/claim-refused` while the user is on a call on the
147
+ * softphone that has the line (it never moves during a call — retry once it ends), or `line/unavailable`
148
+ * when nothing could decide (no presence connection, a backend without the line owner, no answer in
149
+ * time). Neither is emitted on `SoftphoneEvent.Error` — this promise is the answer.
150
+ */
151
+ takeLine(): Promise<void>;
117
152
  /**
118
153
  * Start.
119
154
  *
@@ -126,6 +161,11 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
126
161
  stop(): Promise<void>;
127
162
  /** Idempotent. After this the instance is dead — construct a new one. */
128
163
  destroy(): Promise<void>;
164
+ /**
165
+ * Dial. From `standby` this takes the line first (`takeLine()`), then dials — a call the host starts is
166
+ * the user asking for the phone here — and rejects with that method's `line/*` error if the line cannot
167
+ * be taken.
168
+ */
129
169
  call(destination: string, options?: CallOptions): Promise<Call>;
130
170
  /** The live call, or null. The consult leg of an attended transfer is reachable from it. */
131
171
  readonly activeCall: Call | null;
@@ -140,8 +180,9 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
140
180
  /**
141
181
  * The live agent-presence list (Phase B, `docs/specs/transfer-presence-picker.md`) — every org
142
182
  * member with a phone extension, whatever their state. Empty when there is no `PresenceSource`
143
- * (no Coventia backend, or a host that omitted one), or while this tab is not `online && isLeader`
144
- * — never an error, just nothing to show (spec §4.4's own success criterion). Updates fire
183
+ * (no Coventia backend, or a host that omitted one), or while this tab is not a started leader —
184
+ * `online`, or waiting in `standby` for the line — or its connection is down: never an error, just
185
+ * nothing to show (spec §4.4's own success criterion). Updates fire
145
186
  * `SoftphoneEvent.PresenceChanged`; `usePresence()` is the reactive read.
146
187
  */
147
188
  readonly presence: readonly PresenceEntry[];