@sveltejs/kit 3.0.0-next.6 → 3.0.0-next.8

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.
Files changed (80) hide show
  1. package/package.json +8 -3
  2. package/src/core/adapt/builder.js +11 -39
  3. package/src/core/config/index.js +76 -71
  4. package/src/core/config/options.js +280 -285
  5. package/src/core/config/types.d.ts +1 -1
  6. package/src/core/env.js +86 -1
  7. package/src/core/generate_manifest/index.js +12 -15
  8. package/src/core/sync/create_manifest_data/index.js +7 -45
  9. package/src/core/sync/sync.js +0 -2
  10. package/src/core/sync/write_client_manifest.js +7 -21
  11. package/src/core/sync/write_non_ambient.js +12 -9
  12. package/src/core/sync/write_server.js +0 -4
  13. package/src/core/sync/write_types/index.js +28 -24
  14. package/src/core/utils.js +2 -2
  15. package/src/exports/index.js +3 -3
  16. package/src/exports/internal/client.js +5 -0
  17. package/src/exports/internal/index.js +1 -91
  18. package/src/exports/internal/{event.js → server/event.js} +1 -2
  19. package/src/exports/internal/server/index.js +33 -0
  20. package/src/exports/internal/shared.js +89 -0
  21. package/src/exports/node/index.js +5 -9
  22. package/src/exports/params.js +63 -0
  23. package/src/exports/public.d.ts +103 -46
  24. package/src/exports/url.js +84 -0
  25. package/src/exports/vite/dev/index.js +31 -20
  26. package/src/exports/vite/index.js +280 -199
  27. package/src/exports/vite/preview/index.js +14 -15
  28. package/src/exports/vite/utils.js +8 -10
  29. package/src/runtime/app/env/internal.js +4 -4
  30. package/src/runtime/app/forms.js +2 -2
  31. package/src/runtime/app/paths/client.js +3 -7
  32. package/src/runtime/app/paths/internal/client.js +4 -2
  33. package/src/runtime/app/paths/internal/server.js +2 -23
  34. package/src/runtime/app/paths/server.js +3 -3
  35. package/src/runtime/app/server/remote/query.js +6 -12
  36. package/src/runtime/app/state/client.js +1 -2
  37. package/src/runtime/app/stores.js +13 -76
  38. package/src/runtime/client/bundle.js +1 -1
  39. package/src/runtime/client/client-entry.js +3 -0
  40. package/src/runtime/client/client.js +230 -238
  41. package/src/runtime/client/entry.js +24 -3
  42. package/src/runtime/client/payload.js +17 -0
  43. package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
  44. package/src/runtime/client/remote-functions/form.svelte.js +11 -30
  45. package/src/runtime/client/remote-functions/prerender.svelte.js +10 -3
  46. package/src/runtime/client/remote-functions/query/instance.svelte.js +18 -9
  47. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +16 -6
  48. package/src/runtime/client/remote-functions/shared.svelte.js +1 -2
  49. package/src/runtime/client/state.svelte.js +66 -49
  50. package/src/runtime/client/types.d.ts +3 -7
  51. package/src/runtime/client/utils.js +0 -96
  52. package/src/runtime/components/root.svelte +66 -0
  53. package/src/runtime/form-utils.js +16 -6
  54. package/src/runtime/server/cookie.js +17 -7
  55. package/src/runtime/server/index.js +1 -1
  56. package/src/runtime/server/page/index.js +7 -14
  57. package/src/runtime/server/page/load_data.js +1 -1
  58. package/src/runtime/server/page/render.js +72 -88
  59. package/src/runtime/server/page/server_routing.js +13 -9
  60. package/src/runtime/server/remote.js +23 -13
  61. package/src/runtime/server/respond.js +11 -8
  62. package/src/runtime/server/utils.js +28 -5
  63. package/src/runtime/telemetry/otel.js +1 -1
  64. package/src/runtime/types.d.ts +8 -0
  65. package/src/types/ambient.d.ts +5 -1
  66. package/src/types/global-private.d.ts +11 -18
  67. package/src/types/internal.d.ts +25 -30
  68. package/src/utils/error.js +1 -1
  69. package/src/utils/import.js +6 -1
  70. package/src/utils/mime.js +9 -0
  71. package/src/utils/params.js +66 -0
  72. package/src/utils/routing.js +90 -44
  73. package/src/utils/streaming.js +14 -4
  74. package/src/utils/url.js +0 -79
  75. package/src/version.js +1 -1
  76. package/types/index.d.ts +127 -125
  77. package/types/index.d.ts.map +12 -8
  78. package/src/core/sync/write_root.js +0 -148
  79. package/src/exports/internal/server.js +0 -22
  80. /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
