@coventia/softphone 0.1.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.
- package/dist/3x2-2HQTJDKZ.js +6583 -0
- package/dist/3x2-2HQTJDKZ.js.map +1 -0
- package/dist/3x2-INKWPNLA.js +6585 -0
- package/dist/3x2-INKWPNLA.js.map +1 -0
- package/dist/call-BTYlT2SU.d.cts +680 -0
- package/dist/call-BTYlT2SU.d.ts +680 -0
- package/dist/chunk-2ZSOGGPO.js +169 -0
- package/dist/chunk-2ZSOGGPO.js.map +1 -0
- package/dist/chunk-3V2L6UTK.js +129 -0
- package/dist/chunk-3V2L6UTK.js.map +1 -0
- package/dist/chunk-5BGEHMPP.js +945 -0
- package/dist/chunk-5BGEHMPP.js.map +1 -0
- package/dist/chunk-5WRI5ZAA.js +29 -0
- package/dist/chunk-5WRI5ZAA.js.map +1 -0
- package/dist/chunk-KAXEMJ7E.js +11 -0
- package/dist/chunk-KAXEMJ7E.js.map +1 -0
- package/dist/chunk-MHOJHG5E.js +39 -0
- package/dist/chunk-MHOJHG5E.js.map +1 -0
- package/dist/chunk-MP7CSA24.js +299 -0
- package/dist/chunk-MP7CSA24.js.map +1 -0
- package/dist/chunk-N2N2EQQJ.js +29 -0
- package/dist/chunk-N2N2EQQJ.js.map +1 -0
- package/dist/element/index.d.ts +853 -0
- package/dist/element/index.js +9635 -0
- package/dist/element/index.js.map +1 -0
- package/dist/engine-D9nbeY08.d.cts +115 -0
- package/dist/engine-Doa6Brkm.d.ts +115 -0
- package/dist/index.cjs +2133 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +630 -0
- package/dist/index.js.map +1 -0
- package/dist/min-64GIMWWL.js +5456 -0
- package/dist/min-64GIMWWL.js.map +1 -0
- package/dist/min-ATXYR23R.js +5458 -0
- package/dist/min-ATXYR23R.js.map +1 -0
- package/dist/react/index.cjs +138 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +68 -0
- package/dist/react/index.d.ts +68 -0
- package/dist/react/index.js +4 -0
- package/dist/react/index.js.map +1 -0
- package/dist/softphone-DHmhotbf.d.ts +128 -0
- package/dist/softphone-DhuhcKCy.d.cts +128 -0
- package/dist/softphone.css +1923 -0
- package/dist/sources-N4BOEDRL.js +226 -0
- package/dist/sources-N4BOEDRL.js.map +1 -0
- package/dist/testing/index.cjs +345 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +66 -0
- package/dist/testing/index.d.ts +66 -0
- package/dist/testing/index.js +186 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/ui/index.cjs +15428 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +188 -0
- package/dist/ui/index.d.ts +188 -0
- package/dist/ui/index.js +2256 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/zadarma/index.cjs +1144 -0
- package/dist/zadarma/index.cjs.map +1 -0
- package/dist/zadarma/index.d.cts +86 -0
- package/dist/zadarma/index.d.ts +86 -0
- package/dist/zadarma/index.js +5 -0
- package/dist/zadarma/index.js.map +1 -0
- package/dist/zadarma-engine-DN2SZSQ3.js +5 -0
- package/dist/zadarma-engine-DN2SZSQ3.js.map +1 -0
- package/package.json +123 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var ERROR_CATALOGUE = {
|
|
3
|
+
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
4
|
+
"engine/duplicate-instance": {
|
|
5
|
+
fatal: true,
|
|
6
|
+
message: "A Softphone instance is already running on this page.",
|
|
7
|
+
solution: "Construct one Softphone and share it. React StrictMode double-mounts in development \u2014 hold the instance above the tree, or in a ref, and call destroy() on unmount."
|
|
8
|
+
},
|
|
9
|
+
"engine/not-started": {
|
|
10
|
+
fatal: false,
|
|
11
|
+
message: "The softphone has not been started.",
|
|
12
|
+
solution: "Await softphone.start() before placing or answering a call."
|
|
13
|
+
},
|
|
14
|
+
"engine/destroyed": {
|
|
15
|
+
fatal: true,
|
|
16
|
+
message: "This Softphone has been destroyed and cannot be reused.",
|
|
17
|
+
solution: "Construct a new Softphone."
|
|
18
|
+
},
|
|
19
|
+
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
20
|
+
"auth/token-fetch-failed": {
|
|
21
|
+
fatal: true,
|
|
22
|
+
message: "Could not obtain a voice token from the token endpoint.",
|
|
23
|
+
solution: "Check that the token endpoint is reachable and that the headers callback returns a valid host-app session token."
|
|
24
|
+
},
|
|
25
|
+
"auth/token-rejected": {
|
|
26
|
+
fatal: true,
|
|
27
|
+
message: "The voice token was rejected.",
|
|
28
|
+
solution: "The token is expired, malformed, or scoped to a different organization. Mint a fresh one."
|
|
29
|
+
},
|
|
30
|
+
// ── Media ───────────────────────────────────────────────────────────────────
|
|
31
|
+
/*
|
|
32
|
+
NONE of the four codes below are ever actually raised via `createError` — nothing in the SDK
|
|
33
|
+
constructs a `SoftphoneError` with these codes today. Deleting them was the other option
|
|
34
|
+
(SPEC discussion); they stay, commented, because the taxonomy and Spanish copy (`error-text.ts`)
|
|
35
|
+
are worth keeping intact for whenever a real caller wants them, and a `SoftphoneErrorCode` union
|
|
36
|
+
is a cheap thing to keep honest with a comment, expensive to keep churning as a breaking change.
|
|
37
|
+
*/
|
|
38
|
+
"media/permission-denied": {
|
|
39
|
+
fatal: true,
|
|
40
|
+
message: "Microphone permission was denied.",
|
|
41
|
+
solution: "The permission prompt names the TOP-LEVEL origin. Ask for it from a user gesture, and re-request via the browser UI if previously blocked."
|
|
42
|
+
// NEVER RAISED. `MicPermissionGate` (ui/permission/) handles this condition today — it predates
|
|
43
|
+
// this catalogue and has BETTER, browser-specific unblock instructions than a generic gate could
|
|
44
|
+
// show. It does not import this catalogue at all; the two are unconnected on purpose.
|
|
45
|
+
},
|
|
46
|
+
"media/no-input-device": {
|
|
47
|
+
fatal: true,
|
|
48
|
+
message: "No microphone is available.",
|
|
49
|
+
solution: "Ask the user to connect a microphone, then retry."
|
|
50
|
+
// NEVER RAISED — same reason as `media/permission-denied` above: `MicPermissionGate` already
|
|
51
|
+
// owns this.
|
|
52
|
+
},
|
|
53
|
+
"media/webrtc-unsupported": {
|
|
54
|
+
fatal: true,
|
|
55
|
+
message: "This browser does not expose the WebRTC APIs.",
|
|
56
|
+
solution: "Usually a privacy extension (uBlock Origin Extra, WebRTC Leak Prevent) disabling RTCPeerConnection \u2014 Twilio documents the same. Ask the user to disable it for this origin."
|
|
57
|
+
// NEVER RAISED — same reason again: `MicPermissionGate` already owns this.
|
|
58
|
+
},
|
|
59
|
+
"media/playback-blocked": {
|
|
60
|
+
fatal: false,
|
|
61
|
+
message: "The browser blocked audio playback (autoplay policy).",
|
|
62
|
+
solution: "Call softphone.startAudio() from a click handler. Until then the user cannot hear the caller, and an INCOMING RINGTONE WILL BE SILENT \u2014 this is how calls get missed."
|
|
63
|
+
/*
|
|
64
|
+
NEVER RAISED — different reason from the other three. This is not superseded by an existing
|
|
65
|
+
UI; the feature it describes (autoplay-recovery) is simply not built yet. `Softphone.canPlayAudio`
|
|
66
|
+
/ `startAudio()` (core/softphone.ts) are Phase 2 and currently no-op stubs — `canPlayAudio`
|
|
67
|
+
always reports `true`. This is where that future work would report the block once it exists.
|
|
68
|
+
*/
|
|
69
|
+
},
|
|
70
|
+
// ── Network / signalling ────────────────────────────────────────────────────
|
|
71
|
+
"net/registration-failed": {
|
|
72
|
+
fatal: true,
|
|
73
|
+
message: "SIP registration failed.",
|
|
74
|
+
solution: "Usually a stale credential. The engine refreshes and retries; if it persists, the extension may not exist."
|
|
75
|
+
},
|
|
76
|
+
"net/transport-failed": {
|
|
77
|
+
fatal: true,
|
|
78
|
+
message: "The signalling transport could not be established.",
|
|
79
|
+
solution: "Check the CSP connect-src directive and any corporate proxy blocking WSS."
|
|
80
|
+
},
|
|
81
|
+
"net/ice-failed": {
|
|
82
|
+
fatal: false,
|
|
83
|
+
message: "Media connectivity was lost; attempting an ICE restart.",
|
|
84
|
+
solution: 'Usually a network handoff (wifi \u2192 cellular). The engine recovers on its own; surface a "reconnecting" banner rather than ending the call.'
|
|
85
|
+
/*
|
|
86
|
+
NEVER RAISED. The "reconnecting" banner this `solution` describes already exists — CallScreen
|
|
87
|
+
shows "Reconectando…" directly off the call's own `reconnecting` STATE (`STATUS_LABEL` in
|
|
88
|
+
views/CallScreen.tsx), which the SIP engine drives without going through the error channel at
|
|
89
|
+
all. Raising this too would say the same fact twice through two different mechanisms. It would
|
|
90
|
+
only earn its keep for host-side OBSERVABILITY — counting ICE restarts independently of the UI
|
|
91
|
+
— which nobody has asked for yet.
|
|
92
|
+
*/
|
|
93
|
+
},
|
|
94
|
+
// ── Call ────────────────────────────────────────────────────────────────────
|
|
95
|
+
"call/not-answerable": {
|
|
96
|
+
fatal: false,
|
|
97
|
+
message: "This call cannot be answered in its current state.",
|
|
98
|
+
solution: "Only a call in `ringing` may be answered. Check call.state first."
|
|
99
|
+
},
|
|
100
|
+
"call/busy": {
|
|
101
|
+
fatal: false,
|
|
102
|
+
message: "The remote party is busy.",
|
|
103
|
+
solution: 'Nothing to fix. Note SIP 486 is also what most endpoints send for a manual decline, so do not label this "busy" alone in your UI \u2014 it is equally often "did not answer".'
|
|
104
|
+
},
|
|
105
|
+
"call/no-answer": {
|
|
106
|
+
fatal: false,
|
|
107
|
+
message: "The remote party did not answer.",
|
|
108
|
+
solution: "Nothing to fix. Surface it to the user."
|
|
109
|
+
},
|
|
110
|
+
"call/rejected": {
|
|
111
|
+
fatal: false,
|
|
112
|
+
message: "The remote party rejected the call.",
|
|
113
|
+
solution: "Nothing to fix. Surface it to the user."
|
|
114
|
+
},
|
|
115
|
+
"call/missed": {
|
|
116
|
+
fatal: false,
|
|
117
|
+
message: "An incoming call was cancelled before it was answered.",
|
|
118
|
+
solution: "Nothing to fix \u2014 the caller gave up while it rang. Surface it, and log it as a missed call."
|
|
119
|
+
},
|
|
120
|
+
"call/failed": {
|
|
121
|
+
fatal: false,
|
|
122
|
+
message: "The call could not be completed.",
|
|
123
|
+
solution: "The catch-all for a call that never connected \u2014 a bad number, a rejected offer, a media failure. `cause` carries the underlying SIP cause. Nothing is fatal: the phone still works."
|
|
124
|
+
},
|
|
125
|
+
"call/transfer-failed": {
|
|
126
|
+
fatal: false,
|
|
127
|
+
message: "The transfer could not be completed.",
|
|
128
|
+
solution: "The original call is still up. Let the user retry or resume."
|
|
129
|
+
},
|
|
130
|
+
"call/unsupported": {
|
|
131
|
+
fatal: false,
|
|
132
|
+
message: "The provider does not support this operation.",
|
|
133
|
+
solution: "Guard the UI on the operation before offering it."
|
|
134
|
+
},
|
|
135
|
+
// ── Provider ────────────────────────────────────────────────────────────────
|
|
136
|
+
"provider/provision-failed": {
|
|
137
|
+
fatal: true,
|
|
138
|
+
message: "The provider could not issue SIP credentials.",
|
|
139
|
+
solution: "The backend reached the carrier but got no usable credential. Check the org has a SIP extension provisioned."
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
var CatalogueError = class extends Error {
|
|
143
|
+
fatal;
|
|
144
|
+
code;
|
|
145
|
+
solution;
|
|
146
|
+
constructor(code, cause) {
|
|
147
|
+
const spec = ERROR_CATALOGUE[code];
|
|
148
|
+
super(spec.message, cause === void 0 ? void 0 : { cause });
|
|
149
|
+
this.name = "SoftphoneError";
|
|
150
|
+
this.code = code;
|
|
151
|
+
this.fatal = spec.fatal;
|
|
152
|
+
this.solution = spec.solution;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
function isSoftphoneError(value) {
|
|
156
|
+
if (typeof value !== "object" || value === null) return false;
|
|
157
|
+
const candidate = value;
|
|
158
|
+
return typeof candidate.fatal === "boolean" && candidate.code in ERROR_CATALOGUE;
|
|
159
|
+
}
|
|
160
|
+
function isFatal(error) {
|
|
161
|
+
return error.fatal;
|
|
162
|
+
}
|
|
163
|
+
function createError(code, cause) {
|
|
164
|
+
return new CatalogueError(code, cause);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { ERROR_CATALOGUE, createError, isFatal, isSoftphoneError };
|
|
168
|
+
//# sourceMappingURL=chunk-2ZSOGGPO.js.map
|
|
169
|
+
//# sourceMappingURL=chunk-2ZSOGGPO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/errors.ts"],"names":[],"mappings":"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","file":"chunk-2ZSOGGPO.js","sourcesContent":["/**\r\n * The error taxonomy.\r\n *\r\n * Two things make this worth more than `onError(Error)`:\r\n *\r\n * 1. **Fatal vs non-fatal, as a discriminated union.** A fatal error means the call (or the engine)\r\n * is over and the host must react. A non-fatal one is a warning the host may surface or ignore.\r\n * Collapsing them into one channel — which is what almost every SDK does — forces every\r\n * integrator to re-derive the distinction from string matching. Daily's split is the model.\r\n *\r\n * 2. **A coded catalogue with a `solution` per code.** Twilio and Telnyx both ship this and\r\n * integrators love it, because the support conversation becomes \"send me the code\" instead of\r\n * \"send me your logs\".\r\n *\r\n * The catalogue below is the SINGLE source of truth: `fatal` is derived from it at the type level,\r\n * so a code cannot be fatal in the table and non-fatal in a type, and adding a code without deciding\r\n * its severity is a compile error.\r\n */\r\n\r\ninterface ErrorSpec {\r\n readonly fatal: boolean;\r\n /** What went wrong, for a developer. */\r\n readonly message: string;\r\n /** What to do about it. Shown in logs and in our docs. */\r\n readonly solution: string;\r\n}\r\n\r\nexport const ERROR_CATALOGUE = {\r\n // ── Engine lifecycle ────────────────────────────────────────────────────────\r\n 'engine/duplicate-instance': {\r\n fatal: true,\r\n message: 'A Softphone instance is already running on this page.',\r\n solution:\r\n 'Construct one Softphone and share it. React StrictMode double-mounts in development — hold the instance above the tree, or in a ref, and call destroy() on unmount.',\r\n },\r\n 'engine/not-started': {\r\n fatal: false,\r\n message: 'The softphone has not been started.',\r\n solution: 'Await softphone.start() before placing or answering a call.',\r\n },\r\n 'engine/destroyed': {\r\n fatal: true,\r\n message: 'This Softphone has been destroyed and cannot be reused.',\r\n solution: 'Construct a new Softphone.',\r\n },\r\n\r\n // ── Auth ────────────────────────────────────────────────────────────────────\r\n 'auth/token-fetch-failed': {\r\n fatal: true,\r\n message: 'Could not obtain a voice token from the token endpoint.',\r\n solution:\r\n 'Check that the token endpoint is reachable and that the headers callback returns a valid host-app session token.',\r\n },\r\n 'auth/token-rejected': {\r\n fatal: true,\r\n message: 'The voice token was rejected.',\r\n solution:\r\n 'The token is expired, malformed, or scoped to a different organization. Mint a fresh one.',\r\n },\r\n\r\n // ── Media ───────────────────────────────────────────────────────────────────\r\n /*\r\n NONE of the four codes below are ever actually raised via `createError` — nothing in the SDK\r\n constructs a `SoftphoneError` with these codes today. Deleting them was the other option\r\n (SPEC discussion); they stay, commented, because the taxonomy and Spanish copy (`error-text.ts`)\r\n are worth keeping intact for whenever a real caller wants them, and a `SoftphoneErrorCode` union\r\n is a cheap thing to keep honest with a comment, expensive to keep churning as a breaking change.\r\n */\r\n 'media/permission-denied': {\r\n fatal: true,\r\n message: 'Microphone permission was denied.',\r\n solution:\r\n 'The permission prompt names the TOP-LEVEL origin. Ask for it from a user gesture, and re-request via the browser UI if previously blocked.',\r\n // NEVER RAISED. `MicPermissionGate` (ui/permission/) handles this condition today — it predates\r\n // this catalogue and has BETTER, browser-specific unblock instructions than a generic gate could\r\n // show. It does not import this catalogue at all; the two are unconnected on purpose.\r\n },\r\n 'media/no-input-device': {\r\n fatal: true,\r\n message: 'No microphone is available.',\r\n solution: 'Ask the user to connect a microphone, then retry.',\r\n // NEVER RAISED — same reason as `media/permission-denied` above: `MicPermissionGate` already\r\n // owns this.\r\n },\r\n 'media/webrtc-unsupported': {\r\n fatal: true,\r\n message: 'This browser does not expose the WebRTC APIs.',\r\n solution:\r\n 'Usually a privacy extension (uBlock Origin Extra, WebRTC Leak Prevent) disabling RTCPeerConnection — Twilio documents the same. Ask the user to disable it for this origin.',\r\n // NEVER RAISED — same reason again: `MicPermissionGate` already owns this.\r\n },\r\n 'media/playback-blocked': {\r\n fatal: false,\r\n message: 'The browser blocked audio playback (autoplay policy).',\r\n solution:\r\n 'Call softphone.startAudio() from a click handler. Until then the user cannot hear the caller, and an INCOMING RINGTONE WILL BE SILENT — this is how calls get missed.',\r\n /*\r\n NEVER RAISED — different reason from the other three. This is not superseded by an existing\r\n UI; the feature it describes (autoplay-recovery) is simply not built yet. `Softphone.canPlayAudio`\r\n / `startAudio()` (core/softphone.ts) are Phase 2 and currently no-op stubs — `canPlayAudio`\r\n always reports `true`. This is where that future work would report the block once it exists.\r\n */\r\n },\r\n\r\n // ── Network / signalling ────────────────────────────────────────────────────\r\n 'net/registration-failed': {\r\n fatal: true,\r\n message: 'SIP registration failed.',\r\n solution: 'Usually a stale credential. The engine refreshes and retries; if it persists, the extension may not exist.',\r\n },\r\n 'net/transport-failed': {\r\n fatal: true,\r\n message: 'The signalling transport could not be established.',\r\n solution: 'Check the CSP connect-src directive and any corporate proxy blocking WSS.',\r\n },\r\n 'net/ice-failed': {\r\n fatal: false,\r\n message: 'Media connectivity was lost; attempting an ICE restart.',\r\n solution:\r\n 'Usually a network handoff (wifi → cellular). The engine recovers on its own; surface a \"reconnecting\" banner rather than ending the call.',\r\n /*\r\n NEVER RAISED. The \"reconnecting\" banner this `solution` describes already exists — CallScreen\r\n shows \"Reconectando…\" directly off the call's own `reconnecting` STATE (`STATUS_LABEL` in\r\n views/CallScreen.tsx), which the SIP engine drives without going through the error channel at\r\n all. Raising this too would say the same fact twice through two different mechanisms. It would\r\n only earn its keep for host-side OBSERVABILITY — counting ICE restarts independently of the UI\r\n — which nobody has asked for yet.\r\n */\r\n },\r\n\r\n // ── Call ────────────────────────────────────────────────────────────────────\r\n 'call/not-answerable': {\r\n fatal: false,\r\n message: 'This call cannot be answered in its current state.',\r\n solution: 'Only a call in `ringing` may be answered. Check call.state first.',\r\n },\r\n 'call/busy': {\r\n fatal: false,\r\n message: 'The remote party is busy.',\r\n solution:\r\n 'Nothing to fix. Note SIP 486 is also what most endpoints send for a manual decline, so do not ' +\r\n 'label this \"busy\" alone in your UI — it is equally often \"did not answer\".',\r\n },\r\n 'call/no-answer': {\r\n fatal: false,\r\n message: 'The remote party did not answer.',\r\n solution: 'Nothing to fix. Surface it to the user.',\r\n },\r\n 'call/rejected': {\r\n fatal: false,\r\n message: 'The remote party rejected the call.',\r\n solution: 'Nothing to fix. Surface it to the user.',\r\n },\r\n 'call/missed': {\r\n fatal: false,\r\n message: 'An incoming call was cancelled before it was answered.',\r\n solution:\r\n 'Nothing to fix — the caller gave up while it rang. Surface it, and log it as a missed call.',\r\n },\r\n 'call/failed': {\r\n fatal: false,\r\n message: 'The call could not be completed.',\r\n solution:\r\n 'The catch-all for a call that never connected — a bad number, a rejected offer, a media ' +\r\n 'failure. `cause` carries the underlying SIP cause. Nothing is fatal: the phone still works.',\r\n },\r\n 'call/transfer-failed': {\r\n fatal: false,\r\n message: 'The transfer could not be completed.',\r\n solution: 'The original call is still up. Let the user retry or resume.',\r\n },\r\n 'call/unsupported': {\r\n fatal: false,\r\n message: 'The provider does not support this operation.',\r\n solution: 'Guard the UI on the operation before offering it.',\r\n },\r\n\r\n // ── Provider ────────────────────────────────────────────────────────────────\r\n 'provider/provision-failed': {\r\n fatal: true,\r\n message: 'The provider could not issue SIP credentials.',\r\n solution:\r\n 'The backend reached the carrier but got no usable credential. Check the org has a SIP extension provisioned.',\r\n },\r\n} as const satisfies Record<string, ErrorSpec>;\r\n\r\nexport type SoftphoneErrorCode = keyof typeof ERROR_CATALOGUE;\r\n\r\n/** Codes whose spec says `fatal: true`. Derived — cannot drift from the catalogue. */\r\nexport type FatalErrorCode = {\r\n [K in SoftphoneErrorCode]: (typeof ERROR_CATALOGUE)[K]['fatal'] extends true ? K : never;\r\n}[SoftphoneErrorCode];\r\n\r\nexport type NonFatalErrorCode = Exclude<SoftphoneErrorCode, FatalErrorCode>;\r\n\r\n/**\r\n * A fatal error: the call, or the engine, is over.\r\n *\r\n * `if (err.fatal)` narrows `err.code` to the fatal codes — which is the entire point of the split,\r\n * and the thing JSDoc could not have expressed (SPEC §2.2).\r\n */\r\nexport interface FatalSoftphoneError extends Error {\r\n readonly fatal: true;\r\n readonly code: FatalErrorCode;\r\n readonly solution: string;\r\n}\r\n\r\n/** A non-fatal error: a warning. The call, if any, continues. */\r\nexport interface NonFatalSoftphoneError extends Error {\r\n readonly fatal: false;\r\n readonly code: NonFatalErrorCode;\r\n readonly solution: string;\r\n}\r\n\r\nexport type SoftphoneError = FatalSoftphoneError | NonFatalSoftphoneError;\r\n\r\n/**\r\n * The one concrete error class. Not exported: the public contract is the `SoftphoneError` union and\r\n * the `code` on it, so nothing outside should be reaching for `instanceof`. It is a real `Error`\r\n * subclass, so `instanceof Error`, `.stack` and console formatting all behave as a host expects.\r\n */\r\nclass CatalogueError extends Error {\r\n readonly fatal: boolean;\r\n readonly code: SoftphoneErrorCode;\r\n readonly solution: string;\r\n\r\n constructor(code: SoftphoneErrorCode, cause?: unknown) {\r\n const spec: ErrorSpec = ERROR_CATALOGUE[code];\r\n // `cause` is only passed when there IS one: `{ cause: undefined }` would still define the\r\n // property, and an error carrying an explicit `undefined` cause reads as \"we looked and found\r\n // nothing\" rather than \"nothing was passed\".\r\n super(spec.message, cause === undefined ? undefined : { cause });\r\n this.name = 'SoftphoneError';\r\n this.code = code;\r\n this.fatal = spec.fatal;\r\n this.solution = spec.solution;\r\n }\r\n}\r\n\r\n/**\r\n * Is this already one of ours?\r\n *\r\n * Used where a layer catches whatever a lower one threw: a failure that already carries a catalogue\r\n * code should be reported AS that code, not flattened into the generic one for the layer that caught\r\n * it. Without this, a rejected voice token surfaces as \"the provider could not issue credentials\",\r\n * which is true and useless.\r\n *\r\n * Structural, not `instanceof` — the concrete class is not exported, and a duck-typed object with a\r\n * real code is as good as one of ours.\r\n */\r\nexport function isSoftphoneError(value: unknown): value is SoftphoneError {\r\n if (typeof value !== 'object' || value === null) return false;\r\n const candidate = value as { code?: unknown; fatal?: unknown };\r\n return typeof candidate.fatal === 'boolean' && (candidate.code as string) in ERROR_CATALOGUE;\r\n}\r\n\r\n/** Type guard, for hosts that would rather not rely on structural narrowing. */\r\nexport function isFatal(error: SoftphoneError): error is FatalSoftphoneError {\r\n return error.fatal;\r\n}\r\n\r\n/**\r\n * Construct a catalogue-backed error. **The only way the SDK raises one** — every `message` and\r\n * `solution` a host sees comes from the table above, so no call site invents error prose.\r\n *\r\n * The return type narrows on the code: `createError('call/busy')` is a `NonFatalSoftphoneError`, and\r\n * reading `.fatal` on it gives `false`, not `boolean`.\r\n */\r\nexport function createError<C extends SoftphoneErrorCode>(\r\n code: C,\r\n cause?: unknown,\r\n): C extends FatalErrorCode ? FatalSoftphoneError : NonFatalSoftphoneError {\r\n // The one cast in this module. A conditional return type cannot be verified from inside the\r\n // implementation — TypeScript has no way to prove the branch for an unresolved `C` — so the\r\n // guarantee rests on `fatal` being copied from the catalogue that DERIVES `FatalErrorCode`. The\r\n // test suite asserts that agreement for every code, which is the check the compiler cannot make.\r\n return new CatalogueError(code, cause) as C extends FatalErrorCode\r\n ? FatalSoftphoneError\r\n : NonFatalSoftphoneError;\r\n}\r\n"]}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createContext, useContext, useSyncExternalStore, useCallback, useRef } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/react/context.tsx
|
|
5
|
+
var SoftphoneContext = createContext(null);
|
|
6
|
+
function SoftphoneProvider({ softphone, children }) {
|
|
7
|
+
return /* @__PURE__ */ jsx(SoftphoneContext.Provider, { value: softphone, children });
|
|
8
|
+
}
|
|
9
|
+
function useSoftphone() {
|
|
10
|
+
const softphone = useContext(SoftphoneContext);
|
|
11
|
+
if (!softphone) {
|
|
12
|
+
throw new Error("useSoftphone() must be used within a <SoftphoneProvider>.");
|
|
13
|
+
}
|
|
14
|
+
return softphone;
|
|
15
|
+
}
|
|
16
|
+
function useLastError() {
|
|
17
|
+
const phone = useSoftphone();
|
|
18
|
+
return useSyncExternalStore(
|
|
19
|
+
useCallback(
|
|
20
|
+
(cb) => {
|
|
21
|
+
const offError = phone.on("error" /* Error */, () => cb());
|
|
22
|
+
const offState = phone.on("stateChanged" /* StateChanged */, () => cb());
|
|
23
|
+
return () => {
|
|
24
|
+
offError();
|
|
25
|
+
offState();
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
[phone]
|
|
29
|
+
),
|
|
30
|
+
useCallback(() => phone.lastError, [phone])
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
function useSoftphoneState() {
|
|
34
|
+
const phone = useSoftphone();
|
|
35
|
+
return useSyncExternalStore(
|
|
36
|
+
useCallback((cb) => phone.on("stateChanged" /* StateChanged */, () => cb()), [phone]),
|
|
37
|
+
useCallback(() => phone.state, [phone])
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function useIsLeader() {
|
|
41
|
+
const phone = useSoftphone();
|
|
42
|
+
return useSyncExternalStore(
|
|
43
|
+
useCallback((cb) => phone.on("leaderChanged" /* LeaderChanged */, () => cb()), [phone]),
|
|
44
|
+
useCallback(() => phone.isLeader, [phone])
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function useActiveCall() {
|
|
48
|
+
const phone = useSoftphone();
|
|
49
|
+
return useSyncExternalStore(
|
|
50
|
+
useCallback((cb) => phone.on("activeCallChanged" /* ActiveCallChanged */, () => cb()), [phone]),
|
|
51
|
+
useCallback(() => phone.activeCall, [phone])
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
var CALL_EVENTS = [
|
|
55
|
+
"stateChanged" /* StateChanged */,
|
|
56
|
+
"muted" /* Muted */,
|
|
57
|
+
"held" /* Held */,
|
|
58
|
+
"unheld" /* Unheld */,
|
|
59
|
+
"remotePartyUpdated" /* RemotePartyUpdated */,
|
|
60
|
+
"remoteStream" /* RemoteStream */,
|
|
61
|
+
"ended" /* Ended */
|
|
62
|
+
];
|
|
63
|
+
function useCall(call) {
|
|
64
|
+
const cache = useRef(null);
|
|
65
|
+
const subscribe = useCallback(
|
|
66
|
+
(cb) => {
|
|
67
|
+
if (!call) return () => {
|
|
68
|
+
};
|
|
69
|
+
const unsubs = CALL_EVENTS.map(
|
|
70
|
+
(event) => call.on(event, () => {
|
|
71
|
+
cache.current = null;
|
|
72
|
+
cb();
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
return () => {
|
|
76
|
+
for (const off of unsubs) off();
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
[call]
|
|
80
|
+
);
|
|
81
|
+
const getSnapshot = useCallback(() => {
|
|
82
|
+
if (!call) return null;
|
|
83
|
+
if (cache.current?.call === call) return cache.current.view;
|
|
84
|
+
const view = {
|
|
85
|
+
state: call.state,
|
|
86
|
+
isMuted: call.isMuted,
|
|
87
|
+
isOnHold: call.isOnHold,
|
|
88
|
+
remote: call.remote,
|
|
89
|
+
remoteStream: call.remoteStream,
|
|
90
|
+
answeredAt: call.answeredAt
|
|
91
|
+
};
|
|
92
|
+
cache.current = { call, view };
|
|
93
|
+
return view;
|
|
94
|
+
}, [call]);
|
|
95
|
+
return useSyncExternalStore(subscribe, getSnapshot);
|
|
96
|
+
}
|
|
97
|
+
function useDevices() {
|
|
98
|
+
const phone = useSoftphone();
|
|
99
|
+
const cache = useRef(null);
|
|
100
|
+
const subscribe = useCallback(
|
|
101
|
+
(cb) => phone.on("deviceChange" /* DeviceChange */, () => {
|
|
102
|
+
cache.current = null;
|
|
103
|
+
cb();
|
|
104
|
+
}),
|
|
105
|
+
[phone]
|
|
106
|
+
);
|
|
107
|
+
const getSnapshot = useCallback(() => {
|
|
108
|
+
const audio = phone.audio;
|
|
109
|
+
if (cache.current?.audio === audio) return cache.current.view;
|
|
110
|
+
const view = {
|
|
111
|
+
inputs: audio.inputs,
|
|
112
|
+
outputs: audio.outputs,
|
|
113
|
+
currentInputId: audio.currentInputId,
|
|
114
|
+
currentOutputId: audio.currentOutputId,
|
|
115
|
+
canSelectOutput: audio.canSelectOutput,
|
|
116
|
+
setInput: (deviceId) => audio.setInput(deviceId),
|
|
117
|
+
setOutput: (deviceId) => audio.setOutput(deviceId),
|
|
118
|
+
setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),
|
|
119
|
+
refresh: () => audio.refresh()
|
|
120
|
+
};
|
|
121
|
+
cache.current = { audio, view };
|
|
122
|
+
return view;
|
|
123
|
+
}, [phone]);
|
|
124
|
+
return useSyncExternalStore(subscribe, getSnapshot);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useSoftphone, useSoftphoneState };
|
|
128
|
+
//# sourceMappingURL=chunk-3V2L6UTK.js.map
|
|
129
|
+
//# sourceMappingURL=chunk-3V2L6UTK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/context.tsx","../src/react/hooks.ts"],"names":[],"mappings":"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","file":"chunk-3V2L6UTK.js","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 { 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/** 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 canSelectOutput: audio.canSelectOutput,\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"]}
|