@hitch42/applet 0.1.1-beta.1 → 0.1.1-beta.11

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 (178) hide show
  1. package/dist/_internal/client.js +120 -11
  2. package/dist/_internal/log-context.d.ts +34 -0
  3. package/dist/_internal/log-context.js +2 -0
  4. package/dist/_internal/server.d.ts +14 -1
  5. package/dist/_internal/server.js +122 -118
  6. package/dist/applet.js +53 -29
  7. package/dist/build-Bh_S5BUv.js +2 -0
  8. package/dist/{build-Rk3xDZV7.js → build-CEfFoIZZ.js} +30 -14
  9. package/dist/ci-BKw14X2h.js +69 -0
  10. package/dist/client.d.ts +3 -3
  11. package/dist/{contract-DMrpbaLJ.d.ts → contract-CsUpTBe0.d.ts} +1 -1
  12. package/dist/{deploy-D3BGDqgC.js → deploy-wGYjwXrx.js} +249 -35
  13. package/dist/email-DBTVhNB_.js +62 -0
  14. package/dist/{generate-CH0VQmUi.js → generate-BEIYs17x.js} +79 -25
  15. package/dist/generate-Dox-OYt6.js +2 -0
  16. package/dist/headers-DkNr9MLJ.js +17 -0
  17. package/dist/index.d.ts +123 -11
  18. package/dist/index.js +142 -26
  19. package/dist/{init-BS1Y-eNL.js → init-6oGPM46R.js} +17 -18
  20. package/dist/load-config-xEdTXs0y.js +27 -0
  21. package/dist/log-context-zu8U9Sjm.js +73 -0
  22. package/dist/login-CUx3TmFQ.js +12 -0
  23. package/dist/{logs-m00pzr_O.js → logs-DSjuEN0Q.js} +20 -22
  24. package/dist/{manifest-BQCAxATS.d.ts → manifest-C6_F4bL8.d.ts} +9 -0
  25. package/dist/{manifest-DyoJ1MIL.js → manifest-CJSL9GuC.js} +25 -3
  26. package/dist/me-BW7Paary.js +32 -0
  27. package/dist/{project-CxN-Dtc7.js → project-BRZGd6xq.js} +227 -34
  28. package/dist/prompt-ChS-grYO.js +29 -0
  29. package/dist/query-cache-CXmAyLv3.js +82 -0
  30. package/dist/react.d.ts +3 -2
  31. package/dist/react.js +98 -80
  32. package/dist/schedules-C8_Svr4h.js +42 -0
  33. package/dist/server.d.ts +9 -4
  34. package/dist/server.js +29 -8
  35. package/dist/telephony-protocol-D-nf3Gca.d.ts +76 -0
  36. package/dist/telephony.d.ts +97 -0
  37. package/dist/telephony.js +726 -0
  38. package/dist/{upload-YbRKEqR6.d.ts → upload-DKZzz5kW.d.ts} +6 -6
  39. package/dist/{variables-CP7Ce3Np.js → variables-DT5RyHdH.js} +14 -39
  40. package/dist/vite.d.ts +3 -60
  41. package/dist/vite.js +503 -34
  42. package/dist/workflow-engine-BOTzURKM.js +111 -0
  43. package/dist/{auth-DaVK0k5R.js → workspace-D0dnnlxL.js} +75 -40
  44. package/package.json +19 -6
  45. package/scaffold/.agents/skills/hitch-applets/SKILL.md +60 -15
  46. package/scaffold/.agents/skills/hitch-applets/references/email-tracking.md +49 -0
  47. package/scaffold/.agents/skills/hitch-applets/references/files.md +3 -3
  48. package/scaffold/.agents/skills/hitch-applets/references/manifest.md +29 -2
  49. package/scaffold/.agents/skills/hitch-applets/references/markdown.md +2 -0
  50. package/scaffold/.agents/skills/hitch-applets/references/performance.md +74 -0
  51. package/scaffold/.agents/skills/hitch-applets/references/records.md +32 -8
  52. package/scaffold/.agents/skills/hitch-applets/references/schedules.md +7 -2
  53. package/scaffold/.agents/skills/hitch-applets/references/server.md +0 -3
  54. package/scaffold/.agents/skills/hitch-applets/references/workflows.md +11 -3
  55. package/scaffold/.agents/skills/hitch-cli/SKILL.md +129 -0
  56. package/scaffold/.agents/skills/hitch-data/SKILL.md +52 -0
  57. package/scaffold/.agents/skills/hitch-data/references/attributes.md +127 -0
  58. package/scaffold/.agents/skills/hitch-data/references/limits.md +64 -0
  59. package/scaffold/.agents/skills/hitch-theory/SKILL.md +151 -0
  60. package/scaffold/.agents/skills/react-best-practices/SKILL.md +127 -0
  61. package/scaffold/.agents/skills/react-best-practices/rules/advanced-effect-event-deps.md +50 -0
  62. package/scaffold/.agents/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  63. package/scaffold/.agents/skills/react-best-practices/rules/advanced-init-once.md +42 -0
  64. package/scaffold/.agents/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
  65. package/scaffold/.agents/skills/react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
  66. package/scaffold/.agents/skills/react-best-practices/rules/async-defer-await.md +82 -0
  67. package/scaffold/.agents/skills/react-best-practices/rules/async-dependencies.md +48 -0
  68. package/scaffold/.agents/skills/react-best-practices/rules/async-parallel.md +24 -0
  69. package/scaffold/.agents/skills/react-best-practices/rules/bundle-analyzable-paths.md +42 -0
  70. package/scaffold/.agents/skills/react-best-practices/rules/bundle-barrel-imports.md +39 -0
  71. package/scaffold/.agents/skills/react-best-practices/rules/bundle-conditional.md +35 -0
  72. package/scaffold/.agents/skills/react-best-practices/rules/bundle-defer-third-party.md +46 -0
  73. package/scaffold/.agents/skills/react-best-practices/rules/bundle-dynamic-imports.md +38 -0
  74. package/scaffold/.agents/skills/react-best-practices/rules/bundle-preload.md +40 -0
  75. package/scaffold/.agents/skills/react-best-practices/rules/client-event-listeners.md +78 -0
  76. package/scaffold/.agents/skills/react-best-practices/rules/client-localstorage-schema.md +74 -0
  77. package/scaffold/.agents/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
  78. package/scaffold/.agents/skills/react-best-practices/rules/js-batch-dom-css.md +110 -0
  79. package/scaffold/.agents/skills/react-best-practices/rules/js-cache-function-results.md +58 -0
  80. package/scaffold/.agents/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
  81. package/scaffold/.agents/skills/react-best-practices/rules/js-cache-storage.md +56 -0
  82. package/scaffold/.agents/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
  83. package/scaffold/.agents/skills/react-best-practices/rules/js-early-exit.md +50 -0
  84. package/scaffold/.agents/skills/react-best-practices/rules/js-flatmap-filter.md +51 -0
  85. package/scaffold/.agents/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
  86. package/scaffold/.agents/skills/react-best-practices/rules/js-index-maps.md +37 -0
  87. package/scaffold/.agents/skills/react-best-practices/rules/js-length-check-first.md +50 -0
  88. package/scaffold/.agents/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
  89. package/scaffold/.agents/skills/react-best-practices/rules/js-request-idle-callback.md +104 -0
  90. package/scaffold/.agents/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
  91. package/scaffold/.agents/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
  92. package/scaffold/.agents/skills/react-best-practices/rules/rendering-activity.md +26 -0
  93. package/scaffold/.agents/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +38 -0
  94. package/scaffold/.agents/skills/react-best-practices/rules/rendering-conditional-render.md +32 -0
  95. package/scaffold/.agents/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
  96. package/scaffold/.agents/skills/react-best-practices/rules/rendering-hoist-jsx.md +36 -0
  97. package/scaffold/.agents/skills/react-best-practices/rules/rendering-resource-hints.md +81 -0
  98. package/scaffold/.agents/skills/react-best-practices/rules/rendering-script-defer-async.md +36 -0
  99. package/scaffold/.agents/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
  100. package/scaffold/.agents/skills/react-best-practices/rules/rendering-usetransition-loading.md +43 -0
  101. package/scaffold/.agents/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
  102. package/scaffold/.agents/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
  103. package/scaffold/.agents/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  104. package/scaffold/.agents/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
  105. package/scaffold/.agents/skills/react-best-practices/rules/rerender-functional-setstate.md +77 -0
  106. package/scaffold/.agents/skills/react-best-practices/rules/rerender-lazy-state-init.md +56 -0
  107. package/scaffold/.agents/skills/react-best-practices/rules/rerender-memo-with-default-value.md +36 -0
  108. package/scaffold/.agents/skills/react-best-practices/rules/rerender-memo.md +44 -0
  109. package/scaffold/.agents/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  110. package/scaffold/.agents/skills/react-best-practices/rules/rerender-no-inline-components.md +75 -0
  111. package/scaffold/.agents/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  112. package/scaffold/.agents/skills/react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
  113. package/scaffold/.agents/skills/react-best-practices/rules/rerender-transitions.md +40 -0
  114. package/scaffold/.agents/skills/react-best-practices/rules/rerender-use-deferred-value.md +59 -0
  115. package/scaffold/.agents/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  116. package/scaffold/.agents/skills/tanstack-query/SKILL.md +136 -0
  117. package/scaffold/.agents/skills/tanstack-query/references/compositions/automatic-invalidation-after-mutations.md +124 -0
  118. package/scaffold/.agents/skills/tanstack-query/references/compositions/broadcast-realtime-and-multi-tab-synchronization.md +150 -0
  119. package/scaffold/.agents/skills/tanstack-query/references/compositions/compose-query-with-tanstack-router-and-start.md +248 -0
  120. package/scaffold/.agents/skills/tanstack-query/references/compositions/enforce-query-best-practices-with-eslint.md +167 -0
  121. package/scaffold/.agents/skills/tanstack-query/references/compositions/persist-offline-and-restore-caches.md +181 -0
  122. package/scaffold/.agents/skills/tanstack-query/references/compositions/query-data-and-forms.md +156 -0
  123. package/scaffold/.agents/skills/tanstack-query/references/core/build-query-abstractions.md +130 -0
  124. package/scaffold/.agents/skills/tanstack-query/references/core/cancel-queries-and-consume-abort-signals.md +176 -0
  125. package/scaffold/.agents/skills/tanstack-query/references/core/concurrent-optimistic-updates.md +135 -0
  126. package/scaffold/.agents/skills/tanstack-query/references/core/coordinate-query-execution.md +188 -0
  127. package/scaffold/.agents/skills/tanstack-query/references/core/design-query-keys-and-options.md +182 -0
  128. package/scaffold/.agents/skills/tanstack-query/references/core/fetch-and-observe-queries.md +193 -0
  129. package/scaffold/.agents/skills/tanstack-query/references/core/implement-optimistic-updates-and-cache-writes.md +168 -0
  130. package/scaffold/.agents/skills/tanstack-query/references/core/paginate-and-build-infinite-queries.md +188 -0
  131. package/scaffold/.agents/skills/tanstack-query/references/core/seed-placeholder-select-and-transform-data.md +188 -0
  132. package/scaffold/.agents/skills/tanstack-query/references/core/selectors-and-derived-state.md +145 -0
  133. package/scaffold/.agents/skills/tanstack-query/references/core/tune-defaults-freshness-retries-and-refetching.md +172 -0
  134. package/scaffold/.agents/skills/tanstack-query/references/core/understand-query-internals-and-observers.md +104 -0
  135. package/scaffold/.agents/skills/tanstack-query/references/core/write-mutations-and-invalidate-related-queries.md +183 -0
  136. package/scaffold/.agents/skills/tanstack-query/references/framework/debug-with-devtools.md +169 -0
  137. package/scaffold/.agents/skills/tanstack-query/references/framework/handle-status-and-errors.md +119 -0
  138. package/scaffold/.agents/skills/tanstack-query/references/framework/shape-data-and-render-efficiently.md +184 -0
  139. package/scaffold/.agents/skills/tanstack-query/references/framework/use-framework-adapter-reactivity.md +185 -0
  140. package/scaffold/.agents/skills/tanstack-query/references/framework/use-suspense-and-error-boundaries.md +208 -0
  141. package/scaffold/.agents/skills/tanstack-query/references/lifecycle/migrate-major-versions-and-codemods.md +169 -0
  142. package/scaffold/.agents/skills/tanstack-query/references/lifecycle/prefetch-and-remove-request-waterfalls.md +228 -0
  143. package/scaffold/.agents/skills/tanstack-query/references/lifecycle/setup-query-client-and-providers.md +161 -0
  144. package/scaffold/.agents/skills/tanstack-query/references/lifecycle/ssr-hydration-and-streaming.md +209 -0
  145. package/scaffold/.agents/skills/tanstack-query/references/lifecycle/test-query-code.md +160 -0
  146. package/scaffold/.agents/skills/tanstack-router/SKILL.md +134 -0
  147. package/scaffold/.agents/skills/tanstack-router/references/compositions/router-query.md +412 -0
  148. package/scaffold/.agents/skills/tanstack-router/references/react-router.md +495 -0
  149. package/scaffold/.agents/skills/tanstack-router/references/router-core/auth-and-guards.md +495 -0
  150. package/scaffold/.agents/skills/tanstack-router/references/router-core/code-splitting.md +318 -0
  151. package/scaffold/.agents/skills/tanstack-router/references/router-core/data-loading.md +490 -0
  152. package/scaffold/.agents/skills/tanstack-router/references/router-core/index.md +158 -0
  153. package/scaffold/.agents/skills/tanstack-router/references/router-core/navigation.md +433 -0
  154. package/scaffold/.agents/skills/tanstack-router/references/router-core/not-found-and-errors.md +436 -0
  155. package/scaffold/.agents/skills/tanstack-router/references/router-core/path-params.md +383 -0
  156. package/scaffold/.agents/skills/tanstack-router/references/router-core/search-params.md +343 -0
  157. package/scaffold/.agents/skills/tanstack-router/references/router-core/ssr.md +462 -0
  158. package/scaffold/.agents/skills/tanstack-router/references/router-core/type-safety.md +493 -0
  159. package/scaffold/.agents/skills/tanstack-router/references/router-plugin.md +250 -0
  160. package/scaffold/.agents/skills/tanstack-router/references/virtual-file-routes.md +215 -0
  161. package/scaffold/.agents/skills/web-design-guidelines/SKILL.md +184 -0
  162. package/scaffold/.github/workflows/hitch.yml +15 -0
  163. package/scaffold/AGENTS.md +2 -2
  164. package/scaffold/applet/src/client/components/ui/alert.tsx +1 -1
  165. package/scaffold/applet/src/client/components/ui/button.tsx +1 -1
  166. package/scaffold/applet/src/client/components/ui/checkbox.tsx +1 -1
  167. package/scaffold/applet/src/client/components/ui/dialog.tsx +2 -2
  168. package/scaffold/applet/src/client/components/ui/dropdown-menu.tsx +2 -2
  169. package/scaffold/applet/src/client/components/ui/input-group.tsx +2 -2
  170. package/scaffold/applet/src/client/components/ui/select.tsx +2 -2
  171. package/scaffold/applet/src/client/components/ui/sheet.tsx +2 -2
  172. package/scaffold/applet/src/client/components/ui/spinner.tsx +2 -1
  173. package/scaffold/applet/src/client/components/ui/tabs.tsx +1 -1
  174. package/scaffold/applet/src/client/lib/utils.ts +1 -6
  175. package/scaffold/applet/src/client/styles/globals.css +2 -0
  176. package/dist/build-CEku1lyb.js +0 -2
  177. package/dist/generate-6Nbt7uii.js +0 -2
  178. package/dist/load-config-BnxocLAQ.js +0 -56