@@ -4,6 +4,7 @@
4
4
  import { DEV } from 'esm-env';
5
5
  import * as devalue from 'devalue';
6
6
  import { text_encoder } from './utils.js';
7
+ import { noop } from '../utils/functions.js';
7
8
  import { SvelteKitError } from '@sveltejs/kit/internal';
8
9
 
9
10
  const decoder = new TextDecoder();
@@ -26,6 +27,9 @@ export function set_nested_value(object, path_string, value) {
26
27
  deep_set(object, split_path(path_string), value);
27
28
  }
28
29
 
30
+ /** Pass this to set_nested_value to delete the last part of the given path */
31
+ export const DELETE_KEY = {};
32
+
29
33
  /**
30
34
  * Convert `FormData` into a POJO
31
35
  * @param {FormData} data
@@ -146,10 +150,7 @@ export async function deserialize_binary_form(request) {
146
150
  throw deserialize_error('no body');
147
151
  }
148
152
 
149
- // TODO: remove this workaround once we upgrade to TS 6.0
150
- const reader = /** @type {ReadableStreamDefaultReader<Uint8Array<ArrayBuffer>>} */ (
151
- request.body.getReader()
152
- );
153
+ const reader = request.body.getReader();
153
154
 
154
155
  /** @type {Array<Promise<Uint8Array<ArrayBuffer> | undefined>>} */
155
156
  const chunks = [];
@@ -320,7 +321,7 @@ export async function deserialize_binary_form(request) {
320
321
  const chunk = await get_chunk(chunks.length);
321
322
  has_more = !!chunk;
322
323
  }
323
- })();
324
+ })().catch(noop); // prevent unhandled rejection potentially crashing the process
324
325
 
325
326
  return { data, meta, form_data: null };
326
327
  }
@@ -504,6 +505,10 @@ export function deep_set(object, keys, value) {
504
505
  }
505
506
 
506
507
  if (!exists) {
508
+ if (value === DELETE_KEY) {
509
+ // don't create the nested structure if we want to delete the key anyway
510
+ return;
511
+ }
507
512
  current[key] = is_array ? [] : {};
508
513
  }
509
514
 
@@ -512,7 +517,12 @@ export function deep_set(object, keys, value) {
512
517
 
513
518
  const final_key = keys[keys.length - 1];
514
519
  check_prototype_pollution(final_key);
515
- current[final_key] = value;
520
+
521
+ if (value === DELETE_KEY) {
522
+ delete current[final_key];
523
+ } else {
524
+ current[final_key] = value;
525
+ }
516
526
  }
517
527
 
518
528
  /**
@@ -12,10 +12,11 @@ import { text_encoder } from '../utils.js';
12
12
  const cookie_paths = {};
13
13
 
14
14
  /**
15
- * Cookies that are larger than this size (including the name and other
16
- * attributes) are discarded by browsers
15
+ * Cookies whose name and value combined are larger than this size are
16
+ * discarded by browsers. This is the limit codified for the name/value pair
17
+ * in RFC 6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-20#section-5.6-7.5.1
17
18
  */
18
- const MAX_COOKIE_SIZE = 4129;
19
+ const MAX_COOKIE_SIZE = 4096;
19
20
 
