Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
da3260f
checkpoint; hydratable and base resource work
elliott-with-the-longest-name-on-github Oct 8, 2025
4d766f8
checkpoint
elliott-with-the-longest-name-on-github Oct 11, 2025
7c8c1ad
maximum hydration
elliott-with-the-longest-name-on-github Oct 15, 2025
83643ce
upgrade devalue
elliott-with-the-longest-name-on-github Oct 15, 2025
61d02fe
Merge remote-tracking branch 'origin' into elliott/resources
elliott-with-the-longest-name-on-github Oct 15, 2025
bca87b9
checkpoint
elliott-with-the-longest-name-on-github Oct 15, 2025
82be388
chore: temporarily remove fetcher
elliott-with-the-longest-name-on-github Oct 15, 2025
25210c2
types
elliott-with-the-longest-name-on-github Oct 15, 2025
9c7da6c
only generate hydratables when there's some amount of content
elliott-with-the-longest-name-on-github Oct 15, 2025
5de6383
add hash
elliott-with-the-longest-name-on-github Oct 15, 2025
8449ea7
making progress i think
elliott-with-the-longest-name-on-github Oct 21, 2025
ef11dae
typegen
elliott-with-the-longest-name-on-github Oct 21, 2025
d36894a
it at least basically works
elliott-with-the-longest-name-on-github Oct 21, 2025
0c4ce5a
misc improvements
elliott-with-the-longest-name-on-github Oct 24, 2025
a2bff0c
split stuff out, fix treeshaking
elliott-with-the-longest-name-on-github Oct 29, 2025
2e292b1
cache observer
elliott-with-the-longest-name-on-github Oct 29, 2025
7ee0ce8
fix export
elliott-with-the-longest-name-on-github Oct 29, 2025
90b85d1
add imperative hydratable API
elliott-with-the-longest-name-on-github Oct 30, 2025
598dc30
fix types
elliott-with-the-longest-name-on-github Oct 30, 2025
2c08d4f
fix types
elliott-with-the-longest-name-on-github Oct 30, 2025
5adb3f1
test
elliott-with-the-longest-name-on-github Oct 31, 2025
8179c6a
tests
elliott-with-the-longest-name-on-github Oct 31, 2025
6873685
temp fix
elliott-with-the-longest-name-on-github Oct 31, 2025
8939bbf
fix never-expiring cache entries
elliott-with-the-longest-name-on-github Nov 4, 2025
0f6001d
misc
elliott-with-the-longest-name-on-github Nov 4, 2025
8667dab
Merge remote-tracking branch 'origin' into elliott/resources
elliott-with-the-longest-name-on-github Nov 4, 2025
200b011
types
elliott-with-the-longest-name-on-github Nov 4, 2025
816ddca
import
elliott-with-the-longest-name-on-github Nov 4, 2025
7d44a1d
remove cruft, fix some type errors
elliott-with-the-longest-name-on-github Nov 4, 2025
cc094c5
.js .js .js wah wah wah
elliott-with-the-longest-name-on-github Nov 4, 2025
c6da91f
if you ignore your problems they go away
elliott-with-the-longest-name-on-github Nov 4, 2025
b36ba6d
unused
elliott-with-the-longest-name-on-github Nov 4, 2025
d6f240a
better serialization
elliott-with-the-longest-name-on-github Nov 4, 2025
7d0451e
oops
elliott-with-the-longest-name-on-github Nov 4, 2025
cd7a71f
oops
elliott-with-the-longest-name-on-github Nov 5, 2025
0581bb9
tweak
elliott-with-the-longest-name-on-github Nov 5, 2025
5aa7598
tweak
elliott-with-the-longest-name-on-github Nov 5, 2025
08d755b
tweak
elliott-with-the-longest-name-on-github Nov 5, 2025
9a424cd
add errors
elliott-with-the-longest-name-on-github Nov 5, 2025
e28ced7
tweak hydratable API, add official errors
elliott-with-the-longest-name-on-github Nov 6, 2025
aaf2eb8
fix types
elliott-with-the-longest-name-on-github Nov 6, 2025
555e950
Merge remote-tracking branch 'origin' into elliott/resources
elliott-with-the-longest-name-on-github Nov 6, 2025
d5fef8b
types
elliott-with-the-longest-name-on-github Nov 6, 2025
3f24dd2
Update packages/svelte/src/internal/server/renderer.js
elliott-with-the-longest-name-on-github Nov 6, 2025
551572e
errors
elliott-with-the-longest-name-on-github Nov 6, 2025
998510c
Merge branch 'elliott/resources' of github.com:sveltejs/svelte into e…
elliott-with-the-longest-name-on-github Nov 6, 2025
f9123f4
Update packages/svelte/messages/client-errors/errors.md
elliott-with-the-longest-name-on-github Nov 6, 2025
d5e4af8
fixes
elliott-with-the-longest-name-on-github Nov 6, 2025
61706dd
Merge branch 'elliott/resources' of github.com:sveltejs/svelte into e…
elliott-with-the-longest-name-on-github Nov 6, 2025
0ff9656
fix
elliott-with-the-longest-name-on-github Nov 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions documentation/docs/98-reference/.generated/client-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand All @@ -146,6 +140,12 @@ The `flushSync()` function can be used to flush any pending effects synchronousl

