@bota-dev/react-native-sdk 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -0
- package/lib/commonjs/BotaClient.js +223 -0
- package/lib/commonjs/BotaClient.js.map +1 -0
- package/lib/commonjs/ble/BleManager.js +494 -0
- package/lib/commonjs/ble/BleManager.js.map +1 -0
- package/lib/commonjs/ble/constants.js +166 -0
- package/lib/commonjs/ble/constants.js.map +1 -0
- package/lib/commonjs/ble/index.js +54 -0
- package/lib/commonjs/ble/index.js.map +1 -0
- package/lib/commonjs/ble/parsers.js +345 -0
- package/lib/commonjs/ble/parsers.js.map +1 -0
- package/lib/commonjs/index.js +81 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/managers/DeviceManager.js +437 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -0
- package/lib/commonjs/managers/OTAManager.js +227 -0
- package/lib/commonjs/managers/OTAManager.js.map +1 -0
- package/lib/commonjs/managers/RecordingManager.js +384 -0
- package/lib/commonjs/managers/RecordingManager.js.map +1 -0
- package/lib/commonjs/managers/index.js +27 -0
- package/lib/commonjs/managers/index.js.map +1 -0
- package/lib/commonjs/models/Device.js +2 -0
- package/lib/commonjs/models/Device.js.map +1 -0
- package/lib/commonjs/models/Recording.js +2 -0
- package/lib/commonjs/models/Recording.js.map +1 -0
- package/lib/commonjs/models/Status.js +6 -0
- package/lib/commonjs/models/Status.js.map +1 -0
- package/lib/commonjs/models/index.js +39 -0
- package/lib/commonjs/models/index.js.map +1 -0
- package/lib/commonjs/protocol/ProtocolHandler.js +343 -0
- package/lib/commonjs/protocol/ProtocolHandler.js.map +1 -0
- package/lib/commonjs/protocol/index.js +13 -0
- package/lib/commonjs/protocol/index.js.map +1 -0
- package/lib/commonjs/storage/StorageManager.js +333 -0
- package/lib/commonjs/storage/StorageManager.js.map +1 -0
- package/lib/commonjs/storage/index.js +19 -0
- package/lib/commonjs/storage/index.js.map +1 -0
- package/lib/commonjs/upload/S3Uploader.js +133 -0
- package/lib/commonjs/upload/S3Uploader.js.map +1 -0
- package/lib/commonjs/upload/UploadQueue.js +280 -0
- package/lib/commonjs/upload/UploadQueue.js.map +1 -0
- package/lib/commonjs/upload/index.js +20 -0
- package/lib/commonjs/upload/index.js.map +1 -0
- package/lib/commonjs/utils/errors.js +187 -0
- package/lib/commonjs/utils/errors.js.map +1 -0
- package/lib/commonjs/utils/index.js +40 -0
- package/lib/commonjs/utils/index.js.map +1 -0
- package/lib/commonjs/utils/logger.js +135 -0
- package/lib/commonjs/utils/logger.js.map +1 -0
- package/lib/commonjs/utils/retry.js +160 -0
- package/lib/commonjs/utils/retry.js.map +1 -0
- package/lib/module/BotaClient.js +216 -0
- package/lib/module/BotaClient.js.map +1 -0
- package/lib/module/ble/BleManager.js +484 -0
- package/lib/module/ble/BleManager.js.map +1 -0
- package/lib/module/ble/constants.js +159 -0
- package/lib/module/ble/constants.js.map +1 -0
- package/lib/module/ble/index.js +8 -0
- package/lib/module/ble/index.js.map +1 -0
- package/lib/module/ble/parsers.js +328 -0
- package/lib/module/ble/parsers.js.map +1 -0
- package/lib/module/index.js +22 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/managers/DeviceManager.js +429 -0
- package/lib/module/managers/DeviceManager.js.map +1 -0
- package/lib/module/managers/OTAManager.js +219 -0
- package/lib/module/managers/OTAManager.js.map +1 -0
- package/lib/module/managers/RecordingManager.js +376 -0
- package/lib/module/managers/RecordingManager.js.map +1 -0
- package/lib/module/managers/index.js +8 -0
- package/lib/module/managers/index.js.map +1 -0
- package/lib/module/models/Device.js +2 -0
- package/lib/module/models/Device.js.map +1 -0
- package/lib/module/models/Recording.js +2 -0
- package/lib/module/models/Recording.js.map +1 -0
- package/lib/module/models/Status.js +2 -0
- package/lib/module/models/Status.js.map +1 -0
- package/lib/module/models/index.js +8 -0
- package/lib/module/models/index.js.map +1 -0
- package/lib/module/protocol/ProtocolHandler.js +336 -0
- package/lib/module/protocol/ProtocolHandler.js.map +1 -0
- package/lib/module/protocol/index.js +6 -0
- package/lib/module/protocol/index.js.map +1 -0
- package/lib/module/storage/StorageManager.js +324 -0
- package/lib/module/storage/StorageManager.js.map +1 -0
- package/lib/module/storage/index.js +6 -0
- package/lib/module/storage/index.js.map +1 -0
- package/lib/module/upload/S3Uploader.js +126 -0
- package/lib/module/upload/S3Uploader.js.map +1 -0
- package/lib/module/upload/UploadQueue.js +272 -0
- package/lib/module/upload/UploadQueue.js.map +1 -0
- package/lib/module/upload/index.js +7 -0
- package/lib/module/upload/index.js.map +1 -0
- package/lib/module/utils/errors.js +173 -0
- package/lib/module/utils/errors.js.map +1 -0
- package/lib/module/utils/index.js +8 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/module/utils/logger.js +129 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/module/utils/retry.js +149 -0
- package/lib/module/utils/retry.js.map +1 -0
- package/lib/typescript/src/BotaClient.d.ts +77 -0
- package/lib/typescript/src/BotaClient.d.ts.map +1 -0
- package/lib/typescript/src/ble/BleManager.d.ts +111 -0
- package/lib/typescript/src/ble/BleManager.d.ts.map +1 -0
- package/lib/typescript/src/ble/constants.d.ts +111 -0
- package/lib/typescript/src/ble/constants.d.ts.map +1 -0
- package/lib/typescript/src/ble/index.d.ts +7 -0
- package/lib/typescript/src/ble/index.d.ts.map +1 -0
- package/lib/typescript/src/ble/parsers.d.ts +100 -0
- package/lib/typescript/src/ble/parsers.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +16 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/managers/DeviceManager.d.ts +84 -0
- package/lib/typescript/src/managers/DeviceManager.d.ts.map +1 -0
- package/lib/typescript/src/managers/OTAManager.d.ts +78 -0
- package/lib/typescript/src/managers/OTAManager.d.ts.map +1 -0
- package/lib/typescript/src/managers/RecordingManager.d.ts +90 -0
- package/lib/typescript/src/managers/RecordingManager.d.ts.map +1 -0
- package/lib/typescript/src/managers/index.d.ts +7 -0
- package/lib/typescript/src/managers/index.d.ts.map +1 -0
- package/lib/typescript/src/models/Device.d.ts +139 -0
- package/lib/typescript/src/models/Device.d.ts.map +1 -0
- package/lib/typescript/src/models/Recording.d.ts +110 -0
- package/lib/typescript/src/models/Recording.d.ts.map +1 -0
- package/lib/typescript/src/models/Status.d.ts +104 -0
- package/lib/typescript/src/models/Status.d.ts.map +1 -0
- package/lib/typescript/src/models/index.d.ts +7 -0
- package/lib/typescript/src/models/index.d.ts.map +1 -0
- package/lib/typescript/src/protocol/ProtocolHandler.d.ts +69 -0
- package/lib/typescript/src/protocol/ProtocolHandler.d.ts.map +1 -0
- package/lib/typescript/src/protocol/index.d.ts +5 -0
- package/lib/typescript/src/protocol/index.d.ts.map +1 -0
- package/lib/typescript/src/storage/StorageManager.d.ts +116 -0
- package/lib/typescript/src/storage/StorageManager.d.ts.map +1 -0
- package/lib/typescript/src/storage/index.d.ts +5 -0
- package/lib/typescript/src/storage/index.d.ts.map +1 -0
- package/lib/typescript/src/upload/S3Uploader.d.ts +38 -0
- package/lib/typescript/src/upload/S3Uploader.d.ts.map +1 -0
- package/lib/typescript/src/upload/UploadQueue.d.ts +95 -0
- package/lib/typescript/src/upload/UploadQueue.d.ts.map +1 -0
- package/lib/typescript/src/upload/index.d.ts +6 -0
- package/lib/typescript/src/upload/index.d.ts.map +1 -0
- package/lib/typescript/src/utils/errors.d.ts +82 -0
- package/lib/typescript/src/utils/errors.d.ts.map +1 -0
- package/lib/typescript/src/utils/index.d.ts +7 -0
- package/lib/typescript/src/utils/index.d.ts.map +1 -0
- package/lib/typescript/src/utils/logger.d.ts +68 -0
- package/lib/typescript/src/utils/logger.d.ts.map +1 -0
- package/lib/typescript/src/utils/retry.d.ts +53 -0
- package/lib/typescript/src/utils/retry.d.ts.map +1 -0
- package/package.json +95 -0
- package/src/BotaClient.ts +238 -0
- package/src/ble/BleManager.ts +573 -0
- package/src/ble/constants.ts +158 -0
- package/src/ble/index.ts +7 -0
- package/src/ble/parsers.ts +395 -0
- package/src/index.ts +64 -0
- package/src/managers/DeviceManager.ts +545 -0
- package/src/managers/OTAManager.ts +263 -0
- package/src/managers/RecordingManager.ts +434 -0
- package/src/managers/index.ts +12 -0
- package/src/models/Device.ts +164 -0
- package/src/models/Recording.ts +123 -0
- package/src/models/Status.ts +126 -0
- package/src/models/index.ts +7 -0
- package/src/protocol/ProtocolHandler.ts +459 -0
- package/src/protocol/index.ts +5 -0
- package/src/storage/StorageManager.ts +343 -0
- package/src/storage/index.ts +5 -0
- package/src/upload/S3Uploader.ts +164 -0
- package/src/upload/UploadQueue.ts +310 -0
- package/src/upload/index.ts +6 -0
- package/src/utils/errors.ts +310 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/logger.ts +137 -0
- package/src/utils/retry.ts +177 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Manager - Local persistence for recordings and upload queue
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
6
|
+
import { Buffer } from 'buffer';
|
|
7
|
+
|
|
8
|
+
import type { UploadTask, UploadTaskStatus } from '../models/Recording';
|
|
9
|
+
import { logger } from '../utils/logger';
|
|
10
|
+
|
|
11
|
+
const log = logger.tag('StorageManager');
|
|
12
|
+
|
|
13
|
+
// Storage keys
|
|
14
|
+
const STORAGE_PREFIX = '@bota_sdk:';
|
|
15
|
+
const UPLOAD_QUEUE_KEY = `${STORAGE_PREFIX}upload_queue`;
|
|
16
|
+
const SDK_STATE_KEY = `${STORAGE_PREFIX}sdk_state`;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SDK persistent state
|
|
20
|
+
*/
|
|
21
|
+
interface SdkState {
|
|
22
|
+
lastSyncTimes: Record<string, number>; // deviceId -> timestamp
|
|
23
|
+
deviceInfo: Record<string, { serialNumber: string; firmwareVersion: string }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Storage Manager class
|
|
28
|
+
*/
|
|
29
|
+
export class StorageManager {
|
|
30
|
+
private uploadQueue: UploadTask[] = [];
|
|
31
|
+
private sdkState: SdkState = {
|
|
32
|
+
lastSyncTimes: {},
|
|
33
|
+
deviceInfo: {},
|
|
34
|
+
};
|
|
35
|
+
private isInitialized = false;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize storage manager
|
|
39
|
+
*/
|
|
40
|
+
async initialize(): Promise<void> {
|
|
41
|
+
if (this.isInitialized) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
log.debug('Initializing StorageManager');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Load upload queue
|
|
49
|
+
const queueData = await AsyncStorage.getItem(UPLOAD_QUEUE_KEY);
|
|
50
|
+
if (queueData) {
|
|
51
|
+
this.uploadQueue = JSON.parse(queueData);
|
|
52
|
+
// Restore dates
|
|
53
|
+
this.uploadQueue = this.uploadQueue.map((task) => ({
|
|
54
|
+
...task,
|
|
55
|
+
createdAt: new Date(task.createdAt),
|
|
56
|
+
updatedAt: new Date(task.updatedAt),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Load SDK state
|
|
61
|
+
const stateData = await AsyncStorage.getItem(SDK_STATE_KEY);
|
|
62
|
+
if (stateData) {
|
|
63
|
+
this.sdkState = JSON.parse(stateData);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.isInitialized = true;
|
|
67
|
+
log.info('StorageManager initialized', {
|
|
68
|
+
pendingUploads: this.uploadQueue.length,
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
log.error('Failed to initialize storage', error as Error);
|
|
72
|
+
// Continue with empty state
|
|
73
|
+
this.isInitialized = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Upload Queue Methods
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get all upload tasks
|
|
81
|
+
*/
|
|
82
|
+
getUploadQueue(): UploadTask[] {
|
|
83
|
+
return [...this.uploadQueue];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get pending upload tasks
|
|
88
|
+
*/
|
|
89
|
+
getPendingUploads(): UploadTask[] {
|
|
90
|
+
return this.uploadQueue.filter(
|
|
91
|
+
(task) => task.status === 'pending' || task.status === 'uploading'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get failed upload tasks
|
|
97
|
+
*/
|
|
98
|
+
getFailedUploads(): UploadTask[] {
|
|
99
|
+
return this.uploadQueue.filter((task) => task.status === 'failed');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Add a task to the upload queue
|
|
104
|
+
*/
|
|
105
|
+
async addUploadTask(task: UploadTask): Promise<void> {
|
|
106
|
+
log.debug('Adding upload task', { taskId: task.id, recordingId: task.recordingId });
|
|
107
|
+
|
|
108
|
+
this.uploadQueue.push(task);
|
|
109
|
+
await this.saveUploadQueue();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Update an upload task
|
|
114
|
+
*/
|
|
115
|
+
async updateUploadTask(
|
|
116
|
+
taskId: string,
|
|
117
|
+
updates: Partial<UploadTask>
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
const index = this.uploadQueue.findIndex((t) => t.id === taskId);
|
|
120
|
+
if (index === -1) {
|
|
121
|
+
log.warn('Upload task not found', { taskId });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.uploadQueue[index] = {
|
|
126
|
+
...this.uploadQueue[index],
|
|
127
|
+
...updates,
|
|
128
|
+
updatedAt: new Date(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
await this.saveUploadQueue();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Update task status
|
|
136
|
+
*/
|
|
137
|
+
async updateTaskStatus(
|
|
138
|
+
taskId: string,
|
|
139
|
+
status: UploadTaskStatus,
|
|
140
|
+
errorMessage?: string
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
await this.updateUploadTask(taskId, { status, errorMessage });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Increment retry count for a task
|
|
147
|
+
*/
|
|
148
|
+
async incrementRetryCount(taskId: string): Promise<void> {
|
|
149
|
+
const task = this.uploadQueue.find((t) => t.id === taskId);
|
|
150
|
+
if (task) {
|
|
151
|
+
await this.updateUploadTask(taskId, { retryCount: task.retryCount + 1 });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Remove a task from the queue
|
|
157
|
+
*/
|
|
158
|
+
async removeUploadTask(taskId: string): Promise<void> {
|
|
159
|
+
log.debug('Removing upload task', { taskId });
|
|
160
|
+
|
|
161
|
+
this.uploadQueue = this.uploadQueue.filter((t) => t.id !== taskId);
|
|
162
|
+
await this.saveUploadQueue();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Clear all completed tasks
|
|
167
|
+
*/
|
|
168
|
+
async clearCompletedTasks(): Promise<void> {
|
|
169
|
+
this.uploadQueue = this.uploadQueue.filter(
|
|
170
|
+
(t) => t.status !== 'completed'
|
|
171
|
+
);
|
|
172
|
+
await this.saveUploadQueue();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Clear all tasks
|
|
177
|
+
*/
|
|
178
|
+
async clearAllTasks(): Promise<void> {
|
|
179
|
+
this.uploadQueue = [];
|
|
180
|
+
await this.saveUploadQueue();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get a specific upload task
|
|
185
|
+
*/
|
|
186
|
+
getUploadTask(taskId: string): UploadTask | undefined {
|
|
187
|
+
return this.uploadQueue.find((t) => t.id === taskId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Save upload queue to storage
|
|
192
|
+
*/
|
|
193
|
+
private async saveUploadQueue(): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
await AsyncStorage.setItem(
|
|
196
|
+
UPLOAD_QUEUE_KEY,
|
|
197
|
+
JSON.stringify(this.uploadQueue)
|
|
198
|
+
);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
log.error('Failed to save upload queue', error as Error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// SDK State Methods
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get last sync time for a device
|
|
208
|
+
*/
|
|
209
|
+
getLastSyncTime(deviceId: string): Date | null {
|
|
210
|
+
const timestamp = this.sdkState.lastSyncTimes[deviceId];
|
|
211
|
+
return timestamp ? new Date(timestamp) : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Set last sync time for a device
|
|
216
|
+
*/
|
|
217
|
+
async setLastSyncTime(deviceId: string, time: Date = new Date()): Promise<void> {
|
|
218
|
+
this.sdkState.lastSyncTimes[deviceId] = time.getTime();
|
|
219
|
+
await this.saveSdkState();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get cached device info
|
|
224
|
+
*/
|
|
225
|
+
getDeviceInfo(
|
|
226
|
+
deviceId: string
|
|
227
|
+
): { serialNumber: string; firmwareVersion: string } | undefined {
|
|
228
|
+
return this.sdkState.deviceInfo[deviceId];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Cache device info
|
|
233
|
+
*/
|
|
234
|
+
async setDeviceInfo(
|
|
235
|
+
deviceId: string,
|
|
236
|
+
info: { serialNumber: string; firmwareVersion: string }
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
this.sdkState.deviceInfo[deviceId] = info;
|
|
239
|
+
await this.saveSdkState();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Save SDK state to storage
|
|
244
|
+
*/
|
|
245
|
+
private async saveSdkState(): Promise<void> {
|
|
246
|
+
try {
|
|
247
|
+
await AsyncStorage.setItem(SDK_STATE_KEY, JSON.stringify(this.sdkState));
|
|
248
|
+
} catch (error) {
|
|
249
|
+
log.error('Failed to save SDK state', error as Error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Recording File Methods
|
|
254
|
+
// Note: For actual file storage, we'd need react-native-fs or similar
|
|
255
|
+
// For now, we'll store base64 encoded audio in AsyncStorage for small files
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Save recording data locally
|
|
259
|
+
*/
|
|
260
|
+
async saveRecordingData(
|
|
261
|
+
deviceId: string,
|
|
262
|
+
recordingUuid: string,
|
|
263
|
+
data: Buffer
|
|
264
|
+
): Promise<string> {
|
|
265
|
+
const key = `${STORAGE_PREFIX}recording:${deviceId}:${recordingUuid}`;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// Store as base64 (not ideal for large files, but works for demo)
|
|
269
|
+
await AsyncStorage.setItem(key, data.toString('base64'));
|
|
270
|
+
log.debug('Saved recording data', {
|
|
271
|
+
deviceId,
|
|
272
|
+
recordingUuid,
|
|
273
|
+
size: data.length,
|
|
274
|
+
});
|
|
275
|
+
return key;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
log.error('Failed to save recording data', error as Error);
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Load recording data
|
|
284
|
+
*/
|
|
285
|
+
async loadRecordingData(localPath: string): Promise<Buffer> {
|
|
286
|
+
try {
|
|
287
|
+
const data = await AsyncStorage.getItem(localPath);
|
|
288
|
+
if (!data) {
|
|
289
|
+
throw new Error(`Recording not found: ${localPath}`);
|
|
290
|
+
}
|
|
291
|
+
return Buffer.from(data, 'base64');
|
|
292
|
+
} catch (error) {
|
|
293
|
+
log.error('Failed to load recording data', error as Error);
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Delete recording data
|
|
300
|
+
*/
|
|
301
|
+
async deleteRecordingData(localPath: string): Promise<void> {
|
|
302
|
+
try {
|
|
303
|
+
await AsyncStorage.removeItem(localPath);
|
|
304
|
+
log.debug('Deleted recording data', { localPath });
|
|
305
|
+
} catch (error) {
|
|
306
|
+
log.error('Failed to delete recording data', error as Error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clear all SDK storage
|
|
312
|
+
*/
|
|
313
|
+
async clearAll(): Promise<void> {
|
|
314
|
+
log.info('Clearing all SDK storage');
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const keys = await AsyncStorage.getAllKeys();
|
|
318
|
+
const botaKeys = keys.filter((k) => k.startsWith(STORAGE_PREFIX));
|
|
319
|
+
await AsyncStorage.multiRemove(botaKeys);
|
|
320
|
+
|
|
321
|
+
this.uploadQueue = [];
|
|
322
|
+
this.sdkState = { lastSyncTimes: {}, deviceInfo: {} };
|
|
323
|
+
} catch (error) {
|
|
324
|
+
log.error('Failed to clear storage', error as Error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Clean up resources
|
|
330
|
+
*/
|
|
331
|
+
destroy(): void {
|
|
332
|
+
this.uploadQueue = [];
|
|
333
|
+
this.sdkState = { lastSyncTimes: {}, deviceInfo: {} };
|
|
334
|
+
this.isInitialized = false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Generate a unique task ID
|
|
340
|
+
*/
|
|
341
|
+
export function generateTaskId(): string {
|
|
342
|
+
return `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
343
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 Uploader - Handles uploading audio files to S3 using pre-signed URLs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Buffer } from 'buffer';
|
|
6
|
+
|
|
7
|
+
import { UploadError } from '../utils/errors';
|
|
8
|
+
import { logger } from '../utils/logger';
|
|
9
|
+
|
|
10
|
+
const log = logger.tag('S3Uploader');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Upload progress callback
|
|
14
|
+
*/
|
|
15
|
+
export type UploadProgressCallback = (progress: number) => void;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Upload options
|
|
19
|
+
*/
|
|
20
|
+
export interface UploadOptions {
|
|
21
|
+
/** Content type (default: audio/opus) */
|
|
22
|
+
contentType?: string;
|
|
23
|
+
/** Progress callback */
|
|
24
|
+
onProgress?: UploadProgressCallback;
|
|
25
|
+
/** Abort signal for cancellation */
|
|
26
|
+
abortSignal?: AbortSignal;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* S3 Uploader class
|
|
31
|
+
*/
|
|
32
|
+
export class S3Uploader {
|
|
33
|
+
/**
|
|
34
|
+
* Upload a file to S3 using a pre-signed URL
|
|
35
|
+
*/
|
|
36
|
+
async upload(
|
|
37
|
+
data: Buffer,
|
|
38
|
+
uploadUrl: string,
|
|
39
|
+
options: UploadOptions = {}
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const {
|
|
42
|
+
contentType = 'audio/opus',
|
|
43
|
+
onProgress,
|
|
44
|
+
abortSignal,
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
log.info('Starting S3 upload', {
|
|
48
|
+
size: data.length,
|
|
49
|
+
contentType,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// For React Native, we use fetch with the PUT method
|
|
54
|
+
const response = await fetch(uploadUrl, {
|
|
55
|
+
method: 'PUT',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': contentType,
|
|
58
|
+
'Content-Length': data.length.toString(),
|
|
59
|
+
},
|
|
60
|
+
body: data,
|
|
61
|
+
signal: abortSignal,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
66
|
+
log.error('S3 upload failed', undefined, {
|
|
67
|
+
status: response.status,
|
|
68
|
+
statusText: response.statusText,
|
|
69
|
+
error: errorText,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (response.status === 403) {
|
|
73
|
+
throw UploadError.urlExpired('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new UploadError(
|
|
77
|
+
`S3 upload failed: ${response.status} ${response.statusText}`,
|
|
78
|
+
'S3_UPLOAD_FAILED'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Report completion
|
|
83
|
+
onProgress?.(1.0);
|
|
84
|
+
|
|
85
|
+
log.info('S3 upload completed', { size: data.length });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof UploadError) {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const err = error as Error;
|
|
92
|
+
|
|
93
|
+
// Check for abort
|
|
94
|
+
if (err.name === 'AbortError') {
|
|
95
|
+
throw new UploadError('Upload was cancelled', 'UPLOAD_CANCELLED');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check for network errors
|
|
99
|
+
if (err.message?.includes('Network request failed')) {
|
|
100
|
+
throw UploadError.networkUnavailable();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw new UploadError(
|
|
104
|
+
`Upload failed: ${err.message}`,
|
|
105
|
+
'UPLOAD_FAILED',
|
|
106
|
+
undefined,
|
|
107
|
+
err
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Upload a file in chunks (for larger files)
|
|
114
|
+
* Note: This requires multipart upload support from the pre-signed URL
|
|
115
|
+
*/
|
|
116
|
+
async uploadChunked(
|
|
117
|
+
data: Buffer,
|
|
118
|
+
uploadUrl: string,
|
|
119
|
+
options: UploadOptions = {}
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
// For now, delegate to simple upload
|
|
122
|
+
// In production, you'd implement S3 multipart upload here
|
|
123
|
+
return this.upload(data, uploadUrl, options);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Notify the backend that upload is complete
|
|
128
|
+
*/
|
|
129
|
+
async notifyCompletion(
|
|
130
|
+
completeUrl: string,
|
|
131
|
+
recordingId: string,
|
|
132
|
+
uploadToken: string
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
log.debug('Notifying upload completion', { recordingId });
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(completeUrl, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
'Content-Type': 'application/json',
|
|
141
|
+
Authorization: `Bearer ${uploadToken}`,
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({ recording_id: recordingId }),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
148
|
+
throw new UploadError(
|
|
149
|
+
`Completion notification failed: ${response.status} - ${errorText}`,
|
|
150
|
+
'COMPLETION_FAILED'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
log.info('Upload completion notified', { recordingId });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof UploadError) {
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const err = error as Error;
|
|
161
|
+
throw UploadError.completionFailed('', err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|