UNPKG

jotai-eager

Version:

Jōtai utilities that help with asynchronous atoms

254 lines (245 loc) 6.53 kB
import { atom } from 'jotai/vanilla'; const PENDING = { status: "pending" }; const promiseMetaCache = /* @__PURE__ */ new WeakMap(); function isPromiseLike(value) { return typeof value?.then === "function"; } function getPromiseMeta(promise) { if (!isPromiseLike(promise)) { return void 0; } return promiseMetaCache.get(promise) ?? PENDING; } function setPromiseMeta(promise, meta) { promiseMetaCache.set(promise, meta); } function isKnown(value) { const meta = getPromiseMeta(value); if (meta) { return meta.status === "fulfilled"; } return true; } function getFulfilledValue(promiseOrValue) { const meta = getPromiseMeta(promiseOrValue); if (meta) { return meta.value; } return promiseOrValue; } function soon(first, second) { if (second) { return _soonImpl(first, second); } return (data) => { return _soonImpl(data, first); }; } function _soonImpl(data, process) { const meta = getPromiseMeta(data); if (meta) { if (meta.status === "fulfilled") { return process(meta.value); } if (meta.status === "rejected") { return Promise.reject(meta.reason); } const promise = data; const transformedPromise = promise.then( (value) => { setPromiseMeta(promise, { status: "fulfilled", value }); return process(value); }, (reason) => { setPromiseMeta(promise, { status: "rejected", reason }); throw reason; } ); return transformedPromise; } try { return process(data); } catch (err) { return Promise.reject(err); } } function soonAll(values) { if (values.every(isKnown)) { return values.map((el) => getFulfilledValue(el)); } return Promise.all(values).then((fulfilledValues) => { fulfilledValues.map((fulfilled, idx) => { const promise = values[idx]; if (isPromiseLike(promise)) { setPromiseMeta(promise, { status: "fulfilled", value: fulfilled }); } }); return fulfilledValues; }); } function derive(deps, op) { return atom((get) => { try { return soon( soonAll(deps.map(get)), (values) => op(...values) ); } catch (err) { return Promise.reject(err); } }); } const NotYet = Symbol( "(jotai-eager) Not all dependencies were fulfilled. Are you a dev? Call `isEagerError(e)` to detect this thrown value and rethrow it, as its handled by the library." ); function unwrapPromise(promise) { const meta = getPromiseMeta(promise); if (!meta) { return promise; } if (meta.status === "pending") { throw { [NotYet]: promise }; } if (meta.status === "rejected") { throw meta.reason; } return meta.value; } function resolveSuspension(compute, signal) { try { return compute(); } catch (e) { const suspended = e[NotYet]; if (suspended) { return suspended.then( (value) => { setPromiseMeta(suspended, { status: "fulfilled", value }); if (signal.aborted) { return void 0; } return resolveSuspension(compute, signal); }, (reason) => { if (signal.aborted) { return void 0; } setPromiseMeta(suspended, { status: "rejected", reason }); throw reason; } ); } return Promise.reject(e); } } function eagerAtom(...args) { const [read, write] = args; return atom( (get, { signal }) => { const eagerGet = ((atomToGet) => unwrapPromise(get(atomToGet))); eagerGet.all = (atoms) => atoms.map((a) => get(a)).map((v) => unwrapPromise(v)); eagerGet.await = (promiseOrValue) => unwrapPromise(promiseOrValue); eagerGet.awaitAll = (values) => values.map((v) => unwrapPromise(v)); return resolveSuspension(() => read(eagerGet), signal); }, write ?? (() => void 0) ); } function isEagerError(error) { return !!error?.[NotYet]; } const defaultFallback = () => void 0; function withPending(anAtom, fallback = defaultFallback) { const refreshAtom = atom(0); if (import.meta.env?.MODE !== "production") { refreshAtom.debugPrivate = true; } const promiseAndValueAtom = atom( (get, { setSelf }) => { get(refreshAtom); const prev = get(promiseAndValueAtom); const promise = get(anAtom); if (!isPromiseLike(promise)) { return { v: promise }; } const meta = getPromiseMeta(promise); if (meta?.status === "fulfilled") { return { p: promise, v: meta.value }; } if (meta?.status === "rejected") { throw meta.reason; } if (promise !== prev?.p) { promise.then( (value) => { setPromiseMeta(promise, { status: "fulfilled", value }); setSelf(); }, (reason) => { setPromiseMeta(promise, { status: "rejected", reason }); setSelf(); } ); } if (prev && "v" in prev) { return { p: promise, f: fallback({ get, prev: prev.v, pending: promise }), v: prev.v }; } return { p: promise, f: fallback({ get, prev: void 0, pending: promise }) }; }, (_get, set) => { set(refreshAtom, (c) => c + 1); } ); promiseAndValueAtom.init = void 0; if (import.meta.env?.MODE !== "production") { promiseAndValueAtom.debugPrivate = true; } return atom( (get) => { const state = get(promiseAndValueAtom); if ("f" in state) { return state.f; } return state.v; }, (_get, set, ...args) => set(anAtom, ...args) ); } const cache1 = /* @__PURE__ */ new WeakMap(); const memo1 = (create, dep1) => (cache1.has(dep1) ? cache1 : cache1.set(dep1, create())).get(dep1); const Pending = Symbol("The loadable is pending"); function loadable(anAtom) { return memo1(() => { const atomWithPending = withPending(anAtom, () => Pending); if (import.meta.env?.MODE !== "production") { atomWithPending.debugPrivate = true; } return atom((get) => { let value; try { value = get(atomWithPending); } catch (error) { return { state: "hasError", error }; } if (value === Pending) { return { state: "loading" }; } return { state: "hasData", data: value }; }); }, anAtom); } export { derive, eagerAtom, isEagerError, loadable, soon, soonAll, withPending };