@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,250 @@
|
|
|
1
|
+
import { BrowserStorageError, mapBrowserStorageError, validateStorageIdentifier, validateStorageNamespace, } from "./indexedDbWorkflowStore.js";
|
|
2
|
+
const DEFAULT_STREAM_CHUNK_SIZE = 64 * 1024;
|
|
3
|
+
const SDK_DIRECTORY = 'bota-app-sdk';
|
|
4
|
+
const SCHEMA_DIRECTORY = 'v1';
|
|
5
|
+
const inProcessBlobLocks = new Map();
|
|
6
|
+
export class OpfsBlobStore {
|
|
7
|
+
root;
|
|
8
|
+
createIntegrityHasher;
|
|
9
|
+
namespaceDirectoryName;
|
|
10
|
+
constructor(namespace, root, createIntegrityHasher) {
|
|
11
|
+
validateStorageNamespace(namespace);
|
|
12
|
+
this.root = root;
|
|
13
|
+
this.createIntegrityHasher = createIntegrityHasher;
|
|
14
|
+
this.namespaceDirectoryName = this.hashName(namespace);
|
|
15
|
+
}
|
|
16
|
+
async open(blobId) {
|
|
17
|
+
validateStorageIdentifier(blobId);
|
|
18
|
+
const fileName = this.hashName(blobId);
|
|
19
|
+
const lockName = [
|
|
20
|
+
SDK_DIRECTORY,
|
|
21
|
+
SCHEMA_DIRECTORY,
|
|
22
|
+
this.namespaceDirectoryName,
|
|
23
|
+
fileName,
|
|
24
|
+
].join(':');
|
|
25
|
+
return new OpfsBlobHandle(blobId, fileName, async () => await this.namespaceDirectory(), async (action) => await withBlobLock(lockName, action));
|
|
26
|
+
}
|
|
27
|
+
async clear() {
|
|
28
|
+
try {
|
|
29
|
+
const sdk = await this.root.getDirectoryHandle(SDK_DIRECTORY);
|
|
30
|
+
const version = await sdk.getDirectoryHandle(SCHEMA_DIRECTORY);
|
|
31
|
+
await version.removeEntry(this.namespaceDirectoryName, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (domExceptionName(error) === 'NotFoundError')
|
|
35
|
+
return;
|
|
36
|
+
throw mapBrowserStorageError(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async namespaceDirectory() {
|
|
40
|
+
try {
|
|
41
|
+
const sdk = await this.root.getDirectoryHandle(SDK_DIRECTORY, { create: true });
|
|
42
|
+
const version = await sdk.getDirectoryHandle(SCHEMA_DIRECTORY, { create: true });
|
|
43
|
+
return await version.getDirectoryHandle(this.namespaceDirectoryName, {
|
|
44
|
+
create: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw mapBrowserStorageError(error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
hashName(value) {
|
|
52
|
+
try {
|
|
53
|
+
const hasher = this.createIntegrityHasher();
|
|
54
|
+
hasher.update(new TextEncoder().encode(value));
|
|
55
|
+
const digest = hasher.sha256Snapshot();
|
|
56
|
+
if (digest.byteLength !== 32) {
|
|
57
|
+
throw new BrowserStorageError('storage_unavailable');
|
|
58
|
+
}
|
|
59
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw mapBrowserStorageError(error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
class OpfsBlobHandle {
|
|
67
|
+
id;
|
|
68
|
+
fileName;
|
|
69
|
+
directory;
|
|
70
|
+
withLock;
|
|
71
|
+
constructor(id, fileName, directory, withLock) {
|
|
72
|
+
this.id = id;
|
|
73
|
+
this.fileName = fileName;
|
|
74
|
+
this.directory = directory;
|
|
75
|
+
this.withLock = withLock;
|
|
76
|
+
}
|
|
77
|
+
async size() {
|
|
78
|
+
try {
|
|
79
|
+
return validFileSize((await (await this.fileHandle()).getFile()).size);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw mapBrowserStorageError(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async truncate(size) {
|
|
86
|
+
validateOffset(size);
|
|
87
|
+
await this.withLock(async () => {
|
|
88
|
+
const currentSize = await this.size();
|
|
89
|
+
if (size > currentSize)
|
|
90
|
+
throw new BrowserStorageError('resume_rejected');
|
|
91
|
+
if (size === currentSize)
|
|
92
|
+
return;
|
|
93
|
+
await this.withWritable(async (writable) => {
|
|
94
|
+
await writable.truncate(size);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async write(offset, bytes) {
|
|
99
|
+
validateOffset(offset);
|
|
100
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
101
|
+
throw new BrowserStorageError('invalid_input');
|
|
102
|
+
}
|
|
103
|
+
validateRange(offset, bytes.byteLength);
|
|
104
|
+
const copy = new Uint8Array(bytes);
|
|
105
|
+
await this.withLock(async () => {
|
|
106
|
+
const currentSize = await this.size();
|
|
107
|
+
if (offset !== currentSize) {
|
|
108
|
+
throw new BrowserStorageError('resume_rejected');
|
|
109
|
+
}
|
|
110
|
+
if (copy.byteLength === 0)
|
|
111
|
+
return;
|
|
112
|
+
await this.withWritable(async (writable) => {
|
|
113
|
+
await writable.seek(offset);
|
|
114
|
+
await writable.write(copy);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async read(offset, maximumLength) {
|
|
119
|
+
validateOffset(offset);
|
|
120
|
+
validateLength(maximumLength, true);
|
|
121
|
+
validateRange(offset, maximumLength);
|
|
122
|
+
if (maximumLength === 0)
|
|
123
|
+
return new Uint8Array();
|
|
124
|
+
try {
|
|
125
|
+
const file = await (await this.fileHandle()).getFile();
|
|
126
|
+
const size = validFileSize(file.size);
|
|
127
|
+
if (offset >= size)
|
|
128
|
+
return new Uint8Array();
|
|
129
|
+
const end = Math.min(size, offset + maximumLength);
|
|
130
|
+
return new Uint8Array(await file.slice(offset, end).arrayBuffer());
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
throw mapBrowserStorageError(error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async *stream(chunkSize = DEFAULT_STREAM_CHUNK_SIZE) {
|
|
137
|
+
validateLength(chunkSize, false);
|
|
138
|
+
const size = await this.size();
|
|
139
|
+
let offset = 0;
|
|
140
|
+
while (offset < size) {
|
|
141
|
+
const length = Math.min(chunkSize, size - offset);
|
|
142
|
+
yield await this.read(offset, length);
|
|
143
|
+
offset += length;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async delete() {
|
|
147
|
+
await this.withLock(async () => {
|
|
148
|
+
try {
|
|
149
|
+
const directory = await this.directory();
|
|
150
|
+
await directory.removeEntry(this.fileName);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (domExceptionName(error) === 'NotFoundError')
|
|
154
|
+
return;
|
|
155
|
+
throw mapBrowserStorageError(error);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
async fileHandle() {
|
|
160
|
+
const directory = await this.directory();
|
|
161
|
+
return await directory.getFileHandle(this.fileName, { create: true });
|
|
162
|
+
}
|
|
163
|
+
async withWritable(action) {
|
|
164
|
+
let writable = null;
|
|
165
|
+
try {
|
|
166
|
+
writable = await (await this.fileHandle()).createWritable({
|
|
167
|
+
keepExistingData: true,
|
|
168
|
+
});
|
|
169
|
+
await action(writable);
|
|
170
|
+
await writable.close();
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (writable) {
|
|
174
|
+
try {
|
|
175
|
+
await writable.abort();
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Preserve the original storage failure.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
throw mapBrowserStorageError(error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function withBlobLock(name, action) {
|
|
186
|
+
const lockManager = browserLockManager();
|
|
187
|
+
if (lockManager) {
|
|
188
|
+
try {
|
|
189
|
+
return await lockManager.request(name, action);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw mapBrowserStorageError(error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return await withInProcessBlobLock(name, action);
|
|
196
|
+
}
|
|
197
|
+
async function withInProcessBlobLock(name, action) {
|
|
198
|
+
const previous = inProcessBlobLocks.get(name) ?? Promise.resolve();
|
|
199
|
+
let release = () => undefined;
|
|
200
|
+
const gate = new Promise((resolve) => {
|
|
201
|
+
release = resolve;
|
|
202
|
+
});
|
|
203
|
+
const tail = previous.then(() => gate);
|
|
204
|
+
inProcessBlobLocks.set(name, tail);
|
|
205
|
+
await previous;
|
|
206
|
+
try {
|
|
207
|
+
return await action();
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
release();
|
|
211
|
+
if (inProcessBlobLocks.get(name) === tail)
|
|
212
|
+
inProcessBlobLocks.delete(name);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function browserLockManager() {
|
|
216
|
+
if (typeof navigator === 'undefined' || !('locks' in navigator))
|
|
217
|
+
return null;
|
|
218
|
+
const lockManager = Reflect.get(navigator, 'locks');
|
|
219
|
+
return lockManager && typeof lockManager.request === 'function'
|
|
220
|
+
? lockManager
|
|
221
|
+
: null;
|
|
222
|
+
}
|
|
223
|
+
function validateOffset(value) {
|
|
224
|
+
validateLength(value, true);
|
|
225
|
+
}
|
|
226
|
+
function validateLength(value, allowZero) {
|
|
227
|
+
if (!Number.isSafeInteger(value)
|
|
228
|
+
|| value < 0
|
|
229
|
+
|| (!allowZero && value === 0)) {
|
|
230
|
+
throw new BrowserStorageError('invalid_input');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function validateRange(offset, length) {
|
|
234
|
+
validateOffset(offset);
|
|
235
|
+
validateLength(length, true);
|
|
236
|
+
if (length > Number.MAX_SAFE_INTEGER - offset) {
|
|
237
|
+
throw new BrowserStorageError('invalid_input');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function validFileSize(size) {
|
|
241
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
242
|
+
throw new BrowserStorageError('resume_rejected');
|
|
243
|
+
}
|
|
244
|
+
return size;
|
|
245
|
+
}
|
|
246
|
+
function domExceptionName(error) {
|
|
247
|
+
if (typeof error !== 'object' || error === null || !('name' in error))
|
|
248
|
+
return null;
|
|
249
|
+
return typeof error.name === 'string' ? error.name : null;
|
|
250
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CoreBridge } from './core.ts';
|
|
2
|
+
import { type DeviceManager } from './deviceManager.ts';
|
|
3
|
+
import type { FirmwareImageDescriptor, FirmwareUpdateOptions } from './models.ts';
|
|
4
|
+
import type { FirmwareDownloadProvider } from './providers.ts';
|
|
5
|
+
import { type BrowserSdkStorage } from './storage.ts';
|
|
6
|
+
import { type BrowserBluetoothTransport } from './transport.ts';
|
|
7
|
+
import { BrowserWorkflowRuntime } from './workflowRuntime.ts';
|
|
8
|
+
type Fetcher = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
9
|
+
interface OTAManagerOptions {
|
|
10
|
+
core: CoreBridge;
|
|
11
|
+
transport: BrowserBluetoothTransport;
|
|
12
|
+
runtime: BrowserWorkflowRuntime;
|
|
13
|
+
devices: DeviceManager;
|
|
14
|
+
storage?: BrowserSdkStorage | null;
|
|
15
|
+
provider?: FirmwareDownloadProvider | null;
|
|
16
|
+
fetcher?: Fetcher;
|
|
17
|
+
now?: () => number;
|
|
18
|
+
}
|
|
19
|
+
export declare class OTAManager {
|
|
20
|
+
private readonly core;
|
|
21
|
+
private readonly transport;
|
|
22
|
+
private readonly runtime;
|
|
23
|
+
private readonly devices;
|
|
24
|
+
private readonly storage;
|
|
25
|
+
private readonly provider;
|
|
26
|
+
private readonly fetcher;
|
|
27
|
+
private readonly now;
|
|
28
|
+
private readonly activeOperations;
|
|
29
|
+
private destroyed;
|
|
30
|
+
private destroyPromise;
|
|
31
|
+
constructor(options: OTAManagerOptions);
|
|
32
|
+
updateFirmware(image: FirmwareImageDescriptor, options?: FirmwareUpdateOptions): Promise<void>;
|
|
33
|
+
resumeFirmwareUpdate(operationId: string, options?: Pick<FirmwareUpdateOptions, 'signal' | 'onProgress'>): Promise<void>;
|
|
34
|
+
cancelFirmwareUpdate(operationId: string): Promise<void>;
|
|
35
|
+
destroy(): Promise<void>;
|
|
36
|
+
private runFirmwareWorkflow;
|
|
37
|
+
private createArtifactHost;
|
|
38
|
+
private createFirmwarePersistenceHost;
|
|
39
|
+
private finishFirmwareCleanup;
|
|
40
|
+
private recoverExactConnection;
|
|
41
|
+
private deleteLatestBlob;
|
|
42
|
+
private loadReconnectHint;
|
|
43
|
+
private requireConnectedDevice;
|
|
44
|
+
private runManagedOperation;
|
|
45
|
+
private ensureFirmwareCapability;
|
|
46
|
+
private requireStorage;
|
|
47
|
+
private requireProvider;
|
|
48
|
+
}
|
|
49
|
+
export {};
|