From a025b9a31a08e52ff4329d421138209557f4feb0 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Mon, 17 Nov 2025 15:56:34 -0700 Subject: [PATCH] the easy stuff --- packages/svelte/src/index.d.ts | 2 +- .../svelte/src/internal/client/hydratable.js | 82 +++---------------- .../svelte/src/internal/server/hydratable.js | 49 +---------- .../svelte/src/internal/server/renderer.js | 49 ++--------- .../src/internal/server/renderer.test.ts | 38 +-------- .../svelte/src/internal/shared/types.d.ts | 23 ------ .../svelte/tests/runtime-legacy/shared.ts | 2 +- 7 files changed, 26 insertions(+), 219 deletions(-) diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index 42507385d6..7132c8061e 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -369,4 +369,4 @@ export interface Fork { } export * from './index-client.js'; -export { Hydratable, Transport, Encode, Decode } from '#shared'; +export { Transport, Encode, Decode } from '#shared'; diff --git a/packages/svelte/src/internal/client/hydratable.js b/packages/svelte/src/internal/client/hydratable.js index 6336d3cd55..65377eb315 100644 --- a/packages/svelte/src/internal/client/hydratable.js +++ b/packages/svelte/src/internal/client/hydratable.js @@ -1,4 +1,4 @@ -/** @import { Decode, Hydratable, Transport } from '#shared' */ +/** @import { Decode, Transport } from '#shared' */ import { async_mode_flag } from '../flags/index.js'; import { hydrating } from './dom/hydration.js'; import * as w from './warnings.js'; @@ -9,87 +9,25 @@ import { DEV } from 'esm-env'; * @template T * @param {string} key * @param {() => T} fn - * @param {{ transport?: Transport }} [options] + * @param {Transport} [transport] * @returns {T} */ -function isomorphic_hydratable(key, fn, options) { +export function hydratable(key, fn, transport) { if (!async_mode_flag) { e.experimental_async_required('hydratable'); } - return access_hydratable_store( - key, - (val, has) => { - if (!has) { - hydratable_missing_but_expected(key); - return fn(); - } - return decode(val, options?.transport?.decode); - }, - fn - ); -} - -isomorphic_hydratable['get'] = get_hydratable_value; -isomorphic_hydratable['has'] = has_hydratable_value; -isomorphic_hydratable['set'] = () => e.fn_unavailable_on_client('hydratable.set'); - -/** @type {Hydratable} */ -const hydratable = isomorphic_hydratable; - -export { hydratable }; - -/** - * @template T - * @param {string} key - * @param {{ decode?: Decode }} [options] - * @returns {T | undefined} - */ -function get_hydratable_value(key, options = {}) { - if (!async_mode_flag) { - e.experimental_async_required('hydratable.get'); + if (!hydrating) { + return fn(); } - return access_hydratable_store( - key, - (val, has) => { - if (!has) { - hydratable_missing_but_expected(key); - } - return decode(val, options.decode); - }, - () => undefined - ); -} - -/** - * @param {string} key - * @returns {boolean} - */ -function has_hydratable_value(key) { - if (!async_mode_flag) { - e.experimental_async_required('hydratable.set'); + const store = window.__svelte?.h; + if (!store?.has(key)) { + hydratable_missing_but_expected(key); + return fn(); } - return access_hydratable_store( - key, - (_, has) => has, - () => false - ); -} -/** - * @template T - * @param {string} key - * @param {(val: unknown, has: boolean) => T} on_hydrating - * @param {() => T} on_not_hydrating - * @returns {T} - */ -function access_hydratable_store(key, on_hydrating, on_not_hydrating) { - if (!hydrating) { - return on_not_hydrating(); - } - var store = window.__svelte?.h; - return on_hydrating(store?.get(key), store?.has(key) ?? false); + return decode(store.get(key), transport?.decode); } /** diff --git a/packages/svelte/src/internal/server/hydratable.js b/packages/svelte/src/internal/server/hydratable.js index c08e7aa457..b896b66570 100644 --- a/packages/svelte/src/internal/server/hydratable.js +++ b/packages/svelte/src/internal/server/hydratable.js @@ -1,4 +1,4 @@ -/** @import { Encode, Hydratable, Transport } from '#shared' */ +/** @import { Encode, Transport } from '#shared' */ /** @import { HydratableEntry } from '#server' */ import { async_mode_flag } from '../flags/index.js'; import { get_render_context } from './render-context.js'; @@ -9,10 +9,10 @@ import { DEV } from 'esm-env'; * @template T * @param {string} key * @param {() => T} fn - * @param {{ transport?: Transport }} [options] + * @param {Transport} [transport] * @returns {T} */ -function isomorphic_hydratable(key, fn, options) { +export function hydratable(key, fn, transport) { if (!async_mode_flag) { e.experimental_async_required('hydratable'); } @@ -23,52 +23,11 @@ function isomorphic_hydratable(key, fn, options) { e.hydratable_clobbering(key, store.hydratables.get(key)?.stack || 'unknown'); } - const entry = create_entry(fn(), options?.transport?.encode); + const entry = create_entry(fn(), transport?.encode); store.hydratables.set(key, entry); return entry.value; } -isomorphic_hydratable['get'] = () => e.fn_unavailable_on_server('hydratable.get'); -isomorphic_hydratable['has'] = has_hydratable_value; -isomorphic_hydratable['set'] = set_hydratable_value; - -/** @type {Hydratable} */ -const hydratable = isomorphic_hydratable; - -export { hydratable }; - -/** - * @template T - * @param {string} key - * @param {T} value - * @param {{ encode?: Encode }} [options] - */ -function set_hydratable_value(key, value, options = {}) { - if (!async_mode_flag) { - e.experimental_async_required('hydratable.set'); - } - - const store = get_render_context(); - - if (store.hydratables.has(key)) { - e.hydratable_clobbering(key, store.hydratables.get(key)?.stack || 'unknown'); - } - - store.hydratables.set(key, create_entry(value, options?.encode)); -} - -/** - * @param {string} key - * @returns {boolean} - */ -function has_hydratable_value(key) { - if (!async_mode_flag) { - e.experimental_async_required('hydratable.has'); - } - const store = get_render_context(); - return store.hydratables.has(key); -} - /** * @template T * @param {T} value diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 24a119e089..8df221522f 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -576,12 +576,11 @@ export class Renderer { async #collect_hydratables() { const map = get_render_context().hydratables; /** @type {(value: unknown) => string} */ - const default_encode = new MemoizedUneval().uneval; /** @type {[string, string][]} */ let entries = []; for (const [k, v] of map) { - const encode = v.encode ?? default_encode; + const encode = v.encode ?? uneval; // sequential await is okay here -- all the work is already kicked off entries.push([k, encode(await v.value)]); } @@ -651,14 +650,14 @@ export class Renderer { } // TODO csp -- have discussed but not implemented return ` -`; + `; } } @@ -716,33 +715,3 @@ export class SSRState { } } } - -export class MemoizedUneval { - /** @type {Map} */ - #cache = new Map(); - - /** - * @param {unknown} value - * @returns {string} - */ - uneval = (value) => { - return uneval(value, (value, uneval) => { - const cached = this.#cache.get(value); - if (cached) { - // this breaks my brain a bit, but: - // - when the entry is defined but its value is `undefined`, calling `uneval` below will cause the custom replacer to be called again - // - because the custom replacer returns this, which is `undefined`, it will fall back to the default serialization - // - ...which causes it to return a string - // - ...which is then added to this cache before being returned - return cached.value; - } - - const stub = {}; - this.#cache.set(value, stub); - - const result = uneval(value); - stub.value = result; - return result; - }); - }; -} diff --git a/packages/svelte/src/internal/server/renderer.test.ts b/packages/svelte/src/internal/server/renderer.test.ts index a2a979aeb4..b3e88e52a4 100644 --- a/packages/svelte/src/internal/server/renderer.test.ts +++ b/packages/svelte/src/internal/server/renderer.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, test } from 'vitest'; -import { MemoizedUneval, Renderer, SSRState } from './renderer.js'; +import { Renderer, SSRState } from './renderer.js'; import type { Component } from 'svelte'; import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js'; import { uneval } from 'devalue'; @@ -356,39 +356,3 @@ describe('async', () => { expect(destroyed).toEqual(['c', 'e', 'a', 'b', 'b*', 'd']); }); }); - -describe('MemoizedDevalue', () => { - test.each([ - 1, - 'general kenobi', - { foo: 'bar' }, - [1, 2], - null, - undefined, - new Map([[1, '2']]) - ] as const)('has same behavior as unmemoized devalue for %s', (input) => { - expect(new MemoizedUneval().uneval(input)).toBe(uneval(input)); - }); - - test('caches results', () => { - const memoized = new MemoizedUneval(); - let calls = 0; - - const input = { - get only_once() { - calls++; - return 42; - } - }; - - const first = memoized.uneval(input); - const max_calls = calls; - const second = memoized.uneval(input); - memoized.uneval(input); - - expect(first).toBe(second); - // for reasons I don't quite comprehend, it does get called twice, but both calls happen in the first - // serialization, and don't increase afterwards - expect(calls).toBe(max_calls); - }); -}); diff --git a/packages/svelte/src/internal/shared/types.d.ts b/packages/svelte/src/internal/shared/types.d.ts index 8365b529f9..21f3f8704d 100644 --- a/packages/svelte/src/internal/shared/types.d.ts +++ b/packages/svelte/src/internal/shared/types.d.ts @@ -33,26 +33,3 @@ export type Transport = encode?: undefined; decode: Decode; }; - -/** Make the result of a function hydratable. This means it will be serialized on the server and available synchronously during hydration on the client. */ -export type Hydratable = { - ( - /** - * A key to identify this hydratable value. Each hydratable value must have a unique key. - * If writing a library that utilizes `hydratable`, prefix your keys with your library name to prevent naming collisions. - */ - key: string, - /** - * A function that returns the value to be hydrated. On the server, this value will be stashed and serialized. - * On the client during hydration, the value will be used synchronously instead of invoking the function. - */ - fn: () => T, - options?: { transport?: Transport } - ): T; - /** Get a hydratable value from the server-rendered store. If used after hydration, will always return `undefined`. Only works on the client. */ - get: (key: string, options?: { decode?: Decode }) => T | undefined; - /** Check if a hydratable value exists in the server-rendered store. */ - has: (key: string) => boolean; - /** Set a hydratable value. Only works on the server during `render`. */ - set: (key: string, value: T, options?: { encode?: Encode }) => void; -}; diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 4ff5b03de0..11b3e7bfa4 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -105,7 +105,7 @@ export interface RuntimeTest = Record; } | undefined; }