@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,1508 @@
|
|
|
1
|
+
import { BotaSDKError, normalizeCoreError, } from "./errors.js";
|
|
2
|
+
import { canonicalGattUuid } from "./gatt.js";
|
|
3
|
+
import { BrowserStorageError, } from "./storage.js";
|
|
4
|
+
import { BrowserTransportError, } from "./transport.js";
|
|
5
|
+
export class BrowserWorkflowRuntime {
|
|
6
|
+
core;
|
|
7
|
+
transport;
|
|
8
|
+
devices = new Map();
|
|
9
|
+
characteristicLeases = new Map();
|
|
10
|
+
disconnectListeners = new Set();
|
|
11
|
+
activeOwner = null;
|
|
12
|
+
connectedDevice = null;
|
|
13
|
+
poisonedDeviceId = null;
|
|
14
|
+
destroyed = false;
|
|
15
|
+
destroyPromise = null;
|
|
16
|
+
coreDispatchTail = Promise.resolve();
|
|
17
|
+
nextConnectionAttempt = 0;
|
|
18
|
+
connectionAttempts = new Map();
|
|
19
|
+
pendingConnectionSettlements = new Map();
|
|
20
|
+
constructor(core, transport) {
|
|
21
|
+
this.core = core;
|
|
22
|
+
this.transport = transport;
|
|
23
|
+
}
|
|
24
|
+
get connectedDeviceHandle() {
|
|
25
|
+
return this.connectedDevice;
|
|
26
|
+
}
|
|
27
|
+
registerDevice(device) {
|
|
28
|
+
this.devices.set(device.id, device);
|
|
29
|
+
}
|
|
30
|
+
registeredDevice(deviceId) {
|
|
31
|
+
return this.devices.get(deviceId) ?? null;
|
|
32
|
+
}
|
|
33
|
+
async waitForPendingConnection(deviceId) {
|
|
34
|
+
await this.pendingConnectionSettlements.get(deviceId);
|
|
35
|
+
}
|
|
36
|
+
unregisterDevice(deviceId) {
|
|
37
|
+
this.devices.delete(deviceId);
|
|
38
|
+
if (this.connectedDevice?.id === deviceId)
|
|
39
|
+
this.connectedDevice = null;
|
|
40
|
+
}
|
|
41
|
+
markDeviceDisconnected(deviceId) {
|
|
42
|
+
if (this.poisonedDeviceId === deviceId)
|
|
43
|
+
this.poisonedDeviceId = null;
|
|
44
|
+
if (this.connectedDevice?.id !== deviceId)
|
|
45
|
+
return;
|
|
46
|
+
this.connectedDevice = null;
|
|
47
|
+
for (const listener of [...this.disconnectListeners]) {
|
|
48
|
+
try {
|
|
49
|
+
listener(deviceId);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Lifecycle observers own their asynchronous cleanup and errors.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const owner = this.activeOwner;
|
|
56
|
+
if (!owner)
|
|
57
|
+
return;
|
|
58
|
+
if (owner.kind === 'direct') {
|
|
59
|
+
if (owner.operation === 'disconnect')
|
|
60
|
+
return;
|
|
61
|
+
owner.terminalError ??= new BotaSDKError('device_disconnected', owner.operation);
|
|
62
|
+
owner.abortController.abort();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const generation = owner.generation;
|
|
66
|
+
void this.dispatchDeviceDisconnected(owner, deviceId, generation).catch((error) => {
|
|
67
|
+
void this.failOwner(owner, error);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
poisonBleOwnership(deviceId) {
|
|
71
|
+
if (this.connectedDevice?.id === deviceId)
|
|
72
|
+
this.poisonedDeviceId = deviceId;
|
|
73
|
+
}
|
|
74
|
+
onDeviceDisconnected(listener) {
|
|
75
|
+
this.disconnectListeners.add(listener);
|
|
76
|
+
return () => this.disconnectListeners.delete(listener);
|
|
77
|
+
}
|
|
78
|
+
claimCharacteristicLease(operation, device, serviceUuid, characteristicUuid) {
|
|
79
|
+
if (this.destroyed)
|
|
80
|
+
throw new BotaSDKError('cancelled', operation);
|
|
81
|
+
if (this.connectedDevice?.id !== device.id) {
|
|
82
|
+
throw new BotaSDKError('device_disconnected', operation);
|
|
83
|
+
}
|
|
84
|
+
if (this.poisonedDeviceId !== null) {
|
|
85
|
+
throw new BotaSDKError('operation_in_progress', operation);
|
|
86
|
+
}
|
|
87
|
+
const key = [
|
|
88
|
+
device.id,
|
|
89
|
+
canonicalGattUuid(serviceUuid),
|
|
90
|
+
canonicalGattUuid(characteristicUuid),
|
|
91
|
+
].join(':');
|
|
92
|
+
if (this.characteristicLeases.has(key)) {
|
|
93
|
+
throw new BotaSDKError('operation_in_progress', operation);
|
|
94
|
+
}
|
|
95
|
+
const owner = Symbol(key);
|
|
96
|
+
this.characteristicLeases.set(key, owner);
|
|
97
|
+
let released = false;
|
|
98
|
+
return {
|
|
99
|
+
release: () => {
|
|
100
|
+
if (released)
|
|
101
|
+
return;
|
|
102
|
+
released = true;
|
|
103
|
+
if (this.characteristicLeases.get(key) === owner) {
|
|
104
|
+
this.characteristicLeases.delete(key);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
run(operationId, cancellationId, start, hosts, observer, completionHandoff) {
|
|
110
|
+
const operation = operationFromId(operationId);
|
|
111
|
+
if (this.destroyed) {
|
|
112
|
+
return Promise.reject(new BotaSDKError('cancelled', operation));
|
|
113
|
+
}
|
|
114
|
+
if (this.activeOwner
|
|
115
|
+
|| this.pendingConnectionSettlements.size > 0
|
|
116
|
+
|| (this.poisonedDeviceId !== null && operation !== 'disconnect')) {
|
|
117
|
+
return Promise.reject(new BotaSDKError('operation_in_progress', operation));
|
|
118
|
+
}
|
|
119
|
+
if (operationId.length === 0 || cancellationId.byteLength !== 16) {
|
|
120
|
+
return Promise.reject(new BotaSDKError('invalid_input', operation));
|
|
121
|
+
}
|
|
122
|
+
const result = deferred();
|
|
123
|
+
const owner = {
|
|
124
|
+
kind: 'workflow',
|
|
125
|
+
operationId,
|
|
126
|
+
operation,
|
|
127
|
+
cancellationId: cancellationId.slice(),
|
|
128
|
+
abortController: new AbortController(),
|
|
129
|
+
cancellationAbortController: null,
|
|
130
|
+
cancellationGeneration: null,
|
|
131
|
+
hosts,
|
|
132
|
+
observer,
|
|
133
|
+
result,
|
|
134
|
+
notifications: [],
|
|
135
|
+
queue: [],
|
|
136
|
+
requests: new Map(),
|
|
137
|
+
subscriptions: new Map(),
|
|
138
|
+
timers: new Map(),
|
|
139
|
+
cleanups: [],
|
|
140
|
+
pendingGattSetups: new Set(),
|
|
141
|
+
pendingGattWrites: new Set(),
|
|
142
|
+
pendingExternalOperations: new Set(),
|
|
143
|
+
completionHandoff,
|
|
144
|
+
completionEvidence: null,
|
|
145
|
+
completing: false,
|
|
146
|
+
completionPromise: null,
|
|
147
|
+
completionFailure: null,
|
|
148
|
+
generation: 0,
|
|
149
|
+
pumping: false,
|
|
150
|
+
inlineEffects: null,
|
|
151
|
+
terminal: false,
|
|
152
|
+
cancelling: false,
|
|
153
|
+
cancelPromise: null,
|
|
154
|
+
failure: null,
|
|
155
|
+
failurePromise: null,
|
|
156
|
+
cleanupPromise: null,
|
|
157
|
+
lastProgress: null,
|
|
158
|
+
lastFirmwareProgress: null,
|
|
159
|
+
authorizedScanExhausted: false,
|
|
160
|
+
authorizedReconnectDeviceIds: new Set(this.devices.keys()),
|
|
161
|
+
};
|
|
162
|
+
this.activeOwner = owner;
|
|
163
|
+
try {
|
|
164
|
+
this.enqueueEffects(owner, start(), owner.generation);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
void this.failOwner(owner, error);
|
|
168
|
+
}
|
|
169
|
+
return result.promise;
|
|
170
|
+
}
|
|
171
|
+
async cancel(operationId) {
|
|
172
|
+
const owner = this.activeOwner;
|
|
173
|
+
if (!owner
|
|
174
|
+
|| owner.kind !== 'workflow'
|
|
175
|
+
|| owner.operationId !== operationId
|
|
176
|
+
|| owner.terminal) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await this.enterCancellation(owner);
|
|
180
|
+
}
|
|
181
|
+
async runExclusive(operation, body) {
|
|
182
|
+
if (this.destroyed)
|
|
183
|
+
throw new BotaSDKError('cancelled', operation);
|
|
184
|
+
if (this.activeOwner
|
|
185
|
+
|| this.pendingConnectionSettlements.size > 0
|
|
186
|
+
|| (this.poisonedDeviceId !== null && operation !== 'disconnect')) {
|
|
187
|
+
throw new BotaSDKError('operation_in_progress', operation);
|
|
188
|
+
}
|
|
189
|
+
const abortController = new AbortController();
|
|
190
|
+
let settle;
|
|
191
|
+
const settled = new Promise((resolve) => {
|
|
192
|
+
settle = resolve;
|
|
193
|
+
});
|
|
194
|
+
const owner = {
|
|
195
|
+
kind: 'direct',
|
|
196
|
+
operation,
|
|
197
|
+
abortController,
|
|
198
|
+
terminalError: null,
|
|
199
|
+
settled,
|
|
200
|
+
};
|
|
201
|
+
this.activeOwner = owner;
|
|
202
|
+
try {
|
|
203
|
+
const value = await body(abortController.signal);
|
|
204
|
+
if (owner.terminalError)
|
|
205
|
+
throw owner.terminalError;
|
|
206
|
+
if (abortController.signal.aborted || this.destroyed) {
|
|
207
|
+
throw new BotaSDKError('cancelled', operation);
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
const normalized = normalizeRuntimeError(error, operation);
|
|
213
|
+
if (owner.terminalError) {
|
|
214
|
+
throw normalized.code === 'cancelled'
|
|
215
|
+
? owner.terminalError
|
|
216
|
+
: normalized;
|
|
217
|
+
}
|
|
218
|
+
if (abortController.signal.aborted || this.destroyed) {
|
|
219
|
+
throw new BotaSDKError('cancelled', operation);
|
|
220
|
+
}
|
|
221
|
+
throw normalized;
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
if (this.activeOwner === owner)
|
|
225
|
+
this.activeOwner = null;
|
|
226
|
+
settle();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
destroy() {
|
|
230
|
+
if (this.destroyPromise)
|
|
231
|
+
return this.destroyPromise;
|
|
232
|
+
this.destroyed = true;
|
|
233
|
+
this.destroyPromise = (async () => {
|
|
234
|
+
const owner = this.activeOwner;
|
|
235
|
+
if (!owner)
|
|
236
|
+
return;
|
|
237
|
+
if (owner.kind === 'workflow') {
|
|
238
|
+
await this.cancel(owner.operationId);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
owner.abortController.abort();
|
|
242
|
+
await owner.settled;
|
|
243
|
+
})();
|
|
244
|
+
return this.destroyPromise;
|
|
245
|
+
}
|
|
246
|
+
enqueueEffects(owner, effects, generation) {
|
|
247
|
+
if (owner.terminal || generation !== owner.generation) {
|
|
248
|
+
for (const effect of effects)
|
|
249
|
+
scrubTransientEffect(effect);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const queue = owner.inlineEffects ?? owner.queue;
|
|
253
|
+
for (let index = 0; index < effects.length; index += 1) {
|
|
254
|
+
const effect = effects[index];
|
|
255
|
+
if (!effect)
|
|
256
|
+
continue;
|
|
257
|
+
try {
|
|
258
|
+
this.validateEnvelope(owner, effect, generation);
|
|
259
|
+
queue.push({ envelope: effect, generation });
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
scrubTransientEffect(effect);
|
|
263
|
+
for (const remaining of effects.slice(index + 1)) {
|
|
264
|
+
scrubTransientEffect(remaining);
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (owner.inlineEffects === null)
|
|
270
|
+
this.ensurePump(owner);
|
|
271
|
+
}
|
|
272
|
+
async dispatchDeviceDisconnected(owner, deviceId, generation) {
|
|
273
|
+
if (owner.terminal
|
|
274
|
+
|| this.activeOwner !== owner
|
|
275
|
+
|| owner.generation !== generation)
|
|
276
|
+
return;
|
|
277
|
+
const requestId = owner.subscriptions.keys().next().value
|
|
278
|
+
?? owner.requests.keys().next().value;
|
|
279
|
+
if (requestId === undefined) {
|
|
280
|
+
throw new BotaSDKError('device_disconnected', owner.operation);
|
|
281
|
+
}
|
|
282
|
+
const effects = await this.serializedCoreCall(() => {
|
|
283
|
+
if (owner.terminal
|
|
284
|
+
|| this.activeOwner !== owner
|
|
285
|
+
|| owner.generation !== generation)
|
|
286
|
+
return [];
|
|
287
|
+
const status = this.core.status();
|
|
288
|
+
if (status.kind === 'completed'
|
|
289
|
+
|| status.kind === 'cancelled'
|
|
290
|
+
|| status.kind === 'failed')
|
|
291
|
+
return [];
|
|
292
|
+
return this.dispatchCurrentOwnerEvent(owner, generation, {
|
|
293
|
+
requestId,
|
|
294
|
+
kind: 'ble_disconnected',
|
|
295
|
+
peripheralId: deviceId,
|
|
296
|
+
reasonCode: null,
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
if (owner.terminal
|
|
300
|
+
|| this.activeOwner !== owner
|
|
301
|
+
|| owner.generation !== generation)
|
|
302
|
+
return;
|
|
303
|
+
this.enqueueEffects(owner, effects, generation);
|
|
304
|
+
}
|
|
305
|
+
validateEnvelope(owner, envelope, generation) {
|
|
306
|
+
if (!equalBytes(envelope.cancellationId, owner.cancellationId)) {
|
|
307
|
+
throw new BotaSDKError('internal_error', owner.operation);
|
|
308
|
+
}
|
|
309
|
+
const operation = publicOperation(envelope.operation);
|
|
310
|
+
if (owner.operation === 'unknown')
|
|
311
|
+
owner.operation = operation;
|
|
312
|
+
if (operation !== 'unknown' && owner.operation !== operation) {
|
|
313
|
+
throw new BotaSDKError('internal_error', owner.operation);
|
|
314
|
+
}
|
|
315
|
+
if (owner.requests.has(envelope.requestId)) {
|
|
316
|
+
throw new BotaSDKError('internal_error', owner.operation);
|
|
317
|
+
}
|
|
318
|
+
owner.requests.set(envelope.requestId, {
|
|
319
|
+
generation,
|
|
320
|
+
cancellationId: envelope.cancellationId.slice(),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
ensurePump(owner) {
|
|
324
|
+
if (owner.pumping || owner.terminal)
|
|
325
|
+
return;
|
|
326
|
+
owner.pumping = true;
|
|
327
|
+
void this.pump(owner).catch(async (error) => {
|
|
328
|
+
await this.failOwner(owner, error);
|
|
329
|
+
}).finally(() => {
|
|
330
|
+
owner.pumping = false;
|
|
331
|
+
if (owner.queue.length > 0 && !owner.terminal)
|
|
332
|
+
this.ensurePump(owner);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async pump(owner) {
|
|
336
|
+
while (owner.queue.length > 0 && !owner.terminal) {
|
|
337
|
+
const queued = owner.queue.shift();
|
|
338
|
+
if (!queued)
|
|
339
|
+
continue;
|
|
340
|
+
if (queued.generation !== owner.generation) {
|
|
341
|
+
scrubTransientEffect(queued.envelope);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
await this.executeEffect(owner, queued);
|
|
345
|
+
}
|
|
346
|
+
if (!owner.terminal && owner.queue.length === 0) {
|
|
347
|
+
await this.finishFromCoreStatus(owner);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async finishFromCoreStatus(owner) {
|
|
351
|
+
const status = this.core.status();
|
|
352
|
+
switch (status.kind) {
|
|
353
|
+
case 'completed':
|
|
354
|
+
if (!this.hasCompletionEvidence(owner)) {
|
|
355
|
+
throw new BotaSDKError('internal_error', owner.operation);
|
|
356
|
+
}
|
|
357
|
+
await this.finishSuccess(owner);
|
|
358
|
+
return;
|
|
359
|
+
case 'cancelled':
|
|
360
|
+
await this.finishCancelled(owner);
|
|
361
|
+
return;
|
|
362
|
+
case 'failed':
|
|
363
|
+
await this.finishFailure(owner, normalizeCoreError(status.error, owner.operation));
|
|
364
|
+
return;
|
|
365
|
+
case 'idle':
|
|
366
|
+
return;
|
|
367
|
+
case 'running':
|
|
368
|
+
try {
|
|
369
|
+
owner.observer?.onRunning?.();
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
// Observer failures do not change device workflow state.
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async executeEffect(owner, queued) {
|
|
378
|
+
const { envelope, generation } = queued;
|
|
379
|
+
const context = this.effectContext(owner, envelope, generation);
|
|
380
|
+
const { effect } = envelope;
|
|
381
|
+
switch (effect.kind) {
|
|
382
|
+
case 'notify':
|
|
383
|
+
await this.handleNotification(owner, effect.notification);
|
|
384
|
+
return;
|
|
385
|
+
case 'ble_start_scan':
|
|
386
|
+
await this.startAuthorizedScan(owner, envelope, context, generation);
|
|
387
|
+
return;
|
|
388
|
+
case 'ble_stop_scan':
|
|
389
|
+
await context.dispatch({
|
|
390
|
+
requestId: envelope.requestId,
|
|
391
|
+
kind: 'ble_scan_stopped',
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
case 'ble_connect':
|
|
395
|
+
await this.connectDevice(owner, envelope, context, generation);
|
|
396
|
+
return;
|
|
397
|
+
case 'ble_discover_services':
|
|
398
|
+
await this.discoverServices(owner, envelope, context, generation);
|
|
399
|
+
return;
|
|
400
|
+
case 'ble_disconnect':
|
|
401
|
+
await this.disconnectDevice(owner, envelope, context, generation);
|
|
402
|
+
return;
|
|
403
|
+
case 'ble_read':
|
|
404
|
+
await this.readCharacteristic(owner, envelope, context, generation);
|
|
405
|
+
return;
|
|
406
|
+
case 'ble_write':
|
|
407
|
+
await this.writeCharacteristic(owner, envelope, context, generation);
|
|
408
|
+
return;
|
|
409
|
+
case 'ble_subscribe':
|
|
410
|
+
await this.subscribe(owner, envelope, context, generation);
|
|
411
|
+
return;
|
|
412
|
+
case 'ble_unsubscribe':
|
|
413
|
+
await this.unsubscribe(owner, effect.serviceUuid, effect.characteristicUuid);
|
|
414
|
+
return;
|
|
415
|
+
case 'timer_schedule':
|
|
416
|
+
await this.scheduleTimer(owner, envelope, context, generation);
|
|
417
|
+
return;
|
|
418
|
+
case 'timer_cancel':
|
|
419
|
+
this.cancelTimer(owner, effect.timerId);
|
|
420
|
+
return;
|
|
421
|
+
case 'progress':
|
|
422
|
+
this.reportProgress(owner, effect.completedUnits, effect.totalUnits);
|
|
423
|
+
return;
|
|
424
|
+
case 'persistence_load_checkpoint':
|
|
425
|
+
case 'persistence_save_checkpoint':
|
|
426
|
+
case 'persistence_delete_checkpoint':
|
|
427
|
+
case 'persistence_save_connection_identity':
|
|
428
|
+
await this.executeHost(owner, owner.hosts.persistence, envelope, context, generation);
|
|
429
|
+
return;
|
|
430
|
+
case 'host_material_prepare_provisioning':
|
|
431
|
+
await this.executeHost(owner, owner.hosts.hostMaterial, envelope, context, generation);
|
|
432
|
+
return;
|
|
433
|
+
case 'recording_sink_truncate':
|
|
434
|
+
case 'recording_sink_append':
|
|
435
|
+
case 'recording_sink_finalize':
|
|
436
|
+
case 'recording_sink_discard':
|
|
437
|
+
await this.executeHost(owner, owner.hosts.recordingSink, envelope, context, generation);
|
|
438
|
+
return;
|
|
439
|
+
case 'network_download':
|
|
440
|
+
await this.executeHost(owner, owner.hosts.network, envelope, context, generation);
|
|
441
|
+
return;
|
|
442
|
+
case 'firmware_blob_read_chunk':
|
|
443
|
+
await this.executeHost(owner, owner.hosts.firmwareBlob, envelope, context, generation);
|
|
444
|
+
return;
|
|
445
|
+
case 'encrypted_upload_v2_load_checkpoint':
|
|
446
|
+
case 'encrypted_upload_v2_delete_checkpoint':
|
|
447
|
+
case 'encrypted_upload_v2_truncate_sink':
|
|
448
|
+
case 'encrypted_upload_v2_prepare_session':
|
|
449
|
+
case 'encrypted_upload_v2_start_transfer':
|
|
450
|
+
case 'encrypted_upload_v2_repair_window':
|
|
451
|
+
case 'encrypted_upload_v2_save_checkpoint':
|
|
452
|
+
case 'encrypted_upload_v2_acknowledge_window':
|
|
453
|
+
case 'encrypted_upload_v2_stage_artifacts':
|
|
454
|
+
case 'encrypted_upload_v2_await_completion_receipt':
|
|
455
|
+
case 'encrypted_upload_v2_confirm_with_receipt':
|
|
456
|
+
case 'encrypted_upload_v2_abort':
|
|
457
|
+
await this.executeHost(owner, owner.hosts.encryptedUploadV2, envelope, context, generation);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const unhandledEffect = effect;
|
|
461
|
+
void unhandledEffect;
|
|
462
|
+
throw new BotaSDKError('internal_error', owner.operation);
|
|
463
|
+
}
|
|
464
|
+
effectContext(owner, envelope, generation) {
|
|
465
|
+
const signal = this.ownerSignal(owner, generation);
|
|
466
|
+
const context = {
|
|
467
|
+
operationId: owner.operationId,
|
|
468
|
+
cancellationId: owner.cancellationId.slice(),
|
|
469
|
+
signal,
|
|
470
|
+
dispatch: async (event) => {
|
|
471
|
+
await this.dispatchFromContext(owner, envelope, generation, context, event);
|
|
472
|
+
},
|
|
473
|
+
addCleanup: (cleanup) => {
|
|
474
|
+
if (owner.terminal || generation !== owner.generation) {
|
|
475
|
+
void cleanup().catch(() => undefined);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
owner.cleanups.push(cleanup);
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
return context;
|
|
482
|
+
}
|
|
483
|
+
async dispatchFromContext(owner, envelope, generation, context, event) {
|
|
484
|
+
if (owner.terminal || generation !== owner.generation)
|
|
485
|
+
return;
|
|
486
|
+
if (event.requestId !== envelope.requestId
|
|
487
|
+
|| !equalBytes(context.cancellationId, owner.cancellationId)) {
|
|
488
|
+
throw new BotaSDKError('internal_error', owner.operation);
|
|
489
|
+
}
|
|
490
|
+
const request = owner.requests.get(event.requestId);
|
|
491
|
+
if (!request
|
|
492
|
+
|| request.generation !== generation
|
|
493
|
+
|| !equalBytes(request.cancellationId, owner.cancellationId)) {
|
|
494
|
+
throw new BotaSDKError('internal_error', owner.operation);
|
|
495
|
+
}
|
|
496
|
+
const effects = await this.serializedCoreCall(() => {
|
|
497
|
+
if (owner.terminal || generation !== owner.generation)
|
|
498
|
+
return [];
|
|
499
|
+
const status = this.core.status();
|
|
500
|
+
if (status.kind === 'completed'
|
|
501
|
+
|| status.kind === 'cancelled'
|
|
502
|
+
|| status.kind === 'failed') {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
return this.dispatchCurrentOwnerEvent(owner, generation, event);
|
|
506
|
+
});
|
|
507
|
+
if (owner.terminal || generation !== owner.generation)
|
|
508
|
+
return;
|
|
509
|
+
this.enqueueEffects(owner, effects, generation);
|
|
510
|
+
}
|
|
511
|
+
async startAuthorizedScan(owner, envelope, context, generation) {
|
|
512
|
+
if (!this.transport.supportsAuthorizedDevices) {
|
|
513
|
+
throw new BotaSDKError('picker_required', owner.operation);
|
|
514
|
+
}
|
|
515
|
+
const authorizedResult = await this.awaitOwnerStep(owner, generation, this.trackExternalOperation(owner, this.transport.getAuthorizedDevices()));
|
|
516
|
+
if (authorizedResult.kind === 'cancelled')
|
|
517
|
+
return;
|
|
518
|
+
if (authorizedResult.kind === 'failed')
|
|
519
|
+
throw authorizedResult.error;
|
|
520
|
+
const authorized = authorizedResult.value;
|
|
521
|
+
if (owner.terminal || generation !== owner.generation)
|
|
522
|
+
return;
|
|
523
|
+
let forwarded = 0;
|
|
524
|
+
for (const device of authorized) {
|
|
525
|
+
if (!owner.authorizedReconnectDeviceIds.has(device.id))
|
|
526
|
+
continue;
|
|
527
|
+
this.devices.set(device.id, device);
|
|
528
|
+
forwarded += 1;
|
|
529
|
+
await context.dispatch({
|
|
530
|
+
requestId: envelope.requestId,
|
|
531
|
+
kind: 'ble_scan_result',
|
|
532
|
+
candidate: {
|
|
533
|
+
peripheralId: device.id,
|
|
534
|
+
name: device.name,
|
|
535
|
+
advertisedAddress: null,
|
|
536
|
+
rssi: 0,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
owner.authorizedScanExhausted = forwarded === 0;
|
|
541
|
+
}
|
|
542
|
+
async connectDevice(owner, envelope, context, generation) {
|
|
543
|
+
if (envelope.effect.kind !== 'ble_connect')
|
|
544
|
+
return;
|
|
545
|
+
const device = this.devices.get(envelope.effect.peripheralId);
|
|
546
|
+
if (!device) {
|
|
547
|
+
await this.dispatchBleFailure(context, envelope.requestId, null);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const lifecycle = deferred();
|
|
551
|
+
this.trackExternalSettlement(owner, lifecycle.promise);
|
|
552
|
+
try {
|
|
553
|
+
const attempt = ++this.nextConnectionAttempt;
|
|
554
|
+
this.connectionAttempts.set(device.id, attempt);
|
|
555
|
+
const pendingConnection = this.transport.connect(device);
|
|
556
|
+
const connection = await this.awaitOwnerStep(owner, generation, pendingConnection);
|
|
557
|
+
if (connection.kind === 'cancelled') {
|
|
558
|
+
let settlement;
|
|
559
|
+
settlement = pendingConnection.then(async () => {
|
|
560
|
+
if (this.connectionAttempts.get(device.id) === attempt
|
|
561
|
+
&& this.connectedDevice?.id !== device.id) {
|
|
562
|
+
await this.transport.disconnect(device).catch(() => undefined);
|
|
563
|
+
}
|
|
564
|
+
}, () => undefined).finally(() => {
|
|
565
|
+
if (this.connectionAttempts.get(device.id) === attempt) {
|
|
566
|
+
this.connectionAttempts.delete(device.id);
|
|
567
|
+
}
|
|
568
|
+
if (this.pendingConnectionSettlements.get(device.id) === settlement) {
|
|
569
|
+
this.pendingConnectionSettlements.delete(device.id);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
this.pendingConnectionSettlements.set(device.id, settlement);
|
|
573
|
+
await settlement;
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (this.connectionAttempts.get(device.id) === attempt) {
|
|
577
|
+
this.connectionAttempts.delete(device.id);
|
|
578
|
+
}
|
|
579
|
+
if (connection.kind === 'failed') {
|
|
580
|
+
await this.dispatchBleFailure(context, envelope.requestId, connection.error);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (owner.terminal
|
|
584
|
+
|| generation !== owner.generation
|
|
585
|
+
|| owner.abortController.signal.aborted) {
|
|
586
|
+
await this.transport.disconnect(device).catch(() => undefined);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this.connectedDevice = device;
|
|
590
|
+
await context.dispatch({
|
|
591
|
+
requestId: envelope.requestId,
|
|
592
|
+
kind: 'ble_connected',
|
|
593
|
+
peripheralId: device.id,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
finally {
|
|
597
|
+
lifecycle.resolve(undefined);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async discoverServices(owner, envelope, context, generation) {
|
|
601
|
+
if (envelope.effect.kind !== 'ble_discover_services')
|
|
602
|
+
return;
|
|
603
|
+
const device = this.devices.get(envelope.effect.peripheralId);
|
|
604
|
+
if (!device || this.connectedDevice?.id !== device.id) {
|
|
605
|
+
await this.dispatchBleFailure(context, envelope.requestId, null);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const discovery = await this.awaitOwnerStep(owner, generation, this.trackExternalOperation(owner, this.transport.discoverServices(device)));
|
|
609
|
+
if (discovery.kind === 'cancelled')
|
|
610
|
+
return;
|
|
611
|
+
if (discovery.kind === 'completed') {
|
|
612
|
+
await context.dispatch({
|
|
613
|
+
requestId: envelope.requestId,
|
|
614
|
+
kind: 'ble_services_discovered',
|
|
615
|
+
peripheralId: device.id,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
await this.dispatchBleFailure(context, envelope.requestId, discovery.error);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async disconnectDevice(owner, envelope, context, generation) {
|
|
623
|
+
if (envelope.effect.kind !== 'ble_disconnect')
|
|
624
|
+
return;
|
|
625
|
+
const device = this.devices.get(envelope.effect.peripheralId);
|
|
626
|
+
try {
|
|
627
|
+
if (device && this.connectedDevice?.id === device.id) {
|
|
628
|
+
const disconnected = await this.awaitOwnerStep(owner, generation, this.trackExternalOperation(owner, this.transport.disconnect(device)));
|
|
629
|
+
if (disconnected.kind === 'cancelled')
|
|
630
|
+
return;
|
|
631
|
+
if (disconnected.kind === 'failed')
|
|
632
|
+
throw disconnected.error;
|
|
633
|
+
}
|
|
634
|
+
if (this.connectedDevice?.id === envelope.effect.peripheralId) {
|
|
635
|
+
this.connectedDevice = null;
|
|
636
|
+
}
|
|
637
|
+
await context.dispatch({
|
|
638
|
+
requestId: envelope.requestId,
|
|
639
|
+
kind: 'ble_disconnected',
|
|
640
|
+
peripheralId: envelope.effect.peripheralId,
|
|
641
|
+
reasonCode: null,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
await this.dispatchBleFailure(context, envelope.requestId, error);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async readCharacteristic(owner, envelope, context, generation) {
|
|
649
|
+
if (envelope.effect.kind !== 'ble_read')
|
|
650
|
+
return;
|
|
651
|
+
const device = this.connectedDevice;
|
|
652
|
+
if (!device) {
|
|
653
|
+
await this.dispatchBleFailure(context, envelope.requestId, null);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const read = await this.awaitOwnerStep(owner, generation, this.trackExternalOperation(owner, this.transport.read(device, envelope.effect.serviceUuid, envelope.effect.characteristicUuid)));
|
|
657
|
+
if (read.kind === 'cancelled')
|
|
658
|
+
return;
|
|
659
|
+
if (read.kind === 'completed') {
|
|
660
|
+
await context.dispatch({
|
|
661
|
+
requestId: envelope.requestId,
|
|
662
|
+
kind: 'ble_read_completed',
|
|
663
|
+
value: read.value,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
await this.dispatchBleFailure(context, envelope.requestId, read.error);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async writeCharacteristic(owner, envelope, context, generation) {
|
|
671
|
+
if (envelope.effect.kind !== 'ble_write')
|
|
672
|
+
return;
|
|
673
|
+
const payload = envelope.effect.payload;
|
|
674
|
+
const device = this.connectedDevice;
|
|
675
|
+
if (!device) {
|
|
676
|
+
payload.fill(0);
|
|
677
|
+
await this.dispatchBleFailure(context, envelope.requestId, null);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const transportPayload = payload.slice();
|
|
681
|
+
let pendingWrite;
|
|
682
|
+
try {
|
|
683
|
+
pendingWrite = this.transport.write(device, envelope.effect.serviceUuid, envelope.effect.characteristicUuid, transportPayload, envelope.effect.withResponse).finally(() => {
|
|
684
|
+
transportPayload.fill(0);
|
|
685
|
+
payload.fill(0);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
transportPayload.fill(0);
|
|
690
|
+
payload.fill(0);
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
let trackedWrite;
|
|
694
|
+
trackedWrite = pendingWrite.finally(() => {
|
|
695
|
+
owner.pendingGattWrites.delete(trackedWrite);
|
|
696
|
+
});
|
|
697
|
+
owner.pendingGattWrites.add(trackedWrite);
|
|
698
|
+
const write = await this.awaitOwnerStep(owner, generation, trackedWrite);
|
|
699
|
+
if (write.kind === 'cancelled')
|
|
700
|
+
return;
|
|
701
|
+
if (write.kind === 'completed') {
|
|
702
|
+
await context.dispatch({
|
|
703
|
+
requestId: envelope.requestId,
|
|
704
|
+
kind: 'ble_write_completed',
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
await this.dispatchBleFailure(context, envelope.requestId, write.error);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async subscribe(owner, envelope, context, generation) {
|
|
712
|
+
if (envelope.effect.kind !== 'ble_subscribe')
|
|
713
|
+
return;
|
|
714
|
+
const device = this.connectedDevice;
|
|
715
|
+
if (!device) {
|
|
716
|
+
await this.dispatchBleFailure(context, envelope.requestId, null);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const characteristicUuid = envelope.effect.characteristicUuid;
|
|
720
|
+
const pendingSubscription = this.trackGattSetup(owner, generation, this.transport.subscribe(device, envelope.effect.serviceUuid, envelope.effect.characteristicUuid, (notification) => {
|
|
721
|
+
void context.dispatch({
|
|
722
|
+
requestId: envelope.requestId,
|
|
723
|
+
kind: 'ble_notification',
|
|
724
|
+
characteristicUuid,
|
|
725
|
+
value: notification.value,
|
|
726
|
+
}).catch((error) => {
|
|
727
|
+
void this.failOwner(owner, error);
|
|
728
|
+
});
|
|
729
|
+
}), async (subscription) => subscription.remove());
|
|
730
|
+
const subscribed = await this.awaitOwnerStep(owner, generation, pendingSubscription.promise);
|
|
731
|
+
if (subscribed.kind === 'cancelled')
|
|
732
|
+
return;
|
|
733
|
+
if (subscribed.kind === 'failed') {
|
|
734
|
+
pendingSubscription.cancel();
|
|
735
|
+
await this.dispatchBleFailure(context, envelope.requestId, subscribed.error);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const subscription = subscribed.value;
|
|
739
|
+
if (owner.terminal || generation !== owner.generation) {
|
|
740
|
+
pendingSubscription.cancel();
|
|
741
|
+
await pendingSubscription.settlement;
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
let removal = null;
|
|
746
|
+
const runtimeSubscription = {
|
|
747
|
+
serviceUuid: envelope.effect.serviceUuid,
|
|
748
|
+
characteristicUuid: envelope.effect.characteristicUuid,
|
|
749
|
+
subscription,
|
|
750
|
+
remove: () => {
|
|
751
|
+
removal ??= subscription.remove();
|
|
752
|
+
return removal;
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
owner.subscriptions.set(envelope.requestId, runtimeSubscription);
|
|
756
|
+
context.addCleanup(runtimeSubscription.remove);
|
|
757
|
+
pendingSubscription.transferOwnership();
|
|
758
|
+
await context.dispatch({
|
|
759
|
+
requestId: envelope.requestId,
|
|
760
|
+
kind: 'ble_subscribed',
|
|
761
|
+
characteristicUuid: envelope.effect.characteristicUuid,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
await this.dispatchBleFailure(context, envelope.requestId, error);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async unsubscribe(owner, serviceUuid, characteristicUuid) {
|
|
769
|
+
const matches = [...owner.subscriptions.entries()].filter(([, value]) => value.serviceUuid === serviceUuid
|
|
770
|
+
&& value.characteristicUuid === characteristicUuid);
|
|
771
|
+
for (const [requestId, value] of matches) {
|
|
772
|
+
await value.remove();
|
|
773
|
+
owner.subscriptions.delete(requestId);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async scheduleTimer(owner, envelope, context, generation) {
|
|
777
|
+
if (envelope.effect.kind !== 'timer_schedule')
|
|
778
|
+
return;
|
|
779
|
+
const effect = envelope.effect;
|
|
780
|
+
const delayMs = Number(effect.delayMs);
|
|
781
|
+
if (!Number.isSafeInteger(delayMs)
|
|
782
|
+
|| delayMs < 0
|
|
783
|
+
|| delayMs > 2_147_483_647) {
|
|
784
|
+
throw new BotaSDKError('internal_error', owner.operation);
|
|
785
|
+
}
|
|
786
|
+
this.cancelTimer(owner, effect.timerId);
|
|
787
|
+
if (owner.authorizedScanExhausted) {
|
|
788
|
+
owner.authorizedScanExhausted = false;
|
|
789
|
+
await context.dispatch({
|
|
790
|
+
requestId: envelope.requestId,
|
|
791
|
+
kind: 'timer_fired',
|
|
792
|
+
timerId: effect.timerId,
|
|
793
|
+
});
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const timer = setTimeout(() => {
|
|
797
|
+
owner.timers.delete(effect.timerId);
|
|
798
|
+
if (owner.terminal || generation !== owner.generation)
|
|
799
|
+
return;
|
|
800
|
+
void context.dispatch({
|
|
801
|
+
requestId: envelope.requestId,
|
|
802
|
+
kind: 'timer_fired',
|
|
803
|
+
timerId: effect.timerId,
|
|
804
|
+
}).catch((error) => {
|
|
805
|
+
void this.failOwner(owner, error);
|
|
806
|
+
});
|
|
807
|
+
}, delayMs);
|
|
808
|
+
owner.timers.set(effect.timerId, timer);
|
|
809
|
+
context.addCleanup(async () => {
|
|
810
|
+
clearTimeout(timer);
|
|
811
|
+
owner.timers.delete(effect.timerId);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
cancelTimer(owner, timerId) {
|
|
815
|
+
const timer = owner.timers.get(timerId);
|
|
816
|
+
if (timer)
|
|
817
|
+
clearTimeout(timer);
|
|
818
|
+
owner.timers.delete(timerId);
|
|
819
|
+
}
|
|
820
|
+
async executeHost(owner, host, envelope, context, generation) {
|
|
821
|
+
if (!host) {
|
|
822
|
+
throw new BotaSDKError('unsupported_capability', publicOperation(envelope.operation));
|
|
823
|
+
}
|
|
824
|
+
const execution = this.trackExternalOperation(owner, host.execute(envelope, context));
|
|
825
|
+
const executed = await this.awaitOwnerStep(owner, generation, execution);
|
|
826
|
+
if (executed.kind === 'cancelled')
|
|
827
|
+
return;
|
|
828
|
+
if (executed.kind === 'failed')
|
|
829
|
+
throw executed.error;
|
|
830
|
+
if (!executed.value)
|
|
831
|
+
return;
|
|
832
|
+
const events = Array.isArray(executed.value)
|
|
833
|
+
? executed.value
|
|
834
|
+
: [executed.value];
|
|
835
|
+
for (const event of events)
|
|
836
|
+
await context.dispatch(event);
|
|
837
|
+
}
|
|
838
|
+
async awaitOwnerStep(owner, generation, promise) {
|
|
839
|
+
const signal = this.ownerSignal(owner, generation);
|
|
840
|
+
const settled = promise.then((value) => ({ kind: 'completed', value }), (error) => ({ kind: 'failed', error }));
|
|
841
|
+
if (owner.terminal
|
|
842
|
+
|| generation !== owner.generation
|
|
843
|
+
|| signal.aborted) {
|
|
844
|
+
return { kind: 'cancelled' };
|
|
845
|
+
}
|
|
846
|
+
let resolveCancellation;
|
|
847
|
+
const cancellation = new Promise((resolve) => {
|
|
848
|
+
resolveCancellation = () => resolve({ kind: 'cancelled' });
|
|
849
|
+
});
|
|
850
|
+
signal.addEventListener('abort', resolveCancellation, { once: true });
|
|
851
|
+
const result = await Promise.race([settled, cancellation]);
|
|
852
|
+
signal.removeEventListener('abort', resolveCancellation);
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
trackGattSetup(owner, generation, promise, cleanup) {
|
|
856
|
+
const signal = this.ownerSignal(owner, generation);
|
|
857
|
+
let decisionMade = false;
|
|
858
|
+
let resolveDecision;
|
|
859
|
+
const decision = new Promise((resolve) => {
|
|
860
|
+
resolveDecision = resolve;
|
|
861
|
+
});
|
|
862
|
+
const decide = (value) => {
|
|
863
|
+
if (decisionMade)
|
|
864
|
+
return;
|
|
865
|
+
decisionMade = true;
|
|
866
|
+
signal.removeEventListener('abort', onAbort);
|
|
867
|
+
resolveDecision(value);
|
|
868
|
+
};
|
|
869
|
+
const onAbort = () => decide('cancelled');
|
|
870
|
+
signal.addEventListener('abort', onAbort, {
|
|
871
|
+
once: true,
|
|
872
|
+
});
|
|
873
|
+
if (signal.aborted)
|
|
874
|
+
onAbort();
|
|
875
|
+
let settlement;
|
|
876
|
+
settlement = promise.then(async (value) => {
|
|
877
|
+
const setupDecision = await decision;
|
|
878
|
+
if (setupDecision === 'cancelled'
|
|
879
|
+
|| owner.terminal
|
|
880
|
+
|| generation !== owner.generation) {
|
|
881
|
+
await cleanup(value);
|
|
882
|
+
}
|
|
883
|
+
}, () => undefined).finally(() => {
|
|
884
|
+
signal.removeEventListener('abort', onAbort);
|
|
885
|
+
owner.pendingGattSetups.delete(settlement);
|
|
886
|
+
});
|
|
887
|
+
owner.pendingGattSetups.add(settlement);
|
|
888
|
+
return {
|
|
889
|
+
promise,
|
|
890
|
+
settlement,
|
|
891
|
+
cancel: () => decide('cancelled'),
|
|
892
|
+
transferOwnership: () => decide('transferred'),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
async settlePendingGattSetups(owner) {
|
|
896
|
+
let failure = null;
|
|
897
|
+
while (owner.pendingGattSetups.size > 0) {
|
|
898
|
+
const settlements = [...owner.pendingGattSetups];
|
|
899
|
+
const results = await Promise.allSettled(settlements);
|
|
900
|
+
for (const result of results) {
|
|
901
|
+
if (failure === null && result.status === 'rejected') {
|
|
902
|
+
failure = result.reason;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (failure !== null)
|
|
907
|
+
throw failure;
|
|
908
|
+
}
|
|
909
|
+
async settlePendingGattWrites(owner) {
|
|
910
|
+
let failure = null;
|
|
911
|
+
while (owner.pendingGattWrites.size > 0) {
|
|
912
|
+
const settlements = [...owner.pendingGattWrites];
|
|
913
|
+
const results = await Promise.allSettled(settlements);
|
|
914
|
+
for (const result of results) {
|
|
915
|
+
if (failure === null && result.status === 'rejected') {
|
|
916
|
+
failure = result.reason;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (failure !== null)
|
|
921
|
+
throw failure;
|
|
922
|
+
}
|
|
923
|
+
trackExternalOperation(owner, promise) {
|
|
924
|
+
this.trackExternalSettlement(owner, promise.then(() => undefined, () => undefined));
|
|
925
|
+
return promise;
|
|
926
|
+
}
|
|
927
|
+
trackExternalSettlement(owner, pending) {
|
|
928
|
+
let settlement;
|
|
929
|
+
settlement = pending.finally(() => {
|
|
930
|
+
owner.pendingExternalOperations.delete(settlement);
|
|
931
|
+
});
|
|
932
|
+
owner.pendingExternalOperations.add(settlement);
|
|
933
|
+
}
|
|
934
|
+
async settlePendingExternalOperations(owner) {
|
|
935
|
+
while (owner.pendingExternalOperations.size > 0) {
|
|
936
|
+
await Promise.allSettled([...owner.pendingExternalOperations]);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
ownerSignal(owner, generation) {
|
|
940
|
+
if (owner.cancellationGeneration === generation
|
|
941
|
+
&& owner.cancellationAbortController) {
|
|
942
|
+
return owner.cancellationAbortController.signal;
|
|
943
|
+
}
|
|
944
|
+
return owner.abortController.signal;
|
|
945
|
+
}
|
|
946
|
+
async handleNotification(owner, notification) {
|
|
947
|
+
owner.notifications.push(notification);
|
|
948
|
+
try {
|
|
949
|
+
owner.observer?.onNotification?.(notification);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
// Observer failures do not change device workflow state.
|
|
953
|
+
}
|
|
954
|
+
if (notification.kind === 'progress') {
|
|
955
|
+
this.reportProgress(owner, notification.completedUnits, notification.totalUnits);
|
|
956
|
+
}
|
|
957
|
+
else if (notification.kind === 'firmware_progress') {
|
|
958
|
+
this.reportFirmwareProgress(owner, notification.phase, notification.completedBytes, notification.totalBytes);
|
|
959
|
+
}
|
|
960
|
+
switch (notification.kind) {
|
|
961
|
+
case 'completed':
|
|
962
|
+
await this.finishSuccess(owner);
|
|
963
|
+
return;
|
|
964
|
+
case 'cancelled':
|
|
965
|
+
await this.finishCancelled(owner);
|
|
966
|
+
return;
|
|
967
|
+
case 'failed': {
|
|
968
|
+
const error = normalizeCoreError(notification.error, owner.operation);
|
|
969
|
+
if (owner.cancelling) {
|
|
970
|
+
owner.failure ??= { error };
|
|
971
|
+
await this.finishFailure(owner, owner.failure.error);
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
await this.failOwner(owner, error);
|
|
975
|
+
}
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
default:
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
reportProgress(owner, completedUnits, totalUnits) {
|
|
983
|
+
if (completedUnits < 0n
|
|
984
|
+
|| totalUnits < 0n
|
|
985
|
+
|| completedUnits > totalUnits
|
|
986
|
+
|| (owner.lastProgress !== null
|
|
987
|
+
&& (completedUnits < owner.lastProgress.completedUnits
|
|
988
|
+
|| totalUnits !== owner.lastProgress.totalUnits))) {
|
|
989
|
+
throw new BotaSDKError('protocol_error', owner.operation);
|
|
990
|
+
}
|
|
991
|
+
owner.lastProgress = { completedUnits, totalUnits };
|
|
992
|
+
try {
|
|
993
|
+
owner.observer?.onProgress?.(completedUnits, totalUnits);
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
// Observer failures do not change device workflow state.
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
reportFirmwareProgress(owner, phase, completedBytes, totalBytes) {
|
|
1000
|
+
const previous = owner.lastFirmwareProgress;
|
|
1001
|
+
if (completedBytes < 0n
|
|
1002
|
+
|| totalBytes < 0n
|
|
1003
|
+
|| completedBytes > totalBytes
|
|
1004
|
+
|| (previous !== null
|
|
1005
|
+
&& (firmwarePhaseIndex(phase) < firmwarePhaseIndex(previous.phase)
|
|
1006
|
+
|| totalBytes !== previous.totalBytes
|
|
1007
|
+
|| (phase === previous.phase
|
|
1008
|
+
&& completedBytes < previous.completedBytes)))) {
|
|
1009
|
+
throw new BotaSDKError('protocol_error', owner.operation);
|
|
1010
|
+
}
|
|
1011
|
+
owner.lastFirmwareProgress = { phase, completedBytes, totalBytes };
|
|
1012
|
+
try {
|
|
1013
|
+
owner.observer?.onProgress?.(completedBytes, totalBytes);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
// Observer failures do not change device workflow state.
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async dispatchBleFailure(context, requestId, error) {
|
|
1020
|
+
await context.dispatch({
|
|
1021
|
+
requestId,
|
|
1022
|
+
kind: 'ble_failed',
|
|
1023
|
+
platformCode: platformCode(error),
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
async serializedCoreCall(body) {
|
|
1027
|
+
const result = this.coreDispatchTail.then(body);
|
|
1028
|
+
this.coreDispatchTail = result.then(() => undefined, () => undefined);
|
|
1029
|
+
return await result;
|
|
1030
|
+
}
|
|
1031
|
+
dispatchCurrentOwnerEvent(owner, generation, event) {
|
|
1032
|
+
const effects = this.core.dispatch(event);
|
|
1033
|
+
const status = this.core.status();
|
|
1034
|
+
if (status.kind === 'completed'
|
|
1035
|
+
&& publicOperation(status.operation) === owner.operation) {
|
|
1036
|
+
const request = owner.requests.get(event.requestId);
|
|
1037
|
+
if (request
|
|
1038
|
+
&& request.generation === generation
|
|
1039
|
+
&& equalBytes(request.cancellationId, owner.cancellationId)) {
|
|
1040
|
+
owner.completionEvidence = {
|
|
1041
|
+
requestId: event.requestId,
|
|
1042
|
+
generation,
|
|
1043
|
+
cancellationId: request.cancellationId.slice(),
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return effects;
|
|
1048
|
+
}
|
|
1049
|
+
hasCompletionEvidence(owner) {
|
|
1050
|
+
const evidence = owner.completionEvidence;
|
|
1051
|
+
return evidence !== null
|
|
1052
|
+
&& evidence.generation === owner.generation
|
|
1053
|
+
&& owner.requests.has(evidence.requestId)
|
|
1054
|
+
&& equalBytes(evidence.cancellationId, owner.cancellationId);
|
|
1055
|
+
}
|
|
1056
|
+
enterCancellation(owner) {
|
|
1057
|
+
if (owner.cancelPromise)
|
|
1058
|
+
return owner.cancelPromise;
|
|
1059
|
+
const cancellation = deferred();
|
|
1060
|
+
owner.cancelPromise = cancellation.promise;
|
|
1061
|
+
const started = owner.failure
|
|
1062
|
+
? this.cancelAfterFailure(owner, owner.failure.error)
|
|
1063
|
+
: this.cancelOwner(owner);
|
|
1064
|
+
void started.then(() => cancellation.resolve(undefined), cancellation.reject);
|
|
1065
|
+
return cancellation.promise;
|
|
1066
|
+
}
|
|
1067
|
+
async cancelOwner(owner) {
|
|
1068
|
+
if (owner.terminal)
|
|
1069
|
+
return Promise.resolve();
|
|
1070
|
+
if (owner.completing) {
|
|
1071
|
+
await owner.result.promise.catch(() => undefined);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (await this.confirmationAttemptedOrClaimCancellation(owner)) {
|
|
1075
|
+
await owner.result.promise;
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
this.beginCancellation(owner);
|
|
1079
|
+
let cleanupError = null;
|
|
1080
|
+
try {
|
|
1081
|
+
await this.cleanupAndSettle(owner, true);
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
cleanupError = error;
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
const effects = await this.serializedCoreCall(() => this.core.cancel(owner.cancellationId.slice()));
|
|
1088
|
+
this.enqueueEffects(owner, effects, owner.generation);
|
|
1089
|
+
}
|
|
1090
|
+
catch (error) {
|
|
1091
|
+
await this.finishFailure(owner, cleanupError ?? error);
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
await owner.result.promise;
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
if (cleanupError === null
|
|
1098
|
+
&& error instanceof BotaSDKError
|
|
1099
|
+
&& error.code === 'cancelled') {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
throw normalizeRuntimeError(cleanupError ?? error, owner.operation);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
finishSuccess(owner, priorError) {
|
|
1106
|
+
if (priorError !== undefined) {
|
|
1107
|
+
owner.completionFailure ??= { error: priorError };
|
|
1108
|
+
}
|
|
1109
|
+
if (owner.completionPromise)
|
|
1110
|
+
return owner.completionPromise;
|
|
1111
|
+
if (owner.terminal)
|
|
1112
|
+
return Promise.resolve();
|
|
1113
|
+
if (owner.failure) {
|
|
1114
|
+
return this.finishFailure(owner, owner.failure.error);
|
|
1115
|
+
}
|
|
1116
|
+
owner.completing = true;
|
|
1117
|
+
owner.completionPromise = (async () => {
|
|
1118
|
+
try {
|
|
1119
|
+
await owner.completionHandoff?.({
|
|
1120
|
+
notifications: [...owner.notifications],
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
catch (error) {
|
|
1124
|
+
owner.completionFailure ??= { error };
|
|
1125
|
+
}
|
|
1126
|
+
owner.terminal = true;
|
|
1127
|
+
discardQueuedEffects(owner.queue);
|
|
1128
|
+
try {
|
|
1129
|
+
await this.cleanupAndSettle(owner, false);
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
owner.completionFailure ??= { error };
|
|
1133
|
+
}
|
|
1134
|
+
this.releaseOwner(owner);
|
|
1135
|
+
if (owner.completionFailure) {
|
|
1136
|
+
owner.result.reject(normalizeRuntimeError(owner.completionFailure.error, owner.operation));
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
owner.result.resolve({ notifications: [...owner.notifications] });
|
|
1140
|
+
}
|
|
1141
|
+
})();
|
|
1142
|
+
return owner.completionPromise;
|
|
1143
|
+
}
|
|
1144
|
+
async finishCancelled(owner) {
|
|
1145
|
+
if (owner.terminal)
|
|
1146
|
+
return;
|
|
1147
|
+
owner.terminal = true;
|
|
1148
|
+
discardQueuedEffects(owner.queue);
|
|
1149
|
+
owner.cancellationAbortController?.abort();
|
|
1150
|
+
let cleanupError = null;
|
|
1151
|
+
try {
|
|
1152
|
+
await this.cleanupAndSettle(owner, true);
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
cleanupError = error;
|
|
1156
|
+
}
|
|
1157
|
+
this.releaseOwner(owner);
|
|
1158
|
+
if (owner.failure) {
|
|
1159
|
+
owner.result.reject(normalizeRuntimeError(owner.failure.error, owner.operation));
|
|
1160
|
+
}
|
|
1161
|
+
else if (cleanupError !== null) {
|
|
1162
|
+
owner.result.reject(normalizeRuntimeError(cleanupError, owner.operation));
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
owner.result.reject(new BotaSDKError('cancelled', owner.operation));
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
failOwner(owner, error) {
|
|
1169
|
+
if (owner.terminal)
|
|
1170
|
+
return Promise.resolve();
|
|
1171
|
+
if (this.hasCompletionEvidence(owner)) {
|
|
1172
|
+
return this.finishSuccess(owner, error);
|
|
1173
|
+
}
|
|
1174
|
+
if (owner.failurePromise)
|
|
1175
|
+
return owner.failurePromise;
|
|
1176
|
+
owner.failure = { error };
|
|
1177
|
+
owner.failurePromise = owner.cancelling
|
|
1178
|
+
? this.finishFailure(owner, error)
|
|
1179
|
+
: this.enterCancellation(owner);
|
|
1180
|
+
return owner.failurePromise;
|
|
1181
|
+
}
|
|
1182
|
+
async cancelAfterFailure(owner, error) {
|
|
1183
|
+
if (await this.confirmationAttemptedOrClaimCancellation(owner)) {
|
|
1184
|
+
await this.finishFailure(owner, error);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
this.beginCancellation(owner);
|
|
1188
|
+
await this.cleanupAndSettle(owner, true).catch(() => undefined);
|
|
1189
|
+
let effects;
|
|
1190
|
+
try {
|
|
1191
|
+
effects = await this.serializedCoreCall(() => this.core.cancel(owner.cancellationId.slice()));
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
await this.finishFailure(owner, error);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
try {
|
|
1198
|
+
await this.executeCancellationEffects(owner, effects, owner.generation);
|
|
1199
|
+
}
|
|
1200
|
+
catch {
|
|
1201
|
+
// The originating operation error remains the public terminal failure.
|
|
1202
|
+
}
|
|
1203
|
+
if (!owner.terminal)
|
|
1204
|
+
await this.finishFailure(owner, error);
|
|
1205
|
+
}
|
|
1206
|
+
async executeCancellationEffects(owner, effects, generation) {
|
|
1207
|
+
if (owner.inlineEffects) {
|
|
1208
|
+
throw new BotaSDKError('internal_error', owner.operation);
|
|
1209
|
+
}
|
|
1210
|
+
const queue = [];
|
|
1211
|
+
const failures = [];
|
|
1212
|
+
owner.inlineEffects = queue;
|
|
1213
|
+
try {
|
|
1214
|
+
this.enqueueEffects(owner, effects, generation);
|
|
1215
|
+
while (queue.length > 0 && !owner.terminal) {
|
|
1216
|
+
const queued = queue.shift();
|
|
1217
|
+
if (!queued)
|
|
1218
|
+
continue;
|
|
1219
|
+
if (queued.generation !== owner.generation) {
|
|
1220
|
+
scrubTransientEffect(queued.envelope);
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
try {
|
|
1224
|
+
await this.executeEffect(owner, queued);
|
|
1225
|
+
}
|
|
1226
|
+
catch (error) {
|
|
1227
|
+
failures.push(error);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
finally {
|
|
1232
|
+
discardQueuedEffects(queue);
|
|
1233
|
+
owner.inlineEffects = null;
|
|
1234
|
+
}
|
|
1235
|
+
if (failures.length > 0)
|
|
1236
|
+
throw failures[0];
|
|
1237
|
+
}
|
|
1238
|
+
async finishFailure(owner, error) {
|
|
1239
|
+
if (owner.terminal)
|
|
1240
|
+
return;
|
|
1241
|
+
owner.terminal = true;
|
|
1242
|
+
owner.abortController.abort();
|
|
1243
|
+
owner.cancellationAbortController?.abort();
|
|
1244
|
+
discardQueuedEffects(owner.queue);
|
|
1245
|
+
await this.cleanupAndSettle(owner, true).catch(() => undefined);
|
|
1246
|
+
this.releaseOwner(owner);
|
|
1247
|
+
owner.result.reject(normalizeRuntimeError(error, owner.operation));
|
|
1248
|
+
}
|
|
1249
|
+
beginCancellation(owner) {
|
|
1250
|
+
owner.cancelling = true;
|
|
1251
|
+
owner.generation += 1;
|
|
1252
|
+
owner.abortController.abort();
|
|
1253
|
+
discardQueuedEffects(owner.queue);
|
|
1254
|
+
owner.cancellationAbortController = new AbortController();
|
|
1255
|
+
owner.cancellationGeneration = owner.generation;
|
|
1256
|
+
}
|
|
1257
|
+
async confirmationAttemptedOrClaimCancellation(owner) {
|
|
1258
|
+
for (const host of uniqueHosts(owner.hosts)) {
|
|
1259
|
+
if (await host.confirmationAttemptedOrClaimCancellation?.(owner.cancellationId.slice()))
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
async cleanupAndSettle(owner, cancelHosts) {
|
|
1265
|
+
let failure = null;
|
|
1266
|
+
try {
|
|
1267
|
+
await this.settlePendingGattWrites(owner);
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
failure = error;
|
|
1271
|
+
}
|
|
1272
|
+
try {
|
|
1273
|
+
await this.cleanupOwner(owner, cancelHosts);
|
|
1274
|
+
}
|
|
1275
|
+
catch (error) {
|
|
1276
|
+
if (failure === null)
|
|
1277
|
+
failure = error;
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
await this.settlePendingGattSetups(owner);
|
|
1281
|
+
}
|
|
1282
|
+
catch (error) {
|
|
1283
|
+
if (failure === null)
|
|
1284
|
+
failure = error;
|
|
1285
|
+
}
|
|
1286
|
+
try {
|
|
1287
|
+
await this.settlePendingExternalOperations(owner);
|
|
1288
|
+
}
|
|
1289
|
+
catch (error) {
|
|
1290
|
+
if (failure === null)
|
|
1291
|
+
failure = error;
|
|
1292
|
+
}
|
|
1293
|
+
if (failure !== null)
|
|
1294
|
+
throw failure;
|
|
1295
|
+
}
|
|
1296
|
+
cleanupOwner(owner, cancelHosts) {
|
|
1297
|
+
if (owner.cleanupPromise)
|
|
1298
|
+
return owner.cleanupPromise;
|
|
1299
|
+
owner.cleanupPromise = (async () => {
|
|
1300
|
+
const failures = [];
|
|
1301
|
+
if (cancelHosts) {
|
|
1302
|
+
const hosts = uniqueHosts(owner.hosts);
|
|
1303
|
+
for (const host of hosts) {
|
|
1304
|
+
try {
|
|
1305
|
+
await host.cancel();
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
failures.push(error);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
for (const cleanup of [...owner.cleanups].reverse()) {
|
|
1313
|
+
try {
|
|
1314
|
+
await cleanup();
|
|
1315
|
+
}
|
|
1316
|
+
catch (error) {
|
|
1317
|
+
failures.push(error);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
owner.cleanups.length = 0;
|
|
1321
|
+
for (const timer of owner.timers.values())
|
|
1322
|
+
clearTimeout(timer);
|
|
1323
|
+
owner.timers.clear();
|
|
1324
|
+
owner.subscriptions.clear();
|
|
1325
|
+
if (failures.length > 0)
|
|
1326
|
+
throw failures[0];
|
|
1327
|
+
})();
|
|
1328
|
+
return owner.cleanupPromise;
|
|
1329
|
+
}
|
|
1330
|
+
releaseOwner(owner) {
|
|
1331
|
+
if (this.activeOwner === owner)
|
|
1332
|
+
this.activeOwner = null;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
export function createBrowserPersistenceHost(storage, now = Date.now) {
|
|
1336
|
+
return {
|
|
1337
|
+
execute: async (envelope, context) => {
|
|
1338
|
+
switch (envelope.effect.kind) {
|
|
1339
|
+
case 'persistence_load_checkpoint': {
|
|
1340
|
+
const checkpoint = storage
|
|
1341
|
+
? await storage.loadWorkflowCheckpoint(context.operationId)
|
|
1342
|
+
: null;
|
|
1343
|
+
return {
|
|
1344
|
+
requestId: envelope.requestId,
|
|
1345
|
+
kind: 'checkpoint_loaded',
|
|
1346
|
+
checkpoint: checkpoint,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
case 'persistence_save_checkpoint':
|
|
1350
|
+
if (storage) {
|
|
1351
|
+
await storage.saveWorkflowCheckpoint(context.operationId, envelope.effect.checkpoint);
|
|
1352
|
+
}
|
|
1353
|
+
return { requestId: envelope.requestId, kind: 'checkpoint_saved' };
|
|
1354
|
+
case 'persistence_delete_checkpoint':
|
|
1355
|
+
if (storage) {
|
|
1356
|
+
await storage.deleteWorkflowCheckpoint(context.operationId);
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
case 'persistence_save_connection_identity':
|
|
1360
|
+
if (storage) {
|
|
1361
|
+
await storage.saveVerifiedDevice({
|
|
1362
|
+
schemaVersion: 1,
|
|
1363
|
+
serialNumber: envelope.effect.serialNumber,
|
|
1364
|
+
browserDeviceId: envelope.effect.candidate.peripheralId,
|
|
1365
|
+
name: envelope.effect.candidate.name,
|
|
1366
|
+
updatedAtEpochMs: now(),
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
requestId: envelope.requestId,
|
|
1371
|
+
kind: 'connection_identity_saved',
|
|
1372
|
+
};
|
|
1373
|
+
default:
|
|
1374
|
+
throw new BotaSDKError('internal_error', publicOperation(envelope.operation));
|
|
1375
|
+
}
|
|
1376
|
+
},
|
|
1377
|
+
cancel: async () => undefined,
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function deferred() {
|
|
1381
|
+
let resolve;
|
|
1382
|
+
let reject;
|
|
1383
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
1384
|
+
resolve = resolvePromise;
|
|
1385
|
+
reject = rejectPromise;
|
|
1386
|
+
});
|
|
1387
|
+
return { promise, resolve, reject };
|
|
1388
|
+
}
|
|
1389
|
+
function equalBytes(left, right) {
|
|
1390
|
+
if (left.byteLength !== right.byteLength)
|
|
1391
|
+
return false;
|
|
1392
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
1393
|
+
if (left[index] !== right[index])
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1398
|
+
function platformCode(error) {
|
|
1399
|
+
if (typeof error === 'object'
|
|
1400
|
+
&& error !== null
|
|
1401
|
+
&& 'code' in error
|
|
1402
|
+
&& typeof error.code === 'number') {
|
|
1403
|
+
return error.code;
|
|
1404
|
+
}
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
function discardQueuedEffects(queue) {
|
|
1408
|
+
for (const queued of queue)
|
|
1409
|
+
scrubTransientEffect(queued.envelope);
|
|
1410
|
+
queue.length = 0;
|
|
1411
|
+
}
|
|
1412
|
+
function scrubTransientEffect(envelope) {
|
|
1413
|
+
const effect = envelope.effect;
|
|
1414
|
+
if (effect.kind === 'ble_write') {
|
|
1415
|
+
effect.payload.fill(0);
|
|
1416
|
+
}
|
|
1417
|
+
else if (effect.kind === 'host_material_prepare_provisioning') {
|
|
1418
|
+
effect.nonce.fill(0);
|
|
1419
|
+
effect.devicePublicKey.fill(0);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
function uniqueHosts(hosts) {
|
|
1423
|
+
return [...new Set([
|
|
1424
|
+
hosts.persistence,
|
|
1425
|
+
hosts.recordingSink,
|
|
1426
|
+
hosts.network,
|
|
1427
|
+
hosts.firmwareBlob,
|
|
1428
|
+
hosts.hostMaterial,
|
|
1429
|
+
hosts.encryptedUploadV2,
|
|
1430
|
+
].filter((host) => host !== undefined))];
|
|
1431
|
+
}
|
|
1432
|
+
function firmwarePhaseIndex(phase) {
|
|
1433
|
+
switch (phase) {
|
|
1434
|
+
case 'downloading':
|
|
1435
|
+
return 0;
|
|
1436
|
+
case 'awaiting_device':
|
|
1437
|
+
return 1;
|
|
1438
|
+
case 'transferring':
|
|
1439
|
+
return 2;
|
|
1440
|
+
case 'verifying':
|
|
1441
|
+
return 3;
|
|
1442
|
+
case 'rebooting':
|
|
1443
|
+
return 4;
|
|
1444
|
+
case 'reconnecting':
|
|
1445
|
+
return 5;
|
|
1446
|
+
case 'complete':
|
|
1447
|
+
return 6;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function publicOperation(operation) {
|
|
1451
|
+
switch (operation) {
|
|
1452
|
+
case 'connect':
|
|
1453
|
+
return 'connect';
|
|
1454
|
+
case 'reconnect':
|
|
1455
|
+
return 'reconnect';
|
|
1456
|
+
case 'provision':
|
|
1457
|
+
return 'provision';
|
|
1458
|
+
case 'transfer_recording':
|
|
1459
|
+
return 'transfer_recording';
|
|
1460
|
+
case 'upload':
|
|
1461
|
+
return 'upload';
|
|
1462
|
+
case 'update_firmware':
|
|
1463
|
+
return 'update_firmware';
|
|
1464
|
+
case 'read_device_logs':
|
|
1465
|
+
return 'read_device_logs';
|
|
1466
|
+
default:
|
|
1467
|
+
return 'unknown';
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function operationFromId(operationId) {
|
|
1471
|
+
const prefix = operationId.split(':', 1)[0];
|
|
1472
|
+
switch (prefix) {
|
|
1473
|
+
case 'connect':
|
|
1474
|
+
return 'connect';
|
|
1475
|
+
case 'reconnect':
|
|
1476
|
+
return 'reconnect';
|
|
1477
|
+
case 'provision':
|
|
1478
|
+
return 'provision';
|
|
1479
|
+
case 'transfer_recording':
|
|
1480
|
+
return 'transfer_recording';
|
|
1481
|
+
case 'upload':
|
|
1482
|
+
return 'upload';
|
|
1483
|
+
case 'update_firmware':
|
|
1484
|
+
return 'update_firmware';
|
|
1485
|
+
case 'read_device_logs':
|
|
1486
|
+
return 'read_device_logs';
|
|
1487
|
+
default:
|
|
1488
|
+
return 'unknown';
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function normalizeRuntimeError(error, operation) {
|
|
1492
|
+
if (error instanceof BotaSDKError)
|
|
1493
|
+
return error;
|
|
1494
|
+
if (error instanceof BrowserStorageError) {
|
|
1495
|
+
return new BotaSDKError(error.code, operation, { cause: error });
|
|
1496
|
+
}
|
|
1497
|
+
if (error instanceof BrowserTransportError) {
|
|
1498
|
+
const code = error.code === 'disconnected'
|
|
1499
|
+
? 'device_disconnected'
|
|
1500
|
+
: error.code === 'permission_denied'
|
|
1501
|
+
? 'permission_denied'
|
|
1502
|
+
: error.code === 'picker_cancelled'
|
|
1503
|
+
? 'picker_cancelled'
|
|
1504
|
+
: 'bluetooth_unavailable';
|
|
1505
|
+
return new BotaSDKError(code, operation, { cause: error });
|
|
1506
|
+
}
|
|
1507
|
+
return normalizeCoreError(error, operation);
|
|
1508
|
+
}
|