element-web/src/dispatcher/dispatcher.ts
2023-03-08 14:19:05 +00:00

192 lines
6.5 KiB
TypeScript

/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Action } from "./actions";
import { ActionPayload, AsyncActionPayload } from "./payloads";
type DispatchToken = string;
function invariant(cond: any, error: string): void {
if (!cond) throw new Error(error);
}
/**
* A dispatcher for ActionPayloads (the default within the SDK).
* Based on the old Flux dispatcher https://github.com/facebook/flux/blob/main/src/Dispatcher.js
*/
export class MatrixDispatcher {
private readonly callbacks = new Map<DispatchToken, (payload: ActionPayload) => void>();
private readonly isHandled = new Map<DispatchToken, boolean>();
private readonly isPending = new Map<DispatchToken, boolean>();
private pendingPayload?: ActionPayload;
private lastId = 1;
/**
* Registers a callback to be invoked with every dispatched payload. Returns
* a token that can be used with `waitFor()`.
*/
public register(callback: (payload: ActionPayload) => void): DispatchToken {
const id = "ID_" + this.lastId++;
this.callbacks.set(id, callback);
if (this.isDispatching()) {
// If there is a dispatch happening right now then the newly registered callback should be skipped
this.isPending.set(id, true);
this.isHandled.set(id, true);
}
return id;
}
/**
* Removes a callback based on its token.
*/
public unregister(id: DispatchToken): void {
invariant(this.callbacks.has(id), `Dispatcher.unregister(...): '${id}' does not map to a registered callback.`);
this.callbacks.delete(id);
}
/**
* Waits for the callbacks specified to be invoked before continuing execution
* of the current callback. This method should only be used by a callback in
* response to a dispatched payload.
*/
public waitFor(ids: DispatchToken[]): void {
invariant(this.isDispatching(), "Dispatcher.waitFor(...): Must be invoked while dispatching.");
for (const id of ids) {
if (this.isPending.get(id)) {
invariant(
this.isHandled.get(id),
`Dispatcher.waitFor(...): Circular dependency detected while waiting for '${id}'.`,
);
continue;
}
invariant(
this.callbacks.get(id),
`Dispatcher.waitFor(...): '${id}' does not map to a registered callback.`,
);
this.invokeCallback(id);
}
}
/**
* Dispatches a payload to all registered callbacks.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
private _dispatch = (payload: ActionPayload): void => {
invariant(!this.isDispatching(), "Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.");
this.startDispatching(payload);
try {
for (const [id] of this.callbacks) {
if (this.isPending.get(id)) {
continue;
}
this.invokeCallback(id);
}
} finally {
this.stopDispatching();
}
};
/**
* Is this Dispatcher currently dispatching.
*/
public isDispatching(): boolean {
return !!this.pendingPayload;
}
/**
* Call the callback stored with the given id. Also do some internal
* bookkeeping.
*
* Must only be called with an id which has a callback and pendingPayload set
* @internal
*/
private invokeCallback(id: DispatchToken): void {
this.isPending.set(id, true);
this.callbacks.get(id)!(this.pendingPayload!);
this.isHandled.set(id, true);
}
/**
* Set up bookkeeping needed when dispatching.
*
* @internal
*/
private startDispatching(payload: ActionPayload): void {
for (const [id] of this.callbacks) {
this.isPending.set(id, false);
this.isHandled.set(id, false);
}
this.pendingPayload = payload;
}
/**
* Clear bookkeeping used for dispatching.
*
* @internal
*/
private stopDispatching(): void {
this.pendingPayload = undefined;
}
/**
* Dispatches an event on the dispatcher's event bus.
* @param {ActionPayload} payload Required. The payload to dispatch.
* @param {boolean=false} sync Optional. Pass true to dispatch
* synchronously. This is useful for anything triggering
* an operation that the browser requires user interaction
* for. Default false (async).
*/
public dispatch<T extends ActionPayload>(payload: T, sync = false): void {
if (payload instanceof AsyncActionPayload) {
payload.fn((action: ActionPayload) => {
this.dispatch(action, sync);
});
return;
}
if (sync) {
this._dispatch(payload);
} else {
// Unless the caller explicitly asked for us to dispatch synchronously,
// we always set a timeout to do this: The flux dispatcher complains
// if you dispatch from within a dispatch, so rather than action
// handlers having to worry about not calling anything that might
// then dispatch, we just do dispatches asynchronously.
window.setTimeout(this._dispatch, 0, payload);
}
}
/**
* Shorthand for dispatch({action: Action.WHATEVER}, sync). No additional
* properties can be included with this version.
* @param {Action} action The action to dispatch.
* @param {boolean=false} sync Whether the dispatch should be sync or not.
* @see dispatch(action: ActionPayload, sync: boolean)
*/
public fire(action: Action, sync = false): void {
this.dispatch({ action }, sync);
}
}
export const defaultDispatcher = new MatrixDispatcher();
if (!window.mxDispatcher) {
window.mxDispatcher = defaultDispatcher;
}
export default defaultDispatcher;