add imperative hydratable API

pull/17124/head
Elliott Johnson 3 weeks ago
parent 7ee0ce8da7
commit 90b85d152f

@ -95,6 +95,10 @@
"types": "./types/index.d.ts",
"default": "./src/server/index.js"
},
"./client": {
"types": "./types/index.d.ts",
"default": "./src/client/index.js"
},
"./store": {
"types": "./types/index.d.ts",
"worker": "./src/store/index-server.js",

@ -45,6 +45,7 @@ await createBundle({
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
[`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`,
[`${pkg.name}/client`]: `${dir}/src/client/index.d.ts`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,
[`${pkg.name}/events`]: `${dir}/src/events/public.d.ts`,

@ -0,0 +1 @@
export { get_hydratable_value as getHydratableValue } from '../internal/client/hydratable.js';

@ -247,9 +247,9 @@ export {
getContext,
getAllContexts,
hasContext,
setContext,
hydratable
setContext
} from './internal/client/context.js';
export { hydratable } from './internal/client/hydratable.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack, settled } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';

@ -224,77 +224,6 @@ 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 {() => Promise<T>} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
*/
/**
* @template T
* @overload
* @param {() => Promise<T>} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
*/
/**
* @template T
* @param {string | (() => Promise<T>)} key_or_fn
* @param {(() => Promise<T>) | { transport?: Transport<T> }} [fn_or_options]
* @param {{ transport?: Transport<T> }} [maybe_options]
* @returns {Promise<T>}
*/
export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
/** @type {string} */
let key;
/** @type {() => Promise<T>} */
let fn;
/** @type {{ transport?: Transport<T> }} */
let options;
if (typeof key_or_fn === 'string') {
key = key_or_fn;
fn = /** @type {() => Promise<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 {() => Promise<T>} */ (key_or_fn);
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
}
if (!hydrating) {
return Promise.resolve(fn());
}
var store = window.__svelte?.h;
if (store === undefined) {
throw new Error('TODO this should be impossible?');
}
if (!store.has(key)) {
throw new Error(
`TODO Expected hydratable key "${key}" to exist during hydration, but it does not`
);
}
const entry = /** @type {string} */ (store.get(key));
const parse = options.transport?.parse ?? ((val) => new Function(`return (${val})`)());
return Promise.resolve(/** @type {T} */ (parse(entry)));
}
/**
* @param {string} name
* @returns {Map<unknown, unknown>}

@ -0,0 +1,60 @@
/** @import { Parse, Transport } from '#shared' */
import { hydrating } from './dom/hydration';
/**
* @template T
* @param {string} key
* @param {() => T} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {T}
*/
export function hydratable(key, fn, options = {}) {
if (!hydrating) {
return fn();
}
var store = window.__svelte?.h;
if (store === undefined) {
throw new Error('TODO this should be impossible?');
}
const val = store.get(key);
if (val === undefined) {
throw new Error(
`TODO Expected hydratable key "${key}" to exist during hydration, but it does not`
);
}
return parse(val, options.transport?.parse);
}
/**
* @template T
* @param {string} key
* @param {{ parse?: Parse<T> }} [options]
* @returns {T | undefined}
*/
export function get_hydratable_value(key, options = {}) {
// TODO probably can DRY this out with the above
if (!hydrating) {
return undefined;
}
var store = window.__svelte?.h;
if (store === undefined) {
throw new Error('TODO this should be impossible?');
}
const val = store.get(key);
if (val === undefined) {
return undefined;
}
return parse(val, options.parse);
}
/**
* @template T
* @param {string} val
* @param {Parse<T> | undefined} parse
* @returns {T}
*/
function parse(val, parse) {
return (parse ?? ((val) => new Function(`return (${val})`)()))(val);
}

@ -1,6 +1,5 @@
import { BaseCacheObserver } from '../../shared/cache-observer.js';
import { ObservableCache } from '../../shared/observable-cache.js';
import { set_hydratable_key } from '../context.js';
import { tick } from '../runtime.js';
import { render_effect } from './effects.js';
@ -38,9 +37,7 @@ export function cache(key, fn) {
return entry?.item;
}
set_hydratable_key(key);
const item = fn();
set_hydratable_key(null);
const new_entry = {
item,
count: tracking ? 1 : 0

@ -1,7 +1,7 @@
/** @import { GetRequestInit, Resource } from '#shared' */
import { cache } from './cache';
import { fetch_json } from '../../shared/utils.js';
import { hydratable } from '../context';
import { hydratable } from '../hydratable';
import { resource } from './resource';
/**
@ -11,7 +11,6 @@ import { resource } from './resource';
* @returns {Resource<TReturn>}
*/
export function fetcher(url, init) {
return cache(`svelte/fetcher::::${typeof url === 'string' ? url : url.toString()}`, () =>
resource(() => hydratable(() => fetch_json(url, init)))
);
const key = `svelte/fetcher/${typeof url === 'string' ? url : url.toString()}`;
return cache(key, () => resource(() => hydratable(key, () => fetch_json(url, init))));
}

@ -5,21 +5,21 @@ import { deferred } from '../../shared/utils.js';
/**
* @template T
* @param {() => Promise<T>} fn
* @returns {ResourceType<T>}
* @param {() => T} fn
* @returns {ResourceType<Awaited<T>>}
*/
export function resource(fn) {
return /** @type {ResourceType<T>} */ (new Resource(fn));
return /** @type {ResourceType<Awaited<T>>} */ (new Resource(fn));
}
/**
* @template T
* @implements {Partial<Promise<T>>}
* @implements {Partial<Promise<Awaited<T>>>}
*/
class Resource {
#init = false;
/** @type {() => Promise<T>} */
/** @type {() => T} */
#fn;
/** @type {Source<boolean>} */
@ -31,13 +31,13 @@ class Resource {
/** @type {Source<boolean>} */
#ready = state(false);
/** @type {Source<T | undefined>} */
/** @type {Source<Awaited<T> | undefined>} */
#raw = state(undefined);
/** @type {Source<Promise<any>>} */
#promise;
/** @type {Derived<T | undefined>} */
/** @type {Derived<Awaited<T> | undefined>} */
#current = derived(() => {
if (!get(this.#ready)) return undefined;
return get(this.#raw);
@ -46,7 +46,7 @@ class Resource {
/** {@type Source<any>} */
#error = state(undefined);
/** @type {Derived<Promise<T>['then']>} */
/** @type {Derived<Promise<Awaited<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(() => {
@ -57,7 +57,7 @@ class Resource {
await p;
await tick();
resolve?.(/** @type {T} */ (get(this.#current)));
resolve?.(/** @type {Awaited<T>} */ (get(this.#current)));
} catch (error) {
reject?.(error);
}
@ -65,7 +65,7 @@ class Resource {
});
/**
* @param {() => Promise<T>} fn
* @param {() => T} fn
*/
constructor(fn) {
this.#fn = fn;
@ -166,7 +166,7 @@ class Resource {
};
/**
* @param {T} value
* @param {Awaited<T>} value
*/
set = (value) => {
set(this.#ready, true);

@ -1,4 +1,4 @@
/** @import { Transport } from '#shared' */
/** @import { Stringify, Transport } from '#shared' */
import { get_render_context } from './render-context';
@ -12,58 +12,39 @@ export function set_hydratable_key(key) {
/**
* @template T
* @overload
* @param {string} key
* @param {() => Promise<T>} fn
* @param {() => T} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
*/
/**
* @template T
* @overload
* @param {() => Promise<T>} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
* @returns {T}
*/
export function hydratable(key, fn, options = {}) {
const store = get_render_context();
if (store.hydratables.has(key)) {
// TODO error
throw new Error("can't have two hydratables with the same key");
}
const result = fn();
store.hydratables.set(key, { value: result, stringify: options.transport?.stringify });
return result;
}
/**
* @template T
* @param {string | (() => Promise<T>)} key_or_fn
* @param {(() => Promise<T>) | { transport?: Transport<T> }} [fn_or_options]
* @param {{ transport?: Transport<T> }} [maybe_options]
* @returns {Promise<T>}
* @param {string} key
* @param {T} value
* @param {{ stringify?: Stringify<T> }} [options]
*/
export async function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
// TODO DRY out with #shared
/** @type {string} */
let key;
/** @type {() => Promise<T>} */
let fn;
/** @type {{ transport?: Transport<T> }} */
let options;
if (typeof key_or_fn === 'string') {
key = key_or_fn;
fn = /** @type {() => Promise<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 {() => Promise<T>} */ (key_or_fn);
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
}
const store = await get_render_context();
export function set_hydratable_value(key, value, options = {}) {
const store = get_render_context();
if (store.hydratables.has(key)) {
// TODO error
throw new Error("can't have two hydratables with the same key");
}
const result = fn();
store.hydratables.set(key, { value: result, transport: options.transport });
return result;
store.hydratables.set(key, {
value,
stringify: options.stringify
});
}

@ -11,7 +11,6 @@ import { resource } from './resource.js';
* @returns {Resource<TReturn>}
*/
export function fetcher(url, init) {
return cache(`svelte/fetcher::::${typeof url === 'string' ? url : url.toString()}`, () =>
resource(() => hydratable(() => fetch_json(url, init)))
);
const key = `svelte/fetcher/${typeof url === 'string' ? url : url.toString()}`;
return cache(key, () => resource(() => hydratable(key, () => fetch_json(url, init))));
}

@ -2,16 +2,16 @@
/**
* @template T
* @param {() => Promise<T>} fn
* @returns {ResourceType<T>}
* @param {() => T} fn
* @returns {ResourceType<Awaited<T>>}
*/
export function resource(fn) {
return /** @type {ResourceType<T>} */ (new Resource(fn));
return /** @type {ResourceType<Awaited<T>>} */ (new Resource(fn));
}
/**
* @template T
* @implements {Partial<Promise<T>>}
* @implements {Partial<Promise<Awaited<T>>>}
*/
class Resource {
/** @type {Promise<void>} */
@ -19,12 +19,12 @@ class Resource {
#ready = false;
#loading = true;
/** @type {T | undefined} */
/** @type {Awaited<T> | undefined} */
#current = undefined;
#error = undefined;
/**
* @param {() => Promise<T>} fn
* @param {() => T} fn
*/
constructor(fn) {
this.#promise = Promise.resolve(fn()).then(
@ -85,7 +85,7 @@ class Resource {
};
/**
* @param {T} value
* @param {Awaited<T>} value
*/
set = (value) => {
this.#ready = true;

@ -526,8 +526,7 @@ export class Renderer {
/** @type {[string, string][]} */
let entries = [];
for (const [k, v] of map) {
const serialize =
v.transport?.stringify ?? (default_stringify ??= new MemoizedUneval().uneval);
const serialize = v.stringify ?? (default_stringify ??= new MemoizedUneval().uneval);
// sequential await is okay here -- all the work is already kicked off
entries.push([k, serialize(await v.value)]);
}

@ -1,4 +1,4 @@
import type { Transport } from '#shared';
import type { Stringify, Transport } from '#shared';
import type { ObservableCache } from '../shared/observable-cache';
import type { Element } from './dev';
import type { Renderer } from './renderer';
@ -20,8 +20,8 @@ export interface RenderContext {
hydratables: Map<
string,
{
value: Promise<unknown>;
transport: Transport<any> | undefined;
value: unknown;
stringify: Stringify<any> | undefined;
}
>;
cache: ObservableCache;

@ -11,14 +11,18 @@ export type Snapshot<T> = ReturnType<typeof $state.snapshot<T>>;
export type MaybePromise<T> = T | Promise<T>;
export type Parse<T> = (value: string) => T;
export type Stringify<T> = (value: T) => string;
export type Transport<T> =
| {
stringify: (value: T) => string;
stringify: Stringify<T>;
parse?: undefined;
}
| {
stringify?: undefined;
parse: (value: string) => T;
parse: Parse<T>;
};
export type Resource<T> = {

@ -1 +1,2 @@
export { render } from '../internal/server/index.js';
export { set_hydratable_value as setHydratableValue } from '../internal/server/hydratable.js';

@ -439,6 +439,9 @@ declare module 'svelte' {
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
* */
export function flushSync<T = void>(fn?: (() => T) | undefined): T;
export function hydratable<T>(key: string, fn: () => T, options?: {
transport?: Transport<T>;
} | undefined): T;
/**
* Create a snippet programmatically
* */
@ -488,14 +491,6 @@ declare module 'svelte' {
*
* */
export function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T;
export function hydratable<T>(key: string, fn: () => Promise<T>, options?: {
transport?: Transport<T>;
} | undefined): Promise<T>;
export function hydratable<T>(fn: () => Promise<T>, options?: {
transport?: Transport<T>;
} | undefined): Promise<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`.
@ -569,14 +564,18 @@ declare module 'svelte' {
[K in keyof T]: () => T[K];
};
type Parse<T> = (value: string) => T;
type Stringify<T> = (value: T) => string;
type Transport<T> =
| {
stringify: (value: T) => string;
stringify: Stringify<T>;
parse?: undefined;
}
| {
stringify?: undefined;
parse: (value: string) => T;
parse: Parse<T>;
};
export {};
@ -2421,7 +2420,7 @@ declare module 'svelte/reactivity' {
* @since 5.7.0
*/
export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
export function resource<T>(fn: () => Promise<T>): Resource_1<T>;
export function resource<T>(fn: () => T): Resource_1<Awaited<T>>;
export function fetcher<TReturn>(url: string | URL, init?: GetRequestInit | undefined): Resource_1<TReturn>;
type Resource_1<T> = {
then: Promise<T>['then'];
@ -2619,6 +2618,15 @@ declare module 'svelte/server' {
export {};
}
declare module 'svelte/client' {
export function getHydratableValue<T>(key: string, options?: {
parse?: Parse<T>;
} | undefined): T | undefined;
type Parse<T> = (value: string) => T;
export {};
}
declare module 'svelte/store' {
/** Callback to inform of a value updates. */
export type Subscriber<T> = (value: T) => void;

Loading…
Cancel
Save