20
21
  /**
21
22
  * Generates a unique key for a cookie based on its domain, path, and name in
@@ -124,7 +125,13 @@ export function get_cookies(request, url) {
124
125
 
125
126
  // Add the most specific cookies to the result
126
127
  for (const c of lookup.values()) {
127
- cookies[c.name] = c.value;
128
+ // tombstones (deleted cookies) shadow request-header cookies,
129
+ // mirroring the behavior of `get()`
130
+ if (c.options.maxAge === 0) {
131
+ delete cookies[c.name];
132
+ } else {
133
+ cookies[c.name] = c.value;
134
+ }
128
135
  }
129
136
 
130
137
  return /** @type {Array<{ name: string; value: string }>} */ (
@@ -219,9 +226,12 @@ export function get_cookies(request, url) {
219
226
  new_cookies.set(cookie_key, cookie);
220
227
 
221
228
  if (DEV) {
222
- const { encode, ...rest } = cookie.options;
223
- const serialized = stringifySetCookie({ name, value, ...rest }, { encode });
224
- if (text_encoder.encode(serialized).byteLength > MAX_COOKIE_SIZE) {
229
+ const size =
230
+ // only the name/value pair counts towards MAX_COOKIE_SIZE, not the other attributes
231
+ text_encoder.encode(name).byteLength +
232
+ text_encoder.encode((options.encode ?? encodeURIComponent)(value)).byteLength;
233
+
234
+ if (size > MAX_COOKIE_SIZE) {
225
235
  throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`);
226
236
  }
227
237
 
@@ -27,7 +27,7 @@ export class Server {
27
27
  this.#manifest = manifest;
28
28
 
29
29
  // Since AsyncLocalStorage is not working in webcontainers, we don't reset `sync_store`
30
- // in `src/exports/internal/event.js` and handle only one request at a time.
30
+ // in `src/exports/internal/server/event.js` and handle only one request at a time.
31
31
  if (IN_WEBCONTAINER) {
32
32
  const respond = this.respond.bind(this);
33
33
 
@@ -1,5 +1,6 @@
1
+ /** @import { Component } from 'svelte' */
1
2
  /** @import { ActionResult, RequestEvent, SSRManifest } from '@sveltejs/kit' */
2
- /** @import { PageNodeIndexes, RequestState, RequiredResolveOptions, ServerDataNode, SSRComponent, SSRNode, SSROptions, SSRState } from 'types' */
3
+ /** @import { PageNodeIndexes, RequestState, RequiredResolveOptions, ServerDataNode, SSRNode, SSROptions, SSRState } from 'types' */
3
4
  import { text } from '@sveltejs/kit';
4
5
  import { HttpError, Redirect } from '@sveltejs/kit/internal';
5
6
  import { compact } from '../../../utils/array.js';
@@ -316,13 +317,7 @@ export async function render_page(
316
317
  },
317
318
  status,
318
319
  error,
319
- error_components: await load_error_components(
320
- options,
321
- ssr,
322
- error_branch,
323
- page,
324
- manifest
325
- ),
320
+ error_components: await load_error_components(ssr, error_branch, page, manifest),
326
321
  branch: error_branch,
327
322
  fetched,
328
323
  data_serializer
@@ -374,7 +369,7 @@ export async function render_page(
374
369
  action_result,
375
370
  fetched,
376
371
  data_serializer: !ssr ? server_data_serializer(event, event_state, options) : data_serializer,
377
- error_components: await load_error_components(options, ssr, branch, page, manifest)
372
+ error_components: await load_error_components(ssr, branch, page, manifest)
378
373
  });
379
374
  } catch (e) {
380
375
  // a remote function could have thrown a redirect during render
@@ -398,18 +393,16 @@ export async function render_page(
398
393
  }
399
394
 
400
395
  /**
401
- *
402
- * @param {SSROptions} options
403
396
  * @param {boolean} ssr
404
397
  * @param {Array<import('./types.js').Loaded | null>} branch
405
398
  * @param {PageNodeIndexes} page
406
399
  * @param {SSRManifest} manifest
407
400
  */
408
- async function load_error_components(options, ssr, branch, page, manifest) {
409
- /** @type {Array<SSRComponent | undefined> | undefined} */
401
+ async function load_error_components(ssr, branch, page, manifest) {
402
+ /** @type {Array<Component | undefined> | undefined} */
410
403
  let error_components;
411
404
 
412
- if (options.server_error_boundaries && ssr) {
405
+ if (ssr) {
413
406
  let last_idx = -1;
414
407
  error_components = await Promise.all(
415
408
  // eslint-disable-next-line @typescript-eslint/await-thenable
@@ -377,7 +377,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
377
377
  }
378
378
 
379
379
  void push_fetched(base64_encode(result), true);
380
- })();
380
+ })().catch(noop); // prevent unhandled rejection potentially crashing the process
381
381
 
382
382
  return (teed_body = b);
383
383
  }
@@ -1,5 +1,5 @@
1
+ /** @import { RenderNode } from '../../types.js' */
1
2
  import * as devalue from 'devalue';
2
- import { readable, writable } from 'svelte/store';
3
3
  import { DEV } from 'esm-env';
4
4
  import { isRedirect, text } from '@sveltejs/kit';
5
5
  import * as paths from '$app/paths/internal/server';
@@ -22,14 +22,11 @@ import {
22
22
  } from '../utils.js';
23
23
  import * as env from '__sveltekit/env';
24
24
  import { collect_remote_data } from '../remote.js';
25
+ import Root from '../../components/root.svelte';
26
+ import { render } from 'svelte/server';
25
27
 
26
28
  // TODO rename this function/module
27
29
 
28
- const updated = {
29
- ...readable(false),
30
- check: () => false
31
- };
32
-
33
30
  /**
34
31
  * Creates the HTML response.
35
32
  * @param {{
@@ -46,7 +43,7 @@ const updated = {
46
43
  * resolve_opts: import('types').RequiredResolveOptions;
47
44
  * action_result?: import('@sveltejs/kit').ActionResult;
48
45
  * data_serializer: import('./types.js').ServerDataSerializer;
49
- * error_components?: Array<import('types').SSRComponent | undefined>
46
+ * error_components?: Array<import('svelte').Component | undefined>
50
47
  * }} opts
51
48
  */
52
49
  export async function render_response({
@@ -91,7 +88,8 @@ export async function render_response({
91
88
  // TODO if we add a client entry point one day, we will need to include inline_styles with the entry, otherwise stylesheets will be linked even if they are below inlineStyleThreshold
92
89
  const inline_styles = new Map();
93
90
 
94
- /** @type {ReturnType<typeof options.root.render>} */
91
+ // TODO `svelte/server` should expose `RenderOutput`
92
+ /** @type {{ head: string, body: string, hashes: { script: string[] } }} */
95
93
  let rendered;
96
94
 
97
95
  const form_value =
@@ -142,49 +140,47 @@ export async function render_response({
142
140
  if (page_config.ssr) {
143
141
  /** @type {Record<string, any>} */
144
142
  const props = {
145
- stores: {
146
- page: writable(null),
147
- navigating: writable(null),
148
- updated
149
- },
150
- constructors: await Promise.all(
151
- branch.map(({ node }) => {
152
- if (!node.component) {
153
- // Can only be the leaf, layouts have a fallback component generated
154
- throw new Error(`Missing +page.svelte component for route ${event.route.id}`);
155
- }
156
- return node.component();
157
- })
158
- ),
159
- form: form_value
160
- };
161
-
162
- if (error_components) {
163
- if (error) {
164
- props.error = error;
143
+ components: [],
144
+ resetters: [],
145
+ form: form_value,
146
+ tree: /** @type {RenderNode} */ ({}),
147
+ error,
148
+ page: {
149
+ error,
150
+ params: /** @type {Record<string, any>} */ (event.params),
151
+ route: event.route,
152
+ status,
153
+ url: event.url,
154
+ data: {},
155
+ form: form_value,
156
+ state: {}
165
157
  }
166
- props.errors = error_components;
167
- }
158
+ };
168
159
 
169
- let data = {};
160
+ let current_node = props.tree;
161
+ let data = props.page.data;
170
162
 
171
- // props_n (instead of props[n]) makes it easy to avoid
172
- // unnecessary updates for layout components
173
163
  for (let i = 0; i < branch.length; i += 1) {
174
- data = { ...data, ...branch[i].data };
175
- props[`data_${i}`] = data;
164
+ const node = branch[i];
165
+
166
+ data = { ...data, ...node.data };
167
+
168
+ // TODO this is undefined sometimes... where does the default error component come from?
169
+ const error = error_components?.slice(0, i + 1).findLast((x) => x);
170
+
171
+ current_node.error = error;
172
+ current_node.component = await node.node.component?.();
173
+ current_node.data = data;
174
+
175
+ if (i < branch.length - 1) {
176
+ current_node.child = /** @type {import('../../types.js').RenderNode} */ ({});
177
+ current_node = current_node.child;
178
+ }
176
179
  }
177
180
 
178
- props.page = {
179
- error,
180
- params: /** @type {Record<string, any>} */ (event.params),
181
- route: event.route,
182
- status,
183
- url: event.url,
184
- data,
185
- form: form_value,
186
- state: {}
187
- };
181
+ props.page.data = data;
182
+
183
+ const render_state = { ...event_state, is_in_render: true };
188
184
 
189
185
  const render_opts = {
190
186
  context: new Map([
@@ -197,15 +193,28 @@ export async function render_response({
197
193
  ]),
198
194
  csp: csp.script_needs_nonce ? { nonce: csp.nonce } : { hash: csp.script_needs_hash },
199
195
  transformError: error_components
200
- ? /** @param {unknown} e */ async (e) => {
196
+ ? /** @param {unknown} e */ (e) => {
201
197
  if (isRedirect(e)) {
202
198
  throw e;
203
199
  }
204
200
 
205
- const transformed = await handle_error_and_jsonify(event, event_state, options, e);
206
- props.page.error = props.error = error = transformed;
207
- props.page.status = status = transformed.status;
208
- return transformed;
201
+ const handled = handle_error_and_jsonify(event, render_state, options, e);
202
+
203
+ // TODO 4.0 make this an async function and await `handled`
204
+ if (handled instanceof Promise) {
205
+ return handled.then((e) => {
206
+ error = e;
207
+ props.page.error = error;
208
+ props.page.status = status = error.status;
209
+ return error;
210
+ });
211
+ }
212
+
213
+ error = handled;
214
+ props.page.error = error;
215
+ props.page.status = status = error.status;
216
+
217
+ return error;
209
218
  }
210
219
  : undefined
211
220
  };
@@ -231,53 +240,27 @@ export async function render_response({
231
240
  };
232
241
  }
233
242
 
234
- const state = { ...event_state, is_in_render: true };
235
-
236
- rendered = await with_request_store({ event, state }, async () => {
237
- // use relative paths during rendering, so that the resulting HTML is as
238
- // portable as possible, but reset afterwards
239
- if (paths.relative) paths.override({ base, assets });
240
-
241
- const maybe_promise = options.root.render(props, render_opts);
243
+ rendered = await with_request_store({ event, state: render_state }, async () => {
242
244
  // We have to invoke .then eagerly here in order to kick off rendering: it's only starting on access,
243
245
  // and `await maybe_promise` would eagerly access the .then property but call its function only after a tick, which is too late
244
246
  // for the paths.reset() below and for any eager getRequestEvent() calls during rendering without AsyncLocalStorage available.
245
- const rendered =
246
- options.async && 'then' in maybe_promise
247
- ? /** @type {ReturnType<typeof options.root.render> & Promise<any>} */ (
248
- maybe_promise
249
- ).then((r) => r)
250
- : maybe_promise;
251
-
252
- // TODO 3.0 remove options.async
253
- if (options.async) {
254
- // we reset this synchronously, rather than after async rendering is complete,
255
- // to avoid cross-talk between requests. This is a breaking change for
256
- // anyone who opts into async SSR, since `base` and `assets` will no
257
- // longer be relative to the current pathname.
258
- // TODO 3.0 remove `base` and `assets` in favour of `resolve(...)` and `asset(...)`
259
- paths.reset();
260
- }
247
+ const rendered = render(Root, { ...render_opts, props });
261
248
 
262
- const { head, html, css, hashes } = /** @type {ReturnType<typeof options.root.render>} */ (
263
- options.async ? await rendered : rendered
264
- );
249
+ const { head, body, hashes } = await rendered;
265
250
 
266
251
  if (hashes) {
267
252
  csp.add_script_hashes(hashes.script);
268
253
  }
269
254
 
270
- return { head, html, css, hashes };
255
+ return { head, body, hashes };
271
256
  });
272
257
  } finally {
273
258
  if (DEV) {
274
259
  globalThis.fetch = fetch;
275
260
  }
276
-
277
- paths.reset(); // just in case `options.root.render(...)` failed
278
261
  }
279
262
  } else {
280
- rendered = { head: '', html: '', css: { code: '', map: null }, hashes: { script: [] } };
263
+ rendered = { head: '', body: '', hashes: { script: [] } };
281
264
  }
282
265
 
283
266
  for (const { node } of branch) {
@@ -298,7 +281,7 @@ export async function render_response({
298
281
  }
299
282
 
300
283
  const head = new Head(rendered.head);
301
- let body = rendered.html;
284
+ let body = rendered.body;
302
285
 
303
286
  /** @param {string} path */
304
287
  const prefixed = (path) => {
@@ -421,7 +404,7 @@ export async function render_response({
421
404
 
422
405
  const blocks = [];
423
406
 
424
- const properties = [`base: ${base_expression}`];
407
+ const properties = [`base: ${base_expression}`, `version: ${s(__SVELTEKIT_APP_VERSION__)}`];
425
408
 
426
409
  if (paths.assets) {
427
410
  properties.push(`assets: ${s(paths.assets)}`);
@@ -444,7 +427,9 @@ export async function render_response({
444
427
  if (client.inline) {
445
428
  app_declaration = `const app = ${global}.app.app;`;
446
429
  } else if (client.app) {
447
- app_declaration = `const app = await import(${s(prefixed(client.app))});`;
430
+ app_declaration = `const kit = await import(${s(prefixed(client.start))});
431
+ kit.init(${global});
432
+ const app = await import(${s(prefixed(client.app))});`;
448
433
  } else {
449
434
  app_declaration = `const { app } = await import(${s(prefixed(client.start))});`;
450
435
  }
@@ -538,10 +523,9 @@ export async function render_response({
538
523
 
539
524
  ${serialized_data}${global}.app.start(${args.join(', ')});`
540
525
  : client.app
541
- ? `Promise.all([
542
- import(${s(prefixed(client.start))}),
543
- import(${s(prefixed(client.app))})
544
- ]).then(([kit, app]) => {
526
+ ? `import(${s(prefixed(client.start))}).then(async (kit) => {
527
+ kit.init(${global});
528
+ const app = await import(${s(prefixed(client.app))});
545
529
  ${serialized_data}kit.start(app, ${args.join(', ')});
546
530
  });`
547
531
  : `import(${s(prefixed(client.start))}).then((app) => {
@@ -69,15 +69,19 @@ export async function resolve_route(resolved_path, url, manifest) {
69
69
  return text('Server-side route resolution disabled', { status: 400 });
70
70
  }
71
71
 
72
- const matchers = await manifest._.matchers();
73
- const result = find_route(resolved_path, manifest._.client.routes, matchers);
74
-
75
- return create_server_routing_response(
76
- result?.route ?? null,
77
- result?.params ?? {},
78
- url,
79
- manifest._.client
80
- ).response;
72
+ try {
73
+ const matchers = await manifest._.matchers();
74
+ const result = find_route(resolved_path, manifest._.client.routes, matchers);
75
+
76
+ return create_server_routing_response(
77
+ result?.route ?? null,
78
+ result?.params ?? {},
79
+ url,
80
+ manifest._.client
81
+ ).response;
82
+ } catch {
83
+ return text('Error resolving route', { status: 500 });
84
+ }
81
85
  }
82
86
 
83
87
  /**
@@ -361,14 +361,15 @@ export async function collect_remote_data(data, event, state, options) {
361
361
  * @returns {Promise<App.Error>}
362
362
  */
363
363
  function convert_error(error) {
364
- return handle_error_and_jsonify(event, state, options, error);
364
+ // TODO 4.0 remove the `Promise.resolve(...)`
365
+ return Promise.resolve(handle_error_and_jsonify(event, state, options, error));
365
366
  }
366
367
 
367
368
  /** @type {Promise<any>[]} */
368
369
  const promises = [];
369
370
 
370
371
  if (state.remote.explicit) {
371
- for (const [remote_key, { internals, promise }] of state.remote.explicit) {
372
+ for (const [remote_key, { internals, fn }] of state.remote.explicit) {
372
373
  // there were explicit refreshes/reconnects (via `refresh()`/`set()`/`reconnect()`),
373
374
  // so the client should apply these single-flight updates instead of calling `invalidateAll()`
374
375
  data.r = true;
@@ -377,18 +378,27 @@ export async function collect_remote_data(data, event, state, options) {
377
378
  internals.type === 'query_live' ? 'l' : internals.type[0]
378
379
  );
379
380
 
380
- await promise.then(
381
- (v) => {
382
- ((data[type] ??= {})[remote_key] ??= {}).v = v;
383
- },
384
- async (e) => {
385
- if (e instanceof Redirect) {
386
- // already handled elsewhere
387
- return;
388
- }
381
+ // `fn` is deferred until now so the query runs after any state mutations
382
+ // in the command/form body. If the query was re-awaited in the meantime,
383
+ // `fn` returns the existing (fresh) cache entry rather than re-running.
384
+ // Kick off the query immediately and collect the promise so that multiple
385
+ // explicit refreshes run concurrently rather than serially.
386
+ const promise = fn();
387
+
388
+ promises.push(
389
+ promise.then(
390
+ (v) => {
391
+ ((data[type] ??= {})[remote_key] ??= {}).v = v;
392
+ },
393
+ async (e) => {
394
+ if (e instanceof Redirect) {
395
+ // already handled elsewhere
396
+ return;
397
+ }
389
398
 
390
- ((data[type] ??= {})[remote_key] ??= {}).e = await convert_error(e);
391
- }
399
+ ((data[type] ??= {})[remote_key] ??= {}).e = await convert_error(e);
400
+ }
401
+ )
392
402
  );
393
403
  }
394
404
  }
@@ -336,14 +336,17 @@ export async function internal_respond(request, options, manifest, state) {
336
336
  }
337
337
 
338
338
  if (!state.prerendering?.fallback) {
339
- // TODO this could theoretically break — should probably be inside a try-catch
340
- const matchers = await manifest._.matchers();
341
- const result = find_route(resolved_path, manifest._.routes, matchers);
342
-
343
- if (result) {
344
- route = result.route;
345
- event.route = { id: route.id };
346
- event.params = result.params;
339
+ try {
340
+ const matchers = await manifest._.matchers();
341
+ const result = find_route(resolved_path, manifest._.routes, matchers);
342
+
343
+ if (result) {
344
+ route = result.route;
345
+ event.route = { id: route.id };
346
+ event.params = result.params;
347
+ }
348
+ } catch (e) {
349
+ return await handle_fatal_error(event, event_state, options, e);
347
350
  }
348
351
  }
349
352
 
@@ -98,9 +98,9 @@ export async function handle_fatal_error(event, state, options, error) {
98
98
  * @param {import('types').RequestState} state
99
99
  * @param {import('types').SSROptions} options
100
100
  * @param {any} error
101
- * @returns {Promise<App.Error>}
101
+ * @returns {App.Error | Promise<App.Error>}
102
102
  */
103
- export async function handle_error_and_jsonify(event, state, options, error) {
103
+ export function handle_error_and_jsonify(event, state, options, error) {
104
104
  if (error instanceof HttpError) {
105
105
  // @ts-expect-error custom user errors may not have a message field if App.Error is overwritten
106
106
  return { message: 'Unknown Error', ...error.body };
@@ -113,11 +113,34 @@ export async function handle_error_and_jsonify(event, state, options, error) {
113
113
  const status = get_status(error);
114
114
  const message = get_message(error);
115
115
 
116
- const body = (await with_request_store({ event, state }, () =>
116
+ // TODO 4.0 await this, rather than handling the non-Promise case
117
+ const result = with_request_store({ event, state }, () =>
117
118
  options.hooks.handleError({ error, event, status, message })
118
- )) ?? { message };
119
+ ) ?? { status, message };
120
+
121
+ if (result instanceof Promise) {
122
+ if (!__SVELTEKIT_SUPPORTS_ASYNC__ && state.is_in_render) {
123
+ console.warn(
124
+ `To use an async \`handleError\` hook to handle errors that occur during rendering, you must enable \`compilerOptions.experimental.async\` in the SvelteKit plugin of your Vite config. The returned error has been replaced with a generic object`
125
+ );
126
+
127
+ // we're discarding the result, but we still need to prevent an unhandled
128
+ // rejection if the user's async `handleError` hook rejects
129
+ result.catch(() => {});
130
+
131
+ return {
132
+ status,
133
+ message: 'Internal Error'
134
+ };
135
+ }
136
+
137
+ return result.then((body) => {
138
+ body ??= { status, message };
139
+ return { ...body, status: get_status(body, error) };
140
+ });
141
+ }
119
142
 
120
- return { ...body, status: get_status(body, error) };
143
+ return { ...result, status: get_status(result, error) };
121
144
  }
122
145
 
123
146
  /**
@@ -15,7 +15,7 @@ if (__SVELTEKIT_SERVER_TRACING_ENABLED__) {
15
15
  })
16
16
  .catch(() => {
17
17
  throw new Error(
18
- 'Tracing is enabled (see the SvelteKit plugin `experimental.instrumentation.server` option in your vite.config.js), but `@opentelemetry/api` is not available. This error will likely resolve itself when you set up your tracing instrumentation in `instrumentation.server.js`. For more information, see https://svelte.dev/docs/kit/observability#opentelemetry-api'
18
+ 'Tracing is enabled (see the SvelteKit plugin `tracing.server` option in your vite.config.js), but `@opentelemetry/api` is not available. This error will likely resolve itself when you set up your tracing instrumentation in `instrumentation.server.js`. For more information, see https://svelte.dev/docs/kit/observability#opentelemetry-api'
19
19
  );
20
20
  });
21
21
  }
@@ -0,0 +1,8 @@
1
+ import { Component } from 'svelte';
2
+
3
+ export interface RenderNode {
4
+ component: Component;
5
+ error: Component;
6
+ data: Record<string, any>;
7
+ child?: RenderNode;
8
+ }
@@ -32,23 +32,27 @@ declare namespace App {
32
32
  /**
33
33
  * The interface that defines `event.locals`, which can be accessed in server [hooks](https://svelte.dev/docs/kit/hooks) (`handle`, and `handleError`), server-only `load` functions, and `+server.js` files.
34
34
  */
35
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
35
36
  export interface Locals {}
36
37
 
37
38
  /**
38
- * Defines the common shape of the [page.data state](https://svelte.dev/docs/kit/$app-state#page) and [$page.data store](https://svelte.dev/docs/kit/$app-stores#page) - that is, the data that is shared between all pages.
39
+ * Defines the common shape of the [page.data state](https://svelte.dev/docs/kit/$app-state#page) - that is, the data that is shared between all pages.
39
40
  * The `Load` and `ServerLoad` functions in `./$types` will be narrowed accordingly.
40
41
  * Use optional properties for data that is only present on specific pages. Do not add an index signature (`[key: string]: any`).
41
42
  */
43
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
42
44
  export interface PageData {}
43
45
 
44
46
  /**
45
47
  * The shape of the `page.state` object, which can be manipulated using the [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState) and [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState) functions from `$app/navigation`.
46
48
  */
49
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
47
50
  export interface PageState {}
48
51
 
49
52
  /**
50
53
  * If your adapter provides [platform-specific context](https://svelte.dev/docs/kit/adapters#Platform-specific-context) via `event.platform`, you can specify it here.
51
54
  */
55
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
52
56
  export interface Platform {}
53
57
  }
54
58