tweak hydratable API, add official errors

pull/17124/head
Elliott Johnson 2 weeks ago
parent 9a424cd784
commit e28ced79a4

@ -37,10 +37,10 @@ This error occurs when using `hydratable` or `setHydratableValue` multiple times
</script> </script>
``` ```
### lifecycle_function_unavailable ### render_context_unavailable
``` ```
`%name%(...)` is not available on the server Failed to retrieve `render` context. %addendum%
``` ```
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render. If `AsyncLocalStorage` is available, you're likely calling a function that needs access to the `render` context (`hydratable`, `cache`, or something that depends on these) from outside of `render`. If `AsyncLocalStorage` is not available, these functions must also be called synchronously from within `render` -- i.e. not after any `await`s.

@ -42,6 +42,14 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P
A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
``` ```
### lifecycle_function_unavailable
```
`%name%(...)` is not available on the %location%
```
Certain methods such as `mount` cannot be invoked while running in a server context, while others, such as `hydratable.set`, cannot be invoked while running in the browser.
### lifecycle_outside_component ### lifecycle_outside_component
``` ```

@ -29,8 +29,8 @@ This error occurs when using `hydratable` or `setHydratableValue` multiple times
</script> </script>
``` ```
## lifecycle_function_unavailable ## render_context_unavailable
> `%name%(...)` is not available on the server > Failed to retrieve `render` context. %addendum%
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render. If `AsyncLocalStorage` is available, you're likely calling a function that needs access to the `render` context (`hydratable`, `cache`, or something that depends on these) from outside of `render`. If `AsyncLocalStorage` is not available, these functions must also be called synchronously from within `render` -- i.e. not after any `await`s.

