/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import type { ClientGroup } from "#node/ClientGroup.js"; import type { ClientNode } from "#node/ClientNode.js"; import type { Node } from "#node/Node.js"; import { Construction, MatterAggregateError, MemoryStorageDriver, StorageContext, StorageManager, } from "@matter/general"; import { ClientNodeStore } from "./ClientNodeStore.js"; /** * Prefix for incrementally assigned client IDs that are usually commissioned, so an increasing ID is used instead the * node id. */ const CLIENT_ID_PREFIX = "peer"; /** * Manages {@link ClientNodeStore}s for a {@link Node}. */ export class ClientNodeStores { #storage: StorageContext; #stores = {} as Record; #construction: Construction; #nextAutomaticId = 1; get construction() { return this.#construction; } constructor(storage: StorageContext) { this.#storage = storage; this.#construction = Construction(this); this.#construction.start(); } async [Construction.construct]() { const contexts = await this.#storage.contexts(); for (const id of contexts) { // Keep the auto-allocation counter ahead of any persisted "peerN" id; custom ids are restored too. if (id.startsWith(CLIENT_ID_PREFIX)) { const num = Number.parseInt(id.slice(CLIENT_ID_PREFIX.length)); if (Number.isFinite(num) && this.#nextAutomaticId <= num) { this.#nextAutomaticId = num + 1; } } this.#createNodeStore(id, true); } await MatterAggregateError.allSettled( Object.values(this.#stores).map(store => store.construction.ready), "Error while initializing client stores", ); } /** * Allocate a local ID for a peer. * * The ID may be preassigned or we will assign using an incrementing sequential number. The number is reserved for * the life of this process or, if data is persisted, until erased. * * The ID is not an identity that outlives the peer. The counter starts from the IDs already in storage, so once a * peer is removed and its data is gone, a peer commissioned later may receive the same ID. Anything stored for * longer than the peer itself cannot rely on either identifier. A `PeerAddress` names one device only for as long as * that device stays commissioned. Past that it may be issued to another device under the rules * `ControllerBehavior.allocatePeerAddress` documents, and a lookup can then name that other device. */ allocateId() { this.#construction.assert(); return `${CLIENT_ID_PREFIX}${this.#nextAutomaticId++}`; } /** * Get the store for a single {@link ClientNode} or peer Id. * * These stores are cached internally by Id. */ storeForNode(nodeOrId: ClientNode | string): ClientNodeStore { this.#construction.assert(); if (typeof nodeOrId !== "string") { nodeOrId = nodeOrId.id; } const store = this.#stores[nodeOrId]; if (store) { return store; } return this.#createNodeStore(nodeOrId); } storeForGroup(node: ClientGroup): ClientNodeStore { this.#construction.assert(); const store = this.#stores[node.id]; if (store) { return store; } return this.#createGroupStore(node.id); } /** * List all nodes present. */ get knownIds() { this.#construction.assert(); return Object.keys(this.#stores); } /** * Discard all persisted node data. The collection remains usable and IDs are reassigned from the start. */ async erase() { this.#construction.assert(); const stores = Object.values(this.#stores); this.#stores = {}; await MatterAggregateError.settleSeries( [ ...stores.map(store => () => store.erase()), // Data a store failed to erase must still go: the index is gone either way, so anything left behind // resurrects on the next lookup () => this.#storage.clearAll(), () => void (this.#nextAutomaticId = 1), ], "Error while erasing client stores", ); } async close() { await this.construction.close(async () => { const stores = Object.values(this.#stores); this.#stores = {}; await MatterAggregateError.allSettled( stores.map(store => store.construction.close()), "Error while closing client stores", ); }); } /** * Group stores are always created with a memory backend as they are transient. */ #createGroupStore(id: string) { const manager = new StorageManager(new MemoryStorageDriver()); manager.initialize(); const store = new ClientNodeStore(id, manager.createContext(id), false); store.construction.start(); this.#stores[id] = store; return store; } #createNodeStore(id: string, isPreexisting = false) { const store = new ClientNodeStore( id, this.#storage.createContext(id), isPreexisting, () => delete this.#stores[id], ); store.construction.start(); this.#stores[id] = store; return store; } }