Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
97c132e
working state
elliott-with-the-longest-name-on-github Oct 31, 2025
eaba771
checkpoint, need to merge main
elliott-with-the-longest-name-on-github Nov 3, 2025
068f09d
Merge branch 'main' into elliott/impl-remote-functions-new-primitives
elliott-with-the-longest-name-on-github Nov 3, 2025
2215baf
more tests passing
elliott-with-the-longest-name-on-github Nov 4, 2025
94a5f4a
more tests passing
elliott-with-the-longest-name-on-github Nov 4, 2025
82d5cb7
conflicts
elliott-with-the-longest-name-on-github Nov 4, 2025
a471f45
chore: move/rename
elliott-with-the-longest-name-on-github Nov 4, 2025
3805388
oops
elliott-with-the-longest-name-on-github Nov 4, 2025
e6ded14
pkg.pr.new is annoying
elliott-with-the-longest-name-on-github Nov 4, 2025
44c3936
oops
elliott-with-the-longest-name-on-github Nov 4, 2025
bcc6c91
fix some prerender bug
elliott-with-the-longest-name-on-github Nov 4, 2025
1947871
fix prerendering agian
elliott-with-the-longest-name-on-github Nov 4, 2025
9e1a582
update svelte
elliott-with-the-longest-name-on-github Nov 4, 2025
fe8d72f
fix
elliott-with-the-longest-name-on-github Nov 5, 2025
ce4fe70
fix
elliott-with-the-longest-name-on-github Nov 5, 2025
7a2cf93
your problems go away if you just ignore them
elliott-with-the-longest-name-on-github Nov 5, 2025
f25725a
feat: I think tests pass now
elliott-with-the-longest-name-on-github Nov 5, 2025
cbf04a9
update sv
elliott-with-the-longest-name-on-github Nov 5, 2025
eff3c55
Merge branch 'main' into elliott/impl-remote-functions-new-primitives
elliott-with-the-longest-name-on-github Nov 5, 2025
9dd2332
update svelte
elliott-with-the-longest-name-on-github Nov 6, 2025
4c53cdb
wow did this really do everything??
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
8 changes: 7 additions & 1 deletion packages/kit/src/exports/internal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,10 @@ export class ActionFailure {
}
}

export { init_remote_functions } from './remote-functions.js';
export {
init_remote_functions,
create_remote_id,
create_remote_cache_key,
REMOTE_CACHE_DELIMITER,
REMOTE_CACHE_PREFIX
} from './remote-functions.js';
20 changes: 19 additions & 1 deletion packages/kit/src/exports/internal/remote-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,25 @@ export function init_remote_functions(module, file, hash) {
);
}

fn.__.id = `${hash}/${name}`;
fn.__.id = create_remote_id(hash, name);
fn.__.name = name;
}
}

export const REMOTE_CACHE_PREFIX = '@sveltejs/kit/remote';
export const REMOTE_CACHE_DELIMITER = '::::';

/**
* @param {string} id
* @param {string} payload
*/
export function create_remote_cache_key(id, payload) {
return `${REMOTE_CACHE_PREFIX}${REMOTE_CACHE_DELIMITER}${id}${REMOTE_CACHE_DELIMITER}${payload ?? ''}`;
}

/**
* @param {(string | undefined)[]} identifiers
*/
export function create_remote_id(...identifiers) {
return identifiers.filter((id) => id !== undefined).join(REMOTE_CACHE_DELIMITER);
}
7 changes: 4 additions & 3 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
import { import_peer } from '../../utils/import.js';
import { compact } from '../../utils/array.js';
import { should_ignore } from './static_analysis/utils.js';
import { create_remote_id } from '@sveltejs/kit/internal';

const cwd = process.cwd();

