@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
JavaScript
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);
}
}