@sveltejs/kit 2.55.0 → 2.56.0

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 +38 -12
  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 +67 -13
  42. package/types/index.d.ts.map +6 -1
@@ -0,0 +1,172 @@
1
+ /** @import { RemoteQueryFunction, RequestedResult } from '@sveltejs/kit' */
2
+ /** @import { MaybePromise, RemoteQueryInternals } from 'types' */
3
+ import { get_request_store } from '@sveltejs/kit/internal/server';
4
+ import { create_remote_key, parse_remote_arg } from '../../../shared.js';
5
+ import { mark_argument_validated } from './query.js';
6
+
7
+ /**
8
+ * In the context of a remote `command` or `form` request, returns an iterable
9
+ * of the client-requested refreshes' validated arguments up to the supplied limit.
10
+ * Arguments that fail validation or exceed the limit are recorded as failures in
11
+ * the response to the client.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { requested } from '$app/server';
16
+ *
17
+ * for (const arg of requested(getPost, 5)) {
18
+ * // it's safe to throw away this promise -- SvelteKit
19
+ * // will await it for us and handle any errors by sending
20
+ * // them to the client.
21
+ * void getPost(arg).refresh();
22
+ * }
23
+ * ```
24
+ *
25
+ * As a shorthand for the above, you can also call `refreshAll` on the result:
26
+ *
27
+ * ```ts
28
+ * import { requested } from '$app/server';
29
+ *
30
+ * await requested(getPost, 5).refreshAll();
31
+ * ```
32
+ *
33
+ * @template Input
34
+ * @template Output
35
+ * @param {RemoteQueryFunction<Input, Output>} query
36
+ * @param {number} [limit=Infinity]
37
+ * @returns {RequestedResult<Input>}
38
+ */
39
+ export function requested(query, limit = Infinity) {
40
+ const { state } = get_request_store();
41
+ const internals = /** @type {RemoteQueryInternals | undefined} */ (/** @type {any} */ (query).__);
42
+
43
+ if (!internals || internals.type !== 'query') {
44
+ throw new Error('requested(...) expects a query function created with query(...)');
45
+ }
46
+
47
+ const requested = state.remote.requested;
48
+ const payloads = requested?.get(internals.id) ?? [];
49
+ const refreshes = (state.remote.refreshes ??= {});
50
+ const [selected, skipped] = split_limit(payloads, limit);
51
+
52
+ /**
53
+ * @param {string} payload
54
+ * @param {unknown} error
55
+ */
56
+ const record_failure = (payload, error) => {
57
+ const promise = Promise.reject(error);
58
+ promise.catch(() => {});
59
+
60
+ const key = create_remote_key(internals.id, payload);
61
+ refreshes[key] = promise;
62
+ };
63
+
64
+ for (const payload of skipped) {
65
+ record_failure(
66
+ payload,
67
+ new Error(
68
+ `Requested refresh was rejected because it exceeded requested(${internals.name}, ${limit}) limit`
69
+ )
70
+ );
71
+ }
72
+
73
+ return {
74
+ *[Symbol.iterator]() {
75
+ for (const payload of selected) {
76
+ try {
77
+ const parsed = parse_remote_arg(payload, state.transport);
78
+ const validated = internals.validate(parsed);
79
+
80
+ if (is_thenable(validated)) {
81
+ throw new Error(
82
+ // TODO improve
83
+ `requested(${internals.name}, ${limit}) cannot be used with synchronous iteration because the query validator is async. Use \`for await ... of\` instead`
84
+ );
85
+ }
86
+
87
+ yield mark_argument_validated(internals, state, validated);
88
+ } catch (error) {
89
+ record_failure(payload, error);
90
+ continue;
91
+ }
92
+ }
93
+ },
94
+ async *[Symbol.asyncIterator]() {
95
+ yield* race_all(selected, async (payload) => {
96
+ try {
97
+ const parsed = parse_remote_arg(payload, state.transport);
98
+ const validated = await internals.validate(parsed);
99
+ return mark_argument_validated(internals, state, validated);
100
+ } catch (error) {
101
+ record_failure(payload, error);
102
+ throw new Error(`Skipping ${internals.name}(${payload})`, { cause: error });
103
+ }
104
+ });
105
+ },
106
+ async refreshAll() {
107
+ for await (const arg of this) {
108
+ void query(arg).refresh();
109
+ }
110
+ }
111
+ };
112
+ }
113
+
114
+ /**
115
+ * @template T
116
+ * @param {Array<T>} array
117
+ * @param {number} limit
118
+ * @returns {[Array<T>, Array<T>]}
119
+ */
120
+ function split_limit(array, limit) {
121
+ if (limit === Infinity) {
122
+ return [array, []];
123
+ }
124
+ if (!Number.isInteger(limit) || limit < 0) {
125
+ throw new Error('Limit must be a non-negative integer or Infinity');
126
+ }
127
+ return [array.slice(0, limit), array.slice(limit)];
128
+ }
129
+
130
+ /**
131
+ * @param {any} value
132
+ * @returns {value is PromiseLike<any>}
133
+ */
134
+ function is_thenable(value) {
135
+ return !!value && (typeof value === 'object' || typeof value === 'function') && 'then' in value;
136
+ }
137
+
138
+ /**
139
+ * Runs all callbacks immediately and yields resolved values in completion order.
140
+ * If the promise rejects, it is skipped.
141
+ *
142
+ * @template T
143
+ * @template R
144
+ * @param {Array<T>} array
145
+ * @param {(value: T) => MaybePromise<R>} fn
146
+ * @returns {AsyncIterable<R>}
147
+ */
148
+ async function* race_all(array, fn) {
149
+ /** @type {Set<Promise<{ promise: Promise<any>, value: Awaited<R> }>>} */
150
+ const pending = new Set();
151
+
152
+ for (const value of array) {
153
+ /** @type {Promise<{ promise: Promise<any>, value: Awaited<R> }>} */
154
+ const promise = Promise.resolve(fn(value)).then((result) => ({
155
+ promise,
156
+ value: result
157
+ }));
158
+
159
+ promise.catch(() => {});
160
+ pending.add(promise);
161
+ }
162
+
163
+ while (pending.size > 0) {
164
+ try {
165
+ const { promise, value } = await Promise.race(pending);
166
+ pending.delete(promise);
167
+ yield value;
168
+ } catch {
169
+ // Ignore errors, they are handled in the fn callback and result in skip
170
+ }
171
+ }
172
+ }
@@ -1,9 +1,14 @@
1
1
  /** @import { RequestEvent } from '@sveltejs/kit' */