Expand Down Expand Up @@ -709,14 +710,14 @@ async function kit({ svelte_config }) {
'\n\n' +
dedent`
import * as $$_self_$$ from './${path.basename(id)}';
import { init_remote_functions as $$_init_$$ } from '@sveltejs/kit/internal';
import { init_remote_functions as $$_init_$$, create_remote_id as $$_create_remote_id_$$ } from '@sveltejs/kit/internal';

${dev_server ? 'await Promise.resolve()' : ''}

$$_init_$$($$_self_$$, ${s(file)}, ${s(remote.hash)});

for (const [name, fn] of Object.entries($$_self_$$)) {
fn.__.id = ${s(remote.hash)} + '/' + name;
fn.__.id = $$_create_remote_id_$$(${s(remote.hash)}, name);
fn.__.name = name;
}
`;
Expand Down Expand Up @@ -772,7 +773,7 @@ async function kit({ svelte_config }) {
while (map.has(namespace)) namespace = `__remote${uid++}`;

const exports = Array.from(map).map(([name, type]) => {
return `export const ${name} = ${namespace}.${type}('${remote.hash}/${name}');`;
return `export const ${name} = ${namespace}.${type}('${create_remote_id(remote.hash, name)}');`;
});

let result = `import * as ${namespace} from '__sveltekit/remote';\n\n${exports.join('\n')}\n`;
Expand Down
11 changes: 6 additions & 5 deletions packages/kit/src/runtime/app/server/remote/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
flatten_issues
} from '../../../form-utils.js';
import { get_cache, run_remote_function } from './shared.js';
import { create_remote_id } from '@sveltejs/kit/internal';

/**
* Creates a form object that can be spread onto a `<form>` element.
Expand Down Expand Up @@ -188,7 +189,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).data[''] ??= output;
}

return output;
Expand All @@ -209,7 +210,7 @@ export function form(validate_or_fn, maybe_fn) {

Object.defineProperty(instance, 'fields', {
get() {
const data = get_cache(__)?.[''];
const data = get_cache(__).data[''];
const issues = flatten_issues(data?.issues ?? []);

return create_field_proxy(
Expand All @@ -224,7 +225,7 @@ export function form(validate_or_fn, maybe_fn) {
const input =
path.length === 0 ? value : deep_set(data?.input ?? {}, path.map(String), value);

(get_cache(__)[''] ??= {}).input = input;
(get_cache(__).data[''] ??= {}).input = input;
},
() => issues
);
Expand All @@ -239,7 +240,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 Expand Up @@ -277,7 +278,7 @@ export function form(validate_or_fn, maybe_fn) {

if (!instance) {
instance = create_instance(key);
instance.__.id = `${__.id}/${encodeURIComponent(JSON.stringify(key))}`;
instance.__.id = create_remote_id(__.id, encodeURIComponent(JSON.stringify(key)));
instance.__.name = __.name;

state.form_instances.set(cache_key, instance);
Expand Down
30 changes: 13 additions & 17 deletions packages/kit/src/runtime/app/server/remote/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ import { DEV } from 'esm-env';
import { get_request_store } from '@sveltejs/kit/internal/server';
import { stringify, stringify_remote_arg } from '../../../shared.js';
import { app_dir, base } from '$app/paths/internal/server';
import {
create_validator,
get_cache,
get_response,
parse_remote_response,
run_remote_function
} from './shared.js';
import { create_validator, get_response, run_remote_function } from './shared.js';
import { create_remote_id } from '@sveltejs/kit/internal';
import * as devalue from 'devalue';

/**
* Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call.
Expand Down Expand Up @@ -93,18 +89,15 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
const { event, state } = get_request_store();
const payload = stringify_remote_arg(arg, state.transport);
const id = __.id;
const url = `${base}/${app_dir}/remote/${id}${payload ? `/${payload}` : ''}`;
const url = `${base}/${app_dir}/remote/${create_remote_id(id, payload)}`;

if (!state.prerendering && !DEV && !event.isRemoteRequest) {
try {
return await get_response(__, arg, state, async () => {
const key = stringify_remote_arg(arg, state.transport);
const cache = get_cache(__, state);

// 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) => {
return await fetch(new URL(url, event.url.origin).href)
.then(async (response) => {
if (!response.ok) {
throw new Error('Prerendered response not found');
}
Expand All @@ -116,10 +109,13 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
}

return prerendered.result;
}
));

return parse_remote_response(await promise, state.transport);
})
.then((data) =>
devalue.parse(
data,
Object.fromEntries(Object.entries(state.transport).map(([k, v]) => [k, v.decode]))
)
);
});
} catch {
// not available prerendered, fallback to normal function
Expand Down
5 changes: 3 additions & 2 deletions packages/kit/src/runtime/app/server/remote/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
/** @import { RemoteInfo, MaybePromise } from 'types' */
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
import { get_request_store } from '@sveltejs/kit/internal/server';
import { create_remote_cache_key, stringify_remote_arg } from '../../../shared.js';
import { stringify_remote_arg } from '../../../shared.js';
import { prerendering } from '__sveltekit/environment';
import { create_validator, get_cache, get_response, run_remote_function } from './shared.js';
import { create_remote_cache_key } from '@sveltejs/kit/internal';

/**
* 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 @@ -93,7 +94,7 @@ export function query(validate_or_fn, maybe_fn) {
if (__.id) {
const cache = get_cache(__, state);
const key = stringify_remote_arg(arg, state.transport);
refreshes[create_remote_cache_key(__.id, key)] = cache[key] = Promise.resolve(value);
refreshes[create_remote_cache_key(__.id, key)] = cache.data[key] = Promise.resolve(value);
}
};

Expand Down
36 changes: 18 additions & 18 deletions packages/kit/src/runtime/app/server/remote/shared.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/** @import { RequestEvent } from '@sveltejs/kit' */
/** @import { ServerHooks, MaybePromise, RequestState, RemoteInfo, RequestStore } from 'types' */
import { parse } from 'devalue';
/** @import { MaybePromise, RequestState, RemoteInfo, RequestStore } from 'types' */
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, stringify_remote_arg } from '../../../shared.js';
import { hydratable } from 'svelte';
import { create_remote_cache_key } from '@sveltejs/kit/internal';

