@sveltejs/kit 2.55.0 → 2.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +3 -4
  2. package/src/core/postbuild/analyse.js +3 -3
  3. package/src/core/postbuild/prerender.js +9 -6
  4. package/src/core/sync/write_tsconfig.js +3 -1
  5. package/src/exports/internal/remote-functions.js +2 -2
  6. package/src/exports/public.d.ts +39 -13
  7. package/src/exports/vite/build/build_server.js +24 -4
  8. package/src/exports/vite/build/build_service_worker.js +16 -6
  9. package/src/exports/vite/build/utils.js +18 -3
  10. package/src/exports/vite/index.js +336 -327
  11. package/src/runtime/app/paths/server.js +1 -1
  12. package/src/runtime/app/server/index.js +1 -1
  13. package/src/runtime/app/server/remote/command.js +12 -7
  14. package/src/runtime/app/server/remote/form.js +14 -14
  15. package/src/runtime/app/server/remote/index.js +1 -0
  16. package/src/runtime/app/server/remote/prerender.js +8 -7
  17. package/src/runtime/app/server/remote/query.js +141 -66
  18. package/src/runtime/app/server/remote/requested.js +172 -0
  19. package/src/runtime/app/server/remote/shared.js +32 -10
  20. package/src/runtime/client/client.js +45 -20
  21. package/src/runtime/client/remote-functions/command.svelte.js +39 -16
  22. package/src/runtime/client/remote-functions/form.svelte.js +41 -24
  23. package/src/runtime/client/remote-functions/prerender.svelte.js +105 -76
  24. package/src/runtime/client/remote-functions/query.svelte.js +408 -138
  25. package/src/runtime/client/remote-functions/shared.svelte.js +95 -94
  26. package/src/runtime/components/svelte-5/error.svelte +2 -0
  27. package/src/runtime/form-utils.js +3 -7
  28. package/src/runtime/server/endpoint.js +0 -1
  29. package/src/runtime/server/page/actions.js +2 -1
  30. package/src/runtime/server/page/load_data.js +3 -1
  31. package/src/runtime/server/page/render.js +38 -15
  32. package/src/runtime/server/remote.js +65 -50
  33. package/src/runtime/server/respond.js +17 -3
  34. package/src/runtime/server/utils.js +0 -12
  35. package/src/runtime/shared.js +233 -5
  36. package/src/types/global-private.d.ts +4 -4
  37. package/src/types/internal.d.ts +80 -44
  38. package/src/utils/css.js +0 -3
  39. package/src/utils/escape.js +15 -3
  40. package/src/version.js +1 -1
  41. package/types/index.d.ts +68 -14
  42. package/types/index.d.ts.map +6 -1
@@ -1,12 +1,18 @@
1
- /** @import { RemoteQueryOverride } from '@sveltejs/kit' */
2
- /** @import { RemoteFunctionResponse } from 'types' */
3
- /** @import { Query } from './query.svelte.js' */
1
+ /** @import { RemoteFunctionResponse, RemoteRefreshMap } from 'types' */
2
+ /** @import { RemoteQueryUpdate } from '@sveltejs/kit' */
4
3
  import * as devalue from 'devalue';
5
- import { app, goto, query_map, remote_responses } from '../client.js';
4
+ import { app, goto, query_map } from '../client.js';
6
5
  import { HttpError, Redirect } from '@sveltejs/kit/internal';
7
- import { tick, untrack } from 'svelte';
8
- import { create_remote_key, stringify_remote_arg } from '../../shared.js';
9
- import { page } from '../state.svelte.js';
6
+ import { untrack } from 'svelte';
7
+ import { create_remote_key, split_remote_key } from '../../shared.js';
8
+ import { navigating, page } from '../state.svelte.js';
9
+
10
+ /** Indicates a query function, as opposed to a query instance */
11
+ export const QUERY_FUNCTION_ID = Symbol('sveltekit.query_function_id');
12
+ /** Indicates a query override callback, used to release the override */
13
+ export const QUERY_OVERRIDE_KEY = Symbol('sveltekit.query_override_key');
14
+ /** Indicates a query instance */
15
+ export const QUERY_RESOURCE_KEY = Symbol('sveltekit.query_resource_key');
10
16
 
