rdesign/frontend/node_modules/signal-utils/dist/async-function.ts.js

215 lines
5.9 KiB
JavaScript

import { Signal } from 'signal-polyfill';
import { SignalAsyncData } from './async-data.ts.js';
/**
* Any tracked data accessed in a tracked function _before_ an `await`
* will "entangle" with the function -- we can call these accessed tracked
* properties, the "tracked prelude". If any properties within the tracked
* payload change, the function will re-run.
*/
function signalFunction(fn) {
if (arguments.length === 1) {
if (typeof fn !== "function") {
throw new Error("signalFunction must be called with a function passed");
}
return new State(fn);
}
throw new Error("Unknown arity: signalFunction must be called with 1 argument");
}
/**
* State container that represents the asynchrony of a `signalFunction`
*/
class State {
#data = new Signal.State(null);
get data() {
this.#computed.get();
return this.#data.get();
}
#promise = new Signal.State(undefined);
get promise() {
this.#computed.get();
return this.#promise.get();
}
/**
* ember-async-data doesn't catch errors,
* so we can't rely on it to protect us from "leaky errors"
* during rendering.
*
* See also: https://github.com/qunitjs/qunit/issues/1736
*/
#caughtError = new Signal.State(undefined);
get caughtError() {
this.#computed.get();
return this.#caughtError.get();
}
#fn;
#computed;
constructor(fn) {
this.#fn = fn;
this.#computed = new Signal.Computed(() => {
this.retry();
return this;
});
}
get state() {
this.#computed.get();
return this.data?.state ?? "UNSTARTED";
}
/**
* Initially true, and remains true
* until the underlying promise resolves or rejects.
*/
get isPending() {
this.#computed.get();
if (!this.data) return true;
return this.data.isPending ?? false;
}
/**
* Alias for `isResolved || isRejected`
*/
get isFinished() {
this.#computed.get();
return this.isResolved || this.isRejected;
}
/**
* Alias for `isFinished`
* which is in turn an alias for `isResolved || isRejected`
*/
get isSettled() {
this.#computed.get();
return this.isFinished;
}
/**
* Alias for `isPending`
*/
get isLoading() {
this.#computed.get();
return this.isPending;
}
/**
* When true, the function passed to `signalFunction` has resolved
*/
get isResolved() {
this.#computed.get();
return this.data?.isResolved ?? false;
}
/**
* Alias for `isRejected`
*/
get isError() {
this.#computed.get();
return this.isRejected;
}
/**
* When true, the function passed to `signalFunction` has errored
*/
get isRejected() {
this.#computed.get();
return this.data?.isRejected ?? Boolean(this.caughtError) ?? false;
}
/**
* this.data may not exist yet.
*
* Additionally, prior iterations of TrackedAsyncData did
* not allow the accessing of data before
* .state === 'RESOLVED' (isResolved).
*
* From a correctness standpoint, this is perfectly reasonable,
* as it forces folks to handle the states involved with async functions.
*
* The original version of `signalFunction` did not use TrackedAsyncData,
* and did not have these strictnesses upon property access, leaving folks
* to be as correct or as fast/prototype-y as they wished.
*
* For now, `signalFunction` will retain that flexibility.
*/
get value() {
this.#computed.get();
if (this.data?.isResolved) {
// This is sort of a lie, but it ends up working out due to
// how promises chain automatically when awaited
return this.data.value;
}
return null;
}
/**
* When the function passed to `signalFunction` throws an error,
* that error will be the value returned by this property
*/
get error() {
this.#computed.get();
if (this.state === "UNSTARTED" && this.caughtError) {
return this.caughtError;
}
if (this.data?.state !== "REJECTED") {
return null;
}
if (this.caughtError) {
return this.caughtError;
}
return this.data?.error ?? null;
}
/**
* Will re-invoke the function passed to `signalFunction`
* this will also re-set some properties on the `State` instance.
* This is the same `State` instance as before, as the `State` instance
* is tied to the `fn` passed to `signalFunction`
*
* `error` or `resolvedValue` will remain as they were previously
* until this promise resolves, and then they'll be updated to the new values.
*/
retry = async () => {
try {
/**
* This function has two places where it can error:
* - immediately when inovking `fn` (where auto-tracking occurs)
* - after an await, "eventually"
*/
await this.#dangerousRetry();
} catch (e) {
this.#caughtError.set(e);
}
};
async #dangerousRetry() {
// We've previously had data, but we're about to run-again.
// we need to do this again so `isLoading` goes back to `true` when re-running.
// NOTE: we want to do this _even_ if this.data is already null.
// it's all in the same tracking frame and the important thing is that
// we can't *read* data here.
this.#data.set(null);
// We need to invoke this before going async so that tracked properties are
// consumed (entangled with) synchronously
this.#promise.set(this.#fn());
// TrackedAsyncData interacts with tracked data during instantiation.
// We don't want this internal state to entangle with `signalFunction`
// so that *only* the tracked data in `fn` can be entangled.
await Promise.resolve();
/**
* Before we await to start a new request, let's clear our error.
* This is detached from the tracking frame (via the above await),
* se the UI can update accordingly, without causing us to refetch
*/
this.#caughtError.set(null);
this.#data.set(new SignalAsyncData(this.promise));
return this.promise;
}
}
export { State, signalFunction };
//# sourceMappingURL=async-function.ts.js.map