/**
* @param {any} validate_or_fn
Expand Down Expand Up @@ -75,21 +76,20 @@ export async function get_response(info, arg, state, get_result) {

const cache = get_cache(info, state);

return (cache[stringify_remote_arg(arg, state.transport)] ??= get_result());
}
const payload = stringify_remote_arg(arg, state.transport);
const response = (cache.data[payload] ??= get_result());

/**
* @param {any} data
* @param {ServerHooks['transport']} transport
*/
export function parse_remote_response(data, transport) {
/** @type {Record<string, any>} */
const revivers = {};
for (const key in transport) {
revivers[key] = transport[key].decode;
if (info.id) {
const key = create_remote_cache_key(info.id, payload);
if (state.is_in_render && !hydratable.has(key)) {
hydratable.set(key, response, {
encode: (val) => stringify(val, state.transport)
});
}
cache.universal_load ||= state.is_in_universal_load;
}

return parse(data, revivers);
return response;
}

/**
Expand Down Expand Up @@ -152,11 +152,11 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali
* @param {RequestState} state
*/
export function get_cache(info, state = get_request_store().state) {
let cache = state.remote_data?.get(info);
let cache = state.remote_responses.get(info);

if (cache === undefined) {
cache = {};
(state.remote_data ??= new Map()).set(info, cache);
cache = { universal_load: false, data: {} };
state.remote_responses.set(info, cache);
}

return cache;
Expand Down
18 changes: 8 additions & 10 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { page, update, navigating } from './state.svelte.js';
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
import { noop_span } from '../telemetry/noop.js';
import { text_decoder } from '../utils.js';
import { query_cache } from './remote-functions/query-cache.js';

export { load_css };
const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
Expand Down Expand Up @@ -190,6 +191,9 @@ let target;
export let app;

/**
* TODO this is only needed to catch stuff that's in `load` functions, so it can eventually
* be removed when we deprecate `load` functions. That, or we can decide that it's only valid to
* call remote functions in the render cycle. (This might be good in the long run?)
* Data that was serialized during SSR. This is cleared when the user first navigates
* @type {Record<string, any>}
*/
Expand Down Expand Up @@ -281,12 +285,6 @@ const preload_tokens = new Set();
/** @type {Promise<void> | null} */
export let pending_invalidate;

/**
* @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();

/**
* @param {import('./types.js').SvelteKitApp} _app
* @param {HTMLElement} _target
Expand Down Expand Up @@ -391,7 +389,7 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru

// Rerun queries
if (force_invalidation) {
query_map.forEach(({ resource }) => {
query_cache.forEach((resource) => {
resource.refresh?.();
});
}
Expand Down Expand Up @@ -423,7 +421,7 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
}

// Don't use allSettled yet because it's too new
await Promise.all([...query_map.values()].map(({ resource }) => resource)).catch(noop);
await Promise.all([...query_cache.values()]).catch(noop);
}

function reset_invalidation() {
Expand Down Expand Up @@ -481,7 +479,7 @@ export async function _goto(url, options, redirect_count, nav_token) {
accept: () => {
if (options.invalidateAll) {
force_invalidation = true;
query_keys = [...query_map.keys()];
query_keys = [...query_cache.keys()];
}

if (options.invalidate) {
Expand All @@ -497,7 +495,7 @@ export async function _goto(url, options, redirect_count, nav_token) {
.tick()
.then(svelte.tick)
.then(() => {
query_map.forEach(({ resource }, key) => {
query_cache.forEach((resource, key) => {
// Only refresh those that already existed on the old page
if (query_keys?.includes(key)) {
resource.refresh?.();
Expand Down
18 changes: 12 additions & 6 deletions packages/kit/src/runtime/client/remote-functions/form.svelte.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import { app_dir, base } from '$app/paths/internal/client';
import * as devalue from 'devalue';
import { DEV } from 'esm-env';
import { HttpError } from '@sveltejs/kit/internal';
import { app, remote_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
import { tick } from 'svelte';
import { HttpError, REMOTE_CACHE_DELIMITER } from '@sveltejs/kit/internal';
import { app, _goto, set_nearest_error_page, invalidateAll, remote_responses } from '../client.js';
import { tick, hydratable } from 'svelte';
import { refresh_queries, release_overrides } from './shared.svelte.js';
import { createAttachmentKey } from 'svelte/attachments';
import {
Expand Down Expand Up @@ -55,7 +55,8 @@ export function form(id) {

/** @param {string | number | boolean} [key] */
function create_instance(key) {
const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
const action_id =
id + (key != undefined ? `${REMOTE_CACHE_DELIMITER}${JSON.stringify(key)}` : '');
const action = '?/remote=' + encodeURIComponent(action_id);

/**
Expand All @@ -69,7 +70,12 @@ export function form(id) {
const issues = $derived(flatten_issues(raw_issues));

/** @type {any} */
let result = $state.raw(remote_responses[action_id]);
let result = $state.raw(
remote_responses[action_id] ??
hydratable.get(action_id, {
decode: (val) => devalue.parse(/** @type {string} */ (val), app.decoders)
})
);

/** @type {number} */
let pending_count = $state(0);
Expand Down Expand Up @@ -299,7 +305,7 @@ export function form(id) {
if (element) {
let message = `A form object can only be attached to a single \`<form>\` element`;
if (DEV && !key) {
const name = id.split('/').pop();
const name = id.split(REMOTE_CACHE_DELIMITER).pop();
message += `. To create multiple instances, use \`${name}.for(key)\``;
}

Expand Down
Loading
Loading