@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +517 -0
  3. package/dist/capabilities.d.ts +8 -0
  4. package/dist/capabilities.js +21 -0
  5. package/dist/client.d.ts +39 -0
  6. package/dist/client.js +153 -0
  7. package/dist/controlManager.d.ts +38 -0
  8. package/dist/controlManager.js +344 -0
  9. package/dist/core.d.ts +698 -0
  10. package/dist/core.js +13 -0
  11. package/dist/deviceManager.d.ts +52 -0
  12. package/dist/deviceManager.js +491 -0
  13. package/dist/encryptedUploadV2Host.d.ts +239 -0
  14. package/dist/encryptedUploadV2Host.js +2136 -0
  15. package/dist/errors.d.ts +24 -0
  16. package/dist/errors.js +230 -0
  17. package/dist/gatt.d.ts +39 -0
  18. package/dist/gatt.js +57 -0
  19. package/dist/generated/bota_device_sdk_core.d.ts +202 -0
  20. package/dist/generated/bota_device_sdk_core.js +1025 -0
  21. package/dist/generated/bota_device_sdk_core_bg.wasm +0 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.js +2 -0
  24. package/dist/indexedDbWorkflowStore.d.ts +52 -0
  25. package/dist/indexedDbWorkflowStore.js +763 -0
  26. package/dist/logManager.d.ts +24 -0
  27. package/dist/logManager.js +205 -0
  28. package/dist/models.d.ts +217 -0
  29. package/dist/models.js +1 -0
  30. package/dist/opfsBlobStore.d.ts +12 -0
  31. package/dist/opfsBlobStore.js +250 -0
  32. package/dist/otaManager.d.ts +49 -0
  33. package/dist/otaManager.js +951 -0
  34. package/dist/providerCancellation.d.ts +2 -0
  35. package/dist/providerCancellation.js +23 -0
  36. package/dist/providers.d.ts +138 -0
  37. package/dist/providers.js +1 -0
  38. package/dist/provisioningManager.d.ts +48 -0
  39. package/dist/provisioningManager.js +753 -0
  40. package/dist/recordingManager.d.ts +53 -0
  41. package/dist/recordingManager.js +1397 -0
  42. package/dist/storage.d.ts +103 -0
  43. package/dist/storage.js +60 -0
  44. package/dist/transport.d.ts +33 -0
  45. package/dist/transport.js +8 -0
  46. package/dist/wasmCore.d.ts +3 -0
  47. package/dist/wasmCore.js +1651 -0
  48. package/dist/webBluetoothTransport.d.ts +23 -0
  49. package/dist/webBluetoothTransport.js +369 -0
  50. package/dist/wifiManager.d.ts +54 -0
  51. package/dist/wifiManager.js +528 -0
  52. package/dist/workflowRuntime.d.ts +115 -0
  53. package/dist/workflowRuntime.js +1508 -0
  54. package/package.json +46 -0
