the easy stuff

elliott/hydratable
Elliott Johnson 3 days ago
parent f76c1aad7b
commit a025b9a31a

@ -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';

@ -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<T> }} [options]
* @param {Transport<T>} [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<T> }} [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);
}
/**

@ -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<T> }} [options]
* @param {Transport<T>} [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<T> }} [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

@ -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 `
<script>
{
const store = (window.__svelte ??= {}).h ??= new Map();
for (const [k,v] of [${entries.join(',')}]) {
store.set(k, v);
}
}
</script>`;
<script>
{
const store = (window.__svelte ??= {}).h ??= new Map();
for (const [k,v] of [${entries.join(',')}]) {
store.set(k, v);
}
}
</script>`;
}
}
@ -716,33 +715,3 @@ export class SSRState {
}
}
}
export class MemoizedUneval {
/** @type {Map<unknown, { value?: string }>} */
#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;
});
};
}

@ -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);
});
});

@ -33,26 +33,3 @@ export type Transport<T> =
encode?: undefined;
decode: Decode<T>;
};
/** 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 = {
<T>(
/**
* 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> }
): T;
/** Get a hydratable value from the server-rendered store. If used after hydration, will always return `undefined`. Only works on the client. */
get: <T>(key: string, options?: { decode?: Decode<T> }) => 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: <T>(key: string, value: T, options?: { encode?: Encode<T> }) => void;
};

@ -105,7 +105,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
declare global {
var __svelte:
| {
h?: any;
h?: Map<string, unknown>;
}
| undefined;
}

Loading…
Cancel
Save