it at least basically works

pull/17124/head
Elliott Johnson 4 weeks ago
parent ef11dae8ce
commit d36894a5c0

@ -224,14 +224,60 @@ export function is_runes() {
return !legacy_mode_flag || (component_context !== null && component_context.l === null);
}
/** @type {string | null} */
export let hydratable_key = null;
/** @param {string | null} key */
export function set_hydratable_key(key) {
hydratable_key = key;
}
/**
* @template T
* @overload
* @param {string} key
* @param {() => T} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
* @returns {Promise<Awaited<T>>}
*/
/**
* @template T
* @overload
* @param {() => T} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<Awaited<T>>}
*/
/**
* @template T
* @param {string | (() => T)} key_or_fn
* @param {(() => T) | { transport?: Transport<T> }} [fn_or_options]
* @param {{ transport?: Transport<T> }} [maybe_options]
* @returns {Promise<Awaited<T>>}
*/
export function hydratable(key, fn, { transport } = {}) {
export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
/** @type {string} */
let key;
/** @type {() => T} */
let fn;
/** @type {{ transport?: Transport<T> }} */
let options;
if (typeof key_or_fn === 'string') {
key = key_or_fn;
fn = /** @type {() => T} */ (fn_or_options);
options = /** @type {{ transport?: Transport<T> }} */ (maybe_options);
} else {
if (hydratable_key === null) {
throw new Error(
'TODO error: `hydratable` must be called synchronously within `cache` in order to omit the key'
);
} else {
key = hydratable_key;
}
fn = /** @type {() => T} */ (key_or_fn);
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
}
if (!hydrating) {
return Promise.resolve(fn());
}
@ -245,7 +291,7 @@ export function hydratable(key, fn, { transport } = {}) {
);
}
const entry = /** @type {string} */ (store.get(key));
const parse = transport?.parse ?? ((val) => new Function(`return (${val})`)());
const parse = options.transport?.parse ?? ((val) => new Function(`return (${val})`)());
return Promise.resolve(/** @type {T} */ (parse(entry)));
}

@ -1,3 +1,4 @@
import { set_hydratable_key } from '../context.js';
import { tick } from '../runtime.js';
import { render_effect } from './effects.js';
@ -6,52 +7,49 @@ import { render_effect } from './effects.js';
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}
* @template {(...args: any[]) => any} TFn
* @param {string} key
* @param {TFn} fn
* @returns {ReturnType<TFn>}
*/
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;
}
export function cache(key, fn) {
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;
}
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;
set_hydratable_key(key);
const item = fn();
set_hydratable_key(null);
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;
}
/**
@ -124,11 +122,3 @@ const readonly_cache = new ReadonlyCache();
export function get_cache() {
return readonly_cache;
}
/**
* @param {...any} args
* @returns
*/
function default_hash(...args) {
return JSON.stringify(args);
}

@ -1,44 +1,17 @@
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
/** @import { GetRequestInit, Resource } from '#shared' */
import { cache } from './cache';
import { fetch_json } from '../../shared/utils.js';
import { hydratable } from '../context';
import { resource } from './resource';
/**
* @template {StandardSchemaV1} TSchema
* @param {{ schema?: TSchema, url: string | URL, init?: RequestInit }} args
* @param {string} [key]
* @template TReturn
* @param {string | URL} url
* @param {GetRequestInit} [init]
* @returns {Resource<TReturn>}
*/
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);
export function fetcher(url, init) {
return cache(`svelte/fetcher::::${typeof url === 'string' ? url : url.toString()}`, () =>
resource(() => hydratable(() => fetch_json(url, init)))
);
}

