// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview a base class and utility types for managing javascript WebUI
* state in a redux-like fashion.
*/
export interface Action {
name: string;
}
export type DeferredAction<A extends Action = Action> =
(callback: (p: A|null) => void) => void;
export type Reducer<S, A extends Action = Action> = (state: S, action: A) => S;
export interface StoreObserver<S> {
onStateChanged(newState: S): void;
}
/**
* A generic datastore for the state of a page, where the state is publicly
* readable but can only be modified by dispatching an Action.
* The Store should be extended by specifying S, the page state type
* associated with the store.
*/
export class Store<S, A extends Action = Action> {
data: S;
private reducer_: Reducer<S, A>;
protected initialized_: boolean = false;
private queuedActions_: Array<DeferredAction<A>> = [];
private observers_: Set<StoreObserver<S>> = new Set();
private batchMode_: boolean = false;
constructor(emptyState: S, reducer: Reducer<S, A>) {
this.data = emptyState;
this.reducer_ = reducer;
}
init(initialState: S) {
this.data = initialState;
this.queuedActions_.forEach((action) => {
this.dispatchInternal_(action);
});
this.queuedActions_ = [];
this.initialized_ = true;
this.notifyObservers_(this.data);
}
isInitialized(): boolean {
return this.initialized_;
}
addObserver(observer: StoreObserver<S>) {
this.observers_.add(observer);
}
removeObserver(observer: StoreObserver<S>) {
this.observers_.delete(observer);
}
hasObserver(observer: StoreObserver<S>): boolean {
return this.observers_.has(observer);
}
/**
* Begin a batch update to store data, which will disable updates to the
* UI until `endBatchUpdate` is called. This is useful when a single UI
* operation is likely to cause many sequential model updates (eg, deleting
* 100 bookmarks).
*/
beginBatchUpdate() {
this.batchMode_ = true;
}
/**
* End a batch update to the store data, notifying the UI of any changes
* which occurred while batch mode was enabled.
*/
endBatchUpdate() {
this.batchMode_ = false;
this.notifyObservers_(this.data);
}
/**
* Handles a 'deferred' action, which can asynchronously dispatch actions
* to the Store in order to reach a new UI state. DeferredActions have the
* form `dispatchAsync(function(dispatch) { ... })`). Inside that function,
* the |dispatch| callback can be called asynchronously to dispatch Actions
* directly to the Store.
*/
dispatchAsync(action: DeferredAction<A>) {
if (!this.initialized_) {
this.queuedActions_.push(action);
return;
}
this.dispatchInternal_(action);
}
/**
* Transition to a new UI state based on the supplied |action|, and notify
* observers of the change. If the Store has not yet been initialized, the
* action will be queued and performed upon initialization.
*/
dispatch(action: A|null) {
this.dispatchAsync(function(dispatch) {
dispatch(action);
});
}
private dispatchInternal_(action: DeferredAction<A>) {
action(this.reduce.bind(this));
}
protected reduce(action: A|null) {
if (!action) {
return;
}
this.data = this.reducer_(this.data, action);
// Batch notifications until after all initialization queuedActions are
// resolved.
if (this.isInitialized() && !this.batchMode_) {
this.notifyObservers_(this.data);
}
}
protected notifyObservers_(state: S) {
this.observers_.forEach(function(o) {
o.onStateChanged(state);
});
}
}