@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
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { BotaSDKError, CoreBridgeError, normalizeCoreError, } from "./errors.js";
|
|
2
|
+
import { BOTA_WIFI_CONFIG_SERVICE, DEVICE_INFORMATION_SERVICE, SERIAL_NUMBER_CHARACTERISTIC, WIFI_CREDENTIAL_CHARACTERISTIC, WIFI_GRANT_CHARACTERISTIC, WIFI_SCAN_CHARACTERISTIC, WIFI_STATUS_CHARACTERISTIC, canonicalGattUuid, } from "./gatt.js";
|
|
3
|
+
import { BrowserTransportError, } from "./transport.js";
|
|
4
|
+
const DEFAULT_OPERATION_TIMEOUT_MS = 30_000;
|
|
5
|
+
export class WiFiManager {
|
|
6
|
+
core;
|
|
7
|
+
transport;
|
|
8
|
+
runtime;
|
|
9
|
+
devices;
|
|
10
|
+
operationTimeoutMs;
|
|
11
|
+
removeDisconnectListener;
|
|
12
|
+
activeOperation = null;
|
|
13
|
+
statusOwner = null;
|
|
14
|
+
destroyed = false;
|
|
15
|
+
destroyPromise = null;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.core = options.core;
|
|
18
|
+
this.transport = options.transport;
|
|
19
|
+
this.runtime = options.runtime;
|
|
20
|
+
this.devices = options.devices;
|
|
21
|
+
this.operationTimeoutMs =
|
|
22
|
+
options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
23
|
+
this.removeDisconnectListener = this.runtime.onDeviceDisconnected((deviceId) => {
|
|
24
|
+
const owner = this.statusOwner;
|
|
25
|
+
if (owner?.deviceId === deviceId) {
|
|
26
|
+
void this.closeStatusOwner(owner);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async scanNetworks() {
|
|
31
|
+
this.requireUsable();
|
|
32
|
+
let command;
|
|
33
|
+
try {
|
|
34
|
+
command = this.core.encodeWiFiScanCommand();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw managerError(error, 'wifi');
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return await this.runManaged(async (signal) => {
|
|
41
|
+
const device = await this.verifyConnectedDevice(signal);
|
|
42
|
+
const result = deferred();
|
|
43
|
+
let resultWindowOpen = false;
|
|
44
|
+
let resultReceived = false;
|
|
45
|
+
return await withSubscription({
|
|
46
|
+
runtime: this.runtime,
|
|
47
|
+
transport: this.transport,
|
|
48
|
+
device,
|
|
49
|
+
serviceUuid: BOTA_WIFI_CONFIG_SERVICE,
|
|
50
|
+
characteristicUuid: WIFI_SCAN_CHARACTERISTIC,
|
|
51
|
+
operation: 'wifi',
|
|
52
|
+
signal,
|
|
53
|
+
listener: (notification) => {
|
|
54
|
+
if (!resultWindowOpen
|
|
55
|
+
|| resultReceived
|
|
56
|
+
|| !isCharacteristic(notification, WIFI_SCAN_CHARACTERISTIC))
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
const update = this.core.decodeWiFiScanUpdate(notification.value);
|
|
60
|
+
if (update.kind === 'pending')
|
|
61
|
+
return;
|
|
62
|
+
resultReceived = true;
|
|
63
|
+
result.resolve(publicScanResult(update));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
resultReceived = true;
|
|
67
|
+
result.reject(error);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
body: async () => {
|
|
71
|
+
await gattStep(this.transport.write(device, BOTA_WIFI_CONFIG_SERVICE, WIFI_SCAN_CHARACTERISTIC, command, true), signal, 'wifi');
|
|
72
|
+
resultWindowOpen = true;
|
|
73
|
+
return await resultWithDeadline(result.promise, signal, this.operationTimeoutMs, 'wifi');
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw await this.normalizeFailure(error);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
command.fill(0);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async configure(credentials, grant) {
|
|
86
|
+
this.requireUsable();
|
|
87
|
+
if (typeof credentials !== 'object'
|
|
88
|
+
|| credentials === null
|
|
89
|
+
|| typeof credentials.ssid !== 'string'
|
|
90
|
+
|| typeof credentials.password !== 'string'
|
|
91
|
+
|| typeof grant !== 'string') {
|
|
92
|
+
throw new BotaSDKError('invalid_input', 'wifi');
|
|
93
|
+
}
|
|
94
|
+
let encodedGrant = null;
|
|
95
|
+
let encodedCredentials = null;
|
|
96
|
+
try {
|
|
97
|
+
try {
|
|
98
|
+
encodedCredentials = this.core.encodeWiFiCredentials(credentials.ssid, credentials.password);
|
|
99
|
+
encodedGrant = this.core.encodeWiFiGrant(grant, this.transport.maximumWriteValueLength);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw managerError(error, 'wifi');
|
|
103
|
+
}
|
|
104
|
+
if (!encodedCredentials || !encodedGrant) {
|
|
105
|
+
throw new BotaSDKError('internal_error', 'wifi');
|
|
106
|
+
}
|
|
107
|
+
const operationCredentials = encodedCredentials;
|
|
108
|
+
const operationGrant = encodedGrant;
|
|
109
|
+
try {
|
|
110
|
+
return await this.runManaged(async (signal) => {
|
|
111
|
+
const device = await this.verifyConnectedDevice(signal);
|
|
112
|
+
return await this.configureWithSubscription(device, operationCredentials, signal, async () => {
|
|
113
|
+
await gattStep(this.transport.write(device, BOTA_WIFI_CONFIG_SERVICE, WIFI_GRANT_CHARACTERISTIC, operationGrant, true), signal, 'wifi');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw await this.normalizeFailure(error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
encodedGrant?.fill(0);
|
|
123
|
+
encodedCredentials?.fill(0);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async disconnect() {
|
|
127
|
+
this.requireUsable();
|
|
128
|
+
let command;
|
|
129
|
+
try {
|
|
130
|
+
command = this.core.encodeWiFiCredentials('', '');
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
throw managerError(error, 'wifi');
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return await this.runManaged(async (signal) => {
|
|
137
|
+
const device = await this.verifyConnectedDevice(signal);
|
|
138
|
+
return await this.configureWithSubscription(device, command, signal);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
throw await this.normalizeFailure(error);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
command.fill(0);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async readStatus() {
|
|
149
|
+
this.requireUsable();
|
|
150
|
+
try {
|
|
151
|
+
return await this.runManaged(async (signal) => {
|
|
152
|
+
const device = await this.verifyConnectedDevice(signal);
|
|
153
|
+
const encoded = await gattStep(this.transport.read(device, BOTA_WIFI_CONFIG_SERVICE, WIFI_STATUS_CHARACTERISTIC), signal, 'wifi');
|
|
154
|
+
return publicStatus(this.core.decodeWiFiStatus(encoded));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
throw await this.normalizeFailure(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async subscribeToStatus(listener) {
|
|
162
|
+
this.requireUsable();
|
|
163
|
+
if (typeof listener !== 'function') {
|
|
164
|
+
throw new BotaSDKError('invalid_input', 'wifi');
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return await this.runManaged(async (signal) => {
|
|
168
|
+
const device = await this.verifyConnectedDevice(signal);
|
|
169
|
+
return await this.subscribeToVerifiedStatus(device, listener);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
throw await this.normalizeFailure(error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async subscribeToVerifiedStatus(device, listener) {
|
|
177
|
+
let lease;
|
|
178
|
+
try {
|
|
179
|
+
lease = this.runtime.claimCharacteristicLease('wifi', device, BOTA_WIFI_CONFIG_SERVICE, WIFI_STATUS_CHARACTERISTIC);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
throw managerError(error, 'wifi');
|
|
183
|
+
}
|
|
184
|
+
let owner = null;
|
|
185
|
+
const pendingNotifications = [];
|
|
186
|
+
const deliverNotification = (notification) => {
|
|
187
|
+
const currentOwner = owner;
|
|
188
|
+
if (!currentOwner) {
|
|
189
|
+
pendingNotifications.push({
|
|
190
|
+
characteristicUuid: notification.characteristicUuid,
|
|
191
|
+
value: notification.value.slice(),
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (currentOwner.closing
|
|
196
|
+
|| this.destroyed
|
|
197
|
+
|| !isCharacteristic(notification, WIFI_STATUS_CHARACTERISTIC))
|
|
198
|
+
return;
|
|
199
|
+
try {
|
|
200
|
+
const status = publicStatus(this.core.decodeWiFiStatus(notification.value));
|
|
201
|
+
const completion = listener(status);
|
|
202
|
+
if (isPromiseLike(completion)) {
|
|
203
|
+
void Promise.resolve(completion).catch(() => {
|
|
204
|
+
void this.closeStatusOwner(currentOwner);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
void this.closeStatusOwner(currentOwner);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
let subscriptionPromise;
|
|
213
|
+
try {
|
|
214
|
+
subscriptionPromise = this.transport.subscribe(device, BOTA_WIFI_CONFIG_SERVICE, WIFI_STATUS_CHARACTERISTIC, deliverNotification);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
lease.release();
|
|
218
|
+
throw managerError(error, 'wifi');
|
|
219
|
+
}
|
|
220
|
+
owner = {
|
|
221
|
+
deviceId: device.id,
|
|
222
|
+
lease,
|
|
223
|
+
subscriptionPromise,
|
|
224
|
+
closing: false,
|
|
225
|
+
closePromise: null,
|
|
226
|
+
};
|
|
227
|
+
this.statusOwner = owner;
|
|
228
|
+
for (const notification of pendingNotifications.splice(0)) {
|
|
229
|
+
deliverNotification(notification);
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
await subscriptionPromise;
|
|
233
|
+
if (this.destroyed || owner.closing) {
|
|
234
|
+
await this.closeStatusOwner(owner);
|
|
235
|
+
throw new BotaSDKError('cancelled', 'wifi');
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
remove: async () => await this.closeStatusOwner(owner),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
await this.closeStatusOwner(owner);
|
|
243
|
+
throw managerError(error, 'wifi');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
destroy() {
|
|
247
|
+
if (this.destroyPromise)
|
|
248
|
+
return this.destroyPromise;
|
|
249
|
+
this.destroyed = true;
|
|
250
|
+
this.removeDisconnectListener();
|
|
251
|
+
const operation = this.activeOperation;
|
|
252
|
+
operation?.controller.abort();
|
|
253
|
+
const statusOwner = this.statusOwner;
|
|
254
|
+
this.destroyPromise = Promise.all([
|
|
255
|
+
operation?.settled ?? Promise.resolve(),
|
|
256
|
+
statusOwner
|
|
257
|
+
? this.closeStatusOwner(statusOwner)
|
|
258
|
+
: Promise.resolve(),
|
|
259
|
+
]).then(() => undefined);
|
|
260
|
+
return this.destroyPromise;
|
|
261
|
+
}
|
|
262
|
+
async configureWithSubscription(device, credentials, signal, beforeSubscribe) {
|
|
263
|
+
const result = deferred();
|
|
264
|
+
let resultWindowOpen = false;
|
|
265
|
+
let resultReceived = false;
|
|
266
|
+
return await withSubscription({
|
|
267
|
+
runtime: this.runtime,
|
|
268
|
+
transport: this.transport,
|
|
269
|
+
device,
|
|
270
|
+
serviceUuid: BOTA_WIFI_CONFIG_SERVICE,
|
|
271
|
+
characteristicUuid: WIFI_STATUS_CHARACTERISTIC,
|
|
272
|
+
operation: 'wifi',
|
|
273
|
+
signal,
|
|
274
|
+
beforeSubscribe,
|
|
275
|
+
listener: (notification) => {
|
|
276
|
+
if (!resultWindowOpen
|
|
277
|
+
|| resultReceived
|
|
278
|
+
|| !isCharacteristic(notification, WIFI_STATUS_CHARACTERISTIC))
|
|
279
|
+
return;
|
|
280
|
+
try {
|
|
281
|
+
resultReceived = true;
|
|
282
|
+
result.resolve(publicConfigResult(this.core.decodeWiFiConfigResult(notification.value)));
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
resultReceived = true;
|
|
286
|
+
result.reject(error);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
body: async () => {
|
|
290
|
+
await gattStep(this.transport.write(device, BOTA_WIFI_CONFIG_SERVICE, WIFI_CREDENTIAL_CHARACTERISTIC, credentials, true), signal, 'wifi');
|
|
291
|
+
resultWindowOpen = true;
|
|
292
|
+
return await resultWithDeadline(result.promise, signal, this.operationTimeoutMs, 'wifi');
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async runManaged(body) {
|
|
297
|
+
if (this.activeOperation) {
|
|
298
|
+
throw new BotaSDKError('operation_in_progress', 'wifi');
|
|
299
|
+
}
|
|
300
|
+
let settle;
|
|
301
|
+
const active = {
|
|
302
|
+
controller: new AbortController(),
|
|
303
|
+
settled: new Promise((resolve) => {
|
|
304
|
+
settle = resolve;
|
|
305
|
+
}),
|
|
306
|
+
settle: () => settle(),
|
|
307
|
+
};
|
|
308
|
+
this.activeOperation = active;
|
|
309
|
+
try {
|
|
310
|
+
return await this.runtime.runExclusive('wifi', async (runtimeSignal) => await withCombinedSignal(runtimeSignal, active.controller.signal, body));
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
if (this.activeOperation === active)
|
|
314
|
+
this.activeOperation = null;
|
|
315
|
+
active.settle();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async verifyConnectedDevice(signal) {
|
|
319
|
+
const connected = this.devices.connectedDevice;
|
|
320
|
+
const device = this.runtime.connectedDeviceHandle;
|
|
321
|
+
if (!connected || !device || connected.id !== device.id) {
|
|
322
|
+
throw new BotaSDKError('device_disconnected', 'wifi');
|
|
323
|
+
}
|
|
324
|
+
const encoded = await gattStep(this.transport.read(device, DEVICE_INFORMATION_SERVICE, SERIAL_NUMBER_CHARACTERISTIC), signal, 'wifi');
|
|
325
|
+
if (decodeSerial(encoded) !== connected.serialNumber) {
|
|
326
|
+
throw new BotaSDKError('identity_mismatch', 'wifi');
|
|
327
|
+
}
|
|
328
|
+
return device;
|
|
329
|
+
}
|
|
330
|
+
requireConnectedDevice() {
|
|
331
|
+
const connected = this.devices.connectedDevice;
|
|
332
|
+
const device = this.runtime.connectedDeviceHandle;
|
|
333
|
+
if (!connected || !device || connected.id !== device.id) {
|
|
334
|
+
throw new BotaSDKError('device_disconnected', 'wifi');
|
|
335
|
+
}
|
|
336
|
+
return device;
|
|
337
|
+
}
|
|
338
|
+
async normalizeFailure(error) {
|
|
339
|
+
const normalized = managerError(error, 'wifi');
|
|
340
|
+
if (normalized.code === 'identity_mismatch') {
|
|
341
|
+
await this.devices.disconnect().catch(() => undefined);
|
|
342
|
+
}
|
|
343
|
+
return normalized;
|
|
344
|
+
}
|
|
345
|
+
closeStatusOwner(owner) {
|
|
346
|
+
if (owner.closePromise)
|
|
347
|
+
return owner.closePromise;
|
|
348
|
+
owner.closing = true;
|
|
349
|
+
owner.closePromise = (async () => {
|
|
350
|
+
const subscription = await settled(owner.subscriptionPromise);
|
|
351
|
+
if (subscription.kind === 'completed') {
|
|
352
|
+
await subscription.value.remove().catch(() => undefined);
|
|
353
|
+
}
|
|
354
|
+
owner.lease.release();
|
|
355
|
+
if (this.statusOwner === owner)
|
|
356
|
+
this.statusOwner = null;
|
|
357
|
+
})();
|
|
358
|
+
return owner.closePromise;
|
|
359
|
+
}
|
|
360
|
+
requireUsable() {
|
|
361
|
+
if (this.destroyed)
|
|
362
|
+
throw new BotaSDKError('cancelled', 'wifi');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
export async function withSubscription(options) {
|
|
366
|
+
const lease = options.runtime.claimCharacteristicLease(options.operation, options.device, options.serviceUuid, options.characteristicUuid);
|
|
367
|
+
let subscription = null;
|
|
368
|
+
try {
|
|
369
|
+
await options.beforeSubscribe?.();
|
|
370
|
+
throwIfAborted(options.signal, options.operation);
|
|
371
|
+
const setup = await settled(options.transport.subscribe(options.device, options.serviceUuid, options.characteristicUuid, options.listener));
|
|
372
|
+
if (setup.kind === 'failed')
|
|
373
|
+
throw setup.error;
|
|
374
|
+
subscription = setup.value;
|
|
375
|
+
throwIfAborted(options.signal, options.operation);
|
|
376
|
+
return await options.body();
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
if (subscription)
|
|
380
|
+
await subscription.remove().catch(() => undefined);
|
|
381
|
+
lease.release();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function publicScanResult(update) {
|
|
385
|
+
return {
|
|
386
|
+
networks: update.networks.map((network) => ({ ...network })),
|
|
387
|
+
currentSsid: update.currentSsid,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function publicStatus(status) {
|
|
391
|
+
return {
|
|
392
|
+
status: status.status,
|
|
393
|
+
statusRaw: status.statusRaw,
|
|
394
|
+
...(typeof status.signalStrength === 'number'
|
|
395
|
+
? { signalStrength: status.signalStrength }
|
|
396
|
+
: {}),
|
|
397
|
+
...(typeof status.ssid === 'string' ? { ssid: status.ssid } : {}),
|
|
398
|
+
...(typeof status.lastError === 'string'
|
|
399
|
+
? { lastError: status.lastError }
|
|
400
|
+
: {}),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function publicConfigResult(result) {
|
|
404
|
+
if (result.success)
|
|
405
|
+
return { success: true };
|
|
406
|
+
const error = result.error === 'invalid_grant'
|
|
407
|
+
|| result.error === 'grant_expired'
|
|
408
|
+
|| result.error === 'decryption_error'
|
|
409
|
+
|| result.error === 'storage_error'
|
|
410
|
+
? result.error
|
|
411
|
+
: 'unknown';
|
|
412
|
+
return {
|
|
413
|
+
success: false,
|
|
414
|
+
error,
|
|
415
|
+
...(typeof result.errorRaw === 'number'
|
|
416
|
+
? { errorRaw: result.errorRaw }
|
|
417
|
+
: {}),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
async function gattStep(promise, signal, operation) {
|
|
421
|
+
const outcome = await settled(promise);
|
|
422
|
+
if (outcome.kind === 'failed')
|
|
423
|
+
throw outcome.error;
|
|
424
|
+
throwIfAborted(signal, operation);
|
|
425
|
+
return outcome.value;
|
|
426
|
+
}
|
|
427
|
+
async function resultWithDeadline(result, signal, timeoutMs, operation) {
|
|
428
|
+
throwIfAborted(signal, operation);
|
|
429
|
+
let timer = null;
|
|
430
|
+
let cancel;
|
|
431
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
432
|
+
cancel = () => reject(new BotaSDKError('cancelled', operation));
|
|
433
|
+
});
|
|
434
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
435
|
+
timer = setTimeout(() => {
|
|
436
|
+
reject(new BotaSDKError('connection_failed', operation, {
|
|
437
|
+
retryable: true,
|
|
438
|
+
}));
|
|
439
|
+
}, timeoutMs);
|
|
440
|
+
});
|
|
441
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
442
|
+
try {
|
|
443
|
+
return await Promise.race([result, cancellation, timeout]);
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
signal.removeEventListener('abort', cancel);
|
|
447
|
+
if (timer)
|
|
448
|
+
clearTimeout(timer);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function withCombinedSignal(primary, secondary, body) {
|
|
452
|
+
const controller = new AbortController();
|
|
453
|
+
const abort = () => controller.abort();
|
|
454
|
+
primary.addEventListener('abort', abort, { once: true });
|
|
455
|
+
secondary.addEventListener('abort', abort, { once: true });
|
|
456
|
+
if (primary.aborted || secondary.aborted)
|
|
457
|
+
controller.abort();
|
|
458
|
+
try {
|
|
459
|
+
return await body(controller.signal);
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
primary.removeEventListener('abort', abort);
|
|
463
|
+
secondary.removeEventListener('abort', abort);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function managerError(error, operation) {
|
|
467
|
+
if (error instanceof BotaSDKError) {
|
|
468
|
+
return new BotaSDKError(error.code, operation, {
|
|
469
|
+
retryable: error.retryable,
|
|
470
|
+
protocolStatus: error.protocolStatus,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
if (error instanceof BrowserTransportError) {
|
|
474
|
+
const code = error.code === 'disconnected'
|
|
475
|
+
? 'device_disconnected'
|
|
476
|
+
: error.code === 'permission_denied'
|
|
477
|
+
? 'permission_denied'
|
|
478
|
+
: 'bluetooth_unavailable';
|
|
479
|
+
return new BotaSDKError(code, operation);
|
|
480
|
+
}
|
|
481
|
+
if (error instanceof CoreBridgeError
|
|
482
|
+
&& (error.code === 'invalid_input' || error.code === 'payload_too_large')) {
|
|
483
|
+
return new BotaSDKError('invalid_input', operation);
|
|
484
|
+
}
|
|
485
|
+
const normalized = normalizeCoreError(error, operation);
|
|
486
|
+
return new BotaSDKError(normalized.code, operation, {
|
|
487
|
+
retryable: normalized.retryable,
|
|
488
|
+
protocolStatus: normalized.protocolStatus,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function decodeSerial(value) {
|
|
492
|
+
try {
|
|
493
|
+
const serial = new TextDecoder('utf-8', { fatal: true })
|
|
494
|
+
.decode(value)
|
|
495
|
+
.replace(/^[\0\s]+|[\0\s]+$/g, '');
|
|
496
|
+
if (!serial)
|
|
497
|
+
throw new Error('empty');
|
|
498
|
+
return serial;
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
throw new BotaSDKError('protocol_error', 'wifi');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function isCharacteristic(notification, characteristicUuid) {
|
|
505
|
+
return canonicalGattUuid(notification.characteristicUuid)
|
|
506
|
+
=== canonicalGattUuid(characteristicUuid);
|
|
507
|
+
}
|
|
508
|
+
function throwIfAborted(signal, operation) {
|
|
509
|
+
if (signal.aborted)
|
|
510
|
+
throw new BotaSDKError('cancelled', operation);
|
|
511
|
+
}
|
|
512
|
+
function deferred() {
|
|
513
|
+
let resolve;
|
|
514
|
+
let reject;
|
|
515
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
516
|
+
resolve = resolvePromise;
|
|
517
|
+
reject = rejectPromise;
|
|
518
|
+
});
|
|
519
|
+
return { promise, resolve, reject };
|
|
520
|
+
}
|
|
521
|
+
async function settled(promise) {
|
|
522
|
+
return await promise.then((value) => ({ kind: 'completed', value }), (error) => ({ kind: 'failed', error }));
|
|
523
|
+
}
|
|
524
|
+
function isPromiseLike(value) {
|
|
525
|
+
return typeof value === 'object'
|
|
526
|
+
&& value !== null
|
|
527
|
+
&& typeof value.then === 'function';
|
|
528
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { CoreBridge, CoreEffectEnvelope, CoreHostEvent, CoreNotification } from './core.ts';
|
|
2
|
+
import { type BotaOperation } from './errors.ts';
|
|
3
|
+
import { type BrowserSdkStorage } from './storage.ts';
|
|
4
|
+
import { type BrowserBluetoothTransport, type BrowserDeviceHandle } from './transport.ts';
|
|
5
|
+
export interface WorkflowResult {
|
|
6
|
+
notifications: CoreNotification[];
|
|
7
|
+
}
|
|
8
|
+
export interface WorkflowEffectContext {
|
|
9
|
+
operationId: string;
|
|
10
|
+
cancellationId: Uint8Array;
|
|
11
|
+
signal: AbortSignal;
|
|
12
|
+
dispatch(event: CoreHostEvent): Promise<void>;
|
|
13
|
+
addCleanup(cleanup: () => Promise<void>): void;
|
|
14
|
+
}
|
|
15
|
+
export interface WorkflowEffectHost {
|
|
16
|
+
execute(effect: CoreEffectEnvelope, context: WorkflowEffectContext): Promise<CoreHostEvent | readonly CoreHostEvent[] | null>;
|
|
17
|
+
cancel(): Promise<void>;
|
|
18
|
+
confirmationAttemptedOrClaimCancellation?(cancellationId: Uint8Array): Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
export interface WorkflowObserver {
|
|
21
|
+
onNotification?(notification: CoreNotification): void;
|
|
22
|
+
onProgress?(completedUnits: bigint, totalUnits: bigint): void;
|
|
23
|
+
onRunning?(): void;
|
|
24
|
+
}
|
|
25
|
+
export interface WorkflowEffectHosts {
|
|
26
|
+
persistence: WorkflowEffectHost;
|
|
27
|
+
recordingSink?: WorkflowEffectHost;
|
|
28
|
+
network?: WorkflowEffectHost;
|
|
29
|
+
firmwareBlob?: WorkflowEffectHost;
|
|
30
|
+
hostMaterial?: WorkflowEffectHost;
|
|
31
|
+
encryptedUploadV2?: WorkflowEffectHost;
|
|
32
|
+
}
|
|
33
|
+
export type WorkflowCompletionHandoff = (result: WorkflowResult) => Promise<void>;
|
|
34
|
+
export interface CharacteristicLease {
|
|
35
|
+
release(): void;
|
|
36
|
+
}
|
|
37
|
+
export declare class BrowserWorkflowRuntime {
|
|
38
|
+
private readonly core;
|
|
39
|
+
private readonly transport;
|
|
40
|
+
private readonly devices;
|
|
41
|
+
private readonly characteristicLeases;
|
|
42
|
+
private readonly disconnectListeners;
|
|
43
|
+
private activeOwner;
|
|
44
|
+
private connectedDevice;
|
|
45
|
+
private poisonedDeviceId;
|
|
46
|
+
private destroyed;
|
|
47
|
+
private destroyPromise;
|
|
48
|
+
private coreDispatchTail;
|
|
49
|
+
private nextConnectionAttempt;
|
|
50
|
+
private readonly connectionAttempts;
|
|
51
|
+
private readonly pendingConnectionSettlements;
|
|
52
|
+
constructor(core: CoreBridge, transport: BrowserBluetoothTransport);
|
|
53
|
+
get connectedDeviceHandle(): BrowserDeviceHandle | null;
|
|
54
|
+
registerDevice(device: BrowserDeviceHandle): void;
|
|
55
|
+
registeredDevice(deviceId: string): BrowserDeviceHandle | null;
|
|
56
|
+
waitForPendingConnection(deviceId: string): Promise<void>;
|
|
57
|
+
unregisterDevice(deviceId: string): void;
|
|
58
|
+
markDeviceDisconnected(deviceId: string): void;
|
|
59
|
+
poisonBleOwnership(deviceId: string): void;
|
|
60
|
+
onDeviceDisconnected(listener: (deviceId: string) => void): () => void;
|
|
61
|
+
claimCharacteristicLease(operation: BotaOperation, device: BrowserDeviceHandle, serviceUuid: string, characteristicUuid: string): CharacteristicLease;
|
|
62
|
+
run(operationId: string, cancellationId: Uint8Array, start: () => CoreEffectEnvelope[], hosts: WorkflowEffectHosts, observer?: WorkflowObserver, completionHandoff?: WorkflowCompletionHandoff): Promise<WorkflowResult>;
|
|
63
|
+
cancel(operationId: string): Promise<void>;
|
|
64
|
+
runExclusive<T>(operation: BotaOperation, body: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
65
|
+
destroy(): Promise<void>;
|
|
66
|
+
private enqueueEffects;
|
|
67
|
+
private dispatchDeviceDisconnected;
|
|
68
|
+
private validateEnvelope;
|
|
69
|
+
private ensurePump;
|
|
70
|
+
private pump;
|
|
71
|
+
private finishFromCoreStatus;
|
|
72
|
+
private executeEffect;
|
|
73
|
+
private effectContext;
|
|
74
|
+
private dispatchFromContext;
|
|
75
|
+
private startAuthorizedScan;
|
|
76
|
+
private connectDevice;
|
|
77
|
+
private discoverServices;
|
|
78
|
+
private disconnectDevice;
|
|
79
|
+
private readCharacteristic;
|
|
80
|
+
private writeCharacteristic;
|
|
81
|
+
private subscribe;
|
|
82
|
+
private unsubscribe;
|
|
83
|
+
private scheduleTimer;
|
|
84
|
+
private cancelTimer;
|
|
85
|
+
private executeHost;
|
|
86
|
+
private awaitOwnerStep;
|
|
87
|
+
private trackGattSetup;
|
|
88
|
+
private settlePendingGattSetups;
|
|
89
|
+
private settlePendingGattWrites;
|
|
90
|
+
private trackExternalOperation;
|
|
91
|
+
private trackExternalSettlement;
|
|
92
|
+
private settlePendingExternalOperations;
|
|
93
|
+
private ownerSignal;
|
|
94
|
+
private handleNotification;
|
|
95
|
+
private reportProgress;
|
|
96
|
+
private reportFirmwareProgress;
|
|
97
|
+
private dispatchBleFailure;
|
|
98
|
+
private serializedCoreCall;
|
|
99
|
+
private dispatchCurrentOwnerEvent;
|
|
100
|
+
private hasCompletionEvidence;
|
|
101
|
+
private enterCancellation;
|
|
102
|
+
private cancelOwner;
|
|
103
|
+
private finishSuccess;
|
|
104
|
+
private finishCancelled;
|
|
105
|
+
private failOwner;
|
|
106
|
+
private cancelAfterFailure;
|
|
107
|
+
private executeCancellationEffects;
|
|
108
|
+
private finishFailure;
|
|
109
|
+
private beginCancellation;
|
|
110
|
+
private confirmationAttemptedOrClaimCancellation;
|
|
111
|
+
private cleanupAndSettle;
|
|
112
|
+
private cleanupOwner;
|
|
113
|
+
private releaseOwner;
|
|
114
|
+
}
|
|
115
|
+
export declare function createBrowserPersistenceHost(storage: BrowserSdkStorage | null, now?: () => number): WorkflowEffectHost;
|