@sveltejs/kit 2.54.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.
- package/package.json +3 -4
- package/src/core/postbuild/analyse.js +3 -3
- package/src/core/postbuild/prerender.js +9 -6
- package/src/core/sync/write_non_ambient.js +143 -37
- package/src/core/sync/write_tsconfig.js +3 -1
- package/src/core/sync/write_types/index.js +1 -5
- package/src/exports/internal/remote-functions.js +2 -2
- package/src/exports/public.d.ts +38 -12
- package/src/exports/vite/build/build_server.js +24 -4
- package/src/exports/vite/build/build_service_worker.js +16 -6
- package/src/exports/vite/build/utils.js +18 -3
- package/src/exports/vite/index.js +336 -327
- package/src/runtime/app/paths/server.js +1 -1
- package/src/runtime/app/server/index.js +1 -1
- package/src/runtime/app/server/remote/command.js +12 -7
- package/src/runtime/app/server/remote/form.js +14 -14
- package/src/runtime/app/server/remote/index.js +1 -0
- package/src/runtime/app/server/remote/prerender.js +8 -7
- package/src/runtime/app/server/remote/query.js +141 -66
- package/src/runtime/app/server/remote/requested.js +172 -0
- package/src/runtime/app/server/remote/shared.js +32 -10
- package/src/runtime/app/state/server.js +1 -1
- package/src/runtime/client/client.js +45 -20
- package/src/runtime/client/remote-functions/command.svelte.js +39 -16
- package/src/runtime/client/remote-functions/form.svelte.js +41 -24
- package/src/runtime/client/remote-functions/prerender.svelte.js +105 -76
- package/src/runtime/client/remote-functions/query.svelte.js +408 -138
- package/src/runtime/client/remote-functions/shared.svelte.js +95 -94
- package/src/runtime/components/svelte-5/error.svelte +2 -0
- package/src/runtime/form-utils.js +3 -7
- package/src/runtime/server/endpoint.js +0 -1
- package/src/runtime/server/page/actions.js +2 -1
- package/src/runtime/server/page/load_data.js +3 -1
- package/src/runtime/server/page/render.js +38 -15
- package/src/runtime/server/remote.js +65 -50
- package/src/runtime/server/respond.js +17 -3
- package/src/runtime/server/utils.js +0 -12
- package/src/runtime/shared.js +233 -5
- package/src/types/global-private.d.ts +4 -4
- package/src/types/internal.d.ts +80 -44
- package/src/utils/css.js +0 -3
- package/src/utils/escape.js +15 -3
- package/src/version.js +1 -1
- package/types/index.d.ts +67 -13
- 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,
|
|
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 {
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
171
|
+
* @param {RemoteInternals} internals
|
|
150
172
|
* @param {RequestState} state
|
|
151
173
|
*/
|
|
152
|
-
export function get_cache(
|
|
153
|
-
let cache = state.
|
|
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.
|
|
179
|
+
(state.remote.data ??= new Map()).set(internals, cache);
|
|
158
180
|
}
|
|
159
181
|
|
|
160
182
|
return cache;
|
|
@@ -11,7 +11,7 @@ function context_dev(name) {
|
|
|
11
11
|
return context();
|
|
12
12
|
} catch {
|
|
13
13
|
throw new Error(
|
|
14
|
-
`Can only read '${name}' on the server during rendering (not in e.g. \`load\` functions), as it is bound to the current request via component context. This prevents state from leaking between users
|
|
14
|
+
`Can only read '${name}' on the server during rendering (not in e.g. \`load\` functions), as it is bound to the current request via component context. This prevents state from leaking between users. ` +
|
|
15
15
|
'For more information, see https://svelte.dev/docs/kit/state-management#avoid-shared-state-on-the-server'
|
|
16
16
|
);
|
|
17
17
|
}
|
|
@@ -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
|
|
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
|
|
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,
|
|
296
|
-
* A map of id -> query
|
|
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__
|
|
313
|
-
|
|
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((
|
|
405
|
-
|
|
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(
|
|
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 = [
|
|
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((
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
//
|
|
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,
|
|
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 {
|
|
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<
|
|
25
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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,
|
|
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,
|
|
8
|
+
import { app, query_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
|
|
10
9
|
import { tick } from 'svelte';
|
|
11
|
-
import {
|
|
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(
|
|
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<
|
|
189
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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')
|