@bota.dev/react-native-app-sdk 2.0.0-beta.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 (173) hide show
  1. package/BotaDeviceSDK.podspec +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/android/build.gradle +76 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +172 -0
  7. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycle.kt +100 -0
  8. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +82 -0
  9. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidModels.kt +476 -0
  10. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTA.kt +58 -0
  11. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +180 -0
  12. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +751 -0
  13. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurity.kt +509 -0
  14. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFi.kt +125 -0
  15. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +46 -0
  16. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +723 -0
  17. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +33 -0
  18. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +211 -0
  19. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycleTest.kt +132 -0
  20. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +57 -0
  21. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTATest.kt +75 -0
  22. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +147 -0
  23. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +355 -0
  24. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurityTest.kt +523 -0
  25. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFiTest.kt +100 -0
  26. package/generated/codegen-contract.json +3593 -0
  27. package/ios/BotaDeviceSDK.mm +1300 -0
  28. package/ios/BotaDeviceSDKAppleBridge.swift +2260 -0
  29. package/ios/BotaDeviceSDKAppleDevices.swift +191 -0
  30. package/ios/BotaDeviceSDKAppleLifecycle.swift +167 -0
  31. package/ios/BotaDeviceSDKAppleLogs.swift +79 -0
  32. package/ios/BotaDeviceSDKAppleOTA.swift +73 -0
  33. package/ios/BotaDeviceSDKAppleRecordingUploads.swift +178 -0
  34. package/ios/BotaDeviceSDKAppleRecordings.swift +670 -0
  35. package/ios/BotaDeviceSDKAppleSecurity.swift +586 -0
  36. package/ios/BotaDeviceSDKAppleWiFi.swift +128 -0
  37. package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +99 -0
  38. package/lib/commonjs/BotaClient.js +194 -0
  39. package/lib/commonjs/BotaClient.js.map +1 -0
  40. package/lib/commonjs/ble/constants.js +10 -0
  41. package/lib/commonjs/ble/constants.js.map +1 -0
  42. package/lib/commonjs/ble/deviceLogs.js +50 -0
  43. package/lib/commonjs/ble/deviceLogs.js.map +1 -0
  44. package/lib/commonjs/cache/DeviceStateCache.js +6 -0
  45. package/lib/commonjs/cache/DeviceStateCache.js.map +1 -0
  46. package/lib/commonjs/client.js +746 -0
  47. package/lib/commonjs/client.js.map +1 -0
  48. package/lib/commonjs/compatibility/runtime.js +44 -0
  49. package/lib/commonjs/compatibility/runtime.js.map +1 -0
  50. package/lib/commonjs/index.js +123 -0
  51. package/lib/commonjs/index.js.map +1 -0
  52. package/lib/commonjs/managers/DeviceManager.js +647 -0
  53. package/lib/commonjs/managers/DeviceManager.js.map +1 -0
  54. package/lib/commonjs/managers/OTAManager.js +140 -0
  55. package/lib/commonjs/managers/OTAManager.js.map +1 -0
  56. package/lib/commonjs/managers/RecordingManager.js +444 -0
  57. package/lib/commonjs/managers/RecordingManager.js.map +1 -0
  58. package/lib/commonjs/managers/StreamingSession.js +175 -0
  59. package/lib/commonjs/managers/StreamingSession.js.map +1 -0
  60. package/lib/commonjs/managers/types.js +6 -0
  61. package/lib/commonjs/managers/types.js.map +1 -0
  62. package/lib/commonjs/models/Device.js +2 -0
  63. package/lib/commonjs/models/Device.js.map +1 -0
  64. package/lib/commonjs/models/Recording.js +2 -0
  65. package/lib/commonjs/models/Recording.js.map +1 -0
  66. package/lib/commonjs/models/Status.js +6 -0
  67. package/lib/commonjs/models/Status.js.map +1 -0
  68. package/lib/commonjs/package.json +1 -0
  69. package/lib/commonjs/specs/NativeBotaDeviceSDK.js +9 -0
  70. package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -0
  71. package/lib/commonjs/sync/syncStatus.js +178 -0
  72. package/lib/commonjs/sync/syncStatus.js.map +1 -0
  73. package/lib/commonjs/utils/errors.js +194 -0
  74. package/lib/commonjs/utils/errors.js.map +1 -0
  75. package/lib/commonjs/utils/logger.js +84 -0
  76. package/lib/commonjs/utils/logger.js.map +1 -0
  77. package/lib/module/BotaClient.js +189 -0
  78. package/lib/module/BotaClient.js.map +1 -0
  79. package/lib/module/ble/constants.js +6 -0
  80. package/lib/module/ble/constants.js.map +1 -0
  81. package/lib/module/ble/deviceLogs.js +45 -0
  82. package/lib/module/ble/deviceLogs.js.map +1 -0
  83. package/lib/module/cache/DeviceStateCache.js +4 -0
  84. package/lib/module/cache/DeviceStateCache.js.map +1 -0
  85. package/lib/module/client.js +740 -0
  86. package/lib/module/client.js.map +1 -0
  87. package/lib/module/compatibility/runtime.js +35 -0
  88. package/lib/module/compatibility/runtime.js.map +1 -0
  89. package/lib/module/index.js +17 -0
  90. package/lib/module/index.js.map +1 -0
  91. package/lib/module/managers/DeviceManager.js +641 -0
  92. package/lib/module/managers/DeviceManager.js.map +1 -0
  93. package/lib/module/managers/OTAManager.js +134 -0
  94. package/lib/module/managers/OTAManager.js.map +1 -0
  95. package/lib/module/managers/RecordingManager.js +438 -0
  96. package/lib/module/managers/RecordingManager.js.map +1 -0
  97. package/lib/module/managers/StreamingSession.js +168 -0
  98. package/lib/module/managers/StreamingSession.js.map +1 -0
  99. package/lib/module/managers/types.js +4 -0
  100. package/lib/module/managers/types.js.map +1 -0
  101. package/lib/module/models/Device.js +2 -0
  102. package/lib/module/models/Device.js.map +1 -0
  103. package/lib/module/models/Recording.js +2 -0
  104. package/lib/module/models/Recording.js.map +1 -0
  105. package/lib/module/models/Status.js +4 -0
  106. package/lib/module/models/Status.js.map +1 -0
  107. package/lib/module/specs/NativeBotaDeviceSDK.js +5 -0
  108. package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -0
  109. package/lib/module/sync/syncStatus.js +174 -0
  110. package/lib/module/sync/syncStatus.js.map +1 -0
  111. package/lib/module/utils/errors.js +183 -0
  112. package/lib/module/utils/errors.js.map +1 -0
  113. package/lib/module/utils/logger.js +80 -0
  114. package/lib/module/utils/logger.js.map +1 -0
  115. package/lib/typescript/BotaClient.d.ts +40 -0
  116. package/lib/typescript/BotaClient.d.ts.map +1 -0
  117. package/lib/typescript/ble/constants.d.ts +4 -0
  118. package/lib/typescript/ble/constants.d.ts.map +1 -0
  119. package/lib/typescript/ble/deviceLogs.d.ts +10 -0
  120. package/lib/typescript/ble/deviceLogs.d.ts.map +1 -0
  121. package/lib/typescript/cache/DeviceStateCache.d.ts +14 -0
  122. package/lib/typescript/cache/DeviceStateCache.d.ts.map +1 -0
  123. package/lib/typescript/client.d.ts +208 -0
  124. package/lib/typescript/client.d.ts.map +1 -0
  125. package/lib/typescript/compatibility/runtime.d.ts +11 -0
  126. package/lib/typescript/compatibility/runtime.d.ts.map +1 -0
  127. package/lib/typescript/index.d.ts +19 -0
  128. package/lib/typescript/index.d.ts.map +1 -0
  129. package/lib/typescript/managers/DeviceManager.d.ts +117 -0
  130. package/lib/typescript/managers/DeviceManager.d.ts.map +1 -0
  131. package/lib/typescript/managers/OTAManager.d.ts +25 -0
  132. package/lib/typescript/managers/OTAManager.d.ts.map +1 -0
  133. package/lib/typescript/managers/RecordingManager.d.ts +54 -0
  134. package/lib/typescript/managers/RecordingManager.d.ts.map +1 -0
  135. package/lib/typescript/managers/StreamingSession.d.ts +35 -0
  136. package/lib/typescript/managers/StreamingSession.d.ts.map +1 -0
  137. package/lib/typescript/managers/types.d.ts +19 -0
  138. package/lib/typescript/managers/types.d.ts.map +1 -0
  139. package/lib/typescript/models/Device.d.ts +392 -0
  140. package/lib/typescript/models/Device.d.ts.map +1 -0
  141. package/lib/typescript/models/Recording.d.ts +259 -0
  142. package/lib/typescript/models/Recording.d.ts.map +1 -0
  143. package/lib/typescript/models/Status.d.ts +106 -0
  144. package/lib/typescript/models/Status.d.ts.map +1 -0
  145. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +393 -0
  146. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -0
  147. package/lib/typescript/sync/syncStatus.d.ts +123 -0
  148. package/lib/typescript/sync/syncStatus.d.ts.map +1 -0
  149. package/lib/typescript/utils/errors.d.ts +83 -0
  150. package/lib/typescript/utils/errors.d.ts.map +1 -0
  151. package/lib/typescript/utils/logger.d.ts +36 -0
  152. package/lib/typescript/utils/logger.d.ts.map +1 -0
  153. package/package.json +100 -0
  154. package/scripts/bota_device_sdk_spm_workaround.rb +53 -0
  155. package/src/BotaClient.ts +231 -0
  156. package/src/ble/constants.ts +3 -0
  157. package/src/ble/deviceLogs.ts +61 -0
  158. package/src/cache/DeviceStateCache.ts +20 -0
  159. package/src/client.ts +1410 -0
  160. package/src/compatibility/runtime.ts +55 -0
  161. package/src/index.ts +146 -0
  162. package/src/managers/DeviceManager.ts +927 -0
  163. package/src/managers/OTAManager.ts +195 -0
  164. package/src/managers/RecordingManager.ts +578 -0
  165. package/src/managers/StreamingSession.ts +212 -0
  166. package/src/managers/types.ts +36 -0
  167. package/src/models/Device.ts +484 -0
  168. package/src/models/Recording.ts +281 -0
  169. package/src/models/Status.ts +128 -0
  170. package/src/specs/NativeBotaDeviceSDK.ts +542 -0
  171. package/src/sync/syncStatus.ts +243 -0
  172. package/src/utils/errors.ts +327 -0
  173. package/src/utils/logger.ts +122 -0
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Centralized sync-status derivation.
3
+ *
4
+ * Apps display "what's happening with the recordings on the device right now"
5
+ * in multiple places — sync banners, pending-recordings rows, recording detail
6
+ * pages, list-item subtitles. Without a shared rule set every screen tends to
7
+ * compute its own label from raw inputs, leading to screens that disagree
8
+ * (one shows "Bluetooth syncing", another shows "4G uploading").
9
+ *
10
+ * This module owns the *rules* (pure function over a typed input shape). Apps
11
+ * own the *subscription* (a thin hook that reads from whatever stores/queries
12
+ * they use and calls `deriveSyncStatus`). The label text returned here is the
13
+ * canonical surface — every consumer should read `label` / `shortLabel`
14
+ * verbatim rather than re-deriving from `kind`/`channel`.
15
+ *
16
+ * Precedence policy (when multiple signals are true simultaneously):
17
+ * 1. Live recording — if the device is actively recording AND streaming is
18
+ * enabled, show "Streaming via <channel>" regardless of pending uploads.
19
+ * 2. Actual device transport — if `device.syncActive` is set and a radio
20
+ * (WiFi / 4G) is up, the label reflects *that* transport, even if the
21
+ * app initiated the sync via BLE. This matches what bytes are actually
22
+ * moving and avoids the smart-sync handoff flicker where the app's
23
+ * "BLE pull" state lags the device's "4G upload" reality.
24
+ * 3. App-driven BLE pull — `appDriving.active` (e.g. the consumer's
25
+ * `useSyncAllRecordings` global) shows as "Bluetooth syncing N/M".
26
+ * 4. Device claims sync but no radio is up — `'waiting_channel'`, best-guess
27
+ * channel from `wifiAttempting` / `lteAttempting`.
28
+ * 5. Otherwise — `'idle'`.
29
+ */
30
+
31
+ export type SyncKind =
32
+ | 'idle'
33
+ | 'ble_pull'
34
+ | 'wifi_upload'
35
+ | 'lte_upload'
36
+ | 'streaming'
37
+ | 'waiting_channel';
38
+
39
+ export type SyncChannel = 'Bluetooth' | 'WiFi' | '4G';
40
+
41
+ /**
42
+ * Inputs to `deriveSyncStatus`. Every field is what the *consumer app* already
43
+ * has on hand — the SDK does not subscribe to any state itself.
44
+ *
45
+ * `appDriving` — populated from the consumer's app-level sync state (e.g. the
46
+ * global `isSyncing` + `syncAllProgress` exposed by their `useSyncAll*`
47
+ * hook). Set `active: false` if the app isn't currently pulling.
48
+ *
49
+ * `device` — flags reported by the device. For BLE-connected devices these
50
+ * come from `BotaClient.devices.on('statusUpdate', ...)`. For backend-only
51
+ * visibility (no BLE) they come from the heartbeat-derived state on the
52
+ * server, merged at the app layer.
53
+ *
54
+ * `bleConnected` — whether the app currently has a live BLE link to this
55
+ * device. Used to disambiguate `'ble_pull'` from `'4g/wifi_upload'`.
56
+ */
57
+ export interface SyncStatusInputs {
58
+ appDriving: {
59
+ /** True while the consumer's sync flow is actively pulling via BLE. */
60
+ active: boolean;
61
+ /** 1-indexed current item. Undefined if not tracking per-item progress. */
62
+ currentIndex?: number;
63
+ /** Total items being synced. Undefined if unknown. */
64
+ total?: number;
65
+ };
66
+ device: {
67
+ /** Device's own sync-active flag (bit 5 of the firmware status flags). */
68
+ syncActive: boolean;
69
+ /** Device is currently recording. */
70
+ isRecording: boolean;
71
+ /** Device has a usable WiFi connection. */
72
+ wifiConnected: boolean;
73
+ /** Device has a usable LTE/4G connection. */
74
+ lteConnected: boolean;
75
+ /** Device's WiFi radio is powered up and trying (connecting / scanning). */
76
+ wifiAttempting: boolean;
77
+ /** Device's LTE radio is powered up and trying (registered / searching). */
78
+ lteAttempting: boolean;
79
+ /** Streaming upload mode is enabled on the device. */
80
+ streamingEnabled: boolean;
81
+ /**
82
+ * Device's upload-channel preference order (highest priority first), e.g.
83
+ * `['wifi', 'ble', 'cellular']`. When provided, the streaming channel is
84
+ * resolved by walking this list and picking the first channel that is both
85
+ * enabled (`enabledConnections`) and currently up — so a device whose LTE
86
+ * radio is connected but whose preference ranks BLE higher (or has cellular
87
+ * disabled) is correctly shown as streaming via Bluetooth, not 4G.
88
+ * Omit to fall back to the legacy "first connected radio wins" heuristic.
89
+ */
90
+ uploadPreference?: Array<'wifi' | 'ble' | 'cellular'>;
91
+ /** Which upload channels are enabled on the device. */
92
+ enabledConnections?: { wifi: boolean; cellular: boolean };
93
+ };
94
+ /** BLE link to the device is currently up. */
95
+ bleConnected: boolean;
96
+ }
97
+
98
+ /**
99
+ * Resolve the channel a live stream is actually using. When the device's upload
100
+ * preference + enabled-connection flags are known, walk the preference order and
101
+ * pick the first channel that is enabled AND up (BLE is always available while
102
+ * the link is live) — this matches the firmware/app channel-selection rule. When
103
+ * those inputs are absent, fall back to "first connected radio wins".
104
+ */
105
+ function resolveStreamingChannel(device: SyncStatusInputs['device']): SyncChannel {
106
+ const { wifiConnected, lteConnected, uploadPreference, enabledConnections } = device;
107
+
108
+ if (uploadPreference && enabledConnections) {
109
+ for (const channel of uploadPreference) {
110
+ if (channel === 'ble') return 'Bluetooth';
111
+ if (channel === 'wifi' && enabledConnections.wifi && wifiConnected) return 'WiFi';
112
+ if (channel === 'cellular' && enabledConnections.cellular && lteConnected) return '4G';
113
+ }
114
+ return 'Bluetooth';
115
+ }
116
+
117
+ return wifiConnected ? 'WiFi' : lteConnected ? '4G' : 'Bluetooth';
118
+ }
119
+
120
+ export interface SyncStatus {
121
+ kind: SyncKind;
122
+ /** Resolved channel ('Bluetooth' / 'WiFi' / '4G'), absent for idle. */
123
+ channel?: SyncChannel;
124
+ /** 1-indexed current item, if known. */
125
+ currentIndex?: number;
126
+ /** Total items, if known. */
127
+ total?: number;
128
+ /**
129
+ * Full label suitable for banners. Examples:
130
+ * "Syncing 2/5 via Bluetooth..."
131
+ * "Uploading 1/3 via 4G..."
132
+ * "Streaming via WiFi"
133
+ * "Waiting for upload channel..."
134
+ * "" (when kind === 'idle')
135
+ */
136
+ label: string;
137
+ /**
138
+ * Compact label for tight UI (table rows, badges). Examples:
139
+ * "Bluetooth syncing 2/5"
140
+ * "4G uploading 1/3"
141
+ * "Streaming"
142
+ * "" (when kind === 'idle')
143
+ */
144
+ shortLabel: string;
145
+ }
146
+
147
+ const IDLE: SyncStatus = { kind: 'idle', label: '', shortLabel: '' };
148
+
149
+ function progressFraction(currentIndex?: number, total?: number): string {
150
+ if (currentIndex == null || total == null || total <= 0) return '';
151
+ return ` ${Math.min(currentIndex, total)}/${total}`;
152
+ }
153
+
154
+ /**
155
+ * Pure derivation: given a snapshot of app + device + link state, return the
156
+ * single canonical `SyncStatus`. Safe to call on every render (no I/O, no
157
+ * allocations beyond the return value and label strings).
158
+ */
159
+ export function deriveSyncStatus(inputs: SyncStatusInputs): SyncStatus {
160
+ const { appDriving, device, bleConnected } = inputs;
161
+
162
+ // 1. Live streaming wins — the user is recording right now and bytes are
163
+ // flowing as they're captured, not from queued files.
164
+ if (device.isRecording && device.streamingEnabled) {
165
+ const channel = resolveStreamingChannel(device);
166
+ return {
167
+ kind: 'streaming',
168
+ channel,
169
+ label: `Streaming via ${channel}`,
170
+ shortLabel: `Streaming (${channel})`,
171
+ };
172
+ }
173
+
174
+ // 2. Device says it's syncing AND a radio is up — trust the device's own
175
+ // transport. This wins over `appDriving` so smart-sync handoff (BLE
176
+ // trigger → device 4G upload) doesn't flicker on the label.
177
+ if (device.syncActive && device.wifiConnected) {
178
+ const frac = progressFraction(appDriving.currentIndex, appDriving.total);
179
+ return {
180
+ kind: 'wifi_upload',
181
+ channel: 'WiFi',
182
+ currentIndex: appDriving.currentIndex,
183
+ total: appDriving.total,
184
+ label: `Uploading${frac} via WiFi...`,
185
+ shortLabel: `WiFi uploading${frac}`,
186
+ };
187
+ }
188
+ if (device.syncActive && device.lteConnected && !bleConnected) {
189
+ // When BLE is up the app is in control — see rule 3 — so we let
190
+ // `appDriving` take over if it's active even though LTE is also flagged.
191
+ const frac = progressFraction(appDriving.currentIndex, appDriving.total);
192
+ return {
193
+ kind: 'lte_upload',
194
+ channel: '4G',
195
+ currentIndex: appDriving.currentIndex,
196
+ total: appDriving.total,
197
+ label: `Uploading${frac} via 4G...`,
198
+ shortLabel: `4G uploading${frac}`,
199
+ };
200
+ }
201
+
202
+ // 3. App-driven BLE pull.
203
+ if (appDriving.active) {
204
+ const frac = progressFraction(appDriving.currentIndex, appDriving.total);
205
+ return {
206
+ kind: 'ble_pull',
207
+ channel: 'Bluetooth',
208
+ currentIndex: appDriving.currentIndex,
209
+ total: appDriving.total,
210
+ label: `Syncing${frac} via Bluetooth...`,
211
+ shortLabel: `Bluetooth syncing${frac}`,
212
+ };
213
+ }
214
+
215
+ // 4. Device says it's syncing but no radio is up yet — typically a 4G
216
+ // cold-start (PDP attach in progress) or WiFi reconnect.
217
+ if (device.syncActive) {
218
+ const guess: SyncChannel | undefined = device.wifiAttempting
219
+ ? 'WiFi'
220
+ : device.lteAttempting
221
+ ? '4G'
222
+ : undefined;
223
+ if (guess) {
224
+ const frac = progressFraction(appDriving.currentIndex, appDriving.total);
225
+ return {
226
+ kind: 'waiting_channel',
227
+ channel: guess,
228
+ currentIndex: appDriving.currentIndex,
229
+ total: appDriving.total,
230
+ label: `Uploading${frac} via ${guess}...`,
231
+ shortLabel: `${guess} uploading${frac}`,
232
+ };
233
+ }
234
+ return {
235
+ kind: 'waiting_channel',
236
+ label: 'Waiting for upload channel...',
237
+ shortLabel: 'Waiting for channel',
238
+ };
239
+ }
240
+
241
+ // 5. Idle.
242
+ return IDLE;
243
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Custom error types for the Bota SDK
3
+ */
4
+
5
+ import { BLE_ERROR_STORAGE_KEY_UNAVAILABLE } from '../ble/constants';
6
+
7
+ /**
8
+ * Base error class for all Bota SDK errors
9
+ */
10
+ export class BotaError extends Error {
11
+ constructor(
12
+ message: string,
13
+ public readonly code: string,
14
+ public readonly cause?: Error
15
+ ) {
16
+ super(message);
17
+ this.name = 'BotaError';
18
+ Object.setPrototypeOf(this, BotaError.prototype);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Bluetooth-related errors
24
+ */
25
+ export class BluetoothError extends BotaError {
26
+ constructor(message: string, code: string, cause?: Error) {
27
+ super(message, code, cause);
28
+ this.name = 'BluetoothError';
29
+ Object.setPrototypeOf(this, BluetoothError.prototype);
30
+ }
31
+
32
+ static unavailable(): BluetoothError {
33
+ return new BluetoothError(
34
+ 'Bluetooth is not available on this device',
35
+ 'BLUETOOTH_UNAVAILABLE'
36
+ );
37
+ }
38
+
39
+ static unauthorized(): BluetoothError {
40
+ return new BluetoothError(
41
+ 'Bluetooth permission not granted',
42
+ 'BLUETOOTH_UNAUTHORIZED'
43
+ );
44
+ }
45
+
46
+ static poweredOff(): BluetoothError {
47
+ return new BluetoothError('Bluetooth is powered off', 'BLUETOOTH_OFF');
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Device-related errors
53
+ */
54
+ export class DeviceError extends BotaError {
55
+ constructor(
56
+ message: string,
57
+ code: string,
58
+ public readonly deviceId?: string,
59
+ cause?: Error
60
+ ) {
61
+ super(message, code, cause);
62
+ this.name = 'DeviceError';
63
+ Object.setPrototypeOf(this, DeviceError.prototype);
64
+ }
65
+
66
+ static notFound(deviceId: string): DeviceError {
67
+ return new DeviceError(
68
+ `Device ${deviceId} not found`,
69
+ 'DEVICE_NOT_FOUND',
70
+ deviceId
71
+ );
72
+ }
73
+
74
+ static connectionFailed(deviceId: string, cause?: Error): DeviceError {
75
+ return new DeviceError(
76
+ `Failed to connect to device ${deviceId}`,
77
+ 'CONNECTION_FAILED',
78
+ deviceId,
79
+ cause
80
+ );
81
+ }
82
+
83
+ static connectionLost(deviceId: string, during?: string): DeviceError {
84
+ const message = during
85
+ ? `Connection to device ${deviceId} lost during ${during}`
86
+ : `Connection to device ${deviceId} lost`;
87
+ return new DeviceError(message, 'CONNECTION_LOST', deviceId);
88
+ }
89
+
90
+ static bondingFailed(deviceId: string, cause?: Error): DeviceError {
91
+ return new DeviceError(
92
+ `Bonding failed for device ${deviceId}`,
93
+ 'BONDING_FAILED',
94
+ deviceId,
95
+ cause
96
+ );
97
+ }
98
+
99
+ static notConnected(deviceId: string): DeviceError {
100
+ return new DeviceError(
101
+ `Device ${deviceId} is not connected`,
102
+ 'NOT_CONNECTED',
103
+ deviceId
104
+ );
105
+ }
106
+
107
+ static alreadyConnected(deviceId: string): DeviceError {
108
+ return new DeviceError(
109
+ `Device ${deviceId} is already connected`,
110
+ 'ALREADY_CONNECTED',
111
+ deviceId
112
+ );
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Provisioning-related errors
118
+ */
119
+ export class ProvisioningError extends BotaError {
120
+ constructor(
121
+ message: string,
122
+ code: string,
123
+ public readonly deviceId?: string,
124
+ cause?: Error
125
+ ) {
126
+ super(message, code, cause);
127
+ this.name = 'ProvisioningError';
128
+ Object.setPrototypeOf(this, ProvisioningError.prototype);
129
+ }
130
+
131
+ static invalidToken(deviceId: string): ProvisioningError {
132
+ return new ProvisioningError(
133
+ 'Device rejected the token as invalid',
134
+ 'INVALID_TOKEN',
135
+ deviceId
136
+ );
137
+ }
138
+
139
+ static storageError(deviceId: string): ProvisioningError {
140
+ return new ProvisioningError(
141
+ 'Device failed to store the token',
142
+ 'STORAGE_ERROR',
143
+ deviceId
144
+ );
145
+ }
146
+
147
+ static chunkError(deviceId: string): ProvisioningError {
148
+ return new ProvisioningError(
149
+ 'Token chunk transfer failed',
150
+ 'CHUNK_ERROR',
151
+ deviceId
152
+ );
153
+ }
154
+
155
+ static alreadyProvisioned(deviceId: string): ProvisioningError {
156
+ return new ProvisioningError(
157
+ 'Device is already provisioned',
158
+ 'ALREADY_PROVISIONED',
159
+ deviceId
160
+ );
161
+ }
162
+
163
+ static alreadyPaired(deviceId: string): ProvisioningError {
164
+ return new ProvisioningError(
165
+ 'Device is already paired with another credential. Factory reset the device before re-provisioning.',
166
+ 'ALREADY_PAIRED',
167
+ deviceId
168
+ );
169
+ }
170
+
171
+ static timeout(deviceId: string): ProvisioningError {
172
+ return new ProvisioningError(
173
+ 'Provisioning timed out waiting for device response',
174
+ 'PROVISIONING_TIMEOUT',
175
+ deviceId
176
+ );
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Transfer-related errors
182
+ */
183
+ export class TransferError extends BotaError {
184
+ constructor(
185
+ message: string,
186
+ code: string,
187
+ public readonly recordingUuid?: string,
188
+ cause?: Error
189
+ ) {
190
+ super(message, code, cause);
191
+ this.name = 'TransferError';
192
+ Object.setPrototypeOf(this, TransferError.prototype);
193
+ }
194
+
195
+ static recordingNotFound(uuid: string): TransferError {
196
+ return new TransferError(
197
+ `Recording ${uuid} not found on device`,
198
+ 'RECORDING_NOT_FOUND',
199
+ uuid
200
+ );
201
+ }
202
+
203
+ static checksumMismatch(uuid: string): TransferError {
204
+ return new TransferError(
205
+ `Checksum mismatch for recording ${uuid}`,
206
+ 'CHECKSUM_MISMATCH',
207
+ uuid
208
+ );
209
+ }
210
+
211
+ static interrupted(uuid: string, cause?: Error): TransferError {
212
+ return new TransferError(
213
+ `Transfer interrupted for recording ${uuid}`,
214
+ 'TRANSFER_INTERRUPTED',
215
+ uuid,
216
+ cause
217
+ );
218
+ }
219
+
220
+ static timeout(uuid: string): TransferError {
221
+ return new TransferError(
222
+ `Transfer timed out for recording ${uuid}`,
223
+ 'TRANSFER_TIMEOUT',
224
+ uuid
225
+ );
226
+ }
227
+
228
+ static deviceError(uuid: string, errorCode: number): TransferError {
229
+ if (errorCode === BLE_ERROR_STORAGE_KEY_UNAVAILABLE) {
230
+ return new TransferError(
231
+ `Recording ${uuid} exists, but its device storage key is unavailable`,
232
+ 'STORAGE_KEY_UNAVAILABLE',
233
+ uuid
234
+ );
235
+ }
236
+ return new TransferError(
237
+ `Device error (code: ${errorCode}) during transfer of ${uuid}`,
238
+ 'DEVICE_ERROR',
239
+ uuid
240
+ );
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Upload-related errors
246
+ */
247
+ export class UploadError extends BotaError {
248
+ constructor(
249
+ message: string,
250
+ code: string,
251
+ public readonly taskId?: string,
252
+ cause?: Error
253
+ ) {
254
+ super(message, code, cause);
255
+ this.name = 'UploadError';
256
+ Object.setPrototypeOf(this, UploadError.prototype);
257
+ }
258
+
259
+ static urlExpired(taskId: string): UploadError {
260
+ return new UploadError(
261
+ 'Upload URL has expired',
262
+ 'URL_EXPIRED',
263
+ taskId
264
+ );
265
+ }
266
+
267
+ static networkUnavailable(): UploadError {
268
+ return new UploadError(
269
+ 'Network is not available',
270
+ 'NETWORK_UNAVAILABLE'
271
+ );
272
+ }
273
+
274
+ static uploadFailed(taskId: string, cause?: Error): UploadError {
275
+ return new UploadError(
276
+ `Upload failed for task ${taskId}`,
277
+ 'UPLOAD_FAILED',
278
+ taskId,
279
+ cause
280
+ );
281
+ }
282
+
283
+ static completionFailed(taskId: string, cause?: Error): UploadError {
284
+ return new UploadError(
285
+ `Failed to notify completion for task ${taskId}`,
286
+ 'COMPLETION_FAILED',
287
+ taskId,
288
+ cause
289
+ );
290
+ }
291
+ }
292
+
293
+ /**
294
+ * SDK state errors
295
+ */
296
+ export class SdkError extends BotaError {
297
+ constructor(message: string, code: string, cause?: Error) {
298
+ super(message, code, cause);
299
+ this.name = 'SdkError';
300
+ Object.setPrototypeOf(this, SdkError.prototype);
301
+ }
302
+
303
+ static notInitialized(): SdkError {
304
+ return new SdkError(
305
+ 'SDK has not been initialized. Call BotaClient.configure() first.',
306
+ 'NOT_INITIALIZED'
307
+ );
308
+ }
309
+
310
+ static invalidState(expected: string, actual: string): SdkError {
311
+ return new SdkError(
312
+ `Invalid state: expected ${expected}, got ${actual}`,
313
+ 'INVALID_STATE'
314
+ );
315
+ }
316
+
317
+ static timeout(operation: string): SdkError {
318
+ return new SdkError(`Operation timed out: ${operation}`, 'TIMEOUT');
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Type guard to check if an error is a BotaError
324
+ */
325
+ export function isBotaError(error: unknown): error is BotaError {
326
+ return error instanceof BotaError;
327
+ }
@@ -0,0 +1,122 @@
1
+ import type { LogLevel } from '../models/Status';
2
+
3
+ export type SdkLogLevel = Exclude<LogLevel, 'none'>;
4
+
5
+ export interface SdkLogEntry {
6
+ level: SdkLogLevel;
7
+ message: string;
8
+ context?: Record<string, unknown>;
9
+ timestamp: Date;
10
+ }
11
+
12
+ export type LogHandler = (entry: SdkLogEntry) => void;
13
+
14
+ const LOG_LEVELS: Record<LogLevel, number> = {
15
+ debug: 0,
16
+ info: 1,
17
+ warn: 2,
18
+ error: 3,
19
+ none: 4,
20
+ };
21
+
22
+ class Logger {
23
+ private level: LogLevel = 'warn';
24
+ private readonly prefix = '[Bota SDK]';
25
+ private handler: LogHandler | null = null;
26
+
27
+ setLevel(level: LogLevel): void {
28
+ this.level = level;
29
+ }
30
+
31
+ getLevel(): LogLevel {
32
+ return this.level;
33
+ }
34
+
35
+ setHandler(handler: LogHandler | null): void {
36
+ this.handler = handler;
37
+ }
38
+
39
+ debug(message: string, context?: Record<string, unknown>): void {
40
+ this.write('debug', message, context);
41
+ }
42
+
43
+ info(message: string, context?: Record<string, unknown>): void {
44
+ this.write('info', message, context);
45
+ }
46
+
47
+ warn(message: string, context?: Record<string, unknown>): void {
48
+ this.write('warn', message, context);
49
+ }
50
+
51
+ error(
52
+ message: string,
53
+ error?: Error,
54
+ context?: Record<string, unknown>
55
+ ): void {
56
+ this.write(
57
+ 'error',
58
+ message,
59
+ error
60
+ ? { ...context, error: error.message, stack: error.stack }
61
+ : context
62
+ );
63
+ }
64
+
65
+ tag(tag: string): TaggedLogger {
66
+ return new TaggedLogger(this, tag);
67
+ }
68
+
69
+ private write(
70
+ level: SdkLogLevel,
71
+ message: string,
72
+ context?: Record<string, unknown>
73
+ ): void {
74
+ if (LOG_LEVELS[level] < LOG_LEVELS[this.level]) return;
75
+ if (this.handler) {
76
+ this.handler({ level, message, context, timestamp: new Date() });
77
+ return;
78
+ }
79
+ const output = this.format(level, message, context);
80
+ console[level](output);
81
+ }
82
+
83
+ private format(
84
+ level: SdkLogLevel,
85
+ message: string,
86
+ context?: Record<string, unknown>
87
+ ): string {
88
+ const contextValue = context ? ` ${JSON.stringify(context)}` : '';
89
+ return `${new Date().toISOString()} ${this.prefix} [${level.toUpperCase()}] ${message}${contextValue}`;
90
+ }
91
+ }
92
+
93
+ class TaggedLogger {
94
+ constructor(
95
+ private readonly parent: Logger,
96
+ private readonly tagValue: string
97
+ ) {}
98
+
99
+ debug(message: string, context?: Record<string, unknown>): void {
100
+ this.parent.debug(`[${this.tagValue}] ${message}`, context);
101
+ }
102
+
103
+ info(message: string, context?: Record<string, unknown>): void {
104
+ this.parent.info(`[${this.tagValue}] ${message}`, context);
105
+ }
106
+
107
+ warn(message: string, context?: Record<string, unknown>): void {
108
+ this.parent.warn(`[${this.tagValue}] ${message}`, context);
109
+ }
110
+
111
+ error(
112
+ message: string,
113
+ error?: Error,
114
+ context?: Record<string, unknown>
115
+ ): void {
116
+ this.parent.error(`[${this.tagValue}] ${message}`, error, context);
117
+ }
118
+ }
119
+
120
+ export const logger = new Logger();
121
+
122
+ export type { TaggedLogger };