/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { StorageDriver, StorageError } from "./StorageDriver.js"; import { SupportedStorageTypes, fromJson, toJson } from "./StringifyTools.js"; /** * Async key-value interface that web-style storage backends must implement. * * Mirrors the subset of the `@react-native-async-storage/async-storage` v3 API that storage drivers actually use. */ export interface WebStorageProvider { getItem(key: string): Promise; setItem(key: string, value: string): Promise; removeItem(key: string): Promise; getAllKeys(): Promise; getMany(keys: string[]): Promise>; setMany(entries: Record): Promise; removeMany(keys: string[]): Promise; clear(): Promise; } /** * {@link StorageDriver} for web-style async key-value stores. * * Shared implementation for any backend that satisfies the {@link WebStorageProvider} interface (e.g. React Native * AsyncStorage v2/v3, browser localStorage wrapper, etc.). */ export class WebStorageDriver extends StorageDriver { #storage: WebStorageProvider; #initialized = false; constructor(storage: WebStorageProvider) { super(); this.#storage = storage; } get initialized() { return this.#initialized; } initialize() { if (this.#initialized) { throw new StorageError("Storage already initialized!"); } this.#initialized = true; } close() { this.#initialized = false; } override clear() { return this.#storage.clear(); } getContextBaseKey(contexts: string[]) { for (const ctx of contexts) { if (!ctx.length || ctx.includes(".")) { throw new StorageError("Context must not contain empty segments or leading or trailing dots."); } } return contexts.join("."); } buildStorageKey(contexts: string[], key: string) { if (!key.length) { throw new StorageError("Key must not be an empty string."); } const contextKey = this.getContextBaseKey(contexts); return contextKey.length ? `${contextKey}.${key}` : key; } async get(contexts: string[], key: string): Promise { const value = await this.#storage.getItem(this.buildStorageKey(contexts, key)); if (value === null) return undefined; return fromJson(value) as T; } set(contexts: string[], key: string, value: SupportedStorageTypes): Promise; set(contexts: string[], values: Record): Promise; async set( contexts: string[], keyOrValues: string | Record, value?: SupportedStorageTypes, ) { if (typeof keyOrValues === "string") { await this.#storage.setItem(this.buildStorageKey(contexts, keyOrValues), toJson(value)); } else { const entries = {} as Record; for (const [key, value] of Object.entries(keyOrValues)) { entries[this.buildStorageKey(contexts, key)] = toJson(value); } await this.#storage.setMany(entries); } } delete(contexts: string[], key: string) { return this.#storage.removeItem(this.buildStorageKey(contexts, key)); } async keys(contexts: string[]) { const contextKey = this.getContextBaseKey(contexts); const keys = []; const allKeys = await this.#storage.getAllKeys(); if (contextKey.length) { const contextKeyStart = `${contextKey}.`; for (const key of allKeys) { if (key.startsWith(contextKeyStart) && !key.includes(".", contextKeyStart.length)) { keys.push(key.substring(contextKeyStart.length)); } } } else { for (const key of allKeys) { if (!key.includes(".")) { keys.push(key); } } } return keys; } async values(contexts: string[]) { const keys = await this.keys(contexts); const storageKeys = keys.map(key => this.buildStorageKey(contexts, key)); const entries = await this.#storage.getMany(storageKeys); const values = {} as Record; for (const [index, key] of keys.entries()) { const value = entries[storageKeys[index]]; if (value !== null && value !== undefined) { values[key] = fromJson(value) as SupportedStorageTypes; } } return values; } async contexts(contexts: string[]) { const contextKey = this.getContextBaseKey(contexts); const startContextKey = contextKey.length ? `${contextKey}.` : ""; const foundContexts = new Set(); const allKeys = await this.#storage.getAllKeys(); for (const key of allKeys) { if (key.startsWith(startContextKey)) { const subKeys = key.substring(startContextKey.length).split("."); if (subKeys.length === 1) continue; // found leaf key const context = subKeys[0]; foundContexts.add(context); } } return Array.from(foundContexts); } async clearAll(contexts: string[]) { const contextKey = this.getContextBaseKey(contexts); const startContextKey = contextKey.length ? `${contextKey}.` : ""; const allKeys = await this.#storage.getAllKeys(); const keysToDelete = []; for (const key of allKeys) { if (key.startsWith(startContextKey)) { keysToDelete.push(key); } } if (keysToDelete.length) { await this.#storage.removeMany(keysToDelete); } } }