/* Copyright 2024 New Vector Ltd. Copyright 2020 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import { EventEmitter } from "events"; import AwaitLock from "await-lock"; import { ActionPayload } from "../dispatcher/payloads"; import { MatrixDispatcher } from "../dispatcher/dispatcher"; /** * The event/channel to listen for in an AsyncStore. */ export const UPDATE_EVENT = "update"; /** * Represents a minimal store which works similar to Flux stores. Instead * of everything needing to happen in a dispatch cycle, everything can * happen async to that cycle. * * The store operates by using Object.assign() to mutate state - it sends the * state objects (current and new) through the function onto a new empty * object. Because of this, it is recommended to break out your state to be as * safe as possible. The state mutations are also locked, preventing concurrent * writes. * * All updates to the store happen on the UPDATE_EVENT event channel with the * one argument being the instance of the store. * * To update the state, use updateState() and preferably await the result to * help prevent lock conflicts. */ export abstract class AsyncStore extends EventEmitter { private storeState: Readonly; private lock = new AwaitLock(); private readonly dispatcherRef: string; /** * Creates a new AsyncStore using the given dispatcher. * @param {Dispatcher} dispatcher The dispatcher to rely upon. * @param {T} initialState The initial state for the store. */ protected constructor( private dispatcher: MatrixDispatcher, initialState: T = {}, ) { super(); this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this)); this.storeState = initialState; } /** * The current state of the store. Cannot be mutated. */ protected get state(): T { return this.storeState; } /** * Stops the store's listening functions, such as the listener to the dispatcher. */ protected stop(): void { if (this.dispatcherRef) this.dispatcher.unregister(this.dispatcherRef); } /** * Updates the state of the store. * @param {T|*} newState The state to update in the store using Object.assign() */ protected async updateState(newState: T | Object): Promise { await this.lock.acquireAsync(); try { this.storeState = Object.freeze(Object.assign({}, this.storeState, newState)); this.emit(UPDATE_EVENT, this); } finally { await this.lock.release(); } } /** * Resets the store's to the provided state or an empty object. * @param {T|*} newState The new state of the store. * @param {boolean} quiet If true, the function will not raise an UPDATE_EVENT. */ protected async reset(newState: T | Object | null = null, quiet = false): Promise { await this.lock.acquireAsync(); try { this.storeState = Object.freeze((newState || {})); if (!quiet) this.emit(UPDATE_EVENT, this); } finally { await this.lock.release(); } } /** * Called when the dispatcher broadcasts a dispatch event. * @param {ActionPayload} payload The event being dispatched. */ protected abstract onDispatch(payload: ActionPayload): void; }