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