UNPKG

@mittwald/react-use-promise

Version:

Simple and declarative use of Promises in your React components. Observe their state and refresh them in various advanced ways.

148 lines (147 loc) 5.06 kB
import { ConsolidatedTimeout } from "../lib/ConsolidatedTimeout.js"; import { emptyValue, setValue } from "../lib/EventualValue.js"; import { ObservableValue } from "../observable-value/ObservableValue.js"; import { useWatchObservableValue } from "../observable-value/useWatchObservableValue.js"; import { useWatchResourceValue } from "./useWatchResourceValue.js"; import { loaderContext } from "./context.js"; export class AsyncResource { loader; meta; loaderPromise; suspensePromise; resolveSuspensePromise = () => { throw new Error("Resolving initial suspense promise is not supported"); }; loaderPromiseVersion = 0; autoRefreshTimeout; value = new ObservableValue(emptyValue); valueWithCache = new ObservableValue(emptyValue); error = new ObservableValue(emptyValue); syncValue = emptyValue; syncError = emptyValue; state = new ObservableValue("void"); onRefreshListeners = new Set(); static onBeforeRefreshListeners = new Set(); static onLoadListeners = new Set(); static voidInstance = new AsyncResource(() => Promise.resolve(undefined)); constructor(loader, meta = {}) { this.loader = this.buildLoaderWithContext(loader); this.meta = meta; this.autoRefreshTimeout = new ConsolidatedTimeout(() => this.refresh()); this.resetPromises(); } buildLoaderWithContext(newLoader) { return loaderContext.bind({ asyncResource: this, }, newLoader); } updateLoader(newLoader) { this.loader = this.buildLoaderWithContext(newLoader); } refresh() { this.callListeners(AsyncResource.onBeforeRefreshListeners); this.loaderPromiseVersion++; this.resetPromises(); this.value.updateValue(emptyValue); this.error.updateValue(emptyValue); this.state.updateValue("void"); this.callListeners(this.onRefreshListeners); } callListeners(listeners) { listeners.forEach((handler) => handler(this)); } onRefresh(handler) { this.onRefreshListeners.add(handler); return () => { this.onRefreshListeners.delete(handler); }; } static onBeforeRefresh(handler) { this.onBeforeRefreshListeners.add(handler); return () => { this.onBeforeRefreshListeners.delete(handler); }; } static onLoad(handler) { this.onLoadListeners.add(handler); return () => { this.onLoadListeners.delete(handler); }; } addTTL(ttl) { return this.autoRefreshTimeout.addTimeout(ttl); } load() { if (this.value.value.isSet || this.error.value.isSet) { return; } if (this.loaderPromise === undefined) { const loadingResult = this.handleLoading(); if (loadingResult instanceof Promise) { this.loaderPromise = loadingResult; } } } resetPromises() { this.suspensePromise = new Promise((resolve) => { this.resolveSuspensePromise = resolve; }); this.loaderPromise = undefined; } isMatchingError(error) { if (!this.error.value.isSet) { return false; } return error === true || this.error.value.value === error; } handleLoading() { try { const loaderResult = this.loader(); if (loaderResult instanceof Promise) { this.syncValue = emptyValue; this.syncError = emptyValue; return this.handleAsyncLoading(loaderResult); } this.syncValue = setValue(loaderResult); } catch (e) { this.syncError = setValue(e); } this.callListeners(AsyncResource.onLoadListeners); this.autoRefreshTimeout.start(); } async handleAsyncLoading(loaderPromise) { const loaderPromiseVersion = ++this.loaderPromiseVersion; let result = emptyValue; let error = emptyValue; this.state.updateValue("loading"); try { const awaitedResult = await loaderPromise; result = setValue(awaitedResult); } catch (e) { error = setValue(e); } this.resolveSuspensePromise(); if (this.loaderPromiseVersion === loaderPromiseVersion) { this.resetPromises(); if (result.isSet) { this.valueWithCache.updateValue(result); this.value.updateValue(result); this.state.updateValue("loaded"); } else if (error.isSet) { this.error.updateValue(error); this.state.updateValue("error"); } this.callListeners(AsyncResource.onLoadListeners); this.autoRefreshTimeout.start(); } } use(options = {}) { return useWatchResourceValue(this, options); } watchState() { return useWatchObservableValue(this.state); } }