Skip to content

Commit f360657

Browse files
Rich-Harrisdummdidummteemingc
authored
fix: create separate cache entries for non-exported queries (#14499)
* fix: create separate cache entries for non-exported queries * fix * remove * rename * DRY out * more * tidy up * unused * update docs to use a non-exported query instead of caching on locals * Update .changeset/soft-emus-fry.md Co-authored-by: Tee Ming <[email protected]> * Update packages/kit/src/runtime/app/server/remote/query.js --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Tee Ming <[email protected]>
1 parent e177228 commit f360657

File tree

11 files changed

+102
-43
lines changed

11 files changed

+102
-43
lines changed

.changeset/soft-emus-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: create separate cache entries for non-exported remote function queries

documentation/docs/20-core-concepts/60-remote-functions.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -995,13 +995,13 @@ export const getProfile = query(async () => {
995995
};
996996
});
997997

998-
// this function could be called from multiple places
999-
function getUser() {
1000-
const { cookies, locals } = getRequestEvent();
998+
// this query could be called from multiple places, but
999+
// the function will only run once per request
1000+
const getUser = query(() => {
1001+
const { cookies } = getRequestEvent();
10011002

1002-
locals.userPromise ??= findUser(cookies.get('session_id'));
1003-
return await locals.userPromise;
1004-
}
1003+
return await findUser(cookies.get('session_id'));
1004+
});
10051005
```
10061006
10071007
Note that some properties of `RequestEvent` are different inside remote functions. There are no `params` or `route.id`, and you cannot set headers (other than writing cookies, and then only inside `form` and `command` functions), and `url.pathname` is always `/` (since the path that’s actually being requested by the client is purely an implementation detail).

packages/kit/src/runtime/app/server/remote/form.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
44
import { get_request_store } from '@sveltejs/kit/internal/server';
55
import { DEV } from 'esm-env';
6-
import { run_remote_function } from './shared.js';
6+
import { get_cache, run_remote_function } from './shared.js';
77
import { convert_formdata, flatten_issues } from '../../../utils.js';
88

99
/**
@@ -166,7 +166,7 @@ export function form(validate_or_fn, maybe_fn) {
166166
// We don't need to care about args or deduplicating calls, because uneval results are only relevant in full page reloads
167167
// where only one form submission is active at the same time
168168
if (!event.isRemoteRequest) {
169-
(state.remote_data ??= {})[__.id] = output;
169+
get_cache(__, state)[''] ??= output;
170170
}
171171

172172
return output;
@@ -189,8 +189,7 @@ export function form(validate_or_fn, maybe_fn) {
189189
Object.defineProperty(instance, property, {
190190
get() {
191191
try {
192-
const { remote_data } = get_request_store().state;
193-
return remote_data?.[__.id]?.[property] ?? {};
192+
return get_cache(__)?.['']?.[property] ?? {};
194193
} catch {
195194
return undefined;
196195
}
@@ -201,8 +200,7 @@ export function form(validate_or_fn, maybe_fn) {
201200
Object.defineProperty(instance, 'result', {
202201
get() {
203202
try {
204-
const { remote_data } = get_request_store().state;
205-
return remote_data?.[__.id]?.result;
203+
return get_cache(__)?.['']?.result;
206204
} catch {
207205
return undefined;
208206
}

packages/kit/src/runtime/app/server/remote/prerender.js

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
import { error, json } from '@sveltejs/kit';
55
import { DEV } from 'esm-env';
66
import { get_request_store } from '@sveltejs/kit/internal/server';
7-
import { create_remote_cache_key, stringify, stringify_remote_arg } from '../../../shared.js';
7+
import { stringify, stringify_remote_arg } from '../../../shared.js';
88
import { app_dir, base } from '__sveltekit/paths';
99
import {
1010
create_validator,
11+
get_cache,
1112
get_response,
1213
parse_remote_response,
1314
run_remote_function
@@ -96,25 +97,29 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
9697

9798
if (!state.prerendering && !DEV && !event.isRemoteRequest) {
9899
try {
99-
return await get_response(id, arg, state, async () => {
100+
return await get_response(__, arg, state, async () => {
101+
const key = stringify_remote_arg(arg, state.transport);
102+
const cache = get_cache(__, state);
103+
100104
// TODO adapters can provide prerendered data more efficiently than
101105
// fetching from the public internet
102-
const response = await fetch(new URL(url, event.url.origin).href);
103-
104-
if (!response.ok) {
105-
throw new Error('Prerendered response not found');
106-
}
106+
const promise = (cache[key] ??= fetch(new URL(url, event.url.origin).href).then(
107+
async (response) => {
108+
if (!response.ok) {
109+
throw new Error('Prerendered response not found');
110+
}
107111

108-
const prerendered = await response.json();
112+
const prerendered = await response.json();
109113

110-
if (prerendered.type === 'error') {
111-
error(prerendered.status, prerendered.error);
112-
}
114+
if (prerendered.type === 'error') {
115+
error(prerendered.status, prerendered.error);
116+
}
113117

114-
// TODO can we redirect here?
118+
return prerendered.result;
119+
}
120+
));
115121

116-
(state.remote_data ??= {})[create_remote_cache_key(id, payload)] = prerendered.result;
117-
return parse_remote_response(prerendered.result, state.transport);
122+
return parse_remote_response(await promise, state.transport);
118123
});
119124
} catch {
120125
// not available prerendered, fallback to normal function
@@ -125,7 +130,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
125130
return /** @type {Promise<any>} */ (state.prerendering.remote_responses.get(url));
126131
}
127132

128-
const promise = get_response(id, arg, state, () =>
133+
const promise = get_response(__, arg, state, () =>
129134
run_remote_function(event, state, false, arg, validate, fn)
130135
);
131136

packages/kit/src/runtime/app/server/remote/query.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { get_request_store } from '@sveltejs/kit/internal/server';
55
import { create_remote_cache_key, stringify_remote_arg } from '../../../shared.js';
66
import { prerendering } from '__sveltekit/environment';
7-
import { create_validator, get_response, run_remote_function } from './shared.js';
7+
import { create_validator, get_cache, get_response, run_remote_function } from './shared.js';
88

99
/**
1010
* Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call.
@@ -73,7 +73,7 @@ export function query(validate_or_fn, maybe_fn) {
7373
const { event, state } = get_request_store();
7474

7575
/** @type {Promise<any> & Partial<RemoteQuery<any>>} */
76-
const promise = get_response(__.id, arg, state, () =>
76+
const promise = get_response(__, arg, state, () =>
7777
run_remote_function(event, state, false, arg, validate, fn)
7878
);
7979

@@ -90,8 +90,12 @@ export function query(validate_or_fn, maybe_fn) {
9090
);
9191
}
9292

93-
const cache_key = create_remote_cache_key(__.id, stringify_remote_arg(arg, state.transport));
94-
refreshes[cache_key] = (state.remote_data ??= {})[cache_key] = Promise.resolve(value);
93+
const cache = get_cache(__, state);
94+
const key = stringify_remote_arg(arg, state.transport);
95+
96+
if (__.id) {
97+
refreshes[__.id + '/' + key] = cache[key] = Promise.resolve(value);
98+
}
9599
};
96100

97101
promise.refresh = () => {
@@ -198,7 +202,7 @@ function batch(validate_or_fn, maybe_fn) {
198202
const { event, state } = get_request_store();
199203

200204
/** @type {Promise<any> & Partial<RemoteQuery<any>>} */
201-
const promise = get_response(__.id, arg, state, () => {
205+
const promise = get_response(__, arg, state, () => {
202206
// Collect all the calls to the same query in the same macrotask,
203207
// then execute them as one backend request.
204208
return new Promise((resolve, reject) => {

packages/kit/src/runtime/app/server/remote/shared.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/** @import { RequestEvent } from '@sveltejs/kit' */
2-
/** @import { ServerHooks, MaybePromise, RequestState } from 'types' */
2+
/** @import { ServerHooks, MaybePromise, RequestState, RemoteInfo } from 'types' */
33
import { parse } from 'devalue';
44
import { error } from '@sveltejs/kit';
55
import { with_request_store, get_request_store } from '@sveltejs/kit/internal/server';
6-
import { create_remote_cache_key, stringify_remote_arg } from '../../../shared.js';
6+
import { stringify_remote_arg } from '../../../shared.js';
77

88
/**
99
* @param {any} validate_or_fn
@@ -62,19 +62,20 @@ export function create_validator(validate_or_fn, maybe_fn) {
6262
* Also saves an uneval'ed version of the result for later HTML inlining for hydration.
6363
*
6464
* @template {MaybePromise<any>} T
65-
* @param {string} id
65+
* @param {RemoteInfo} info
6666
* @param {any} arg
6767
* @param {RequestState} state
6868
* @param {() => Promise<T>} get_result
6969
* @returns {Promise<T>}
7070
*/
71-
export async function get_response(id, arg, state, get_result) {
71+
export async function get_response(info, arg, state, get_result) {
7272
// wait a beat, in case `myQuery().set(...)` is immediately called
7373
// eslint-disable-next-line @typescript-eslint/await-thenable
7474
await 0;
7575

76-
const cache_key = create_remote_cache_key(id, stringify_remote_arg(arg, state.transport));
77-
return ((state.remote_data ??= {})[cache_key] ??= get_result());
76+
const cache = get_cache(info, state);
77+
78+
return (cache[stringify_remote_arg(arg, state.transport)] ??= get_result());
7879
}
7980

8081
/**
@@ -141,3 +142,18 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali
141142
const validated = await with_request_store({ event: cleansed, state }, () => validate(arg));
142143
return with_request_store({ event: cleansed, state }, () => fn(validated));
143144
}
145+
146+
/**
147+
* @param {RemoteInfo} info
148+
* @param {RequestState} state
149+
*/
150+
export function get_cache(info, state = get_request_store().state) {
151+
let cache = state.remote_data?.get(info);
152+
153+
if (cache === undefined) {
154+
cache = {};
155+
(state.remote_data ??= new Map()).set(info, cache);
156+
}
157+
158+
return cache;
159+
}

packages/kit/src/runtime/server/page/render.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,16 +467,22 @@ export async function render_response({
467467
args.push(`{\n${indent}\t${hydrate.join(`,\n${indent}\t`)}\n${indent}}`);
468468
}
469469

470-
const { remote_data } = event_state;
470+
const { remote_data: remote_cache } = event_state;
471471

472472
let serialized_remote_data = '';
473473

474-
if (remote_data) {
474+
if (remote_cache) {
475475
/** @type {Record<string, any>} */
476476
const remote = {};
477477

478-
for (const key in remote_data) {
479-
remote[key] = await remote_data[key];
478+
for (const [info, cache] of remote_cache) {
479+
// remote functions without an `id` aren't exported, and thus
480+
// cannot be called from the client
481+
if (!info.id) continue;
482+
483+
for (const key in cache) {
484+
remote[key ? info.id + '/' + key : info.id] = await cache[key];
485+
}
480486
}
481487

482488
// TODO this is repeated in a few places — dedupe it

packages/kit/src/types/internal.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ export interface RequestState {
597597
record_span: RecordSpan;
598598
};
599599
form_instances?: Map<any, any>;
600-
remote_data?: Record<string, MaybePromise<any>>;
600+
remote_data?: Map<RemoteInfo, Record<string, MaybePromise<any>>>;
601601
refreshes?: Record<string, Promise<any>>;
602602
is_endpoint_request?: boolean;
603603
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
import { total } from './data.remote.js';
3+
</script>
4+
5+
<!-- TODO replace with inline await -->
6+
{#await total() then t}
7+
<h1>{t}</h1>
8+
{/await}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { query } from '$app/server';
2+
3+
const one = query(() => 1);
4+
const two = query(() => 2);
5+
6+
export const total = query(async () => {
7+
return (await one()) + (await two());
8+
});

0 commit comments

Comments
 (0)