/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Endpoint } from "#endpoint/Endpoint.js"; import type { ServerNode } from "#node/ServerNode.js"; import { asyncNew, type BlobStorageDriver, type BlobStorageHandle, DatafileRoot, Destructable, Diagnostic, Environment, ImplementationError, Logger, MatterAggregateError, StorageManager, StorageService, } from "@matter/general"; import { ClientNodeStores } from "../client/ClientNodeStores.js"; import { NodeStore } from "../NodeStore.js"; import { ServerEndpointStores } from "./ServerEndpointStores.js"; const logger = Logger.get("ServerNodeStore"); /** * {@link ServerNode} persistence. * * Each {@link ServerNode} has an instance of this store. * * Exclusive access is enforced by {@link Directory.lock} when a filesystem-backed storage driver is in use. */ export class ServerNodeStore extends NodeStore implements Destructable { #env: Environment; #nodeId: string; #endpointStores: ServerEndpointStores; #storageManager?: StorageManager; #clientStores?: ClientNodeStores; #bdxHandle?: BlobStorageHandle; constructor(environment: Environment, nodeId: string) { super({ createContext: (name: string) => { if (!this.#storageManager) { throw new ImplementationError( `Cannot create storage context ${name} because store is not initialized`, ); } return this.#storageManager.createContext(name); }, }); this.#endpointStores = new ServerEndpointStores(); this.#env = environment; this.#nodeId = nodeId; this.construction.start(); } static async create(environment: Environment, nodeId: string) { return await asyncNew(this, environment, nodeId); } async close() { await this.construction.close(async () => { await this.#endpointStores.close(); await this.#clientStores?.close(); await this.#bdxHandle?.close(); await this.#storageManager?.close(); await this.#env.get(StorageService).close(this.#nodeId); this.#logChange("Closed"); }); } /** * Stores associated with server endpoints supported by this node. */ get endpointStores() { return this.construction.assert("endpoint stores", this.#endpointStores); } /** * Stores associated with remote nodes known by this node. */ get clientStores() { return this.construction.assert("client stores", this.#clientStores); } /** * The underlying {@link StorageManager} that provides node data. */ get storage() { return this.construction.assert("storage manager", this.#storageManager); } #logChange(what: "Opened" | "Closed") { const root = this.#env.has(DatafileRoot) ? this.#env.get(DatafileRoot) : undefined; logger.info( what, Diagnostic.strong(this.#nodeId ?? "node"), "storage", Diagnostic.dict({ location: root?.path ?? "(unknown location)", driver: this.#storageManager?.driverId ?? "unknown", // Only meaningful on close, where a non-zero count means a holder did not release ...(what === "Closed" && root ? { locks: root.lockCount } : {}), }), ); } storeForEndpoint(endpoint: Endpoint) { return this.#endpointStores.storeForEndpoint(endpoint); } /** * Lazily opens and returns the BDX blob storage driver using a dedicated namespace. */ override async bdxStore(): Promise { if (!this.#bdxHandle) { const root = this.#env.has(DatafileRoot) ? this.#env.get(DatafileRoot) : undefined; const blobNamespace = root ? new DatafileRoot(root.directory.directory(`${this.#nodeId}-bdx`)) : `${this.#nodeId}-bdx`; this.#bdxHandle = await this.#env.get(StorageService).openBlobStorage(blobNamespace); } return this.#bdxHandle.driver; } /** * Discard the endpoint, peer and BDX data persisted for the node. Each is erased even if another fails, so a * single failure cannot leave the others behind. */ async erase() { await MatterAggregateError.allSettled( [this.#clientStores?.erase(), this.#endpointStores.erase(), this.#eraseBdxStore()], "Error while erasing node storage", ); } /** * BDX blobs live in a namespace of their own, which the node's other storage does not reach. The namespace opens * on first use, so a reset with no transfer behind it opens one to erase what an earlier session left, then * releases it again rather than holding storage the node has not asked for. */ async #eraseBdxStore() { const opened = this.#bdxHandle !== undefined; if (!opened && !(await this.#hasBdxStore())) { return; } const driver = await this.bdxStore(); try { await driver.clearAll([]); } finally { if (!opened) { const handle = this.#bdxHandle; this.#bdxHandle = undefined; await handle?.close(); } } } async #hasBdxStore() { if (!this.#env.get(StorageService).isBlobConfigured) { return false; } // A filesystem namespace is visible without opening it, so an absent one is left alone rather than created. // Any other driver may be persistent and offers no such test, so its namespace is opened to find out const root = this.#env.has(DatafileRoot) ? this.#env.get(DatafileRoot) : undefined; if (root === undefined) { return true; } return root.directory.directory(`${this.#nodeId}-bdx`).exists(); } async load() { const root = this.#env.has(DatafileRoot) ? this.#env.get(DatafileRoot) : undefined; this.#storageManager = await this.#env.get(StorageService).open(root ?? this.#nodeId); this.#env.set(StorageManager, this.#storageManager); this.#clientStores = await asyncNew(ClientNodeStores, this.#storageManager.createContext("nodes")); const rootContext = this.storageFactory.createContext("root"); await this.#endpointStores.load(rootContext); this.#logChange("Opened"); } }