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>
```
### 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 ...}`
```
### 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
```

@ -29,8 +29,8 @@ This error occurs when using `hydratable` or `setHydratableValue` multiple times
</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 ...}`
## 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
> `%name%(...)` can only be used during component initialisation

@ -95,10 +95,6 @@
"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,7 +45,6 @@ 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`,

@ -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() {
e.lifecycle_function_unavailable('mount');
e.lifecycle_function_unavailable('mount', 'server');
}
export function hydrate() {
e.lifecycle_function_unavailable('hydrate');
e.lifecycle_function_unavailable('hydrate', 'server');
}
export function unmount() {
e.lifecycle_function_unavailable('unmount');
e.lifecycle_function_unavailable('unmount', 'server');
}
export function fork() {
e.lifecycle_function_unavailable('fork');
e.lifecycle_function_unavailable('fork', 'server');
}
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 { hydrating } from './dom/hydration.js';
import * as w from './warnings.js';
@ -11,7 +11,7 @@ import * as e from './errors.js';
* @param {Transport<T>} [options]
* @returns {T}
*/
export function hydratable(key, fn, options) {
function isomorphic_hydratable(key, fn, options) {
if (!async_mode_flag) {
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
* @param {string} key
* @param {{ decode?: Decode<T> }} [options]
* @returns {T | undefined}
*/
export function get_hydratable_value(key, options = {}) {
function get_hydratable_value(key, options = {}) {
if (!async_mode_flag) {
e.experimental_async_required('getHydratableValue');
}
@ -50,7 +59,7 @@ export function get_hydratable_value(key, options = {}) {
* @param {string} key
* @returns {boolean}
*/
export function has_hydratable_value(key) {
function has_hydratable_value(key) {
if (!async_mode_flag) {
e.experimental_async_required('hasHydratableValue');
}

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

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

@ -3,6 +3,7 @@
/** @import { RenderContext } from '#server' */
import { deferred } from '../shared/utils.js';
import * as e from './errors.js';
/** @type {Promise<void> | null} */
let current_render = null;
@ -38,18 +39,9 @@ export function get_render_context() {
const store = try_get_render_context();
if (!store) {
// TODO make this a proper e.error
let message = 'Could not get rendering context.';
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);
e.render_context_unavailable(
`\`AsyncLocalStorage\` is ${als ? 'available' : 'not available'}.`
);
}
return store;
@ -93,6 +85,7 @@ export async function init_render_context() {
} catch {}
}
// this has to be a function because rollup won't treeshake it if it's a constant
function in_webcontainer() {
// @ts-ignore -- this will fail when we run typecheck because we exclude node types
// eslint-disable-next-line n/prefer-global/process

@ -576,12 +576,12 @@ export class Renderer {
async #collect_hydratables() {
const map = get_render_context().hydratables;
/** @type {(value: unknown) => string} */
let default_stringify;
const default_encode = new MemoizedUneval().uneval;
/** @type {[string, unknown][]} */
let entries = [];
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
entries.push([k, encode(await v.value)]);
}
@ -649,9 +649,7 @@ export class Renderer {
for (const [k, v] of serialized) {
entries.push(`["${k}",${v}]`);
}
// TODO csp?
// 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)
// TODO csp -- have discussed but not implemented
return `
<script>
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
* @param {string} name

@ -25,6 +25,13 @@ export type Transport<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> = {
then: Promise<Awaited<T>>['then'];
catch: Promise<Awaited<T>>['catch'];

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

@ -1,2 +1 @@
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
* */
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
* */
@ -610,6 +610,13 @@ declare module 'svelte' {
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 {};
}
@ -2610,21 +2617,6 @@ declare module 'svelte/server' {
}
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 {};
}

Loading…
Cancel
Save