@ -125,14 +125,60 @@ export async function save(promise) {
};
}
/** @type {string | null} */
export let hydratable_key = null;
/** @param {string | null} key */
export function set_hydratable_key(key) {
hydratable_key = key;
}
/**
* @template T
* @overload
* @param {string} key
* @param {() => T} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
* @returns {Promise<Awaited<T>>}
*/
export function hydratable(key, fn, { transport } = {}) {
/**
* @template T
* @overload
* @param {() => T} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<Awaited<T>>}
*/
/**
* @template T
* @param {string | (() => T)} key_or_fn
* @param {(() => T) | { transport?: Transport<T> }} [fn_or_options]
* @param {{ transport?: Transport<T> }} [maybe_options]
* @returns {Promise<Awaited<T>>}
*/
export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
// TODO DRY out with #shared
/** @type {string} */
let key;
/** @type {() => T} */
let fn;
/** @type {{ transport?: Transport<T> }} */
let options;
if (typeof key_or_fn === 'string') {
key = key_or_fn;
fn = /** @type {() => T} */ (fn_or_options);
options = /** @type {{ transport?: Transport<T> }} */ (maybe_options);
} else {
if (hydratable_key === null) {
throw new Error(
'TODO error: `hydratable` must be called synchronously within `cache` in order to omit the key'
);
} else {
key = hydratable_key;
}
fn = /** @type {() => T} */ (key_or_fn);
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
}
const store = get_render_store();
if (store.hydratables.has(key)) {
@ -141,7 +187,7 @@ export function hydratable(key, fn, { transport } = {}) {
}
const result = fn();
store.hydratables.set(key, { value: result, transport });
store.hydratables.set(key, { value: result, transport: options.transport });
return Promise.resolve(result);
}

@ -1,33 +1,22 @@
import { get_render_store } from '../context';
import { get_render_store, set_hydratable_key } 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}
* @template {(...args: any[]) => any} TFn
* @param {string} key
* @param {TFn} fn
* @returns {ReturnType<TFn>}
*/
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 cache(key, fn) {
const cache = get_render_store().cache;
const entry = cache.get(key);
if (entry) {
return /** @type {ReturnType<TFn>} */ (entry);
}
set_hydratable_key(key);
const new_entry = fn();
set_hydratable_key(null);
cache.set(key, new_entry);
return new_entry;
}
export function get_cache() {

@ -0,0 +1,17 @@
/** @import { GetRequestInit, Resource } from '#shared' */
import { fetch_json } from '../../shared/utils.js';
import { hydratable } from '../context.js';
import { cache } from './cache';
import { resource } from './resource.js';
/**
* @template TReturn
* @param {string | URL} url
* @param {GetRequestInit} [init]
* @returns {Resource<TReturn>}
*/
export function fetcher(url, init) {
return cache(`svelte/fetcher::::${typeof url === 'string' ? url : url.toString()}`, () =>
resource(() => hydratable(() => fetch_json(url, init)))
);
}

@ -375,7 +375,7 @@ export class Renderer {
});
return Promise.resolve(user_result);
}
async ??= with_render_store({ hydratables: new Map(), resources: new Map() }, () =>
async ??= with_render_store({ hydratables: new Map(), cache: new Map() }, () =>
Renderer.#render_async(component, options)
);
return async.then((result) => {

@ -35,3 +35,5 @@ export type Resource<T> = {
error: any;
}
);
export type GetRequestInit = Omit<RequestInit, 'method' | 'body'> & { method?: 'GET' };

@ -1,3 +1,5 @@
/** @import { GetRequestInit } from '#shared' */
// Store the references to globals in case someone tries to monkey patch these, causing the below
// to de-opt (this occurs often when using popular extensions).
export var is_array = Array.isArray;
@ -116,3 +118,15 @@ export function to_array(value, n) {
return array;
}
/**
* @param {string | URL} url
* @param {GetRequestInit} [init]
*/
export async function fetch_json(url, init) {
const response = await fetch(url, init);
if (!response.ok) {
throw new Error(`TODO error: Fetch error: ${response.status} ${response.statusText}`);
}
return response.json();
}

@ -7,3 +7,4 @@ export { MediaQuery } from './media-query.js';
export { createSubscriber } from './create-subscriber.js';
export { resource } from '../internal/client/reactivity/resource.js';
export { cache, get_cache as getCache } from '../internal/client/reactivity/cache.js';
export { fetcher } from '../internal/client/reactivity/fetcher.js';

@ -1,5 +1,6 @@
export { resource } from '../internal/server/reactivity/resource.js';
export { cache, get_cache as getCache } from '../internal/server/reactivity/cache.js';
export { fetcher } from '../internal/server/reactivity/fetcher.js';
export const SvelteDate = globalThis.Date;
export const SvelteSet = globalThis.Set;

@ -489,9 +489,13 @@ declare module 'svelte' {
* */
export function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T;
export function hydratable<T>(key: string, fn: () => T, { transport }?: {
export function hydratable<T>(key: string, fn: () => T, options?: {
transport?: Transport<T>;
} | undefined): Promise<T>;
} | undefined): Promise<Awaited<T>>;
export function hydratable<T>(fn: () => T, options?: {
transport?: Transport<T>;
} | undefined): Promise<Awaited<T>>;
/**
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component.
* Transitions will play during the initial render unless the `intro` option is set to `false`.
@ -2412,9 +2416,8 @@ declare module 'svelte/reactivity' {
*/
export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
export function resource<T>(fn: () => Promise<T>): Resource<T>;
export function cache<TReturn, TArg extends unknown>(name: string, fn: (arg: TArg, key: string) => TReturn, { hash }?: {
hash?: (arg: TArg) => string;
} | undefined): (arg: TArg) => TReturn;
export function fetcher<TReturn>(url: string | URL, init?: GetRequestInit | undefined): Resource<TReturn>;
export function cache<TFn extends (...args: any[]) => any>(key: string, fn: TFn): ReturnType<TFn>;
export function getCache(): ReadonlyMap<string, any>;
class ReactiveValue<T> {
@ -2443,6 +2446,8 @@ declare module 'svelte/reactivity' {
}
);
type GetRequestInit = Omit<RequestInit, 'method' | 'body'> & { method?: 'GET' };
export {};
}

Loading…
Cancel
Save