@bota.dev/web-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.
- package/LICENSE +21 -0
- package/README.md +517 -0
- package/dist/capabilities.d.ts +8 -0
- package/dist/capabilities.js +21 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +153 -0
- package/dist/controlManager.d.ts +38 -0
- package/dist/controlManager.js +344 -0
- package/dist/core.d.ts +698 -0
- package/dist/core.js +13 -0
- package/dist/deviceManager.d.ts +52 -0
- package/dist/deviceManager.js +491 -0
- package/dist/encryptedUploadV2Host.d.ts +239 -0
- package/dist/encryptedUploadV2Host.js +2136 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.js +230 -0
- package/dist/gatt.d.ts +39 -0
- package/dist/gatt.js +57 -0
- package/dist/generated/bota_device_sdk_core.d.ts +202 -0
- package/dist/generated/bota_device_sdk_core.js +1025 -0
- package/dist/generated/bota_device_sdk_core_bg.wasm +0 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2 -0
- package/dist/indexedDbWorkflowStore.d.ts +52 -0
- package/dist/indexedDbWorkflowStore.js +763 -0
- package/dist/logManager.d.ts +24 -0
- package/dist/logManager.js +205 -0
- package/dist/models.d.ts +217 -0
- package/dist/models.js +1 -0
- package/dist/opfsBlobStore.d.ts +12 -0
- package/dist/opfsBlobStore.js +250 -0
- package/dist/otaManager.d.ts +49 -0
- package/dist/otaManager.js +951 -0
- package/dist/providerCancellation.d.ts +2 -0
- package/dist/providerCancellation.js +23 -0
- package/dist/providers.d.ts +138 -0
- package/dist/providers.js +1 -0
- package/dist/provisioningManager.d.ts +48 -0
- package/dist/provisioningManager.js +753 -0
- package/dist/recordingManager.d.ts +53 -0
- package/dist/recordingManager.js +1397 -0
- package/dist/storage.d.ts +103 -0
- package/dist/storage.js +60 -0
- package/dist/transport.d.ts +33 -0
- package/dist/transport.js +8 -0
- package/dist/wasmCore.d.ts +3 -0
- package/dist/wasmCore.js +1651 -0
- package/dist/webBluetoothTransport.d.ts +23 -0
- package/dist/webBluetoothTransport.js +369 -0
- package/dist/wifiManager.d.ts +54 -0
- package/dist/wifiManager.js +528 -0
- package/dist/workflowRuntime.d.ts +115 -0
- package/dist/workflowRuntime.js +1508 -0
- package/package.json +46 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { ControlManager } from "./controlManager.js";
|
|
2
|
+
import { destroyDeviceManagerAfter, DeviceManager, } from "./deviceManager.js";
|
|
3
|
+
import { BotaSDKError } from "./errors.js";
|
|
4
|
+
import { LogManager } from "./logManager.js";
|
|
5
|
+
import { OTAManager } from "./otaManager.js";
|
|
6
|
+
import { ProvisioningManager } from "./provisioningManager.js";
|
|
7
|
+
import { validateStorageNamespace } from "./indexedDbWorkflowStore.js";
|
|
8
|
+
import { RecordingManager } from "./recordingManager.js";
|
|
9
|
+
import { BrowserStorageError, createDefaultBrowserStorage, } from "./storage.js";
|
|
10
|
+
import { loadDefaultCore } from "./wasmCore.js";
|
|
11
|
+
import { WebBluetoothTransport } from "./webBluetoothTransport.js";
|
|
12
|
+
import { WiFiManager } from "./wifiManager.js";
|
|
13
|
+
import { BrowserWorkflowRuntime } from "./workflowRuntime.js";
|
|
14
|
+
export class BotaDeviceClient {
|
|
15
|
+
devices;
|
|
16
|
+
controls;
|
|
17
|
+
provisioning;
|
|
18
|
+
ota;
|
|
19
|
+
logs;
|
|
20
|
+
recordings;
|
|
21
|
+
wifi;
|
|
22
|
+
runtime;
|
|
23
|
+
storage;
|
|
24
|
+
destroyPromise = null;
|
|
25
|
+
constructor(runtime, storage, devices, controls, provisioning, ota, logs, recordings, wifi) {
|
|
26
|
+
this.runtime = runtime;
|
|
27
|
+
this.storage = storage;
|
|
28
|
+
this.devices = devices;
|
|
29
|
+
this.controls = controls;
|
|
30
|
+
this.provisioning = provisioning;
|
|
31
|
+
this.ota = ota;
|
|
32
|
+
this.logs = logs;
|
|
33
|
+
this.recordings = recordings;
|
|
34
|
+
this.wifi = wifi;
|
|
35
|
+
}
|
|
36
|
+
static async create(options = {}) {
|
|
37
|
+
const storage = await resolveStorage(options);
|
|
38
|
+
const core = await (options.coreLoader ?? loadDefaultCore)();
|
|
39
|
+
const transport = options.transport ?? new WebBluetoothTransport();
|
|
40
|
+
const runtime = new BrowserWorkflowRuntime(core, transport);
|
|
41
|
+
const devices = new DeviceManager(core, transport, {
|
|
42
|
+
runtime,
|
|
43
|
+
storage,
|
|
44
|
+
});
|
|
45
|
+
return new BotaDeviceClient(runtime, storage, devices, new ControlManager({
|
|
46
|
+
core,
|
|
47
|
+
transport,
|
|
48
|
+
runtime,
|
|
49
|
+
devices,
|
|
50
|
+
provider: options.providers?.recordingControl ?? null,
|
|
51
|
+
}), new ProvisioningManager({
|
|
52
|
+
core,
|
|
53
|
+
transport,
|
|
54
|
+
runtime,
|
|
55
|
+
devices,
|
|
56
|
+
storage,
|
|
57
|
+
provider: options.providers?.provisioning ?? null,
|
|
58
|
+
}), new OTAManager({
|
|
59
|
+
core,
|
|
60
|
+
transport,
|
|
61
|
+
runtime,
|
|
62
|
+
devices,
|
|
63
|
+
storage,
|
|
64
|
+
provider: options.providers?.firmwareDownload ?? null,
|
|
65
|
+
}), new LogManager({ core, runtime, devices }), new RecordingManager(core, transport, runtime, devices, storage, options.providers?.recordingUpload ?? null), new WiFiManager({
|
|
66
|
+
core,
|
|
67
|
+
transport,
|
|
68
|
+
runtime,
|
|
69
|
+
devices,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
async clearPersistedData() {
|
|
73
|
+
const storage = this.storage;
|
|
74
|
+
if (!storage)
|
|
75
|
+
return;
|
|
76
|
+
if (this.destroyPromise) {
|
|
77
|
+
await this.destroyPromise;
|
|
78
|
+
await clearStorage(storage);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await this.runtime.runExclusive('clear_persisted_data', async () => {
|
|
83
|
+
await clearStorage(storage);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof BotaSDKError)
|
|
88
|
+
throw error;
|
|
89
|
+
throw storageError(error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
destroy() {
|
|
93
|
+
if (this.destroyPromise)
|
|
94
|
+
return this.destroyPromise;
|
|
95
|
+
const managerCleanup = joinManagerCleanup([
|
|
96
|
+
this.logs.destroy(),
|
|
97
|
+
this.ota.destroy(),
|
|
98
|
+
this.wifi.destroy(),
|
|
99
|
+
this.controls.destroy(),
|
|
100
|
+
this.provisioning.destroy(),
|
|
101
|
+
this.recordings.destroy(),
|
|
102
|
+
]);
|
|
103
|
+
this.destroyPromise = destroyDeviceManagerAfter(this.devices, managerCleanup);
|
|
104
|
+
return this.destroyPromise;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function joinManagerCleanup(cleanups) {
|
|
108
|
+
const results = await Promise.allSettled(cleanups);
|
|
109
|
+
const failure = results.find((result) => result.status === 'rejected');
|
|
110
|
+
if (failure)
|
|
111
|
+
throw failure.reason;
|
|
112
|
+
}
|
|
113
|
+
async function resolveStorage(options) {
|
|
114
|
+
if (options.storageNamespace !== undefined) {
|
|
115
|
+
try {
|
|
116
|
+
validateStorageNamespace(options.storageNamespace);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw storageError(error, 'initialize');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (options.storage) {
|
|
123
|
+
if (options.storageNamespace === undefined
|
|
124
|
+
|| options.storage.namespace !== options.storageNamespace) {
|
|
125
|
+
throw new BotaSDKError('invalid_input', 'initialize');
|
|
126
|
+
}
|
|
127
|
+
return options.storage;
|
|
128
|
+
}
|
|
129
|
+
if (options.storageNamespace === undefined)
|
|
130
|
+
return null;
|
|
131
|
+
try {
|
|
132
|
+
return await createDefaultBrowserStorage(options.storageNamespace);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw storageError(error, 'initialize');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function clearStorage(storage) {
|
|
139
|
+
try {
|
|
140
|
+
await storage.clear();
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw storageError(error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function storageError(error, operation = 'clear_persisted_data') {
|
|
147
|
+
if (error instanceof BotaSDKError)
|
|
148
|
+
return error;
|
|
149
|
+
if (error instanceof BrowserStorageError) {
|
|
150
|
+
return new BotaSDKError(error.code, operation);
|
|
151
|
+
}
|
|
152
|
+
return new BotaSDKError('storage_unavailable', operation);
|
|
153
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CoreBridge } from './core.ts';
|
|
2
|
+
import { DeviceManager } from './deviceManager.ts';
|
|
3
|
+
import type { RecordingControlRequest, RecordingControlResult } from './models.ts';
|
|
4
|
+
import type { RecordingControlProvider } from './providers.ts';
|
|
5
|
+
import { type BrowserBluetoothTransport } from './transport.ts';
|
|
6
|
+
import { BrowserWorkflowRuntime } from './workflowRuntime.ts';
|
|
7
|
+
interface ControlManagerOptions {
|
|
8
|
+
core: CoreBridge;
|
|
9
|
+
transport: BrowserBluetoothTransport;
|
|
10
|
+
runtime: BrowserWorkflowRuntime;
|
|
11
|
+
devices: DeviceManager;
|
|
12
|
+
provider?: RecordingControlProvider | null | undefined;
|
|
13
|
+
resultTimeoutMs?: number | undefined;
|
|
14
|
+
delay?: ((milliseconds: number) => Promise<void>) | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare class ControlManager {
|
|
17
|
+
private readonly core;
|
|
18
|
+
private readonly transport;
|
|
19
|
+
private readonly runtime;
|
|
20
|
+
private readonly devices;
|
|
21
|
+
private readonly provider;
|
|
22
|
+
private readonly resultTimeoutMs;
|
|
23
|
+
private readonly delay;
|
|
24
|
+
private activeControl;
|
|
25
|
+
private destroyed;
|
|
26
|
+
private destroyPromise;
|
|
27
|
+
constructor(options: ControlManagerOptions);
|
|
28
|
+
startRecording(request: RecordingControlRequest): Promise<RecordingControlResult>;
|
|
29
|
+
stopRecording(request: RecordingControlRequest): Promise<RecordingControlResult>;
|
|
30
|
+
destroy(): Promise<void>;
|
|
31
|
+
private recordingControl;
|
|
32
|
+
private executeControl;
|
|
33
|
+
private controlDelay;
|
|
34
|
+
private runManaged;
|
|
35
|
+
private verifyConnectedDevice;
|
|
36
|
+
private validateRequest;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { BotaSDKError, normalizeCoreError, } from "./errors.js";
|
|
2
|
+
import { BOTA_CONTROL_SERVICE, DEVICE_COMMAND_CHARACTERISTIC, DEVICE_INFORMATION_SERVICE, RECORDING_CONTROL_CHARACTERISTIC, RECORDING_STATUS_CHARACTERISTIC, SERIAL_NUMBER_CHARACTERISTIC, canonicalGattUuid, } from "./gatt.js";
|
|
3
|
+
import { awaitProviderCall } from "./providerCancellation.js";
|
|
4
|
+
import { BrowserTransportError, } from "./transport.js";
|
|
5
|
+
import { withSubscription } from "./wifiManager.js";
|
|
6
|
+
const DEFAULT_RESULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
const STOP_SEQUENCE_DELAY_MS = 50;
|
|
8
|
+
export class ControlManager {
|
|
9
|
+
core;
|
|
10
|
+
transport;
|
|
11
|
+
runtime;
|
|
12
|
+
devices;
|
|
13
|
+
provider;
|
|
14
|
+
resultTimeoutMs;
|
|
15
|
+
delay;
|
|
16
|
+
activeControl = null;
|
|
17
|
+
destroyed = false;
|
|
18
|
+
destroyPromise = null;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.core = options.core;
|
|
21
|
+
this.transport = options.transport;
|
|
22
|
+
this.runtime = options.runtime;
|
|
23
|
+
this.devices = options.devices;
|
|
24
|
+
this.provider = options.provider ?? null;
|
|
25
|
+
this.resultTimeoutMs = options.resultTimeoutMs ?? DEFAULT_RESULT_TIMEOUT_MS;
|
|
26
|
+
this.delay = options.delay ?? delay;
|
|
27
|
+
}
|
|
28
|
+
startRecording(request) {
|
|
29
|
+
return this.recordingControl('start', request);
|
|
30
|
+
}
|
|
31
|
+
stopRecording(request) {
|
|
32
|
+
return this.recordingControl('stop', request);
|
|
33
|
+
}
|
|
34
|
+
destroy() {
|
|
35
|
+
if (this.destroyPromise)
|
|
36
|
+
return this.destroyPromise;
|
|
37
|
+
this.destroyed = true;
|
|
38
|
+
const active = this.activeControl;
|
|
39
|
+
active?.controller.abort();
|
|
40
|
+
this.destroyPromise = active?.settled ?? Promise.resolve();
|
|
41
|
+
return this.destroyPromise;
|
|
42
|
+
}
|
|
43
|
+
async recordingControl(action, request) {
|
|
44
|
+
const operationId = this.validateRequest(request);
|
|
45
|
+
if (!this.provider) {
|
|
46
|
+
throw new BotaSDKError('unsupported_capability', 'recording_control');
|
|
47
|
+
}
|
|
48
|
+
let command;
|
|
49
|
+
try {
|
|
50
|
+
command = this.core.encodeRecordingControlCommand(action);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw managerError(error, 'recording_control');
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return await this.runManaged(request.signal, async (signal) => {
|
|
57
|
+
const { device, serialNumber } = await this.verifyConnectedDevice(signal);
|
|
58
|
+
let grant = null;
|
|
59
|
+
try {
|
|
60
|
+
let prepared;
|
|
61
|
+
const pending = this.provider.prepare({
|
|
62
|
+
operationId,
|
|
63
|
+
serialNumber,
|
|
64
|
+
action,
|
|
65
|
+
authorityId: request.authorityId,
|
|
66
|
+
signal,
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
prepared = await awaitProviderCall(pending, signal, 'recording_control');
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (signal.aborted) {
|
|
73
|
+
void pending.then(scrubLateGrant).catch(() => undefined);
|
|
74
|
+
}
|
|
75
|
+
if (error instanceof BotaSDKError
|
|
76
|
+
&& (error.code === 'authorization_expired'
|
|
77
|
+
|| error.code === 'cancelled'))
|
|
78
|
+
throw error;
|
|
79
|
+
throw new BotaSDKError('internal_error', 'recording_control');
|
|
80
|
+
}
|
|
81
|
+
if (!(prepared.grant instanceof Uint8Array)
|
|
82
|
+
|| prepared.grant.byteLength === 0) {
|
|
83
|
+
throw new BotaSDKError('internal_error', 'recording_control');
|
|
84
|
+
}
|
|
85
|
+
grant = prepared.grant;
|
|
86
|
+
throwIfAborted(signal);
|
|
87
|
+
return await this.executeControl(action, device, grant, command, signal);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
grant?.fill(0);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const normalized = managerError(error, 'recording_control');
|
|
96
|
+
if (normalized.code === 'identity_mismatch') {
|
|
97
|
+
await this.devices.disconnect().catch(() => undefined);
|
|
98
|
+
}
|
|
99
|
+
throw normalized;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
command.fill(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async executeControl(action, device, grant, command, signal) {
|
|
106
|
+
const result = deferred();
|
|
107
|
+
let resultWindowOpen = false;
|
|
108
|
+
let resultReceived = false;
|
|
109
|
+
return await withSubscription({
|
|
110
|
+
runtime: this.runtime,
|
|
111
|
+
transport: this.transport,
|
|
112
|
+
device,
|
|
113
|
+
serviceUuid: BOTA_CONTROL_SERVICE,
|
|
114
|
+
characteristicUuid: RECORDING_STATUS_CHARACTERISTIC,
|
|
115
|
+
operation: 'recording_control',
|
|
116
|
+
signal,
|
|
117
|
+
beforeSubscribe: async () => {
|
|
118
|
+
await gattStep(this.transport.write(device, BOTA_CONTROL_SERVICE, DEVICE_COMMAND_CHARACTERISTIC, grant, true), signal);
|
|
119
|
+
if (action === 'stop')
|
|
120
|
+
await this.controlDelay(signal);
|
|
121
|
+
},
|
|
122
|
+
listener: (notification) => {
|
|
123
|
+
if (!resultWindowOpen
|
|
124
|
+
|| resultReceived
|
|
125
|
+
|| !isCharacteristic(notification, RECORDING_STATUS_CHARACTERISTIC))
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
const decoded = publicControlResult(this.core.decodeRecordingControlResult(notification.value));
|
|
129
|
+
resultReceived = true;
|
|
130
|
+
result.resolve(decoded);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
resultReceived = true;
|
|
134
|
+
result.reject(error);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
body: async () => {
|
|
138
|
+
if (action === 'stop')
|
|
139
|
+
await this.controlDelay(signal);
|
|
140
|
+
await gattStep(this.transport.write(device, BOTA_CONTROL_SERVICE, RECORDING_CONTROL_CHARACTERISTIC, command, true), signal);
|
|
141
|
+
resultWindowOpen = true;
|
|
142
|
+
return await resultWithDeadline(result.promise, signal, this.resultTimeoutMs);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async controlDelay(signal) {
|
|
147
|
+
await this.delay(STOP_SEQUENCE_DELAY_MS);
|
|
148
|
+
throwIfAborted(signal);
|
|
149
|
+
}
|
|
150
|
+
async runManaged(requestSignal, body) {
|
|
151
|
+
if (this.activeControl) {
|
|
152
|
+
throw new BotaSDKError('operation_in_progress', 'recording_control');
|
|
153
|
+
}
|
|
154
|
+
let settle;
|
|
155
|
+
const active = {
|
|
156
|
+
controller: new AbortController(),
|
|
157
|
+
settled: new Promise((resolve) => {
|
|
158
|
+
settle = resolve;
|
|
159
|
+
}),
|
|
160
|
+
settle: () => settle(),
|
|
161
|
+
};
|
|
162
|
+
this.activeControl = active;
|
|
163
|
+
const abort = () => active.controller.abort();
|
|
164
|
+
requestSignal?.addEventListener('abort', abort, { once: true });
|
|
165
|
+
if (requestSignal?.aborted)
|
|
166
|
+
abort();
|
|
167
|
+
try {
|
|
168
|
+
return await this.runtime.runExclusive('recording_control', async (runtimeSignal) => await withCombinedSignal(runtimeSignal, active.controller.signal, body));
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
requestSignal?.removeEventListener('abort', abort);
|
|
172
|
+
if (this.activeControl === active)
|
|
173
|
+
this.activeControl = null;
|
|
174
|
+
active.settle();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async verifyConnectedDevice(signal) {
|
|
178
|
+
const connected = this.devices.connectedDevice;
|
|
179
|
+
const device = this.runtime.connectedDeviceHandle;
|
|
180
|
+
if (!connected || !device || connected.id !== device.id) {
|
|
181
|
+
throw new BotaSDKError('device_disconnected', 'recording_control');
|
|
182
|
+
}
|
|
183
|
+
const encoded = await gattStep(this.transport.read(device, DEVICE_INFORMATION_SERVICE, SERIAL_NUMBER_CHARACTERISTIC), signal);
|
|
184
|
+
const serialNumber = decodeSerial(encoded);
|
|
185
|
+
if (serialNumber !== connected.serialNumber) {
|
|
186
|
+
throw new BotaSDKError('identity_mismatch', 'recording_control');
|
|
187
|
+
}
|
|
188
|
+
return { device, serialNumber };
|
|
189
|
+
}
|
|
190
|
+
validateRequest(request) {
|
|
191
|
+
if (this.destroyed) {
|
|
192
|
+
throw new BotaSDKError('cancelled', 'recording_control');
|
|
193
|
+
}
|
|
194
|
+
if (!request
|
|
195
|
+
|| typeof request.authorityId !== 'string'
|
|
196
|
+
|| request.authorityId.length === 0
|
|
197
|
+
|| (request.operationId !== undefined
|
|
198
|
+
&& (typeof request.operationId !== 'string'
|
|
199
|
+
|| request.operationId.length === 0))) {
|
|
200
|
+
throw new BotaSDKError('invalid_input', 'recording_control');
|
|
201
|
+
}
|
|
202
|
+
if (request.signal?.aborted) {
|
|
203
|
+
throw new BotaSDKError('cancelled', 'recording_control');
|
|
204
|
+
}
|
|
205
|
+
return request.operationId ?? `recording_control:${crypto.randomUUID()}`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function publicControlResult(result) {
|
|
209
|
+
if (result.success)
|
|
210
|
+
return { success: true };
|
|
211
|
+
if (result.error === 'grant_expired') {
|
|
212
|
+
throw new BotaSDKError('authorization_expired', 'recording_control');
|
|
213
|
+
}
|
|
214
|
+
const error = result.error === 'already_recording'
|
|
215
|
+
|| result.error === 'not_recording'
|
|
216
|
+
|| result.error === 'invalid_grant'
|
|
217
|
+
|| result.error === 'invalid_state'
|
|
218
|
+
|| result.error === 'invalid_response'
|
|
219
|
+
? result.error
|
|
220
|
+
: 'unknown_error';
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
error,
|
|
224
|
+
...(typeof result.errorRaw === 'number'
|
|
225
|
+
? { errorRaw: result.errorRaw }
|
|
226
|
+
: {}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function gattStep(promise, signal) {
|
|
230
|
+
const outcome = await settled(promise);
|
|
231
|
+
if (outcome.kind === 'failed')
|
|
232
|
+
throw outcome.error;
|
|
233
|
+
throwIfAborted(signal);
|
|
234
|
+
return outcome.value;
|
|
235
|
+
}
|
|
236
|
+
async function resultWithDeadline(result, signal, timeoutMs) {
|
|
237
|
+
throwIfAborted(signal);
|
|
238
|
+
let timer = null;
|
|
239
|
+
let cancel;
|
|
240
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
241
|
+
cancel = () => reject(new BotaSDKError('cancelled', 'recording_control'));
|
|
242
|
+
});
|
|
243
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
244
|
+
timer = setTimeout(() => {
|
|
245
|
+
reject(new BotaSDKError('connection_failed', 'recording_control', {
|
|
246
|
+
retryable: true,
|
|
247
|
+
}));
|
|
248
|
+
}, timeoutMs);
|
|
249
|
+
});
|
|
250
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
251
|
+
try {
|
|
252
|
+
return await Promise.race([result, cancellation, timeout]);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
signal.removeEventListener('abort', cancel);
|
|
256
|
+
if (timer)
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function withCombinedSignal(primary, secondary, body) {
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
const abort = () => controller.abort();
|
|
263
|
+
primary.addEventListener('abort', abort, { once: true });
|
|
264
|
+
secondary.addEventListener('abort', abort, { once: true });
|
|
265
|
+
if (primary.aborted || secondary.aborted)
|
|
266
|
+
controller.abort();
|
|
267
|
+
try {
|
|
268
|
+
return await body(controller.signal);
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
primary.removeEventListener('abort', abort);
|
|
272
|
+
secondary.removeEventListener('abort', abort);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function managerError(error, operation) {
|
|
276
|
+
if (error instanceof BotaSDKError) {
|
|
277
|
+
return new BotaSDKError(error.code, operation, {
|
|
278
|
+
retryable: error.retryable,
|
|
279
|
+
protocolStatus: error.protocolStatus,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (error instanceof BrowserTransportError) {
|
|
283
|
+
const code = error.code === 'disconnected'
|
|
284
|
+
? 'device_disconnected'
|
|
285
|
+
: error.code === 'permission_denied'
|
|
286
|
+
? 'permission_denied'
|
|
287
|
+
: 'bluetooth_unavailable';
|
|
288
|
+
return new BotaSDKError(code, operation);
|
|
289
|
+
}
|
|
290
|
+
const normalized = normalizeCoreError(error, operation);
|
|
291
|
+
return new BotaSDKError(normalized.code, operation, {
|
|
292
|
+
retryable: normalized.retryable,
|
|
293
|
+
protocolStatus: normalized.protocolStatus,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function decodeSerial(value) {
|
|
297
|
+
try {
|
|
298
|
+
const serial = new TextDecoder('utf-8', { fatal: true })
|
|
299
|
+
.decode(value)
|
|
300
|
+
.replace(/^[\0\s]+|[\0\s]+$/g, '');
|
|
301
|
+
if (!serial)
|
|
302
|
+
throw new Error('empty');
|
|
303
|
+
return serial;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
throw new BotaSDKError('protocol_error', 'recording_control');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function isCharacteristic(notification, characteristicUuid) {
|
|
310
|
+
return canonicalGattUuid(notification.characteristicUuid)
|
|
311
|
+
=== canonicalGattUuid(characteristicUuid);
|
|
312
|
+
}
|
|
313
|
+
function throwIfAborted(signal) {
|
|
314
|
+
if (signal.aborted) {
|
|
315
|
+
throw new BotaSDKError('cancelled', 'recording_control');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function scrubLateGrant(value) {
|
|
319
|
+
try {
|
|
320
|
+
if (typeof value !== 'object' || value === null)
|
|
321
|
+
return;
|
|
322
|
+
const grant = value.grant;
|
|
323
|
+
if (grant instanceof Uint8Array)
|
|
324
|
+
grant.fill(0);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Application-owned late values are untrusted and already ignored.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function delay(milliseconds) {
|
|
331
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
332
|
+
}
|
|
333
|
+
function deferred() {
|
|
334
|
+
let resolve;
|
|
335
|
+
let reject;
|
|
336
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
337
|
+
resolve = resolvePromise;
|
|
338
|
+
reject = rejectPromise;
|
|
339
|
+
});
|
|
340
|
+
return { promise, resolve, reject };
|
|
341
|
+
}
|
|
342
|
+
async function settled(promise) {
|
|
343
|
+
return await promise.then((value) => ({ kind: 'completed', value }), (error) => ({ kind: 'failed', error }));
|
|
344
|
+
}
|