Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
68c96a1
feat: `hydratable` and a more consistent remote functions model
elliott-with-the-longest-name-on-github Mar 11, 2026
7079723
another batch reviewed
elliott-with-the-longest-name-on-github Mar 11, 2026
3b0ed66
i think it work
elliott-with-the-longest-name-on-github Mar 13, 2026
6370070
types
elliott-with-the-longest-name-on-github Mar 13, 2026
0a8ba50
fix test
elliott-with-the-longest-name-on-github Mar 13, 2026
af3a09c
Fix: Debug console.log statements left in production code will spam b…
vercel[bot] Mar 13, 2026
e2ab37f
whoops
elliott-with-the-longest-name-on-github Mar 13, 2026
ec96caf
LazyPromise
elliott-with-the-longest-name-on-github Mar 13, 2026
b4b5c9b
promise chain correction
elliott-with-the-longest-name-on-github Mar 13, 2026
72bc665
better cleanup
elliott-with-the-longest-name-on-github Mar 13, 2026
903abe8
fix options-2 tests
Rich-Harris Mar 14, 2026
9862e78
cache bust
Rich-Harris Mar 15, 2026
abe6f28
Merge branch 'elliott/remote-functions-hydratable-take-2' of github.c…
Rich-Harris Mar 15, 2026
ebcc1ed
merge main
Rich-Harris Mar 15, 2026
5621893
i'm going to become the joker
Rich-Harris Mar 15, 2026
226c237
wtf wtf wtf
Rich-Harris Mar 15, 2026
cb3a98c
doh
Rich-Harris Mar 15, 2026
9e987c4
finally, it fails locally
Rich-Harris Mar 15, 2026
317230b
fix
Rich-Harris Mar 15, 2026
34295c7
simplify
Rich-Harris Mar 15, 2026
89f345f
Merge branch 'main' into elliott/remote-functions-hydratable-take-2
Rich-Harris Mar 16, 2026
c253e62
chore: hydratable remote functions tweaks (#15548)
Rich-Harris Mar 16, 2026
a4a834f
take one
elliott-with-the-longest-name-on-github Mar 16, 2026
cca9288
better
elliott-with-the-longest-name-on-github Mar 16, 2026
fb03b36
fixes
elliott-with-the-longest-name-on-github Mar 16, 2026
5895a4d
deprecate unfriendly_hydratable
Rich-Harris Mar 17, 2026
d066e33
robustify event state
Rich-Harris Mar 17, 2026
fb672bf
make it readonly
Rich-Harris Mar 17, 2026
9525d11
fix/simplify
Rich-Harris Mar 17, 2026
93fc91b
simplify
Rich-Harris Mar 17, 2026
cd0aee2
disallow run() in effects
Rich-Harris Mar 17, 2026
35da696
get rid of all the Awaited stuff, we definitely don't want that
Rich-Harris Mar 17, 2026
dd8f8fb
changeset
Rich-Harris Mar 17, 2026
7636377
lint
Rich-Harris Mar 17, 2026
4e7550d
chore: start queries on data property access, remove init
elliott-with-the-longest-name-on-github Mar 17, 2026
e593610
meh
elliott-with-the-longest-name-on-github Mar 17, 2026
e6dd225
misc
elliott-with-the-longest-name-on-github Mar 17, 2026
362d566
giddy up
elliott-with-the-longest-name-on-github Mar 17, 2026
2b94ad6
tests + batch fix
elliott-with-the-longest-name-on-github Mar 17, 2026
e3c0764
changesets
elliott-with-the-longest-name-on-github Mar 17, 2026
95fdd23
better
elliott-with-the-longest-name-on-github Mar 17, 2026
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
<script>
import { onMount } from 'svelte';
import { getData } from './example.remote';

let data = $state.raw(new Promise(() => {}));
onMount(() => {
console.log('updating');
data = getData();
});
</script>

{#await getData() then data}
<p>a: {data.a}</p>
{/await}
<svelte:boundary>
<p>a: {(await data).a}</p>

{#snippet pending()}
Loading...
{/snippet}
</svelte:boundary>
1 change: 1 addition & 0 deletions packages/adapter-netlify/test/apps/split/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import adapter from '../../../index.js';

/** @type {import('@sveltejs/kit').Config} */
const config = {
compilerOptions: { experimental: { async: true } },
kit: {
adapter: adapter({ split: true }),
experimental: {
Expand Down
6 changes: 6 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2137,6 +2137,12 @@ export type RemoteResource<T> = Promise<Awaited<T>> & {
);

export type RemoteQuery<T> = RemoteResource<T> & {
/**
* Returns a plain promise with the result.
* Unlike awaiting the resource directly, this can be used anywhere
* (load functions, event handlers, etc) without requiring a reactive context.
*/
run(): Promise<Awaited<T>>;
/**
* On the client, this function will update the value of the query without re-fetching it.
*
Expand Down
18 changes: 9 additions & 9 deletions packages/kit/src/runtime/app/server/remote/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export function form(validate_or_fn, maybe_fn) {
// We don't need to care about args or deduplicating calls, because uneval results are only relevant in full page reloads
// where only one form submission is active at the same time
if (!event.isRemoteRequest) {
get_cache(__, state)[''] ??= output;
get_cache(__, state)[''] ??= { serialize: true, data: output };
}

return output;
Expand All @@ -178,26 +178,26 @@ export function form(validate_or_fn, maybe_fn) {
get() {
return create_field_proxy(
{},
() => get_cache(__)?.['']?.input ?? {},
() => get_cache(__)?.['']?.data?.input ?? {},
(path, value) => {
const cache = get_cache(__);
const data = cache[''];
const entry = cache[''];

if (data?.submission) {
if (entry?.data?.submission) {
// don't override a submission
return;
}

if (path.length === 0) {
(cache[''] ??= {}).input = value;
(cache[''] ??= { serialize: true, data: {} }).data.input = value;
return;
}

const input = data?.input ?? {};
const input = entry?.data?.input ?? {};
deep_set(input, path.map(String), value);
(cache[''] ??= {}).input = input;
(cache[''] ??= { serialize: true, data: {} }).data.input = input;
},
() => flatten_issues(get_cache(__)?.['']?.issues ?? [])
() => flatten_issues(get_cache(__)?.['']?.data?.issues ?? [])
);
}
});
Expand All @@ -219,7 +219,7 @@ export function form(validate_or_fn, maybe_fn) {
Object.defineProperty(instance, 'result', {
get() {
try {
return get_cache(__)?.['']?.result;
return get_cache(__)?.['']?.data?.result;
} catch {
return undefined;
}
Expand Down
9 changes: 5 additions & 4 deletions packages/kit/src/runtime/app/server/remote/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {

// TODO adapters can provide prerendered data more efficiently than
// fetching from the public internet
const promise = (cache[key] ??= fetch(new URL(url, event.url.origin).href).then(
async (response) => {
const promise = (cache[key] ??= {
serialize: true,
data: fetch(new URL(url, event.url.origin).href).then(async (response) => {
if (!response.ok) {
throw new Error('Prerendered response not found');
}
Expand All @@ -116,8 +117,8 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
}

return prerendered.result;
}
));
})
}).data;

return parse_remote_response(await promise, state.transport);
});
Expand Down
115 changes: 76 additions & 39 deletions packages/kit/src/runtime/app/server/remote/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { prerendering } from '__sveltekit/environment';
import { create_validator, get_cache, get_response, run_remote_function } from './shared.js';
import { handle_error_and_jsonify } from '../../../server/utils.js';
import { HttpError, SvelteKitError } from '@sveltejs/kit/internal';
import { lazy_promise } from '../../../../utils/promise.js';

/**
* Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call.
Expand Down Expand Up @@ -77,25 +78,13 @@ export function query(validate_or_fn, maybe_fn) {
const get_remote_function_result = () =>
run_remote_function(event, state, false, () => validate(arg), fn);

/** @type {Promise<any> & Partial<RemoteQuery<any>>} */
const promise = get_response(__, arg, state, get_remote_function_result);

promise.catch(() => {});

promise.set = (value) => update_refresh_value(get_refresh_context(__, 'set', arg), value);

promise.refresh = () => {
const refresh_context = get_refresh_context(__, 'refresh', arg);
const is_immediate_refresh = !refresh_context.cache[refresh_context.cache_key];
const value = is_immediate_refresh ? promise : get_remote_function_result();
return update_refresh_value(refresh_context, value, is_immediate_refresh);
};

promise.withOverride = () => {
throw new Error(`Cannot call '${__.name}.withOverride()' on the server`);
};

return /** @type {RemoteQuery<Output>} */ (promise);
return create_query_resource({
__,
arg,
state,
start: () => get_response(__, arg, state, get_remote_function_result),
refresh: () => get_remote_function_result()
});
};

Object.defineProperty(wrapper, '__', { value: __ });
Expand Down Expand Up @@ -236,30 +225,78 @@ function batch(validate_or_fn, maybe_fn) {
});
};

/** @type {Promise<any> & Partial<RemoteQuery<any>>} */
const promise = get_response(__, arg, state, get_remote_function_result);
return create_query_resource({
__,
arg,
state,
start: () => get_response(__, arg, state, get_remote_function_result),
refresh: () => get_remote_function_result()
});
};

promise.catch(() => {});
Object.defineProperty(wrapper, '__', { value: __ });

promise.set = (value) => update_refresh_value(get_refresh_context(__, 'set', arg), value);
return wrapper;
}

promise.refresh = () => {
const refresh_context = get_refresh_context(__, 'refresh', arg);
const is_immediate_refresh = !refresh_context.cache[refresh_context.cache_key];
const value = is_immediate_refresh ? promise : get_remote_function_result();
return update_refresh_value(refresh_context, value, is_immediate_refresh);
};
const safe_query_keys = new Set(['run', 'set', 'refresh', '__']);

promise.withOverride = () => {
throw new Error(`Cannot call '${__.name}.withOverride()' on the server`);
};
/**
* @param {object} options
* @param {RemoteInfo} options.__
* @param {any} options.arg
* @param {any} options.state
* @param {() => Promise<any>} options.start
* @param {() => Promise<any>} options.refresh
* @returns {RemoteQuery<any>}
*/
function create_query_resource({ __, arg, state, start, refresh }) {
const promise = lazy_promise(safe_query_keys, start);

return /** @type {RemoteQuery<any>} */ (
new Proxy(promise, {
get(target, property, receiver) {
if (state.is_in_universal_load && property !== 'run') {
throw new Error(
// TODO docs
'This query was called in a universal `load` function and is limited to calling `.run`.'
);
}

return /** @type {RemoteQuery<Output>} */ (promise);
};
if (property === 'run') {
return () => promise;
}

Object.defineProperty(wrapper, '__', { value: __ });
if (property === 'set') {
/** @param {any} value */
return (value) => update_refresh_value(get_refresh_context(__, 'set', arg), value);
}

return wrapper;
if (property === 'refresh') {
return () => {
const refresh_context = get_refresh_context(__, 'refresh', arg);
const is_immediate_refresh = !refresh_context.cache[refresh_context.cache_key];
const value = is_immediate_refresh ? promise : refresh();
return update_refresh_value(refresh_context, value, is_immediate_refresh);
};
}

if (property === 'withOverride') {
return () => {
throw new Error(`Cannot call '${__.name}.withOverride()' on the server`);
};
}

const value = Reflect.get(target, property, receiver);

if (typeof value === 'function') {
return value.bind(target);
}

return value;
}
})
);
}

// Add batch as a property to the query function
Expand All @@ -269,7 +306,7 @@ Object.defineProperty(query, 'batch', { value: batch, enumerable: true });
* @param {RemoteInfo} __
* @param {'set' | 'refresh'} action
* @param {any} [arg]
* @returns {{ __: RemoteInfo; state: any; refreshes: Record<string, Promise<any>>; cache: Record<string, Promise<any>>; refreshes_key: string; cache_key: string }}
* @returns {{ __: RemoteInfo; state: any; refreshes: Record<string, Promise<any>>; cache: Record<string, { serialize: boolean; data: any }>; refreshes_key: string; cache_key: string }}
*/
function get_refresh_context(__, action, arg) {
const { state } = get_request_store();
Expand All @@ -290,7 +327,7 @@ function get_refresh_context(__, action, arg) {
}

/**
* @param {{ __: RemoteInfo; refreshes: Record<string, Promise<any>>; cache: Record<string, Promise<any>>; refreshes_key: string; cache_key: string }} context
* @param {{ __: RemoteInfo; refreshes: Record<string, Promise<any>>; cache: Record<string, { serialize: boolean; data: any }>; refreshes_key: string; cache_key: string }} context
* @param {any} value
* @param {boolean} [is_immediate_refresh=false]
* @returns {Promise<void>}
Expand All @@ -303,7 +340,7 @@ function update_refresh_value(
const promise = Promise.resolve(value);

if (!is_immediate_refresh) {
cache[cache_key] = promise;
cache[cache_key] = { serialize: true, data: promise };
}

if (__.id) {
Expand Down
18 changes: 16 additions & 2 deletions packages/kit/src/runtime/app/server/remote/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { parse } from 'devalue';
import { error } from '@sveltejs/kit';
import { with_request_store, get_request_store } from '@sveltejs/kit/internal/server';
import { stringify_remote_arg } from '../../../shared.js';
import { stringify_remote_arg, create_remote_key } from '../../../shared.js';
import { server_hydratable_transport } from '../../../server/utils.js';

/**
* @param {any} validate_or_fn
Expand Down Expand Up @@ -73,8 +74,21 @@ export async function get_response(info, arg, state, get_result) {
await 0;

const cache = get_cache(info, state);
const key = stringify_remote_arg(arg, state.transport);
const entry = (cache[key] ??= {
serialize: false,
data: get_result()
});

return (cache[stringify_remote_arg(arg, state.transport)] ??= get_result());
entry.serialize ||= !!state.is_in_universal_load;

if (state.is_in_render && info.id) {
const remote_key = create_remote_key(info.id, key);

void server_hydratable_transport(remote_key, state.transport, () => entry.data);
}

return entry.data;
}

/**
Expand Down
25 changes: 16 additions & 9 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,18 @@ let target;
export let app;

/**
* Data that was serialized during SSR. This is cleared when the user first navigates
* Data that was serialized during SSR for queries/forms/commands.
* This is cleared before client-side loads run.
* @type {Record<string, any>}
*/
export let remote_responses = {};
export let query_responses = {};

/**
* Data that was serialized during SSR for prerender functions.
* This persists across client-side navigations.
* @type {Record<string, any>}
*/
export let prerender_responses = {};

/** @type {Array<((url: URL) => boolean)>} */
const invalidated = [];
Expand Down Expand Up @@ -282,7 +290,7 @@ const preload_tokens = new Set();
export let pending_invalidate;

/**
* @type {Map<string, {count: number, resource: any}>}
* @type {Map<string, { count: number, resource: any }>}
* A map of id -> query info with all queries that currently exist in the app.
*/
export const query_map = new Map();
Expand All @@ -299,8 +307,9 @@ export async function start(_app, _target, hydrate) {
);
}

if (__SVELTEKIT_PAYLOAD__?.data) {
remote_responses = __SVELTEKIT_PAYLOAD__.data;
if (__SVELTEKIT_PAYLOAD__) {
query_responses = __SVELTEKIT_PAYLOAD__.query ?? {};
prerender_responses = __SVELTEKIT_PAYLOAD__.prerender ?? {};
}

// detect basic auth credentials in the current URL
Expand Down Expand Up @@ -1535,8 +1544,6 @@ async function navigate({
block = noop,
event
}) {
remote_responses = {};

const prev_token = token;
token = nav_token;

Expand Down Expand Up @@ -2177,8 +2184,6 @@ export function refreshAll({ includeLoadFunctions = true } = {}) {
throw new Error('Cannot call refreshAll() on the server');
}

remote_responses = {};

force_invalidation = true;
return _invalidate(includeLoadFunctions, false);
}
Expand Down Expand Up @@ -2855,6 +2860,8 @@ async function _hydrate(

target.textContent = '';
hydrate = false;
} finally {
query_responses = {};
}

if (result.props.page) {
Expand Down
Loading
Loading