Skip to content

Instantly share code, notes, and snippets.

@arleighdickerson
Last active March 11, 2024 19:38
Show Gist options
  • Save arleighdickerson/6ea89063da981128bc99f27247273e69 to your computer and use it in GitHub Desktop.
Save arleighdickerson/6ea89063da981128bc99f27247273e69 to your computer and use it in GitHub Desktop.
export interface StandardAction {
type: string;
payload?: any;
error?: any;
meta?: any;
}
export interface IDispatchStore {
handleDispatch: (action: StandardAction) => void;
dispatchToken: string;
dispatcher: IDispatcher;
}
export interface IDispatcher {
isDispatching: boolean;
register: (callback: (action: StandardAction) => void) => string;
unregister: (id: string) => void;
waitFor: (ids: string[]) => void;
dispatch: (action: StandardAction) => void;
}
// @Component
export class Dispatcher implements IDispatcher {
private static __prefix__ = 0;
private readonly idPrefix = `[${Dispatcher.__prefix__++}]`;
private readonly callbacks: {
[key: string]: (action: StandardAction) => void;
} = {};
private readonly handled: { [key: string]: boolean } = {};
private readonly pending: { [key: string]: boolean } = {};
private lastId = 1;
private pendingAction?: StandardAction;
private _isDispatching = false;
get isDispatching(): boolean {
return this._isDispatching;
}
register(callback: (action: StandardAction) => void): string {
const id = this.idPrefix + this.lastId++;
this.callbacks[id] = callback;
return id;
}
unregister(id: string): void {
if (!this.callbacks[id]) {
throw new Error('Dispatcher.unregister(...): "+ id +" does not map to a registered callback.');
}
delete this.callbacks[id];
}
waitFor(ids: string[]): void {
if (!this._isDispatching) {
throw new Error('Dispatcher.waitFor(...): Must be invoked while dispatching.');
}
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
if (this.pending[id]) {
if (!this.handled[id]) {
throw new Error(`Dispatcher.waitFor(...): Circular dependency detected while waiting for ${id}`);
}
continue;
}
if (!this.callbacks[id]) {
throw new Error(`Dispatcher.waitFor(...): ${id} does not map to a registered callback.`);
}
this.invokeCallback(id);
}
}
dispatch(action: StandardAction): void {
// logger.debug(`Dispatching: ${action.type}`);
if (this._isDispatching) {
throw new Error('Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.');
}
this.startDispatching(action);
try {
for (const id in this.callbacks) {
if (this.pending[id]) {
continue;
}
this.invokeCallback(id);
}
} finally {
this.stopDispatching();
}
}
private invokeCallback(id: string): void {
this.pending[id] = true;
this.callbacks[id](this.pendingAction as StandardAction);
this.handled[id] = true;
}
private startDispatching(action: StandardAction): void {
Object.keys(this.callbacks).forEach(id => {
this.pending[id] = false;
this.handled[id] = false;
});
this.pendingAction = action;
this._isDispatching = true;
}
private stopDispatching(): void {
delete this.pendingAction;
this._isDispatching = false;
}
}
export abstract class DispatchStore implements IDispatchStore {
private _dispatchToken?: string;
abstract readonly dispatcher: Dispatcher;
get dispatchToken(): string {
if (!this._dispatchToken) {
throw new Error('self._dispatchToken is not set');
}
return this._dispatchToken;
}
abstract handleDispatch(action: StandardAction): void;
// @PostConstruct
protected registerWithDispatcher() {
this._dispatchToken = this.dispatcher.register(this.handleDispatch);
}
// @PreDestroy
protected unregisterWithDispatcher() {
this.dispatcher.unregister(this.dispatchToken);
}
}
type ThunkAction = (dispatch: IDispatcher['dispatch']) => Promise<void>;
export function createThunkDispatch(dispatcher: IDispatcher) {
const dispatch = async (action: ThunkAction | StandardAction) => {
if (action instanceof Function) {
await action(dispatch);
} else {
dispatcher.dispatch(action);
}
};
return dispatch;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment