@bota.dev/react-native-app-sdk 2.0.0-beta.1 → 2.0.0-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 (152) hide show
  1. package/README.md +115 -4
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +22 -0
  4. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDiagnostics.kt +53 -0
  5. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +16 -0
  6. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +144 -14
  7. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +150 -29
  8. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +27 -0
  9. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +86 -3
  10. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +9 -0
  11. package/android/src/main/java/dev/bota/sdk/reactnative/upload/BotaUploadV2BackendModule.kt +66 -0
  12. package/android/src/main/java/dev/bota/sdk/reactnative/upload/NativeUpload.kt +400 -0
  13. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadHttp.kt +158 -0
  14. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadJournal.kt +112 -0
  15. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadModels.kt +171 -0
  16. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +35 -0
  17. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +33 -0
  18. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +135 -6
  19. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +236 -10
  20. package/generated/codegen-contract.json +751 -32
  21. package/ios/BotaDeviceSDK.mm +114 -0
  22. package/ios/BotaDeviceSDKAppleBridge.swift +141 -4
  23. package/ios/BotaDeviceSDKAppleDevices.swift +11 -0
  24. package/ios/BotaDeviceSDKAppleDiagnostics.swift +40 -0
  25. package/ios/BotaDeviceSDKAppleLogs.swift +18 -0
  26. package/ios/BotaDeviceSDKAppleRecordingUploads.swift +138 -6
  27. package/ios/BotaDeviceSDKAppleRecordings.swift +113 -5
  28. package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +40 -0
  29. package/ios/BotaUploadV2Backend.mm +49 -0
  30. package/ios/UploadBackend/BotaUploadV2BackendBridge.swift +98 -0
  31. package/ios/UploadBackend/UploadCoordinator.swift +84 -0
  32. package/ios/UploadBackend/UploadHTTP.swift +82 -0
  33. package/ios/UploadBackend/UploadJournal.swift +187 -0
  34. package/ios/UploadBackend/UploadModels.swift +273 -0
  35. package/ios/UploadBackend/UploadOperation.swift +391 -0
  36. package/lib/commonjs/BotaClient.js +16 -2
  37. package/lib/commonjs/BotaClient.js.map +1 -1
  38. package/lib/commonjs/ble/constants.js +5 -1
  39. package/lib/commonjs/ble/constants.js.map +1 -1
  40. package/lib/commonjs/ble/deviceDiagnostics.js +240 -0
  41. package/lib/commonjs/ble/deviceDiagnostics.js.map +1 -0
  42. package/lib/commonjs/client.js +183 -28
  43. package/lib/commonjs/client.js.map +1 -1
  44. package/lib/commonjs/clientPresence.js +26 -0
  45. package/lib/commonjs/clientPresence.js.map +1 -0
  46. package/lib/commonjs/encryptedUploadV2Backend.js +200 -0
  47. package/lib/commonjs/encryptedUploadV2Backend.js.map +1 -0
  48. package/lib/commonjs/index.js +34 -0
  49. package/lib/commonjs/index.js.map +1 -1
  50. package/lib/commonjs/managers/DeviceManager.js +23 -0
  51. package/lib/commonjs/managers/DeviceManager.js.map +1 -1
  52. package/lib/commonjs/managers/NativeUploadQueue.js +386 -0
  53. package/lib/commonjs/managers/NativeUploadQueue.js.map +1 -0
  54. package/lib/commonjs/managers/RecordingManager.js +83 -102
  55. package/lib/commonjs/managers/RecordingManager.js.map +1 -1
  56. package/lib/commonjs/managers/uploadRecoveryMetadata.js +83 -0
  57. package/lib/commonjs/managers/uploadRecoveryMetadata.js.map +1 -0
  58. package/lib/commonjs/sdkIdentity.js +10 -0
  59. package/lib/commonjs/sdkIdentity.js.map +1 -0
  60. package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -1
  61. package/lib/commonjs/specs/NativeBotaUploadV2Backend.js +11 -0
  62. package/lib/commonjs/specs/NativeBotaUploadV2Backend.js.map +1 -0
  63. package/lib/commonjs/uploadRecovery.js +68 -0
  64. package/lib/commonjs/uploadRecovery.js.map +1 -0
  65. package/lib/module/BotaClient.js +16 -2
  66. package/lib/module/BotaClient.js.map +1 -1
  67. package/lib/module/ble/constants.js +4 -0
  68. package/lib/module/ble/constants.js.map +1 -1
  69. package/lib/module/ble/deviceDiagnostics.js +234 -0
  70. package/lib/module/ble/deviceDiagnostics.js.map +1 -0
  71. package/lib/module/client.js +183 -28
  72. package/lib/module/client.js.map +1 -1
  73. package/lib/module/clientPresence.js +22 -0
  74. package/lib/module/clientPresence.js.map +1 -0
  75. package/lib/module/encryptedUploadV2Backend.js +196 -0
  76. package/lib/module/encryptedUploadV2Backend.js.map +1 -0
  77. package/lib/module/index.js +7 -0
  78. package/lib/module/index.js.map +1 -1
  79. package/lib/module/managers/DeviceManager.js +23 -0
  80. package/lib/module/managers/DeviceManager.js.map +1 -1
  81. package/lib/module/managers/NativeUploadQueue.js +380 -0
  82. package/lib/module/managers/NativeUploadQueue.js.map +1 -0
  83. package/lib/module/managers/RecordingManager.js +83 -102
  84. package/lib/module/managers/RecordingManager.js.map +1 -1
  85. package/lib/module/managers/uploadRecoveryMetadata.js +77 -0
  86. package/lib/module/managers/uploadRecoveryMetadata.js.map +1 -0
  87. package/lib/module/sdkIdentity.js +6 -0
  88. package/lib/module/sdkIdentity.js.map +1 -0
  89. package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -1
  90. package/lib/module/specs/NativeBotaUploadV2Backend.js +9 -0
  91. package/lib/module/specs/NativeBotaUploadV2Backend.js.map +1 -0
  92. package/lib/module/uploadRecovery.js +64 -0
  93. package/lib/module/uploadRecovery.js.map +1 -0
  94. package/lib/typescript/BotaClient.d.ts +2 -0
  95. package/lib/typescript/BotaClient.d.ts.map +1 -1
  96. package/lib/typescript/ble/constants.d.ts +4 -0
  97. package/lib/typescript/ble/constants.d.ts.map +1 -1
  98. package/lib/typescript/ble/deviceDiagnostics.d.ts +10 -0
  99. package/lib/typescript/ble/deviceDiagnostics.d.ts.map +1 -0
  100. package/lib/typescript/client.d.ts +24 -3
  101. package/lib/typescript/client.d.ts.map +1 -1
  102. package/lib/typescript/clientPresence.d.ts +26 -0
  103. package/lib/typescript/clientPresence.d.ts.map +1 -0
  104. package/lib/typescript/encryptedUploadV2Backend.d.ts +41 -0
  105. package/lib/typescript/encryptedUploadV2Backend.d.ts.map +1 -0
  106. package/lib/typescript/index.d.ts +13 -4
  107. package/lib/typescript/index.d.ts.map +1 -1
  108. package/lib/typescript/managers/DeviceManager.d.ts +5 -1
  109. package/lib/typescript/managers/DeviceManager.d.ts.map +1 -1
  110. package/lib/typescript/managers/NativeUploadQueue.d.ts +41 -0
  111. package/lib/typescript/managers/NativeUploadQueue.d.ts.map +1 -0
  112. package/lib/typescript/managers/RecordingManager.d.ts +3 -8
  113. package/lib/typescript/managers/RecordingManager.d.ts.map +1 -1
  114. package/lib/typescript/managers/types.d.ts +6 -1
  115. package/lib/typescript/managers/types.d.ts.map +1 -1
  116. package/lib/typescript/managers/uploadRecoveryMetadata.d.ts +6 -0
  117. package/lib/typescript/managers/uploadRecoveryMetadata.d.ts.map +1 -0
  118. package/lib/typescript/models/Device.d.ts +42 -0
  119. package/lib/typescript/models/Device.d.ts.map +1 -1
  120. package/lib/typescript/models/Recording.d.ts +47 -0
  121. package/lib/typescript/models/Recording.d.ts.map +1 -1
  122. package/lib/typescript/models/Status.d.ts +5 -1
  123. package/lib/typescript/models/Status.d.ts.map +1 -1
  124. package/lib/typescript/sdkIdentity.d.ts +3 -0
  125. package/lib/typescript/sdkIdentity.d.ts.map +1 -0
  126. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +70 -0
  127. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -1
  128. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts +18 -0
  129. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts.map +1 -0
  130. package/lib/typescript/uploadRecovery.d.ts +27 -0
  131. package/lib/typescript/uploadRecovery.d.ts.map +1 -0
  132. package/package.json +1 -1
  133. package/scripts/bota_device_sdk_spm_workaround.rb +26 -15
  134. package/src/BotaClient.ts +17 -3
  135. package/src/ble/constants.ts +5 -0
  136. package/src/ble/deviceDiagnostics.ts +238 -0
  137. package/src/client.ts +189 -18
  138. package/src/clientPresence.ts +33 -0
  139. package/src/encryptedUploadV2Backend.ts +171 -0
  140. package/src/index.ts +24 -0
  141. package/src/managers/DeviceManager.ts +23 -0
  142. package/src/managers/NativeUploadQueue.ts +330 -0
  143. package/src/managers/RecordingManager.ts +83 -116
  144. package/src/managers/types.ts +7 -1
  145. package/src/managers/uploadRecoveryMetadata.ts +59 -0
  146. package/src/models/Device.ts +43 -0
  147. package/src/models/Recording.ts +52 -0
  148. package/src/models/Status.ts +5 -1
  149. package/src/sdkIdentity.ts +3 -0
  150. package/src/specs/NativeBotaDeviceSDK.ts +66 -0
  151. package/src/specs/NativeBotaUploadV2Backend.ts +19 -0
  152. package/src/uploadRecovery.ts +66 -0
