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 { state, derived, set, get, tick } from '../../index.js';
|
||||
import { deferred } from '../../../shared/utils.js';
|
||||
/** @import { Resource as ResourceType } from '#shared' */
|
||||
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
|
||||
* @implements {Partial<Promise<T>>}
|
||||
*/
|
||||
export class Resource {
|
||||
class Resource {
|
||||
#init = false;
|
||||
|
||||
/** @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