@sveltejs/kit 2.58.0 → 2.59.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.
- package/package.json +1 -1
- package/src/exports/internal/remote-functions.js +1 -1
- package/src/exports/public.d.ts +77 -10
- package/src/exports/vite/dev/index.js +1 -1
- package/src/runtime/app/server/remote/command.js +7 -6
- package/src/runtime/app/server/remote/form.js +2 -1
- package/src/runtime/app/server/remote/query.js +304 -75
- package/src/runtime/app/server/remote/requested.js +109 -20
- package/src/runtime/app/server/remote/shared.js +84 -9
- package/src/runtime/client/client.js +92 -62
- package/src/runtime/client/ndjson.js +42 -0
- package/src/runtime/client/remote-functions/command.svelte.js +6 -1
- package/src/runtime/client/remote-functions/form.svelte.js +23 -6
- package/src/runtime/client/remote-functions/index.js +3 -1
- package/src/runtime/client/remote-functions/query-batch.svelte.js +105 -0
- package/src/runtime/client/remote-functions/query-live.svelte.js +636 -0
- package/src/runtime/client/remote-functions/query.svelte.js +15 -115
- package/src/runtime/client/remote-functions/shared.svelte.js +76 -23
- package/src/runtime/form-utils.js +31 -10
- package/src/runtime/server/page/load_data.js +4 -2
- package/src/runtime/server/page/render.js +4 -1
- package/src/runtime/server/remote.js +117 -9
- package/src/runtime/server/respond.js +1 -0
- package/src/runtime/shared.js +2 -2
- package/src/runtime/utils.js +0 -1
- package/src/types/internal.d.ts +45 -13
- package/src/version.js +1 -1
- package/types/index.d.ts +137 -15
- package/types/index.d.ts.map +7 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @import { RequestEvent } from '@sveltejs/kit' */
|
|
2
|
-
/** @import { ServerHooks, MaybePromise, RequestState, RemoteInternals, RequestStore } from 'types' */
|
|
2
|
+
/** @import { ServerHooks, MaybePromise, RequestState, RemoteInternals, RequestStore, RemoteLiveQueryUserFunctionReturnType } 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';
|
|
@@ -8,7 +8,7 @@ import { create_remote_key, stringify, unfriendly_hydratable } from '../../../sh
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @param {any} validate_or_fn
|
|
11
|
-
* @param {(arg?: any) => any} [maybe_fn]
|
|
11
|
+
* @param {((arg?: any) => any) | undefined} [maybe_fn]
|
|
12
12
|
* @returns {(arg?: any) => MaybePromise<any>}
|
|
13
13
|
*/
|
|
14
14
|
export function create_validator(validate_or_fn, maybe_fn) {
|
|
@@ -31,6 +31,7 @@ export function create_validator(validate_or_fn, maybe_fn) {
|
|
|
31
31
|
return async (arg) => {
|
|
32
32
|
// Get event before async validation to ensure it's available in server environments without AsyncLocalStorage, too
|
|
33
33
|
const { event, state } = get_request_store();
|
|
34
|
+
|
|
34
35
|
// access property and call method in one go to preserve potential this context
|
|
35
36
|
const result = await validate_or_fn['~standard'].validate(arg);
|
|
36
37
|
|
|
@@ -109,17 +110,13 @@ export function parse_remote_response(data, transport) {
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/**
|
|
112
|
-
* Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`.
|
|
113
|
-
* @template T
|
|
114
113
|
* @param {RequestEvent} event
|
|
115
114
|
* @param {RequestState} state
|
|
116
115
|
* @param {boolean} allow_cookies
|
|
117
|
-
* @
|
|
118
|
-
* @param {(arg?: any) => T} fn
|
|
116
|
+
* @returns {RequestStore}
|
|
119
117
|
*/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const store = {
|
|
118
|
+
function derive_remote_function_event(event, state, allow_cookies) {
|
|
119
|
+
return {
|
|
123
120
|
event: {
|
|
124
121
|
...event,
|
|
125
122
|
setHeaders: () => {
|
|
@@ -156,12 +153,90 @@ export async function run_remote_function(event, state, allow_cookies, get_input
|
|
|
156
153
|
is_in_remote_function: true
|
|
157
154
|
}
|
|
158
155
|
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`.
|
|
160
|
+
* @template T
|
|
161
|
+
* @param {RequestEvent} event
|
|
162
|
+
* @param {RequestState} state
|
|
163
|
+
* @param {boolean} allow_cookies
|
|
164
|
+
* @param {() => any} get_input
|
|
165
|
+
* @param {(arg?: any) => T} fn
|
|
166
|
+
*/
|
|
167
|
+
export async function run_remote_function(event, state, allow_cookies, get_input, fn) {
|
|
168
|
+
const store = derive_remote_function_event(event, state, allow_cookies);
|
|
159
169
|
|
|
160
170
|
// In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function
|
|
161
171
|
const input = await with_request_store(store, get_input);
|
|
162
172
|
return with_request_store(store, () => fn(input));
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`.
|
|
177
|
+
* @template T
|
|
178
|
+
* @param {RequestEvent} event
|
|
179
|
+
* @param {RequestState} state
|
|
180
|
+
* @param {boolean} allow_cookies
|
|
181
|
+
* @param {() => any} get_input
|
|
182
|
+
* @param {(arg?: any) => RemoteLiveQueryUserFunctionReturnType<T>} fn
|
|
183
|
+
* @param {string} name
|
|
184
|
+
*/
|
|
185
|
+
export async function* run_remote_generator(event, state, allow_cookies, get_input, fn, name) {
|
|
186
|
+
const store = derive_remote_function_event(event, state, allow_cookies);
|
|
187
|
+
|
|
188
|
+
// In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function / calls to next
|
|
189
|
+
const input = await with_request_store(store, get_input);
|
|
190
|
+
const source = await with_request_store(store, () => fn(input));
|
|
191
|
+
const iterator = to_iterator(source, name);
|
|
192
|
+
let done = false;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
while (true) {
|
|
196
|
+
// the code of a generator function is basically chopped apart at each
|
|
197
|
+
// yield, and each part is an invocation of `.next`. So, to provide
|
|
198
|
+
// access to the request context in generator functions, we have to
|
|
199
|
+
// provide it to every invocation of `.next`. (It's more obvious that
|
|
200
|
+
// this is necessary with plain iterators.)
|
|
201
|
+
const result = await with_request_store(store, () => iterator.next());
|
|
202
|
+
if (result.done) {
|
|
203
|
+
done = true;
|
|
204
|
+
return result.value;
|
|
205
|
+
}
|
|
206
|
+
yield result.value;
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
if (!done && typeof iterator.return === 'function') {
|
|
210
|
+
await with_request_store(store, () => iterator.return?.(undefined));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @template T
|
|
217
|
+
* @param {Awaited<RemoteLiveQueryUserFunctionReturnType<T>>} source
|
|
218
|
+
* @param {string} name
|
|
219
|
+
* @returns {Iterator<T> | AsyncIterator<T>}
|
|
220
|
+
*/
|
|
221
|
+
function to_iterator(source, name) {
|
|
222
|
+
// intentionally using `in` because these could be inherited
|
|
223
|
+
if ('next' in source && typeof source.next === 'function') {
|
|
224
|
+
return source;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (Symbol.asyncIterator in source && typeof source[Symbol.asyncIterator] === 'function') {
|
|
228
|
+
return source[Symbol.asyncIterator]();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (Symbol.iterator in source && typeof source[Symbol.iterator] === 'function') {
|
|
232
|
+
return source[Symbol.iterator]();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
throw new Error(
|
|
236
|
+
`query.live '${name}' must return an Iterator, Iterable, AsyncIterator or AsyncIterable`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
165
240
|
/**
|
|
166
241
|
* @param {RemoteInternals} internals
|
|
167
242
|
* @param {RequestState} state
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @import { RemoteQueryCacheEntry } from './remote-functions/query.svelte.js' */
|
|
2
|
+
/** @import { RemoteLiveQueryCacheEntry } from './remote-functions/query-live.svelte.js' */
|
|
2
3
|
import { BROWSER, DEV } from 'esm-env';
|
|
3
4
|
import * as svelte from 'svelte';
|
|
4
5
|
import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
|
|
@@ -44,6 +45,7 @@ import { compact } from '../../utils/array.js';
|
|
|
44
45
|
import {
|
|
45
46
|
INVALIDATED_PARAM,
|
|
46
47
|
TRAILING_SLASH_PARAM,
|
|
48
|
+
create_remote_key,
|
|
47
49
|
validate_depends,
|
|
48
50
|
validate_load_response
|
|
49
51
|
} from '../shared.js';
|
|
@@ -52,7 +54,7 @@ import { writable } from 'svelte/store';
|
|
|
52
54
|
import { page, update, navigating } from './state.svelte.js';
|
|
53
55
|
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
|
|
54
56
|
import { noop_span } from '../telemetry/noop.js';
|
|
55
|
-
import {
|
|
57
|
+
import { read_ndjson } from './ndjson.js';
|
|
56
58
|
|
|
57
59
|
export { load_css };
|
|
58
60
|
const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
|
|
@@ -305,6 +307,12 @@ export let pending_invalidate;
|
|
|
305
307
|
*/
|
|
306
308
|
export const query_map = new Map();
|
|
307
309
|
|
|
310
|
+
/**
|
|
311
|
+
* @type {Map<string, Map<string, RemoteLiveQueryCacheEntry<any>>>}
|
|
312
|
+
* A map of id -> payload -> live query internals for all active queries.
|
|
313
|
+
*/
|
|
314
|
+
export const live_query_map = new Map();
|
|
315
|
+
|
|
308
316
|
/**
|
|
309
317
|
* @param {import('./types.js').SvelteKitApp} _app
|
|
310
318
|
* @param {HTMLElement} _target
|
|
@@ -409,12 +417,23 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
|
|
|
409
417
|
discard_load_cache();
|
|
410
418
|
|
|
411
419
|
// Rerun queries
|
|
420
|
+
/** @type {Map<string, Promise<void>>} */
|
|
421
|
+
const live_query_reconnects = new Map();
|
|
412
422
|
if (force_invalidation) {
|
|
413
|
-
query_map.
|
|
414
|
-
|
|
415
|
-
void resource.refresh
|
|
416
|
-
}
|
|
417
|
-
}
|
|
423
|
+
for (const entries of query_map.values()) {
|
|
424
|
+
for (const { resource } of entries.values()) {
|
|
425
|
+
void resource.refresh();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (const [query_id, entries] of live_query_map) {
|
|
430
|
+
for (const [payload, { resource }] of entries) {
|
|
431
|
+
const key = create_remote_key(query_id, payload);
|
|
432
|
+
const promise = resource.reconnect();
|
|
433
|
+
promise.catch(noop);
|
|
434
|
+
live_query_reconnects.set(key, promise);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
418
437
|
}
|
|
419
438
|
|
|
420
439
|
if (include_load_functions) {
|
|
@@ -443,12 +462,26 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
|
|
|
443
462
|
reset_invalidation();
|
|
444
463
|
}
|
|
445
464
|
|
|
465
|
+
// only wait for promises that are connected to queries that still exist
|
|
466
|
+
/** @type {Promise<any>[]} */
|
|
467
|
+
const promises = [];
|
|
468
|
+
for (const entries of query_map.values()) {
|
|
469
|
+
for (const { resource } of entries.values()) {
|
|
470
|
+
promises.push(resource);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const [query_id, entries] of live_query_map) {
|
|
474
|
+
for (const payload of entries.keys()) {
|
|
475
|
+
const key = create_remote_key(query_id, payload);
|
|
476
|
+
const promise = live_query_reconnects.get(key);
|
|
477
|
+
if (promise) {
|
|
478
|
+
promises.push(promise);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
446
483
|
// Don't use allSettled yet because it's too new
|
|
447
|
-
await Promise.all(
|
|
448
|
-
[...query_map.values()].flatMap((entries) =>
|
|
449
|
-
[...entries.values()].map(({ resource }) => resource)
|
|
450
|
-
)
|
|
451
|
-
).catch(noop);
|
|
484
|
+
await Promise.all(promises).catch(noop);
|
|
452
485
|
}
|
|
453
486
|
|
|
454
487
|
function reset_invalidation() {
|
|
@@ -485,8 +518,10 @@ function persist_state() {
|
|
|
485
518
|
* @param {{}} [nav_token]
|
|
486
519
|
*/
|
|
487
520
|
export async function _goto(url, options, redirect_count, nav_token) {
|
|
488
|
-
/** @type {string
|
|
521
|
+
/** @type {Set<string>} */
|
|
489
522
|
let query_keys;
|
|
523
|
+
/** @type {Set<string>} */
|
|
524
|
+
let live_query_keys;
|
|
490
525
|
|
|
491
526
|
// Clear preload cache when invalidateAll is true to ensure fresh data
|
|
492
527
|
// after form submissions or explicit invalidations
|
|
@@ -506,12 +541,18 @@ export async function _goto(url, options, redirect_count, nav_token) {
|
|
|
506
541
|
accept: () => {
|
|
507
542
|
if (options.invalidateAll) {
|
|
508
543
|
force_invalidation = true;
|
|
509
|
-
query_keys =
|
|
510
|
-
|
|
544
|
+
query_keys = new Set();
|
|
545
|
+
for (const [id, entries] of query_map) {
|
|
511
546
|
for (const payload of entries.keys()) {
|
|
512
|
-
query_keys.
|
|
547
|
+
query_keys.add(create_remote_key(id, payload));
|
|
513
548
|
}
|
|
514
|
-
}
|
|
549
|
+
}
|
|
550
|
+
live_query_keys = new Set();
|
|
551
|
+
for (const [id, entries] of live_query_map) {
|
|
552
|
+
for (const payload of entries.keys()) {
|
|
553
|
+
live_query_keys.add(create_remote_key(id, payload));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
515
556
|
}
|
|
516
557
|
|
|
517
558
|
if (options.invalidate) {
|
|
@@ -527,13 +568,20 @@ export async function _goto(url, options, redirect_count, nav_token) {
|
|
|
527
568
|
.tick()
|
|
528
569
|
.then(svelte.tick)
|
|
529
570
|
.then(() => {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (query_keys?.
|
|
533
|
-
void resource.refresh
|
|
571
|
+
for (const [id, entries] of query_map) {
|
|
572
|
+
for (const [payload, { resource }] of entries) {
|
|
573
|
+
if (query_keys?.has(create_remote_key(id, payload))) {
|
|
574
|
+
void resource.refresh();
|
|
534
575
|
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (const [id, entries] of live_query_map) {
|
|
579
|
+
for (const [payload, { resource }] of entries) {
|
|
580
|
+
if (live_query_keys?.has(create_remote_key(id, payload))) {
|
|
581
|
+
void resource.reconnect();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
537
585
|
});
|
|
538
586
|
}
|
|
539
587
|
}
|
|
@@ -3035,49 +3083,31 @@ async function load_data(url, invalid) {
|
|
|
3035
3083
|
});
|
|
3036
3084
|
}
|
|
3037
3085
|
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
const { done, value } = await reader.read();
|
|
3043
|
-
if (done && !text) break;
|
|
3044
|
-
|
|
3045
|
-
text += !value && text ? '\n' : text_decoder.decode(value, { stream: true }); // no value -> final chunk -> add a new line to trigger the last parse
|
|
3046
|
-
|
|
3047
|
-
while (true) {
|
|
3048
|
-
const split = text.indexOf('\n');
|
|
3049
|
-
if (split === -1) {
|
|
3050
|
-
break;
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
const node = JSON.parse(text.slice(0, split));
|
|
3054
|
-
text = text.slice(split + 1);
|
|
3086
|
+
for await (const node of read_ndjson(reader)) {
|
|
3087
|
+
if (node.type === 'redirect') {
|
|
3088
|
+
return resolve(node);
|
|
3089
|
+
}
|
|
3055
3090
|
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
}
|
|
3091
|
+
if (node.type === 'data') {
|
|
3092
|
+
// This is the first (and possibly only, if no pending promises) chunk
|
|
3093
|
+
node.nodes?.forEach((/** @type {any} */ node) => {
|
|
3094
|
+
if (node?.type === 'data') {
|
|
3095
|
+
node.uses = deserialize_uses(node.uses);
|
|
3096
|
+
node.data = deserialize(node.data);
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3059
3099
|
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
}
|
|
3067
|
-
});
|
|
3100
|
+
resolve(node);
|
|
3101
|
+
} else if (node.type === 'chunk') {
|
|
3102
|
+
// This is a subsequent chunk containing deferred data
|
|
3103
|
+
const { id, data, error } = node;
|
|
3104
|
+
const deferred = /** @type {import('types').Deferred} */ (deferreds.get(id));
|
|
3105
|
+
deferreds.delete(id);
|
|
3068
3106
|
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
const deferred = /** @type {import('types').Deferred} */ (deferreds.get(id));
|
|
3074
|
-
deferreds.delete(id);
|
|
3075
|
-
|
|
3076
|
-
if (error) {
|
|
3077
|
-
deferred.reject(deserialize(error));
|
|
3078
|
-
} else {
|
|
3079
|
-
deferred.fulfil(deserialize(data));
|
|
3080
|
-
}
|
|
3107
|
+
if (error) {
|
|
3108
|
+
deferred.reject(deserialize(error));
|
|
3109
|
+
} else {
|
|
3110
|
+
deferred.fulfil(deserialize(data));
|
|
3081
3111
|
}
|
|
3082
3112
|
}
|
|
3083
3113
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yields parsed JSON objects from a ReadableStream of newline-delimited JSON.
|
|
3
|
+
* Each yielded value is the raw `JSON.parse`'d object — callers handle deserialization.
|
|
4
|
+
* @param {ReadableStreamDefaultReader<Uint8Array>} reader
|
|
5
|
+
*/
|
|
6
|
+
export async function* read_ndjson(reader) {
|
|
7
|
+
let done = false;
|
|
8
|
+
let buffer = '';
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
|
|
11
|
+
while (true) {
|
|
12
|
+
let split = buffer.indexOf('\n');
|
|
13
|
+
while (split !== -1) {
|
|
14
|
+
const line = buffer.slice(0, split).trim();
|
|
15
|
+
buffer = buffer.slice(split + 1);
|
|
16
|
+
|
|
17
|
+
if (line) {
|
|
18
|
+
yield JSON.parse(line);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
split = buffer.indexOf('\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (done) {
|
|
25
|
+
const line = buffer.trim();
|
|
26
|
+
if (line) {
|
|
27
|
+
yield JSON.parse(line);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const chunk = await reader.read();
|
|
33
|
+
done = chunk.done;
|
|
34
|
+
if (chunk.value) {
|
|
35
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (done) {
|
|
39
|
+
buffer += decoder.decode();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -8,7 +8,8 @@ import { stringify_remote_arg } from '../../shared.js';
|
|
|
8
8
|
import {
|
|
9
9
|
get_remote_request_headers,
|
|
10
10
|
apply_refreshes,
|
|
11
|
-
categorize_updates
|
|
11
|
+
categorize_updates,
|
|
12
|
+
apply_reconnections
|
|
12
13
|
} from './shared.svelte.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -79,6 +80,10 @@ export function command(id) {
|
|
|
79
80
|
apply_refreshes(result.refreshes);
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
if (result.reconnects) {
|
|
84
|
+
apply_reconnections(result.reconnects);
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
return devalue.parse(result.result, app.decoders);
|
|
83
88
|
}
|
|
84
89
|
} finally {
|
|
@@ -7,7 +7,7 @@ import { DEV } from 'esm-env';
|
|
|
7
7
|
import { HttpError } from '@sveltejs/kit/internal';
|
|
8
8
|
import { app, query_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
|
|
9
9
|
import { tick } from 'svelte';
|
|
10
|
-
import { apply_refreshes, categorize_updates } from './shared.svelte.js';
|
|
10
|
+
import { apply_refreshes, categorize_updates, apply_reconnections } from './shared.svelte.js';
|
|
11
11
|
import { createAttachmentKey } from 'svelte/attachments';
|
|
12
12
|
import {
|
|
13
13
|
convert_formdata,
|
|
@@ -156,7 +156,6 @@ export function form(id) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
try {
|
|
159
|
-
// eslint-disable-next-line @typescript-eslint/await-thenable -- `callback` is typed as returning `void` to allow returning e.g. `Promise<boolean>`
|
|
160
159
|
await callback({
|
|
161
160
|
form,
|
|
162
161
|
data,
|
|
@@ -235,21 +234,39 @@ export function form(id) {
|
|
|
235
234
|
const succeeded = raw_issues.length === 0;
|
|
236
235
|
|
|
237
236
|
if (succeeded) {
|
|
238
|
-
if (form_result.refreshes) {
|
|
239
|
-
apply_refreshes(form_result.refreshes);
|
|
240
|
-
} else {
|
|
237
|
+
if (refreshes === null && !form_result.refreshes && !form_result.reconnects) {
|
|
241
238
|
void invalidateAll();
|
|
239
|
+
} else {
|
|
240
|
+
if (form_result.refreshes) {
|
|
241
|
+
apply_refreshes(form_result.refreshes);
|
|
242
|
+
}
|
|
243
|
+
if (form_result.reconnects) {
|
|
244
|
+
apply_reconnections(form_result.reconnects);
|
|
245
|
+
}
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
return succeeded;
|
|
246
250
|
} else if (form_result.type === 'redirect') {
|
|
247
251
|
const stringified_refreshes = form_result.refreshes ?? '';
|
|
252
|
+
const stringified_reconnects = form_result.reconnects ?? '';
|
|
248
253
|
if (stringified_refreshes) {
|
|
249
254
|
apply_refreshes(stringified_refreshes);
|
|
250
255
|
}
|
|
256
|
+
|
|
257
|
+
if (stringified_reconnects) {
|
|
258
|
+
apply_reconnections(stringified_reconnects);
|
|
259
|
+
}
|
|
260
|
+
|
|
251
261
|
// Use internal version to allow redirects to external URLs
|
|
252
|
-
void _goto(
|
|
262
|
+
void _goto(
|
|
263
|
+
form_result.location,
|
|
264
|
+
{
|
|
265
|
+
invalidateAll:
|
|
266
|
+
refreshes === null && !stringified_refreshes && !stringified_reconnects
|
|
267
|
+
},
|
|
268
|
+
0
|
|
269
|
+
);
|
|
253
270
|
return true;
|
|
254
271
|
} else {
|
|
255
272
|
throw new HttpError(form_result.status ?? 500, form_result.error);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { command } from './command.svelte.js';
|
|
2
2
|
export { form } from './form.svelte.js';
|
|
3
3
|
export { prerender } from './prerender.svelte.js';
|
|
4
|
-
export { query
|
|
4
|
+
export { query } from './query.svelte.js';
|
|
5
|
+
export { query_batch } from './query-batch.svelte.js';
|
|
6
|
+
export { query_live } from './query-live.svelte.js';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** @import { RemoteQueryFunction } from '@sveltejs/kit' */
|
|
2
|
+
/** @import { RemoteFunctionResponse } from 'types' */
|
|
3
|
+
import { app_dir, base } from '$app/paths/internal/client';
|
|
4
|
+
import { app, goto } from '../client.js';
|
|
5
|
+
import { get_remote_request_headers, QUERY_FUNCTION_ID } from './shared.svelte.js';
|
|
6
|
+
import { QueryProxy } from './query.svelte.js';
|
|
7
|
+
import * as devalue from 'devalue';
|
|
8
|
+
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
9
|
+
import { unfriendly_hydratable } from '../../shared.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} id
|
|
13
|
+
* @returns {RemoteQueryFunction<any, any>}
|
|
14
|
+
*/
|
|
15
|
+
export function query_batch(id) {
|
|
16
|
+
/** @type {Map<string, Array<{resolve: (value: any) => void, reject: (error: any) => void}>>} */
|
|
17
|
+
let batching = new Map();
|
|
18
|
+
|
|
19
|
+
/** @type {RemoteQueryFunction<any, any>} */
|
|
20
|
+
const wrapper = (arg) => {
|
|
21
|
+
return new QueryProxy(id, arg, async (key, payload) => {
|
|
22
|
+
const serialized = await unfriendly_hydratable(key, () => {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
// create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
|
|
25
|
+
// is invoked multiple times with the same payload, so we need to deduplicate here
|
|
26
|
+
const entry = batching.get(payload) ?? [];
|
|
27
|
+
entry.push({ resolve, reject });
|
|
28
|
+
batching.set(payload, entry);
|
|
29
|
+
|
|
30
|
+
if (batching.size > 1) return;
|
|
31
|
+
|
|
32
|
+
// Do this here, after await Svelte' reactivity context is gone.
|
|
33
|
+
// TODO is it possible to have batches of the same key
|
|
34
|
+
// but in different forks/async contexts and in the same macrotask?
|
|
35
|
+
// If so this would potentially be buggy
|
|
36
|
+
const headers = {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
...get_remote_request_headers()
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
|
|
42
|
+
// and flushes could reveal more queries that should be batched.
|
|
43
|
+
setTimeout(async () => {
|
|
44
|
+
const batched = batching;
|
|
45
|
+
batching = new Map();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
payloads: Array.from(batched.keys())
|
|
52
|
+
}),
|
|
53
|
+
headers
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error('Failed to execute batch query');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
|
|
61
|
+
if (result.type === 'error') {
|
|
62
|
+
throw new HttpError(result.status ?? 500, result.error);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (result.type === 'redirect') {
|
|
66
|
+
await goto(result.location);
|
|
67
|
+
throw new Redirect(307, result.location);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const results = devalue.parse(result.result, app.decoders);
|
|
71
|
+
|
|
72
|
+
// Resolve individual queries
|
|
73
|
+
// Maps guarantee insertion order so we can do it like this
|
|
74
|
+
let i = 0;
|
|
75
|
+
|
|
76
|
+
for (const resolvers of batched.values()) {
|
|
77
|
+
for (const { resolve, reject } of resolvers) {
|
|
78
|
+
if (results[i].type === 'error') {
|
|
79
|
+
reject(new HttpError(results[i].status, results[i].error));
|
|
80
|
+
} else {
|
|
81
|
+
resolve(results[i].data);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Reject all queries in the batch
|
|
88
|
+
for (const resolver of batched.values()) {
|
|
89
|
+
for (const { reject } of resolver) {
|
|
90
|
+
reject(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, 0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return devalue.parse(serialized, app.decoders);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
Object.defineProperty(wrapper, QUERY_FUNCTION_ID, { value: id });
|
|
103
|
+
|
|
104
|
+
return wrapper;
|
|
105
|
+
}
|