From 0c4ce5a9ec831bc65d9c693165bc9bcaab6c0b31 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Fri, 24 Oct 2025 16:59:18 -0600 Subject: [PATCH] misc improvements --- .../svelte/src/internal/client/context.js | 20 +++--- .../src/internal/client/reactivity/cache.js | 57 ++-------------- .../internal/client/reactivity/resource.js | 2 +- .../svelte/src/internal/server/context.js | 34 +++++----- .../src/internal/server/reactivity/cache.js | 7 +- .../internal/server/reactivity/resource.js | 2 +- .../svelte/src/internal/server/types.d.ts | 6 +- .../src/internal/shared/cache-observer.js | 56 +++++++++++++++ .../svelte/src/internal/shared/types.d.ts | 20 +++--- packages/svelte/src/internal/shared/utils.js | 4 +- .../svelte/src/reactivity/index-client.js | 8 ++- .../svelte/src/reactivity/index-server.js | 8 ++- packages/svelte/types/index.d.ts | 68 ++++++++++++------- 13 files changed, 170 insertions(+), 122 deletions(-) create mode 100644 packages/svelte/src/internal/shared/cache-observer.js diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 76865b0bbb..6fb708c04c 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -236,35 +236,35 @@ export function set_hydratable_key(key) { * @template T * @overload * @param {string} key - * @param {() => T} fn + * @param {() => Promise} fn * @param {{ transport?: Transport }} [options] - * @returns {Promise>} + * @returns {Promise} */ /** * @template T * @overload - * @param {() => T} fn + * @param {() => Promise} fn * @param {{ transport?: Transport }} [options] - * @returns {Promise>} + * @returns {Promise} */ /** * @template T - * @param {string | (() => T)} key_or_fn - * @param {(() => T) | { transport?: Transport }} [fn_or_options] + * @param {string | (() => Promise)} key_or_fn + * @param {(() => Promise) | { transport?: Transport }} [fn_or_options] * @param {{ transport?: Transport }} [maybe_options] - * @returns {Promise>} + * @returns {Promise} */ export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) { /** @type {string} */ let key; - /** @type {() => T} */ + /** @type {() => Promise} */ let fn; /** @type {{ transport?: Transport }} */ let options; if (typeof key_or_fn === 'string') { key = key_or_fn; - fn = /** @type {() => T} */ (fn_or_options); + fn = /** @type {() => Promise} */ (fn_or_options); options = /** @type {{ transport?: Transport }} */ (maybe_options); } else { if (hydratable_key === null) { @@ -274,7 +274,7 @@ export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) { } else { key = hydratable_key; } - fn = /** @type {() => T} */ (key_or_fn); + fn = /** @type {() => Promise} */ (key_or_fn); options = /** @type {{ transport?: Transport }} */ (fn_or_options); } diff --git a/packages/svelte/src/internal/client/reactivity/cache.js b/packages/svelte/src/internal/client/reactivity/cache.js index 224de6bd9b..379ae978cb 100644 --- a/packages/svelte/src/internal/client/reactivity/cache.js +++ b/packages/svelte/src/internal/client/reactivity/cache.js @@ -1,3 +1,4 @@ +import { BaseCacheObserver } from '../../shared/cache-observer.js'; import { set_hydratable_key } from '../context.js'; import { tick } from '../runtime.js'; import { render_effect } from './effects.js'; @@ -67,58 +68,8 @@ function create_remover(key) { }); } -/** @implements {ReadonlyMap} */ -class ReadonlyCache { - /** @type {ReadonlyMap['get']} */ - get(key) { - const entry = client_cache.get(key); - return entry?.item; - } - - /** @type {ReadonlyMap['has']} */ - has(key) { - return client_cache.has(key); - } - - /** @type {ReadonlyMap['size']} */ - get size() { - return client_cache.size; - } - - /** @type {ReadonlyMap['forEach']} */ - forEach(cb) { - client_cache.forEach((entry, key) => cb(entry.item, key, this)); - } - - /** @type {ReadonlyMap['entries']} */ - *entries() { - for (const [key, entry] of client_cache.entries()) { - yield [key, entry.item]; - } +export class CacheObserver extends BaseCacheObserver { + constructor() { + super(client_cache); } - - /** @type {ReadonlyMap['keys']} */ - *keys() { - for (const key of client_cache.keys()) { - yield key; - } - } - - /** @type {ReadonlyMap['values']} */ - *values() { - for (const entry of client_cache.values()) { - yield entry.item; - } - } - - [Symbol.iterator]() { - return this.entries(); - } -} - -const readonly_cache = new ReadonlyCache(); - -/** @returns {ReadonlyMap} */ -export function get_cache() { - return readonly_cache; } diff --git a/packages/svelte/src/internal/client/reactivity/resource.js b/packages/svelte/src/internal/client/reactivity/resource.js index 97ab835664..e231a8a862 100644 --- a/packages/svelte/src/internal/client/reactivity/resource.js +++ b/packages/svelte/src/internal/client/reactivity/resource.js @@ -9,7 +9,7 @@ import { deferred } from '../../shared/utils.js'; * @returns {ResourceType} */ export function resource(fn) { - return /** @type {ResourceType} */ (/** @type {unknown} */ (new Resource(fn))); + return /** @type {ResourceType} */ (new Resource(fn)); } /** diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index 54fc122077..8783d5522f 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -1,4 +1,4 @@ -/** @import { ALSContext, SSRContext } from '#server' */ +/** @import { RenderContext, SSRContext } from '#server' */ /** @import { AsyncLocalStorage } from 'node:async_hooks' */ /** @import { Transport } from '#shared' */ import { DEV } from 'esm-env'; @@ -137,36 +137,36 @@ export function set_hydratable_key(key) { * @template T * @overload * @param {string} key - * @param {() => T} fn + * @param {() => Promise} fn * @param {{ transport?: Transport }} [options] - * @returns {Promise>} + * @returns {Promise} */ /** * @template T * @overload - * @param {() => T} fn + * @param {() => Promise} fn * @param {{ transport?: Transport }} [options] - * @returns {Promise>} + * @returns {Promise} */ /** * @template T - * @param {string | (() => T)} key_or_fn - * @param {(() => T) | { transport?: Transport }} [fn_or_options] + * @param {string | (() => Promise)} key_or_fn + * @param {(() => Promise) | { transport?: Transport }} [fn_or_options] * @param {{ transport?: Transport }} [maybe_options] - * @returns {Promise>} + * @returns {Promise} */ export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) { // TODO DRY out with #shared /** @type {string} */ let key; - /** @type {() => T} */ + /** @type {() => Promise} */ let fn; /** @type {{ transport?: Transport }} */ let options; if (typeof key_or_fn === 'string') { key = key_or_fn; - fn = /** @type {() => T} */ (fn_or_options); + fn = /** @type {() => Promise} */ (fn_or_options); options = /** @type {{ transport?: Transport }} */ (maybe_options); } else { if (hydratable_key === null) { @@ -176,7 +176,7 @@ export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) { } else { key = hydratable_key; } - fn = /** @type {() => T} */ (key_or_fn); + fn = /** @type {() => Promise} */ (key_or_fn); options = /** @type {{ transport?: Transport }} */ (fn_or_options); } const store = get_render_store(); @@ -191,15 +191,15 @@ export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) { return Promise.resolve(result); } -/** @type {ALSContext | null} */ +/** @type {RenderContext | null} */ export let sync_store = null; -/** @param {ALSContext | null} store */ +/** @param {RenderContext | null} store */ export function set_sync_store(store) { sync_store = store; } -/** @type {AsyncLocalStorage | null} */ +/** @type {AsyncLocalStorage | null} */ let als = null; import('node:async_hooks') @@ -209,12 +209,12 @@ import('node:async_hooks') return null; }); -/** @returns {ALSContext | null} */ +/** @returns {RenderContext | null} */ function try_get_render_store() { return sync_store ?? als?.getStore() ?? null; } -/** @returns {ALSContext} */ +/** @returns {RenderContext} */ export function get_render_store() { const store = try_get_render_store(); @@ -238,7 +238,7 @@ export function get_render_store() { /** * @template T - * @param {ALSContext} store + * @param {RenderContext} store * @param {() => Promise} fn * @returns {Promise} */ diff --git a/packages/svelte/src/internal/server/reactivity/cache.js b/packages/svelte/src/internal/server/reactivity/cache.js index 4904633863..a387570f4d 100644 --- a/packages/svelte/src/internal/server/reactivity/cache.js +++ b/packages/svelte/src/internal/server/reactivity/cache.js @@ -1,3 +1,4 @@ +import { BaseCacheObserver } from '../../shared/cache-observer'; import { get_render_store, set_hydratable_key } from '../context'; /** @@ -19,6 +20,8 @@ export function cache(key, fn) { return new_entry; } -export function get_cache() { - throw new Error('TODO: cannot get cache on the server'); +export class CacheObserver extends BaseCacheObserver { + constructor() { + super(get_render_store().cache); + } } diff --git a/packages/svelte/src/internal/server/reactivity/resource.js b/packages/svelte/src/internal/server/reactivity/resource.js index e542f5e3de..7484fd9f53 100644 --- a/packages/svelte/src/internal/server/reactivity/resource.js +++ b/packages/svelte/src/internal/server/reactivity/resource.js @@ -6,7 +6,7 @@ * @returns {ResourceType} */ export function resource(fn) { - return /** @type {ResourceType} */ (/** @type {unknown} */ (new Resource(fn))); + return /** @type {ResourceType} */ (new Resource(fn)); } /** diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index cbc0a385a9..6563fdf6f9 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -1,4 +1,4 @@ -import type { MaybePromise, Transport } from '#shared'; +import type { Transport } from '#shared'; import type { Element } from './dev'; import type { Renderer } from './renderer'; @@ -15,11 +15,11 @@ export interface SSRContext { element?: Element; } -export interface ALSContext { +export interface RenderContext { hydratables: Map< string, { - value: MaybePromise; + value: Promise; transport: Transport | undefined; } >; diff --git a/packages/svelte/src/internal/shared/cache-observer.js b/packages/svelte/src/internal/shared/cache-observer.js new file mode 100644 index 0000000000..1dd72b2b34 --- /dev/null +++ b/packages/svelte/src/internal/shared/cache-observer.js @@ -0,0 +1,56 @@ +/** @implements {ReadonlyMap} */ +export class BaseCacheObserver { + /** @type {ReadonlyMap} */ + #cache; + + /** @param {Map} cache */ + constructor(cache) { + this.#cache = cache; + } + + /** @type {ReadonlyMap['get']} */ + get(key) { + const entry = this.#cache.get(key); + return entry?.item; + } + + /** @type {ReadonlyMap['has']} */ + has(key) { + return this.#cache.has(key); + } + + /** @type {ReadonlyMap['size']} */ + get size() { + return this.#cache.size; + } + + /** @type {ReadonlyMap['forEach']} */ + forEach(cb) { + this.#cache.forEach((entry, key) => cb(entry.item, key, this)); + } + + /** @type {ReadonlyMap['entries']} */ + *entries() { + for (const [key, entry] of this.#cache.entries()) { + yield [key, entry.item]; + } + } + + /** @type {ReadonlyMap['keys']} */ + *keys() { + for (const key of this.#cache.keys()) { + yield key; + } + } + + /** @type {ReadonlyMap['values']} */ + *values() { + for (const entry of this.#cache.values()) { + yield entry.item; + } + } + + [Symbol.iterator]() { + return this.entries(); + } +} diff --git a/packages/svelte/src/internal/shared/types.d.ts b/packages/svelte/src/internal/shared/types.d.ts index 549d870d88..781e6e1e5e 100644 --- a/packages/svelte/src/internal/shared/types.d.ts +++ b/packages/svelte/src/internal/shared/types.d.ts @@ -11,10 +11,15 @@ export type Snapshot = ReturnType>; export type MaybePromise = T | Promise; -export type Transport = { - stringify: (value: T) => string; - parse: (value: string) => T; -}; +export type Transport = + | { + stringify: (value: T) => string; + parse?: undefined; + } + | { + stringify?: undefined; + parse: (value: string) => T; + }; export type Resource = { then: Promise['then']; @@ -23,16 +28,15 @@ export type Resource = { refresh: () => Promise; set: (value: T) => void; loading: boolean; + error: any; } & ( | { ready: false; - value: undefined; - error: undefined; + current: undefined; } | { ready: true; - value: T; - error: any; + current: T; } ); diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js index 1f375c4001..53700df8e8 100644 --- a/packages/svelte/src/internal/shared/utils.js +++ b/packages/svelte/src/internal/shared/utils.js @@ -50,7 +50,7 @@ export function run_all(arr) { /** * TODO replace with Promise.withResolvers once supported widely enough - * @template T + * @template [T=void] */ export function deferred() { /** @type {(value: T) => void} */ @@ -120,8 +120,10 @@ export function to_array(value, n) { } /** + * @template [TReturn=any] * @param {string | URL} url * @param {GetRequestInit} [init] + * @returns {Promise} */ export async function fetch_json(url, init) { const response = await fetch(url, init); diff --git a/packages/svelte/src/reactivity/index-client.js b/packages/svelte/src/reactivity/index-client.js index cc65588c89..8b2abd9076 100644 --- a/packages/svelte/src/reactivity/index-client.js +++ b/packages/svelte/src/reactivity/index-client.js @@ -1,3 +1,4 @@ +/** @import { Resource as ResourceType } from '#shared' */ export { SvelteDate } from './date.js'; export { SvelteSet } from './set.js'; export { SvelteMap } from './map.js'; @@ -6,5 +7,10 @@ export { SvelteURLSearchParams } from './url-search-params.js'; 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 { cache, CacheObserver } from '../internal/client/reactivity/cache.js'; export { fetcher } from '../internal/client/reactivity/fetcher.js'; + +/** + * @template T + * @typedef {ResourceType} Resource + */ diff --git a/packages/svelte/src/reactivity/index-server.js b/packages/svelte/src/reactivity/index-server.js index 231741028d..0dcc459e64 100644 --- a/packages/svelte/src/reactivity/index-server.js +++ b/packages/svelte/src/reactivity/index-server.js @@ -1,5 +1,6 @@ +/** @import { Resource as ResourceType } from '#shared' */ export { resource } from '../internal/server/reactivity/resource.js'; -export { cache, get_cache as getCache } from '../internal/server/reactivity/cache.js'; +export { cache, CacheObserver } from '../internal/server/reactivity/cache.js'; export { fetcher } from '../internal/server/reactivity/fetcher.js'; export const SvelteDate = globalThis.Date; @@ -25,3 +26,8 @@ export class MediaQuery { export function createSubscriber(_) { return () => {}; } + +/** + * @template T + * @typedef {ResourceType} Resource + */ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index e5cfbdb0ac..fd55a7649c 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -489,13 +489,13 @@ declare module 'svelte' { * */ export function getAllContexts = Map>(): T; - export function hydratable(key: string, fn: () => T, options?: { + export function hydratable(key: string, fn: () => Promise, options?: { transport?: Transport; - } | undefined): Promise>; + } | undefined): Promise; - export function hydratable(fn: () => T, options?: { + export function hydratable(fn: () => Promise, options?: { transport?: Transport; - } | undefined): Promise>; + } | undefined): Promise; /** * 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,10 +569,15 @@ declare module 'svelte' { [K in keyof T]: () => T[K]; }; - type Transport = { - stringify: (value: T) => string; - parse: (value: string) => T; - }; + type Transport = + | { + stringify: (value: T) => string; + parse?: undefined; + } + | { + stringify?: undefined; + parse: (value: string) => T; + }; export {}; } @@ -2152,6 +2157,7 @@ declare module 'svelte/motion' { } declare module 'svelte/reactivity' { + export type Resource = Resource_1; /** * A reactive version of the built-in [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object. * Reading the date (whether with methods like `date.getTime()` or `date.toString()`, or via things like [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)) @@ -2415,38 +2421,52 @@ declare module 'svelte/reactivity' { * @since 5.7.0 */ export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void; - export function resource(fn: () => Promise): Resource; - export function fetcher(url: string | URL, init?: GetRequestInit | undefined): Resource; - export function cache any>(key: string, fn: TFn): ReturnType; - - export function getCache(): ReadonlyMap; - class ReactiveValue { - - constructor(fn: () => T, onsubscribe: (update: () => void) => void); - get current(): T; - #private; - } - type Resource = { + export function resource(fn: () => Promise): Resource_1; + export function fetcher(url: string | URL, init?: GetRequestInit | undefined): Resource_1; + type Resource_1 = { then: Promise['then']; catch: Promise['catch']; finally: Promise['finally']; refresh: () => Promise; set: (value: T) => void; loading: boolean; + error: any; } & ( | { ready: false; - value: undefined; - error: undefined; + current: undefined; } | { ready: true; - value: T; - error: any; + current: T; } ); type GetRequestInit = Omit & { method?: 'GET' }; + export function cache any>(key: string, fn: TFn): ReturnType; + export class CacheObserver extends BaseCacheObserver { + constructor(); + } + class ReactiveValue { + + constructor(fn: () => T, onsubscribe: (update: () => void) => void); + get current(): T; + #private; + } + class BaseCacheObserver implements ReadonlyMap { + + constructor(cache: Map); + get(key: string): any; + has(key: string): boolean; + + get size(): number; + forEach(callbackfn: (value: any, key: string, map: ReadonlyMap) => void, thisArg?: any): void; + entries(): IterableIterator<[string, any]>; + keys(): IterableIterator; + values(): IterableIterator; + [Symbol.iterator](): IterableIterator<[string, any]>; + #private; + } export {}; }