Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move browser bindings into own package #7592

Merged
merged 4 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
{
Expand Down
59 changes: 59 additions & 0 deletions packages/bindings-browser/package.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]"
},
"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:*"
}
}
188 changes: 188 additions & 0 deletions packages/bindings-browser/src/db/browser.ts
Original file line number Diff line number Diff line change
@@ -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<IDBDatabase> {
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<V> implements Database<V> {
private filename: string;
private cache: Map<string, { value: V; timestamp?: number }> = new Map();

#db: IDBDatabase | undefined;
async #getDb(): Promise<IDBDatabase> {
if (!this.#db) {
this.#db = await openDatabase();
}
return this.#db;
}

constructor(filename: string) {
this.filename = filename;
}

async open(): Promise<void> {
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<IDBCursorWithValue>).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<void> {
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<void> {
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<void> {
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<void> {
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<IDBCursorWithValue>).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<V>(
filename: string,
_options?: DatabaseOptions<V>,
): Database<V> {
return new IndexedDBCache(filename);
},
};
Loading
Loading