@@ -16,7 +16,9 @@ import type {
16
16
  UploadTask,
17
17
  } from '../models/Recording';
18
18
  import type { RecordingManagerEvents } from '../models/Status';
19
- import type { UploadInfoProvider } from './types';
19
+ import type { RecordingManagerOptions, UploadInfoProvider } from './types';
20
+ import { NativeUploadQueue } from './NativeUploadQueue';
21
+ import { rejectRecordingDataStore } from './uploadRecoveryMetadata';
20
22
  import {
21
23
  StreamingSession,
22
24
  setStreamingSessionTerminalHandler,
@@ -85,26 +87,30 @@ const observeProgress = <T>(
85
87
 
86
88
  export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
87
89
  private readonly client: BotaDeviceSDKClient;
88
- private tasks: UploadTask[] = [];
90
+ private readonly queue: NativeUploadQueue;
89
91
  private initialized = false;
90
- private paused = false;
91
92
  private destroyed = false;
92
93
  private activeStreamingSession: StreamingSession | null = null;
93
94
 
94
- constructor() {
95
+ constructor();
96
+ constructor(options: RecordingManagerOptions);
97
+ constructor(options: RecordingManagerOptions = {}) {
95
98
  super();
99
+ rejectRecordingDataStore(options.recordingDataStore);
96
100
  this.client = getCompatibilityClient();
101
+ this.queue = new NativeUploadQueue(this.client.recordings, options.uploadRecoveryProvider);
102
+ this.queue.on('queueUpdated', tasks => this.emit('queueUpdated', tasks));
103
+ this.queue.on('uploadStarted', id => this.emit('uploadStarted', id));
104
+ this.queue.on('uploadProgress', (id, progress) => this.emit('uploadProgress', id, progress));
105
+ this.queue.on('uploadCompleted', (id, recordingId) => this.emit('uploadCompleted', id, recordingId));
106
+ this.queue.on('uploadFailed', (id, error) => this.emit('uploadFailed', id, error));
97
107
  }
98
108
 
99
109
  async initialize(): Promise<void> {
100
110
  if (this.initialized) return;
101
111
  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
- }));
112
+ await this.queue.initialize();
113
+ this.assertAlive();
108
114
  this.initialized = true;
109
115
  }