2
- /** @import { ServerHooks, MaybePromise, RequestState, RemoteInfo, RequestStore } from 'types' */
2
+ /** @import { ServerHooks, MaybePromise, RequestState, RemoteInternals, RequestStore } from 'types' */
3
3
  import { parse } from 'devalue';
4
4
  import { error } from '@sveltejs/kit';
5
5
  import { with_request_store, get_request_store } from '@sveltejs/kit/internal/server';
6
- import { stringify_remote_arg } from '../../../shared.js';
6
+ import {
7
+ stringify_remote_arg,
8
+ create_remote_key,
9
+ stringify,
10
+ unfriendly_hydratable
11
+ } from '../../../shared.js';
7
12
 
8
13
  /**
9
14
  * @param {any} validate_or_fn
@@ -61,20 +66,37 @@ export function create_validator(validate_or_fn, maybe_fn) {
61
66
  * Also saves an uneval'ed version of the result for later HTML inlining for hydration.
62
67
  *
63
68
  * @template {MaybePromise<any>} T
64
- * @param {RemoteInfo} info
69
+ * @param {RemoteInternals} internals
65
70
  * @param {any} arg
66
71
  * @param {RequestState} state
67
72
  * @param {() => Promise<T>} get_result
68
73
  * @returns {Promise<T>}
69
74
  */