@@ -0,0 +1,726 @@
1
+ import { useEffect, useSyncExternalStore } from "react";
2
+ //#region src/telephony/api.ts
3
+ var TelephonyApiError = class extends Error {
4
+ code;
5
+ status;
6
+ constructor(message, code, status) {
7
+ super(message);
8
+ this.code = code;
9
+ this.status = status;
10
+ this.name = "TelephonyApiError";
11
+ }
12
+ };
13
+ const createTelephonyApi = (baseUrl = location.origin) => {
14
+ const request = async (path, init) => {
15
+ const headers = new Headers(init?.headers);
16
+ if (init?.body) headers.set("content-type", "application/json");
17
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/telephony/${path}`, {
18
+ credentials: "include",
19
+ ...init,
20
+ headers
21
+ });
22
+ if (!response.ok) {
23
+ const body = await response.json().catch(() => null);
24
+ const error = body && typeof body === "object" ? body : {};
25
+ const code = "code" in error && typeof error.code === "string" ? error.code : "request_failed";
26
+ throw new TelephonyApiError("message" in error && typeof error.message === "string" ? error.message : response.statusText, code, response.status);
27
+ }
28
+ return response.json();
29
+ };
30
+ return {
31
+ token: (deviceKey, acceptsCalls) => request("voice/token", {
32
+ method: "POST",
33
+ body: JSON.stringify({
34
+ deviceKey,
35
+ acceptsCalls
36
+ })
37
+ }),
38
+ startCall: (numberId, to) => request("calls", {
39
+ method: "POST",
40
+ body: JSON.stringify({
41
+ numberId,
42
+ to
43
+ })
44
+ }),
45
+ getCall: (callId) => request(`calls/${encodeURIComponent(callId)}`),
46
+ uploadUrl: (callId) => request(`calls/${encodeURIComponent(callId)}/recording`, {
47
+ method: "POST",
48
+ body: "{}"
49
+ })
50
+ };
51
+ };
52
+ //#endregion
53
+ //#region src/telephony/recorder.ts
54
+ const DB_NAME = "hitch-telephony";
55
+ const STORE = "recordings";
56
+ const openDatabase = () => new Promise((resolve, reject) => {
57
+ const request = indexedDB.open(DB_NAME, 1);
58
+ request.onupgradeneeded = () => request.result.createObjectStore(STORE, { keyPath: "key" });
59
+ request.onsuccess = () => resolve(request.result);
60
+ request.onerror = () => reject(request.error);
61
+ });
62
+ const withStore = async (mode, action) => {
63
+ const db = await openDatabase();
64
+ try {
65
+ return await new Promise((resolve, reject) => {
66
+ const transaction = db.transaction(STORE, mode);
67
+ const request = action(transaction.objectStore(STORE));
68
+ request.onsuccess = () => resolve(request.result);
69
+ request.onerror = () => reject(request.error);
70
+ transaction.onerror = () => reject(transaction.error);
71
+ });
72
+ } finally {
73
+ db.close();
74
+ }
75
+ };
76
+ const allItems = () => withStore("readonly", (store) => store.getAll());
77
+ const putItem = (item) => withStore("readwrite", (store) => store.put(item));
78
+ const deleteItem = (key) => withStore("readwrite", (store) => store.delete(key));
79
+ const uploadRecording = async (callId, api) => {
80
+ const items = (await allItems()).filter((item) => item.callId === callId);
81
+ const chunks = items.filter((item) => item.blob).sort((a, b) => a.index - b.index);
82
+ const forget = () => Promise.all(items.map((item) => deleteItem(item.key)));
83
+ if (chunks.length === 0) {
84
+ await forget();
85
+ return;
86
+ }
87
+ const upload = await api.uploadUrl(callId).catch(async (error) => {
88
+ if (error instanceof TelephonyApiError && error.code === "recording_exists") return null;
89
+ throw error;
90
+ });
91
+ if (!upload) {
92
+ await forget();
93
+ return;
94
+ }
95
+ const { uploadUrl } = upload;
96
+ const response = await fetch(uploadUrl, {
97
+ method: "PUT",
98
+ headers: { "content-type": "audio/webm" },
99
+ body: new Blob(chunks.flatMap((item) => item.blob ? [item.blob] : []), { type: "audio/webm" })
100
+ });
101
+ if (!response.ok) throw new Error(`Recording upload failed: ${response.status}`);
102
+ await forget();
103
+ };
104
+ const uploadPendingRecordings = async (api) => {
105
+ await (await allItems()).filter((item) => item.index === -1).sort((a, b) => a.createdAt - b.createdAt).reduce(async (previous, item) => {
106
+ await previous;
107
+ try {
108
+ await uploadRecording(item.callId, api);
109
+ } catch (error) {
110
+ console.error("Recording upload failed", error);
111
+ }
112
+ }, Promise.resolve());
113
+ };
114
+ const startRecording = async (callId, call, api, onError) => {
115
+ const context = new AudioContext();
116
+ try {
117
+ await context.resume();
118
+ const merger = context.createChannelMerger(2);
119
+ context.createMediaStreamSource(call.localStream).connect(merger, 0, 0);
120
+ context.createMediaStreamSource(call.remoteStream).connect(merger, 0, 1);
121
+ const destination = context.createMediaStreamDestination();
122
+ destination.channelCount = 2;
123
+ merger.connect(destination);
124
+ const recorder = new MediaRecorder(destination.stream, {
125
+ mimeType: "audio/webm;codecs=opus",
126
+ audioBitsPerSecond: 32e3
127
+ });
128
+ await putItem({
129
+ key: `${callId}:pending`,
130
+ callId,
131
+ index: -1,
132
+ createdAt: Date.now()
133
+ });
134
+ let index = 0;
135
+ let writes = Promise.resolve();
136
+ recorder.ondataavailable = (event) => {
137
+ if (event.data.size === 0) return;
138
+ const item = {
139
+ key: `${callId}:${index}`,
140
+ callId,
141
+ index,
142
+ createdAt: Date.now(),
143
+ blob: event.data
144
+ };
145
+ index += 1;
146
+ writes = writes.then(() => putItem(item)).then(() => void 0);
147
+ writes.catch(onError);
148
+ };
149
+ recorder.onerror = onError;
150
+ recorder.start(5e3);
151
+ return async () => {
152
+ await new Promise((resolve) => {
153
+ recorder.onstop = () => resolve();
154
+ recorder.stop();
155
+ });
156
+ await writes;
157
+ await context.close();
158
+ await uploadRecording(callId, api);
159
+ };
160
+ } catch (error) {
161
+ await context.close();
162
+ throw error;
163
+ }
164
+ };
165
+ //#endregion
166
+ //#region src/telephony/phone.ts
167
+ const DEVICE_KEY = "hitch:telephony:device";
168
+ let singleton;
169
+ var Phone = class {
170
+ state = {
171
+ owner: false,
172
+ status: "off",
173
+ error: null,
174
+ incoming: null,
175
+ active: null
176
+ };
177
+ listeners = /* @__PURE__ */ new Set();
178
+ api;
179
+ client = null;
180
+ currentCall = null;
181
+ incomingCall = null;
182
+ acceptsCalls;
183
+ connectPromise = null;
184
+ releaseLock = null;
185
+ lockAbortController = null;
186
+ channel = null;
187
+ ownsPhone = false;
188
+ connectResolve = null;
189
+ pendingCommands = /* @__PURE__ */ new Map();
190
+ pollTimer = null;
191
+ stopRecording = null;
192
+ recordingPromise = null;
193
+ audio = null;
194
+ firstLogin = true;
195
+ callStarting = false;
196
+ reconnectOnPageShow = false;
197
+ connectionRequested = false;
198
+ constructor(options = {}) {
199
+ this.api = createTelephonyApi(options.baseUrl);
200
+ this.acceptsCalls = options.acceptsCalls ?? false;
201
+ }
202
+ subscribe = (listener) => {
203
+ this.listeners.add(listener);
204
+ return () => {
205
+ this.listeners.delete(listener);
206
+ };
207
+ };
208
+ getSnapshot = () => this.state;
209
+ update(patch) {
210
+ const wasActive = this.state.active !== null;
211
+ this.state = {
212
+ ...this.state,
213
+ ...patch
214
+ };
215
+ this.listeners.forEach((listener) => listener());
216
+ if (this.ownsPhone) {
217
+ this.channel?.postMessage({
218
+ type: "state",
219
+ state: this.state
220
+ });
221
+ if (wasActive !== (this.state.active !== null) && window.parent !== window) window.parent.postMessage({
222
+ type: "hitch:call-active",
223
+ active: !wasActive
224
+ }, "*");
225
+ }
226
+ }
227
+ get active() {
228
+ if (!this.state.active) return null;
229
+ if (!this.ownsPhone) return {
230
+ hangup: () => this.sendCommand("hangup"),
231
+ mute: () => void this.sendCommand("mute").catch(console.error),
232
+ unmute: () => void this.sendCommand("unmute").catch(console.error),
233
+ hold: () => this.sendCommand("hold"),
234
+ unhold: () => this.sendCommand("unhold"),
235
+ dtmf: (digit) => void this.sendCommand("dtmf", digit).catch(console.error)
236
+ };
237
+ if (!this.currentCall) return null;
238
+ const call = this.currentCall;
239
+ return {
240
+ hangup: () => call.hangup(),
241
+ mute: () => {
242
+ call.muteAudio();
243
+ this.update({ active: {
244
+ ...this.state.active,
245
+ muted: true
246
+ } });
247
+ },
248
+ unmute: () => {
249
+ call.unmuteAudio();
250
+ this.update({ active: {
251
+ ...this.state.active,
252
+ muted: false
253
+ } });
254
+ },
255
+ hold: () => call.hold(),
256
+ unhold: () => call.unhold(),
257
+ dtmf: (digit) => call.dtmf(digit)
258
+ };
259
+ }
260
+ connect = () => {
261
+ this.connectionRequested = true;
262
+ this.connectPromise ??= this.connectInternal().finally(() => {
263
+ this.connectPromise = null;
264
+ });
265
+ return this.connectPromise;
266
+ };
267
+ async connectInternal() {
268
+ if (this.state.status === "ready") return;
269
+ window.addEventListener("pagehide", this.pageHide);
270
+ window.addEventListener("pageshow", this.pageShow);
271
+ this.channel ??= new BroadcastChannel("hitch:telephony");
272
+ this.channel.onmessage = this.onChannelMessage;
273
+ this.update({
274
+ status: "connecting",
275
+ error: null
276
+ });
277
+ if ("locks" in navigator) {
278
+ this.lockAbortController = new AbortController();
279
+ await new Promise((resolve) => {
280
+ this.connectResolve = resolve;
281
+ this.channel?.postMessage({ type: "state-request" });
282
+ navigator.locks.request("hitch:telephony", {
283
+ mode: "exclusive",
284
+ signal: this.lockAbortController.signal
285
+ }, async () => {
286
+ const released = new Promise((release) => {
287
+ this.releaseLock = release;
288
+ });
289
+ this.ownsPhone = true;
290
+ this.update({
291
+ owner: true,
292
+ status: "connecting",
293
+ error: null,
294
+ incoming: null,
295
+ active: null
296
+ });
297
+ await this.connectOwned();
298
+ this.connectResolve?.();
299
+ this.connectResolve = null;
300
+ await released;
301
+ }).catch((error) => {
302
+ if (error instanceof DOMException && error.name === "AbortError") return;
303
+ console.error("Phone lock failed", error);
304
+ this.update({
305
+ status: "error",
306
+ error: String(error)
307
+ });
308
+ this.connectResolve?.();
309
+ this.connectResolve = null;
310
+ });
311
+ });
312
+ return;
313
+ }
314
+ this.ownsPhone = true;
315
+ this.update({ owner: true });
316
+ await this.connectOwned();
317
+ }
318
+ async connectOwned() {
319
+ try {
320
+ const deviceKey = localStorage.getItem(DEVICE_KEY) ?? crypto.randomUUID();
321
+ localStorage.setItem(DEVICE_KEY, deviceKey);
322
+ uploadPendingRecordings(this.api).catch((error) => console.error("Recording recovery failed", error));
323
+ for (const delay of [
324
+ 0,
325
+ 2e3,
326
+ 4e3,
327
+ 8e3
328
+ ]) {
329
+ if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
330
+ const { token } = await this.api.token(deviceKey, this.acceptsCalls);
331
+ const { TelnyxRTC: Client } = await import("@telnyx/webrtc");
332
+ const client = new Client({
333
+ login_token: token,
334
+ keepConnectionAliveOnSocketClose: true
335
+ });
336
+ this.client = client;
337
+ client.on("telnyx.notification", ({ type, call }) => {
338
+ if (type !== "callUpdate" || !call) return;
339
+ if (call === this.currentCall) {
340
+ this.onCallUpdate(call);
341
+ return;
342
+ }
343
+ if (call.direction !== "inbound") return;
344
+ if ([
345
+ "hangup",
346
+ "destroy",
347
+ "purge"
348
+ ].includes(call.state)) {
349
+ if (call === this.incomingCall) {
350
+ this.incomingCall = null;
351
+ this.update({ incoming: null });
352
+ }
353
+ return;
354
+ }
355
+ if (!this.acceptsCalls || this.currentCall || this.callStarting || this.incomingCall && this.incomingCall !== call) {
356
+ call.hangup().catch((error) => console.error("Call decline failed", error));
357
+ return;
358
+ }
359
+ this.incomingCall = call;
360
+ if (call.state === "ringing") this.update({ incoming: {
361
+ id: call.id,
362
+ from: call.options.remoteCallerNumber ?? ""
363
+ } });
364
+ });
365
+ client.on("telnyx.warning", (warning) => {
366
+ if (Number(warning.warning.code) !== 34001) return;
367
+ this.api.token(deviceKey, this.acceptsCalls).then(({ token: nextToken }) => client.login({ creds: { login_token: nextToken } })).catch((error) => {
368
+ console.error("Phone token renewal failed", error);
369
+ this.update({ error: String(error) });
370
+ });
371
+ });
372
+ try {
373
+ await new Promise((resolve, reject) => {
374
+ client.on("telnyx.ready", () => resolve());
375
+ client.on("telnyx.error", (error) => reject(error));
376
+ client.connect().catch(reject);
377
+ });
378
+ this.firstLogin = false;
379
+ this.update({ status: "ready" });
380
+ return;
381
+ } catch (error) {
382
+ await client.disconnect().catch((disconnectError) => console.error("Phone disconnect failed", disconnectError));
383
+ this.client = null;
384
+ if (!this.firstLogin || delay === 8e3) throw error;
385
+ }
386
+ }
387
+ } catch (error) {
388
+ this.update({
389
+ owner: false,
390
+ status: "error",
391
+ error: error instanceof Error ? error.message : String(error)
392
+ });
393
+ this.ownsPhone = false;
394
+ this.releaseLock?.();
395
+ this.releaseLock = null;
396
+ }
397
+ }
398
+ disconnect = () => {
399
+ this.connectionRequested = false;
400
+ this.lockAbortController?.abort();
401
+ this.lockAbortController = null;
402
+ this.connectResolve?.();
403
+ this.connectResolve = null;
404
+ if (this.pollTimer) clearTimeout(this.pollTimer);
405
+ this.pollTimer = null;
406
+ window.removeEventListener("pagehide", this.pageHide);
407
+ window.removeEventListener("pageshow", this.pageShow);
408
+ this.reconnectOnPageShow = false;
409
+ window.removeEventListener("beforeunload", this.beforeUnload);
410
+ this.client?.disconnect().catch((error) => console.error("Phone disconnect failed", error));
411
+ this.client = null;
412
+ this.pendingCommands.forEach(({ reject, timer }) => {
413
+ clearTimeout(timer);
414
+ reject(/* @__PURE__ */ new Error("The phone did not respond."));
415
+ });
416
+ this.pendingCommands.clear();
417
+ this.releaseLock?.();
418
+ this.releaseLock = null;
419
+ this.currentCall = null;
420
+ this.incomingCall = null;
421
+ this.update({
422
+ owner: false,
423
+ status: "off",
424
+ incoming: null,
425
+ active: null
426
+ });
427
+ this.ownsPhone = false;
428
+ this.channel?.close();
429
+ this.channel = null;
430
+ };
431
+ pageHide = () => {
432
+ const reconnect = this.connectionRequested;
433
+ this.disconnect();
434
+ this.reconnectOnPageShow = reconnect;
435
+ if (reconnect) window.addEventListener("pageshow", this.pageShow);
436
+ };
437
+ pageShow = (event) => {
438
+ if (!event.persisted || !this.reconnectOnPageShow) return;
439
+ this.reconnectOnPageShow = false;
440
+ this.connect();
441
+ };
442
+ beforeUnload = (event) => {
443
+ event.preventDefault();
444
+ event.returnValue = "";
445
+ };
446
+ call = async ({ numberId, to }) => {
447
+ if (this.callStarting || this.state.active || this.incomingCall || this.state.incoming) throw new Error("A call is already in progress.");
448
+ if (this.state.status !== "ready") throw new Error("The phone is not ready.");
449
+ if (!this.ownsPhone) return this.sendCommand("call", {
450
+ numberId,
451
+ to
452
+ });
453
+ if (!this.client) throw new Error("The phone is not ready.");
454
+ this.callStarting = true;
455
+ try {
456
+ const { callId, pass, callerNumber, record } = await this.api.startCall(numberId, to);
457
+ this.audio ??= document.createElement("audio");
458
+ this.audio.autoplay = true;
459
+ this.audio.hidden = true;
460
+ if (!this.audio.isConnected) document.body.append(this.audio);
461
+ const call = this.client.newCall({
462
+ destinationNumber: to,
463
+ callerNumber,
464
+ customHeaders: [{
465
+ name: "X-Hitch-Pass",
466
+ value: pass
467
+ }],
468
+ remoteElement: this.audio
469
+ });
470
+ this.currentCall = call;
471
+ this.update({
472
+ error: null,
473
+ active: {
474
+ direction: "outbound",
475
+ callId,
476
+ to,
477
+ from: callerNumber,
478
+ state: "connecting",
479
+ muted: false,
480
+ startedAt: Date.now(),
481
+ recording: record
482
+ }
483
+ });
484
+ window.addEventListener("beforeunload", this.beforeUnload);
485
+ this.pollTimer = setTimeout(() => void this.pollCall(callId), 15e3);
486
+ } finally {
487
+ this.callStarting = false;
488
+ }
489
+ };
490
+ answer = async () => {
491
+ if (!this.state.incoming) throw new Error("There is no incoming call.");
492
+ if (!this.ownsPhone) return this.sendCommand("answer", this.state.incoming.id);
493
+ const call = this.incomingCall;
494
+ if (!call) throw new Error("There is no incoming call.");
495
+ if (!this.client) throw new Error("The phone is not ready.");
496
+ this.audio ??= document.createElement("audio");
497
+ this.audio.autoplay = true;
498
+ this.audio.hidden = true;
499
+ if (!this.audio.isConnected) document.body.append(this.audio);
500
+ this.client.remoteElement = this.audio;
501
+ call.options.remoteElement = this.audio;
502
+ this.currentCall = call;
503
+ this.incomingCall = null;
504
+ this.update({
505
+ incoming: null,
506
+ error: null,
507
+ active: {
508
+ direction: "inbound",
509
+ from: this.state.incoming.from,
510
+ state: "connecting",
511
+ muted: false,
512
+ startedAt: Date.now(),
513
+ recording: false
514
+ }
515
+ });
516
+ window.addEventListener("beforeunload", this.beforeUnload);
517
+ try {
518
+ await call.answer();
519
+ } catch (error) {
520
+ if (this.currentCall === call) {
521
+ this.currentCall = null;
522
+ this.incomingCall = call;
523
+ this.update({
524
+ active: null,
525
+ incoming: {
526
+ id: call.id,
527
+ from: call.options.remoteCallerNumber ?? ""
528
+ }
529
+ });
530
+ window.removeEventListener("beforeunload", this.beforeUnload);
531
+ }
532
+ throw error;
533
+ }
534
+ };
535
+ decline = async () => {
536
+ if (!this.state.incoming) throw new Error("There is no incoming call.");
537
+ if (!this.ownsPhone) return this.sendCommand("decline", this.state.incoming.id);
538
+ const call = this.incomingCall;
539
+ if (!call) throw new Error("There is no incoming call.");
540
+ await call.hangup();
541
+ if (this.incomingCall === call) {
542
+ this.incomingCall = null;
543
+ this.update({ incoming: null });
544
+ }
545
+ };
546
+ sendCommand(action, value) {
547
+ const id = crypto.randomUUID();
548
+ return new Promise((resolve, reject) => {
549
+ const timer = setTimeout(() => {
550
+ this.pendingCommands.delete(id);
551
+ reject(/* @__PURE__ */ new Error("The phone did not respond."));
552
+ }, 15e3);
553
+ this.pendingCommands.set(id, {
554
+ resolve,
555
+ reject,
556
+ timer
557
+ });
558
+ this.channel?.postMessage({
559
+ type: "command",
560
+ id,
561
+ action,
562
+ value
563
+ });
564
+ });
565
+ }
566
+ onChannelMessage = (event) => {
567
+ const data = event.data;
568
+ if (typeof data !== "object" || data === null || !("type" in data)) return;
569
+ if (data.type === "state-request" && this.ownsPhone) this.channel?.postMessage({
570
+ type: "state",
571
+ state: this.state
572
+ });
573
+ else if (data.type === "state" && !this.ownsPhone && "state" in data) {
574
+ const state = data.state;
575
+ if (typeof state !== "object" || state === null || !("status" in state)) return;
576
+ this.state = {
577
+ ...state,
578
+ owner: false
579
+ };
580
+ this.listeners.forEach((listener) => listener());
581
+ this.connectResolve?.();
582
+ this.connectResolve = null;
583
+ } else if (data.type === "reply" && "id" in data && typeof data.id === "string") {
584
+ const pending = this.pendingCommands.get(data.id);
585
+ if (!pending) return;
586
+ clearTimeout(pending.timer);
587
+ this.pendingCommands.delete(data.id);
588
+ if ("error" in data && typeof data.error === "string") pending.reject(new Error(data.error));
589
+ else pending.resolve();
590
+ } else if (data.type === "command" && this.ownsPhone && "id" in data && typeof data.id === "string" && "action" in data) this.runCommand(data).then(() => this.channel?.postMessage({
591
+ type: "reply",
592
+ id: data.id
593
+ }), (error) => this.channel?.postMessage({
594
+ type: "reply",
595
+ id: data.id,
596
+ error: error instanceof Error ? error.message : String(error)
597
+ }));
598
+ };
599
+ async runCommand(data) {
600
+ if (data.action === "answer" || data.action === "decline") {
601
+ if (!this.incomingCall || data.value !== this.incomingCall.id) throw new Error("That call is no longer ringing.");
602
+ return data.action === "answer" ? this.answer() : this.decline();
603
+ }
604
+ if (data.action === "call") {
605
+ const value = data.value;
606
+ if (!value || typeof value !== "object" || !("numberId" in value) || !("to" in value) || typeof value.numberId !== "string" || typeof value.to !== "string") throw new Error("Invalid call command.");
607
+ await this.call({
608
+ numberId: value.numberId,
609
+ to: value.to
610
+ });
611
+ return;
612
+ }
613
+ const active = this.active;
614
+ if (!active) throw new Error("The call is not active.");
615
+ switch (data.action) {
616
+ case "hangup":
617
+ await active.hangup();
618
+ return;
619
+ case "mute":
620
+ active.mute();
621
+ return;
622
+ case "unmute":
623
+ active.unmute();
624
+ return;
625
+ case "hold":
626
+ await active.hold();
627
+ return;
628
+ case "unhold":
629
+ await active.unhold();
630
+ return;
631
+ case "dtmf":
632
+ if (typeof data.value !== "string") throw new Error("Invalid digit.");
633
+ active.dtmf(data.value);
634
+ return;
635
+ default: throw new Error("Invalid phone command.");
636
+ }
637
+ }
638
+ async pollCall(callId) {
639
+ if (this.state.active?.direction !== "outbound" || this.state.active.callId !== callId || this.state.active.state === "active") return;
640
+ try {
641
+ const { status } = await this.api.getCall(callId);
642
+ if ([
643
+ "starting",
644
+ "failed",
645
+ "ended"
646
+ ].includes(status)) {
647
+ await this.currentCall?.hangup();
648
+ this.update({ error: "The call could not be connected." });
649
+ return;
650
+ }
651
+ } catch (error) {
652
+ console.error("Call status check failed", error);
653
+ }
654
+ const active = this.getSnapshot().active;
655
+ if (active?.direction === "outbound" && active.callId === callId) this.pollTimer = setTimeout(() => void this.pollCall(callId), 1e4);
656
+ }
657
+ onCallUpdate(call) {
658
+ const active = this.state.active;
659
+ if (!active) return;
660
+ const state = call.state;
661
+ if ([
662
+ "hangup",
663
+ "destroy",
664
+ "purge"
665
+ ].includes(state)) {
666
+ if (this.pollTimer) clearTimeout(this.pollTimer);
667
+ this.pollTimer = null;
668
+ window.removeEventListener("beforeunload", this.beforeUnload);
669
+ this.update({ active: {
670
+ ...active,
671
+ state: "ended"
672
+ } });
673
+ const stop = this.stopRecording;
674
+ const pending = this.recordingPromise;
675
+ this.stopRecording = null;
676
+ this.recordingPromise = null;
677
+ if (stop) stop().catch((error) => console.error("Recording upload failed", error));
678
+ else if (pending) pending.then((finish) => finish()).catch((error) => console.error("Recording upload failed", error));
679
+ this.currentCall = null;
680
+ this.update({ active: null });
681
+ return;
682
+ }
683
+ const mapped = state === "active" ? "active" : state === "held" ? "held" : state === "ringing" ? "ringing" : "connecting";
684
+ if (mapped === "active" && active.direction === "outbound" && active.recording && !this.stopRecording && !this.recordingPromise) {
685
+ this.recordingPromise = startRecording(active.callId, call, this.api, (error) => {
686
+ console.error("Call recording failed", error);
687
+ if (this.state.active?.direction === "outbound" && this.state.active.callId === active.callId) this.update({ active: {
688
+ ...this.state.active,
689
+ recording: false
690
+ } });
691
+ }).then((stop) => {
692
+ if (this.state.active?.direction === "outbound" && this.state.active.callId === active.callId) this.stopRecording = stop;
693
+ return stop;
694
+ });
695
+ this.recordingPromise.catch((error) => {
696
+ console.error("Call recording failed", error);
697
+ if (this.state.active) this.update({ active: {
698
+ ...this.state.active,
699
+ recording: false
700
+ } });
701
+ });
702
+ }
703
+ if (mapped === "active" && this.pollTimer) clearTimeout(this.pollTimer);
704
+ this.update({ active: {
705
+ ...active,
706
+ state: mapped
707
+ } });
708
+ }
709
+ };
710
+ const createPhone = (options = {}) => singleton ??= new Phone(options);
711
+ //#endregion
712
+ //#region src/telephony/use-phone.ts
713
+ const usePhone = () => {
714
+ const phone = createPhone();
715
+ const state = useSyncExternalStore(phone.subscribe, phone.getSnapshot);
716
+ useEffect(() => {
717
+ phone.connect();
718
+ }, [phone]);
719
+ return {
720
+ state,
721
+ call: phone.call,
722
+ active: phone.active
723
+ };
724
+ };
725
+ //#endregion
726
+ export { createPhone, usePhone };