11
17
  /**
12
18
  * @returns {{ 'x-sveltekit-pathname': string, 'x-sveltekit-search': string }}
@@ -15,10 +21,14 @@ export function get_remote_request_headers() {
15
21
  // This will be the correct value of the current or soon-current url,
16
22
  // even in forks because it's state-based - therefore not using window.location.
17
23
  // Use untrack(...) to Avoid accidental reactive dependency on pathname/search
18
- return untrack(() => ({
19
- 'x-sveltekit-pathname': page.url.pathname,
20
- 'x-sveltekit-search': page.url.search
21
- }));
24
+ return untrack(() => {
25
+ const url = navigating.current?.to?.url ?? page.url;
26
+
27
+ return {
28
+ 'x-sveltekit-pathname': url.pathname,
29
+ 'x-sveltekit-search': url.search
30
+ };
31
+ });
22
32
  }
23
33
 
24
34
  /**
@@ -52,103 +62,94 @@ export async function remote_request(url, headers) {
52
62
  }
53
63
 
54
64
  /**
55
- * Client-version of the `query`/`prerender`/`cache` function from `$app/server`.
56
- * @param {string} id
57
- * @param {(key: string, args: string) => any} create
65
+ * Given an array of updates, which could be query instances, query functions, or query override release functions,
66
+ * categorize them into overrides (which need to be released after the command completes), refreshes (which
67
+ * just need to be refreshed after the command completes), or both.
68
+ *
69
+ * @param {RemoteQueryUpdate[]} updates
70
+ * @returns {{ overrides: Array<() => void>, refreshes: Set<string> }}
58
71
  */
