@sveltejs/kit 3.0.0-next.4 → 3.0.0-next.6
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 +17 -17
- package/src/core/adapt/builder.js +18 -4
- package/src/core/adapt/index.js +1 -4
- package/src/core/config/index.js +64 -5
- package/src/core/config/options.js +73 -21
- package/src/core/env.js +35 -4
- 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 +23 -5
- package/src/core/sync/write_client_manifest.js +7 -0
- package/src/core/sync/write_server.js +13 -10
- package/src/core/sync/write_tsconfig.js +2 -4
- package/src/exports/index.js +32 -10
- package/src/exports/internal/env.js +1 -1
- package/src/exports/internal/index.js +6 -5
- package/src/exports/node/index.js +57 -6
- package/src/exports/public.d.ts +218 -114
- package/src/exports/vite/build/build_server.js +6 -1
- package/src/exports/vite/dev/index.js +10 -20
- package/src/exports/vite/index.js +306 -222
- package/src/exports/vite/preview/index.js +15 -7
- package/src/exports/vite/utils.js +8 -10
- 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 +1 -3
- package/src/runtime/app/paths/public.d.ts +0 -28
- package/src/runtime/app/paths/server.js +7 -3
- package/src/runtime/app/server/remote/form.js +10 -3
- package/src/runtime/app/server/remote/query.js +3 -6
- package/src/runtime/app/server/remote/requested.js +8 -4
- package/src/runtime/app/server/remote/shared.js +5 -7
- package/src/runtime/client/client.js +184 -93
- package/src/runtime/client/fetcher.js +3 -2
- package/src/runtime/client/remote-functions/form.svelte.js +134 -24
- package/src/runtime/client/remote-functions/prerender.svelte.js +8 -2
- package/src/runtime/client/remote-functions/query/index.js +3 -2
- package/src/runtime/client/remote-functions/query/instance.svelte.js +9 -2
- package/src/runtime/client/remote-functions/query-batch.svelte.js +4 -3
- package/src/runtime/client/remote-functions/query-live/instance.svelte.js +26 -9
- package/src/runtime/client/remote-functions/shared.svelte.js +17 -7
- package/src/runtime/client/types.d.ts +9 -1
- package/src/runtime/client/utils.js +1 -1
- package/src/runtime/form-utils.js +84 -16
- 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 +1 -1
- package/src/runtime/server/page/actions.js +41 -26
- package/src/runtime/server/page/index.js +2 -1
- package/src/runtime/server/page/render.js +21 -23
- package/src/runtime/server/page/respond_with_error.js +7 -8
- package/src/runtime/server/remote.js +64 -27
- package/src/runtime/server/respond.js +81 -50
- package/src/runtime/server/utils.js +7 -7
- package/src/runtime/telemetry/otel.js +1 -1
- package/src/types/ambient.d.ts +5 -4
- package/src/types/global-private.d.ts +4 -4
- package/src/types/internal.d.ts +8 -10
- package/src/types/private.d.ts +33 -1
- package/src/types/synthetic/$lib.md +1 -1
- package/src/utils/error.js +11 -3
- package/src/utils/shared-iterator.js +5 -0
- package/src/utils/url.js +99 -2
- package/src/version.js +1 -1
- package/types/index.d.ts +340 -186
- package/types/index.d.ts.map +10 -9
|
@@ -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
|
|
|
@@ -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,7 +23,8 @@ import {
|
|
|
17
23
|
build_path_string,
|
|
18
24
|
normalize_issue,
|
|
19
25
|
serialize_binary_form,
|
|
20
|
-
BINARY_FORM_CONTENT_TYPE
|
|
26
|
+
BINARY_FORM_CONTENT_TYPE,
|
|
27
|
+
deep_get
|
|
21
28
|
} from '../../form-utils.js';
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -52,6 +59,9 @@ export function form(id) {
|
|
|
52
59
|
/** @type {Map<any, { count: number, instance: RemoteForm<T, U> }>} */
|
|
53
60
|
const instances = new Map();
|
|
54
61
|
|
|
62
|
+
/** @type {StandardSchemaV1 | null} */
|
|
63
|
+
let shared_preflight_schema = null;
|
|
64
|
+
|
|
55
65
|
/** @param {string | number | boolean} [key] */
|
|
56
66
|
function create_instance(key) {
|
|
57
67
|
const action_id_without_key = id;
|
|
@@ -89,7 +99,9 @@ export function form(id) {
|
|
|
89
99
|
*/
|
|
90
100
|
let enhance_callback = async (instance) => {
|
|
91
101
|
if (await instance.submit()) {
|
|
92
|
-
|
|
102
|
+
await tick();
|
|
103
|
+
// We call reset from the prototype to avoid DOM clobbering
|
|
104
|
+
HTMLFormElement.prototype.reset.call(instance.element);
|
|
93
105
|
}
|
|
94
106
|
};
|
|
95
107
|
|
|
@@ -97,13 +109,22 @@ export function form(id) {
|
|
|
97
109
|
let element = null;
|
|
98
110
|
|
|
99
111
|
/** @type {Record<string, boolean>} */
|
|
100
|
-
let touched = {};
|
|
112
|
+
let touched = $state({});
|
|
113
|
+
|
|
114
|
+
/** @type {Record<string, boolean>} */
|
|
115
|
+
let dirty = $state({});
|
|
116
|
+
|
|
117
|
+
/** @type {Record<string, boolean>} */
|
|
118
|
+
let can_validate = {};
|
|
101
119
|
|
|
102
|
-
let submitted = false;
|
|
120
|
+
let submitted = $state(false);
|
|
103
121
|
|
|
104
122
|
/** @type {InternalRemoteFormIssue[] | null} */
|
|
105
123
|
let unread_issues = null;
|
|
106
124
|
|
|
125
|
+
/** @type {string | null} */
|
|
126
|
+
let previous_submitter_name = null;
|
|
127
|
+
|
|
107
128
|
/**
|
|
108
129
|
* In dev, warn if there are validation issues going unread
|
|
109
130
|
*/
|
|
@@ -302,7 +323,8 @@ export function form(id) {
|
|
|
302
323
|
*/
|
|
303
324
|
async function preflight(form_data) {
|
|
304
325
|
const data = convert(form_data);
|
|
305
|
-
const
|
|
326
|
+
const schema = preflight_schema ?? shared_preflight_schema;
|
|
327
|
+
const validated = await schema?.['~standard'].validate(data);
|
|
306
328
|
|
|
307
329
|
if (validated?.issues) {
|
|
308
330
|
raw_issues = merge_with_server_issues(
|
|
@@ -346,6 +368,8 @@ export function form(id) {
|
|
|
346
368
|
element = form;
|
|
347
369
|
|
|
348
370
|
touched = {};
|
|
371
|
+
dirty = {};
|
|
372
|
+
can_validate = {};
|
|
349
373
|
|
|
350
374
|
/** @param {SubmitEvent} event */
|
|
351
375
|
const handle_submit = async (event) => {
|
|
@@ -379,6 +403,29 @@ export function form(id) {
|
|
|
379
403
|
|
|
380
404
|
const form_data = new FormData(form, event.submitter);
|
|
381
405
|
|
|
406
|
+
if (
|
|
407
|
+
previous_submitter_name !== null &&
|
|
408
|
+
!Array.from(form_data.keys()).map(strip_prefix).includes(previous_submitter_name)
|
|
409
|
+
) {
|
|
410
|
+
// Strip any `n:`/`b:` type prefix before clearing, otherwise
|
|
411
|
+
// `set_nested_value` would coerce `undefined` to `NaN`/`false`
|
|
412
|
+
// instead of clearing the previously-submitted value.
|
|
413
|
+
set_nested_value(input, previous_submitter_name, undefined);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (event.submitter) {
|
|
417
|
+
const name = event.submitter.getAttribute('name');
|
|
418
|
+
const value = /** @type {any} */ (event.submitter).value;
|
|
419
|
+
|
|
420
|
+
if (name !== null && value !== undefined) {
|
|
421
|
+
set_nested_value(input, name, value);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
previous_submitter_name = strip_prefix(name);
|
|
425
|
+
} else {
|
|
426
|
+
previous_submitter_name = null;
|
|
427
|
+
}
|
|
428
|
+
|
|
382
429
|
if (DEV) {
|
|
383
430
|
validate_form_data(form_data, clone(form).enctype);
|
|
384
431
|
}
|
|
@@ -396,9 +443,14 @@ export function form(id) {
|
|
|
396
443
|
await enhance_callback(create_enhance_callback_instance(form, form_data));
|
|
397
444
|
} catch (e) {
|
|
398
445
|
const error =
|
|
399
|
-
e instanceof HttpError
|
|
400
|
-
|
|
401
|
-
|
|
446
|
+
e instanceof HttpError
|
|
447
|
+
? e.body
|
|
448
|
+
: await handle_error(e, {
|
|
449
|
+
params: {},
|
|
450
|
+
route: { id: null },
|
|
451
|
+
url: new URL(location.href)
|
|
452
|
+
});
|
|
453
|
+
void set_nearest_error_page(error);
|
|
402
454
|
} finally {
|
|
403
455
|
pending_count--;
|
|
404
456
|
}
|
|
@@ -418,8 +470,6 @@ export function form(id) {
|
|
|
418
470
|
|
|
419
471
|
const is_file = element.type === 'file';
|
|
420
472
|
|
|
421
|
-
touched[name] = true;
|
|
422
|
-
|
|
423
473
|
if (is_array) {
|
|
424
474
|
let value;
|
|
425
475
|
|
|
@@ -481,9 +531,9 @@ export function form(id) {
|
|
|
481
531
|
);
|
|
482
532
|
}
|
|
483
533
|
|
|
484
|
-
name = name
|
|
534
|
+
name = strip_prefix(name);
|
|
485
535
|
|
|
486
|
-
|
|
536
|
+
dirty[name] = true;
|
|
487
537
|
};
|
|
488
538
|
|
|
489
539
|
const handle_reset = async () => {
|
|
@@ -492,18 +542,38 @@ export function form(id) {
|
|
|
492
542
|
await tick();
|
|
493
543
|
|
|
494
544
|
input = convert_formdata(new FormData(form));
|
|
545
|
+
raw_issues = [];
|
|
546
|
+
touched = {};
|
|
547
|
+
dirty = {};
|
|
548
|
+
can_validate = {};
|
|
549
|
+
submitted = false;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
/** @param {Event} e */
|
|
553
|
+
const handle_focusout = (e) => {
|
|
554
|
+
let name = /** @type {HTMLInputElement} */ (e.target).name;
|
|
555
|
+
if (!name) return;
|
|
556
|
+
|
|
557
|
+
name = strip_prefix(name).replace(/\[\]$/, '');
|
|
558
|
+
|
|
559
|
+
touched[name] = true;
|
|
560
|
+
|
|
561
|
+
if (Object.hasOwn(dirty, name)) {
|
|
562
|
+
can_validate[name] = true;
|
|
563
|
+
}
|
|
495
564
|
};
|
|
496
565
|
|
|
497
566
|
form.addEventListener('submit', handle_submit);
|
|
498
567
|
form.addEventListener('input', handle_input);
|
|
568
|
+
form.addEventListener('focusout', handle_focusout);
|
|
499
569
|
form.addEventListener('reset', handle_reset);
|
|
500
570
|
|
|
501
571
|
return () => {
|
|
502
572
|
form.removeEventListener('submit', handle_submit);
|
|
503
573
|
form.removeEventListener('input', handle_input);
|
|
574
|
+
form.removeEventListener('focusout', handle_focusout);
|
|
504
575
|
form.removeEventListener('reset', handle_reset);
|
|
505
576
|
element = null;
|
|
506
|
-
preflight_schema = undefined;
|
|
507
577
|
};
|
|
508
578
|
};
|
|
509
579
|
|
|
@@ -534,9 +604,10 @@ export function form(id) {
|
|
|
534
604
|
|
|
535
605
|
const submission = submit(form_data, true);
|
|
536
606
|
|
|
537
|
-
|
|
607
|
+
const decrement = () => {
|
|
538
608
|
pending_count--;
|
|
539
|
-
}
|
|
609
|
+
};
|
|
610
|
+
void submission.then(decrement, decrement);
|
|
540
611
|
|
|
541
612
|
return submission;
|
|
542
613
|
}
|
|
@@ -549,11 +620,16 @@ export function form(id) {
|
|
|
549
620
|
(path, value) => {
|
|
550
621
|
if (path.length === 0) {
|
|
551
622
|
input = value;
|
|
552
|
-
} else {
|
|
623
|
+
} else if (value !== deep_get(input, path)) {
|
|
553
624
|
deep_set(input, path.map(String), value);
|
|
554
625
|
|
|
555
626
|
const key = build_path_string(path);
|
|
556
|
-
|
|
627
|
+
|
|
628
|
+
if (element) {
|
|
629
|
+
touched[key] = true;
|
|
630
|
+
dirty[key] = true;
|
|
631
|
+
can_validate[key] = true;
|
|
632
|
+
}
|
|
557
633
|
}
|
|
558
634
|
},
|
|
559
635
|
(path, all) => {
|
|
@@ -567,7 +643,10 @@ export function form(id) {
|
|
|
567
643
|
}
|
|
568
644
|
|
|
569
645
|
return issues;
|
|
570
|
-
}
|
|
646
|
+
},
|
|
647
|
+
() => touched,
|
|
648
|
+
() => dirty,
|
|
649
|
+
[]
|
|
571
650
|
)
|
|
572
651
|
},
|
|
573
652
|
result: {
|
|
@@ -576,16 +655,29 @@ export function form(id) {
|
|
|
576
655
|
pending: {
|
|
577
656
|
get: () => pending_count
|
|
578
657
|
},
|
|
658
|
+
submitted: {
|
|
659
|
+
get: () => submitted
|
|
660
|
+
},
|
|
579
661
|
preflight: {
|
|
580
662
|
/** @type {RemoteForm<T, U>['preflight']} */
|
|
581
663
|
value: (schema) => {
|
|
582
664
|
preflight_schema = schema;
|
|
665
|
+
|
|
666
|
+
if (key === undefined) {
|
|
667
|
+
shared_preflight_schema = schema;
|
|
668
|
+
}
|
|
669
|
+
|
|
583
670
|
return instance;
|
|
584
671
|
}
|
|
585
672
|
},
|
|
586
673
|
validate: {
|
|
587
674
|
/** @type {RemoteForm<any, any>['validate']} */
|
|
588
|
-
value: async ({
|
|
675
|
+
value: async ({
|
|
676
|
+
all = false,
|
|
677
|
+
preflightOnly = false,
|
|
678
|
+
// @ts-expect-error TODO remove this in 3.0
|
|
679
|
+
includeUntouched
|
|
680
|
+
} = {}) => {
|
|
589
681
|
if (!element) return;
|
|
590
682
|
|
|
591
683
|
const id = ++validate_id;
|
|
@@ -603,8 +695,8 @@ export function form(id) {
|
|
|
603
695
|
let array = [];
|
|
604
696
|
|
|
605
697
|
const data = convert(form_data);
|
|
606
|
-
|
|
607
|
-
const validated = await
|
|
698
|
+
const schema = preflight_schema ?? shared_preflight_schema;
|
|
699
|
+
const validated = await schema?.['~standard'].validate(data);
|
|
608
700
|
|
|
609
701
|
if (validate_id !== id) {
|
|
610
702
|
return;
|
|
@@ -636,8 +728,16 @@ export function form(id) {
|
|
|
636
728
|
array = /** @type {InternalRemoteFormIssue[]} */ (result._);
|
|
637
729
|
}
|
|
638
730
|
|
|
639
|
-
if (
|
|
640
|
-
|
|
731
|
+
if (includeUntouched !== undefined) {
|
|
732
|
+
console.warn(
|
|
733
|
+
`\`{ includeUntouched: ${includeUntouched} }\` has been replaced with \`{ all: ${includeUntouched} }\``
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
all = includeUntouched;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (!all && !submitted) {
|
|
740
|
+
array = array.filter((issue) => can_validate[issue.name]);
|
|
641
741
|
}
|
|
642
742
|
|
|
643
743
|
const is_server_validation = !validated?.issues && !preflightOnly;
|
|
@@ -728,3 +828,13 @@ function validate_form_data(form_data, enctype) {
|
|
|
728
828
|
}
|
|
729
829
|
}
|
|
730
830
|
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Remove the `n:` or `b:` prefix from a field name
|
|
834
|
+
* @template {string | null} T
|
|
835
|
+
* @param {T} name
|
|
836
|
+
* @returns {T}
|
|
837
|
+
*/
|
|
838
|
+
function strip_prefix(name) {
|
|
839
|
+
return /** @type {T} */ (name && name.replace(/^[nb]:/, ''));
|
|
840
|
+
}
|
|
@@ -2,9 +2,10 @@
|
|
|
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, 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';
|
|
8
9
|
|
|
9
10
|
// Initialize Cache API for prerender functions
|
|
10
11
|
const CACHE_NAME = __SVELTEKIT_DEV__ ? `sveltekit:${Date.now()}` : `sveltekit:${version}`;
|
|
@@ -97,7 +98,8 @@ export function prerender(id) {
|
|
|
97
98
|
const result = await remote_request(url, { headers });
|
|
98
99
|
|
|
99
100
|
if (result.redirect) {
|
|
100
|
-
|
|
101
|
+
// Use internal version to allow redirects to external URLs
|
|
102
|
+
void _goto(result.redirect, {}, 0);
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
105
|
|
|
@@ -166,6 +168,10 @@ class Prerender {
|
|
|
166
168
|
throw error;
|
|
167
169
|
}
|
|
168
170
|
);
|
|
171
|
+
|
|
172
|
+
// rejections are surfaced via `.error` for reactive consumers — make sure the
|
|
173
|
+
// stored promise (consumed without `await`) never becomes an unhandled rejection
|
|
174
|
+
this.#promise.catch(noop);
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
/**
|
|
@@ -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
|
};
|
|
@@ -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())
|
|
@@ -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
|
}
|
|
@@ -91,7 +91,7 @@ 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
96
|
this.#error = error;
|
|
97
97
|
|
|
@@ -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);
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/** @import { RemoteQueryUpdate } from '@sveltejs/kit' */
|
|
3
3
|
/** @import { CacheEntry } from './cache.svelte.js' */
|
|
4
4
|
import * as devalue from 'devalue';
|
|
5
|
-
import { app,
|
|
5
|
+
import { app, _goto, live_query_map, query_map, query_responses } from '../client.js';
|
|
6
6
|
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
7
7
|
import { untrack } from 'svelte';
|
|
8
8
|
import { create_remote_key, split_remote_key } from '../../shared.js';
|
|
@@ -86,7 +86,7 @@ export function pin_while_resolving(cache_map, cache, id, payload, then) {
|
|
|
86
86
|
*/
|
|
87
87
|
export function unwrap_node(node) {
|
|
88
88
|
if (node.e) {
|
|
89
|
-
throw new HttpError(node.e
|
|
89
|
+
throw new HttpError(node.e.status, node.e);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
return node.v;
|
|
@@ -114,13 +114,22 @@ export async function remote_request(url, init) {
|
|
|
114
114
|
const response = await fetch(url, init);
|
|
115
115
|
|
|
116
116
|
if (!response.ok) {
|
|
117
|
-
|
|
117
|
+
const result = await response.json().catch(() => ({
|
|
118
|
+
type: 'error',
|
|
119
|
+
status: response.status,
|
|
120
|
+
error: response.statusText
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
throw new HttpError(
|
|
124
|
+
result.error?.status ?? result.status ?? response.status ?? 500,
|
|
125
|
+
result.error
|
|
126
|
+
);
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
|
|
121
130
|
|
|
122
131
|
if (result.type === 'error') {
|
|
123
|
-
throw new HttpError(result.status
|
|
132
|
+
throw new HttpError(result.error.status, result.error);
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
const data = /** @type {RemoteFunctionData} */ (
|
|
@@ -136,7 +145,7 @@ export async function remote_request(url, init) {
|
|
|
136
145
|
function refresh(key, entry, result) {
|
|
137
146
|
if (entry?.resource) {
|
|
138
147
|
if (result.e) {
|
|
139
|
-
entry.resource.fail(new HttpError(result.e
|
|
148
|
+
entry.resource.fail(new HttpError(result.e.status, result.e));
|
|
140
149
|
} else {
|
|
141
150
|
entry.resource.set(result.v);
|
|
142
151
|
}
|
|
@@ -184,12 +193,13 @@ export async function remote_request(url, init) {
|
|
|
184
193
|
*/
|
|
185
194
|
export async function handle_side_channel_response(response) {
|
|
186
195
|
if (response.type === 'redirect') {
|
|
187
|
-
|
|
196
|
+
// Use internal version to allow redirects to external URLs
|
|
197
|
+
await _goto(response.location, {}, 0);
|
|
188
198
|
throw new Redirect(307, response.location);
|
|
189
199
|
}
|
|
190
200
|
|
|
191
201
|
if (response.type === 'error') {
|
|
192
|
-
throw new HttpError(response.status
|
|
202
|
+
throw new HttpError(response.error.status, response.error);
|
|
193
203
|
}
|
|
194
204
|
|
|
195
205
|
return response;
|
|
@@ -58,6 +58,12 @@ export interface SvelteKitApp {
|
|
|
58
58
|
hash: boolean;
|
|
59
59
|
|
|
60
60
|
root: typeof SvelteComponent;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Lazily loads the contents of src/error.html, used as a last-resort
|
|
64
|
+
* error page when the root layout's load function throws during client-side rendering.
|
|
65
|
+
*/
|
|
66
|
+
get_error_template: () => Promise<(data: { status: number; message: string }) => string>;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export type NavigationIntent = {
|
|
@@ -77,6 +83,7 @@ export type NavigationResult = NavigationRedirect | NavigationFinished;
|
|
|
77
83
|
|
|
78
84
|
export type NavigationRedirect = {
|
|
79
85
|
type: 'redirect';
|
|
86
|
+
status: number;
|
|
80
87
|
location: string;
|
|
81
88
|
};
|
|
82
89
|
|
|
@@ -119,7 +126,8 @@ export interface NavigationState {
|
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
export interface HydrateOptions {
|
|
122
|
-
|
|
129
|
+
/** Provided in the case of a form action that returns `fail`, but otherwise derived from `error` */
|
|
130
|
+
status?: number;
|
|
123
131
|
error: App.Error | null;
|
|
124
132
|
node_ids: number[];
|
|
125
133
|
params: Record<string, string>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BROWSER, DEV } from 'esm-env';
|
|
2
2
|
import { writable } from 'svelte/store';
|
|
3
|
-
import { assets } from '$app/paths';
|
|
3
|
+
import { assets } from '$app/paths/internal/client';
|
|
4
4
|
import { version } from '$app/env';
|
|
5
5
|
import { noop } from '../../utils/functions.js';
|
|
6
6
|
import { PRELOAD_PRIORITIES } from './constants.js';
|