@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,578 @@
1
+ import EventEmitter from 'eventemitter3';
2
+
3
+ import type {
4
+ BotaDeviceSDKClient,
5
+ BotaRecordingTransferProgress,
6
+ BotaUploadOwnershipResult,
7
+ } from '../client';
8
+ import { getCompatibilityClient } from '../compatibility/runtime';
9
+ import type { ConnectedDevice, StorageInfo } from '../models/Device';
10
+ import type {
11
+ DeviceRecording,
12
+ SyncProgress,
13
+ StreamingSyncOptions,
14
+ StreamingUploadProvider,
15
+ UploadInfo,
16
+ UploadTask,
17
+ } from '../models/Recording';
18
+ import type { RecordingManagerEvents } from '../models/Status';
19
+ import type { UploadInfoProvider } from './types';
20
+ import {
21
+ StreamingSession,
22
+ setStreamingSessionTerminalHandler,
23
+ } from './StreamingSession';
24
+
25
+ type DeviceUploadMonitorOutcome = 'completed' | 'failed' | 'detached';
26
+
27
+ interface TriggerDeviceUploadResponse {
28
+ accepted: boolean;
29
+ errorCode?: number;
30
+ }
31
+
32
+ type ProgressiveOperation<T> = {
33
+ next(): Promise<IteratorResult<BotaRecordingTransferProgress, T>>;
34
+ };
35
+
36
+ const createTaskId = (): string =>
37
+ `task_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
38
+
39
+ const createOpaqueId = (): string =>
40
+ 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (value) => {
41
+ const random = Math.floor(Math.random() * 16);
42
+ const nibble = value === 'x' ? random : (random & 0x3) | 0x8;
43
+ return nibble.toString(16);
44
+ });
45
+
46
+ const errorMessage = (error: unknown): string =>
47
+ error instanceof Error ? error.message : String(error);
48
+
49
+ const observeProgress = <T>(
50
+ start: (onProgress: (progress: BotaRecordingTransferProgress) => void) => Promise<T>
51
+ ): ProgressiveOperation<T> => {
52
+ const queued: BotaRecordingTransferProgress[] = [];
53
+ const waiting: Array<() => void> = [];
54
+ let result: T | undefined;
55
+ let failure: unknown;
56
+ let settled = false;
57
+ const wake = () => waiting.splice(0).forEach((resolve) => resolve());
58
+ void start((progress) => {
59
+ queued.push(progress);
60
+ wake();
61
+ }).then(
62
+ (value) => {
63
+ result = value;
64
+ settled = true;
65
+ wake();
66
+ },
67
+ (error) => {
68
+ failure = error;
69
+ settled = true;
70
+ wake();
71
+ }
72
+ );
73
+ return {
74
+ async next() {
75
+ while (queued.length === 0 && !settled) {
76
+ await new Promise<void>((resolve) => waiting.push(resolve));
77
+ }
78
+ const progress = queued.shift();
79
+ if (progress) return { done: false, value: progress };
80
+ if (failure !== undefined) throw failure;
81
+ return { done: true, value: result as T };
82
+ },
83
+ };
84
+ };
85
+
86
+ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
87
+ private readonly client: BotaDeviceSDKClient;
88
+ private tasks: UploadTask[] = [];
89
+ private initialized = false;
90
+ private paused = false;
91
+ private destroyed = false;
92
+ private activeStreamingSession: StreamingSession | null = null;
93
+
94
+ constructor() {
95
+ super();
96
+ this.client = getCompatibilityClient();
97
+ }
98
+
99
+ async initialize(): Promise<void> {
100
+ if (this.initialized) return;
101
+ this.assertAlive();
102
+ this.tasks = (await this.client.recordings.loadUploadQueue()).map((task) => ({
103
+ ...task,
104
+ createdAt: new Date(task.createdAt),
105
+ updatedAt: new Date(task.updatedAt),
106
+ status: task.status === 'uploading' ? 'pending' : task.status,
107
+ }));
108
+ this.initialized = true;
109
+ }
110
+
111
+ async getStorageInfo(device: ConnectedDevice): Promise<StorageInfo> {
112
+ this.assertReady();
113
+ const [status, recordings] = await Promise.all([
114
+ this.client.devices.readStatus(),
115
+ this.client.recordings.listRecordings(device),
116
+ ]);
117
+ return {
118
+ totalKb: status.storageTotalMb * 1024,
119
+ usedKb: status.storageUsedMb * 1024,
120
+ totalRecordings: recordings.length,
121
+ pendingSyncCount: status.pendingRecordings,
122
+ };
123
+ }
124
+
125
+ async listRecordings(device: ConnectedDevice): Promise<DeviceRecording[]> {
126
+ this.assertReady();
127
+ return this.client.recordings.listRecordings(device);
128
+ }
129
+
130
+ async *syncRecording(
131
+ device: ConnectedDevice,
132
+ recording: DeviceRecording,
133
+ uploadInfo: UploadInfo
134
+ ): AsyncGenerator<SyncProgress> {
135
+ this.assertReady();
136
+ this.emit('syncStarted', recording.uuid);
137
+ const sinkId = createOpaqueId();
138
+ const now = new Date();
139
+ const task: UploadTask = {
140
+ id: createTaskId(),
141
+ recordingId: uploadInfo.recordingId,
142
+ deviceId: device.id,
143
+ localPath: '',
144
+ uploadUrl: uploadInfo.uploadUrl,
145
+ ...(uploadInfo.uploadToken ? { uploadToken: uploadInfo.uploadToken } : {}),
146
+ ...(uploadInfo.completeUrl ? { completeUrl: uploadInfo.completeUrl } : {}),
147
+ ...(uploadInfo.contentType ? { contentType: uploadInfo.contentType } : {}),
148
+ status: 'pending',
149
+ retryCount: 0,
150
+ createdAt: now,
151
+ updatedAt: now,
152
+ };
153
+ this.tasks.push(task);
154
+ await this.persistTasks();
155
+ this.emitQueueUpdated();
156
+
157
+ try {
158
+ yield this.emitSyncProgress(recording.uuid, {
159
+ stage: 'preparing',
160
+ progress: 0,
161
+ totalBytes: recording.fileSizeBytes,
162
+ });
163
+
164
+ const transfer = observeProgress((onProgress) =>
165
+ this.client.recordings.syncRecording(
166
+ device,
167
+ recording,
168
+ onProgress,
169
+ sinkId
170
+ )
171
+ );
172
+ let transferResult: Awaited<
173
+ ReturnType<BotaDeviceSDKClient['recordings']['syncRecording']>
174
+ >;
175
+ while (true) {
176
+ const value = await transfer.next();
177
+ if (value.done) {
178
+ transferResult = value.value;
179
+ break;
180
+ }
181
+ yield this.emitSyncProgress(recording.uuid, {
182
+ stage: 'transferring',
183
+ progress:
184
+ value.value.totalBytes > 0
185
+ ? Math.min(value.value.completedBytes / value.value.totalBytes, 1)
186
+ : 0,
187
+ bytesTransferred: value.value.completedBytes,
188
+ totalBytes: value.value.totalBytes,
189
+ });
190
+ }
191
+ if (transferResult.e2eEncrypted && !uploadInfo.relay) {
192
+ throw new Error(
193
+ 'Device delivered encrypted recording data without an upload relay'
194
+ );
195
+ }
196
+ await this.updateTask(task.id, {
197
+ localPath: transferResult.localPath,
198
+ contentSha256: transferResult.contentSha256,
199
+ relay: transferResult.e2eEncrypted ? uploadInfo.relay : undefined,
200
+ });
201
+
202
+ yield this.emitSyncProgress(recording.uuid, {
203
+ stage: 'uploading',
204
+ progress: 0,
205
+ bytesUploaded: 0,
206
+ totalBytes: recording.fileSizeBytes,
207
+ contentSha256: transferResult.contentSha256,
208
+ });
209
+ const upload = observeProgress((onProgress) =>
210
+ this.runUpload(task.id, onProgress)
211
+ );
212
+ while (true) {
213
+ const value = await upload.next();
214
+ if (value.done) break;
215
+ const progress =
216
+ value.value.totalBytes > 0
217
+ ? Math.min(value.value.completedBytes / value.value.totalBytes, 1)
218
+ : 0;
219
+ yield this.emitSyncProgress(recording.uuid, {
220
+ stage: 'uploading',
221
+ progress,
222
+ bytesUploaded: value.value.completedBytes,
223
+ totalBytes: value.value.totalBytes,
224
+ });
225
+ }
226
+
227
+ yield this.emitSyncProgress(recording.uuid, {
228
+ stage: 'completing',
229
+ progress: 0.5,
230
+ });
231
+ await this.client.recordings.confirmRecording(device, recording.uuid);
232
+ const completed = this.emitSyncProgress(recording.uuid, {
233
+ stage: 'completed',
234
+ progress: 1,
235
+ recordingId: uploadInfo.recordingId,
236
+ contentSha256: transferResult.contentSha256,
237
+ });
238
+ yield completed;
239
+ this.emit('syncCompleted', recording.uuid, uploadInfo.recordingId);
240
+ } catch (error) {
241
+ const failure = error instanceof Error ? error : new Error(String(error));
242
+ if (this.requireTask(task.id).status !== 'completed') {
243
+ await this.updateTask(task.id, {
244
+ status: 'failed',
245
+ errorMessage: failure.message,
246
+ });
247
+ }
248
+ yield this.emitSyncProgress(recording.uuid, {
249
+ stage: 'failed',
250
+ progress: 0,
251
+ error: failure.message,
252
+ });
253
+ this.emit('syncFailed', recording.uuid, failure);
254
+ throw failure;
255
+ }
256
+ }
257
+
258
+ async triggerDeviceUpload(
259
+ device: ConnectedDevice
260
+ ): Promise<TriggerDeviceUploadResponse | null> {
261
+ this.assertReady();
262
+ const result = await this.client.recordings.observeUploadOwnership(device, {
263
+ recordingUuid: '00000000-0000-0000-0000-000000000000',
264
+ uploadId: 'ownership-probe',
265
+ destinationId: 'ownership-probe',
266
+ });
267
+ return result.kind === 'bluetooth_fallback'
268
+ ? { accepted: false }
269
+ : { accepted: true };
270
+ }
271
+
272
+ async *monitorDeviceUpload(
273
+ device: ConnectedDevice,
274
+ initialPendingCount: number
275
+ ): AsyncGenerator<
276
+ SyncProgress & { recordingIndex?: number; totalRecordings?: number },
277
+ DeviceUploadMonitorOutcome
278
+ > {
279
+ this.assertReady();
280
+ yield {
281
+ stage: 'device_uploading',
282
+ progress: 0,
283
+ recordingIndex: 0,
284
+ totalRecordings: initialPendingCount,
285
+ };
286
+ try {
287
+ const result = await this.client.recordings.observeUploadOwnership(device, {
288
+ recordingUuid: '00000000-0000-0000-0000-000000000000',
289
+ uploadId: 'ownership-probe',
290
+ destinationId: 'ownership-probe',
291
+ });
292
+ if (result.kind === 'device_upload_completed') {
293
+ yield {
294
+ stage: 'completed',
295
+ progress: 1,
296
+ recordingIndex: initialPendingCount,
297
+ totalRecordings: initialPendingCount,
298
+ };
299
+ return 'completed';
300
+ }
301
+ return result.kind === 'bluetooth_fallback' ? 'failed' : 'detached';
302
+ } catch {
303
+ return 'detached';
304
+ }
305
+ }
306
+
307
+ async *syncAllRecordings(
308
+ device: ConnectedDevice,
309
+ uploadInfoProvider: UploadInfoProvider
310
+ ): AsyncGenerator<
311
+ SyncProgress & { recordingIndex?: number; totalRecordings?: number }
312
+ > {
313
+ this.assertReady();
314
+ const recordings = await this.listRecordings(device);
315
+ if (recordings.length === 0) return;
316
+
317
+ const ownership = observeProgress((onProgress) =>
318
+ this.client.recordings.observeUploadOwnership(
319
+ device,
320
+ {
321
+ recordingUuid: recordings[0].uuid,
322
+ uploadId: 'ownership-probe',
323
+ destinationId: 'ownership-probe',
324
+ },
325
+ onProgress
326
+ )
327
+ );
328
+ let result: BotaUploadOwnershipResult;
329
+ try {
330
+ while (true) {
331
+ const value = await ownership.next();
332
+ if (value.done) {
333
+ result = value.value;
334
+ break;
335
+ }
336
+ yield {
337
+ stage: 'device_uploading',
338
+ progress:
339
+ value.value.totalBytes > 0
340
+ ? Math.min(value.value.completedBytes / value.value.totalBytes, 1)
341
+ : 0,
342
+ recordingIndex: value.value.completedBytes,
343
+ totalRecordings: recordings.length,
344
+ };
345
+ }
346
+ } catch {
347
+ yield {
348
+ stage: 'device_uploading',
349
+ progress: 0,
350
+ recordingIndex: 0,
351
+ totalRecordings: recordings.length,
352
+ };
353
+ return;
354
+ }
355
+
356
+ if (result.kind === 'device_upload_completed') {
357
+ yield {
358
+ stage: 'completed',
359
+ progress: 1,
360
+ recordingIndex: recordings.length,
361
+ totalRecordings: recordings.length,
362
+ };
363
+ return;
364
+ }
365
+ if (result.kind === 'device_upload_preserved') return;
366
+
367
+ for (let index = 0; index < recordings.length; index += 1) {
368
+ const recording = recordings[index];
369
+ try {
370
+ const uploadInfo = await uploadInfoProvider(recording);
371
+ for await (const progress of this.syncRecording(
372
+ device,
373
+ recording,
374
+ uploadInfo
375
+ )) {
376
+ yield {
377
+ ...progress,
378
+ recordingIndex: index,
379
+ totalRecordings: recordings.length,
380
+ };
381
+ }
382
+ } catch (error) {
383
+ yield {
384
+ stage: 'failed',
385
+ progress: 0,
386
+ error: errorMessage(error),
387
+ recordingIndex: index,
388
+ totalRecordings: recordings.length,
389
+ };
390
+ }
391
+ }
392
+ }
393
+
394
+ getPendingUploads(): UploadTask[] {
395
+ return this.tasks.filter(
396
+ (task) => task.status === 'pending' || task.status === 'uploading'
397
+ );
398
+ }
399
+
400
+ getAllUploads(): UploadTask[] {
401
+ return this.tasks.map((task) => ({ ...task }));
402
+ }
403
+
404
+ startStreamingSync(
405
+ device: ConnectedDevice,
406
+ recordingUuid: string,
407
+ uploadInfoProvider: StreamingUploadProvider,
408
+ options: StreamingSyncOptions = {}
409
+ ): StreamingSession {
410
+ this.assertReady();
411
+ if (this.activeStreamingSession?.isActive) {
412
+ throw new Error('A streaming session is already active');
413
+ }
414
+ const session = new StreamingSession(
415
+ null,
416
+ null,
417
+ device,
418
+ recordingUuid,
419
+ uploadInfoProvider,
420
+ options.chunkSizeKb,
421
+ options.flushIntervalMs
422
+ );
423
+ this.activeStreamingSession = session;
424
+ setStreamingSessionTerminalHandler(session, () => {
425
+ if (this.activeStreamingSession === session) {
426
+ this.activeStreamingSession = null;
427
+ }
428
+ });
429
+ void session.start().catch(() => undefined);
430
+ return session;
431
+ }
432
+
433
+ getActiveStreamingSession(): StreamingSession | null {
434
+ return this.activeStreamingSession;
435
+ }
436
+
437
+ async cancelUpload(taskId: string): Promise<void> {
438
+ await this.client.recordings.cancelRecordingUpload(taskId);
439
+ this.tasks = this.tasks.filter((task) => task.id !== taskId);
440
+ await this.persistTasks();
441
+ this.emitQueueUpdated();
442
+ }
443
+
444
+ async retryFailedUploads(): Promise<void> {
445
+ this.assertReady();
446
+ if (this.paused) return;
447
+ for (const task of this.tasks.filter((value) => value.status === 'failed')) {
448
+ await this.updateTask(task.id, {
449
+ status: 'pending',
450
+ retryCount: task.retryCount + 1,
451
+ errorMessage: undefined,
452
+ });
453
+ try {
454
+ await this.runUpload(task.id);
455
+ } catch {
456
+ // Keep failed tasks available for the next explicit retry.
457
+ }
458
+ }
459
+ }
460
+
461
+ async clearCompletedUploads(): Promise<void> {
462
+ this.tasks = this.tasks.filter((task) => task.status !== 'completed');
463
+ await this.persistTasks();
464
+ this.emitQueueUpdated();
465
+ }
466
+
467
+ async clearAllUploads(): Promise<void> {
468
+ const ids = this.tasks.map((task) => task.id);
469
+ await Promise.all(
470
+ ids.map((taskId) =>
471
+ this.client.recordings.cancelRecordingUpload(taskId).catch(() => undefined)
472
+ )
473
+ );
474
+ this.tasks = [];
475
+ await this.persistTasks();
476
+ this.emitQueueUpdated();
477
+ }
478
+
479
+ pauseUploads(): void {
480
+ this.paused = true;
481
+ }
482
+
483
+ resumeUploads(): void {
484
+ this.paused = false;
485
+ void this.retryFailedUploads();
486
+ }
487
+
488
+ destroy(): void {
489
+ if (this.destroyed) return;
490
+ this.destroyed = true;
491
+ this.activeStreamingSession?.abort();
492
+ this.activeStreamingSession = null;
493
+ void this.client.recordings.destroyCompatibilityOperations().catch(() => undefined);
494
+ this.removeAllListeners();
495
+ this.initialized = false;
496
+ }
497
+
498
+ private async runUpload(
499
+ taskId: string,
500
+ onProgress?: (progress: BotaRecordingTransferProgress) => void
501
+ ): Promise<void> {
502
+ if (this.paused) throw new Error('Upload queue is paused');
503
+ const task = this.requireTask(taskId);
504
+ await this.updateTask(taskId, { status: 'uploading', errorMessage: undefined });
505
+ this.emit('uploadStarted', taskId);
506
+ try {
507
+ await this.client.recordings.uploadRecordingFile(
508
+ this.requireTask(taskId),
509
+ (progress) => {
510
+ onProgress?.(progress);
511
+ const ratio =
512
+ progress.totalBytes > 0
513
+ ? Math.min(progress.completedBytes / progress.totalBytes, 1)
514
+ : 0;
515
+ this.emit('uploadProgress', taskId, ratio);
516
+ }
517
+ );
518
+ await this.updateTask(taskId, { status: 'completed', errorMessage: undefined });
519
+ this.emit('uploadCompleted', taskId, task.recordingId);
520
+ } catch (error) {
521
+ const failure = error instanceof Error ? error : new Error(String(error));
522
+ await this.updateTask(taskId, {
523
+ status: 'failed',
524
+ errorMessage: failure.message,
525
+ });
526
+ this.emit('uploadFailed', taskId, failure);
527
+ throw failure;
528
+ }
529
+ }
530
+
531
+ private emitSyncProgress(
532
+ recordingUuid: string,
533
+ progress: SyncProgress
534
+ ): SyncProgress {
535
+ this.emit('syncProgress', recordingUuid, progress);
536
+ return progress;
537
+ }
538
+
539
+ private requireTask(taskId: string): UploadTask {
540
+ const task = this.tasks.find((value) => value.id === taskId);
541
+ if (!task) throw new Error(`Upload task not found: ${taskId}`);
542
+ return task;
543
+ }
544
+
545
+ private async updateTask(
546
+ taskId: string,
547
+ patch: Partial<UploadTask>
548
+ ): Promise<void> {
549
+ const index = this.tasks.findIndex((task) => task.id === taskId);
550
+ if (index < 0) throw new Error(`Upload task not found: ${taskId}`);
551
+ this.tasks[index] = {
552
+ ...this.tasks[index],
553
+ ...patch,
554
+ updatedAt: new Date(),
555
+ };
556
+ await this.persistTasks();
557
+ this.emitQueueUpdated();
558
+ }
559
+
560
+ private async persistTasks(): Promise<void> {
561
+ await this.client.recordings.saveUploadQueue(this.tasks);
562
+ }
563
+
564
+ private emitQueueUpdated(): void {
565
+ this.emit('queueUpdated', this.getAllUploads());
566
+ }
567
+
568
+ private assertReady(): void {
569
+ this.assertAlive();
570
+ if (!this.initialized) {
571
+ throw new Error('RecordingManager has not been initialized');
572
+ }
573
+ }
574
+
575
+ private assertAlive(): void {
576
+ if (this.destroyed) throw new Error('RecordingManager has been destroyed');
577
+ }
578
+ }