/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import type { Directory } from "../fs/Directory.js"; import { ImplementationError, MatterError } from "../MatterError.js"; import { MaybePromise } from "../util/Promises.js"; import { BaseStorageDriver, type StorageType } from "./BaseStorageDriver.js"; import { DatafileRoot } from "./DatafileRoot.js"; import type { DataNamespace } from "./DataNamespace.js"; import { SupportedStorageTypes } from "./StringifyTools.js"; export class StorageError extends MatterError {} export class StorageCommitError extends StorageError {} export class StorageLockError extends StorageError {} /** * Matter.js uses this key/value API to manage persistent state. */ export abstract class StorageDriver extends BaseStorageDriver { override readonly type = "kv" as const; abstract get(contexts: string[], key: string): MaybePromise; abstract set(contexts: string[], values: Record): MaybePromise; abstract set(contexts: string[], key: string, value: SupportedStorageTypes): MaybePromise; abstract values(contexts: string[]): MaybePromise>; /** @deprecated Use {@link clearAll} instead. */ clear(_completely?: boolean): MaybePromise { throw new StorageError("clear() is deprecated; use clearAll() instead"); } /** * Checks if a key exists in the storage for the given contexts. * Important Note: This default implementation just reads the value for the key and checks if it is undefined. * Please implement this method in your storage implementation if you want to optimize it. */ has(contexts: string[], key: string): MaybePromise { const value = this.get(contexts, key); if (MaybePromise.is(value)) { return MaybePromise.then(value, v => v !== undefined); } return value !== undefined; } begin(): MaybePromise { return new StorageDriver.Transaction(this); } } export namespace StorageDriver { /** * Filenames that live in the storage root directory but are not data values. Storage drivers that enumerate files * to discover keys (e.g. {@link FilesystemStorageDriver}) must ignore these on read and reject them on write. */ export const RESERVED_FILENAMES = BaseStorageDriver.RESERVED_FILENAMES; /** * Serializable descriptor stored as `driver.json` inside the storage directory. The `kind` field identifies the * driver implementation. Drivers extend this with optional fields for driver-specific options so that a plain * `{ kind }` is always a valid descriptor for any driver. */ export interface Descriptor { kind: string; type?: StorageType; } /** * Static interface that a registerable driver class must satisfy. */ export interface Implementation { /** Short identifier such as `"file"`, `"sqlite"`, `"wal"`, or `"memory"`. */ id: string; /** Create a storage driver for the given namespace and descriptor. */ create(namespace: DataNamespace, descriptor: D): MaybePromise; /** * Optional hook called before {@link create}. Allows the driver to rearrange files on disk (e.g. move a * legacy `.db` file into its directory). If no directory exists after this call, `driver.json` will not be * written. */ preinitialize?(parentDir: Directory, descriptor: D): MaybePromise; } /** * A transactional wrapper around a {@link StorageDriver}. * * Use {@link StorageDriver#begin} to create a transaction, then use `await using` for automatic cleanup: * * ```ts * await using tx = storage.begin(); * tx.set(["ctx"], "key", "value"); * tx.commit(); * ``` * * If `commit()` is not called before disposal, the transaction is rolled back. */ export class Transaction extends StorageDriver implements Disposable, AsyncDisposable { #committed = false; #disposed = false; constructor(protected readonly storage: StorageDriver) { super(); } get committed() { return this.#committed; } get disposed() { return this.#disposed; } protected assertActive() { if (this.#disposed) { throw new StorageCommitError("Transaction is disposed"); } if (this.#committed) { throw new StorageCommitError("Transaction is already committed"); } } commit(): MaybePromise { this.assertActive(); this.#committed = true; } protected rollback(): MaybePromise { // No-op in base class; override point for subclasses } [Symbol.dispose]() { if (this.#disposed) { return; } this.#disposed = true; if (!this.#committed) { this.rollback(); } } async [Symbol.asyncDispose]() { if (this.#disposed) { return; } this.#disposed = true; if (!this.#committed) { await this.rollback(); } } // Storage abstract method implementations — delegate to wrapped storage override get initialized() { return this.storage.initialized; } override initialize(): MaybePromise { throw new StorageCommitError("Cannot initialize storage from a transaction"); } override close(): MaybePromise { throw new StorageCommitError("Cannot close storage from a transaction"); } override begin(): MaybePromise { throw new StorageCommitError("Nested transactions are not supported"); } override get(contexts: string[], key: string): MaybePromise { return this.storage.get(contexts, key); } override set(contexts: string[], values: Record): MaybePromise; override set(contexts: string[], key: string, value: SupportedStorageTypes): MaybePromise; override set( contexts: string[], keyOrValues: string | Record, value?: SupportedStorageTypes, ): MaybePromise { if (typeof keyOrValues === "string") { return this.storage.set(contexts, keyOrValues, value!); } return this.storage.set(contexts, keyOrValues); } override delete(contexts: string[], key: string): MaybePromise { return this.storage.delete(contexts, key); } override keys(contexts: string[]): MaybePromise { return this.storage.keys(contexts); } override values(contexts: string[]): MaybePromise> { return this.storage.values(contexts); } override contexts(contexts: string[]): MaybePromise { return this.storage.contexts(contexts); } override clearAll(contexts: string[]): MaybePromise { return this.storage.clearAll(contexts); } /** @deprecated Use {@link clearAll} instead. */ override clear(_completely?: boolean): MaybePromise { throw new StorageError("clear() is deprecated; use clearAll() instead"); } } } /** * {@link StorageDriver} subclass for drivers backed by the filesystem. * * Manages a {@link DatafileRoot.Lock} that is acquired during {@link initialize} and released during * {@link close}. Filesystem-specific KV drivers should extend this instead of {@link StorageDriver} * directly. Blob drivers should extend {@link FilesystemBlobStorageDriver} instead. */ export abstract class FilesystemStorageDriver extends StorageDriver { readonly #root?: DatafileRoot; #lock?: DatafileRoot.Lock; constructor(namespace?: DataNamespace) { super(); if (namespace !== undefined) { if (!(namespace instanceof DatafileRoot)) { throw new ImplementationError("Filesystem storage driver requires a DatafileRoot namespace"); } this.#root = namespace; } } get root(): DatafileRoot | undefined { return this.#root; } async initialize() { if (this.#lock) { throw new ImplementationError("Filesystem storage driver is already initialized"); } if (this.#root) { this.#lock = await this.#root.lock(); } } async close() { await this.#lock?.close(); this.#lock = undefined; } } /** * @deprecated Use {@link StorageDriver}. */ export const Storage = StorageDriver; /** * @deprecated Use {@link StorageDriver}. */ export interface Storage extends StorageDriver {} /** * Extended interface for storage that supports snapshotting. */ export interface CloneableStorage { clone(): MaybePromise; } export namespace CloneableStorage { export function is(storage: T): storage is T & CloneableStorage { return "clone" in storage && typeof storage.clone === "function"; } export function assert(storage: T): asserts storage is T & CloneableStorage { if (!is(storage)) { throw new ImplementationError("Storage does not support required snapshotting function"); } } }