@ -34,6 +34,12 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P
> A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` > A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
## lifecycle_function_unavailable
> `%name%(...)` is not available on the %location%
Certain methods such as `mount` cannot be invoked while running in a server context, while others, such as `hydratable.set`, cannot be invoked while running in the browser.
## lifecycle_outside_component ## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation > `%name%(...)` can only be used during component initialisation

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

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

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

@ -22,19 +22,19 @@ export function createEventDispatcher() {
} }
export function mount() { export function mount() {
e.lifecycle_function_unavailable('mount'); e.lifecycle_function_unavailable('mount', 'server');
} }
export function hydrate() { export function hydrate() {
e.lifecycle_function_unavailable('hydrate'); e.lifecycle_function_unavailable('hydrate', 'server');
} }
export function unmount() { export function unmount() {
e.lifecycle_function_unavailable('unmount'); e.lifecycle_function_unavailable('unmount', 'server');
} }
export function fork() { export function fork() {
e.lifecycle_function_unavailable('fork'); e.lifecycle_function_unavailable('fork', 'server');
} }
export async function tick() {} export async function tick() {}

@ -1,4 +1,4 @@
/** @import { Decode, Transport } from '#shared' */ /** @import { Decode, Hydratable, Transport } from '#shared' */
import { async_mode_flag } from '../flags/index.js'; import { async_mode_flag } from '../flags/index.js';
import { hydrating } from './dom/hydration.js'; import { hydrating } from './dom/hydration.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
@ -11,7 +11,7 @@ import * as e from './errors.js';
* @param {Transport<T>} [options] * @param {Transport<T>} [options]
* @returns {T} * @returns {T}
*/ */
export function hydratable(key, fn, options) { function isomorphic_hydratable(key, fn, options) {
if (!async_mode_flag) { if (!async_mode_flag) {
e.experimental_async_required('hydratable'); e.experimental_async_required('hydratable');
} }
@ -28,13 +28,22 @@ export function hydratable(key, fn, options) {
); );
} }
isomorphic_hydratable['get'] = get_hydratable_value;
isomorphic_hydratable['has'] = has_hydratable_value;
isomorphic_hydratable['set'] = () => e.lifecycle_function_unavailable('hydratable.set', 'browser');
/** @type {Hydratable} */
const hydratable = isomorphic_hydratable;
export { hydratable };
/** /**
* @template T * @template T
* @param {string} key * @param {string} key
* @param {{ decode?: Decode<T> }} [options] * @param {{ decode?: Decode<T> }} [options]
* @returns {T | undefined} * @returns {T | undefined}
*/ */
export function get_hydratable_value(key, options = {}) { function get_hydratable_value(key, options = {}) {
if (!async_mode_flag) { if (!async_mode_flag) {
e.experimental_async_required('getHydratableValue'); e.experimental_async_required('getHydratableValue');
} }
@ -50,7 +59,7 @@ export function get_hydratable_value(key, options = {}) {
* @param {string} key * @param {string} key
* @returns {boolean} * @returns {boolean}
*/ */
export function has_hydratable_value(key) { function has_hydratable_value(key) {
if (!async_mode_flag) { if (!async_mode_flag) {
e.experimental_async_required('hasHydratableValue'); e.experimental_async_required('hasHydratableValue');
} }

@ -47,12 +47,12 @@ ${stack}\nhttps://svelte.dev/e/hydratable_clobbering`);
} }
/** /**
* `%name%(...)` is not available on the server * Failed to retrieve `render` context. %addendum%
* @param {string} name * @param {string} addendum
* @returns {never} * @returns {never}
*/ */
export function lifecycle_function_unavailable(name) { export function render_context_unavailable(addendum) {
const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server\nhttps://svelte.dev/e/lifecycle_function_unavailable`); const error = new Error(`render_context_unavailable\nFailed to retrieve \`render\` context. ${addendum}\nhttps://svelte.dev/e/render_context_unavailable`);
error.name = 'Svelte error'; error.name = 'Svelte error';

@ -1,4 +1,4 @@
/** @import { Encode, Transport } from '#shared' */ /** @import { Encode, Hydratable, Transport } from '#shared' */
/** @import { HydratableEntry } from '#server' */ /** @import { HydratableEntry } from '#server' */
import { async_mode_flag } from '../flags/index.js'; import { async_mode_flag } from '../flags/index.js';
@ -13,7 +13,7 @@ import { DEV } from 'esm-env';
* @param {Transport<T>} [options] * @param {Transport<T>} [options]
* @returns {T} * @returns {T}
*/ */
export function hydratable(key, fn, options) { function isomorphic_hydratable(key, fn, options) {
if (!async_mode_flag) { if (!async_mode_flag) {
e.experimental_async_required('hydratable'); e.experimental_async_required('hydratable');
} }
@ -28,13 +28,23 @@ export function hydratable(key, fn, options) {
store.hydratables.set(key, entry); store.hydratables.set(key, entry);
return entry.value; return entry.value;
} }
isomorphic_hydratable['get'] = () => e.lifecycle_function_unavailable('hydratable.get', 'server');
isomorphic_hydratable['has'] = has_hydratable_value;
isomorphic_hydratable['set'] = set_hydratable_value;
/** @type {Hydratable} */
const hydratable = isomorphic_hydratable;
export { hydratable };
/** /**
* @template T * @template T
* @param {string} key * @param {string} key
* @param {T} value * @param {T} value
* @param {{ encode?: Encode<T> }} [options] * @param {{ encode?: Encode<T> }} [options]
*/ */
export function set_hydratable_value(key, value, options = {}) { function set_hydratable_value(key, value, options = {}) {
if (!async_mode_flag) { if (!async_mode_flag) {
e.experimental_async_required('setHydratableValue'); e.experimental_async_required('setHydratableValue');
} }
@ -48,6 +58,18 @@ export function set_hydratable_value(key, value, options = {}) {
store.hydratables.set(key, create_entry(value, options?.encode)); 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('hasHydratableValue');
}
const store = get_render_context();
return store.hydratables.has(key);
}
/** /**
* @template T * @template T
* @param {T} value * @param {T} value

@ -3,6 +3,7 @@
/** @import { RenderContext } from '#server' */ /** @import { RenderContext } from '#server' */
import { deferred } from '../shared/utils.js'; import { deferred } from '../shared/utils.js';
import * as e from './errors.js';
/** @type {Promise<void> | null} */ /** @type {Promise<void> | null} */
let current_render = null; let current_render = null;
@ -38,18 +39,9 @@ export function get_render_context() {
const store = try_get_render_context(); const store = try_get_render_context();
if (!store) { if (!store) {
// TODO make this a proper e.error e.render_context_unavailable(
let message = 'Could not get rendering context.'; `\`AsyncLocalStorage\` is ${als ? 'available' : 'not available'}.`
);
if (als) {
message += ' You may have called `hydratable` or `cache` outside of the render lifecycle.';
} else {
message +=
' In environments without `AsyncLocalStorage`, `hydratable` must be accessed synchronously, not after an `await`.' +
' If it was accessed synchronously then this is an internal error or you may have called `hydratable` or `cache` outside of the render lifecycle.';
}
throw new Error(message);
} }
return store; return store;
@ -93,6 +85,7 @@ export async function init_render_context() {
} catch {} } catch {}
} }
// this has to be a function because rollup won't treeshake it if it's a constant
function in_webcontainer() { function in_webcontainer() {
// @ts-ignore -- this will fail when we run typecheck because we exclude node types // @ts-ignore -- this will fail when we run typecheck because we exclude node types
// eslint-disable-next-line n/prefer-global/process // eslint-disable-next-line n/prefer-global/process

@ -576,12 +576,12 @@ export class Renderer {
async #collect_hydratables() { async #collect_hydratables() {
const map = get_render_context().hydratables; const map = get_render_context().hydratables;
/** @type {(value: unknown) => string} */ /** @type {(value: unknown) => string} */
let default_stringify; const default_encode = new MemoizedUneval().uneval;
/** @type {[string, unknown][]} */ /** @type {[string, unknown][]} */
let entries = []; let entries = [];
for (const [k, v] of map) { for (const [k, v] of map) {
const encode = v.encode ?? (default_stringify ??= new MemoizedUneval().uneval); const encode = v.encode ?? default_encode;
// sequential await is okay here -- all the work is already kicked off // sequential await is okay here -- all the work is already kicked off
entries.push([k, encode(await v.value)]); entries.push([k, encode(await v.value)]);
} }
@ -649,9 +649,7 @@ export class Renderer {
for (const [k, v] of serialized) { for (const [k, v] of serialized) {
entries.push(`["${k}",${v}]`); entries.push(`["${k}",${v}]`);
} }
// TODO csp? // TODO csp -- have discussed but not implemented
// TODO how can we communicate this error better? Is there a way to not just send it to the console?
// (it is probably very rare so... not too worried)
return ` return `
<script> <script>
var store = (window.__svelte ??= {}).h ??= new Map(); var store = (window.__svelte ??= {}).h ??= new Map();

@ -51,6 +51,24 @@ export function invalid_snippet_arguments() {
} }
} }
/**
* `%name%(...)` is not available on the %location%
* @param {string} name
* @param {string} location
* @returns {never}
*/
export function lifecycle_function_unavailable(name, location) {
if (DEV) {
const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the ${location}\nhttps://svelte.dev/e/lifecycle_function_unavailable`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/lifecycle_function_unavailable`);
}
}
/** /**
* `%name%(...)` can only be used during component initialisation * `%name%(...)` can only be used during component initialisation
* @param {string} name * @param {string} name

@ -25,6 +25,13 @@ export type Transport<T> =
decode: Decode<T>; decode: Decode<T>;
}; };
export type Hydratable = {
<T>(key: string, fn: () => T, options?: Transport<T>): T;
get: <T>(key: string) => T | undefined;
has: (key: string) => boolean;
set: <T>(key: string, value: T, options?: Transport<T>) => void;
};
export type Resource<T> = { export type Resource<T> = {
then: Promise<Awaited<T>>['then']; then: Promise<Awaited<T>>['then'];
catch: Promise<Awaited<T>>['catch']; catch: Promise<Awaited<T>>['catch'];

@ -27,5 +27,3 @@ export function render<
} }
] ]
): RenderOutput; ): RenderOutput;
export type { set_hydratable_value as setHydratableValue } from '../internal/server/hydratable.js';

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

@ -450,7 +450,7 @@ declare module 'svelte' {
* @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead
* */ * */
export function afterUpdate(fn: () => void): void; export function afterUpdate(fn: () => void): void;
export function hydratable<T>(key: string, fn: () => T, options?: Transport<T> | undefined): T; export const hydratable: Hydratable;
/** /**
* Create a snippet programmatically * Create a snippet programmatically
* */ * */
@ -610,6 +610,13 @@ declare module 'svelte' {
decode: Decode<T>; decode: Decode<T>;
}; };
type Hydratable = {
<T>(key: string, fn: () => T, options?: Transport<T>): T;
get: <T>(key: string) => T | undefined;
has: (key: string) => boolean;
set: <T>(key: string, value: T, options?: Transport<T>) => void;
};
export {}; export {};
} }
@ -2610,21 +2617,6 @@ declare module 'svelte/server' {
} }
type RenderOutput = SyncRenderOutput & PromiseLike<SyncRenderOutput>; type RenderOutput = SyncRenderOutput & PromiseLike<SyncRenderOutput>;
export function setHydratableValue<T>(key: string, value: T, options?: {
encode?: Encode<T>;
} | undefined): void;
type Encode<T> = (value: T) => unknown;
export {};
}
declare module 'svelte/client' {
export function getHydratableValue<T>(key: string, options?: {
decode?: Decode<T>;
} | undefined): T | undefined;
export function hasHydratableValue(key: string): boolean;
type Decode<T> = (value: any) => T;
export {}; export {};
} }

Loading…
Cancel
Save