@sveltejs/kit 3.0.0-next.16 → 3.0.0-next.17
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/core/sync/write_types/index.js +1 -0
- package/src/exports/hooks/sequence.js +2 -1
- package/src/exports/internal/server/index.js +2 -0
- package/src/exports/internal/server/telemetry.js +95 -0
- package/src/exports/public.d.ts +17 -6
- package/src/runtime/app/forms.js +51 -39
- package/src/runtime/app/navigation.js +1 -0
- package/src/runtime/client/client.js +203 -49
- package/src/runtime/client/constants.js +1 -0
- package/src/runtime/client/snapshots.js +147 -0
- package/src/runtime/props.svelte.js +1 -0
- package/src/runtime/server/index.js +3 -0
- package/src/runtime/server/page/actions.js +34 -6
- package/src/runtime/server/page/load_data.js +1 -2
- package/src/runtime/server/remote-functions.js +7 -4
- package/src/runtime/server/respond.js +6 -3
- package/src/runtime/server/state.js +0 -4
- package/src/types/internal.d.ts +0 -3
- package/src/utils/import.js +1 -0
- package/src/version.js +1 -1
- package/types/index.d.ts +42 -14
- package/types/index.d.ts.map +4 -1
- package/src/runtime/telemetry/otel.js +0 -21
- package/src/runtime/telemetry/record_span.js +0 -65
|
@@ -29,6 +29,14 @@ import {
|
|
|
29
29
|
PRELOAD_PRIORITIES,
|
|
30
30
|
SNAPSHOT_KEY
|
|
31
31
|
} from './constants.js';
|
|
32
|
+
import {
|
|
33
|
+
capture_navigation_snapshot,
|
|
34
|
+
current_registrations,
|
|
35
|
+
delete_navigation_snapshot,
|
|
36
|
+
init_snapshots,
|
|
37
|
+
persist_navigation_snapshots,
|
|
38
|
+
restore_navigation_snapshot
|
|
39
|
+
} from './snapshots.js';
|
|
32
40
|
import { validate_page_exports } from '../../utils/exports.js';
|
|
33
41
|
import { noop } from '../../utils/functions.js';
|
|
34
42
|
import {
|
|
@@ -91,9 +99,12 @@ const history_info = storage.get(HISTORY_INFO_KEY) ?? {};
|
|
|
91
99
|
/**
|
|
92
100
|
* navigation index -> any
|
|
93
101
|
* @type {Record<string, any[]>}
|
|
102
|
+
* @deprecated TODO 4.0 get rid of this
|
|
94
103
|
*/
|
|
95
104
|
const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
|
|
96
105
|
|
|
106
|
+
init_snapshots(() => (started && !is_navigating && !updating ? current_history_index : null));
|
|
107
|
+
|
|
97
108
|
/** @type {Props} */
|
|
98
109
|
let props;
|
|
99
110
|
|
|
@@ -203,9 +214,12 @@ function reset_scroll_and_focus(url, scroll, reset, active_element) {
|
|
|
203
214
|
function clear_onward_history(current_history_index, current_navigation_index) {
|
|
204
215
|
// if we navigated back, then pushed a new state, we can
|
|
205
216
|
// release memory by pruning the scroll/snapshot lookup
|
|
217
|
+
// the new entry reuses the first abandoned index, so its slot must start clean
|
|
218
|
+
delete_navigation_snapshot(current_history_index);
|
|
206
219
|
let i = current_history_index + 1;
|
|
207
220
|
while (history_info[i]) {
|
|
208
221
|
delete history_info[i];
|
|
222
|
+
delete_navigation_snapshot(i);
|
|
209
223
|
i += 1;
|
|
210
224
|
}
|
|
211
225
|
|
|
@@ -673,14 +687,28 @@ function reset_invalidation() {
|
|
|
673
687
|
force_invalidation = false;
|
|
674
688
|
}
|
|
675
689
|
|
|
676
|
-
|
|
690
|
+
let warned_snapshot_export = false;
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* @param {number} index
|
|
694
|
+
* @deprecated TODO 4.0 get rid of this
|
|
695
|
+
*/
|
|
677
696
|
function capture_snapshot(index) {
|
|
678
697
|
if (props.components.some((c) => c?.snapshot)) {
|
|
698
|
+
if (DEV && !warned_snapshot_export) {
|
|
699
|
+
warned_snapshot_export = true;
|
|
700
|
+
console.warn(
|
|
701
|
+
'`export const snapshot` is deprecated. Use the `snapshot` helper from `$app/navigation` instead.'
|
|
702
|
+
);
|
|
703
|
+
}
|
|
679
704
|
snapshots[index] = props.components.map((c) => c?.snapshot?.capture());
|
|
680
705
|
}
|
|
681
706
|
}
|
|
682
707
|
|
|
683
|
-
/**
|
|
708
|
+
/**
|
|
709
|
+
* @param {number} index
|
|
710
|
+
* @deprecated TODO 4.0 get rid of this
|
|
711
|
+
*/
|
|
684
712
|
function restore_snapshot(index) {
|
|
685
713
|
snapshots[index]?.forEach((value, i) => {
|
|
686
714
|
props.components[i]?.snapshot?.restore(value);
|
|
@@ -693,11 +721,13 @@ function persist_state() {
|
|
|
693
721
|
|
|
694
722
|
capture_snapshot(current_navigation_index);
|
|
695
723
|
storage.set(SNAPSHOT_KEY, snapshots);
|
|
724
|
+
|
|
725
|
+
persist_navigation_snapshots(current_history_index);
|
|
696
726
|
}
|
|
697
727
|
|
|
698
728
|
/**
|
|
699
729
|
* @param {string | URL} url
|
|
700
|
-
* @param {{ type?: import('@sveltejs/kit').NavigationType; replace?: boolean; reset?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any>; persistState?: boolean; event?: Event }} [options]
|
|
730
|
+
* @param {{ type?: import('@sveltejs/kit').NavigationType; replace?: boolean; reset?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any>; persistState?: boolean; event?: Event; action_result?: import('@sveltejs/kit').ActionResult }} [options]
|
|
701
731
|
* @param {number} [redirect_count]
|
|
702
732
|
* @param {{}} [nav_token]
|
|
703
733
|
* @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
|
|
@@ -723,6 +753,7 @@ export async function _goto(url, options = {}, redirect_count = 0, nav_token = {
|
|
|
723
753
|
state: options.state,
|
|
724
754
|
persist_state: options.persistState,
|
|
725
755
|
event: options.event,
|
|
756
|
+
action_result: options.action_result,
|
|
726
757
|
redirect_count,
|
|
727
758
|
nav_token,
|
|
728
759
|
intent,
|
|
@@ -947,6 +978,7 @@ async function initialize(result, target, should_hydrate) {
|
|
|
947
978
|
}
|
|
948
979
|
|
|
949
980
|
restore_snapshot(current_navigation_index);
|
|
981
|
+
restore_navigation_snapshot(current_history_index);
|
|
950
982
|
|
|
951
983
|
started = true;
|
|
952
984
|
}
|
|
@@ -1366,7 +1398,7 @@ function diff_search_params(old_url, new_url) {
|
|
|
1366
1398
|
|
|
1367
1399
|
/**
|
|
1368
1400
|
* @overload
|
|
1369
|
-
* @param {import('./types.js').NavigationIntent} intent
|
|
1401
|
+
* @param {import('./types.js').NavigationIntent & { action_result?: import('@sveltejs/kit').ActionResult }} intent
|
|
1370
1402
|
* @returns {Promise<import('./types.js').NavigationResult | undefined>}
|
|
1371
1403
|
*/
|
|
1372
1404
|
/**
|
|
@@ -1375,11 +1407,11 @@ function diff_search_params(old_url, new_url) {
|
|
|
1375
1407
|
* @returns {Promise<import('./types.js').NavigationResult>}
|
|
1376
1408
|
*/
|
|
1377
1409
|
/**
|
|
1378
|
-
* @param {import('./types.js').NavigationIntent & { preload?: {} }} intent
|
|
1410
|
+
* @param {import('./types.js').NavigationIntent & { preload?: {}; action_result?: import('@sveltejs/kit').ActionResult }} intent
|
|
1379
1411
|
* @returns {Promise<import('./types.js').NavigationResult | undefined>}
|
|
1380
1412
|
*/
|
|
1381
|
-
async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
1382
|
-
if (load_cache?.id === id) {
|
|
1413
|
+
async function load_route({ id, invalidating, url, params, route, preload, action_result }) {
|
|
1414
|
+
if (!action_result && load_cache?.id === id) {
|
|
1383
1415
|
// the preload becomes the real navigation
|
|
1384
1416
|
preload_tokens.delete(load_cache.token);
|
|
1385
1417
|
return load_cache.promise;
|
|
@@ -1388,6 +1420,8 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1388
1420
|
const { errors, layouts, leaf } = route;
|
|
1389
1421
|
|
|
1390
1422
|
const loaders = [...layouts, leaf];
|
|
1423
|
+
const leaf_index = loaders.length - 1;
|
|
1424
|
+
const action_error = action_result?.type === 'error';
|
|
1391
1425
|
|
|
1392
1426
|
// preload modules to avoid waterfall, but handle rejections
|
|
1393
1427
|
// so they don't get reported to Sentry et al (we don't need
|
|
@@ -1406,6 +1440,8 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1406
1440
|
|
|
1407
1441
|
if (__SVELTEKIT_HAS_SERVER_LOAD__) {
|
|
1408
1442
|
const invalid_server_nodes = loaders.map((loader, i) => {
|
|
1443
|
+
if (action_error && i === leaf_index) return false;
|
|
1444
|
+
|
|
1409
1445
|
const previous = current.branch[i];
|
|
1410
1446
|
|
|
1411
1447
|
const invalid =
|
|
@@ -1456,7 +1492,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1456
1492
|
let parent_changed = false;
|
|
1457
1493
|
|
|
1458
1494
|
const branch_promises = loaders.map(async (loader, i) => {
|
|
1459
|
-
if (!loader) return;
|
|
1495
|
+
if (!loader || (action_error && i === leaf_index)) return;
|
|
1460
1496
|
|
|
1461
1497
|
/** @type {import('./types.js').BranchNode | undefined} */
|
|
1462
1498
|
const previous = current.branch[i];
|
|
@@ -1548,19 +1584,15 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1548
1584
|
error = await handle_error(err, { params, url, route: { id: route.id } });
|
|
1549
1585
|
}
|
|
1550
1586
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
});
|
|
1561
|
-
} else {
|
|
1562
|
-
return await server_fallback(url, { id: route.id }, error);
|
|
1563
|
-
}
|
|
1587
|
+
return load_route_error({
|
|
1588
|
+
i,
|
|
1589
|
+
branch,
|
|
1590
|
+
errors,
|
|
1591
|
+
error,
|
|
1592
|
+
url,
|
|
1593
|
+
params,
|
|
1594
|
+
route
|
|
1595
|
+
});
|
|
1564
1596
|
}
|
|
1565
1597
|
} else {
|
|
1566
1598
|
// push an empty slot so we can rewind past gaps to the
|
|
@@ -1569,6 +1601,21 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1569
1601
|
}
|
|
1570
1602
|
}
|
|
1571
1603
|
|
|
1604
|
+
if (action_result?.type === 'error') {
|
|
1605
|
+
// render the nearest error boundary of the route the action belongs to,
|
|
1606
|
+
// as a native form submission would
|
|
1607
|
+
return load_route_error({
|
|
1608
|
+
i: loaders.length,
|
|
1609
|
+
branch,
|
|
1610
|
+
errors,
|
|
1611
|
+
error: action_result.error,
|
|
1612
|
+
status: action_result.status,
|
|
1613
|
+
url,
|
|
1614
|
+
params,
|
|
1615
|
+
route
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1572
1619
|
return get_navigation_result_from_branch({
|
|
1573
1620
|
url,
|
|
1574
1621
|
params,
|
|
@@ -1576,8 +1623,15 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1576
1623
|
errors,
|
|
1577
1624
|
error: null,
|
|
1578
1625
|
route,
|
|
1579
|
-
|
|
1580
|
-
|
|
1626
|
+
status: action_result?.status,
|
|
1627
|
+
// `error` results returned above, `redirect` results are never passed here
|
|
1628
|
+
form:
|
|
1629
|
+
action_result?.type === 'success' || action_result?.type === 'failure'
|
|
1630
|
+
? (action_result.data ?? null)
|
|
1631
|
+
: // Reset `form` on navigation, but not invalidation
|
|
1632
|
+
invalidating
|
|
1633
|
+
? undefined
|
|
1634
|
+
: null
|
|
1581
1635
|
});
|
|
1582
1636
|
}
|
|
1583
1637
|
|
|
@@ -1610,6 +1664,35 @@ async function load_nearest_error_page(i, branch, errors) {
|
|
|
1610
1664
|
}
|
|
1611
1665
|
}
|
|
1612
1666
|
|
|
1667
|
+
/**
|
|
1668
|
+
* @param {{
|
|
1669
|
+
* i: number;
|
|
1670
|
+
* branch: Array<import('./types.js').BranchNode | undefined>;
|
|
1671
|
+
* errors: Array<import('types').CSRPageNodeLoader | undefined>;
|
|
1672
|
+
* error: App.Error;
|
|
1673
|
+
* status?: number;
|
|
1674
|
+
* url: URL;
|
|
1675
|
+
* params: Record<string, string>;
|
|
1676
|
+
* route: import('./types.js').NavigationIntent['route'];
|
|
1677
|
+
* }} opts
|
|
1678
|
+
*/
|
|
1679
|
+
async function load_route_error({ i, branch, errors, error, status, url, params, route }) {
|
|
1680
|
+
const error_load = await load_nearest_error_page(i, branch, errors);
|
|
1681
|
+
if (error_load) {
|
|
1682
|
+
return get_navigation_result_from_branch({
|
|
1683
|
+
url,
|
|
1684
|
+
params,
|
|
1685
|
+
branch: branch.slice(0, error_load.idx).concat(error_load.node),
|
|
1686
|
+
errors,
|
|
1687
|
+
error,
|
|
1688
|
+
status,
|
|
1689
|
+
route
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
return server_fallback(url, { id: route.id }, error);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1613
1696
|
/**
|
|
1614
1697
|
* @param {{
|
|
1615
1698
|
* error: App.Error;
|
|
@@ -1894,7 +1977,8 @@ function _before_navigate({ url, type, intent, delta, event, scroll, shallow = f
|
|
|
1894
1977
|
* accept?: () => void;
|
|
1895
1978
|
* block?: () => void;
|
|
1896
1979
|
* event?: Event;
|
|
1897
|
-
* intent?: NavigationIntent | undefined
|
|
1980
|
+
* intent?: NavigationIntent | undefined;
|
|
1981
|
+
* action_result?: import('@sveltejs/kit').ActionResult;
|
|
1898
1982
|
* }} opts
|
|
1899
1983
|
* @returns {Promise<void>}
|
|
1900
1984
|
*/
|
|
@@ -1911,7 +1995,8 @@ async function navigate({
|
|
|
1911
1995
|
accept = noop,
|
|
1912
1996
|
block = noop,
|
|
1913
1997
|
event,
|
|
1914
|
-
intent
|
|
1998
|
+
intent,
|
|
1999
|
+
action_result
|
|
1915
2000
|
}) {
|
|
1916
2001
|
const prev_token = navigation_token;
|
|
1917
2002
|
const prev_invalidation_token = invalidation_token;
|
|
@@ -1943,6 +2028,7 @@ async function navigate({
|
|
|
1943
2028
|
// store this before calling `accept()`, which may change the index
|
|
1944
2029
|
const previous_history_index = current_history_index;
|
|
1945
2030
|
const previous_navigation_index = current_navigation_index;
|
|
2031
|
+
const previous_snapshot_registrations = current_registrations();
|
|
1946
2032
|
|
|
1947
2033
|
accept();
|
|
1948
2034
|
|
|
@@ -1952,7 +2038,7 @@ async function navigate({
|
|
|
1952
2038
|
navigating.current = nav.navigation;
|
|
1953
2039
|
}
|
|
1954
2040
|
|
|
1955
|
-
let navigation_result = intent && (await load_route(intent));
|
|
2041
|
+
let navigation_result = intent && (await load_route({ ...intent, action_result }));
|
|
1956
2042
|
|
|
1957
2043
|
if (!navigation_result) {
|
|
1958
2044
|
if (is_external_url(url, base, app.hash)) {
|
|
@@ -2051,6 +2137,11 @@ async function navigate({
|
|
|
2051
2137
|
|
|
2052
2138
|
capture_scroll(previous_history_index);
|
|
2053
2139
|
capture_snapshot(previous_navigation_index);
|
|
2140
|
+
if (replace_state) {
|
|
2141
|
+
delete_navigation_snapshot(previous_history_index);
|
|
2142
|
+
} else {
|
|
2143
|
+
capture_navigation_snapshot(previous_history_index);
|
|
2144
|
+
}
|
|
2054
2145
|
|
|
2055
2146
|
// ensure the url pathname matches the page's trailing slash option
|
|
2056
2147
|
if (navigation_result.props.page.url.pathname !== url.pathname) {
|
|
@@ -2218,6 +2309,8 @@ async function navigate({
|
|
|
2218
2309
|
if (type === 'popstate') {
|
|
2219
2310
|
restore_snapshot(current_navigation_index);
|
|
2220
2311
|
}
|
|
2312
|
+
// new and replaced entries have no stored values, so this only resets there
|
|
2313
|
+
restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);
|
|
2221
2314
|
|
|
2222
2315
|
navigating.current = null;
|
|
2223
2316
|
|
|
@@ -2883,6 +2976,7 @@ export async function replaceState(url, state) {
|
|
|
2883
2976
|
*/
|
|
2884
2977
|
async function update_state(intent, state, { replace, persist_state, reset }, caller) {
|
|
2885
2978
|
const url = intent.url;
|
|
2979
|
+
const previous_snapshot_registrations = current_registrations();
|
|
2886
2980
|
|
|
2887
2981
|
if (DEV && !started) {
|
|
2888
2982
|
throw new Error(`Cannot call ${caller}(...) before router is initialized`);
|
|
@@ -2903,7 +2997,12 @@ async function update_state(intent, state, { replace, persist_state, reset }, ca
|
|
|
2903
2997
|
updating = true;
|
|
2904
2998
|
}
|
|
2905
2999
|
|
|
2906
|
-
if (
|
|
3000
|
+
if (replace) {
|
|
3001
|
+
delete_navigation_snapshot(current_history_index);
|
|
3002
|
+
} else {
|
|
3003
|
+
capture_scroll(current_history_index);
|
|
3004
|
+
capture_navigation_snapshot(current_history_index);
|
|
3005
|
+
}
|
|
2907
3006
|
if (reset) current_reset_index += 1;
|
|
2908
3007
|
|
|
2909
3008
|
// as above, serialize-then-parse to prevent bugs
|
|
@@ -2959,38 +3058,43 @@ async function update_state(intent, state, { replace, persist_state, reset }, ca
|
|
|
2959
3058
|
url
|
|
2960
3059
|
};
|
|
2961
3060
|
|
|
2962
|
-
if (
|
|
3061
|
+
if (nav) {
|
|
3062
|
+
const { activeElement } = document;
|
|
2963
3063
|
|
|
2964
|
-
|
|
3064
|
+
await settled();
|
|
2965
3065
|
|
|
2966
|
-
|
|
3066
|
+
if (navigation_token !== nav_token) {
|
|
3067
|
+
// a new navigation happened while we were waiting for the DOM to update, so abort
|
|
3068
|
+
nav.reject(new Error('navigation aborted'));
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
2967
3071
|
|
|
2968
|
-
|
|
2969
|
-
// a new navigation happened while we were waiting for the DOM to update, so abort
|
|
2970
|
-
nav.reject(new Error('navigation aborted'));
|
|
2971
|
-
return;
|
|
2972
|
-
}
|
|
3072
|
+
reset_scroll_and_focus(url, reset ? null : scroll_state(), reset, activeElement);
|
|
2973
3073
|
|
|
2974
|
-
|
|
3074
|
+
is_navigating = false;
|
|
3075
|
+
nav.fulfil(undefined);
|
|
2975
3076
|
|
|
2976
|
-
|
|
2977
|
-
|
|
3077
|
+
if (nav.navigation.to) {
|
|
3078
|
+
nav.navigation.to.scroll = scroll_state();
|
|
3079
|
+
}
|
|
2978
3080
|
|
|
2979
|
-
|
|
2980
|
-
|
|
3081
|
+
after_navigate_callbacks.forEach((fn) =>
|
|
3082
|
+
fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
|
|
3083
|
+
);
|
|
2981
3084
|
}
|
|
2982
3085
|
|
|
2983
|
-
|
|
2984
|
-
fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
|
|
2985
|
-
);
|
|
3086
|
+
restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);
|
|
2986
3087
|
|
|
2987
|
-
|
|
2988
|
-
|
|
3088
|
+
if (nav) {
|
|
3089
|
+
navigating.current = null;
|
|
3090
|
+
updating = false;
|
|
3091
|
+
}
|
|
2989
3092
|
}
|
|
2990
3093
|
|
|
2991
3094
|
/**
|
|
2992
|
-
*
|
|
2993
|
-
* In case of an error, it
|
|
3095
|
+
* Updates the `form` property of the current page with the given data and updates `page.status`.
|
|
3096
|
+
* In case of an error, it renders the nearest error page. In case of a redirect, it navigates to
|
|
3097
|
+
* the redirect location.
|
|
2994
3098
|
* @template {Record<string, unknown> | undefined} Success
|
|
2995
3099
|
* @template {Record<string, unknown> | undefined} Failure
|
|
2996
3100
|
* @param {import('@sveltejs/kit').ActionResult<Success, Failure>} result
|
|
@@ -3001,10 +3105,13 @@ export async function applyAction(result) {
|
|
|
3001
3105
|
throw new Error('Cannot call applyAction(...) on the server');
|
|
3002
3106
|
}
|
|
3003
3107
|
|
|
3108
|
+
if (result.type === 'redirect') {
|
|
3109
|
+
await _goto(result.location, { refreshAll: true });
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3004
3113
|
if (result.type === 'error') {
|
|
3005
3114
|
await set_nearest_error_page(result.error);
|
|
3006
|
-
} else if (result.type === 'redirect') {
|
|
3007
|
-
await _goto(result.location, { refreshAll: true });
|
|
3008
3115
|
} else {
|
|
3009
3116
|
page.form = result.data;
|
|
3010
3117
|
page.status = result.status;
|
|
@@ -3024,6 +3131,51 @@ export async function applyAction(result) {
|
|
|
3024
3131
|
}
|
|
3025
3132
|
}
|
|
3026
3133
|
|
|
3134
|
+
/**
|
|
3135
|
+
* Whether a submission should update the current page in place rather than navigating — either
|
|
3136
|
+
* because it lands where we already are, or because the result carries no location at all
|
|
3137
|
+
* (hand-constructed results, which have always applied in place).
|
|
3138
|
+
* @param {string | undefined} value
|
|
3139
|
+
*/
|
|
3140
|
+
export function is_current_location(value) {
|
|
3141
|
+
if (value === undefined) return true;
|
|
3142
|
+
|
|
3143
|
+
const destination = resolve_url(value);
|
|
3144
|
+
const current = new URL(location.href);
|
|
3145
|
+
if (destination.pathname !== current.pathname) return false;
|
|
3146
|
+
|
|
3147
|
+
const keys = new Set([...destination.searchParams.keys(), ...current.searchParams.keys()]);
|
|
3148
|
+
for (const key of keys) {
|
|
3149
|
+
const destination_values = destination.searchParams.getAll(key).sort();
|
|
3150
|
+
const current_values = current.searchParams.getAll(key).sort();
|
|
3151
|
+
|
|
3152
|
+
if (
|
|
3153
|
+
destination_values.length !== current_values.length ||
|
|
3154
|
+
destination_values.some((value, i) => value !== current_values[i])
|
|
3155
|
+
) {
|
|
3156
|
+
return false;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
return true;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
/**
|
|
3164
|
+
* Navigate to where a form submission should land, rendering that page with the given result
|
|
3165
|
+
* — what the browser would do for a native submission.
|
|
3166
|
+
* @param {string} location
|
|
3167
|
+
* @param {import('@sveltejs/kit').ActionResult} result
|
|
3168
|
+
* @param {boolean} refresh_all
|
|
3169
|
+
* @returns {Promise<void>}
|
|
3170
|
+
*/
|
|
3171
|
+
export function apply_action_navigation(location, result, refresh_all) {
|
|
3172
|
+
return _goto(location, {
|
|
3173
|
+
type: 'form',
|
|
3174
|
+
refreshAll: refresh_all,
|
|
3175
|
+
action_result: result
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3027
3179
|
/**
|
|
3028
3180
|
* @param {App.Error} error
|
|
3029
3181
|
*/
|
|
@@ -3336,9 +3488,11 @@ function _start_router() {
|
|
|
3336
3488
|
update_url(url);
|
|
3337
3489
|
|
|
3338
3490
|
capture_scroll(current_history_index);
|
|
3491
|
+
capture_navigation_snapshot(current_history_index);
|
|
3339
3492
|
current_history_index = history_index;
|
|
3340
3493
|
current_reset_index = reset_index;
|
|
3341
3494
|
if (reset && scroll) scrollTo(scroll.x, scroll.y);
|
|
3495
|
+
restore_navigation_snapshot(current_history_index, current_registrations());
|
|
3342
3496
|
return;
|
|
3343
3497
|
}
|
|
3344
3498
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { BROWSER, DEV } from 'esm-env';
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import * as storage from './session-storage.js';
|
|
4
|
+
import { NAVIGATION_SNAPSHOT_KEY } from './constants.js';
|
|
5
|
+
import { hash } from '../../utils/hash.js';
|
|
6
|
+
import { parse, stringify } from '#app/internal/transport';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{ id: string; capture: () => any; restore: (value: any) => void; reset?: () => void }} SnapshotRegistration
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Function snapshots use history indexes so shallow entries have distinct state.
|
|
13
|
+
/** @type {Record<string, Record<string, string>>} */
|
|
14
|
+
const navigation_snapshots = storage.get(NAVIGATION_SNAPSHOT_KEY) ?? {};
|
|
15
|
+
|
|
16
|
+
/** @type {Set<SnapshotRegistration>} */
|
|
17
|
+
const snapshot_registrations = new Set();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The registrations mounted right now. Take before navigating and pass to
|
|
21
|
+
* `restore_navigation_snapshot` — only these are eligible for `reset`.
|
|
22
|
+
* @returns {Set<SnapshotRegistration>}
|
|
23
|
+
*/
|
|
24
|
+
export function current_registrations() {
|
|
25
|
+
return new Set(snapshot_registrations);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// injected by client.js: the live history index while the router is idle, null mid-navigation
|
|
29
|
+
/** @type {() => number | null} */
|
|
30
|
+
let get_idle_history_index = () => null;
|
|
31
|
+
|
|
32
|
+
/** @param {() => number | null} fn */
|
|
33
|
+
export function init_snapshots(fn) {
|
|
34
|
+
get_idle_history_index = fn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @param {number} index */
|
|
38
|
+
export function capture_navigation_snapshot(index) {
|
|
39
|
+
if (snapshot_registrations.size === 0) {
|
|
40
|
+
delete navigation_snapshots[index];
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
navigation_snapshots[index] = Object.fromEntries(
|
|
45
|
+
Array.from(snapshot_registrations, ({ id, capture }) => [id, stringify(capture())])
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {number} index
|
|
51
|
+
* @param {Set<SnapshotRegistration>} [reset_registrations]
|
|
52
|
+
*/
|
|
53
|
+
export function restore_navigation_snapshot(index, reset_registrations) {
|
|
54
|
+
const values = navigation_snapshots[index];
|
|
55
|
+
|
|
56
|
+
for (const registration of snapshot_registrations) {
|
|
57
|
+
if (values && Object.hasOwn(values, registration.id)) {
|
|
58
|
+
registration.restore(parse(values[registration.id]));
|
|
59
|
+
} else if (reset_registrations?.has(registration)) {
|
|
60
|
+
registration.reset?.();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @param {number} index */
|
|
66
|
+
export function delete_navigation_snapshot(index) {
|
|
67
|
+
delete navigation_snapshots[index];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {number} index */
|
|
71
|
+
export function persist_navigation_snapshots(index) {
|
|
72
|
+
capture_navigation_snapshot(index);
|
|
73
|
+
storage.set(NAVIGATION_SNAPSHOT_KEY, navigation_snapshots);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string | undefined} stack
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function callsite_id(stack) {
|
|
81
|
+
let frames = stack?.split('\n') ?? [];
|
|
82
|
+
if (frames[0]?.trim() === 'Error') frames = frames.slice(1);
|
|
83
|
+
|
|
84
|
+
// only the callsite frame is stable: frames above it differ between hydration and
|
|
85
|
+
// re-mounting, and Vite query strings (stripped here) change on module invalidation
|
|
86
|
+
const frame = frames[1]?.replace(/\?[^)\s]*(?=:\d+:\d+\)?$)/, '');
|
|
87
|
+
|
|
88
|
+
if (!frame) {
|
|
89
|
+
throw new Error('Could not generate a snapshot id from the stack trace. Pass an `id` instead.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return hash(frame);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A lifecycle function that captures state before navigating and restores it when traversing history.
|
|
97
|
+
*
|
|
98
|
+
* By default, the snapshot `id` is generated from the call site. Pass an explicit `id` to keep snapshots stable across deployments or distinguish multiple uses of a shared helper.
|
|
99
|
+
*
|
|
100
|
+
* The optional `reset` callback runs on navigations where there is no captured value to restore, such as when a new history entry is created. Captured values are serialized with the app's transport hook.
|
|
101
|
+
*
|
|
102
|
+
* `snapshot` must be called during a component initialization. It remains active as long as the component is mounted.
|
|
103
|
+
* @template T
|
|
104
|
+
* @param {{ id?: string; capture: () => T; restore: (value: T) => void; reset?: () => void }} options
|
|
105
|
+
* @returns {void}
|
|
106
|
+
*/
|
|
107
|
+
export function snapshot(options) {
|
|
108
|
+
if (!BROWSER) return;
|
|
109
|
+
|
|
110
|
+
let id = options.id;
|
|
111
|
+
if (id === undefined) {
|
|
112
|
+
// restore any lowered third-party limit, else every callsite collapses to one id
|
|
113
|
+
const limit = Error.stackTraceLimit;
|
|
114
|
+
const lowered = typeof limit === 'number' && limit < 3;
|
|
115
|
+
if (lowered) Error.stackTraceLimit = 3;
|
|
116
|
+
const stack = new Error().stack;
|
|
117
|
+
if (lowered) Error.stackTraceLimit = limit;
|
|
118
|
+
|
|
119
|
+
id = callsite_id(stack);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const registration = { ...options, id };
|
|
123
|
+
|
|
124
|
+
onMount(() => {
|
|
125
|
+
if (DEV && Array.from(snapshot_registrations).some((existing) => existing.id === id)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
options.id === undefined
|
|
128
|
+
? 'snapshot() was called multiple times from the same call site. Pass a unique `id` to distinguish the instances.'
|
|
129
|
+
: `A snapshot with id "${options.id}" is already registered. Pass a unique \`id\`.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const index = get_idle_history_index();
|
|
134
|
+
if (index !== null) {
|
|
135
|
+
const values = navigation_snapshots[index];
|
|
136
|
+
if (values && Object.hasOwn(values, id)) {
|
|
137
|
+
registration.restore(parse(values[id]));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
snapshot_registrations.add(registration);
|
|
142
|
+
|
|
143
|
+
return () => {
|
|
144
|
+
snapshot_registrations.delete(registration);
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -12,6 +12,7 @@ export class Props {
|
|
|
12
12
|
* that currently live on the page — used for capturing and restoring snapshots.
|
|
13
13
|
* It's updated/manipulated through `bind:this` in `Root.svelte`.
|
|
14
14
|
* @type {Array<Record<string, any>>}
|
|
15
|
+
* @deprecated only used for `export const snapshot` — TODO 4.0 get rid
|
|
15
16
|
*/
|
|
16
17
|
components = [];
|
|
17
18
|
|
|
@@ -6,6 +6,7 @@ import { options, get_hooks } from '__SERVER__/internal.js';
|
|
|
6
6
|
import { set_read_implementation, set_manifest, fix_stack_trace } from './internal.js';
|
|
7
7
|
import { set_env } from '__sveltekit/env';
|
|
8
8
|
import { SvelteKitError } from '@sveltejs/kit/internal';
|
|
9
|
+
import { init_tracing } from '@sveltejs/kit/internal/server';
|
|
9
10
|
import { DEV } from 'esm-env';
|
|
10
11
|
import { init_transport } from '#app/internal/transport';
|
|
11
12
|
|
|
@@ -90,6 +91,8 @@ export class Server {
|
|
|
90
91
|
// so anything that shouldn't be rerun should be wrapped in an `if` block to make sure it hasn't
|
|
91
92
|
// been done already.
|
|
92
93
|
|
|
94
|
+
if (__SVELTEKIT_SERVER_TRACING_ENABLED__) init_tracing(import('@opentelemetry/api'));
|
|
95
|
+
|
|
93
96
|
// set env, in case it's used in initialisation
|
|
94
97
|
set_env(env);
|
|
95
98
|
|