@@ -0,0 +1,23 @@
1
+ import { type BrowserBluetoothTransport, type BrowserDeviceHandle, type BrowserNotification, type BrowserSubscription } from './transport.ts';
2
+ export declare class WebBluetoothTransport implements BrowserBluetoothTransport {
3
+ readonly maximumWriteValueLength = 128;
4
+ private readonly connections;
5
+ private readonly connectionTeardowns;
6
+ get isSupported(): boolean;
7
+ get supportsAuthorizedDevices(): boolean;
8
+ requestDevice(): Promise<BrowserDeviceHandle>;
9
+ getAuthorizedDevices(): Promise<BrowserDeviceHandle[]>;
10
+ connect(device: BrowserDeviceHandle): Promise<void>;
11
+ discoverServices(device: BrowserDeviceHandle): Promise<void>;
12
+ read(device: BrowserDeviceHandle, serviceUuid: string, characteristicUuid: string): Promise<Uint8Array>;
13
+ write(device: BrowserDeviceHandle, serviceUuid: string, characteristicUuid: string, value: Uint8Array, withResponse: boolean): Promise<void>;
14
+ subscribe(device: BrowserDeviceHandle, serviceUuid: string, characteristicUuid: string, listener: (notification: BrowserNotification) => void): Promise<BrowserSubscription>;
15
+ disconnect(device: BrowserDeviceHandle): Promise<void>;
16
+ onDisconnected(device: BrowserDeviceHandle, listener: () => void): () => void;
17
+ private connection;
18
+ private assertCurrent;
19
+ private characteristic;
20
+ private invalidateConnection;
21
+ private detachConnection;
22
+ private clearConnectionTeardown;
23
+ }
@@ -0,0 +1,369 @@
1
+ import { canonicalGattUuid, FOREGROUND_GATT_SERVICES, } from "./gatt.js";
2
+ import { BrowserTransportError, } from "./transport.js";
3
+ const MAXIMUM_WRITE_VALUE_LENGTH = 128;
4
+ export class WebBluetoothTransport {
5
+ maximumWriteValueLength = MAXIMUM_WRITE_VALUE_LENGTH;
6
+ connections = new Map();
7
+ connectionTeardowns = new Map();
8
+ get isSupported() {
9
+ return typeof browserBluetooth()?.requestDevice === 'function';
10
+ }
11
+ get supportsAuthorizedDevices() {
12
+ return this.isSupported && typeof browserBluetooth()?.getDevices === 'function';
13
+ }
14
+ async requestDevice() {
15
+ const bluetooth = browserBluetooth();
16
+ if (!bluetooth || typeof bluetooth.requestDevice !== 'function') {
17
+ throw new BrowserTransportError('unavailable');
18
+ }
19
+ try {
20
+ return deviceHandle(await bluetooth.requestDevice({
21
+ filters: [{ namePrefix: 'Bota' }],
22
+ optionalServices: [...FOREGROUND_GATT_SERVICES],
23
+ }));
24
+ }
25
+ catch (error) {
26
+ throw sanitizeDomError(error, 'picker');
27
+ }
28
+ }
29
+ async getAuthorizedDevices() {
30
+ const bluetooth = browserBluetooth();
31
+ if (!bluetooth || typeof bluetooth.getDevices !== 'function') {
32
+ throw new BrowserTransportError('unavailable');
33
+ }
34
+ try {
35
+ return (await bluetooth.getDevices()).map(deviceHandle);
36
+ }
37
+ catch (error) {
38
+ throw sanitizeDomError(error, 'availability');
39
+ }
40
+ }
41
+ async connect(device) {
42
+ const native = nativeDevice(device);
43
+ if (!native.gatt)
44
+ throw new BrowserTransportError('unavailable');
45
+ try {
46
+ const existing = this.connections.get(device.id);
47
+ if (existing) {
48
+ await this.detachConnection(device.id, existing);
49
+ }
50
+ else {
51
+ await this.connectionTeardowns.get(device.id);
52
+ }
53
+ const server = await native.gatt.connect();
54
+ if (!server.connected)
55
+ throw new BrowserTransportError('disconnected');
56
+ let cache;
57
+ const disconnectedListener = () => {
58
+ if (this.connections.get(device.id) === cache) {
59
+ this.invalidateConnection(device.id);
60
+ }
61
+ };
62
+ cache = {
63
+ native,
64
+ server,
65
+ services: new Map(),
66
+ characteristics: new Map(),
67
+ notificationSessions: new Map(),
68
+ disconnectedListener,
69
+ };
70
+ this.connections.set(device.id, cache);
71
+ native.addEventListener('gattserverdisconnected', disconnectedListener);
72
+ }
73
+ catch (error) {
74
+ throw sanitizeDomError(error, 'gatt');
75
+ }
76
+ }
77
+ async discoverServices(device) {
78
+ const cache = this.connection(device);
79
+ try {
80
+ const services = await cache.server.getPrimaryServices();
81
+ this.assertCurrent(device.id, cache);
82
+ for (const service of services) {
83
+ cache.services.set(canonicalGattUuid(service.uuid), service);
84
+ }
85
+ }
86
+ catch (error) {
87
+ throw sanitizeDomError(error, 'gatt');
88
+ }
89
+ }
90
+ async read(device, serviceUuid, characteristicUuid) {
91
+ try {
92
+ const characteristic = await this.characteristic(device, serviceUuid, characteristicUuid);
93
+ return cloneDataView(await characteristic.readValue());
94
+ }
95
+ catch (error) {
96
+ throw sanitizeDomError(error, 'characteristic');
97
+ }
98
+ }
99
+ async write(device, serviceUuid, characteristicUuid, value, withResponse) {
100
+ let ownedValue = null;
101
+ try {
102
+ const characteristic = await this.characteristic(device, serviceUuid, characteristicUuid);
103
+ ownedValue = new Uint8Array(value.length);
104
+ ownedValue.set(value);
105
+ if (withResponse) {
106
+ await characteristic.writeValueWithResponse(ownedValue);
107
+ }
108
+ else {
109
+ await characteristic.writeValueWithoutResponse(ownedValue);
110
+ }
111
+ }
112
+ catch (error) {
113
+ throw sanitizeDomError(error, 'characteristic');
114
+ }
115
+ finally {
116
+ ownedValue?.fill(0);
117
+ }
118
+ }
119
+ async subscribe(device, serviceUuid, characteristicUuid, listener) {
120
+ const cache = this.connection(device);
121
+ let subscription = null;
122
+ try {
123
+ const characteristic = await this.characteristic(device, serviceUuid, characteristicUuid);
124
+ this.assertCurrent(device.id, cache);
125
+ let session = cache.notificationSessions.get(characteristic);
126
+ if (session?.isStopping) {
127
+ await session.teardown();
128
+ this.assertCurrent(device.id, cache);
129
+ session = cache.notificationSessions.get(characteristic);
130
+ }
131
+ if (!session) {
132
+ let created;
133
+ created = new WebBluetoothNotificationSession(characteristic, () => {
134
+ if (cache.notificationSessions.get(characteristic) === created) {
135
+ cache.notificationSessions.delete(characteristic);
136
+ }
137
+ });
138
+ cache.notificationSessions.set(characteristic, created);
139
+ session = created;
140
+ }
141
+ subscription = session.add(listener);
142
+ await session.start();
143
+ this.assertCurrent(device.id, cache);
144
+ return subscription;
145
+ }
146
+ catch (error) {
147
+ await subscription?.remove().catch(() => undefined);
148
+ throw sanitizeDomError(error, 'characteristic');
149
+ }
150
+ }
151
+ async disconnect(device) {
152
+ const native = nativeDevice(device);
153
+ const cache = this.connections.get(device.id);
154
+ const teardown = cache
155
+ ? this.detachConnection(device.id, cache)
156
+ : this.connectionTeardowns.get(device.id);
157
+ let teardownError = null;
158
+ try {
159
+ await teardown;
160
+ }
161
+ catch (error) {
162
+ teardownError = error;
163
+ }
164
+ try {
165
+ native.gatt?.disconnect();
166
+ }
167
+ catch (error) {
168
+ if (!teardownError)
169
+ teardownError = sanitizeDomError(error, 'gatt');
170
+ }
171
+ if (teardownError)
172
+ throw teardownError;
173
+ }
174
+ onDisconnected(device, listener) {
175
+ const native = nativeDevice(device);
176
+ native.addEventListener('gattserverdisconnected', listener);
177
+ return () => native.removeEventListener('gattserverdisconnected', listener);
178
+ }
179
+ connection(device) {
180
+ const cache = this.connections.get(device.id);
181
+ if (!cache?.server.connected) {
182
+ this.invalidateConnection(device.id);
183
+ throw new BrowserTransportError('disconnected');
184
+ }
185
+ return cache;
186
+ }
187
+ assertCurrent(deviceId, cache) {
188
+ if (this.connections.get(deviceId) !== cache || !cache.server.connected) {
189
+ throw new BrowserTransportError('disconnected');
190
+ }
191
+ }
192
+ async characteristic(device, serviceUuid, characteristicUuid) {
193
+ const cache = this.connection(device);
194
+ const serviceKey = canonicalGattUuid(serviceUuid);
195
+ const characteristicKey = `${serviceKey}:${canonicalGattUuid(characteristicUuid)}`;
196
+ const cached = cache.characteristics.get(characteristicKey);
197
+ if (cached)
198
+ return cached;
199
+ let service = cache.services.get(serviceKey);
200
+ if (!service) {
201
+ service = await cache.server.getPrimaryService(serviceUuid);
202
+ this.assertCurrent(device.id, cache);
203
+ cache.services.set(serviceKey, service);
204
+ }
205
+ const characteristic = await service.getCharacteristic(characteristicUuid);
206
+ this.assertCurrent(device.id, cache);
207
+ cache.characteristics.set(characteristicKey, characteristic);
208
+ return characteristic;
209
+ }
210
+ invalidateConnection(deviceId) {
211
+ const cache = this.connections.get(deviceId);
212
+ if (!cache)
213
+ return;
214
+ void this.detachConnection(deviceId, cache).catch(() => undefined);
215
+ }
216
+ detachConnection(deviceId, cache) {
217
+ if (this.connections.get(deviceId) !== cache) {
218
+ return this.connectionTeardowns.get(deviceId) ?? Promise.resolve();
219
+ }
220
+ this.connections.delete(deviceId);
221
+ cache.native.removeEventListener('gattserverdisconnected', cache.disconnectedListener);
222
+ const teardowns = [...cache.notificationSessions.values()].map((session) => session.teardown());
223
+ const teardown = Promise.allSettled(teardowns).then((outcomes) => {
224
+ const failure = outcomes.find((outcome) => outcome.status === 'rejected');
225
+ if (failure?.status === 'rejected')
226
+ throw failure.reason;
227
+ });
228
+ this.connectionTeardowns.set(deviceId, teardown);
229
+ void teardown.then(() => this.clearConnectionTeardown(deviceId, teardown), () => this.clearConnectionTeardown(deviceId, teardown));
230
+ cache.notificationSessions.clear();
231
+ cache.characteristics.clear();
232
+ cache.services.clear();
233
+ return teardown;
234
+ }
235
+ clearConnectionTeardown(deviceId, teardown) {
236
+ if (this.connectionTeardowns.get(deviceId) === teardown) {
237
+ this.connectionTeardowns.delete(deviceId);
238
+ }
239
+ }
240
+ }
241
+ class WebBluetoothNotificationSession {
242
+ characteristic;
243
+ onStopped;
244
+ listeners = new Map();
245
+ eventListener;
246
+ startPromise = null;
247
+ stopPromise = null;
248
+ constructor(characteristic, onStopped) {
249
+ this.characteristic = characteristic;
250
+ this.onStopped = onStopped;
251
+ this.eventListener = () => {
252
+ if (!this.characteristic.value)
253
+ return;
254
+ for (const listener of [...this.listeners.values()]) {
255
+ try {
256
+ listener({
257
+ characteristicUuid: this.characteristic.uuid,
258
+ value: cloneDataView(this.characteristic.value),
259
+ });
260
+ }
261
+ catch {
262
+ // A consumer callback cannot block or escape the shared DOM listener.
263
+ }
264
+ }
265
+ };
266
+ characteristic.addEventListener('characteristicvaluechanged', this.eventListener);
267
+ }
268
+ get isStopping() {
269
+ return this.stopPromise !== null;
270
+ }
271
+ add(listener) {
272
+ if (this.stopPromise)
273
+ throw new BrowserTransportError('unavailable');
274
+ const subscription = new WebBluetoothSubscription(this);
275
+ this.listeners.set(subscription, listener);
276
+ return subscription;
277
+ }
278
+ start() {
279
+ this.startPromise ??= this.characteristic.startNotifications()
280
+ .then(() => undefined)
281
+ .catch((error) => {
282
+ throw sanitizeDomError(error, 'characteristic');
283
+ });
284
+ return this.startPromise;
285
+ }
286
+ remove(subscription) {
287
+ this.listeners.delete(subscription);
288
+ if (this.listeners.size > 0)
289
+ return Promise.resolve();
290
+ return this.teardown();
291
+ }
292
+ teardown() {
293
+ if (this.stopPromise)
294
+ return this.stopPromise;
295
+ this.characteristic.removeEventListener('characteristicvaluechanged', this.eventListener);
296
+ const stopping = this.startPromise
297
+ ? this.startPromise.then(() => this.characteristic.stopNotifications())
298
+ : Promise.resolve();
299
+ this.stopPromise = stopping
300
+ .then(() => undefined)
301
+ .catch((error) => {
302
+ throw sanitizeDomError(error, 'gatt');
303
+ })
304
+ .finally(this.onStopped);
305
+ for (const subscription of this.listeners.keys()) {
306
+ subscription.useTeardown(this.stopPromise);
307
+ }
308
+ this.listeners.clear();
309
+ return this.stopPromise;
310
+ }
311
+ }
312
+ class WebBluetoothSubscription {
313
+ session;
314
+ removePromise = null;
315
+ constructor(session) {
316
+ this.session = session;
317
+ }
318
+ remove() {
319
+ this.removePromise ??= this.session.remove(this);
320
+ return this.removePromise;
321
+ }
322
+ useTeardown(teardown) {
323
+ this.removePromise ??= teardown;
324
+ }
325
+ }
326
+ function browserBluetooth() {
327
+ if (typeof navigator === 'undefined')
328
+ return null;
329
+ return navigator.bluetooth ?? null;
330
+ }
331
+ function deviceHandle(device) {
332
+ return { id: device.id, name: device.name ?? null, nativeValue: device };
333
+ }
334
+ function nativeDevice(device) {
335
+ if (!device.nativeValue)
336
+ throw new BrowserTransportError('unavailable');
337
+ return device.nativeValue;
338
+ }
339
+ function cloneDataView(value) {
340
+ const copy = new Uint8Array(value.byteLength);
341
+ copy.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
342
+ return copy;
343
+ }
344
+ function sanitizeDomError(error, context) {
345
+ if (error instanceof BrowserTransportError)
346
+ return error;
347
+ const name = domErrorName(error);
348
+ if (context === 'picker' && (name === 'AbortError' || name === 'NotFoundError')) {
349
+ return new BrowserTransportError('picker_cancelled', { cause: error });
350
+ }
351
+ if (name === 'NotAllowedError' || name === 'SecurityError') {
352
+ return new BrowserTransportError('permission_denied', { cause: error });
353
+ }
354
+ if (context === 'characteristic' && name === 'NotFoundError') {
355
+ return new BrowserTransportError('characteristic_not_found', { cause: error });
356
+ }
357
+ if (name === 'InvalidStateError' || name === 'NetworkError') {
358
+ return new BrowserTransportError('disconnected', { cause: error });
359
+ }
360
+ if (name === 'NotReadableError' || name === 'NotSupportedError') {
361
+ return new BrowserTransportError('unavailable', { cause: error });
362
+ }
363
+ return new BrowserTransportError('unavailable', { cause: error });
364
+ }
365
+ function domErrorName(error) {
366
+ return typeof error === 'object' && error !== null && 'name' in error
367
+ ? String(error.name)
368
+ : '';
369
+ }
@@ -0,0 +1,54 @@
1
+ import type { CoreBridge } from './core.ts';
2
+ import { DeviceManager } from './deviceManager.ts';
3
+ import { type BotaOperation } from './errors.ts';
4
+ import type { WiFiConfigResult, WiFiCredentials, WiFiScanResult, WiFiStatusInfo, WiFiStatusSubscription } from './models.ts';
5
+ import { type BrowserBluetoothTransport, type BrowserDeviceHandle, type BrowserNotification } from './transport.ts';
6
+ import { BrowserWorkflowRuntime } from './workflowRuntime.ts';
7
+ interface WiFiManagerOptions {
8
+ core: CoreBridge;
9
+ transport: BrowserBluetoothTransport;
10
+ runtime: BrowserWorkflowRuntime;
11
+ devices: DeviceManager;
12
+ operationTimeoutMs?: number | undefined;
13
+ }
14
+ interface SubscriptionOptions<T> {
15
+ runtime: BrowserWorkflowRuntime;
16
+ transport: BrowserBluetoothTransport;
17
+ device: BrowserDeviceHandle;
18
+ serviceUuid: string;
19
+ characteristicUuid: string;
20
+ operation: BotaOperation;
21
+ signal: AbortSignal;
22
+ listener(notification: BrowserNotification): void;
23
+ beforeSubscribe?: (() => Promise<void>) | undefined;
24
+ body(): Promise<T>;
25
+ }
26
+ export declare class WiFiManager {
27
+ private readonly core;
28
+ private readonly transport;
29
+ private readonly runtime;
30
+ private readonly devices;
31
+ private readonly operationTimeoutMs;
32
+ private readonly removeDisconnectListener;
33
+ private activeOperation;
34
+ private statusOwner;
35
+ private destroyed;
36
+ private destroyPromise;
37
+ constructor(options: WiFiManagerOptions);
38
+ scanNetworks(): Promise<WiFiScanResult>;
39
+ configure(credentials: WiFiCredentials, grant: string): Promise<WiFiConfigResult>;
40
+ disconnect(): Promise<WiFiConfigResult>;
41
+ readStatus(): Promise<WiFiStatusInfo>;
42
+ subscribeToStatus(listener: (status: WiFiStatusInfo) => void): Promise<WiFiStatusSubscription>;
43
+ private subscribeToVerifiedStatus;
44
+ destroy(): Promise<void>;
45
+ private configureWithSubscription;
46
+ private runManaged;
47
+ private verifyConnectedDevice;
48
+ private requireConnectedDevice;
49
+ private normalizeFailure;
50
+ private closeStatusOwner;
51
+ private requireUsable;
52
+ }
53
+ export declare function withSubscription<T>(options: SubscriptionOptions<T>): Promise<T>;
54
+ export {};