mirror of https://github.com/sveltejs/svelte
parent
30e2b23b59
commit
da3260f99a
@ -0,0 +1,172 @@
|
|||||||
|
/** @import { Source, Derived } from '#client' */
|
||||||
|
import { state, derived, set, get, tick } from 'svelte/internal/client';
|
||||||
|
import { deferred, noop } from '../internal/shared/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @implements {Partial<Promise<T>>}
|
||||||
|
*/
|
||||||
|
export class Resource {
|
||||||
|
#init = false;
|
||||||
|
|
||||||
|
/** @type {() => Promise<T>} */
|
||||||
|
#fn;
|
||||||
|
|
||||||
|
/** @type {Source<boolean>} */
|
||||||
|
#loading = state(true);
|
||||||
|
|
||||||
|
/** @type {Array<(...args: any[]) => void>} */
|
||||||
|
#latest = [];
|
||||||
|
|
||||||
|
/** @type {Source<boolean>} */
|
||||||
|
#ready = state(false);
|
||||||
|
|
||||||
|
/** @type {Source<T | undefined>} */
|
||||||
|
#raw = state(undefined);
|
||||||
|
|
||||||
|
/** @type {Source<Promise<void>>} */
|
||||||
|
#promise;
|
||||||
|
|
||||||
|
/** @type {Derived<T | undefined>} */
|
||||||
|
#current = derived(() => {
|
||||||
|
if (!get(this.#ready)) return undefined;
|
||||||
|
return get(this.#raw);
|
||||||
|
});
|
||||||
|
|
||||||
|
#onrefresh;
|
||||||
|
|
||||||
|
/** {@type Source<any>} */
|
||||||
|
#error = state(undefined);
|
||||||
|
|
||||||
|
/** @type {Derived<Promise<T>['then']>} */
|
||||||
|
// @ts-expect-error - I feel this might actually be incorrect but I can't prove it yet.
|
||||||
|
// we are technically not returning a promise that resolves to the correct type... but it _is_ a promise that resolves at the correct time
|
||||||
|
#then = derived(() => {
|
||||||
|
const p = get(this.#promise);
|
||||||
|
|
||||||
|
return async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
await p;
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
resolve?.(/** @type {T} */ (get(this.#current)));
|
||||||
|
} catch (error) {
|
||||||
|
reject?.(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {() => Promise<T>} fn
|
||||||
|
* @param {() => Promise<T>} [init]
|
||||||
|
* @param {() => void} [onrefresh]
|
||||||
|
*/
|
||||||
|
constructor(fn, init = fn, onrefresh = noop) {
|
||||||
|
this.#fn = fn;
|
||||||
|
this.#promise = state(this.#run(init));
|
||||||
|
this.#onrefresh = onrefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {() => Promise<T>} fn */
|
||||||
|
#run(fn = this.#fn) {
|
||||||
|
if (this.#init) {
|
||||||
|
set(this.#loading, true);
|
||||||
|
} else {
|
||||||
|
this.#init = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resolve, reject, promise } = deferred();
|
||||||
|
|
||||||
|
this.#latest.push(resolve);
|
||||||
|
|
||||||
|
Promise.resolve(fn())
|
||||||
|
.then((value) => {
|
||||||
|
// Skip the response if resource was refreshed with a later promise while we were waiting for this one to resolve
|
||||||
|
const idx = this.#latest.indexOf(resolve);
|
||||||
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
this.#latest.splice(0, idx).forEach((r) => r());
|
||||||
|
set(this.#ready, true);
|
||||||
|
set(this.#loading, false);
|
||||||
|
set(this.#raw, value);
|
||||||
|
set(this.#error, undefined);
|
||||||
|
|
||||||
|
resolve(undefined);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
const idx = this.#latest.indexOf(resolve);
|
||||||
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
this.#latest.splice(0, idx).forEach((r) => r());
|
||||||
|
set(this.#error, e);
|
||||||
|
set(this.#loading, false);
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
get then() {
|
||||||
|
return get(this.#then);
|
||||||
|
}
|
||||||
|
|
||||||
|
get catch() {
|
||||||
|
get(this.#then);
|
||||||
|
return (/** @type {any} */ reject) => {
|
||||||
|
return get(this.#then)(undefined, reject);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get finally() {
|
||||||
|
get(this.#then);
|
||||||
|
return (/** @type {any} */ fn) => {
|
||||||
|
return get(this.#then)(
|
||||||
|
() => fn(),
|
||||||
|
() => fn()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get current() {
|
||||||
|
return get(this.#current);
|
||||||
|
}
|
||||||
|
|
||||||
|
get error() {
|
||||||
|
return get(this.#error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the resource is loading or reloading.
|
||||||
|
*/
|
||||||
|
get loading() {
|
||||||
|
return get(this.#loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true once the resource has been loaded for the first time.
|
||||||
|
*/
|
||||||
|
get ready() {
|
||||||
|
return get(this.#ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
refresh() {
|
||||||
|
this.#onrefresh();
|
||||||
|
const promise = this.#run();
|
||||||
|
set(this.#promise, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {T} value
|
||||||
|
*/
|
||||||
|
set(value) {
|
||||||
|
set(this.#ready, true);
|
||||||
|
set(this.#loading, false);
|
||||||
|
set(this.#error, undefined);
|
||||||
|
set(this.#raw, value);
|
||||||
|
set(this.#promise, Promise.resolve());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue