@sveltejs/kit 2.57.1 → 2.59.0

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 (35) hide show
  1. package/package.json +2 -2
  2. package/src/exports/internal/remote-functions.js +1 -1
  3. package/src/exports/public.d.ts +107 -16
  4. package/src/runtime/app/paths/client.js +7 -0
  5. package/src/runtime/app/paths/server.js +7 -0
  6. package/src/runtime/app/server/remote/command.js +7 -6
  7. package/src/runtime/app/server/remote/form.js +2 -1
  8. package/src/runtime/app/server/remote/prerender.js +3 -4
  9. package/src/runtime/app/server/remote/query.js +327 -113
  10. package/src/runtime/app/server/remote/requested.js +127 -32
  11. package/src/runtime/app/server/remote/shared.js +89 -20
  12. package/src/runtime/client/client.js +92 -62
  13. package/src/runtime/client/ndjson.js +42 -0
  14. package/src/runtime/client/remote-functions/command.svelte.js +8 -3
  15. package/src/runtime/client/remote-functions/form.svelte.js +24 -6
  16. package/src/runtime/client/remote-functions/index.js +3 -1
  17. package/src/runtime/client/remote-functions/query-batch.svelte.js +105 -0
  18. package/src/runtime/client/remote-functions/query-live.svelte.js +636 -0
  19. package/src/runtime/client/remote-functions/query.svelte.js +48 -148
  20. package/src/runtime/client/remote-functions/shared.svelte.js +76 -23
  21. package/src/runtime/form-utils.js +33 -12
  22. package/src/runtime/server/page/index.js +26 -16
  23. package/src/runtime/server/page/load_data.js +4 -2
  24. package/src/runtime/server/page/render.js +21 -18
  25. package/src/runtime/server/remote.js +117 -9
  26. package/src/runtime/server/respond.js +11 -8
  27. package/src/runtime/server/utils.js +10 -0
  28. package/src/runtime/shared.js +3 -3
  29. package/src/runtime/utils.js +0 -1
  30. package/src/types/internal.d.ts +54 -14
  31. package/src/utils/page_nodes.js +1 -0
  32. package/src/utils/url.js +3 -3
  33. package/src/version.js +1 -1
  34. package/types/index.d.ts +182 -30
  35. package/types/index.d.ts.map +8 -4
@@ -1,16 +1,15 @@
1
1
  /** @import { RemoteQueryFunction } from '@sveltejs/kit' */
2
- /** @import { RemoteFunctionResponse } from 'types' */
3
2
  import { app_dir, base } from '$app/paths/internal/client';
4
- import { app, goto, query_map, query_responses } from '../client.js';
3
+ import { app, query_map, query_responses } from '../client.js';
5
4
  import {
6
5
  get_remote_request_headers,
6
+ is_in_effect,
7
7
  QUERY_FUNCTION_ID,
8
8
  QUERY_OVERRIDE_KEY,
9
9
  QUERY_RESOURCE_KEY,
10
10
  remote_request
11
11
  } from './shared.svelte.js';
12
12
  import * as devalue from 'devalue';
13
- import { HttpError, Redirect } from '@sveltejs/kit/internal';
14
13
  import { DEV } from 'esm-env';
15
14
  import { noop } from '../../../utils/functions.js';
16
15
  import { with_resolvers } from '../../../utils/promise.js';
@@ -26,18 +25,6 @@ import { create_remote_key, stringify_remote_arg, unfriendly_hydratable } from '
26
25
  * }} RemoteQueryCacheEntry
27
26
  */
28
27
 
29
- /**
30
- * @returns {boolean} Returns `true` if we are in an effect
31
- */
32
- function is_in_effect() {
33
- try {
34
- $effect.pre(noop);
35
- return true;
36
- } catch {
37
- return false;
38
- }
39
- }
40
-
41
28
  /**
42
29
  * @param {string} id
43
30
  * @returns {RemoteQueryFunction<any, any>}
@@ -49,8 +36,7 @@ export function query(id) {
49
36
 
50
37
  if (entries) {
51
38
  for (const entry of entries.values()) {
52
- // use optional chaining in case a prerender function was turned into a query
53
- void entry.resource.refresh?.();
39
+ void entry.resource.refresh();
54
40
  }
55
41
  }
56
42
  }
@@ -73,102 +59,6 @@ export function query(id) {
73
59
  return wrapper;
74
60
  }
75
61
 
76
- /**
77
- * @param {string} id
78
- * @returns {RemoteQueryFunction<any, any>}
79
- */
80
- export function query_batch(id) {
81
- /** @type {Map<string, Array<{resolve: (value: any) => void, reject: (error: any) => void}>>} */
82
- let batching = new Map();
83
-
84
- /** @type {RemoteQueryFunction<any, any>} */
85
- const wrapper = (arg) => {
86
- return new QueryProxy(id, arg, async (key, payload) => {
87
- const serialized = await unfriendly_hydratable(key, () => {
88
- return new Promise((resolve, reject) => {
89
- // create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
90
- // is invoked multiple times with the same payload, so we need to deduplicate here
91
- const entry = batching.get(payload) ?? [];
92
- entry.push({ resolve, reject });
93
- batching.set(payload, entry);
94
-
95
- if (batching.size > 1) return;
96
-
97
- // Do this here, after await Svelte' reactivity context is gone.
98
- // TODO is it possible to have batches of the same key
99
- // but in different forks/async contexts and in the same macrotask?
100
- // If so this would potentially be buggy
101
- const headers = {
102
- 'Content-Type': 'application/json',
103
- ...get_remote_request_headers()
104
- };
105
-
106
- // Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
107
- // and flushes could reveal more queries that should be batched.
108
- setTimeout(async () => {
109
- const batched = batching;
110
- batching = new Map();
111
-
112
- try {
113
- const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
114
- method: 'POST',
115
- body: JSON.stringify({
116
- payloads: Array.from(batched.keys())
117
- }),
118
- headers
119
- });
120
-
121
- if (!response.ok) {
122
- throw new Error('Failed to execute batch query');
123
- }
124
-
125
- const result = /** @type {RemoteFunctionResponse} */ (await response.json());
126
- if (result.type === 'error') {
127
- throw new HttpError(result.status ?? 500, result.error);
128
- }
129
-
130
- if (result.type === 'redirect') {
131
- await goto(result.location);
132
- throw new Redirect(307, result.location);
133
- }
134
-
135
- const results = devalue.parse(result.result, app.decoders);
136
-
137
- // Resolve individual queries
138
- // Maps guarantee insertion order so we can do it like this
139
- let i = 0;
140
-
141
- for (const resolvers of batched.values()) {
142
- for (const { resolve, reject } of resolvers) {
143
- if (results[i].type === 'error') {
144
- reject(new HttpError(results[i].status, results[i].error));
145
- } else {
146
- resolve(results[i].data);
147
- }
148
- }
149
- i++;
150
- }
151
- } catch (error) {
152
- // Reject all queries in the batch
153
- for (const resolver of batched.values()) {
154
- for (const { reject } of resolver) {
155
- reject(error);
156
- }
157
- }
158
- }
159
- }, 0);
160
- });
161
- });
162
-
163
- return devalue.parse(serialized, app.decoders);
164
- });
165
- };
166
-
167
- Object.defineProperty(wrapper, QUERY_FUNCTION_ID, { value: id });
168
-
169
- return wrapper;
170
- }
171
-
172
62
  /**
173
63
  * The actual query instance. There should only ever be one active query instance per key.
174
64
  *
@@ -274,6 +164,11 @@ export class Query {
274
164
  resolve(undefined);
275
165
  })
276
166
  .catch((e) => {
167
+ // TODO: Our behavior here could be better:
168
+ // - We should not reject on redirects, but should hook into the router
169
+ // to ensure the query is properly refreshed before the navigation completes
170
+ // - Instead of failing on transport-level errors, we should probably do what
171
+ // LiveQuery does and preserve the last known good value and retry the connection
277
172
  const idx = this.#latest.indexOf(resolve);
278
173
  if (idx === -1) return;
279
174
 
@@ -290,10 +185,14 @@ export class Query {
290
185
  }
291
186
 
292
187
  get then() {
188
+ // TODO this should be unnecessary but due to the bug described
189
+ // in #start, we need to do this in some circumstances
190
+ this.#start();
293
191
  return this.#then;
294
192
  }
295
193
 
296
194
  get catch() {
195
+ this.#start();
297
196
  this.#then;
298
197
  return (/** @type {any} */ reject) => {
299
198
  return this.#then(undefined, reject);
@@ -301,6 +200,7 @@ export class Query {
301
200
  }
302
201
 
303
202
  get finally() {
203
+ this.#start();
304
204
  this.#then;
305
205
  return (/** @type {any} */ fn) => {
306
206
  return this.#then(
@@ -410,7 +310,7 @@ export class Query {
410
310
  * @template T
411
311
  * @implements {Promise<T>}
412
312
  */
413
- class QueryProxy {
313
+ export class QueryProxy {
414
314
  #id;
415
315
  #key;
416
316
  #payload;
@@ -500,53 +400,24 @@ class QueryProxy {
500
400
  };
501
401
  }
502
402
 
503
- #get_cached_query() {
504
- // TODO iterate on error messages
505
- if (!this.#tracking) {
506
- throw new Error(
507
- 'This query was not created in a reactive context and is limited to calling `.run`, `.refresh`, and `.set`.'
508
- );
509
- }
510
-
511
- if (!this.#active) {
512
- throw new Error(
513
- 'This query instance is no longer active and can no longer be used for reactive state access. ' +
514
- 'This typically means you created the query in a tracking context and stashed it somewhere outside of a tracking context.'
515
- );
516
- }
517
-
518
- const cached = query_map.get(this.#id)?.get(this.#payload);
519
-
520
- if (!cached) {
521
- // The only case where `this.#active` can be `true` is when we've added an entry to `query_map`, and the
522
- // only way that entry can get removed is if this instance (and all others) have been deactivated.
523
- // So if we get here, someone (us, check git blame and point fingers) did `entry.count -= 1` improperly.
524
- throw new Error(
525
- 'No cached query found. This should be impossible. Please file a bug report.'
526
- );
527
- }
528
-
529
- return cached.resource;
530
- }
531
-
532
403
  #safe_get_cached_query() {
533
404
  return query_map.get(this.#id)?.get(this.#payload)?.resource;
534
405
  }
535
406
 
536
407
  get current() {
537
- return this.#get_cached_query().current;
408
+ return this.#safe_get_cached_query()?.current;
538
409
  }
539
410
 
540
411
  get error() {
541
- return this.#get_cached_query().error;
412
+ return this.#safe_get_cached_query()?.error;
542
413
  }
543
414
 
544
415
  get loading() {
545
- return this.#get_cached_query().loading;
416
+ return this.#safe_get_cached_query()?.loading ?? false;
546
417
  }
547
418
 
548
419
  get ready() {
549
- return this.#get_cached_query().ready;
420
+ return this.#safe_get_cached_query()?.ready ?? false;
550
421
  }
551
422
 
552
423
  run() {
@@ -574,7 +445,7 @@ class QueryProxy {
574
445
  /** @type {Query<T>['withOverride']} */
575
446
  withOverride(fn) {
576
447
  const entry = this.#get_or_create_cache_entry();
577
- const override = entry.resource.withOverride(fn);
448
+ const override = /** @type {Query<T>} */ (entry.resource).withOverride(fn);
578
449
 
579
450
  const release = /** @type {(() => void) & { [QUERY_OVERRIDE_KEY]: string }} */ (
580
451
  () => {
@@ -588,6 +459,35 @@ class QueryProxy {
588
459
  return release;
589
460
  }
590
461
 
462
+ #get_cached_query() {
463
+ // TODO iterate on error messages
464
+ if (!this.#tracking) {
465
+ throw new Error(
466
+ 'This query was not created in a reactive context and cannot be awaited. Use `.run()` to execute the query instead.'
467
+ );
468
+ }
469
+
470
+ if (!this.#active) {
471
+ throw new Error(
472
+ 'This query instance is no longer active and can no longer be awaited. ' +
473
+ 'This typically means you created the query in a tracking context and stashed it somewhere outside of a tracking context.'
474
+ );
475
+ }
476
+
477
+ const cached = query_map.get(this.#id)?.get(this.#payload);
478
+
479
+ if (!cached) {
480
+ // The only case where `this.#active` can be `true` is when we've added an entry to `query_map`, and the
481
+ // only way that entry can get removed is if this instance (and all others) have been deactivated.
482
+ // So if we get here, someone (us, check git blame and point fingers) did `entry.count -= 1` improperly.
483
+ throw new Error(
484
+ 'No cached query found. This should be impossible. Please file a bug report.'
485
+ );
486
+ }
487
+
488
+ return cached.resource;
489
+ }
490
+
591
491
  /** @type {Query<T>['then']} */
592
492
  get then() {
593
493
  const cached = this.#get_cached_query();
@@ -1,11 +1,12 @@
1
- /** @import { RemoteFunctionResponse, RemoteRefreshMap } from 'types' */
1
+ /** @import { RemoteFunctionResponse, RemoteSingleflightMap, RemoteSingleflightEntry } from 'types' */
2
2
  /** @import { RemoteQueryUpdate } from '@sveltejs/kit' */
3
3
  import * as devalue from 'devalue';
4
- import { app, goto, query_map } from '../client.js';
4
+ import { app, goto, live_query_map, query_map } from '../client.js';
5
5
  import { HttpError, Redirect } from '@sveltejs/kit/internal';
6
6
  import { untrack } from 'svelte';
7
7
  import { create_remote_key, split_remote_key } from '../../shared.js';
8
8
  import { navigating, page } from '../state.svelte.js';
9
+ import { noop } from '../../../utils/functions.js';
9
10
 
10
11
  /** Indicates a query function, as opposed to a query instance */
11
12
  export const QUERY_FUNCTION_ID = Symbol('sveltekit.query_function_id');
@@ -14,6 +15,18 @@ export const QUERY_OVERRIDE_KEY = Symbol('sveltekit.query_override_key');
14
15
  /** Indicates a query instance */
15
16
  export const QUERY_RESOURCE_KEY = Symbol('sveltekit.query_resource_key');
16
17
 
18
+ /**
19
+ * @returns {boolean} Returns `true` if we are in an effect
20
+ */
21
+ export function is_in_effect() {
22
+ try {
23
+ $effect.pre(noop);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
17
30
  /**
18
31
  * @returns {{ 'x-sveltekit-pathname': string, 'x-sveltekit-search': string }}
19
32
  */
@@ -49,16 +62,26 @@ export async function remote_request(url, headers) {
49
62
 
50
63
  const result = /** @type {RemoteFunctionResponse} */ (await response.json());
51
64
 
52
- if (result.type === 'redirect') {
53
- await goto(result.location);
54
- throw new Redirect(307, result.location);
65
+ const resolved = await handle_side_channel_response(result);
66
+
67
+ return resolved.result;
68
+ }
69
+
70
+ /**
71
+ * @param {RemoteFunctionResponse} response
72
+ * @returns {Promise<Extract<RemoteFunctionResponse, { type: 'result' }>>}
73
+ */
74
+ export async function handle_side_channel_response(response) {
75
+ if (response.type === 'redirect') {
76
+ await goto(response.location);
77
+ throw new Redirect(307, response.location);
55
78
  }
56
79
 
57
- if (result.type === 'error') {
58
- throw new HttpError(result.status ?? 500, result.error);
80
+ if (response.type === 'error') {
81
+ throw new HttpError(response.status ?? 500, response.error);
59
82
  }
60
83
 
61
- return result.result;
84
+ return response;
62
85
  }
63
86
 
64
87
  /**
@@ -81,10 +104,10 @@ export function categorize_updates(updates) {
81
104
  if (typeof update === 'function') {
82
105
  if (Object.hasOwn(update, QUERY_FUNCTION_ID)) {
83
106
  // this is a query function (not instance), so we need to find all active instances
84
- // of this functionand request that they be refreshed by the command handler
107
+ // of this function and request that they be refreshed/reconnected by the command handler
85
108
  // @ts-expect-error
86
109
  const id = /** @type {string} */ (update[QUERY_FUNCTION_ID]);
87
- const entries = query_map.get(id);
110
+ const entries = query_map.get(id) ?? live_query_map.get(id);
88
111
 
89
112
  if (entries) {
90
113
  for (const payload of entries.keys()) {
@@ -112,6 +135,10 @@ export function categorize_updates(updates) {
112
135
  overrides.push(/** @type {() => void} */ (update));
113
136
  continue;
114
137
  }
138
+
139
+ // this is just a regular function provided by some user integration, so we can just stash it in the overrides array
140
+ overrides.push(/** @type {() => void} */ (update));
141
+ continue;
115
142
  }
116
143
 
117
144
  if (
@@ -125,31 +152,57 @@ export function categorize_updates(updates) {
125
152
  continue;
126
153
  }
127
154
 
128
- throw new Error('updates() expects a query function, query resource, or query override');
155
+ throw new Error(
156
+ 'updates() expects a query or live query function, query resource, or query override'
157
+ );
129
158
  }
130
159
 
131
160
  return { overrides, refreshes };
132
161
  }
133
162
 
134
163
  /**
135
- * Apply refresh data from the server to the relevant queries
136
- *
137
- * @param {string} stringified_refreshes
164
+ * @template TResource
165
+ * @param {string} stringified_singleflight
166
+ * @param {Map<string, Map<string, { resource: TResource }>>} map
167
+ * @param {(resource: TResource, value: RemoteSingleflightEntry) => void} callback
138
168
  */
139
- export function apply_refreshes(stringified_refreshes) {
140
- const refreshes = Object.entries(
141
- /** @type {RemoteRefreshMap} */ (devalue.parse(stringified_refreshes, app.decoders))
169
+ function apply_singleflight(stringified_singleflight, map, callback) {
170
+ const singleflight = /** @type {RemoteSingleflightMap} */ (
171
+ devalue.parse(stringified_singleflight, app.decoders)
142
172
  );
143
173
 
144
- for (const [key, value] of refreshes) {
174
+ for (const [key, value] of Object.entries(singleflight)) {
145
175
  const parts = split_remote_key(key);
176
+ const entry = map.get(parts.id)?.get(parts.payload);
177
+ if (entry?.resource) {
178
+ callback(entry.resource, value);
179
+ }
180
+ }
181
+ }
146
182
 
147
- const entry = query_map.get(parts.id)?.get(parts.payload);
183
+ /**
184
+ * Apply refresh data from the server to the relevant queries
185
+ *
186
+ * @param {string} stringified_refreshes
187
+ */
188
+ export const apply_refreshes = (stringified_refreshes) => {
189
+ apply_singleflight(stringified_refreshes, query_map, (resource, value) => {
190
+ if (value.type === 'result') {
191
+ resource?.set(value.data);
192
+ } else {
193
+ resource?.fail(new HttpError(value.status ?? 500, value.error));
194
+ }
195
+ });
196
+ };
148
197
 
198
+ /** @param {string} stringified_reconnects */
199
+ export const apply_reconnections = (stringified_reconnects) => {
200
+ apply_singleflight(stringified_reconnects, live_query_map, (resource, value) => {
149
201
  if (value.type === 'result') {
150
- entry?.resource.set(value.data);
202
+ resource?.set(value.data);
203
+ void resource?.reconnect();
151
204
  } else {
152
- entry?.resource.fail(new HttpError(value.status ?? 500, value.error));
205
+ resource?.fail(new HttpError(value.status ?? 500, value.error));
153
206
  }
154
- }
155
- }
207
+ });
208
+ };
@@ -4,9 +4,11 @@
4
4
 
5
5
  import { DEV } from 'esm-env';
6
6
  import * as devalue from 'devalue';
7
- import { text_decoder, text_encoder } from './utils.js';
7
+ import { text_encoder } from './utils.js';
8
8
  import { SvelteKitError } from '@sveltejs/kit/internal';
9
9
 
10
+ const decoder = new TextDecoder();
11
+
10
12
  /**
11
13
  * Sets a value in a nested object using a path string, mutating the original object
12
14
  * @param {Record<string, any>} object
@@ -250,7 +252,7 @@ export async function deserialize_binary_form(request) {
250
252
  const file_offsets_buffer = await get_buffer(HEADER_BYTES + data_length, file_offsets_length);
251
253
  if (!file_offsets_buffer) throw deserialize_error('file offset table too short');
252
254
 
253
- const parsed_offsets = JSON.parse(text_decoder.decode(file_offsets_buffer));
255
+ const parsed_offsets = JSON.parse(decoder.decode(file_offsets_buffer));
254
256
 
255
257
  if (
256
258
  !Array.isArray(parsed_offsets) ||
@@ -265,7 +267,7 @@ export async function deserialize_binary_form(request) {
265
267
 
266
268
  /** @type {Array<{ offset: number, size: number }>} */
267
269
  const file_spans = [];
268
- const [data, meta] = devalue.parse(text_decoder.decode(data_buffer), {
270
+ const [data, meta] = devalue.parse(decoder.decode(data_buffer), {
269
271
  File: ([name, type, size, last_modified, index]) => {
270
272
  if (
271
273
  typeof name !== 'string' ||
@@ -458,7 +460,7 @@ class LazyFile {
458
460
  });
459
461
  }
460
462
  async text() {
461
- return text_decoder.decode(await this.arrayBuffer());
463
+ return decoder.decode(await this.arrayBuffer());
462
464
  }
463
465
  }
464
466
 
@@ -503,8 +505,8 @@ export function deep_set(object, keys, value) {
503
505
  check_prototype_pollution(key);
504
506
 
505
507
  const is_array = /^\d+$/.test(keys[i + 1]);
506
- const exists = Object.hasOwn(current, key);
507
- const inner = current[key];
508
+ const inner = Object.hasOwn(current, key) ? current[key] : undefined;
509
+ const exists = inner != null;
508
510
 
509
511
  if (exists && is_array !== Array.isArray(inner)) {
510
512
  throw new Error(`Invalid array key ${keys[i + 1]}`);
@@ -664,7 +666,7 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
664
666
  if (prop === 'as') {
665
667
  /**
666
668
  * @param {string} type
667
- * @param {string} [input_value]
669
+ * @param {unknown} [input_value]
668
670
  */
669
671
  const as_func = (type, input_value) => {
670
672
  const is_array =
@@ -732,6 +734,23 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
732
734
  }
733
735
  }
734
736
 
737
+ if (type === 'checkbox' && !is_array) {
738
+ return Object.defineProperties(base_props, {
739
+ defaultChecked: {
740
+ enumerable: true,
741
+ get() {
742
+ return input_value;
743
+ }
744
+ },
745
+ checked: {
746
+ enumerable: true,
747
+ get() {
748
+ return get_value() ?? input_value;
749
+ }
750
+ }
751
+ });
752
+ }
753
+
735
754
  return Object.defineProperties(base_props, {
736
755
  value: { value: input_value ?? 'on', enumerable: true },
737
756
  checked: {
@@ -743,11 +762,7 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
743
762
  return value === input_value;
744
763
  }
745
764
 
746
- if (is_array) {
747
- return (value ?? []).includes(input_value);
748
- }
749
-
750
- return value;
765
+ return (value ?? []).includes(input_value);
751
766
  }
752
767
  }
753
768
  });
@@ -797,6 +812,12 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
797
812
 
798
813
  // Handle all other input types (text, number, etc.)
799
814
  return Object.defineProperties(base_props, {
815
+ defaultValue: {
816
+ enumerable: true,
817
+ get() {
818
+ return input_value;
819
+ }
820
+ },
800
821
  value: {
801
822
  enumerable: true,
802
823
  get() {
@@ -1,3 +1,5 @@
1
+ /** @import { ActionResult, RequestEvent, SSRManifest } from '@sveltejs/kit' */
2
+ /** @import { PageNodeIndexes, RequestState, RequiredResolveOptions, ServerDataNode, SSRComponent, SSRNode, SSROptions, SSRState } from 'types' */
1
3
  import { text } from '@sveltejs/kit';
2
4
  import { HttpError, Redirect } from '@sveltejs/kit/internal';
3
5
  import { compact } from '../../../utils/array.js';
@@ -25,14 +27,14 @@ import { PageNodes } from '../../../utils/page_nodes.js';
25
27
  const MAX_DEPTH = 10;
26
28
 
27
29
  /**
28
- * @param {import('@sveltejs/kit').RequestEvent} event
29
- * @param {import('types').RequestState} event_state
30
- * @param {import('types').PageNodeIndexes} page
31
- * @param {import('types').SSROptions} options
32
- * @param {import('@sveltejs/kit').SSRManifest} manifest
33
- * @param {import('types').SSRState} state
30
+ * @param {RequestEvent} event
31
+ * @param {RequestState} event_state
32
+ * @param {PageNodeIndexes} page
33
+ * @param {SSROptions} options
34
+ * @param {SSRManifest} manifest
35
+ * @param {SSRState} state
34
36
  * @param {import('../../../utils/page_nodes.js').PageNodes} nodes
35
- * @param {import('types').RequiredResolveOptions} resolve_opts
37
+ * @param {RequiredResolveOptions} resolve_opts
36
38
  * @returns {Promise<Response>}
37
39
  */
38
40
  export async function render_page(
@@ -58,11 +60,11 @@ export async function render_page(
58
60
  }
59
61
 
60
62
  try {
61
- const leaf_node = /** @type {import('types').SSRNode} */ (nodes.page());
63
+ const leaf_node = /** @type {SSRNode} */ (nodes.page());
62
64
 
63
65
  let status = 200;
64
66
 
65
- /** @type {import('@sveltejs/kit').ActionResult | undefined} */
67
+ /** @type {ActionResult | undefined} */
66
68
  let action_result = undefined;
67
69
 
68
70
  if (is_action_request(event)) {
@@ -135,7 +137,15 @@ export async function render_page(
135
137
  }
136
138
 
137
139
  return await render_response({
138
- branch: [],
140
+ // provide nodes without running load functions so that the styles and
141
+ // fonts are linked in the head before CSR takes over
142
+ branch: compact(nodes.data).map((node) => {
143
+ return {
144
+ node,
145
+ data: null,
146
+ server_data: null
147
+ };
148
+ }),
139
149
  fetched,
140
150
  page_config: {
141
151
  ssr: false,
@@ -165,7 +175,7 @@ export async function render_page(
165
175
  ? server_data_serializer_json(event, event_state, options)
166
176
  : null;
167
177
 
168
- /** @type {Array<Promise<import('types').ServerDataNode | null>>} */
178
+ /** @type {Array<Promise<ServerDataNode | null>>} */
169
179
  const server_promises = nodes.data.map((node, i) => {
170
180
  if (load_error) {
171
181
  // if an error happens immediately, don't bother with the rest of the nodes
@@ -359,7 +369,7 @@ export async function render_page(
359
369
  },
360
370
  status,
361
371
  error: null,
362
- branch: !ssr ? [] : compact(branch),
372
+ branch: compact(branch),
363
373
  action_result,
364
374
  fetched,
365
375
  data_serializer: !ssr ? server_data_serializer(event, event_state, options) : data_serializer,
@@ -388,14 +398,14 @@ export async function render_page(
388
398
 
389
399
  /**
390
400
  *
391
- * @param {import('types').SSROptions} options
401
+ * @param {SSROptions} options
392
402
  * @param {boolean} ssr
393
403
  * @param {Array<import('./types.js').Loaded | null>} branch
394
- * @param {import('types').PageNodeIndexes} page
395
- * @param {import('@sveltejs/kit').SSRManifest} manifest
404
+ * @param {PageNodeIndexes} page
405
+ * @param {SSRManifest} manifest
396
406
  */
397
407
  async function load_error_components(options, ssr, branch, page, manifest) {
398
- /** @type {Array<import('types').SSRComponent | undefined> | undefined} */
408
+ /** @type {Array<SSRComponent | undefined> | undefined} */
399
409
  let error_components;
400
410
 
401
411
  if (options.server_error_boundaries && ssr) {
@@ -4,7 +4,7 @@ import { disable_search, make_trackable } from '../../../utils/url.js';
4
4
  import { validate_depends, validate_load_response } from '../../shared.js';
5
5
  import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server';
6
6
  import { record_span } from '../../telemetry/record_span.js';
7
- import { base64_encode, text_decoder } from '../../utils.js';
7
+ import { base64_encode } from '../../utils.js';
8
8
  import { NULL_BODY_STATUS } from '../constants.js';
9
9
  import { get_node_type } from '../utils.js';
10
10
 
@@ -493,12 +493,14 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
493
493
  async function stream_to_string(stream) {
494
494
  let result = '';
495
495
  const reader = stream.getReader();
496
+ const decoder = new TextDecoder();
496
497
  while (true) {
497
498
  const { done, value } = await reader.read();
498
499
  if (done) {
500
+ result += decoder.decode();
499
501
  break;
500
502
  }
501
- result += text_decoder.decode(value);
503
+ result += decoder.decode(value, { stream: true });
502
504
  }
503
505
  return result;
504
506
  }