diff --git a/eslint.config.mjs b/eslint.config.mjs index e2caa16f102..bb7afce077e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,6 +24,10 @@ import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); export default tseslint.config( + // Ignore the browser bindings for now + { + ignores: ["packages/bindings-browser/**/*.ts"], + }, ...tseslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, { diff --git a/packages/bindings-browser/package.json b/packages/bindings-browser/package.json new file mode 100644 index 00000000000..1fe4ec38a97 --- /dev/null +++ b/packages/bindings-browser/package.json @@ -0,0 +1,59 @@ +{ + "name": "@zwave-js/bindings-browser", + "version": "14.3.7", + "description": "zwave-js: Host bindings for the browser", + "keywords": [], + "publishConfig": { + "access": "public" + }, + "type": "module", + "exports": { + "./db": { + "@@dev": "./src/db/browser.ts", + "default": "./build/db/browser.js" + }, + "./fs": { + "@@dev": "./src/fs/browser.ts", + "default": "./build/fs/browser.js" + }, + "./serial": { + "@@dev": "./src/serial/browser.ts", + "default": "./build/serial/browser.js" + } + }, + "files": [ + "build/**/*.{js,cjs,mjs,d.ts,d.cts,d.mts,map}", + "build/**/package.json" + ], + "author": { + "name": "AlCalzone", + "email": "d.griesel@gmx.net" + }, + "license": "MIT", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/zwave-js/node-zwave-js.git" + }, + "bugs": { + "url": "https://github.com/zwave-js/node-zwave-js/issues" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + }, + "engines": { + "node": ">= 20" + }, + "scripts": { + "build": "tsc -b tsconfig.json --pretty", + "clean": "del-cli build/ \"*.tsbuildinfo\"" + }, + "devDependencies": { + "@types/w3c-web-serial": "^1.0.7", + "@zwave-js/serial": "workspace:*", + "@zwave-js/shared": "workspace:*", + "del-cli": "^6.0.0", + "typescript": "5.7.3", + "zwave-js": "workspace:*" + } +} diff --git a/packages/bindings-browser/src/db/browser.ts b/packages/bindings-browser/src/db/browser.ts new file mode 100644 index 00000000000..6ba3b616b4e --- /dev/null +++ b/packages/bindings-browser/src/db/browser.ts @@ -0,0 +1,188 @@ +import { + type Database, + type DatabaseFactory, + type DatabaseOptions, +} from "@zwave-js/shared/bindings"; + +const DB_NAME_CACHE = "db"; +const DB_VERSION_CACHE = 1; +const OBJECT_STORE_CACHE = "cache"; + +function openDatabase(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME_CACHE, DB_VERSION_CACHE); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(OBJECT_STORE_CACHE)) { + db.createObjectStore(OBJECT_STORE_CACHE, { + keyPath: ["filename", "valueid"], + }); + } + }; + + request.onsuccess = (event) => { + resolve((event.target as IDBOpenDBRequest).result); + }; + + request.onerror = (event) => { + reject((event.target as IDBOpenDBRequest).error!); + }; + }); +} + +/** An implementation of the Database bindings for the browser, based on IndexedDB */ +class IndexedDBCache implements Database { + private filename: string; + private cache: Map = new Map(); + + #db: IDBDatabase | undefined; + async #getDb(): Promise { + if (!this.#db) { + this.#db = await openDatabase(); + } + return this.#db; + } + + constructor(filename: string) { + this.filename = filename; + } + + async open(): Promise { + const db = await this.#getDb(); + const transaction = db.transaction(OBJECT_STORE_CACHE, "readonly"); + const store = transaction.objectStore(OBJECT_STORE_CACHE); + const request = store.openCursor(); + + return new Promise((resolve, reject) => { + request.onsuccess = (event) => { + const cursor = + (event.target as IDBRequest).result; + if (cursor) { + if (cursor.value.filename === this.filename) { + this.cache.set(cursor.value.valueid, { + value: cursor.value.value, + timestamp: cursor.value.timestamp, + }); + } + cursor.continue(); + } else { + resolve(); + } + }; + request.onerror = () => reject(request.error!); + }); + } + + close(): Promise { + this.cache.clear(); + return Promise.resolve(); + } + + has(key: string): boolean { + return this.cache.has(key); + } + + get(key: string): V | undefined { + return this.cache.get(key)?.value; + } + + private async _set( + key: string, + value: V, + timestamp?: number, + ): Promise { + const db = await this.#getDb(); + const transaction = db.transaction(OBJECT_STORE_CACHE, "readwrite"); + const store = transaction.objectStore(OBJECT_STORE_CACHE); + store.put({ + filename: this.filename, + valueid: key, + value, + timestamp, + }); + } + + set(key: string, value: V, updateTimestamp: boolean = true): this { + const entry = { + value, + timestamp: updateTimestamp + ? Date.now() + : this.cache.get(key)?.timestamp, + }; + this.cache.set(key, entry); + + // Update IndexedDB in the background + void this._set(key, value, entry.timestamp); + + return this; + } + + private async _delete(key: string): Promise { + const db = await this.#getDb(); + const transaction = db.transaction(OBJECT_STORE_CACHE, "readwrite"); + const store = transaction.objectStore(OBJECT_STORE_CACHE); + store.delete([this.filename, key]); + } + + delete(key: string): boolean { + const result = this.cache.delete(key); + + // Update IndexedDB in the background + void this._delete(key); + + return result; + } + + private async _clear(): Promise { + const db = await this.#getDb(); + const transaction = db.transaction(OBJECT_STORE_CACHE, "readwrite"); + const store = transaction.objectStore(OBJECT_STORE_CACHE); + const request = store.openCursor(); + + request.onsuccess = (event) => { + const cursor = + (event.target as IDBRequest).result; + if (cursor) { + if (cursor.value.filename === this.filename) { + cursor.delete(); + } + cursor.continue(); + } + }; + } + + clear(): void { + this.cache.clear(); + + // Update IndexedDB in the background + void this._clear(); + } + + getTimestamp(key: string): number | undefined { + return this.cache.get(key)?.timestamp; + } + + get size(): number { + return this.cache.size; + } + + keys() { + return this.cache.keys(); + } + + *entries(): MapIterator<[string, V]> { + for (const [key, { value }] of this.cache.entries()) { + yield [key, value]; + } + } +} + +export const db: DatabaseFactory = { + createInstance( + filename: string, + _options?: DatabaseOptions, + ): Database { + return new IndexedDBCache(filename); + }, +}; diff --git a/packages/bindings-browser/src/fs/browser.ts b/packages/bindings-browser/src/fs/browser.ts new file mode 100644 index 00000000000..2d8fdd20777 --- /dev/null +++ b/packages/bindings-browser/src/fs/browser.ts @@ -0,0 +1,192 @@ +import type { + FSStats, + FileHandle, + FileSystem, +} from "@zwave-js/shared/bindings"; + +const DB_NAME_FS = "filesystem"; +const DB_VERSION_FS = 1; +const OBJECT_STORE_FILES = "files"; + +function openDatabase(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME_FS, DB_VERSION_FS); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(OBJECT_STORE_FILES)) { + db.createObjectStore(OBJECT_STORE_FILES, { keyPath: "path" }); + } + }; + + request.onsuccess = (event) => { + resolve((event.target as IDBOpenDBRequest).result); + }; + + request.onerror = (event) => { + reject((event.target as IDBOpenDBRequest).error!); + }; + }); +} + +async function writeFile( + db: IDBDatabase, + path: string, + data: Uint8Array, +): Promise { + const transaction = db.transaction(OBJECT_STORE_FILES, "readwrite"); + const store = transaction.objectStore(OBJECT_STORE_FILES); + + const request = store.put({ path, data }); + + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error!); + }); +} + +// Datei lesen +async function readFile(db: IDBDatabase, path: string): Promise { + const transaction = db.transaction(OBJECT_STORE_FILES, "readonly"); + const store = transaction.objectStore(OBJECT_STORE_FILES); + + const request = store.get(path); + + return new Promise((resolve, reject) => { + request.onsuccess = () => { + const result = request.result; + if (result) { + resolve(result.data); + } else { + reject(new Error(`File ${path} not found`)); + } + }; + request.onerror = () => reject(request.error!); + }); +} + +async function listKeysWithPrefix( + db: IDBDatabase, + prefix: string, +): Promise { + const transaction = db.transaction(OBJECT_STORE_FILES, "readonly"); + const store = transaction.objectStore(OBJECT_STORE_FILES); + + return new Promise((resolve, reject) => { + const keys: string[] = []; + const request = store.openCursor(); + + request.onsuccess = (event) => { + const cursor = + (event.target as IDBRequest).result; + if (cursor) { + if ( + typeof cursor.key === "string" + && cursor.key.startsWith(prefix) + ) { + keys.push(cursor.key); + } + cursor.continue(); + } else { + resolve(keys); + } + }; + + request.onerror = () => reject(request.error!); + }); +} + +async function deleteKeysWithPrefix( + db: IDBDatabase, + prefix: string, +): Promise { + const transaction = db.transaction(OBJECT_STORE_FILES, "readwrite"); + const store = transaction.objectStore(OBJECT_STORE_FILES); + + return new Promise((resolve, reject) => { + const request = store.openCursor(); + + request.onsuccess = (event) => { + const cursor = + (event.target as IDBRequest).result; + if (cursor) { + if ( + typeof cursor.key === "string" + && cursor.key.startsWith(prefix) + ) { + cursor.delete(); + } + cursor.continue(); + } else { + resolve(); + } + }; + + request.onerror = () => reject(request.error!); + }); +} + +export class IndexedDBFileSystem implements FileSystem { + #db: IDBDatabase | undefined; + async #getDb(): Promise { + if (!this.#db) { + this.#db = await openDatabase(); + } + return this.#db; + } + + async readFile(path: string): Promise { + const db = await this.#getDb(); + return readFile(db, path); + } + + async writeFile(path: string, data: Uint8Array): Promise { + const db = await this.#getDb(); + return writeFile(db, path, data); + } + + async copyFile(source: string, dest: string): Promise { + const db = await this.#getDb(); + const data = await readFile(db, source); + await writeFile(db, dest, data); + } + + // eslint-disable-next-line @typescript-eslint/require-await + async open( + _path: string, + _flags: { + read: boolean; + write: boolean; + create: boolean; + truncate: boolean; + }, + ): Promise { + throw new Error("Method not implemented."); + } + + async readDir(path: string): Promise { + const db = await this.#getDb(); + return listKeysWithPrefix(db, path); + } + + // eslint-disable-next-line @typescript-eslint/require-await + async stat(_path: string): Promise { + throw new Error("Method not implemented."); + } + + async ensureDir(_path: string): Promise { + // No need to create directories + } + + async deleteDir(path: string): Promise { + const db = await this.#getDb(); + return deleteKeysWithPrefix(db, path); + } + + // eslint-disable-next-line @typescript-eslint/require-await + async makeTempDir(_prefix: string): Promise { + throw new Error("Function not implemented."); + } +} + +export const fs: FileSystem = new IndexedDBFileSystem(); diff --git a/packages/bindings-browser/src/serial/browser.ts b/packages/bindings-browser/src/serial/browser.ts new file mode 100644 index 00000000000..e65295ff707 --- /dev/null +++ b/packages/bindings-browser/src/serial/browser.ts @@ -0,0 +1,39 @@ +import { type ZWaveSerialBindingFactory } from "@zwave-js/serial"; + +export function createWebSerialPortFactory( + port: SerialPort, +): ZWaveSerialBindingFactory { + const sink: UnderlyingSink = { + close() { + port.close(); + }, + async write(chunk) { + const writer = port.writable!.getWriter(); + try { + await writer.write(chunk); + } finally { + writer.releaseLock(); + } + }, + }; + + const source: UnderlyingDefaultSource = { + async start(controller) { + const reader = port.readable!.getReader(); + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + controller.enqueue(value); + } + } finally { + reader.releaseLock(); + } + }, + }; + + // @ts-expect-error Slight mismatch between the web types and Node.js + return () => Promise.resolve({ source, sink }); +} diff --git a/packages/bindings-browser/tsconfig.json b/packages/bindings-browser/tsconfig.json new file mode 100644 index 00000000000..9c23a401164 --- /dev/null +++ b/packages/bindings-browser/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ESNext", + "lib": ["DOM", "ESNext"], + "customConditions": ["browser"], + "types": ["w3c-web-serial"] + }, + "references": [ + { + "path": "../shared/tsconfig.build.json" + }, + { + "path": "../serial/tsconfig.build.json" + } + ], + "include": ["src/**/*.ts"] +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 3d1e2856a00..227a44b1dbd 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -2,6 +2,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": {}, - "include": ["src/**/*.ts"], + "include": [ + "src/**/*.ts" + ], "exclude": ["build/**", "node_modules/**"] } diff --git a/packages/web/.build.mjs b/packages/web/.build.mjs index a392316269f..137f8d91028 100644 --- a/packages/web/.build.mjs +++ b/packages/web/.build.mjs @@ -38,6 +38,7 @@ try { // logLevel: "verbose", logLevel: "info", logLimit: 0, + keepNames: true, plugins: [ logImportsPlugin, nodeModulesPolyfillPlugin({ diff --git a/packages/web/.dev.mjs b/packages/web/.dev.mjs index 3a3bae73728..c9b1ddf3124 100644 --- a/packages/web/.dev.mjs +++ b/packages/web/.dev.mjs @@ -3,7 +3,7 @@ import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill // Create a context for incremental builds const context = await esbuild.context({ - entryPoints: ["src/script.ts"], + entryPoints: ["src/flasher.ts"], bundle: true, sourcemap: true, // analyze: "verbose", @@ -22,6 +22,7 @@ const context = await esbuild.context({ // logLevel: "verbose", logLevel: "info", logLimit: 0, + keepNames: true, plugins: [ nodeModulesPolyfillPlugin({ // fallback: "error", diff --git a/packages/web/index.html b/packages/web/index.html index 20023ce94cd..12143e8687c 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -1,17 +1,25 @@ - - - - Z-Wave JS: In the browser?! - - - - -

Z-Wave JS: In the browser?!

+ + + + Z-Wave JS: In the browser?! + + + + +

Z-Wave JS: In the browser?!

- +
+ +
- - +
+ +
+ +
+ + + diff --git a/packages/web/package.json b/packages/web/package.json index 13f14ef4b3d..8951b2173dc 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -31,6 +31,7 @@ "devDependencies": { "@types/w3c-web-serial": "^1.0.7", "@types/w3c-web-usb": "^1.0.10", + "@zwave-js/bindings-browser": "workspace:^", "@zwave-js/core": "workspace:*", "@zwave-js/shared": "workspace:*", "esbuild": "^0.24.0", diff --git a/packages/web/src/flasher.ts b/packages/web/src/flasher.ts new file mode 100644 index 00000000000..7640a45c452 --- /dev/null +++ b/packages/web/src/flasher.ts @@ -0,0 +1,110 @@ +import { db } from "@zwave-js/bindings-browser/db"; +import { fs } from "@zwave-js/bindings-browser/fs"; +import { createWebSerialPortFactory } from "@zwave-js/bindings-browser/serial"; +import { log as createLogContainer } from "@zwave-js/core/bindings/log/browser"; +import { + ControllerFirmwareUpdateStatus, + Driver, + getEnumMemberName, +} from "zwave-js"; + +const flashButton = document.getElementById("flash") as HTMLButtonElement; +const fileInput = document.getElementById("file") as HTMLInputElement; +const flashProgress = document.getElementById( + "progress", +) as HTMLProgressElement; +let firmwareFileContent: ArrayBuffer | null = null; +let driver!: Driver; + +async function init() { + let port: SerialPort; + try { + port = await navigator.serial.requestPort({ + filters: [ + { usbVendorId: 0x10c4, usbProductId: 0xea60 }, + ], + }); + await port.open({ baudRate: 115200 }); + } catch (e) { + console.error(e); + return; + } + + const serialBinding = createWebSerialPortFactory(port); + + driver = new Driver(serialBinding, { + host: { + fs, + db, + log: createLogContainer, + serial: { + // no listing, no creating by path! + }, + }, + testingHooks: { + skipNodeInterview: true, + loadConfiguration: false, + }, + bootloaderMode: "stay", + }) + .once("driver ready", ready) + .once("bootloader ready", ready); + (globalThis as any).driver = driver; + + await driver.start(); +} + +function ready() { + driver.controller.on("firmware update progress", (progress) => { + flashProgress.value = progress.progress; + }); + driver.controller.on("firmware update finished", (_result) => { + flashProgress.style.display = "none"; + }); + fileInput.disabled = false; +} + +fileInput.addEventListener("change", (event) => { + const file = (event.target as HTMLInputElement).files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => { + firmwareFileContent = reader.result as ArrayBuffer; + flashButton.disabled = false; + }; + reader.readAsArrayBuffer(file); + } +}); + +async function flash() { + if (!firmwareFileContent) { + console.error("No firmware file loaded"); + return; + } + + try { + const driver = (globalThis as any).driver as Driver; + flashProgress.style.display = "initial"; + + const result = await driver.controller.firmwareUpdateOTW( + new Uint8Array(firmwareFileContent), + ); + if (result.success) { + alert("Firmware flashed successfully"); + } else { + alert( + `Failed to flash firmware: ${ + getEnumMemberName( + ControllerFirmwareUpdateStatus, + result.status, + ) + }`, + ); + } + } catch (e) { + console.error("Failed to flash firmware", e); + } +} + +document.getElementById("connect").addEventListener("click", init); +flashButton.addEventListener("click", flash); diff --git a/packages/web/src/script.ts b/packages/web/src/script.ts index f2403ae2e4c..56549230b76 100644 --- a/packages/web/src/script.ts +++ b/packages/web/src/script.ts @@ -1,293 +1,10 @@ +import { db } from "@zwave-js/bindings-browser/db"; +import { fs } from "@zwave-js/bindings-browser/fs"; +import { createWebSerialPortFactory } from "@zwave-js/bindings-browser/serial"; import { log as createLogContainer } from "@zwave-js/core/bindings/log/browser"; import { Bytes } from "@zwave-js/shared"; -import { - type Database, - type DatabaseFactory, - type FSStats, - type FileHandle, - type FileSystem, -} from "@zwave-js/shared/bindings"; import { Driver } from "zwave-js"; -const OBJECT_STORE_FILES = "files"; -const OBJECT_STORE_CACHE = "cache"; - -// IndexedDB-Datenbank öffnen oder erstellen -function openDatabase(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open("filesystem", 1); - - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - if (!db.objectStoreNames.contains(OBJECT_STORE_FILES)) { - db.createObjectStore(OBJECT_STORE_FILES, { keyPath: "path" }); - } - if (!db.objectStoreNames.contains(OBJECT_STORE_CACHE)) { - db.createObjectStore(OBJECT_STORE_CACHE, { - keyPath: ["filename", "valueid"], - }); - } - }; - - request.onsuccess = (event) => { - resolve((event.target as IDBOpenDBRequest).result); - }; - - request.onerror = (event) => { - reject((event.target as IDBOpenDBRequest).error); - }; - }); -} - -const db = await openDatabase(); - -// Datei erstellen oder schreiben -async function writeFile(path: string, data: Uint8Array): Promise { - const transaction = db.transaction(OBJECT_STORE_FILES, "readwrite"); - const store = transaction.objectStore(OBJECT_STORE_FILES); - - const request = store.put({ path, data }); - - return new Promise((resolve, reject) => { - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); -} - -// Datei lesen -async function readFile(path: string): Promise { - const transaction = db.transaction(OBJECT_STORE_FILES, "readonly"); - const store = transaction.objectStore(OBJECT_STORE_FILES); - - const request = store.get(path); - - return new Promise((resolve, reject) => { - request.onsuccess = () => { - const result = request.result; - if (result) { - resolve(result.data); - } else { - reject(new Error(`File ${path} not found`)); - } - }; - request.onerror = () => reject(request.error); - }); -} - -async function listKeysWithPrefix(prefix: string): Promise { - const transaction = db.transaction(OBJECT_STORE_FILES, "readonly"); - const store = transaction.objectStore(OBJECT_STORE_FILES); - - return new Promise((resolve, reject) => { - const keys: string[] = []; - const request = store.openCursor(); - - request.onsuccess = (event) => { - const cursor = - (event.target as IDBRequest).result; - if (cursor) { - if ( - typeof cursor.key === "string" - && cursor.key.startsWith(prefix) - ) { - keys.push(cursor.key as string); - } - cursor.continue(); - } else { - resolve(keys); - } - }; - - request.onerror = () => reject(request.error); - }); -} - -async function deleteKeysWithPrefix(prefix: string): Promise { - const transaction = db.transaction(OBJECT_STORE_FILES, "readwrite"); - const store = transaction.objectStore(OBJECT_STORE_FILES); - - return new Promise((resolve, reject) => { - const request = store.openCursor(); - - request.onsuccess = (event) => { - const cursor = - (event.target as IDBRequest).result; - if (cursor) { - if ( - typeof cursor.key === "string" - && cursor.key.startsWith(prefix) - ) { - cursor.delete(); - } - cursor.continue(); - } else { - resolve(); - } - }; - - request.onerror = () => reject(request.error); - }); -} - -const webFS: FileSystem = { - readFile(path) { - return readFile(path); - }, - writeFile(path, data) { - return writeFile(path, data); - }, - async copyFile(source, dest) { - const data = await readFile(source); - await writeFile(dest, data); - }, - open: function( - path: string, - flags: { - read: boolean; - write: boolean; - create: boolean; - truncate: boolean; - }, - ): Promise { - throw new Error("Function not implemented."); - }, - readDir(path) { - return listKeysWithPrefix(path); - }, - stat: function(path: string): Promise { - throw new Error("Function not implemented."); - }, - ensureDir: function(path: string): Promise { - // No need to create directories - }, - deleteDir(path) { - return deleteKeysWithPrefix(path); - }, - makeTempDir: function(prefix: string): Promise { - throw new Error("Function not implemented."); - }, -}; - -class IndexedDBBackedCache implements Database { - private filename: string; - private cache: Map = new Map(); - - constructor(filename: string) { - this.filename = filename; - } - - async open(): Promise { - const transaction = db.transaction(OBJECT_STORE_CACHE, "readonly"); - const store = transaction.objectStore(OBJECT_STORE_CACHE); - const request = store.openCursor(); - - return new Promise((resolve, reject) => { - request.onsuccess = (event) => { - const cursor = - (event.target as IDBRequest).result; - if (cursor) { - if (cursor.value.filename === this.filename) { - this.cache.set(cursor.value.valueid, { - value: cursor.value.value, - timestamp: cursor.value.timestamp, - }); - } - cursor.continue(); - } else { - resolve(); - } - }; - request.onerror = () => reject(request.error); - }); - } - - close(): Promise { - this.cache.clear(); - return Promise.resolve(); - } - - has(key: string): boolean { - return this.cache.has(key); - } - - get(key: string): V | undefined { - return this.cache.get(key)?.value; - } - - set(key: string, value: V, updateTimestamp: boolean = true): this { - const entry = { - value, - timestamp: updateTimestamp - ? Date.now() - : this.cache.get(key)?.timestamp, - }; - this.cache.set(key, entry); - - // Update IndexedDB in the background - const transaction = db.transaction(OBJECT_STORE_CACHE, "readwrite"); - const store = transaction.objectStore(OBJECT_STORE_CACHE); - store.put({ - filename: this.filename, - valueid: key, - value, - timestamp: entry.timestamp, - }); - - return this; - } - - delete(key: string): boolean { - const result = this.cache.delete(key); - - // Update IndexedDB in the background - const transaction = db.transaction(OBJECT_STORE_CACHE, "readwrite"); - const store = transaction.objectStore(OBJECT_STORE_CACHE); - store.delete([this.filename, key]); - - return result; - } - - clear(): void { - this.cache.clear(); - - // Update IndexedDB in the background - const transaction = db.transaction(OBJECT_STORE_CACHE, "readwrite"); - const store = transaction.objectStore(OBJECT_STORE_CACHE); - const request = store.openCursor(); - - request.onsuccess = (event) => { - const cursor = - (event.target as IDBRequest).result; - if (cursor) { - if (cursor.value.filename === this.filename) { - cursor.delete(); - } - cursor.continue(); - } - }; - } - - getTimestamp(key: string): number | undefined { - return this.cache.get(key)?.timestamp; - } - - get size(): number { - return this.cache.size; - } - - keys() { - return this.cache.keys(); - } - - *entries() { - return function*() { - for (const [key, { value }] of this.cache.entries()) { - yield [key, value]; - } - }; - } -} - async function init() { let port: SerialPort; try { @@ -302,50 +19,12 @@ async function init() { return; } - const sink: UnderlyingSink = { - close() { - port.close(); - }, - async write(chunk) { - let writer: WritableStreamDefaultWriter; - try { - writer = port.writable.getWriter(); - await writer.write(chunk); - } finally { - writer.releaseLock(); - } - }, - }; - - const source: UnderlyingDefaultSource = { - async start(controller) { - const reader = port.readable.getReader(); - try { - while (true) { - const { value, done } = await reader.read(); - if (done) { - break; - } - controller.enqueue(value); - } - } finally { - reader.releaseLock(); - } - }, - }; - - const serialBinding = () => Promise.resolve({ source, sink }); - - const dbFactory: DatabaseFactory = { - createInstance(filename, options) { - return new IndexedDBBackedCache(filename); - }, - }; + const serialBinding = createWebSerialPortFactory(port); const d = new Driver(serialBinding, { host: { - fs: webFS, - db: dbFactory, + fs, + db, log: createLogContainer, serial: { // no listing, no creating by path! diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index df5119fccee..beb4e0259a6 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -7612,7 +7612,7 @@ ${handlers.length} left`, await this.destroy(); // Let the async calling context finish before emitting the error - process.nextTick(() => { + setImmediate(() => { this.emit( "error", new ZWaveError( diff --git a/yarn.lock b/yarn.lock index 70071736a89..3a23127d934 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2475,6 +2475,19 @@ __metadata: languageName: node linkType: hard +"@zwave-js/bindings-browser@workspace:^, @zwave-js/bindings-browser@workspace:packages/bindings-browser": + version: 0.0.0-use.local + resolution: "@zwave-js/bindings-browser@workspace:packages/bindings-browser" + dependencies: + "@types/w3c-web-serial": "npm:^1.0.7" + "@zwave-js/serial": "workspace:*" + "@zwave-js/shared": "workspace:*" + del-cli: "npm:^6.0.0" + typescript: "npm:5.7.3" + zwave-js: "workspace:*" + languageName: unknown + linkType: soft + "@zwave-js/cc@workspace:*, @zwave-js/cc@workspace:packages/cc": version: 0.0.0-use.local resolution: "@zwave-js/cc@workspace:packages/cc" @@ -2844,6 +2857,7 @@ __metadata: dependencies: "@types/w3c-web-serial": "npm:^1.0.7" "@types/w3c-web-usb": "npm:^1.0.10" + "@zwave-js/bindings-browser": "workspace:^" "@zwave-js/core": "workspace:*" "@zwave-js/shared": "workspace:*" esbuild: "npm:^0.24.0"