59
- export function create_remote_function(id, create) {
60
- return (/** @type {any} */ arg) => {
61
- const payload = stringify_remote_arg(arg, app.hooks.transport);
62
- const cache_key = create_remote_key(id, payload);
63
- let entry = query_map.get(cache_key);
64
-
65
- let tracking = true;
66
- try {
67
- $effect.pre(() => {
68
- if (entry) entry.count++;
69
- return () => {
70
- const entry = query_map.get(cache_key);
71
- if (entry) {
72
- entry.count--;
73
- void tick().then(() => {
74
- if (!entry.count && entry === query_map.get(cache_key)) {
75
- query_map.delete(cache_key);
76
- delete remote_responses[cache_key];
77
- }
78
- });
72
+ export function categorize_updates(updates) {
73
+ /** @type {Set<string>} */
74
+ const override_keys = new Set();
75
+ /** @type {Array<() => void>} */
76
+ const overrides = [];
77
+ /** @type {Set<string>} */
78
+ const refreshes = new Set();
79
+
80
+ for (const update of updates) {
81
+ if (typeof update === 'function') {
82
+ if (Object.hasOwn(update, QUERY_FUNCTION_ID)) {
83
+ // this is a query function (not instance), so we need to find all active instances
84
+ // of this functionand request that they be refreshed by the command handler
85
+ // @ts-expect-error
86
+ const id = /** @type {string} */ (update[QUERY_FUNCTION_ID]);
87
+ const entries = query_map.get(id);
88
+
89
+ if (entries) {
90
+ for (const payload of entries.keys()) {
91
+ refreshes.add(create_remote_key(id, payload));
79
92
  }
80
- };
81
- });
82
- } catch {
83
- tracking = false;
93
+ }
94
+
95
+ continue;
96
+ }
97
+
98
+ if (Object.hasOwn(update, QUERY_OVERRIDE_KEY)) {
99
+ // this is a query override release function, so we need to both request that the query instance
100
+ // be refreshed _and_ stash the release function so we can release the override after the command completes
101
+ // @ts-expect-error
102
+ const key = /** @type {string} */ (update[QUERY_OVERRIDE_KEY]);
103
+ refreshes.add(key);
104
+
105
+ if (override_keys.has(key)) {
106
+ throw new Error(
107
+ 'Multiple overrides for the same query are not allowed in a single updates() invocation'
108
+ );
109
+ }
110
+
111
+ override_keys.add(key);
112
+ overrides.push(/** @type {() => void} */ (update));
113
+ continue;
114
+ }
84
115
  }
85
116
 
86
- let resource = entry?.resource;
87
- if (!resource) {
88
- resource = create(cache_key, payload);
89
-
90
- Object.defineProperty(resource, '_key', {
91
- value: cache_key
92
- });
93
-
94
- query_map.set(
95
- cache_key,
96
- (entry = {
97
- count: tracking ? 1 : 0,
98
- resource
99
- })
100
- );
101
-
102
- resource
103
- .then(() => {
104
- void tick().then(() => {
105
- if (
106
- !(/** @type {NonNullable<typeof entry>} */ (entry).count) &&
107
- entry === query_map.get(cache_key)
108
- ) {
109
- // If no one is tracking this resource anymore, we can delete it from the cache
110
- query_map.delete(cache_key);
111
- }
112
- });
113
- })
114
- .catch(() => {
115
- // error delete the resource from the cache
116
- // TODO is that correct?
117
- query_map.delete(cache_key);
118
- });
117
+ if (
118
+ typeof update === 'object' &&
119
+ update !== null &&
120
+ Object.hasOwn(update, QUERY_RESOURCE_KEY)
121
+ ) {
122
+ // this is a query instance, so we just need to request that it be refreshed
123
+ // @ts-expect-error
124
+ refreshes.add(/** @type {string} */ (update[QUERY_RESOURCE_KEY]));
125
+ continue;
119
126
  }
120
127
 
121
- return resource;
122
- };
123
- }
124
-
125
- /**
126
- * @param {Array<Query<any> | RemoteQueryOverride>} updates
127
- */
128
- export function release_overrides(updates) {
129
- for (const update of updates) {
130
- if ('release' in update) {
131
- update.release();
132
- }
128
+ throw new Error('updates() expects a query function, query resource, or query override');
133
129
  }
130
+
131
+ return { overrides, refreshes };
134
132
  }
135
133
 
136
134
  /**
135
+ * Apply refresh data from the server to the relevant queries
136
+ *
137
137
  * @param {string} stringified_refreshes
138
- * @param {Array<Query<any> | RemoteQueryOverride>} updates
139
138
  */
140
- export function refresh_queries(stringified_refreshes, updates = []) {
141
- const refreshes = Object.entries(devalue.parse(stringified_refreshes, app.decoders));
139
+ export function apply_refreshes(stringified_refreshes) {
140
+ const refreshes = Object.entries(
141
+ /** @type {RemoteRefreshMap} */ (devalue.parse(stringified_refreshes, app.decoders))
142
+ );
142
143
 
143
- // `refreshes` is a superset of `updates`
144
144
  for (const [key, value] of refreshes) {
145
- // If there was an optimistic update, release it right before we update the query
146
- const update = updates.find((u) => u._key === key);
147
- if (update && 'release' in update) {
148
- update.release();
145
+ const parts = split_remote_key(key);
146
+
147
+ const entry = query_map.get(parts.id)?.get(parts.payload);
148
+
149
+ if (value.type === 'result') {
150
+ entry?.resource.set(value.data);
151
+ } else {
152
+ entry?.resource.fail(new HttpError(value.status ?? 500, value.error));
149
153
  }
150
- // Update the query with the new value
151
- const entry = query_map.get(key);
152
- entry?.resource.set(value);
153
154
  }
154
155
  }
@@ -1,3 +1,5 @@
1
+ <svelte:options runes={true} />
2
+
1
3
  <script>
2
4
  import { page } from '$app/state';
3
5
  </script>
@@ -84,10 +84,6 @@ export function serialize_binary_form(data, meta) {
84
84
  /** @type {Array<[file: File, index: number]>} */
85
85
  const files = [];
86
86
 
87
- if (!meta.remote_refreshes?.length) {
88
- delete meta.remote_refreshes;
89
- }
90
-
91
87
  const encoded_header = devalue.stringify([data, meta], {
92
88
  File: (file) => {
93
89
  if (!(file instanceof File)) return;
@@ -324,7 +320,7 @@ export async function deserialize_binary_form(request) {
324
320
  }
325
321
  }
326
322
 
327
- // Read the request body asyncronously so it doesn't stall
323
+ // Read the request body asynchronously so it doesn't stall
328
324
  void (async () => {
329
325
  let has_more = true;
330
326
  while (has_more) {
@@ -718,7 +714,7 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
718
714
  value: {
719
715
  enumerable: true,
720
716
  get() {
721
- return get_value();
717
+ return input_value !== undefined ? input_value : get_value();
722
718
  }
723
719
  }
724
720
  });
@@ -804,7 +800,7 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
804
800
  value: {
805
801
  enumerable: true,
806
802
  get() {
807
- const value = get_value();
803
+ const value = input_value !== undefined ? input_value : get_value();
808
804
  return value != null ? String(value) : '';
809
805
  }
810
806
  }
@@ -42,7 +42,6 @@ export async function render_endpoint(event, event_state, mod, state) {
42
42
  }
43
43
 
44
44
  try {
45
- event_state.allows_commands = true;
46
45
  const response = await with_request_store({ event, state: event_state }, () =>
47
46
  handler(/** @type {import('@sveltejs/kit').RequestEvent<Record<string, any>>} */ (event))
48
47
  );
@@ -266,10 +266,11 @@ async function call_action(event, event_state, actions) {
266
266
  },
267
267
  fn: async (current) => {
268
268
  const traced_event = merge_tracing(event, current);
269
- event_state.allows_commands = true;
269
+
270
270
  const result = await with_request_store({ event: traced_event, state: event_state }, () =>
271
271
  action(traced_event)
272
272
  );
273
+
273
274
  if (result instanceof ActionFailure) {
274
275
  current.setAttributes({
275
276
  'sveltekit.form_action.result.type': 'failure',
@@ -233,7 +233,9 @@ export async function load_data({
233
233
  },
234
234
  fn: async (current) => {
235
235
  const traced_event = merge_tracing(event, current);
236
- return await with_request_store({ event: traced_event, state: event_state }, () =>
236
+ const child_state = { ...event_state, is_in_universal_load: true };
237
+
238
+ return await with_request_store({ event: traced_event, state: child_state }, () =>
237
239
  load.call(null, {
238
240
  url: event.url,
239
241
  params: event.params,
@@ -1,7 +1,7 @@
1
1
  import * as devalue from 'devalue';
2
2
  import { readable, writable } from 'svelte/store';
3
3
  import { DEV } from 'esm-env';
4
- import { text } from '@sveltejs/kit';
4
+ import { isRedirect, text } from '@sveltejs/kit';
5
5
  import * as paths from '$app/paths/internal/server';
6
6
  import { hash } from '../../../utils/hash.js';
7
7
  import { serialize_data } from './serialize_data.js';
@@ -189,6 +189,10 @@ export async function render_response({
189
189
  csp: csp.script_needs_nonce ? { nonce: csp.nonce } : { hash: csp.script_needs_hash },
190
190
  transformError: error_components
191
191
  ? /** @param {unknown} e */ async (e) => {
192
+ if (isRedirect(e)) {
193
+ throw e;
194
+ }
195
+
192
196
  const transformed = await handle_error_and_jsonify(event, event_state, options, e);
193
197
  props.page.error = props.error = error = transformed;
194
198
  props.page.status = status = get_status(e);
@@ -218,8 +222,9 @@ export async function render_response({
218
222
  };
219
223
  }
220
224
 
221
- event_state.allows_commands = false;
222
- rendered = await with_request_store({ event, state: event_state }, async () => {
225
+ const state = { ...event_state, is_in_render: true };
226
+
227
+ rendered = await with_request_store({ event, state }, async () => {
223
228
  // use relative paths during rendering, so that the resulting HTML is as
224
229
  // portable as possible, but reset afterwards
225
230
  if (paths.relative) paths.override({ base, assets });
@@ -498,31 +503,41 @@ export async function render_response({
498
503
  args.push(`{\n${indent}\t${hydrate.join(`,\n${indent}\t`)}\n${indent}}`);
499
504
  }
500
505
 
501
- const { remote_data: remote_cache } = event_state;
506
+ const { remote } = event_state;
502
507
 
503
- let serialized_remote_data = '';
508
+ let serialized_query_data = '';
509
+ let serialized_prerender_data = '';
504
510
 
505
- if (remote_cache) {
511
+ if (remote.data) {
506
512
  /** @type {Record<string, any>} */
507
- const remote = {};
513
+ const query = {};
508
514
 
509
- for (const [info, cache] of remote_cache) {
515
+ /** @type {Record<string, any>} */
516
+ const prerender = {};
517
+
518
+ for (const [internals, cache] of remote.data) {
510
519
  // remote functions without an `id` aren't exported, and thus
511
520
  // cannot be called from the client
512
- if (!info.id) continue;
521
+ if (!internals.id) continue;
513
522
 
514
523
  for (const key in cache) {
515
- const remote_key = create_remote_key(info.id, key);
524
+ const entry = cache[key];
525
+
526
+ if (!entry.serialize) continue;
516
527
 
517
- if (event_state.refreshes?.[remote_key] !== undefined) {
528
+ const remote_key = create_remote_key(internals.id, key);
529
+
530
+ const store = internals.type === 'prerender' ? prerender : query;
531
+
532
+ if (event_state.remote.refreshes?.[remote_key] !== undefined) {
518
533
  // This entry was refreshed/set by a command or form action.
519
534
  // Always await it so the mutation result is serialized.
520
- remote[remote_key] = await cache[key];
535
+ store[remote_key] = await entry.data;
521
536
  } else {
522
537
  // Don't block the response on pending remote data - if a query
523
538
  // hasn't settled yet, it wasn't awaited in the template (or is behind a pending boundary).
524
539
  const result = await Promise.race([
525
- Promise.resolve(cache[key]).then(
540
+ Promise.resolve(entry.data).then(
526
541
  (v) => /** @type {const} */ ({ settled: true, value: v }),
527
542
  (e) => /** @type {const} */ ({ settled: true, error: e })
528
543
  ),
@@ -533,7 +548,7 @@ export async function render_response({
533
548
 
534
549
  if (result.settled) {
535
550
  if ('error' in result) throw result.error;
536
- remote[remote_key] = result.value;
551
+ store[remote_key] = result.value;
537
552
  }
538
553
  }
539
554
  }
@@ -549,9 +564,17 @@ export async function render_response({
549
564
  }
550
565
  };
551
566
 
552
- serialized_remote_data = `${global}.data = ${devalue.uneval(remote, replacer)};\n\n\t\t\t\t\t\t`;
567
+ if (Object.keys(query).length > 0) {
568
+ serialized_query_data = `${global}.query = ${devalue.uneval(query, replacer)};\n\n\t\t\t\t\t\t`;
569
+ }
570
+
571
+ if (Object.keys(prerender).length > 0) {
572
+ serialized_prerender_data = `${global}.prerender = ${devalue.uneval(prerender, replacer)};\n\n\t\t\t\t\t\t`;
573
+ }
553
574
  }
554
575
 
576
+ const serialized_remote_data = `${serialized_query_data}${serialized_prerender_data}`;
577
+
555
578
  // `client.app` is a proxy for `bundleStrategy === 'split'`
556
579
  const boot = client.inline
557
580
  ? `${client.inline.script}
@@ -1,12 +1,12 @@
1
1
  /** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest } from '@sveltejs/kit' */
2
- /** @import { RemoteFunctionResponse, RemoteInfo, RequestState, SSROptions } from 'types' */
2
+ /** @import { RemoteFormInternals, RemoteFunctionResponse, RemoteInternals, RequestState, SSROptions } from 'types' */
3
3
 
4
4
  import { json, error } from '@sveltejs/kit';
5
5
  import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
6
6
  import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server';
7
7
  import { app_dir, base } from '$app/paths/internal/server';
8
8
  import { is_form_content_type } from '../../utils/http.js';
9
- import { parse_remote_arg, stringify } from '../shared.js';
9
+ import { parse_remote_arg, split_remote_key, stringify } from '../shared.js';
10
10
  import { handle_error_and_jsonify } from './utils.js';
11
11
  import { normalize_error } from '../../utils/error.js';
12
12
  import { check_incorrect_fail_use } from './page/actions.js';
@@ -48,20 +48,17 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
48
48
 
49
49
  if (!fn) error(404);
50
50
 
51
- /** @type {RemoteInfo} */
52
- const info = fn.__;
51
+ /** @type {RemoteInternals} */
52
+ const internals = fn.__;
53
53
  const transport = options.hooks.transport;
54
54
 
55
55
  event.tracing.current.setAttributes({
56
- 'sveltekit.remote.call.type': info.type,
57
- 'sveltekit.remote.call.name': info.name
56
+ 'sveltekit.remote.call.type': internals.type,
57
+ 'sveltekit.remote.call.name': internals.name
58
58
  });
59
59
 
60
- /** @type {string[] | undefined} */
61
- let form_client_refreshes;
62
-
63
60
  try {
64
- if (info.type === 'query_batch') {
61
+ if (internals.type === 'query_batch') {
65
62
  if (event.request.method !== 'POST') {
66
63
  throw new SvelteKitError(
67
64
  405,
@@ -77,7 +74,9 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
77
74
  payloads.map((payload) => parse_remote_arg(payload, transport))
78
75
  );
79
76
 
80
- const results = await with_request_store({ event, state }, () => info.run(args, options));
77
+ const results = await with_request_store({ event, state }, () =>
78
+ internals.run(args, options)
79
+ );
81
80
 
82
81
  return json(
83
82
  /** @type {RemoteFunctionResponse} */ ({
@@ -87,7 +86,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
87
86
  );
88
87
  }
89
88
 
90
- if (info.type === 'form') {
89
+ if (internals.type === 'form') {
91
90
  if (event.request.method !== 'POST') {
92
91
  throw new SvelteKitError(
93
92
  405,
@@ -107,8 +106,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
107
106
  }
108
107
 
109
108
  const { data, meta, form_data } = await deserialize_binary_form(event.request);
110
-
111
- form_client_refreshes = meta.remote_refreshes;
109
+ state.remote.requested = create_requested_map(meta.remote_refreshes);
112
110
 
113
111
  // If this is a keyed form instance (created via form.for(key)), add the key to the form data (unless already set)
114
112
  // Note that additional_args will only be set if the form is not enhanced, as enhanced forms transfer the key inside `data`.
@@ -116,21 +114,22 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
116
114
  data.id = JSON.parse(decodeURIComponent(additional_args));
117
115
  }
118
116
 
119
- const fn = info.fn;
117
+ const fn = internals.fn;
120
118
  const result = await with_request_store({ event, state }, () => fn(data, meta, form_data));
121
119
 
122
120
  return json(
123
121
  /** @type {RemoteFunctionResponse} */ ({
124
122
  type: 'result',
125
123
  result: stringify(result, transport),
126
- refreshes: result.issues ? undefined : await serialize_refreshes(meta.remote_refreshes)
124
+ refreshes: result.issues ? undefined : await serialize_refreshes()
127
125
  })
128
126
  );
129
127
  }
130
128
 
131
- if (info.type === 'command') {
132
- /** @type {{ payload: string, refreshes: string[] }} */
129
+ if (internals.type === 'command') {
130
+ /** @type {{ payload: string, refreshes?: string[] }} */
133
131
  const { payload, refreshes } = await event.request.json();
132
+ state.remote.requested = create_requested_map(refreshes);
134
133
  const arg = parse_remote_arg(payload, transport);
135
134
  const data = await with_request_store({ event, state }, () => fn(arg));
136
135
 
@@ -138,13 +137,13 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
138
137
  /** @type {RemoteFunctionResponse} */ ({
139
138
  type: 'result',
140
139
  result: stringify(data, transport),
141
- refreshes: await serialize_refreshes(refreshes)
140
+ refreshes: await serialize_refreshes()
142
141
  })
143
142
  );
144
143
  }
145
144
 
146
145
  const payload =
147
- info.type === 'prerender'
146
+ internals.type === 'prerender'
148
147
  ? additional_args
149
148
  : /** @type {string} */ (
150
149
  // new URL(...) necessary because we're hiding the URL from the user in the event object
@@ -167,7 +166,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
167
166
  /** @type {RemoteFunctionResponse} */ ({
168
167
  type: 'redirect',
169
168
  location: error.location,
170
- refreshes: await serialize_refreshes(form_client_refreshes)
169
+ refreshes: await serialize_refreshes()
171
170
  })
172
171
  );
173
172
  }
@@ -192,42 +191,58 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
192
191
  );
193
192
  }
194
193
 
195
- /**
196
- * @param {string[]=} client_refreshes
197
- */
198
- async function serialize_refreshes(client_refreshes) {
199
- const refreshes = state.refreshes ?? {};
194
+ async function serialize_refreshes() {
195
+ const refreshes = state.remote.refreshes ?? {};
196
+
197
+ const entries = Object.entries(refreshes);
198
+ if (entries.length === 0) {
199
+ return undefined;
200
+ }
200
201
 
201
- if (client_refreshes) {
202
- for (const key of client_refreshes) {
203
- if (refreshes[key] !== undefined) continue;
202
+ const results = await Promise.all(
203
+ entries.map(async ([key, promise]) => {
204
+ try {
205
+ return [key, { type: 'result', data: await promise }];
206
+ } catch (error) {
207
+ const status =
208
+ error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500;
209
+
210
+ return [
211
+ key,
212
+ {
213
+ type: 'error',
214
+ status,
215
+ error: await handle_error_and_jsonify(event, state, options, error)
216
+ }
217
+ ];
218
+ }
219
+ })
220
+ );
204
221
 
205
- const [hash, name, payload] = key.split('/');
222
+ return stringify(Object.fromEntries(results), transport);
223
+ }
224
+ }
206
225
 
207
- const loader = manifest._.remotes[hash];
208
- const fn = (await loader?.())?.default?.[name];
226
+ /**
227
+ * @param {string[] | undefined} refreshes
228
+ */
229
+ function create_requested_map(refreshes) {
230
+ /** @type {Map<string, string[]>} */
231
+ const requested = new Map();
209
232
 
210
- if (!fn) error(400, 'Bad Request');
233
+ for (const key of refreshes ?? []) {
234
+ const parts = split_remote_key(key);
211
235
 
212
- refreshes[key] = with_request_store({ event, state }, () =>
213
- fn(parse_remote_arg(payload, transport))
214
- );
215
- }
216
- }
236
+ const existing = requested.get(parts.id);
217
237
 
218
- if (Object.keys(refreshes).length === 0) {
219
- return undefined;
238
+ if (existing) {
239
+ existing.push(parts.payload);
240
+ } else {
241
+ requested.set(parts.id, [parts.payload]);
220
242
  }
221
-
222
- return stringify(
223
- Object.fromEntries(
224
- await Promise.all(
225
- Object.entries(refreshes).map(async ([key, promise]) => [key, await promise])
226
- )
227
- ),
228
- transport
229
- );
230
243
  }
244
+
245
+ return requested;
231
246
  }
232
247
 
233
248
  /** @type {typeof handle_remote_form_post_internal} */
@@ -282,7 +297,7 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
282
297
  }
283
298
 
284
299
  try {
285
- const fn = /** @type {RemoteInfo & { type: 'form' }} */ (/** @type {any} */ (form).__).fn;
300
+ const fn = /** @type {RemoteFormInternals} */ (/** @type {any} */ (form).__).fn;
286
301
 
287
302
  const { data, meta, form_data } = await deserialize_binary_form(event.request);
288
303
 
@@ -39,7 +39,6 @@ import { server_data_serializer } from './page/data_serializer.js';
39
39
  import { get_remote_id, handle_remote_call } from './remote.js';
40
40
  import { record_span } from '../telemetry/record_span.js';
41
41
  import { otel } from '../telemetry/otel.js';
42
- import { MUTATIVE_METHODS } from '../../constants.js';
43
42
 
44
43
  /* global __SVELTEKIT_ADAPTER_NAME__ */
45
44
 
@@ -150,7 +149,22 @@ export async function internal_respond(request, options, manifest, state) {
150
149
  tracing: {
151
150
  record_span
152
151
  },
153
- is_in_remote_function: false
152
+ remote: {
153
+ data: null,
154
+ forms: null,
155
+ /** A map of remote function key to corresponding single-flight-mutation promise */
156
+ refreshes: null,
157
+ /** A map of remote function ID to payloads requested for refreshing by the client */
158
+ requested: null,
159
+ /**
160
+ * A map of remote function ID to objects that have passed validation;
161
+ * used to prevent revalidating parameters returned from `requested`
162
+ */
163
+ validated: null
164
+ },
165
+ is_in_remote_function: false,
166
+ is_in_render: false,
167
+ is_in_universal_load: false
154
168
  };
155
169
 
156
170
  /** @type {import('@sveltejs/kit').RequestEvent} */
@@ -420,7 +434,7 @@ export async function internal_respond(request, options, manifest, state) {
420
434
  current: root_span
421
435
  }
422
436
  };
423
- event_state.allows_commands = MUTATIVE_METHODS.includes(request.method);
437
+
424
438
  return await with_request_store({ event: traced_event, state: event_state }, () =>
425
439
  options.hooks.handle({
426
440
  event: traced_event,