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

@ -130,12 +130,6 @@ $effect(() => {
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
### experimental_async_fork
```
Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
```
### flush_sync_in_effect
```

@ -140,6 +140,25 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
%handler% should be a function. Did you mean to %suggestion%?
```
### hydratable_missing_but_expected
```
Expected to find a hydratable with key `%key%` during hydration, but did not.
```
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
### hydration_attribute_changed
```

@ -14,6 +14,29 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render)
The `html` property of server render results has been deprecated. Use `body` instead.
```
### hydratable_clobbering
```
Attempted to set hydratable with key `%key%` twice. This behavior is undefined.
First set occurred at:
%stack%
```
This error occurs when using `hydratable` or `setHydratableValue` multiple times with the same key. To avoid this, you can combine `hydratable` with `cache`, or check whether the value has already been set with `hasHydratableValue`.
```svelte
<script>
import { hydratable } from 'svelte';
await Promise.all([
// which one should "win" and be serialized in the rendered response?
hydratable('hello', () => 'world'),
hydratable('hello', () => 'dad')
])
</script>
```
### lifecycle_function_unavailable
```

@ -1,5 +1,11 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### experimental_async_required
```
Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
```
### invalid_default_snippet
```

@ -100,10 +100,6 @@ $effect(() => {
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
## experimental_async_fork
> Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
## flush_sync_in_effect
> Cannot use `flushSync` inside an effect

@ -124,6 +124,23 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
> %handler% should be a function. Did you mean to %suggestion%?
## hydratable_missing_but_expected
> Expected to find a hydratable with key `%key%` during hydration, but did not.
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
## hydration_attribute_changed
> The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value

@ -8,6 +8,27 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render)
> The `html` property of server render results has been deprecated. Use `body` instead.
## hydratable_clobbering
> Attempted to set hydratable with key `%key%` twice. This behavior is undefined.
>
> First set occurred at:
> %stack%
This error occurs when using `hydratable` or `setHydratableValue` multiple times with the same key. To avoid this, you can combine `hydratable` with `cache`, or check whether the value has already been set with `hasHydratableValue`.
```svelte
<script>
import { hydratable } from 'svelte';
await Promise.all([
// which one should "win" and be serialized in the rendered response?
hydratable('hello', () => 'world'),
hydratable('hello', () => 'dad')
])
</script>
```
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server

@ -1,3 +1,7 @@
## experimental_async_required
> Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead

@ -229,22 +229,6 @@ export function effect_update_depth_exceeded() {
}
}
/**
* Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
* @returns {never}
*/
export function experimental_async_fork() {
if (DEV) {
const error = new Error(`experimental_async_fork\nCannot use \`fork(...)\` unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async_fork`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/experimental_async_fork`);
}
}
/**
* Cannot use `flushSync` inside an effect
* @returns {never}

@ -1,5 +1,8 @@
/** @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';
import * as e from './errors.js';
/**
* @template T
@ -9,17 +12,20 @@ import { hydrating } from './dom/hydration.js';
* @returns {T}
*/
export function hydratable(key, fn, options) {
if (!hydrating) {
return fn();
}
var store = window.__svelte?.h;
const val = store?.get(key);
if (val === undefined) {
// TODO this should really be an error or at least a warning because it would be disastrous to expect
// something to be synchronously hydratable and then have it not be
return fn();
if (!async_mode_flag) {
e.experimental_async_required('hydratable');
}
return decode(val, options?.decode);
return access_hydratable_store(
key,
(val, has) => {
if (!has) {
w.hydratable_missing_but_expected(key);
}
return decode(val, options?.decode);
},
fn
);
}
/**
@ -29,18 +35,15 @@ export function hydratable(key, fn, options) {
* @returns {T | undefined}
*/
export function get_hydratable_value(key, options = {}) {
// TODO probably can DRY this out with the above
if (!hydrating) {
return undefined;
}
var store = window.__svelte?.h;
const val = store?.get(key);
if (val === undefined) {
return undefined;
if (!async_mode_flag) {
e.experimental_async_required('getHydratableValue');
}
return decode(val, options.decode);
return access_hydratable_store(
key,
(val) => decode(val, options.decode),
() => undefined
);
}
/**
@ -48,11 +51,29 @@ export function get_hydratable_value(key, options = {}) {
* @returns {boolean}
*/
export function has_hydratable_value(key) {
if (!async_mode_flag) {
e.experimental_async_required('hasHydratableValue');
}
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 false;
return on_not_hydrating();
}
var store = window.__svelte?.h;
return store?.has(key) ?? false;
return on_hydrating(store?.get(key), store?.has(key) ?? false);
}
/**

@ -895,7 +895,7 @@ export function eager(fn) {
*/
export function fork(fn) {
if (!async_mode_flag) {
e.experimental_async_fork();
e.experimental_async_required('fork');
}
if (current_batch !== null) {

@ -1,7 +1,9 @@
/** @import { CacheEntry } from '#shared' */
import { async_mode_flag } from '../../flags/index.js';
import { BaseCacheObserver } from '../../shared/cache-observer.js';
import { tick } from '../runtime.js';
import { get_effect_validation_error_code, render_effect } from './effects.js';
import * as e from '../errors.js';
/** @typedef {{ count: number, item: any }} Entry */
/** @type {Map<string, CacheEntry>} */
@ -14,6 +16,10 @@ const client_cache = new Map();
* @returns {ReturnType<TFn>}
*/
export function cache(key, fn) {
if (!async_mode_flag) {
e.experimental_async_required('cache');
}
const cached = client_cache.has(key);
const entry = client_cache.get(key);
const maybe_remove = create_remover(key);
@ -70,6 +76,9 @@ function create_remover(key) {
*/
export class CacheObserver extends BaseCacheObserver {
constructor(prefix = '') {
if (!async_mode_flag) {
e.experimental_async_required('CacheObserver');
}
super(() => client_cache, prefix);
}
}

@ -3,6 +3,8 @@ import { cache } from './cache';
import { fetch_json } from '../../shared/utils.js';
import { hydratable } from '../hydratable';
import { resource } from './resource';
import { async_mode_flag } from '../../flags';
import * as e from '../errors.js';
/**
* @template TReturn
@ -11,6 +13,10 @@ import { resource } from './resource';
* @returns {Resource<TReturn>}
*/
export function fetcher(url, init) {
if (!async_mode_flag) {
e.experimental_async_required('fetcher');
}
const key = `svelte/fetcher/${typeof url === 'string' ? url : url.toString()}`;
return cache(key, () => resource(() => hydratable(key, () => fetch_json(url, init))));
}

@ -2,6 +2,8 @@
/** @import { Resource as ResourceType } from '#shared' */
import { state, derived, set, get, tick } from '../index.js';
import { deferred } from '../../shared/utils.js';
import { async_mode_flag } from '../../flags/index.js';
import * as e from '../errors.js';
/**
* @template T
@ -9,6 +11,9 @@ import { deferred } from '../../shared/utils.js';
* @returns {ResourceType<T>}
*/
export function resource(fn) {
if (!async_mode_flag) {
e.experimental_async_required('resource');
}
return /** @type {ResourceType<T>} */ (new Resource(fn));
}

@ -6,7 +6,7 @@ declare global {
interface Window {
__svelte?: {
/** hydratables */
h?: Map<string, string>;
h?: Map<string, unknown>;
};
}
}

@ -87,6 +87,18 @@ export function event_handler_invalid(handler, suggestion) {
}
}
/**
* Expected to find a hydratable with key `%key%` during hydration, but did not.
* @param {string} key
*/
export function hydratable_missing_but_expected(key) {
if (DEV) {
console.warn(`%c[svelte] hydratable_missing_but_expected\n%cExpected to find a hydratable with key \`${key}\` during hydration, but did not.\nhttps://svelte.dev/e/hydratable_missing_but_expected`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/hydratable_missing_but_expected`);
}
}
/**
* The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
* @param {string} attribute

@ -26,6 +26,26 @@ export function html_deprecated() {
throw error;
}
/**
* Attempted to set hydratable with key `%key%` twice. This behavior is undefined.
*
* First set occurred at:
* %stack%
* @param {string} key
* @param {string} stack
* @returns {never}
*/
export function hydratable_clobbering(key, stack) {
const error = new Error(`hydratable_clobbering\nAttempted to set hydratable with key \`${key}\` twice. This behavior is undefined.
First set occurred at:
${stack}\nhttps://svelte.dev/e/hydratable_clobbering`);
error.name = 'Svelte error';
throw error;
}
/**
* `%name%(...)` is not available on the server
* @param {string} name

@ -1,14 +1,10 @@
/** @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';
/** @type {string | null} */
export let hydratable_key = null;
/** @param {string | null} key */
export function set_hydratable_key(key) {
hydratable_key = key;
}
import * as e from './errors.js';
import { DEV } from 'esm-env';
/**
* @template T
@ -18,16 +14,19 @@ export function set_hydratable_key(key) {
* @returns {T}
*/
export function hydratable(key, fn, options) {
if (!async_mode_flag) {
e.experimental_async_required('hydratable');
}
const store = get_render_context();
if (store.hydratables.has(key)) {
// TODO error
throw new Error("can't have two hydratables with the same key");
e.hydratable_clobbering(key, store.hydratables.get(key)?.stack || 'unknown');
}
const result = fn();
store.hydratables.set(key, { value: result, encode: options?.encode });
return result;
const entry = create_entry(fn(), options?.encode);
store.hydratables.set(key, entry);
return entry.value;
}
/**
* @template T
@ -36,15 +35,34 @@ export function hydratable(key, fn, options) {
* @param {{ encode?: Encode<T> }} [options]
*/
export function set_hydratable_value(key, value, options = {}) {
if (!async_mode_flag) {
e.experimental_async_required('setHydratableValue');
}
const store = get_render_context();
if (store.hydratables.has(key)) {
// TODO error
throw new Error("can't have two hydratables with the same key");
e.hydratable_clobbering(key, store.hydratables.get(key)?.stack || 'unknown');
}
store.hydratables.set(key, {
store.hydratables.set(key, create_entry(value, options?.encode));
}
/**
* @template T
* @param {T} value
* @param {Encode<T> | undefined} encode
*/
function create_entry(value, encode) {
/** @type {Omit<HydratableEntry, 'value'> & { value: T }} */
const entry = {
value,
encode: options.encode
});
encode
};
if (DEV) {
entry.stack = new Error().stack;
}
return entry;
}

@ -1,6 +1,7 @@
import { async_mode_flag } from '../../flags/index.js';
import { BaseCacheObserver } from '../../shared/cache-observer.js';
import { set_hydratable_key } from '../hydratable.js';
import { get_render_context } from '../render-context.js';
import * as e from '../errors.js';
/**
* @template {(...args: any[]) => any} TFn
@ -9,14 +10,16 @@ import { get_render_context } from '../render-context.js';
* @returns {ReturnType<TFn>}
*/
export function cache(key, fn) {
if (!async_mode_flag) {
e.experimental_async_required('cache');
}
const cache = get_render_context().cache;
const entry = cache.get(key);
if (entry) {
return /** @type {ReturnType<TFn>} */ (entry);
}
set_hydratable_key(key);
const new_entry = fn();
set_hydratable_key(null);
cache.set(key, new_entry);
return new_entry;
}
@ -27,6 +30,9 @@ export function cache(key, fn) {
*/
export class CacheObserver extends BaseCacheObserver {
constructor(prefix = '') {
if (!async_mode_flag) {
e.experimental_async_required('CacheObserver');
}
super(() => get_render_context().cache, prefix);
}
}

@ -1,8 +1,10 @@
/** @import { GetRequestInit, Resource } from '#shared' */
import { async_mode_flag } from '../../flags/index.js';
import { fetch_json } from '../../shared/utils.js';
import { hydratable } from '../hydratable.js';
import { cache } from './cache';
import { resource } from './resource.js';
import * as e from '../errors.js';
/**
* @template TReturn
@ -11,6 +13,9 @@ import { resource } from './resource.js';
* @returns {Resource<TReturn>}
*/
export function fetcher(url, init) {
if (!async_mode_flag) {
e.experimental_async_required('fetcher');
}
const key = `svelte/fetcher/${typeof url === 'string' ? url : url.toString()}`;
return cache(key, () => resource(() => hydratable(key, () => fetch_json(url, init))));
}

@ -1,4 +1,6 @@
/** @import { Resource as ResourceType } from '#shared' */
import { async_mode_flag } from '../../flags/index.js';
import * as e from '../errors.js';
/**
* @template T
@ -6,6 +8,9 @@
* @returns {ResourceType<T>}
*/
export function resource(fn) {
if (!async_mode_flag) {
e.experimental_async_required('resource');
}
return /** @type {ResourceType<T>} */ (new Resource(fn));
}

@ -15,14 +15,14 @@ export interface SSRContext {
element?: Element;
}
export interface HydratableEntry {
value: unknown;
encode: Encode<any> | undefined;
stack?: string;
}
export interface RenderContext {
hydratables: Map<
string,
{
value: unknown;
encode: Encode<any> | undefined;
}
>;
hydratables: Map<string, HydratableEntry>;
cache: Map<string, CacheEntry>;
}

@ -2,6 +2,23 @@
import { DEV } from 'esm-env';
/**
* Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
* @param {string} name
* @returns {never}
*/
export function experimental_async_required(name) {
if (DEV) {
const error = new Error(`experimental_async_required\nCannot use \`${name}(...)\` unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async_required`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/experimental_async_required`);
}
}
/**
* Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
* @returns {never}

Loading…
Cancel
Save