110
116
 
@@ -133,6 +139,10 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
133
139
  uploadInfo: UploadInfo
134
140
  ): AsyncGenerator<SyncProgress> {
135
141
  this.assertReady();
142
+ if (uploadInfo.signal?.aborted) {
143
+ uploadInfo.dispose?.();
144
+ throw new Error('Upload authorization context cancelled');
145
+ }
136
146
  this.emit('syncStarted', recording.uuid);
137
147
  const sinkId = createOpaqueId();
138
148
  const now = new Date();
@@ -140,6 +150,9 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
140
150
  id: createTaskId(),
141
151
  recordingId: uploadInfo.recordingId,
142
152
  deviceId: device.id,
153
+ recordingUuid: recording.uuid,
154
+ recoveryScope: uploadInfo.recoveryScope,
155
+ fileSizeBytes: recording.fileSizeBytes,
143
156
  localPath: '',
144
157
  uploadUrl: uploadInfo.uploadUrl,
145
158
  ...(uploadInfo.uploadToken ? { uploadToken: uploadInfo.uploadToken } : {}),
@@ -150,10 +163,33 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
150
163
  createdAt: now,
151
164
  updatedAt: now,
152
165
  };
153
- this.tasks.push(task);
154
- await this.persistTasks();
155
- this.emitQueueUpdated();
166
+ const recovered = uploadInfo.recoveryScope && this.queue.all().find(value =>
167
+ value.recoveryScope === uploadInfo.recoveryScope && value.recordingId === uploadInfo.recordingId &&
168
+ value.deviceId === device.id && value.recordingUuid === recording.uuid && !!value.localPath
169
+ );
170
+ if (recovered) {
171
+ const adopted = this.queue.offerCredentials(recovered.id, uploadInfo);
172
+ try {
173
+ await this.queue.run(recovered.id);
174
+ this.assertReady();
175
+ if (uploadInfo.signal?.aborted) throw new Error('Upload authorization context cancelled');
176
+ await this.client.recordings.confirmRecording(device, recording.uuid);
177
+ this.assertReady();
178
+ yield this.emitSyncProgress(recording.uuid, { stage: 'completed', progress: 1, recordingId: recovered.recordingId, contentSha256: recovered.contentSha256 });
179
+ this.emit('syncCompleted', recording.uuid, recovered.recordingId);
180
+ } finally {
181
+ if (!adopted) uploadInfo.dispose?.();
182
+ }
183
+ return;
184
+ }
185
+ try {
186
+ await this.queue.add(task, uploadInfo);
187
+ } catch (error) {
188
+ uploadInfo.dispose?.();
189
+ throw error;
190
+ }
156
191
 
