mirror of https://github.com/sveltejs/svelte
parent
5de63834dc
commit
8449ea76d8
@ -0,0 +1,134 @@
|
|||||||
|
import { tick } from '../runtime.js';
|
||||||
|
import { render_effect } from './effects.js';
|
||||||
|
|
||||||
|
/** @typedef {{ count: number, item: any }} Entry */
|
||||||
|
/** @type {Map<string, Entry>} */
|
||||||
|
const client_cache = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template TReturn
|
||||||
|
* @template {unknown} TArg
|
||||||
|
* @param {string} name
|
||||||
|
* @param {(arg: TArg, key: string) => TReturn} fn
|
||||||
|
* @param {{ hash?: (arg: TArg) => string }} [options]
|
||||||
|
* @returns {(arg: TArg) => TReturn}
|
||||||
|
*/
|
||||||
|
export function cache(name, fn, { hash = default_hash } = {}) {
|
||||||
|
return (arg) => {
|
||||||
|
const key = `${name}::::${hash(arg)}`;
|
||||||
|
const cached = client_cache.has(key);
|
||||||
|
const entry = client_cache.get(key);
|
||||||
|
const maybe_remove = create_remover(key);
|
||||||
|
|
||||||
|
let tracking = true;
|
||||||
|
try {
|
||||||
|
render_effect(() => {
|
||||||
|
if (entry) entry.count++;
|
||||||
|
return () => {
|
||||||
|
const entry = client_cache.get(key);
|
||||||
|
if (!entry) return;
|
||||||
|
entry.count--;
|
||||||
|
maybe_remove(entry);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
tracking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
return entry?.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = fn(arg, key);
|
||||||
|
const new_entry = {
|
||||||
|
item,
|
||||||
|
count: tracking ? 1 : 0
|
||||||
|
};
|
||||||
|
client_cache.set(key, new_entry);
|
||||||
|
|
||||||
|
Promise.resolve(item).then(
|
||||||
|
() => maybe_remove(new_entry),
|
||||||
|
() => maybe_remove(new_entry)
|
||||||
|
);
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
*/
|
||||||
|
function create_remover(key) {
|
||||||
|
/**
|
||||||
|
* @param {Entry | undefined} entry
|
||||||
|
*/
|
||||||
|
return (entry) =>
|
||||||
|
tick().then(() => {
|
||||||
|
if (!entry?.count && entry === client_cache.get(key)) {
|
||||||
|
client_cache.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @implements {ReadonlyMap<string, any>} */
|
||||||
|
class ReadonlyCache {
|
||||||
|
/** @type {ReadonlyMap<string, any>['get']} */
|
||||||
|
get(key) {
|
||||||
|
const entry = client_cache.get(key);
|
||||||
|
return entry?.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ReadonlyMap<string, any>['has']} */
|
||||||
|
has(key) {
|
||||||
|
return client_cache.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ReadonlyMap<string, any>['size']} */
|
||||||
|
get size() {
|
||||||
|
return client_cache.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ReadonlyMap<string, any>['forEach']} */
|
||||||
|
forEach(cb) {
|
||||||
|
client_cache.forEach((entry, key) => cb(entry.item, key, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ReadonlyMap<string, any>['entries']} */
|
||||||
|
*entries() {
|
||||||
|
for (const [key, entry] of client_cache.entries()) {
|
||||||
|
yield [key, entry.item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ReadonlyMap<string, any>['keys']} */
|
||||||
|
*keys() {
|
||||||
|
for (const key of client_cache.keys()) {
|
||||||
|
yield key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ReadonlyMap<string, any>['values']} */
|
||||||
|
*values() {
|
||||||
|
for (const entry of client_cache.values()) {
|
||||||
|
yield entry.item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.entries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readonly_cache = new ReadonlyCache();
|
||||||
|
|
||||||
|
/** @returns {ReadonlyMap<string, any>} */
|
||||||
|
export function get_cache() {
|
||||||
|
return readonly_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {...any} args
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function default_hash(...args) {
|
||||||
|
return JSON.stringify(args);
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
||||||
|
import { cache } from './cache';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {StandardSchemaV1} TSchema
|
||||||
|
* @param {{ schema?: TSchema, url: string | URL, init?: RequestInit }} args
|
||||||
|
* @param {string} [key]
|
||||||
|
*/
|
||||||
|
async function fetcher_impl({ schema, url, init }, key) {
|
||||||
|
const response = await fetch(url, init);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Fetch error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
if (schema) {
|
||||||
|
const data = await response.json();
|
||||||
|
return schema['~standard'].validate(data);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached_fetch = cache('svelte/fetcher', fetcher_impl, {
|
||||||
|
hash: (arg) => {
|
||||||
|
return `${typeof arg.url === 'string' ? arg.url : arg.url.toString()}}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {StandardSchemaV1} TSchema
|
||||||
|
* @overload
|
||||||
|
* @param {{ schema: TSchema, url: string | URL, init?: RequestInit }} arg
|
||||||
|
* @returns {Promise<StandardSchemaV1.InferOutput<TSchema>>}
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @overload
|
||||||
|
* @param {{ schema?: undefined, url: string | URL, init?: RequestInit }} arg
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @template {StandardSchemaV1} TSchema
|
||||||
|
* @param {{ schema?: TSchema, url: string | URL, init?: RequestInit }} arg
|
||||||
|
*/
|
||||||
|
export function fetcher(arg) {
|
||||||
|
return cached_fetch(arg);
|
||||||
|
}
|
||||||
@ -1,12 +1,22 @@
|
|||||||
/** @import { Source, Derived } from '#client' */
|
/** @import { Source, Derived } from '#client' */
|
||||||
import { state, derived, set, get, tick } from '../../index.js';
|
/** @import { Resource as ResourceType } from '#shared' */
|
||||||
import { deferred } from '../../../shared/utils.js';
|
import { state, derived, set, get, tick } from '../index.js';
|
||||||
|
import { deferred } from '../../shared/utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {() => Promise<T>} fn
|
||||||
|
* @returns {ResourceType<T>}
|
||||||
|
*/
|
||||||
|
export function resource(fn) {
|
||||||
|
return /** @type {ResourceType<T>} */ (/** @type {unknown} */ (new Resource(fn)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @implements {Partial<Promise<T>>}
|
* @implements {Partial<Promise<T>>}
|
||||||
*/
|
*/
|
||||||
export class Resource {
|
class Resource {
|
||||||
#init = false;
|
#init = false;
|
||||||
|
|
||||||
/** @type {() => Promise<T>} */
|
/** @type {() => Promise<T>} */
|
||||||
@ -1,78 +0,0 @@
|
|||||||
/** @import { Transport } from '#shared' */
|
|
||||||
import { hydratable } from '../../context.js';
|
|
||||||
import { tick } from '../../runtime.js';
|
|
||||||
import { render_effect } from '../effects.js';
|
|
||||||
import { Resource } from './resource.js';
|
|
||||||
|
|
||||||
/** @typedef {{ count: number, resource: Resource<any> }} Entry */
|
|
||||||
/** @type {Map<string, Entry>} */
|
|
||||||
const cache = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template TReturn
|
|
||||||
* @template {unknown[]} [TArgs=[]]
|
|
||||||
* @template {typeof Resource} [TResource=typeof Resource]
|
|
||||||
* @param {string} name
|
|
||||||
* @param {(...args: TArgs) => TReturn} fn
|
|
||||||
* @param {{ Resource?: TResource, transport?: Transport, hash?: (args: TArgs) => string }} [options]
|
|
||||||
* @returns {(...args: TArgs) => Resource<TReturn>}
|
|
||||||
*/
|
|
||||||
export function define_resource(name, fn, options = {}) {
|
|
||||||
const ResolvedResource = options?.Resource ?? Resource;
|
|
||||||
return (...args) => {
|
|
||||||
const stringified_args = (options.hash ?? JSON.stringify)(args);
|
|
||||||
const cache_key = `${name}:${stringified_args}`;
|
|
||||||
let entry = cache.get(cache_key);
|
|
||||||
const maybe_remove = create_remover(cache_key);
|
|
||||||
|
|
||||||
let tracking = true;
|
|
||||||
try {
|
|
||||||
render_effect(() => {
|
|
||||||
if (entry) entry.count++;
|
|
||||||
return () => {
|
|
||||||
const entry = cache.get(cache_key);
|
|
||||||
if (!entry) return;
|
|
||||||
entry.count--;
|
|
||||||
maybe_remove(entry, cache);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
tracking = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let resource = entry?.resource;
|
|
||||||
if (!resource) {
|
|
||||||
resource = new ResolvedResource(() =>
|
|
||||||
hydratable(cache_key, () => fn(...args), { transport: options.transport })
|
|
||||||
);
|
|
||||||
const entry = {
|
|
||||||
resource,
|
|
||||||
count: tracking ? 1 : 0
|
|
||||||
};
|
|
||||||
cache.set(cache_key, entry);
|
|
||||||
|
|
||||||
resource.then(
|
|
||||||
() => maybe_remove(entry, cache),
|
|
||||||
() => maybe_remove(entry, cache)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resource;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} key
|
|
||||||
*/
|
|
||||||
function create_remover(key) {
|
|
||||||
/**
|
|
||||||
* @param {Entry | undefined} entry
|
|
||||||
* @param {Map<string, Entry>} cache
|
|
||||||
*/
|
|
||||||
return (entry, cache) =>
|
|
||||||
tick().then(() => {
|
|
||||||
if (!entry?.count && entry === cache.get(key)) {
|
|
||||||
cache.delete(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { get_render_store } from '../context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template TReturn
|
||||||
|
* @template {unknown} TArg
|
||||||
|
* @param {string} name
|
||||||
|
* @param {(arg: TArg, key: string) => TReturn} fn
|
||||||
|
* @param {{ hash?: (arg: TArg) => string }} [options]
|
||||||
|
* @returns {(arg: TArg) => TReturn}
|
||||||
|
*/
|
||||||
|
export function cache(name, fn, { hash = default_hash } = {}) {
|
||||||
|
return (arg) => {
|
||||||
|
const cache = get_render_store().cache;
|
||||||
|
const key = `${name}::::${hash(arg)}`;
|
||||||
|
const entry = cache.get(key);
|
||||||
|
if (entry) {
|
||||||
|
return /** @type {TReturn} */ (entry);
|
||||||
|
}
|
||||||
|
const new_entry = fn(arg, key);
|
||||||
|
cache.set(key, new_entry);
|
||||||
|
return new_entry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} arg
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function default_hash(arg) {
|
||||||
|
return JSON.stringify(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_cache() {
|
||||||
|
throw new Error('TODO: cannot get cache on the server');
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
/** @import { Resource as ResourceType } from '#shared' */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {() => Promise<T>} fn
|
||||||
|
* @returns {ResourceType<T>}
|
||||||
|
*/
|
||||||
|
export function resource(fn) {
|
||||||
|
return /** @type {ResourceType<T>} */ (/** @type {unknown} */ (new Resource(fn)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @implements {Partial<Promise<T>>}
|
||||||
|
*/
|
||||||
|
class Resource {
|
||||||
|
/** @type {Promise<void>} */
|
||||||
|
#promise;
|
||||||
|
#ready = false;
|
||||||
|
#loading = true;
|
||||||
|
|
||||||
|
/** @type {T | undefined} */
|
||||||
|
#current = undefined;
|
||||||
|
#error = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {() => Promise<T>} fn
|
||||||
|
*/
|
||||||
|
constructor(fn) {
|
||||||
|
this.#promise = Promise.resolve(fn()).then(
|
||||||
|
(val) => {
|
||||||
|
this.#ready = true;
|
||||||
|
this.#loading = false;
|
||||||
|
this.#current = val;
|
||||||
|
this.#error = undefined;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.#error = error;
|
||||||
|
this.#loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get then() {
|
||||||
|
// @ts-expect-error
|
||||||
|
return (onfulfilled, onrejected) =>
|
||||||
|
this.#promise.then(
|
||||||
|
() => onfulfilled?.(this.#current),
|
||||||
|
() => onrejected?.(this.#error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get catch() {
|
||||||
|
return (/** @type {any} */ onrejected) => this.then(undefined, onrejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
get finally() {
|
||||||
|
return (/** @type {any} */ onfinally) => this.then(onfinally, onfinally);
|
||||||
|
}
|
||||||
|
|
||||||
|
get current() {
|
||||||
|
return this.#current;
|
||||||
|
}
|
||||||
|
|
||||||
|
get error() {
|
||||||
|
return this.#error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the resource is loading or reloading.
|
||||||
|
*/
|
||||||
|
get loading() {
|
||||||
|
return this.#loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true once the resource has been loaded for the first time.
|
||||||
|
*/
|
||||||
|
get ready() {
|
||||||
|
return this.#ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh = () => {
|
||||||
|
throw new Error('TODO Cannot refresh a resource on the server');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {T} value
|
||||||
|
*/
|
||||||
|
set = (value) => {
|
||||||
|
this.#ready = true;
|
||||||
|
this.#loading = false;
|
||||||
|
this.#error = undefined;
|
||||||
|
this.#current = value;
|
||||||
|
this.#promise = Promise.resolve();
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in new issue