Skip to content

Instantly share code, notes, and snippets.

@ztrehagem
Created September 15, 2023 09:32
Show Gist options
  • Save ztrehagem/8b7866462e87592c404ec6ebcd828bae to your computer and use it in GitHub Desktop.
Save ztrehagem/8b7866462e87592c404ec6ebcd828bae to your computer and use it in GitHub Desktop.
interface Action<T, U = unknown> {
act: (state: T) => Promise<U>;
deriveActual: (state: T, actResult: U) => T;
deriveOptimistic: (state: T) => T;
}
export class OptimisticStore<T> {
#optimisticState: T;
#committedState: T;
readonly #runner = new SerialTaskRunner();
readonly #actionQueue: Action<T>[] = [];
constructor(initialState: T) {
this.#optimisticState = initialState;
this.#committedState = initialState;
}
get latestState(): T {
return this.#optimisticState;
}
describe(): { stable: T; latest: T } {
return { stable: this.#committedState, latest: this.#optimisticState };
}
async dispatch<U>(action: Action<T, U>): Promise<void> {
this.#actionQueue.push(action as Action<T>);
// derive optimistic state
this.#optimisticState = action.deriveOptimistic(this.#optimisticState);
try {
await this.#runner.offer(async () => {
try {
const result = await action.act(this.#committedState);
// derive next stable state with action result
this.#committedState = action.deriveActual(
this.#committedState,
result,
);
// re-derive optimistic state
this.#optimisticState = this.#actionQueue
.slice(1)
.reduce(
(state, action) => action.deriveOptimistic(state),
this.#committedState,
);
} catch (error) {
// rollback optimistic state to last committed state
this.#optimisticState = this.#committedState;
throw error;
}
});
} finally {
// remove settled action from queue
this.#actionQueue.shift();
}
}
}
class SerialTaskRunner {
#lastPromise: Promise<unknown> | null = null;
/**
* Reserve function call after preceded tasks unless one of them throws.
*/
async offer<T>(fn: () => Promise<T>): Promise<T> {
const newPromise = this.#lastPromise?.then(fn) ?? fn();
try {
return await (this.#lastPromise = newPromise);
} finally {
// Cleanup if there are no trailing promises.
if (this.#lastPromise == newPromise) {
this.#lastPromise = null;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment