import { ReadWriteLock } from "./locks"; import { ReactPromise, pending, rejected, resolved } from "./promises"; import { AsyncResult, Result } from "./results"; import { generateUuid } from "./uuids"; export type ReadonlyStore = { get(): T, onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void }, onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void }, }; export type AsyncStoreStateChangeCallback = (args: { state: AsyncResult, oldState: AsyncResult, lastOkValue: T | undefined }) => void; export type ReadonlyAsyncStore = { isAvailable(): boolean, get(): AsyncResult, getOrWait(): ReactPromise, onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void }, onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void }, onStateChange(callback: AsyncStoreStateChangeCallback): { unsubscribe: () => void }, onceStateChange(callback: AsyncStoreStateChangeCallback): { unsubscribe: () => void }, }; export class Store implements ReadonlyStore { private readonly _callbacks: Map void)> = new Map(); constructor( private _value: T ) {} get(): T { return this._value; } set(value: T): void { const oldValue = this._value; this._value = value; this._callbacks.forEach((callback) => callback(value, oldValue)); } update(updater: (value: T) => T): T { const value = updater(this._value); this.set(value); return value; } onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } { const uuid = generateUuid(); this._callbacks.set(uuid, callback); return { unsubscribe: () => { this._callbacks.delete(uuid); }, }; } onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } { const { unsubscribe } = this.onChange((...args) => { unsubscribe(); callback(...args); }); return { unsubscribe }; } } export const storeLock = new ReadWriteLock(); export class AsyncStore implements ReadonlyAsyncStore { private _isAvailable: boolean; private _mostRecentOkValue: T | undefined = undefined; private _isRejected = false; private _rejectionError: unknown; private readonly _waitingRejectFunctions = new Map void)>(); private readonly _callbacks: Map> = new Map(); private _updateCounter = 0; private _lastSuccessfulUpdate = -1; constructor(...args: [] | [T]) { if (args.length === 0) { this._isAvailable = false; } else { this._isAvailable = true; this._mostRecentOkValue = args[0]; } } isAvailable(): boolean { return this._isAvailable; } isRejected(): boolean { return this._isRejected; } get() { if (this.isRejected()) { return AsyncResult.error(this._rejectionError); } else if (this.isAvailable()) { return AsyncResult.ok(this._mostRecentOkValue as T); } else { return AsyncResult.pending(); } } getOrWait(): ReactPromise { const uuid = generateUuid(); if (this.isRejected()) { return rejected(this._rejectionError); } else if (this.isAvailable()) { return resolved(this._mostRecentOkValue as T); } const promise = new Promise((resolve, reject) => { this.onceChange((value) => { resolve(value); }); this._waitingRejectFunctions.set(uuid, reject); }); const withFinally = promise.finally(() => { this._waitingRejectFunctions.delete(uuid); }); return pending(withFinally); } _setIfLatest(result: Result, curCounter: number) { const oldState = this.get(); const oldValue = this._mostRecentOkValue; if (curCounter > this._lastSuccessfulUpdate) { switch (result.status) { case "ok": { if (!this._isAvailable || this._isRejected || this._mostRecentOkValue !== result.data) { this._lastSuccessfulUpdate = curCounter; this._isAvailable = true; this._isRejected = false; this._mostRecentOkValue = result.data; this._rejectionError = undefined; this._callbacks.forEach((callback) => callback({ state: this.get(), oldState, lastOkValue: oldValue, })); return true; } return false; } case "error": { this._lastSuccessfulUpdate = curCounter; this._isAvailable = false; this._isRejected = true; this._rejectionError = result.error; this._waitingRejectFunctions.forEach((reject) => reject(result.error)); this._callbacks.forEach((callback) => callback({ state: this.get(), oldState, lastOkValue: oldValue, })); return true; } } } return false; } set(value: T): void { this._setIfLatest(Result.ok(value), ++this._updateCounter); } update(updater: (value: T | undefined) => T): T { const value = updater(this._mostRecentOkValue); this.set(value); return value; } async setAsync(promise: Promise): Promise { return await storeLock.withReadLock(async () => { const curCounter = ++this._updateCounter; const result = await Result.fromPromise(promise); return this._setIfLatest(result, curCounter); }); } setUnavailable(): void { this._lastSuccessfulUpdate = ++this._updateCounter; this._mostRecentOkValue = undefined; this._isAvailable = false; this._isRejected = false; this._rejectionError = undefined; } setRejected(error: unknown): void { this._setIfLatest(Result.error(error), ++this._updateCounter); } map(mapper: (value: T) => U): AsyncStore { const store = new AsyncStore(); this.onChange((value) => { store.set(mapper(value)); }); return store; } onChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } { return this.onStateChange(({ state, lastOkValue }) => { if (state.status === "ok") { callback(state.data, lastOkValue); } }); } onStateChange(callback: AsyncStoreStateChangeCallback): { unsubscribe: () => void } { const uuid = generateUuid(); this._callbacks.set(uuid, callback); return { unsubscribe: () => { this._callbacks.delete(uuid); }, }; } onceChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } { const { unsubscribe } = this.onChange((...args) => { unsubscribe(); callback(...args); }); return { unsubscribe }; } onceStateChange(callback: AsyncStoreStateChangeCallback): { unsubscribe: () => void } { const { unsubscribe } = this.onStateChange((...args) => { unsubscribe(); callback(...args); }); return { unsubscribe }; } }