@bota.dev/react-native-app-sdk 2.0.0-beta.12 → 2.0.0-beta.14
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 +21 -7
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +2 -2
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +23 -0
- package/generated/codegen-contract.json +2 -2
- package/ios/BotaDeviceSDKAppleRecordingUploads.swift +3 -3
- package/lib/commonjs/client.js +5 -0
- package/lib/commonjs/client.js.map +1 -1
- package/lib/commonjs/managers/NativeUploadQueue.js +22 -8
- package/lib/commonjs/managers/NativeUploadQueue.js.map +1 -1
- package/lib/commonjs/managers/uploadRecoveryMetadata.js +4 -0
- package/lib/commonjs/managers/uploadRecoveryMetadata.js.map +1 -1
- package/lib/commonjs/managers/uploadVerificationPending.js +20 -0
- package/lib/commonjs/managers/uploadVerificationPending.js.map +1 -0
- package/lib/commonjs/sdkIdentity.js +1 -1
- package/lib/commonjs/uploadRecovery.js +50 -7
- package/lib/commonjs/uploadRecovery.js.map +1 -1
- package/lib/module/client.js +5 -0
- package/lib/module/client.js.map +1 -1
- package/lib/module/managers/NativeUploadQueue.js +22 -8
- package/lib/module/managers/NativeUploadQueue.js.map +1 -1
- package/lib/module/managers/uploadRecoveryMetadata.js +4 -0
- package/lib/module/managers/uploadRecoveryMetadata.js.map +1 -1
- package/lib/module/managers/uploadVerificationPending.js +14 -0
- package/lib/module/managers/uploadVerificationPending.js.map +1 -0
- package/lib/module/sdkIdentity.js +1 -1
- package/lib/module/uploadRecovery.js +50 -7
- package/lib/module/uploadRecovery.js.map +1 -1
- package/lib/typescript/client.d.ts.map +1 -1
- package/lib/typescript/managers/NativeUploadQueue.d.ts.map +1 -1
- package/lib/typescript/managers/uploadRecoveryMetadata.d.ts.map +1 -1
- package/lib/typescript/managers/uploadVerificationPending.d.ts +6 -0
- package/lib/typescript/managers/uploadVerificationPending.d.ts.map +1 -0
- package/lib/typescript/models/Recording.d.ts +6 -0
- package/lib/typescript/models/Recording.d.ts.map +1 -1
- package/lib/typescript/sdkIdentity.d.ts +1 -1
- package/lib/typescript/uploadRecovery.d.ts +1 -1
- package/lib/typescript/uploadRecovery.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +7 -0
- package/src/managers/NativeUploadQueue.ts +20 -8
- package/src/managers/uploadRecoveryMetadata.ts +2 -0
- package/src/managers/uploadVerificationPending.ts +10 -0
- package/src/models/Recording.ts +6 -0
- package/src/sdkIdentity.ts +1 -1
- package/src/uploadRecovery.ts +42 -8
package/src/uploadRecovery.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { UploadInfo, UploadRecoveryContext, UploadRecoveryProvider } from './models/Recording';
|
|
2
|
+
import { isUploadVerificationPending, UploadVerificationPendingError } from './managers/uploadVerificationPending';
|
|
3
|
+
|
|
4
|
+
const waitForCompletion = (milliseconds: number, signals: AbortSignal[]): Promise<void> =>
|
|
5
|
+
new Promise((resolve, reject) => {
|
|
6
|
+
const unique = [...new Set(signals)];
|
|
7
|
+
const clean = () => { clearTimeout(timer); unique.forEach(signal => signal.removeEventListener('abort', abort)); };
|
|
8
|
+
const abort = () => { clean(); reject(new Error('Upload recovery cancelled')); };
|
|
9
|
+
const timer = setTimeout(() => { clean(); resolve(); }, milliseconds);
|
|
10
|
+
unique.forEach(signal => signal.addEventListener('abort', abort, { once: true }));
|
|
11
|
+
if (unique.some(signal => signal.aborted)) abort();
|
|
12
|
+
});
|
|
2
13
|
|
|
3
14
|
/** A host-owned, authenticated lease. It must permanently reject changed identities. */
|
|
4
15
|
export interface UploadRecoveryBackend {
|
|
@@ -8,7 +19,7 @@ export interface UploadRecoveryBackend {
|
|
|
8
19
|
dispose(): void;
|
|
9
20
|
getRecording(id: string, signal: AbortSignal): Promise<{
|
|
10
21
|
id: string; status: string; metadata?: Record<string, unknown>;
|
|
11
|
-
content_sha256?: string | null; file_size_bytes?: number | null;
|
|
22
|
+
content_sha256?: string | null; file_size_bytes?: number | string | null;
|
|
12
23
|
}>;
|
|
13
24
|
getUploadInfo(id: string, relay: boolean, contentType: string | undefined, signal: AbortSignal): Promise<Pick<UploadInfo, 'uploadUrl' | 'relay' | 'contentType'>>;
|
|
14
25
|
completeRecording(id: string, data: { duration_seconds?: number; file_size_bytes: number; content_sha256?: string }, signal: AbortSignal): Promise<unknown>;
|
|
@@ -36,26 +47,49 @@ export function createUploadRecoveryProvider(
|
|
|
36
47
|
check();
|
|
37
48
|
const recording = await api.getRecording(task.recordingId, task.signal);
|
|
38
49
|
check();
|
|
50
|
+
const wireSize = recording.file_size_bytes;
|
|
51
|
+
if (typeof wireSize === 'string' && !/^(0|[1-9][0-9]*)$/.test(wireSize)) throw new Error('Invalid upload recovery file size');
|
|
52
|
+
const fileSize = typeof wireSize === 'string' ? Number(wireSize) : wireSize;
|
|
53
|
+
if (fileSize != null && (typeof fileSize !== 'number' || !Number.isSafeInteger(fileSize) || fileSize < 0)) {
|
|
54
|
+
throw new Error('Invalid upload recovery file size');
|
|
55
|
+
}
|
|
39
56
|
if (recording.id !== task.recordingId || (!task.relayUpload && (
|
|
40
57
|
(task.contentSha256 && recording.content_sha256 && task.contentSha256 !== recording.content_sha256) ||
|
|
41
|
-
(task.fileSizeBytes !== undefined &&
|
|
58
|
+
(task.fileSizeBytes !== undefined && fileSize != null && task.fileSizeBytes !== fileSize)
|
|
42
59
|
))) throw new Error('Upload recovery identity or evidence changed');
|
|
43
60
|
if (!['pending', 'uploaded', 'integrity_failure'].includes(recording.status)) throw new Error('Recording unavailable for recovery');
|
|
44
61
|
const start = Date.parse(String(recording.metadata?.started_at ?? ''));
|
|
45
62
|
const end = Date.parse(String(recording.metadata?.ended_at ?? ''));
|
|
46
63
|
const duration = Number.isFinite(start) && Number.isFinite(end) && end >= start ? Math.floor((end - start) / 1000) : undefined;
|
|
47
|
-
const
|
|
64
|
+
const alreadyUploaded = recording.status === 'uploaded' || (task.completionPending === true && recording.status === 'pending');
|
|
65
|
+
const target = alreadyUploaded ? { uploadUrl: '' }
|
|
48
66
|
: await api.getUploadInfo(task.recordingId, task.relayUpload, task.contentType, task.signal);
|
|
49
67
|
check();
|
|
50
68
|
return {
|
|
51
69
|
...target, recordingId: task.recordingId, recoveryScope: task.recoveryScope,
|
|
52
|
-
alreadyUploaded: recording.status === '
|
|
70
|
+
alreadyUploaded, reuploadRequired: recording.status === 'integrity_failure',
|
|
71
|
+
signal: api.signal, dispose: () => api.dispose(),
|
|
53
72
|
complete: async ({ fileSizeBytes, contentSha256, signal }) => {
|
|
54
|
-
|
|
55
|
-
await api.completeRecording(task.recordingId, {
|
|
73
|
+
const data = {
|
|
56
74
|
duration_seconds: duration, file_size_bytes: fileSizeBytes, content_sha256: contentSha256,
|
|
57
|
-
}
|
|
58
|
-
|
|
75
|
+
};
|
|
76
|
+
const deadline = Date.now() + 120_000;
|
|
77
|
+
for (let poll = 0; ; poll++) {
|
|
78
|
+
check(signal);
|
|
79
|
+
if (poll > 0 && (Date.now() >= deadline || poll >= 120)) throw new UploadVerificationPendingError();
|
|
80
|
+
try {
|
|
81
|
+
await api.completeRecording(task.recordingId, data, signal);
|
|
82
|
+
check(signal);
|
|
83
|
+
return;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
check(signal);
|
|
86
|
+
if (!isUploadVerificationPending(error)) throw error;
|
|
87
|
+
const remaining = deadline - Date.now();
|
|
88
|
+
if (remaining <= 0) throw new UploadVerificationPendingError();
|
|
89
|
+
await waitForCompletion(Math.min(1_000 + Math.floor(Math.random() * 1_000), remaining),
|
|
90
|
+
[signal, task.signal, api.signal]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
59
93
|
},
|
|
60
94
|
};
|
|
61
95
|
} catch {
|