192
+ let uploadAttempted = false;
157
193
  try {
158
194
  yield this.emitSyncProgress(recording.uuid, {
159
195
  stage: 'preparing',
@@ -193,10 +229,19 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
193
229
  'Device delivered encrypted recording data without an upload relay'
194
230
  );
195
231
  }
196
- await this.updateTask(task.id, {
232
+ this.assertReady();
233
+ if (uploadInfo.signal?.aborted || !this.queue.get(task.id)) throw new Error('Upload cancelled');
234
+ const nativeSize = 'fileSizeBytes' in transferResult && typeof transferResult.fileSizeBytes === 'number'
235
+ ? transferResult.fileSizeBytes : undefined;
236
+ if (nativeSize !== undefined && (!Number.isSafeInteger(nativeSize) || nativeSize < 0)) throw new Error('Invalid native recording file size');
237
+ if (nativeSize === undefined && uploadInfo.complete) throw new Error('Host completion requires actual native recording file size');
238
+ // Route selection comes from native transfer metadata, never list flags.
239
+ uploadInfo = { ...uploadInfo, relay: transferResult.e2eEncrypted ? uploadInfo.relay : undefined };
240
+ await this.queue.update(task.id, {
197
241
  localPath: transferResult.localPath,
242
+ fileSizeBytes: nativeSize ?? (transferResult.e2eEncrypted ? undefined : recording.fileSizeBytes),
198
243
  contentSha256: transferResult.contentSha256,
199
- relay: transferResult.e2eEncrypted ? uploadInfo.relay : undefined,
244
+ relayUpload: transferResult.e2eEncrypted,
200
245
  });
201
246
 
202
247
  yield this.emitSyncProgress(recording.uuid, {
@@ -206,8 +251,9 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
206
251
  totalBytes: recording.fileSizeBytes,
207
252
  contentSha256: transferResult.contentSha256,
208
253
  });
254
+ uploadAttempted = true;
209
255
  const upload = observeProgress((onProgress) =>
210
- this.runUpload(task.id, onProgress)
256
+ this.queue.run(task.id, onProgress)
211
257
  );
212
258
  while (true) {
213
259
  const value = await upload.next();
@@ -228,7 +274,10 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
228
274
  stage: 'completing',
229
275
  progress: 0.5,
230
276
  });
277
+ this.assertReady();
278
+ if (uploadInfo.signal?.aborted || !this.queue.get(task.id)) throw new Error('Upload cancelled');
231
279
  await this.client.recordings.confirmRecording(device, recording.uuid);
280
+ this.assertReady();
232
281
  const completed = this.emitSyncProgress(recording.uuid, {
233
282
  stage: 'completed',
234
283
  progress: 1,
@@ -239,12 +288,13 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
239
288
  this.emit('syncCompleted', recording.uuid, uploadInfo.recordingId);
240
289
  } catch (error) {
241
290
  const failure = error instanceof Error ? error : new Error(String(error));
242
- if (this.requireTask(task.id).status !== 'completed') {
243
- await this.updateTask(task.id, {
291
+ if (!this.destroyed && this.queue.get(task.id)?.status === 'pending' && !this.queue.get(task.id)?.localPath) {
292
+ await this.queue.update(task.id, {
244
293
  status: 'failed',
245
- errorMessage: failure.message,
294
+ errorMessage: 'Recording transfer failed',
246
295
  });
247
296
  }
297
+ if (this.destroyed) throw failure;
248
298
  yield this.emitSyncProgress(recording.uuid, {
249
299
  stage: 'failed',
250
300
  progress: 0,
@@ -252,6 +302,8 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
252
302
  });
253
303
  this.emit('syncFailed', recording.uuid, failure);
254
304
  throw failure;
305
+ } finally {
306
+ if (!uploadAttempted) this.queue.discardCredentials(task.id);
255
307
  }
256
308
  }
257
309
 
@@ -392,13 +444,13 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
392
444
  }
393
445
 
394
446
  getPendingUploads(): UploadTask[] {
395
- return this.tasks.filter(
447
+ return this.queue.all().filter(
396
448
  (task) => task.status === 'pending' || task.status === 'uploading'
397
449
  );
398
450
  }
399
451
 
400
452
  getAllUploads(): UploadTask[] {
401
- return this.tasks.map((task) => ({ ...task }));
453
+ return this.queue.all();
402
454
  }
403
455
 
404
456
  startStreamingSync(
@@ -435,99 +487,43 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
435
487
  }
436
488
 
437
489
  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();
490
+ this.assertReady();
491
+ await this.queue.cancel(taskId);
442
492
  }
443
493
 
444
494
  async retryFailedUploads(): Promise<void> {
445
495
  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
- }
496
+ await this.queue.retryFailed();
459
497
  }
460
498
 
461
499
  async clearCompletedUploads(): Promise<void> {
462
- this.tasks = this.tasks.filter((task) => task.status !== 'completed');
463
- await this.persistTasks();
464
- this.emitQueueUpdated();
500
+ this.assertReady();
501
+ await this.queue.clearCompleted();
465
502
  }
466
503
 
467
504
  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();
505
+ this.assertReady();
506
+ await this.queue.clearAll();
477
507
  }
478
508
 
479
509
  pauseUploads(): void {
480
- this.paused = true;
510
+ this.queue.pause();
481
511
  }
482
512
 
483
513
  resumeUploads(): void {
484
- this.paused = false;
485
- void this.retryFailedUploads();
514
+ this.queue.resume();
486
515
  }
487
516
 
488
517
  destroy(): void {
489
518
  if (this.destroyed) return;
490
519
  this.destroyed = true;
520
+ this.queue.destroy();
491
521
  this.activeStreamingSession?.abort();
492
522
  this.activeStreamingSession = null;
493
- void this.client.recordings.destroyCompatibilityOperations().catch(() => undefined);
494
523
  this.removeAllListeners();
495
524
  this.initialized = false;
496
525
  }
497
526
 
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
527
  private emitSyncProgress(
532
528
  recordingUuid: string,
533
529
  progress: SyncProgress
@@ -536,35 +532,6 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
536
532
  return progress;
537
533
  }
538
534
 
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
535
  private assertReady(): void {
569
536
  this.assertAlive();
570
537
  if (!this.initialized) {
@@ -1,4 +1,10 @@
1
- import type { DeviceRecording, UploadInfo } from '../models/Recording';
1
+ import type { DeviceRecording, RecordingDataStore, UploadInfo, UploadRecoveryProvider } from '../models/Recording';
2
+
3
+ export interface RecordingManagerOptions {
4
+ uploadRecoveryProvider?: UploadRecoveryProvider;
5
+ /** Unsupported: recording bytes remain native-owned. Configuration throws. */
6
+ recordingDataStore?: RecordingDataStore;
7
+ }
2
8
 
3
9
  export type UploadInfoProvider = (
4
10
  recording: DeviceRecording
@@ -0,0 +1,59 @@
1
+ import type { UploadTask } from '../models/Recording';
2
+
3
+ const invalid = (): never => { throw new Error('Invalid upload recovery journal'); };
4
+
5
+ /** Whitelist, never spread a runtime object into persistent storage. */
6
+ export function persistedUploadQueue(tasks: UploadTask[]): UploadTask[] {
7
+ return tasks.map((task) => ({
8
+ id: task.id,
9
+ recordingId: task.recordingId,
10
+ deviceId: task.deviceId,
11
+ localPath: task.localPath,
12
+ uploadUrl: '',
13
+ status: task.status,
14
+ retryCount: task.retryCount,
15
+ createdAt: new Date(task.createdAt),
16
+ updatedAt: new Date(task.updatedAt),
17
+ ...(task.recordingUuid === undefined ? {} : { recordingUuid: task.recordingUuid }),
18
+ ...(task.recoveryScope === undefined ? {} : { recoveryScope: task.recoveryScope }),
19
+ ...(task.fileSizeBytes === undefined ? {} : { fileSizeBytes: task.fileSizeBytes }),
20
+ ...(task.nextAttemptAt === undefined ? {} : { nextAttemptAt: task.nextAttemptAt }),
21
+ ...(task.contentType === undefined ? {} : { contentType: task.contentType }),
22
+ ...(task.contentSha256 === undefined ? {} : { contentSha256: task.contentSha256 }),
23
+ relayUpload: task.relayUpload ?? !!task.relay,
24
+ }));
25
+ }
26
+
27
+ export function restoreUploadQueue(value: unknown): UploadTask[] {
28
+ if (!Array.isArray(value)) return invalid();
29
+ const ids = new Set<string>();
30
+ const validated = value.map((entry): UploadTask => {
31
+ if (!entry || typeof entry !== 'object') return invalid();
32
+ const task = entry as UploadTask;
33
+ for (const field of ['id', 'recordingId', 'deviceId'] as const) {
34
+ if (typeof task[field] !== 'string' || !task[field]) return invalid();
35
+ }
36
+ if (typeof task.localPath !== 'string' || ids.has(task.id)) return invalid();
37
+ ids.add(task.id);
38
+ if (!['pending', 'uploading', 'failed', 'completed'].includes(task.status)) return invalid();
39
+ if (!Number.isSafeInteger(task.retryCount) || task.retryCount < 0) return invalid();
40
+ for (const field of ['recordingUuid', 'recoveryScope', 'contentType', 'contentSha256'] as const) {
41
+ if (task[field] !== undefined && typeof task[field] !== 'string') return invalid();
42
+ }
43
+ for (const field of ['fileSizeBytes', 'nextAttemptAt'] as const) {
44
+ if (task[field] !== undefined && (!Number.isSafeInteger(task[field]) || task[field]! < 0)) return invalid();
45
+ }
46
+ if (task.relayUpload !== undefined && typeof task.relayUpload !== 'boolean') return invalid();
47
+ const createdAt = new Date(task.createdAt);
48
+ const updatedAt = new Date(task.updatedAt);
49
+ if (!Number.isFinite(createdAt.getTime()) || !Number.isFinite(updatedAt.getTime())) return invalid();
50
+ return { ...task, createdAt, updatedAt, status: task.status === 'uploading' ? 'pending' : task.status };
51
+ });
52
+ return persistedUploadQueue(validated);
53
+ }
54
+
55
+ export function rejectRecordingDataStore(value: unknown): void {
56
+ if (value !== undefined) {
57
+ throw new Error('RecordingDataStore byte callbacks are unsupported; recording files are native-owned');
58
+ }
59
+ }
@@ -199,6 +199,49 @@ export interface DeviceLogEvent {
199
199
  isBacklog: boolean;
200
200
  }
201
201
 
202
+ export type DeviceDiagnosticEventType =
203
+ | 'reset' | 'watchdog' | 'hard_fault' | 'storage'
204
+ | 'power' | 'radio' | 'security';
205
+
206
+ export type DeviceDiagnosticReasonCode =
207
+ | 'watchdog_timeout' | 'cpu_illegal_instruction'
208
+ | 'cpu_misaligned_access' | 'cpu_stack_overflow'
209
+ | 'cpu_usage_fault' | 'memory_protection_fault'
210
+ | 'invalid_register_read' | 'invalid_register_write'
211
+ | 'audio_subsystem_fault' | 'wireless_subsystem_fault'
212
+ | 'flash_mmu_fault' | 'low_voltage_reset'
213
+ | 'unknown_cpu_exception';
214
+
215
+ /** A retained device fault. Reading does not acknowledge or delete it. */
216
+ export interface DeviceDiagnosticEvent {
217
+ event_id: string;
218
+ event_type: DeviceDiagnosticEventType;
219
+ reason_code: DeviceDiagnosticReasonCode;
220
+ uptime_ms: number;
221
+ signature: string;
222
+ firmware_build_id: string;
223
+ subsystem: string;
224
+ state_before_event: string;
225
+ report?: {
226
+ fault: {
227
+ cpu_id: number;
228
+ cpu_emu: string;
229
+ core_emu: string;
230
+ hsb_emu: string;
231
+ audio_emu: string;
232
+ wireless_emu: string;
233
+ };
234
+ execution: { task?: string; reti?: string; rets?: string; pc_trace: string[] };
235
+ runtime?: { heap_free_bytes?: number; task_stack_remaining_bytes?: number };
236
+ breadcrumbs?: Array<{ delta_ms: number; code: string; arg0: number }>;
237
+ };
238
+ }
239
+
240
+ export interface DeviceDiagnosticsBatch {
241
+ schema_version: 1;
242
+ events: DeviceDiagnosticEvent[];
243
+ }
244
+
202
245
  /**
203
246
  * Storage information from device
204
247
  */
@@ -30,6 +30,20 @@ export interface DeviceRecording {
30
30
  * (obtained by calling customer's API, not Bota API directly)
31
31
  */
32
32
  export interface UploadInfo {
33
+ /** Non-secret account/project/environment identity. Must match during recovery. */
34
+ recoveryScope?: string;
35
+ /** Skip sending the object; complete must still acknowledge host durability. */
36
+ alreadyUploaded?: boolean;
37
+ /** Resolve only after the backend durably acknowledges this exact recording. */
38
+ complete?: (context: {
39
+ fileSizeBytes: number;
40
+ contentSha256?: string;
41
+ signal: AbortSignal;
42
+ }) => Promise<void>;
43
+ /** Aborted by the host when this authorization context becomes invalid. */
44
+ signal?: AbortSignal;
45
+ /** Releases host listeners/leases after the attempt, including cancellation. */
46
+ dispose?: () => void;
33
47
  /** Pre-signed S3 URL for upload */
34
48
  uploadUrl: string;
35
49
  /** Recording ID assigned by Bota API (rec_*) */
@@ -55,6 +69,37 @@ export interface UploadInfo {
55
69
  };
56
70
  }
57
71
 
72
+ /** Maintenance compatibility shape only. JS byte stores are unsupported by the
73
+ * native-owned facade; supplying one throws before native configuration. */
74
+ export interface RecordingDataStore {
75
+ saveRecordingData(input: {
76
+ deviceId: string;
77
+ recordingUuid: string;
78
+ data: Uint8Array;
79
+ }): Promise<string>;
80
+ loadRecordingData(localPath: string): Promise<Uint8Array>;
81
+ deleteRecordingData(localPath: string): Promise<void>;
82
+ }
83
+
84
+ /** Metadata only. The host must refresh credentials for this exact identity. */
85
+ export interface UploadRecoveryContext {
86
+ taskId: string;
87
+ recordingId: string;
88
+ deviceId: string;
89
+ recordingUuid: string;
90
+ recoveryScope?: string;
91
+ fileSizeBytes?: number;
92
+ relayUpload: boolean;
93
+ contentType?: string;
94
+ contentSha256?: string;
95
+ signal: AbortSignal;
96
+ }
97
+
98
+ /** Null parks an unavailable account without consuming the retry budget. */
99
+ export type UploadRecoveryProvider = (
100
+ context: UploadRecoveryContext
101
+ ) => Promise<UploadInfo | null>;
102
+
58
103
  /**
59
104
  * Sync progress stages
60
105
  */
@@ -102,6 +147,13 @@ export type UploadTaskStatus = 'pending' | 'uploading' | 'completed' | 'failed';
102
147
  * Upload task in the queue
103
148
  */
104
149
  export interface UploadTask {
150
+ recordingUuid?: string;
151
+ recoveryScope?: string;
152
+ fileSizeBytes?: number;
153
+ relayUpload?: boolean;
154
+ nextAttemptAt?: number;
155
+ /** Volatile compatibility field; never included in queue persistence. */
156
+ complete?: UploadInfo['complete'];
105
157
  /** Unique task identifier */
106
158
  id: string;
107
159
  /** Recording ID from Bota API */
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { DeviceStatus, StorageInfo } from './Device';
6
- import type { SyncProgress, UploadTask } from './Recording';
6
+ import type { RecordingDataStore, SyncProgress, UploadRecoveryProvider, UploadTask } from './Recording';
7
7
 
8
8
  /**
9
9
  * SDK log level
@@ -14,6 +14,10 @@ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
14
14
  * SDK configuration options
15
15
  */
16
16
  export interface BotaConfig {
17
+ /** Refreshes credentials for retained native recordings after restart. */
18
+ uploadRecoveryProvider?: UploadRecoveryProvider;
19
+ /** Unsupported compatibility option: byte callbacks cannot cross the bridge. */
20
+ recordingDataStore?: RecordingDataStore;
17
21
  /** Environment for API endpoint (development, gamma, or production) */
18
22
  environment?: 'development' | 'gamma' | 'production';
19
23
  /** Enable background sync (default: true) */
@@ -0,0 +1,3 @@
1
+ // Generated by cargo xtask client-identity generate. Do not edit.
2
+ export const SDK_PACKAGE = '@bota.dev/react-native-app-sdk'
3
+ export const SDK_VERSION = '2.0.0-beta.11'
@@ -156,6 +156,16 @@ export type NativeEncryptedUploadV2Recording = {
156
156
  generation: number;
157
157
  ciphertextLength: string;
158
158
  ciphertextSha256: string;
159
+ startedAtMs?: string;
160
+ durationMs?: string;
161
+ plaintextLength?: string;
162
+ storageFormat?: number;
163
+ };
164
+
165
+ export type NativePendingRecording = {
166
+ profile: string;
167
+ legacy?: NativeDeviceRecording;
168
+ encrypted?: NativeEncryptedUploadV2Recording;
159
169
  };
160
170
 
161
171
  export type NativeEncryptedUploadV2Capability = {
@@ -223,6 +233,7 @@ export type NativeRecordingTransferResult = {
223
233
  localPath: string;
224
234
  e2eEncrypted: boolean;
225
235
  contentSha256?: string;
236
+ fileSizeBytes: number;
226
237
  };
227
238
 
228
239
  export type NativeRecordingUploadProgress = {
@@ -236,6 +247,7 @@ export type NativeRecordingUploadRequest = {
236
247
  recordingId: string;
237
248
  deviceId: string;
238
249
  localPath: string;
250
+ fileSizeBytes?: number;
239
251
  uploadUrl: string;
240
252
  uploadToken?: string;
241
253
  completeUrl?: string;
@@ -329,6 +341,44 @@ export type NativeDeviceLogLine = {
329
341
  isBacklog: boolean;
330
342
  };
331
343
 
344
+ export type NativeDiagnosticFault = {
345
+ cpu_id: number;
346
+ cpu_emu: string;
347
+ core_emu: string;
348
+ hsb_emu: string;
349
+ audio_emu: string;
350
+ wireless_emu: string;
351
+ };
352
+ export type NativeDiagnosticExecution = {
353
+ task?: string;
354
+ reti?: string;
355
+ rets?: string;
356
+ pc_trace: ReadonlyArray<string>;
357
+ };
358
+ export type NativeDiagnosticRuntime = {
359
+ heap_free_bytes?: number;
360
+ task_stack_remaining_bytes?: number;
361
+ };
362
+ export type NativeDiagnosticBreadcrumb = { delta_ms: number; code: string; arg0: number };
363
+ export type NativeDiagnosticReport = {
364
+ fault: NativeDiagnosticFault;
365
+ execution: NativeDiagnosticExecution;
366
+ runtime?: NativeDiagnosticRuntime;
367
+ breadcrumbs?: ReadonlyArray<NativeDiagnosticBreadcrumb>;
368
+ };
369
+ export type NativeDiagnosticEvent = {
370
+ event_id: string;
371
+ event_type: string;
372
+ reason_code: string;
373
+ uptime_ms: number;
374
+ signature: string;
375
+ firmware_build_id: string;
376
+ subsystem: string;
377
+ state_before_event: string;
378
+ report?: NativeDiagnosticReport;
379
+ };
380
+ export type NativeDiagnosticsBatch = { schema_version: number; events: ReadonlyArray<NativeDiagnosticEvent> };
381
+
332
382
  export type NativeWiFiConfigResult = {
333
383
  success: boolean;
334
384
  error?: string;
@@ -353,7 +403,17 @@ export type NativeDeviceWiFiScanResult = {
353
403
  currentSsid?: string;
354
404
  };
355
405
 
406
+ export type NativeClientContext = {
407
+ schemaVersion: number;
408
+ sessionId: string;
409
+ sequence: number;
410
+ platform: string;
411
+ sdkPackage: string;
412
+ sdkVersion: string;
413
+ };
414
+
356
415
  export interface Spec extends TurboModule {
416
+ nextClientPresence: (deviceId: string) => Promise<NativeClientContext | null>;
357
417
  readonly onDeviceDiscovered: EventEmitter<NativeDiscoveredDevice>;
358
418
  readonly onDeviceDisconnected: EventEmitter<NativeDeviceDisconnection>;
359
419
  readonly onDeviceStatusUpdated: EventEmitter<NativeDeviceStatus>;
@@ -480,6 +540,8 @@ export interface Spec extends TurboModule {
480
540
  startWiFiStatusUpdates: (device: NativeConnectedDevice) => Promise<void>;
481
541
  startDeviceLogs: (device: NativeConnectedDevice) => Promise<void>;
482
542
  stopDeviceLogs: () => Promise<void>;
543
+ readDiagnosticEvents: (device: NativeConnectedDevice) => Promise<NativeDiagnosticsBatch>;
544
+ acknowledgeDiagnosticEvents: (device: NativeConnectedDevice, acceptedEventIds: ReadonlyArray<string>) => Promise<void>;
483
545
  stopStatusUpdates: () => Promise<void>;
484
546
  stopWiFiStatusUpdates: () => Promise<void>;
485
547
  stopScan: () => Promise<void>;
@@ -493,6 +555,9 @@ export interface Spec extends TurboModule {
493
555
  recording: NativeEncryptedUploadV2Recording,
494
556
  operationId: string
495
557
  ) => Promise<void>;
558
+ listPendingRecordings: (device: NativeConnectedDevice) => Promise<ReadonlyArray<NativePendingRecording>>;
559
+ cancelEncryptedRecordingV2: (operationId: string) => Promise<void>;
560
+ releaseEncryptedUploadV2Material: (materialRegistrationId: string) => Promise<void>;
496
561
  resolveEncryptedUploadV2Profile: (
497
562
  requestId: string,
498
563
  decision: NativeEncryptedUploadV2ProfileDecision
@@ -507,6 +572,7 @@ export interface Spec extends TurboModule {
507
572
  ) => Promise<void>;
508
573
  uploadRecordingFile: (request: NativeRecordingUploadRequest) => Promise<void>;
509
574
  cancelRecordingUpload: (taskId: string) => Promise<void>;
575
+ releaseRecordingFile: (taskId: string, localPath: string) => Promise<void>;
510
576
  loadCompatibilityUploadQueue: () => Promise<string>;
511
577
  saveCompatibilityUploadQueue: (serializedTasks: string) => Promise<void>;
512
578
  stopAllRecordingOperations: () => Promise<void>;