This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.

### fn_unavailable_on_client

```
`%name%`(...) is unavailable on the client.
```

### fork_discarded

```
Expand Down
19 changes: 19 additions & 0 deletions documentation/docs/98-reference/.generated/client-warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
38 changes: 37 additions & 1 deletion documentation/docs/98-reference/.generated/server-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,52 @@ Encountered asynchronous work while rendering synchronously.

You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.

### fn_unavailable_on_server

```
`%name%`(...) is unavailable on the server.
```

### html_deprecated

```
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
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
```

Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
### render_context_unavailable

```
Failed to retrieve `render` context. %addendum%
```

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.
6 changes: 6 additions & 0 deletions documentation/docs/98-reference/.generated/shared-errors.md
Original file line number Diff line number Diff line change
@@ -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

```
Expand Down
8 changes: 4 additions & 4 deletions packages/svelte/messages/client-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -112,6 +108,10 @@ The `flushSync()` function can be used to flush any pending effects synchronousl

This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.

## fn_unavailable_on_client

> `%name%`(...) is unavailable in the browser.

## fork_discarded

> Cannot commit a fork that was already discarded
Expand Down
17 changes: 17 additions & 0 deletions packages/svelte/messages/client-warnings/warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion packages/svelte/messages/server-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,42 @@

You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.

## fn_unavailable_on_server

> `%name%`(...) is unavailable on the server.

## html_deprecated

> 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 `hydratable.set` 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 `hydratable.has`.

```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

Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

## render_context_unavailable

> Failed to retrieve `render` context. %addendum%

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.
4 changes: 4 additions & 0 deletions packages/svelte/messages/shared-errors/errors.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.4.1",
"esm-env": "^1.2.1",
"esrap": "^2.1.0",
"is-reference": "^3.0.3",
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/index-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ export {
hasContext,
setContext
} from './internal/client/context.js';
export { hydratable } from './internal/client/hydratable.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack, settled } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';
2 changes: 2 additions & 0 deletions packages/svelte/src/index-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,6 @@ export {
setContext
} from './internal/server/context.js';

export { hydratable } from './internal/server/hydratable.js';

export { createRawSnippet } from './internal/server/blocks/snippet.js';
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { active_effect, active_reaction } from './runtime.js';
import { create_user_effect } from './reactivity/effects.js';
import { async_mode_flag, legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
import { BRANCH_EFFECT, EFFECT_RAN } from './constants.js';
import { BRANCH_EFFECT } from './constants.js';

/** @type {ComponentContext | null} */
export let component_context = null;
Expand Down
17 changes: 9 additions & 8 deletions packages/svelte/src/internal/client/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,34 +230,35 @@ export function effect_update_depth_exceeded() {
}

/**
* Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
* Cannot use `flushSync` inside an effect
* @returns {never}
*/
export function experimental_async_fork() {
export function flush_sync_in_effect() {
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`);
const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`);

error.name = 'Svelte error';

throw error;
} else {
throw new Error(`https://svelte.dev/e/experimental_async_fork`);
throw new Error(`https://svelte.dev/e/flush_sync_in_effect`);
}
}

/**
* Cannot use `flushSync` inside an effect
* `%name%`(...) is unavailable on the client.
* @param {string} name
* @returns {never}
*/
export function flush_sync_in_effect() {
export function fn_unavailable_on_client(name) {
if (DEV) {
const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`);
const error = new Error(`fn_unavailable_on_client\n\`${name}\`(...) is unavailable on the client.\nhttps://svelte.dev/e/fn_unavailable_on_client`);

error.name = 'Svelte error';

throw error;
} else {
throw new Error(`https://svelte.dev/e/flush_sync_in_effect`);
throw new Error(`https://svelte.dev/e/fn_unavailable_on_client`);
}
}

Expand Down
97 changes: 97 additions & 0 deletions packages/svelte/src/internal/client/hydratable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/** @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';
import * as e from './errors.js';

/**
* @template T
* @param {string} key
* @param {() => T} fn
* @param {Transport<T>} [options]
* @returns {T}
*/
function isomorphic_hydratable(key, fn, options) {
if (!async_mode_flag) {
e.experimental_async_required('hydratable');
}

return access_hydratable_store(
key,
(val, has) => {
if (!has) {
w.hydratable_missing_but_expected(key);
return fn();
}
return decode(val, options?.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;
Comment on lines +36 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the indirection? why not export function hydratable(...) {...}?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the only way I could figure out to type it correctly


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

return access_hydratable_store(
key,
(val) => 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');
}
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);
}

/**
* @template T
* @param {unknown} val
* @param {Decode<T> | undefined} decode
* @returns {T}
*/
function decode(val, decode) {
return (decode ?? ((val) => /** @type {T} */ (val)))(val);
}
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/reactivity/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading