@sveltejs/kit 3.0.0-next.4 → 3.0.0-next.7
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 +23 -18
- package/src/core/adapt/builder.js +29 -43
- package/src/core/adapt/index.js +1 -4
- package/src/core/config/index.js +135 -71
- package/src/core/config/options.js +291 -244
- package/src/core/config/types.d.ts +1 -1
- package/src/core/env.js +120 -5
- package/src/core/generate_manifest/index.js +12 -15
- package/src/core/postbuild/analyse.js +8 -12
- package/src/core/postbuild/crawl.js +22 -6
- package/src/core/postbuild/prerender.js +40 -23
- package/src/core/sync/create_manifest_data/index.js +29 -49
- package/src/core/sync/write_client_manifest.js +14 -14
- package/src/core/sync/write_non_ambient.js +10 -7
- package/src/core/sync/write_root.js +9 -30
- package/src/core/sync/write_server.js +13 -11
- package/src/core/sync/write_tsconfig.js +2 -4
- package/src/core/sync/write_types/index.js +11 -10
- package/src/exports/index.js +34 -12
- package/src/exports/internal/client.js +5 -0
- package/src/exports/internal/env.js +1 -1
- package/src/exports/internal/index.js +1 -90
- package/src/exports/internal/{event.js → server/event.js} +1 -2
- package/src/exports/internal/server/index.js +33 -0
- package/src/exports/internal/shared.js +89 -0
- package/src/exports/node/index.js +58 -7
- package/src/exports/params.js +63 -0
- package/src/exports/public.d.ts +299 -145
- package/src/exports/url.js +84 -0
- package/src/exports/vite/build/build_server.js +6 -1
- package/src/exports/vite/dev/index.js +38 -37
- package/src/exports/vite/index.js +498 -353
- package/src/exports/vite/preview/index.js +16 -8
- package/src/exports/vite/utils.js +7 -11
- package/src/runtime/app/env/types.d.ts +1 -1
- package/src/runtime/app/forms.js +22 -5
- package/src/runtime/app/paths/client.js +4 -10
- package/src/runtime/app/paths/public.d.ts +0 -28
- package/src/runtime/app/paths/server.js +8 -4
- package/src/runtime/app/server/remote/form.js +10 -3
- package/src/runtime/app/server/remote/query.js +9 -18
- package/src/runtime/app/server/remote/requested.js +8 -4
- package/src/runtime/app/server/remote/shared.js +5 -7
- package/src/runtime/app/state/client.js +1 -2
- package/src/runtime/app/stores.js +13 -76
- package/src/runtime/client/client.js +205 -167
- package/src/runtime/client/fetcher.js +3 -2
- package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
- package/src/runtime/client/remote-functions/form.svelte.js +122 -31
- package/src/runtime/client/remote-functions/prerender.svelte.js +17 -4
- package/src/runtime/client/remote-functions/query/index.js +3 -2
- package/src/runtime/client/remote-functions/query/instance.svelte.js +27 -11
- package/src/runtime/client/remote-functions/query-batch.svelte.js +4 -3
- package/src/runtime/client/remote-functions/query-live/instance.svelte.js +42 -15
- package/src/runtime/client/remote-functions/shared.svelte.js +18 -9
- package/src/runtime/client/state.svelte.js +66 -49
- package/src/runtime/client/types.d.ts +10 -2
- package/src/runtime/client/utils.js +0 -96
- package/src/runtime/form-utils.js +99 -18
- package/src/runtime/server/cookie.js +22 -33
- package/src/runtime/server/csrf.js +65 -0
- package/src/runtime/server/data/index.js +7 -7
- package/src/runtime/server/env_module.js +0 -5
- package/src/runtime/server/index.js +2 -2
- package/src/runtime/server/page/actions.js +41 -26
- package/src/runtime/server/page/index.js +2 -1
- package/src/runtime/server/page/load_data.js +1 -1
- package/src/runtime/server/page/render.js +36 -47
- package/src/runtime/server/page/respond_with_error.js +7 -8
- package/src/runtime/server/page/server_routing.js +13 -9
- package/src/runtime/server/remote.js +85 -39
- package/src/runtime/server/respond.js +92 -58
- package/src/runtime/server/utils.js +7 -7
- package/src/runtime/telemetry/otel.js +1 -1
- package/src/types/ambient.d.ts +10 -5
- package/src/types/global-private.d.ts +4 -4
- package/src/types/internal.d.ts +17 -20
- package/src/types/private.d.ts +33 -1
- package/src/types/synthetic/$lib.md +1 -1
- package/src/utils/error.js +12 -4
- package/src/utils/mime.js +9 -0
- package/src/utils/params.js +66 -0
- package/src/utils/routing.js +84 -38
- package/src/utils/shared-iterator.js +5 -0
- package/src/utils/streaming.js +14 -4
- package/src/utils/url.js +20 -2
- package/src/version.js +1 -1
- package/types/index.d.ts +434 -269
- package/types/index.d.ts.map +19 -15
- package/src/exports/internal/server.js +0 -22
- /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
|
@@ -94,8 +94,6 @@ export function initial_fetch(resource, opts) {
|
|
|
94
94
|
script.remove(); // In case multiple script tags match the same selector
|
|
95
95
|
let { body, ...init } = JSON.parse(script.textContent);
|
|
96
96
|
|
|
97
|
-
const ttl = script.getAttribute('data-ttl');
|
|
98
|
-
if (ttl) cache.set(selector, { body, init, ttl: 1000 * Number(ttl) });
|
|
99
97
|
const b64 = script.getAttribute('data-b64');
|
|
100
98
|
if (b64 !== null) {
|
|
101
99
|
// Can't use native_fetch('data:...;base64,${body}')
|
|
@@ -103,6 +101,9 @@ export function initial_fetch(resource, opts) {
|
|
|
103
101
|
body = base64_decode(body);
|
|
104
102
|
}
|
|
105
103
|
|
|
104
|
+
const ttl = script.getAttribute('data-ttl');
|
|
105
|
+
if (ttl) cache.set(selector, { body, init, ttl: 1000 * Number(ttl) });
|
|
106
|
+
|
|
106
107
|
return Promise.resolve(new Response(body, init));
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
/** @import { Query } from './query/instance.svelte.js' */
|
|
2
|
+
/** @import { LiveQuery } from './query-live/instance.svelte.js' */
|
|
1
3
|
import { tick } from 'svelte';
|
|
2
4
|
import { once } from '../../../utils/functions.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
|
-
* @template R
|
|
7
|
+
* @template [R=Query<any> | LiveQuery<any>]
|
|
6
8
|
* @typedef {object} CacheEntry
|
|
7
9
|
* @property {number} proxy_count The number of live proxy instances referencing this
|
|
8
10
|
* entry. The entry is eligible for eviction when this hits zero.
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
import { app_dir, base } from '$app/paths/internal/client';
|
|
5
5
|
import { DEV } from 'esm-env';
|
|
6
6
|
import { HttpError } from '@sveltejs/kit/internal';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
query_responses,
|
|
9
|
+
_goto,
|
|
10
|
+
set_nearest_error_page,
|
|
11
|
+
invalidateAll,
|
|
12
|
+
handle_error
|
|
13
|
+
} from '../client.js';
|
|
8
14
|
import { tick } from 'svelte';
|
|
9
15
|
import { categorize_updates, remote_request } from './shared.svelte.js';
|
|
10
16
|
import { createAttachmentKey } from 'svelte/attachments';
|
|
@@ -17,6 +23,8 @@ import {
|
|
|
17
23
|
build_path_string,
|
|
18
24
|
normalize_issue,
|
|
19
25
|
serialize_binary_form,
|
|
26
|
+
deep_get,
|
|
27
|
+
DELETE_KEY,
|
|
20
28
|
BINARY_FORM_CONTENT_TYPE
|
|
21
29
|
} from '../../form-utils.js';
|
|
22
30
|
|
|
@@ -52,6 +60,9 @@ export function form(id) {
|
|
|
52
60
|
/** @type {Map<any, { count: number, instance: RemoteForm<T, U> }>} */
|
|
53
61
|
const instances = new Map();
|
|
54
62
|
|
|
63
|
+
/** @type {StandardSchemaV1 | null} */
|
|
64
|
+
let shared_preflight_schema = null;
|
|
65
|
+
|
|
55
66
|
/** @param {string | number | boolean} [key] */
|
|
56
67
|
function create_instance(key) {
|
|
57
68
|
const action_id_without_key = id;
|
|
@@ -89,7 +100,9 @@ export function form(id) {
|
|
|
89
100
|
*/
|
|
90
101
|
let enhance_callback = async (instance) => {
|
|
91
102
|
if (await instance.submit()) {
|
|
92
|
-
|
|
103
|
+
await tick();
|
|
104
|
+
// We call reset from the prototype to avoid DOM clobbering
|
|
105
|
+
HTMLFormElement.prototype.reset.call(instance.element);
|
|
93
106
|
}
|
|
94
107
|
};
|
|
95
108
|
|
|
@@ -97,13 +110,22 @@ export function form(id) {
|
|
|
97
110
|
let element = null;
|
|
98
111
|
|
|
99
112
|
/** @type {Record<string, boolean>} */
|
|
100
|
-
let touched = {};
|
|
113
|
+
let touched = $state({});
|
|
114
|
+
|
|
115
|
+
/** @type {Record<string, boolean>} */
|
|
116
|
+
let dirty = $state({});
|
|
117
|
+
|
|
118
|
+
/** @type {Record<string, boolean>} */
|
|
119
|
+
let can_validate = {};
|
|
101
120
|
|
|
102
|
-
let submitted = false;
|
|
121
|
+
let submitted = $state(false);
|
|
103
122
|
|
|
104
123
|
/** @type {InternalRemoteFormIssue[] | null} */
|
|
105
124
|
let unread_issues = null;
|
|
106
125
|
|
|
126
|
+
/** @type {string | null} */
|
|
127
|
+
let previous_submitter_name = null;
|
|
128
|
+
|
|
107
129
|
/**
|
|
108
130
|
* In dev, warn if there are validation issues going unread
|
|
109
131
|
*/
|
|
@@ -302,7 +324,8 @@ export function form(id) {
|
|
|
302
324
|
*/
|
|
303
325
|
async function preflight(form_data) {
|
|
304
326
|
const data = convert(form_data);
|
|
305
|
-
const
|
|
327
|
+
const schema = preflight_schema ?? shared_preflight_schema;
|
|
328
|
+
const validated = await schema?.['~standard'].validate(data);
|
|
306
329
|
|
|
307
330
|
if (validated?.issues) {
|
|
308
331
|
raw_issues = merge_with_server_issues(
|
|
@@ -346,6 +369,8 @@ export function form(id) {
|
|
|
346
369
|
element = form;
|
|
347
370
|
|
|
348
371
|
touched = {};
|
|
372
|
+
dirty = {};
|
|
373
|
+
can_validate = {};
|
|
349
374
|
|
|
350
375
|
/** @param {SubmitEvent} event */
|
|
351
376
|
const handle_submit = async (event) => {
|
|
@@ -379,6 +404,29 @@ export function form(id) {
|
|
|
379
404
|
|
|
380
405
|
const form_data = new FormData(form, event.submitter);
|
|
381
406
|
|
|
407
|
+
if (
|
|
408
|
+
previous_submitter_name !== null &&
|
|
409
|
+
!Array.from(form_data.keys()).map(strip_prefix).includes(previous_submitter_name)
|
|
410
|
+
) {
|
|
411
|
+
// Strip any `n:`/`b:` type prefix before clearing, otherwise
|
|
412
|
+
// `set_nested_value` would coerce `undefined` to `NaN`/`false`
|
|
413
|
+
// instead of clearing the previously-submitted value.
|
|
414
|
+
set_nested_value(input, previous_submitter_name, undefined);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (event.submitter) {
|
|
418
|
+
const name = event.submitter.getAttribute('name');
|
|
419
|
+
const value = /** @type {any} */ (event.submitter).value;
|
|
420
|
+
|
|
421
|
+
if (name !== null && value !== undefined) {
|
|
422
|
+
set_nested_value(input, name, value);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
previous_submitter_name = strip_prefix(name);
|
|
426
|
+
} else {
|
|
427
|
+
previous_submitter_name = null;
|
|
428
|
+
}
|
|
429
|
+
|
|
382
430
|
if (DEV) {
|
|
383
431
|
validate_form_data(form_data, clone(form).enctype);
|
|
384
432
|
}
|
|
@@ -396,9 +444,14 @@ export function form(id) {
|
|
|
396
444
|
await enhance_callback(create_enhance_callback_instance(form, form_data));
|
|
397
445
|
} catch (e) {
|
|
398
446
|
const error =
|
|
399
|
-
e instanceof HttpError
|
|
400
|
-
|
|
401
|
-
|
|
447
|
+
e instanceof HttpError
|
|
448
|
+
? e.body
|
|
449
|
+
: await handle_error(e, {
|
|
450
|
+
params: {},
|
|
451
|
+
route: { id: null },
|
|
452
|
+
url: new URL(location.href)
|
|
453
|
+
});
|
|
454
|
+
void set_nearest_error_page(error);
|
|
402
455
|
} finally {
|
|
403
456
|
pending_count--;
|
|
404
457
|
}
|
|
@@ -418,8 +471,6 @@ export function form(id) {
|
|
|
418
471
|
|
|
419
472
|
const is_file = element.type === 'file';
|
|
420
473
|
|
|
421
|
-
touched[name] = true;
|
|
422
|
-
|
|
423
474
|
if (is_array) {
|
|
424
475
|
let value;
|
|
425
476
|
|
|
@@ -464,14 +515,7 @@ export function form(id) {
|
|
|
464
515
|
if (file) {
|
|
465
516
|
set_nested_value(input, name, file);
|
|
466
517
|
} else {
|
|
467
|
-
|
|
468
|
-
const path_parts = name.split(/\.|\[|\]/).filter(Boolean);
|
|
469
|
-
let current = /** @type {any} */ (input);
|
|
470
|
-
for (let i = 0; i < path_parts.length - 1; i++) {
|
|
471
|
-
if (current[path_parts[i]] == null) return;
|
|
472
|
-
current = current[path_parts[i]];
|
|
473
|
-
}
|
|
474
|
-
delete current[path_parts[path_parts.length - 1]];
|
|
518
|
+
set_nested_value(input, name, DELETE_KEY);
|
|
475
519
|
}
|
|
476
520
|
} else {
|
|
477
521
|
set_nested_value(
|
|
@@ -481,9 +525,9 @@ export function form(id) {
|
|
|
481
525
|
);
|
|
482
526
|
}
|
|
483
527
|
|
|
484
|
-
name = name
|
|
528
|
+
name = strip_prefix(name);
|
|
485
529
|
|
|
486
|
-
|
|
530
|
+
dirty[name] = true;
|
|
487
531
|
};
|
|
488
532
|
|
|
489
533
|
const handle_reset = async () => {
|
|
@@ -492,18 +536,38 @@ export function form(id) {
|
|
|
492
536
|
await tick();
|
|
493
537
|
|
|
494
538
|
input = convert_formdata(new FormData(form));
|
|
539
|
+
raw_issues = [];
|
|
540
|
+
touched = {};
|
|
541
|
+
dirty = {};
|
|
542
|
+
can_validate = {};
|
|
543
|
+
submitted = false;
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
/** @param {Event} e */
|
|
547
|
+
const handle_focusout = (e) => {
|
|
548
|
+
let name = /** @type {HTMLInputElement} */ (e.target).name;
|
|
549
|
+
if (!name) return;
|
|
550
|
+
|
|
551
|
+
name = strip_prefix(name).replace(/\[\]$/, '');
|
|
552
|
+
|
|
553
|
+
touched[name] = true;
|
|
554
|
+
|
|
555
|
+
if (Object.hasOwn(dirty, name)) {
|
|
556
|
+
can_validate[name] = true;
|
|
557
|
+
}
|
|
495
558
|
};
|
|
496
559
|
|
|
497
560
|
form.addEventListener('submit', handle_submit);
|
|
498
561
|
form.addEventListener('input', handle_input);
|
|
562
|
+
form.addEventListener('focusout', handle_focusout);
|
|
499
563
|
form.addEventListener('reset', handle_reset);
|
|
500
564
|
|
|
501
565
|
return () => {
|
|
502
566
|
form.removeEventListener('submit', handle_submit);
|
|
503
567
|
form.removeEventListener('input', handle_input);
|
|
568
|
+
form.removeEventListener('focusout', handle_focusout);
|
|
504
569
|
form.removeEventListener('reset', handle_reset);
|
|
505
570
|
element = null;
|
|
506
|
-
preflight_schema = undefined;
|
|
507
571
|
};
|
|
508
572
|
};
|
|
509
573
|
|
|
@@ -534,9 +598,10 @@ export function form(id) {
|
|
|
534
598
|
|
|
535
599
|
const submission = submit(form_data, true);
|
|
536
600
|
|
|
537
|
-
|
|
601
|
+
const decrement = () => {
|
|
538
602
|
pending_count--;
|
|
539
|
-
}
|
|
603
|
+
};
|
|
604
|
+
void submission.then(decrement, decrement);
|
|
540
605
|
|
|
541
606
|
return submission;
|
|
542
607
|
}
|
|
@@ -549,11 +614,16 @@ export function form(id) {
|
|
|
549
614
|
(path, value) => {
|
|
550
615
|
if (path.length === 0) {
|
|
551
616
|
input = value;
|
|
552
|
-
} else {
|
|
617
|
+
} else if (value !== deep_get(input, path)) {
|
|
553
618
|
deep_set(input, path.map(String), value);
|
|
554
619
|
|
|
555
620
|
const key = build_path_string(path);
|
|
556
|
-
|
|
621
|
+
|
|
622
|
+
if (element) {
|
|
623
|
+
touched[key] = true;
|
|
624
|
+
dirty[key] = true;
|
|
625
|
+
can_validate[key] = true;
|
|
626
|
+
}
|
|
557
627
|
}
|
|
558
628
|
},
|
|
559
629
|
(path, all) => {
|
|
@@ -567,7 +637,10 @@ export function form(id) {
|
|
|
567
637
|
}
|
|
568
638
|
|
|
569
639
|
return issues;
|
|
570
|
-
}
|
|
640
|
+
},
|
|
641
|
+
() => touched,
|
|
642
|
+
() => dirty,
|
|
643
|
+
[]
|
|
571
644
|
)
|
|
572
645
|
},
|
|
573
646
|
result: {
|
|
@@ -576,16 +649,24 @@ export function form(id) {
|
|
|
576
649
|
pending: {
|
|
577
650
|
get: () => pending_count
|
|
578
651
|
},
|
|
652
|
+
submitted: {
|
|
653
|
+
get: () => submitted
|
|
654
|
+
},
|
|
579
655
|
preflight: {
|
|
580
656
|
/** @type {RemoteForm<T, U>['preflight']} */
|
|
581
657
|
value: (schema) => {
|
|
582
658
|
preflight_schema = schema;
|
|
659
|
+
|
|
660
|
+
if (key === undefined) {
|
|
661
|
+
shared_preflight_schema = schema;
|
|
662
|
+
}
|
|
663
|
+
|
|
583
664
|
return instance;
|
|
584
665
|
}
|
|
585
666
|
},
|
|
586
667
|
validate: {
|
|
587
668
|
/** @type {RemoteForm<any, any>['validate']} */
|
|
588
|
-
value: async ({
|
|
669
|
+
value: async ({ all = false, preflightOnly = false } = {}) => {
|
|
589
670
|
if (!element) return;
|
|
590
671
|
|
|
591
672
|
const id = ++validate_id;
|
|
@@ -603,8 +684,8 @@ export function form(id) {
|
|
|
603
684
|
let array = [];
|
|
604
685
|
|
|
605
686
|
const data = convert(form_data);
|
|
606
|
-
|
|
607
|
-
const validated = await
|
|
687
|
+
const schema = preflight_schema ?? shared_preflight_schema;
|
|
688
|
+
const validated = await schema?.['~standard'].validate(data);
|
|
608
689
|
|
|
609
690
|
if (validate_id !== id) {
|
|
610
691
|
return;
|
|
@@ -636,8 +717,8 @@ export function form(id) {
|
|
|
636
717
|
array = /** @type {InternalRemoteFormIssue[]} */ (result._);
|
|
637
718
|
}
|
|
638
719
|
|
|
639
|
-
if (!
|
|
640
|
-
array = array.filter((issue) =>
|
|
720
|
+
if (!all && !submitted) {
|
|
721
|
+
array = array.filter((issue) => can_validate[issue.name]);
|
|
641
722
|
}
|
|
642
723
|
|
|
643
724
|
const is_server_validation = !validated?.issues && !preflightOnly;
|
|
@@ -728,3 +809,13 @@ function validate_form_data(form_data, enctype) {
|
|
|
728
809
|
}
|
|
729
810
|
}
|
|
730
811
|
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Remove the `n:` or `b:` prefix from a field name
|
|
815
|
+
* @template {string | null} T
|
|
816
|
+
* @param {T} name
|
|
817
|
+
* @returns {T}
|
|
818
|
+
*/
|
|
819
|
+
function strip_prefix(name) {
|
|
820
|
+
return /** @type {T} */ (name && name.replace(/^[nb]:/, ''));
|
|
821
|
+
}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { app_dir, base } from '$app/paths/internal/client';
|
|
3
3
|
import { version } from '$app/env';
|
|
4
4
|
import * as devalue from 'devalue';
|
|
5
|
-
import { app,
|
|
5
|
+
import { app, _goto, handle_error, prerender_responses } from '../client.js';
|
|
6
6
|
import { get_remote_request_headers, remote_request, unwrap_node } from './shared.svelte.js';
|
|
7
7
|
import { create_remote_key, stringify_remote_arg } from '../../shared.js';
|
|
8
|
+
import { noop } from '../../../utils/functions.js';
|
|
9
|
+
import { HttpError } from '@sveltejs/kit/internal';
|
|
8
10
|
|
|
9
11
|
// Initialize Cache API for prerender functions
|
|
10
12
|
const CACHE_NAME = __SVELTEKIT_DEV__ ? `sveltekit:${Date.now()}` : `sveltekit:${version}`;
|
|
@@ -97,7 +99,8 @@ export function prerender(id) {
|
|
|
97
99
|
const result = await remote_request(url, { headers });
|
|
98
100
|
|
|
99
101
|
if (result.redirect) {
|
|
100
|
-
|
|
102
|
+
// Use internal version to allow redirects to external URLs
|
|
103
|
+
void _goto(result.redirect, {}, 0);
|
|
101
104
|
return;
|
|
102
105
|
}
|
|
103
106
|
|
|
@@ -146,6 +149,7 @@ class Prerender {
|
|
|
146
149
|
/** @type {T | undefined} */
|
|
147
150
|
#current = $state.raw();
|
|
148
151
|
|
|
152
|
+
/** @type {App.Error | undefined} */
|
|
149
153
|
#error = $state.raw(undefined);
|
|
150
154
|
|
|
151
155
|
/**
|
|
@@ -160,12 +164,21 @@ class Prerender {
|
|
|
160
164
|
this.#error = undefined;
|
|
161
165
|
return value;
|
|
162
166
|
},
|
|
163
|
-
(
|
|
167
|
+
async (e) => {
|
|
168
|
+
const error = await handle_error(e, {
|
|
169
|
+
params: {},
|
|
170
|
+
route: { id: null },
|
|
171
|
+
url: new URL(location.href)
|
|
172
|
+
});
|
|
164
173
|
this.#loading = false;
|
|
165
174
|
this.#error = error;
|
|
166
|
-
throw error;
|
|
175
|
+
throw new HttpError(error.status, error); // so that transformError doesn't transform it again
|
|
167
176
|
}
|
|
168
177
|
);
|
|
178
|
+
|
|
179
|
+
// rejections are surfaced via `.error` for reactive consumers — make sure the
|
|
180
|
+
// stored promise (consumed without `await`) never becomes an unhandled rejection
|
|
181
|
+
this.#promise.catch(noop);
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @import { RemoteQueryFunction } from '@sveltejs/kit' */
|
|
2
2
|
import { app_dir, base } from '$app/paths/internal/client';
|
|
3
|
-
import {
|
|
3
|
+
import { _goto, query_map } from '../../client.js';
|
|
4
4
|
import { get_remote_request_headers, QUERY_FUNCTION_ID, remote_request } from '../shared.svelte.js';
|
|
5
5
|
import { DEV } from 'esm-env';
|
|
6
6
|
import { QueryProxy } from './proxy.js';
|
|
@@ -29,7 +29,8 @@ export function query(id) {
|
|
|
29
29
|
const result = await remote_request(url, { headers: get_remote_request_headers() });
|
|
30
30
|
|
|
31
31
|
if (result.redirect) {
|
|
32
|
-
|
|
32
|
+
// Use internal version to allow redirects to external URLs
|
|
33
|
+
await _goto(result.redirect, {}, 0);
|
|
33
34
|
}
|
|
34
35
|
});
|
|
35
36
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { query_responses } from '../../client.js';
|
|
1
|
+
import { query_responses, handle_error } from '../../client.js';
|
|
2
2
|
import { HttpError } from '@sveltejs/kit/internal';
|
|
3
3
|
import { QUERY_OVERRIDE_KEY } from '../shared.svelte.js';
|
|
4
4
|
import { noop } from '../../../../utils/functions.js';
|
|
@@ -37,7 +37,7 @@ export class Query {
|
|
|
37
37
|
return this.#overrides.reduce((v, r) => r(v), /** @type {T} */ (this.#raw));
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
/** @type {
|
|
40
|
+
/** @type {App.Error | undefined} */
|
|
41
41
|
#error = $state.raw(undefined);
|
|
42
42
|
|
|
43
43
|
/** @type {Promise<T>['then']} */
|
|
@@ -70,7 +70,7 @@ export class Query {
|
|
|
70
70
|
delete query_responses[key];
|
|
71
71
|
|
|
72
72
|
if (node.e) {
|
|
73
|
-
this.fail(new HttpError(node.e
|
|
73
|
+
this.fail(new HttpError(node.e.status, node.e));
|
|
74
74
|
} else {
|
|
75
75
|
this.set(/** @type {T} */ (node.v));
|
|
76
76
|
}
|
|
@@ -87,7 +87,9 @@ export class Query {
|
|
|
87
87
|
// every time you see this comment, try removing the `tick.then` here and see
|
|
88
88
|
// if all the tests still pass with the latest svelte version
|
|
89
89
|
// if they do, congrats, you can remove tick.then
|
|
90
|
-
void tick()
|
|
90
|
+
void tick()
|
|
91
|
+
.then(() => this.#get_promise())
|
|
92
|
+
.catch(noop);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
#clear_pending() {
|
|
@@ -100,6 +102,11 @@ export class Query {
|
|
|
100
102
|
|
|
101
103
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
102
104
|
|
|
105
|
+
// the rejection is surfaced via `.error` / the `then` getter for awaiting
|
|
106
|
+
// consumers — a purely reactive consumer (`.current`) attaches no handler,
|
|
107
|
+
// so make sure the stored promise can never become an unhandled rejection
|
|
108
|
+
promise.catch(noop);
|
|
109
|
+
|
|
103
110
|
this.#latest.push(resolve);
|
|
104
111
|
|
|
105
112
|
Promise.resolve(this.#fn())
|
|
@@ -119,22 +126,33 @@ export class Query {
|
|
|
119
126
|
|
|
120
127
|
resolve(undefined);
|
|
121
128
|
})
|
|
122
|
-
.catch((e) => {
|
|
129
|
+
.catch(async (e) => {
|
|
123
130
|
// TODO: Our behavior here could be better:
|
|
124
131
|
// - We should not reject on redirects, but should hook into the router
|
|
125
132
|
// to ensure the query is properly refreshed before the navigation completes
|
|
126
133
|
// - Instead of failing on transport-level errors, we should probably do what
|
|
127
134
|
// LiveQuery does and preserve the last known good value and retry the connection
|
|
135
|
+
if (this.#latest.indexOf(resolve) === -1) return;
|
|
136
|
+
|
|
137
|
+
const error = await handle_error(e, {
|
|
138
|
+
params: {},
|
|
139
|
+
route: { id: null },
|
|
140
|
+
url: new URL(location.href)
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Re-check after the async `handle_error` gap: a later request may have
|
|
144
|
+
// resolved/rejected while we were awaiting and superseded this one, so
|
|
145
|
+
// recompute the index and bail out if this request is no longer current
|
|
128
146
|
const idx = this.#latest.indexOf(resolve);
|
|
129
147
|
if (idx === -1) return;
|
|
130
148
|
|
|
131
149
|
untrack(() => {
|
|
132
150
|
this.#latest.splice(0, idx).forEach((r) => r(undefined));
|
|
133
|
-
this.#error =
|
|
151
|
+
this.#error = error;
|
|
134
152
|
this.#loading = false;
|
|
135
153
|
});
|
|
136
154
|
|
|
137
|
-
reject(
|
|
155
|
+
reject(new HttpError(error.status, error)); // so that transformError doesn't transform it again
|
|
138
156
|
});
|
|
139
157
|
|
|
140
158
|
return promise;
|
|
@@ -222,9 +240,7 @@ export class Query {
|
|
|
222
240
|
this.#promise = Promise.resolve();
|
|
223
241
|
}
|
|
224
242
|
|
|
225
|
-
/**
|
|
226
|
-
* @param {unknown} error
|
|
227
|
-
*/
|
|
243
|
+
/** @param {HttpError} error */
|
|
228
244
|
fail(error) {
|
|
229
245
|
// normally consumed in the constructor, but make sure a leftover
|
|
230
246
|
// SSR record can never shadow the newly-set error
|
|
@@ -232,7 +248,7 @@ export class Query {
|
|
|
232
248
|
|
|
233
249
|
this.#clear_pending();
|
|
234
250
|
this.#loading = false;
|
|
235
|
-
this.#error = error;
|
|
251
|
+
this.#error = error.body;
|
|
236
252
|
|
|
237
253
|
const promise = Promise.reject(error);
|
|
238
254
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @import { RemoteQueryFunction } from '@sveltejs/kit' */
|
|
2
2
|
import { app_dir, base } from '$app/paths/internal/client';
|
|
3
|
-
import {
|
|
3
|
+
import { _goto } from '../client.js';
|
|
4
4
|
import { get_remote_request_headers, QUERY_FUNCTION_ID, remote_request } from './shared.svelte.js';
|
|
5
5
|
import { QueryProxy } from './query/proxy.js';
|
|
6
6
|
import { HttpError } from '@sveltejs/kit/internal';
|
|
@@ -50,7 +50,8 @@ export function query_batch(id) {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
if (response.redirect) {
|
|
53
|
-
|
|
53
|
+
// Use internal version to allow redirects to external URLs
|
|
54
|
+
await _goto(response.redirect, {}, 0);
|
|
54
55
|
|
|
55
56
|
// settle all batched promises (with `undefined`, like a redirect
|
|
56
57
|
// from a non-batched query) so that callers don't hang forever
|
|
@@ -71,7 +72,7 @@ export function query_batch(id) {
|
|
|
71
72
|
|
|
72
73
|
for (const { resolve, reject } of resolvers) {
|
|
73
74
|
if (result.type === 'error') {
|
|
74
|
-
reject(new HttpError(result.status, result.error));
|
|
75
|
+
reject(new HttpError(result.error.status, result.error));
|
|
75
76
|
} else {
|
|
76
77
|
resolve(result.data);
|
|
77
78
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { query_responses } from '../../client.js';
|
|
1
|
+
import { query_responses, handle_error } from '../../client.js';
|
|
2
2
|
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
3
3
|
import { noop, once } from '../../../../utils/functions.js';
|
|
4
4
|
import { SharedIterator } from '../../../../utils/shared-iterator.js';
|
|
@@ -24,7 +24,7 @@ export class LiveQuery {
|
|
|
24
24
|
#done = $state(false);
|
|
25
25
|
/** @type {T | undefined} */
|
|
26
26
|
#raw = $state.raw();
|
|
27
|
-
/** @type {
|
|
27
|
+
/** @type {App.Error | undefined} */
|
|
28
28
|
#error = $state.raw(undefined);
|
|
29
29
|
/** @type {Promise<void>} */
|
|
30
30
|
#promise;
|
|
@@ -91,9 +91,9 @@ export class LiveQuery {
|
|
|
91
91
|
// the query failed during SSR — seed the failed state (mirroring `fail()`,
|
|
92
92
|
// minus its terminal `#done`), so the main loop still connects as usual
|
|
93
93
|
// and the query can recover
|
|
94
|
-
const error = new HttpError(node.e
|
|
94
|
+
const error = new HttpError(node.e.status, node.e);
|
|
95
95
|
this.#loading = false;
|
|
96
|
-
this.#error = error;
|
|
96
|
+
this.#error = error.body;
|
|
97
97
|
|
|
98
98
|
promise.catch(noop);
|
|
99
99
|
this.#reject_first?.(error);
|
|
@@ -122,6 +122,7 @@ export class LiveQuery {
|
|
|
122
122
|
|
|
123
123
|
/** @type {PromiseWithResolvers<void>} */
|
|
124
124
|
const { promise: stopped, resolve: on_stop } = Promise.withResolvers();
|
|
125
|
+
let connected = false;
|
|
125
126
|
|
|
126
127
|
while (!this.#done) {
|
|
127
128
|
const controller = new AbortController();
|
|
@@ -134,16 +135,22 @@ export class LiveQuery {
|
|
|
134
135
|
const generator = create_live_iterator(this.#id, this.#payload, controller, () => {
|
|
135
136
|
this.#connected = true;
|
|
136
137
|
this.#attempt = 0;
|
|
138
|
+
connected = true;
|
|
137
139
|
on_connect();
|
|
138
140
|
});
|
|
139
141
|
|
|
140
142
|
try {
|
|
141
143
|
const { done, value } = await generator.next();
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
if (done) {
|
|
146
|
+
if (!this.#ready) {
|
|
147
|
+
throw new Error('Live query completed before yielding a value');
|
|
148
|
+
}
|
|
149
|
+
// stream completed without yielding (e.g. the generator returned
|
|
150
|
+
// immediately on reconnect) — keep the last good value
|
|
151
|
+
this.#done = true;
|
|
152
|
+
this.#fan_out.done();
|
|
153
|
+
break;
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
this.set(value);
|
|
@@ -168,7 +175,7 @@ export class LiveQuery {
|
|
|
168
175
|
|
|
169
176
|
if (!this.#ready) {
|
|
170
177
|
// If we haven't successfully connected and received a value yet, surface the error
|
|
171
|
-
this
|
|
178
|
+
await this.#fail(error);
|
|
172
179
|
on_connect_failed(error);
|
|
173
180
|
break;
|
|
174
181
|
}
|
|
@@ -200,6 +207,12 @@ export class LiveQuery {
|
|
|
200
207
|
}
|
|
201
208
|
|
|
202
209
|
this.#interrupt = null;
|
|
210
|
+
// If the loop exited without ever successfully connecting, settle the
|
|
211
|
+
// reconnect handshake so callers (e.g. `invalidateAll()`) never await
|
|
212
|
+
// a forever-pending promise.
|
|
213
|
+
if (!connected) {
|
|
214
|
+
on_connect_failed(this.#error ?? new Error('Live query connection was interrupted'));
|
|
215
|
+
}
|
|
203
216
|
on_stop();
|
|
204
217
|
}
|
|
205
218
|
|
|
@@ -343,10 +356,13 @@ export class LiveQuery {
|
|
|
343
356
|
promise.catch(noop);
|
|
344
357
|
this.#done = false;
|
|
345
358
|
this.#attempt = 0;
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
359
|
+
// Keep the existing fan-out open so active `for await` consumers
|
|
360
|
+
// continue receiving values from the new connection without interruption.
|
|
361
|
+
// Only replace it if it was already closed by a prior `done()`/`fail()`
|
|
362
|
+
// (e.g. reconnecting after a finite or hard-failed stream)
|
|
363
|
+
if (this.#fan_out.closed) {
|
|
364
|
+
this.#fan_out = new SharedIterator();
|
|
365
|
+
}
|
|
350
366
|
this.#main({ on_connect, on_connect_failed }).catch(noop);
|
|
351
367
|
await promise;
|
|
352
368
|
}
|
|
@@ -369,10 +385,10 @@ export class LiveQuery {
|
|
|
369
385
|
this.#fan_out.push(value);
|
|
370
386
|
}
|
|
371
387
|
|
|
372
|
-
/** @param {
|
|
388
|
+
/** @param {HttpError} error */
|
|
373
389
|
fail(error) {
|
|
374
390
|
this.#loading = false;
|
|
375
|
-
this.#error = error;
|
|
391
|
+
this.#error = error.body;
|
|
376
392
|
// `fail` is terminal — once a live query has hard-failed, the only way to start
|
|
377
393
|
// streaming again is via `reconnect()`. Mark it done and abort any in-flight
|
|
378
394
|
// request so that callers from outside the main loop (e.g. `apply_reconnections`)
|
|
@@ -381,6 +397,7 @@ export class LiveQuery {
|
|
|
381
397
|
void this.#interrupt?.();
|
|
382
398
|
|
|
383
399
|
if (this.#reject_first) {
|
|
400
|
+
this.#promise.catch(noop);
|
|
384
401
|
this.#reject_first(error);
|
|
385
402
|
this.#resolve_first = null;
|
|
386
403
|
this.#reject_first = null;
|
|
@@ -393,6 +410,16 @@ export class LiveQuery {
|
|
|
393
410
|
this.#fan_out.fail(error);
|
|
394
411
|
}
|
|
395
412
|
|
|
413
|
+
/** @param {unknown} e */
|
|
414
|
+
async #fail(e) {
|
|
415
|
+
const error = await handle_error(e, {
|
|
416
|
+
params: {},
|
|
417
|
+
route: { id: null },
|
|
418
|
+
url: new URL(location.href)
|
|
419
|
+
});
|
|
420
|
+
this.fail(new HttpError(error.status, error));
|
|
421
|
+
}
|
|
422
|
+
|
|
396
423
|
get [Symbol.toStringTag]() {
|
|
397
424
|
return 'LiveQuery';
|
|
398
425
|
}
|