70
- export async function get_response(info, arg, state, get_result) {
75
+ export async function get_response(internals, arg, state, get_result) {
71
76
  // wait a beat, in case `myQuery().set(...)` or `myQuery().refresh()` is immediately called
72
77
  // eslint-disable-next-line @typescript-eslint/await-thenable
73
78
  await 0;
74
79
 
75
- const cache = get_cache(info, state);
80
+ const cache = get_cache(internals, state);
81
+ const key = stringify_remote_arg(arg, state.transport);
82
+ const entry = (cache[key] ??= {
83
+ serialize: false,
84
+ data: get_result()
85
+ });
76
86
 
77
- return (cache[stringify_remote_arg(arg, state.transport)] ??= get_result());
87
+ entry.serialize ||= !!state.is_in_universal_load;
88
+
89
+ if (state.is_in_render && internals.id) {
90
+ const remote_key = create_remote_key(internals.id, key);
91
+
92
+ Promise.resolve(entry.data)
93
+ .then((value) => {
94
+ void unfriendly_hydratable(remote_key, () => stringify(value, state.transport));
95
+ })
96
+ .catch(() => {});
97
+ }
98
+
99
+ return entry.data;
78
100
  }
79
101
 
80
102
  /**
@@ -146,15 +168,15 @@ export async function run_remote_function(event, state, allow_cookies, get_input
146
168
  }
147
169
 
148
170
  /**
149
- * @param {RemoteInfo} info
171
+ * @param {RemoteInternals} internals
150
172
  * @param {RequestState} state
151
173
  */
152
- export function get_cache(info, state = get_request_store().state) {
153
- let cache = state.remote_data?.get(info);
174
+ export function get_cache(internals, state = get_request_store().state) {
175
+ let cache = state.remote.data?.get(internals);
154
176
 
155
177
  if (cache === undefined) {
156
178
  cache = {};
157
- (state.remote_data ??= new Map()).set(info, cache);
179
+ (state.remote.data ??= new Map()).set(internals, cache);
158
180
  }
159
181
 
160
182
  return cache;
@@ -1,3 +1,4 @@
1
+ /** @import { RemoteQueryCacheEntry } from './remote-functions/query.svelte.js' */
1
2
  import { BROWSER, DEV } from 'esm-env';
2
3
  import * as svelte from 'svelte';
3
4
  import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
@@ -198,10 +199,18 @@ let target;
198
199
  export let app;
199
200
 
200
201
  /**
201
- * Data that was serialized during SSR. This is cleared when the user first navigates
202
+ * Data that was serialized during SSR for queries/forms/commands.
203
+ * This is cleared before client-side loads run.
202
204
  * @type {Record<string, any>}
203
205
  */
204
- export let remote_responses = {};
206
+ export let query_responses = {};
207
+
208
+ /**
209
+ * Data that was serialized during SSR for prerender functions.
210
+ * This persists across client-side navigations.
211
+ * @type {Record<string, any>}
212
+ */
213
+ export let prerender_responses = {};
205
214
 
206
215
  /** @type {Array<((url: URL) => boolean)>} */
207
216
  const invalidated = [];
@@ -292,8 +301,8 @@ const preload_tokens = new Set();
292
301
  export let pending_invalidate;
293
302
 
294
303
  /**
295
- * @type {Map<string, {count: number, resource: any}>}
296
- * A map of id -> query info with all queries that currently exist in the app.
304
+ * @type {Map<string, Map<string, RemoteQueryCacheEntry<any>>>}
305
+ * A map of query id -> payload -> query internals for all active queries.
297
306
  */
298
307
  export const query_map = new Map();
299
308
 
@@ -309,8 +318,9 @@ export async function start(_app, _target, hydrate) {
309
318
  );
310
319
  }
311
320
 
312
- if (__SVELTEKIT_PAYLOAD__?.data) {
313
- remote_responses = __SVELTEKIT_PAYLOAD__.data;
321
+ if (__SVELTEKIT_PAYLOAD__) {
322
+ query_responses = __SVELTEKIT_PAYLOAD__.query ?? {};
323
+ prerender_responses = __SVELTEKIT_PAYLOAD__.prerender ?? {};
314
324
  }
315
325
 
316
326
  // detect basic auth credentials in the current URL
@@ -401,8 +411,10 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
401
411
 
402
412
  // Rerun queries
403
413
  if (force_invalidation) {
404
- query_map.forEach(({ resource }) => {
405
- resource.refresh?.();
414
+ query_map.forEach((entries) => {
415
+ entries.forEach(({ resource }) => {
416
+ void resource.refresh?.();
417
+ });
406
418
  });
407
419
  }
408
420
 
@@ -433,7 +445,11 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
433
445
  }
434
446
 
435
447
  // Don't use allSettled yet because it's too new
436
- await Promise.all([...query_map.values()].map(({ resource }) => resource)).catch(noop);
448
+ await Promise.all(
449
+ [...query_map.values()].flatMap((entries) =>
450
+ [...entries.values()].map(({ resource }) => resource)
451
+ )
452
+ ).catch(noop);
437
453
  }
438
454
 
439
455
  function reset_invalidation() {
@@ -491,7 +507,12 @@ export async function _goto(url, options, redirect_count, nav_token) {
491
507
  accept: () => {
492
508
  if (options.invalidateAll) {
493
509
  force_invalidation = true;
494
- query_keys = [...query_map.keys()];
510
+ query_keys = [];
511
+ query_map.forEach((entries, id) => {
512
+ for (const payload of entries.keys()) {
513
+ query_keys.push(id + '/' + payload);
514
+ }
515
+ });
495
516
  }
496
517
 
497
518
  if (options.invalidate) {
@@ -507,11 +528,12 @@ export async function _goto(url, options, redirect_count, nav_token) {
507
528
  .tick()
508
529
  .then(svelte.tick)
509
530
  .then(() => {
510
- query_map.forEach(({ resource }, key) => {
511
- // Only refresh those that already existed on the old page
512
- if (query_keys?.includes(key)) {
513
- resource.refresh?.();
514
- }
531
+ query_map.forEach((entries, id) => {
532
+ entries.forEach(({ resource }, payload) => {
533
+ if (query_keys?.includes(id + '/' + payload)) {
534
+ void resource.refresh?.();
535
+ }
536
+ });
515
537
  });
516
538
  });
517
539
  }
@@ -1602,8 +1624,6 @@ async function navigate({
1602
1624
  block = noop,
1603
1625
  event
1604
1626
  }) {
1605
- remote_responses = {};
1606
-
1607
1627
  const prev_token = token;
1608
1628
  token = nav_token;
1609
1629
 
@@ -1767,6 +1787,10 @@ async function navigate({
1767
1787
  // also compare ids to avoid using wrong fork (e.g. a new one could've been added while navigating)
1768
1788
  const load_cache_fork = intent && load_cache?.id === intent.id ? load_cache.fork : null;
1769
1789
  // reset preload synchronously after the history state has been set to avoid race conditions
1790
+ if (load_cache?.fork && !load_cache_fork) {
1791
+ // discard fork of different route
1792
+ discard_load_cache();
1793
+ }
1770
1794
  load_cache = null;
1771
1795
 
1772
1796
  navigation_result.props.page.state = state;
@@ -2262,8 +2286,6 @@ export function refreshAll({ includeLoadFunctions = true } = {}) {
2262
2286
  throw new Error('Cannot call refreshAll() on the server');
2263
2287
  }
2264
2288
 
2265
- remote_responses = {};
2266
-
2267
2289
  force_invalidation = true;
2268
2290
  return _invalidate(includeLoadFunctions, false);
2269
2291
  }
@@ -2942,6 +2964,8 @@ async function _hydrate(
2942
2964
 
2943
2965
  target.textContent = '';
2944
2966
  hydrate = false;
2967
+ } finally {
2968
+ query_responses = {};
2945
2969
  }
2946
2970
 
2947
2971
  if (result.props.page) {
@@ -3130,7 +3154,8 @@ function reset_focus(url, scroll = true) {
3130
3154
  const tabindex = root.getAttribute('tabindex');
3131
3155
 
3132
3156
  root.tabIndex = -1;
3133
- // @ts-expect-error options.focusVisible is only supported in Firefox
3157
+ // TODO: remove this when we switch to TypeScript 6
3158
+ // @ts-ignore options.focusVisible is only typed in TypeScript 6
3134
3159
  // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#browser_compatibility
3135
3160
  root.focus({ preventScroll: true, focusVisible: false });
3136
3161
 
@@ -1,12 +1,15 @@
1
- /** @import { RemoteCommand, RemoteQueryOverride } from '@sveltejs/kit' */
1
+ /** @import { RemoteCommand, RemoteQueryUpdate } from '@sveltejs/kit' */
2
2
  /** @import { RemoteFunctionResponse } from 'types' */
3
- /** @import { Query } from './query.svelte.js' */
4
3
  import { app_dir, base } from '$app/paths/internal/client';
5
4
  import * as devalue from 'devalue';
6
5
  import { HttpError } from '@sveltejs/kit/internal';
7
6
  import { app } from '../client.js';
8
7
  import { stringify_remote_arg } from '../../shared.js';
9
- import { get_remote_request_headers, refresh_queries, release_overrides } from './shared.svelte.js';
8
+ import {
9
+ get_remote_request_headers,
10
+ apply_refreshes,
11
+ categorize_updates
12
+ } from './shared.svelte.js';
10
13
 
11
14
  /**
12
15
  * Client-version of the `command` function from `$app/server`.
@@ -21,8 +24,13 @@ export function command(id) {
21
24
  // If we make it async, the return type will be a promise that resolves to a promise with an updates() method, which is not what we want.
22
25
  /** @type {RemoteCommand<any, any>} */
23
26
  const command_function = (arg) => {
24
- /** @type {Array<Query<any> | RemoteQueryOverride>} */
25
- let updates = [];
27
+ let overrides = /** @type {Array<() => void> | null} */ (null);
28
+
29
+ /** @type {Set<string> | null} */
30
+ let refreshes = null;
31
+
32
+ /** @type {Error | undefined} */
33
+ let updates_error;
26
34
 
27
35
  // Increment pending count when command starts
28
36
  pending_count++;
@@ -34,23 +42,26 @@ export function command(id) {
34
42
  ...get_remote_request_headers()
35
43
  };
36
44
 
37
- /** @type {Promise<any> & { updates: (...args: any[]) => any }} */
45
+ /** @type {Promise<any> & { updates: (...args: RemoteQueryUpdate[]) => Promise<any> }} */
38
46
  const promise = (async () => {
39
47
  try {
40
48
  // Wait a tick to give room for the `updates` method to be called
41
49
  await Promise.resolve();
42
50
 
51
+ if (updates_error) {
52
+ throw updates_error;
53
+ }
54
+
43
55
  const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
44
56
  method: 'POST',
45
57
  body: JSON.stringify({
46
- payload: stringify_remote_arg(arg, app.hooks.transport),
47
- refreshes: updates.map((u) => u._key)
58
+ payload: stringify_remote_arg(arg, app.hooks.transport, false),
59
+ refreshes: Array.from(refreshes ?? [])
48
60
  }),
49
61
  headers
50
62
  });
51
63
 
52
64
  if (!response.ok) {
53
- release_overrides(updates);
54
65
  // We only end up here in case of a network error or if the server has an internal error
55
66
  // (which shouldn't happen because we handle errors on the server and always send a 200 response)
56
67
  throw new Error('Failed to execute remote function');
@@ -58,30 +69,42 @@ export function command(id) {
58
69
 
59
70
  const result = /** @type {RemoteFunctionResponse} */ (await response.json());
60
71
  if (result.type === 'redirect') {
61
- release_overrides(updates);
62
72
  throw new Error(
63
73
  'Redirects are not allowed in commands. Return a result instead and use goto on the client'
64
74
  );
65
75
  } else if (result.type === 'error') {
66
- release_overrides(updates);
67
76
  throw new HttpError(result.status ?? 500, result.error);
68
77
  } else {
69
78
  if (result.refreshes) {
70
- refresh_queries(result.refreshes, updates);
79
+ apply_refreshes(result.refreshes);
71
80
  }
72
81
 
73
82
  return devalue.parse(result.result, app.decoders);
74
83
  }
75
84
  } finally {
85
+ overrides?.forEach((fn) => fn());
86
+
76
87
  // Decrement pending count when command completes
77
88
  pending_count--;
78
89
  }
79
90
  })();
80
91
 
81
- promise.updates = (/** @type {any} */ ...args) => {
82
- updates = args;
83
- // @ts-expect-error Don't allow updates to be called multiple times
84
- delete promise.updates;
92
+ let updates_called = false;
93
+ promise.updates = (...args) => {
94
+ if (updates_called) {
95
+ console.warn(
96
+ 'Updates can only be sent once per command invocation. Ignoring additional updates.'
97
+ );
98
+ return promise;
99
+ }
100
+ updates_called = true;
101
+
102
+ try {
103
+ ({ refreshes, overrides } = categorize_updates(args));
104
+ } catch (error) {
105
+ updates_error = /** @type {Error} */ (error);
106
+ }
107
+
85
108
  return promise;
86
109
  };
87
110
 
@@ -1,14 +1,13 @@
1
1
  /** @import { StandardSchemaV1 } from '@standard-schema/spec' */
2
- /** @import { RemoteFormInput, RemoteForm, RemoteQueryOverride } from '@sveltejs/kit' */
2
+ /** @import { RemoteFormInput, RemoteForm, RemoteQueryUpdate } from '@sveltejs/kit' */
3
3
  /** @import { InternalRemoteFormIssue, RemoteFunctionResponse } from 'types' */
4
- /** @import { Query } from './query.svelte.js' */
5
4
  import { app_dir, base } from '$app/paths/internal/client';
6
5
  import * as devalue from 'devalue';
7
6
  import { DEV } from 'esm-env';
8
7
  import { HttpError } from '@sveltejs/kit/internal';
9
- import { app, remote_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
8
+ import { app, query_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
10
9
  import { tick } from 'svelte';
11
- import { refresh_queries, release_overrides } from './shared.svelte.js';
10
+ import { apply_refreshes, categorize_updates } from './shared.svelte.js';
12
11
  import { createAttachmentKey } from 'svelte/attachments';
13
12
  import {
14
13
  convert_formdata,
@@ -53,7 +52,6 @@ function merge_with_server_issues(form_data, current_issues, client_issues) {
53
52
  */
54
53
  export function form(id) {
55
54
  /** @type {Map<any, { count: number, instance: RemoteForm<T, U> }>} */
56
- // eslint-disable-next-line svelte/prefer-svelte-reactivity -- we don't need reactivity for this
57
55
  const instances = new Map();
58
56
 
59
57
  /** @param {string | number | boolean} [key] */
@@ -73,7 +71,7 @@ export function form(id) {
73
71
  const issues = $derived(flatten_issues(raw_issues));
74
72
 
75
73
  /** @type {any} */
76
- let result = $state.raw(remote_responses[action_id]);
74
+ let result = $state.raw(query_responses[action_id]);
77
75
 
78
76
  /** @type {number} */
79
77
  let pending_count = $state(0);
@@ -167,6 +165,8 @@ export function form(id) {
167
165
  const error = e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
168
166
  const status = e instanceof HttpError ? e.status : 500;
169
167
  void set_nearest_error_page(error, status);
168
+ } finally {
169
+ pending_count--;
170
170
  }
171
171
  }
172
172
 
@@ -185,16 +185,25 @@ export function form(id) {
185
185
  entry.count++;
186
186
  }
187
187
 
188
- /** @type {Array<Query<any> | RemoteQueryOverride>} */
189
- let updates = [];
188
+ let overrides = /** @type {Array<() => void> | null} */ (null);
189
+
190
+ /** @type {Set<string> | null} */
191
+ let refreshes = null;
192
+
193
+ /** @type {Error | undefined} */
194
+ let updates_error;
190
195
 
191
- /** @type {Promise<any> & { updates: (...args: any[]) => any }} */
196
+ /** @type {Promise<any> & { updates: (...args: RemoteQueryUpdate[]) => Promise<any> }} */
192
197
  const promise = (async () => {
193
198
  try {
194
199
  await Promise.resolve();
195
200
 
201
+ if (updates_error) {
202
+ throw updates_error;
203
+ }
204
+
196
205
  const { blob } = serialize_binary_form(convert(data), {
197
- remote_refreshes: updates.map((u) => u._key)
206
+ remote_refreshes: Array.from(refreshes ?? [])
198
207
  });
199
208
 
200
209
  const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
@@ -222,33 +231,28 @@ export function form(id) {
222
231
  if (form_result.type === 'result') {
223
232
  ({ issues: raw_issues = [], result } = devalue.parse(form_result.result, app.decoders));
224
233
 
225
- if (issues.$) {
226
- release_overrides(updates);
227
- } else {
234
+ if (!issues.$) {
228
235
  if (form_result.refreshes) {
229
- refresh_queries(form_result.refreshes, updates);
236
+ apply_refreshes(form_result.refreshes);
230
237
  } else {
231
238
  void invalidateAll();
232
239
  }
233
240
  }
234
241
  } else if (form_result.type === 'redirect') {
235
- const refreshes = form_result.refreshes ?? '';
236
- const invalidateAll = !refreshes && updates.length === 0;
237
- if (!invalidateAll) {
238
- refresh_queries(refreshes, updates);
242
+ const stringified_refreshes = form_result.refreshes ?? '';
243
+ if (stringified_refreshes) {
244
+ apply_refreshes(stringified_refreshes);
239
245
  }
240
246
  // Use internal version to allow redirects to external URLs
241
- void _goto(form_result.location, { invalidateAll }, 0);
247
+ void _goto(form_result.location, { invalidateAll: !stringified_refreshes }, 0);
242
248
  } else {
243
249
  throw new HttpError(form_result.status ?? 500, form_result.error);
244
250
  }
245
251
  } catch (e) {
246
252
  result = undefined;
247
- release_overrides(updates);
248
253
  throw e;
249
254
  } finally {
250
- // Decrement pending count when submission completes
251
- pending_count--;
255
+ overrides?.forEach((fn) => fn());
252
256
 
253
257
  void tick().then(() => {
254
258
  if (entry) {
@@ -261,8 +265,22 @@ export function form(id) {
261
265
  }
262
266
  })();
263
267
 
268
+ let updates_called = false;
264
269
  promise.updates = (...args) => {
265
- updates = args;
270
+ if (updates_called) {
271
+ console.warn(
272
+ 'Updates can only be sent once per form submission. Ignoring additional updates.'
273
+ );
274
+ return promise;
275
+ }
276
+ updates_called = true;
277
+
278
+ try {
279
+ ({ refreshes, overrides } = categorize_updates(args));
280
+ } catch (error) {
281
+ updates_error = /** @type {Error} */ (error);
282
+ }
283
+
266
284
  return promise;
267
285
  };
268
286
 
@@ -286,7 +304,6 @@ export function form(id) {
286
304
 
287
305
  if (method !== 'post') return;
288
306
 
289
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
290
307
  const action = new URL(
291
308
  // We can't do submitter.formAction directly because that property is always set
292
309
  event.submitter?.hasAttribute('formaction')