diff --git a/packages/core/src/bindings/db/jsonl.ts b/packages/core/src/bindings/db/jsonl.ts new file mode 100644 index 00000000000..4aea5c53eff --- /dev/null +++ b/packages/core/src/bindings/db/jsonl.ts @@ -0,0 +1,16 @@ +import { JsonlDB } from "@alcalzone/jsonl-db"; +import { + type Database, + type DatabaseFactory, + type DatabaseOptions, +} from "@zwave-js/shared/bindings"; + +/** An implementation of the Database bindings for Node.js based on JsonlDB */ +export const db: DatabaseFactory = { + createInstance( + filename: string, + options?: DatabaseOptions, + ): Database { + return new JsonlDB(filename, options); + }, +}; diff --git a/packages/core/src/values/CacheBackedMap.ts b/packages/core/src/values/CacheBackedMap.ts index 71119ef67ee..8525d133984 100644 --- a/packages/core/src/values/CacheBackedMap.ts +++ b/packages/core/src/values/CacheBackedMap.ts @@ -1,4 +1,4 @@ -import type { JsonlDB } from "@alcalzone/jsonl-db"; +import { type Database } from "@zwave-js/shared/bindings"; export interface CacheBackedMapKeys { /** The common prefix all keys start with */ @@ -12,7 +12,7 @@ export interface CacheBackedMapKeys { /** Wrapper class which allows storing a Map as a subset of a JsonlDB */ export class CacheBackedMap implements Map { constructor( - private readonly cache: JsonlDB, + private readonly cache: Database, private readonly cacheKeys: CacheBackedMapKeys, ) { this.map = new Map(); diff --git a/packages/core/src/values/ValueDB.ts b/packages/core/src/values/ValueDB.ts index ddfe67842dd..a46cb83dd3e 100644 --- a/packages/core/src/values/ValueDB.ts +++ b/packages/core/src/values/ValueDB.ts @@ -1,5 +1,5 @@ -import type { JsonlDB } from "@alcalzone/jsonl-db"; import { TypedEventTarget } from "@zwave-js/shared"; +import { type Database } from "@zwave-js/shared/bindings"; import type { CommandClasses } from "../definitions/CommandClasses.js"; import { ZWaveError, @@ -106,8 +106,8 @@ export class ValueDB extends TypedEventTarget { */ public constructor( nodeId: number, - valueDB: JsonlDB, - metadataDB: JsonlDB, + valueDB: Database, + metadataDB: Database, ownKeys?: Set, ) { super(); @@ -119,8 +119,8 @@ export class ValueDB extends TypedEventTarget { } private nodeId: number; - private _db: JsonlDB; - private _metadata: JsonlDB; + private _db: Database; + private _metadata: Database; private _index: Set; private buildIndex(): Set { @@ -603,7 +603,9 @@ function compareDBKeyFast( } /** Extracts an index for each node from one or more JSONL DBs */ -export function indexDBsByNode(databases: JsonlDB[]): Map> { +export function indexDBsByNode( + databases: Database[], +): Map> { const indexes = new Map>(); for (const db of databases) { for (const key of db.keys()) { diff --git a/packages/shared/src/bindings.ts b/packages/shared/src/bindings.ts index 02d0b712782..939bbeca8c8 100644 --- a/packages/shared/src/bindings.ts +++ b/packages/shared/src/bindings.ts @@ -135,3 +135,42 @@ export interface FileSystem {} export type Platform = "linux" | "darwin" | "win32" | "browser" | "other"; + +export type DatabaseOptions = { + /** + * An optional reviver function (similar to JSON.parse) to transform parsed values before they are accessible in the database. + * If this function is defined, it must always return a value. + */ + reviver?: (key: string, value: any) => V; + /** + * An optional serializer function (similar to JSON.serialize) to transform values before they are written to the database file. + * If this function is defined, it must always return a value. + */ + serializer?: (key: string, value: V) => any; + /** Whether timestamps should be recorded when setting values. Default: false */ + enableTimestamps?: boolean; +}; + +export interface DatabaseFactory { + createInstance( + filename: string, + options?: DatabaseOptions, + ): Database; +} + +export interface Database { + open(): Promise; + close(): Promise; + + has: Map["has"]; + get: Map["get"]; + set(key: string, value: V, updateTimestamp?: boolean): this; + delete(key: string): boolean; + clear(): void; + + getTimestamp(key: string): number | undefined; + get size(): number; + + keys: Map["keys"]; + entries: Map["entries"]; +} diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index 20e21cec3f0..b18dc0427c8 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -1,4 +1,4 @@ -import { JsonlDB, type JsonlDBOptions } from "@alcalzone/jsonl-db"; +import { type JsonlDBOptions } from "@alcalzone/jsonl-db"; import { type CCAPIHost, type CCEncodingContext, @@ -193,6 +193,7 @@ import { pick, } from "@zwave-js/shared"; import { + type Database, type ReadFile, type ReadFileSystemInfo, } from "@zwave-js/shared/bindings"; @@ -894,19 +895,19 @@ export class Driver extends TypedEventTarget public readonly cacheDir: string; - private _valueDB: JsonlDB | undefined; + private _valueDB: Database | undefined; /** @internal */ - public get valueDB(): JsonlDB | undefined { + public get valueDB(): Database | undefined { return this._valueDB; } - private _metadataDB: JsonlDB | undefined; + private _metadataDB: Database | undefined; /** @internal */ - public get metadataDB(): JsonlDB | undefined { + public get metadataDB(): Database | undefined { return this._metadataDB; } - private _networkCache: JsonlDB | undefined; + private _networkCache: Database | undefined; /** @internal */ - public get networkCache(): JsonlDB { + public get networkCache(): Database { if (this._networkCache == undefined) { throw new ZWaveError( "The network cache was not yet initialized!", @@ -1332,6 +1333,8 @@ export class Driver extends TypedEventTarget ?? (await import("@zwave-js/core/bindings/fs/node")).fs, serial: this._options.host?.serial ?? (await import("@zwave-js/serial/bindings/node")).serial, + db: this._options.host?.db + ?? (await import("@zwave-js/core/bindings/db/jsonl")).db, }; const spOpenPromise = createDeferredPromise(); @@ -1593,7 +1596,7 @@ export class Driver extends TypedEventTarget this.cacheDir, `${homeId.toString(16)}.jsonl`, ); - this._networkCache = new JsonlDB(networkCacheFile, { + this._networkCache = this.bindings.db.createInstance(networkCacheFile, { ...options, serializer: serializeNetworkCacheValue, reviver: deserializeNetworkCacheValue, @@ -1614,7 +1617,7 @@ export class Driver extends TypedEventTarget this.cacheDir, `${homeId.toString(16)}.values.jsonl`, ); - this._valueDB = new JsonlDB(valueDBFile, { + this._valueDB = this.bindings.db.createInstance(valueDBFile, { ...options, enableTimestamps: true, reviver: (_key, value) => deserializeCacheValue(value), @@ -1626,7 +1629,10 @@ export class Driver extends TypedEventTarget this.cacheDir, `${homeId.toString(16)}.metadata.jsonl`, ); - this._metadataDB = new JsonlDB(metadataDBFile, options); + this._metadataDB = this.bindings.db.createInstance( + metadataDBFile, + options, + ); await this._metadataDB.open(); if (process.env.NO_CACHE === "true") { diff --git a/packages/zwave-js/src/lib/driver/Host.ts b/packages/zwave-js/src/lib/driver/Host.ts index e0c2857346e..53d4a7dcc04 100644 --- a/packages/zwave-js/src/lib/driver/Host.ts +++ b/packages/zwave-js/src/lib/driver/Host.ts @@ -1,11 +1,16 @@ // FIXME: This should eventually live in @zwave-js/host import { type Serial } from "@zwave-js/serial"; -import { type FileSystem, type Platform } from "@zwave-js/shared/bindings"; +import { + type DatabaseFactory, + type FileSystem, + type Platform, +} from "@zwave-js/shared/bindings"; /** Abstractions for a host system Z-Wave JS is running on */ export interface Host { fs: FileSystem; platform: Platform; serial: Serial; + db: DatabaseFactory; } diff --git a/packages/zwave-js/src/lib/driver/NetworkCache.ts b/packages/zwave-js/src/lib/driver/NetworkCache.ts index 4d3aa13ce38..cb6139947a1 100644 --- a/packages/zwave-js/src/lib/driver/NetworkCache.ts +++ b/packages/zwave-js/src/lib/driver/NetworkCache.ts @@ -1,4 +1,3 @@ -import type { JsonlDB } from "@alcalzone/jsonl-db"; import { type AssociationAddress } from "@zwave-js/cc"; import { type CommandClasses, @@ -12,7 +11,11 @@ import { securityClassOrder, } from "@zwave-js/core"; import { Bytes, getEnumMemberName, num2hex, pickDeep } from "@zwave-js/shared"; -import type { ReadFile, ReadFileSystemInfo } from "@zwave-js/shared/bindings"; +import type { + Database, + ReadFile, + ReadFileSystemInfo, +} from "@zwave-js/shared/bindings"; import { isArray, isObject } from "alcalzone-shared/typeguards"; import path from "pathe"; import { @@ -621,8 +624,8 @@ const legacyPaths = { export async function migrateLegacyNetworkCache( homeId: number, - networkCache: JsonlDB, - valueDB: JsonlDB, + networkCache: Database, + valueDB: Database, fs: ReadFileSystemInfo & ReadFile, cacheDir: string, ): Promise { diff --git a/packages/zwave-js/src/lib/driver/ZWaveOptions.ts b/packages/zwave-js/src/lib/driver/ZWaveOptions.ts index 959a1dad3cc..2f145c3c398 100644 --- a/packages/zwave-js/src/lib/driver/ZWaveOptions.ts +++ b/packages/zwave-js/src/lib/driver/ZWaveOptions.ts @@ -6,7 +6,7 @@ import type { } from "@zwave-js/core"; import { type Serial, type ZWaveSerialStream } from "@zwave-js/serial"; import { type DeepPartial, type Expand } from "@zwave-js/shared"; -import type { FileSystem } from "@zwave-js/shared/bindings"; +import type { DatabaseFactory, FileSystem } from "@zwave-js/shared/bindings"; import type { InclusionUserCallbacks, JoinNetworkUserCallbacks, @@ -137,6 +137,11 @@ export interface ZWaveOptions { * Specifies which bindings are used interact with serial ports. */ serial?: Serial; + + /** + * Specifies which bindings are used to interact with the database used to store the cache. + */ + db?: DatabaseFactory; }; storage: { diff --git a/packages/zwave-js/src/lib/zniffer/Zniffer.ts b/packages/zwave-js/src/lib/zniffer/Zniffer.ts index 5f3c80b1675..e39cc1c868e 100644 --- a/packages/zwave-js/src/lib/zniffer/Zniffer.ts +++ b/packages/zwave-js/src/lib/zniffer/Zniffer.ts @@ -279,7 +279,12 @@ export class Zniffer extends TypedEventTarget { * The host bindings used to access file system etc. */ // This is set during `init()` and should not be accessed before - private bindings!: Required>; + private bindings!: Omit< + Required< + NonNullable + >, + "db" + >; private serialFactory: ZnifferSerialStreamFactory | undefined; /** The serial port instance */