@sveltejs/kit 2.63.1 → 2.65.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 (33) hide show
  1. package/package.json +2 -2
  2. package/src/core/postbuild/analyse.js +0 -5
  3. package/src/core/postbuild/prerender.js +2 -0
  4. package/src/exports/public.d.ts +1 -1
  5. package/src/exports/vite/build/build_server.js +47 -57
  6. package/src/exports/vite/build/utils.js +0 -8
  7. package/src/exports/vite/index.js +226 -178
  8. package/src/runtime/app/server/remote/command.js +0 -3
  9. package/src/runtime/app/server/remote/form.js +18 -13
  10. package/src/runtime/app/server/remote/prerender.js +28 -34
  11. package/src/runtime/app/server/remote/query.js +105 -94
  12. package/src/runtime/app/server/remote/requested.js +14 -10
  13. package/src/runtime/app/server/remote/shared.js +25 -18
  14. package/src/runtime/client/client.js +19 -13
  15. package/src/runtime/client/remote-functions/command.svelte.js +7 -32
  16. package/src/runtime/client/remote-functions/form.svelte.js +62 -82
  17. package/src/runtime/client/remote-functions/prerender.svelte.js +14 -6
  18. package/src/runtime/client/remote-functions/query/index.js +6 -14
  19. package/src/runtime/client/remote-functions/query/instance.svelte.js +20 -0
  20. package/src/runtime/client/remote-functions/query/proxy.js +3 -3
  21. package/src/runtime/client/remote-functions/query-batch.svelte.js +59 -68
  22. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +20 -6
  23. package/src/runtime/client/remote-functions/shared.svelte.js +76 -59
  24. package/src/runtime/server/page/render.js +20 -80
  25. package/src/runtime/server/page/server_routing.js +20 -15
  26. package/src/runtime/server/remote.js +296 -204
  27. package/src/runtime/server/respond.js +4 -2
  28. package/src/runtime/shared.js +83 -28
  29. package/src/types/global-private.d.ts +3 -3
  30. package/src/types/internal.d.ts +53 -34
  31. package/src/version.js +1 -1
  32. package/types/index.d.ts +4 -4
  33. package/types/index.d.ts.map +1 -1
@@ -1,16 +1,8 @@
1
1
  /** @import { RemoteCommand, RemoteQueryUpdate } from '@sveltejs/kit' */
2
- /** @import { RemoteFunctionResponse } from 'types' */
3
2
  import { app_dir, base } from '$app/paths/internal/client';
4
- import * as devalue from 'devalue';
5
- import { HttpError } from '@sveltejs/kit/internal';
6
3
  import { app } from '../client.js';
7
- import { stringify_remote_arg } from '../../shared.js';
8
- import {
9
- get_remote_request_headers,
10
- apply_refreshes,
11
- categorize_updates,
12
- apply_reconnections
13
- } from './shared.svelte.js';
4
+ import { stringify_command_arg } from '../../shared.js';
5
+ import { get_remote_request_headers, categorize_updates, remote_request } from './shared.svelte.js';
14
6
 
15
7
  /**
16
8
  * Client-version of the `command` function from `$app/server`.
@@ -53,39 +45,22 @@ export function command(id) {
53
45
  throw updates_error;
54
46
  }
55
47
 
56
- const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
48
+ const response = await remote_request(`${base}/${app_dir}/remote/${id}`, {
57
49
  method: 'POST',
58
50
  body: JSON.stringify({
59
- payload: stringify_remote_arg(arg, app.hooks.transport, false),
51
+ payload: await stringify_command_arg(arg, app.hooks.transport),
60
52
  refreshes: Array.from(refreshes ?? [])
61
53
  }),
62
54
  headers
63
55
  });
64
56
 
65
- if (!response.ok) {
66
- // We only end up here in case of a network error or if the server has an internal error
67
- // (which shouldn't happen because we handle errors on the server and always send a 200 response)
68
- throw new Error('Failed to execute remote function');
69
- }
70
-
71
- const result = /** @type {RemoteFunctionResponse} */ (await response.json());
72
- if (result.type === 'redirect') {
57
+ if (response.redirect) {
73
58
  throw new Error(
74
59
  'Redirects are not allowed in commands. Return a result instead and use goto on the client'
75
60
  );
76
- } else if (result.type === 'error') {
77
- throw new HttpError(result.status ?? 500, result.error);
78
- } else {
79
- if (result.refreshes) {
80
- apply_refreshes(result.refreshes);
81
- }
82
-
83
- if (result.reconnects) {
84
- apply_reconnections(result.reconnects);
85
- }
86
-
87
- return devalue.parse(result.result, app.decoders);
88
61
  }
62
+
63
+ return response._;
89
64
  } finally {
90
65
  overrides?.forEach((fn) => fn());
91
66
 
@@ -1,13 +1,12 @@
1
1
  /** @import { StandardSchemaV1 } from '@standard-schema/spec' */
2
2
  /** @import { RemoteFormInput, RemoteForm, RemoteQueryUpdate } from '@sveltejs/kit' */
3
- /** @import { InternalRemoteFormIssue, RemoteFunctionResponse } from 'types' */
3
+ /** @import { InternalRemoteFormIssue } from 'types' */
4
4
  import { app_dir, base } from '$app/paths/internal/client';
5
- import * as devalue from 'devalue';
6
5
  import { DEV } from 'esm-env';
7
6
  import { HttpError } from '@sveltejs/kit/internal';
8
- import { app, query_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
7
+ import { query_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
9
8
  import { tick } from 'svelte';
10
- import { apply_refreshes, categorize_updates, apply_reconnections } from './shared.svelte.js';
9
+ import { categorize_updates, remote_request } from './shared.svelte.js';
11
10
  import { createAttachmentKey } from 'svelte/attachments';
12
11
  import {
13
12
  convert_formdata,
@@ -60,18 +59,25 @@ export function form(id) {
60
59
  const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
61
60
  const action = '?/remote=' + encodeURIComponent(action_id);
62
61
 
62
+ // the output of a non-enhanced submission that resulted in this page —
63
+ // consume it so the form's state survives hydration (form outputs are
64
+ // always value nodes; the server never serializes them as errors)
65
+ /** @type {{ input?: Record<string, any>, issues?: InternalRemoteFormIssue[], result?: any } | undefined} */
66
+ const initial = query_responses[action_id]?.v;
67
+ delete query_responses[action_id];
68
+
63
69
  /**
64
70
  * @type {Record<string, string | string[] | File | File[]>}
65
71
  */
66
- let input = $state({});
72
+ let input = $state(initial?.input ?? {});
67
73
 
68
74
  /** @type {InternalRemoteFormIssue[]} */
69
- let raw_issues = $state.raw([]);
75
+ let raw_issues = $state.raw(initial?.issues ?? []);
70
76
 
71
77
  const issues = $derived(flatten_issues(raw_issues));
72
78
 
73
79
  /** @type {any} */
74
- let result = $state.raw(query_responses[action_id]);
80
+ let result = $state.raw(initial?.result);
75
81
 
76
82
  /** @type {number} */
77
83
  let pending_count = $state(0);
@@ -182,77 +188,54 @@ export function form(id) {
182
188
  remote_refreshes: Array.from(refreshes ?? [])
183
189
  });
184
190
 
185
- const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
186
- method: 'POST',
187
- headers: {
188
- 'Content-Type': BINARY_FORM_CONTENT_TYPE,
189
- // Forms cannot be called during rendering, so it's save to use location here
190
- 'x-sveltekit-pathname': location.pathname,
191
- 'x-sveltekit-search': location.search
192
- },
193
- body: blob
194
- });
195
-
196
- if (!response.ok) {
197
- // We only end up here in case of a network error or if the server has an internal error
198
- // (which shouldn't happen because we handle errors on the server and always send a 200 response)
199
- throw new Error('Failed to execute remote function');
200
- }
201
-
202
- const form_result = /** @type { RemoteFunctionResponse} */ (await response.json());
203
-
204
- // reset issues in case it's a redirect or error (but issues passed in that case)
205
- raw_issues = [];
206
- result = undefined;
207
-
208
- if (form_result.type === 'result') {
209
- ({ issues: raw_issues = [], result } = devalue.parse(form_result.result, app.decoders));
210
- const succeeded = raw_issues.length === 0;
211
-
212
- if (succeeded) {
213
- if (refreshes === null && !form_result.refreshes && !form_result.reconnects) {
214
- void invalidateAll();
215
- } else {
216
- if (form_result.refreshes) {
217
- apply_refreshes(form_result.refreshes);
218
- }
219
- if (form_result.reconnects) {
220
- apply_reconnections(form_result.reconnects);
221
- }
222
- }
223
- } else {
224
- if (DEV) {
225
- warn_on_missing_issue_reads();
226
- }
191
+ const response = await remote_request(
192
+ `${base}/${app_dir}/remote/${action_id_without_key}`,
193
+ {
194
+ method: 'POST',
195
+ headers: {
196
+ 'Content-Type': BINARY_FORM_CONTENT_TYPE,
197
+ // Forms cannot be called during rendering, so it's save to use location here
198
+ 'x-sveltekit-pathname': location.pathname,
199
+ 'x-sveltekit-search': location.search
200
+ },
201
+ body: blob
227
202
  }
203
+ );
228
204
 
229
- return succeeded;
230
- } else if (form_result.type === 'redirect') {
231
- const stringified_refreshes = form_result.refreshes ?? '';
232
- const stringified_reconnects = form_result.reconnects ?? '';
233
- if (stringified_refreshes) {
234
- apply_refreshes(stringified_refreshes);
235
- }
205
+ ({ issues: raw_issues = [], result } = response._ ?? {});
236
206
 
237
- if (stringified_reconnects) {
238
- apply_reconnections(stringified_reconnects);
239
- }
207
+ // if the developer took control of updates via `.updates(...)` (even with
208
+ // no arguments), or the server performed explicit refreshes, don't invalidateAll
209
+ const should_invalidate = refreshes === null && !response.r;
240
210
 
211
+ if (response.redirect) {
241
212
  // Use internal version to allow redirects to external URLs
242
213
  void _goto(
243
- form_result.location,
214
+ response.redirect,
244
215
  {
245
- invalidateAll:
246
- refreshes === null && !stringified_refreshes && !stringified_reconnects
216
+ invalidateAll: should_invalidate
247
217
  },
248
218
  0
249
219
  );
250
220
  return true;
221
+ }
222
+
223
+ const succeeded = raw_issues.length === 0;
224
+
225
+ if (succeeded) {
226
+ if (should_invalidate) {
227
+ void invalidateAll();
228
+ }
251
229
  } else {
252
- throw new HttpError(form_result.status ?? 500, form_result.error);
230
+ if (DEV) {
231
+ warn_on_missing_issue_reads();
232
+ }
253
233
  }
234
+
235
+ return succeeded;
254
236
  } catch (e) {
255
237
  result = undefined;
238
+ raw_issues = [];
256
239
  throw e;
257
240
  } finally {
258
241
  overrides?.forEach((fn) => fn());
@@ -661,30 +644,27 @@ export function form(id) {
661
644
  if (validated?.issues) {
662
645
  array = validated.issues.map((issue) => normalize_issue(issue, false));
663
646
  } else if (!preflightOnly) {
664
- const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
665
- method: 'POST',
666
- headers: {
667
- 'Content-Type': BINARY_FORM_CONTENT_TYPE,
668
- // Validation should not be and will not be called during rendering, so it's save to use location here
669
- 'x-sveltekit-pathname': location.pathname,
670
- 'x-sveltekit-search': location.search
671
- },
672
- body: serialize_binary_form(data, {
673
- validate_only: true
674
- }).blob
675
- });
676
-
677
- const result = await response.json();
647
+ const result = await remote_request(
648
+ `${base}/${app_dir}/remote/${action_id_without_key}`,
649
+ {
650
+ method: 'POST',
651
+ headers: {
652
+ 'Content-Type': BINARY_FORM_CONTENT_TYPE,
653
+ // Validation should not be and will not be called during rendering, so it's save to use location here
654
+ 'x-sveltekit-pathname': location.pathname,
655
+ 'x-sveltekit-search': location.search
656
+ },
657
+ body: serialize_binary_form(data, {
658
+ validate_only: true
659
+ }).blob
660
+ }
661
+ );
678
662
 
679
663
  if (validate_id !== id) {
680
664
  return;
681
665
  }
682
666
 
683
- if (result.type === 'result') {
684
- array = /** @type {InternalRemoteFormIssue[]} */ (
685
- devalue.parse(result.result, app.decoders)
686
- );
687
- }
667
+ array = /** @type {InternalRemoteFormIssue[]} */ (result._);
688
668
  }
689
669
 
690
670
  if (!includeUntouched && !submitted) {
@@ -2,8 +2,8 @@
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, prerender_responses } from '../client.js';
6
- import { get_remote_request_headers, remote_request } from './shared.svelte.js';
5
+ import { app, goto, prerender_responses } from '../client.js';
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
8
 
9
9
  // Initialize Cache API for prerender functions
@@ -67,7 +67,7 @@ export function prerender(id) {
67
67
  const url = `${base}/${app_dir}/remote/${id}${payload ? `/${payload}` : ''}`;
68
68
 
69
69
  if (Object.hasOwn(prerender_responses, cache_key)) {
70
- const data = prerender_responses[cache_key];
70
+ const data = unwrap_node(prerender_responses[cache_key]);
71
71
 
72
72
  if (prerender_cache) {
73
73
  void put(url, devalue.stringify(data, app.encoders));
@@ -77,6 +77,7 @@ export function prerender(id) {
77
77
  }
78
78
 
79
79
  // Do this here, after await Svelte' reactivity context is gone.
80
+ // TODO we really don't want to be sending these specific headers here?
80
81
  const headers = get_remote_request_headers();
81
82
 
82
83
  // Check the Cache API first
@@ -93,14 +94,21 @@ export function prerender(id) {
93
94
  }
94
95
  }
95
96
 
96
- const encoded = await remote_request(url, headers);
97
+ const result = await remote_request(url, { headers });
98
+
99
+ if (result.redirect) {
100
+ void goto(result.redirect);
101
+ return;
102
+ }
103
+
104
+ const data = result._;
97
105
 
98
106
  // For successful prerender requests, save to cache
99
107
  if (prerender_cache) {
100
- void put(url, encoded);
108
+ void put(url, devalue.stringify(data, app.encoders));
101
109
  }
102
110
 
103
- return devalue.parse(encoded, app.decoders);
111
+ return data;
104
112
  });
105
113
 
106
114
  prerender_resources.set(cache_key, new WeakRef(resource));
@@ -1,10 +1,8 @@
1
1
  /** @import { RemoteQueryFunction } from '@sveltejs/kit' */
2
2
  import { app_dir, base } from '$app/paths/internal/client';
3
- import { app, query_map, query_responses } from '../../client.js';
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
- import * as devalue from 'devalue';
6
5
  import { DEV } from 'esm-env';
7
- import { unfriendly_hydratable } from '../../../shared.js';
8
6
  import { QueryProxy } from './proxy.js';
9
7
 
10
8
  /**
@@ -25,20 +23,14 @@ export function query(id) {
25
23
 
26
24
  /** @type {RemoteQueryFunction<any, any>} */
27
25
  const wrapper = (arg) => {
28
- return new QueryProxy(id, arg, async (key, payload) => {
29
- if (Object.hasOwn(query_responses, key)) {
30
- const value = query_responses[key];
31
- delete query_responses[key];
32
- return value;
33
- }
34
-
26
+ return new QueryProxy(id, arg, async (payload) => {
35
27
  const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
36
28
 
37
- const serialized = await unfriendly_hydratable(key, () =>
38
- remote_request(url, get_remote_request_headers())
39
- );
29
+ const result = await remote_request(url, { headers: get_remote_request_headers() });
40
30
 
41
- return devalue.parse(serialized, app.decoders);
31
+ if (result.redirect) {
32
+ await goto(result.redirect);
33
+ }
42
34
  });
43
35
  };
44
36
 
@@ -1,4 +1,5 @@
1
1
  import { query_responses } from '../../client.js';
2
+ import { HttpError } from '@sveltejs/kit/internal';
2
3
  import { QUERY_OVERRIDE_KEY } from '../shared.svelte.js';
3
4
  import { noop } from '../../../../utils/functions.js';
4
5
  import { with_resolvers } from '../../../../utils/promise.js';
@@ -64,6 +65,17 @@ export class Query {
64
65
  constructor(key, fn) {
65
66
  this.#key = key;
66
67
  this.#fn = fn;
68
+
69
+ if (Object.hasOwn(query_responses, key)) {
70
+ const node = query_responses[key];
71
+ delete query_responses[key];
72
+
73
+ if (node.e) {
74
+ this.fail(new HttpError(node.e[0] ?? 500, node.e[1]));
75
+ } else {
76
+ this.set(/** @type {T} */ (node.v));
77
+ }
78
+ }
67
79
  }
68
80
 
69
81
  #get_promise() {
@@ -199,6 +211,10 @@ export class Query {
199
211
  * @param {T} value
200
212
  */
201
213
  set(value) {
214
+ // normally consumed in the constructor, but make sure a leftover
215
+ // SSR record can never shadow the newly-set value
216
+ delete query_responses[this.#key];
217
+
202
218
  this.#clear_pending();
203
219
  this.#ready = true;
204
220
  this.#loading = false;
@@ -211,6 +227,10 @@ export class Query {
211
227
  * @param {unknown} error
212
228
  */
213
229
  fail(error) {
230
+ // normally consumed in the constructor, but make sure a leftover
231
+ // SSR record can never shadow the newly-set error
232
+ delete query_responses[this.#key];
233
+
214
234
  this.#clear_pending();
215
235
  this.#loading = false;
216
236
  this.#error = error;
@@ -25,7 +25,7 @@ export class QueryProxy {
25
25
  /**
26
26
  * @param {string} id
27
27
  * @param {any} arg
28
- * @param {(key: string, payload: string) => Promise<T>} fn
28
+ * @param {(payload: string) => Promise<T>} fn
29
29
  */
30
30
  constructor(id, arg, fn) {
31
31
  this.#id = id;
@@ -41,7 +41,7 @@ export class QueryProxy {
41
41
  this.#payload,
42
42
  // IMPORTANT: This cannot close over `this` or it becomes impossible to
43
43
  // garbage collect the QueryProxy and thus impossible to evict cache entries.
44
- () => new Query(key, () => fn(key, payload))
44
+ () => new Query(key, () => fn(payload))
45
45
  );
46
46
 
47
47
  cache.ref(this, entry, this.#id, this.#payload);
@@ -98,7 +98,7 @@ export class QueryProxy {
98
98
  this.#payload,
99
99
  // IMPORTANT: This cannot close over `this` or it becomes impossible to
100
100
  // garbage collect the QueryProxy and thus impossible to evict cache entries.
101
- () => new Query(key_ref, () => fn_ref(key_ref, payload_ref))
101
+ () => new Query(key_ref, () => fn_ref(payload_ref))
102
102
  );
103
103
 
104
104
  const deref = cache.manual_ref(entry, this.#id, this.#payload);
@@ -1,12 +1,9 @@
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 } from '../client.js';
5
- import { get_remote_request_headers, QUERY_FUNCTION_ID } from './shared.svelte.js';
3
+ import { goto } from '../client.js';
4
+ import { get_remote_request_headers, QUERY_FUNCTION_ID, remote_request } from './shared.svelte.js';
6
5
  import { QueryProxy } from './query/proxy.js';
7
- import * as devalue from 'devalue';
8
- import { HttpError, Redirect } from '@sveltejs/kit/internal';
9
- import { unfriendly_hydratable } from '../../shared.js';
6
+ import { HttpError } from '@sveltejs/kit/internal';
10
7
 
11
8
  /**
12
9
  * @param {string} id
@@ -18,84 +15,78 @@ export function query_batch(id) {
18
15
 
19
16
  /** @type {RemoteQueryFunction<any, any>} */
20
17
  const wrapper = (arg) => {
21
- return new QueryProxy(id, arg, async (key, payload) => {
22
- const serialized = await unfriendly_hydratable(key, () => {
23
- return new Promise((resolve, reject) => {
24
- // create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
25
- // is invoked multiple times with the same payload, so we need to deduplicate here
26
- const entry = batching.get(payload) ?? [];
27
- entry.push({ resolve, reject });
28
- batching.set(payload, entry);
18
+ return new QueryProxy(id, arg, async (payload) => {
19
+ return await new Promise((resolve, reject) => {
20
+ // create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
21
+ // is invoked multiple times with the same payload, so we need to deduplicate here
22
+ const entry = batching.get(payload) ?? [];
23
+ entry.push({ resolve, reject });
24
+ batching.set(payload, entry);
29
25
 
30
- if (batching.size > 1) return;
26
+ if (batching.size > 1) return;
31
27
 
32
- // Do this here, after await Svelte' reactivity context is gone.
33
- // TODO is it possible to have batches of the same key
34
- // but in different forks/async contexts and in the same macrotask?
35
- // If so this would potentially be buggy
36
- const headers = {
37
- 'Content-Type': 'application/json',
38
- ...get_remote_request_headers()
39
- };
28
+ // Do this here, after await Svelte' reactivity context is gone.
29
+ // TODO is it possible to have batches of the same key
30
+ // but in different forks/async contexts and in the same macrotask?
31
+ // If so this would potentially be buggy
32
+ const headers = {
33
+ 'Content-Type': 'application/json',
34
+ ...get_remote_request_headers()
35
+ };
40
36
 
41
- // Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
42
- // and flushes could reveal more queries that should be batched.
43
- setTimeout(async () => {
44
- const batched = batching;
45
- batching = new Map();
37
+ // Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
38
+ // and flushes could reveal more queries that should be batched.
39
+ setTimeout(async () => {
40
+ const batched = batching;
41
+ batching = new Map();
46
42
 
47
- try {
48
- const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
49
- method: 'POST',
50
- body: JSON.stringify({
51
- payloads: Array.from(batched.keys())
52
- }),
53
- headers
54
- });
43
+ try {
44
+ const response = await remote_request(`${base}/${app_dir}/remote/${id}`, {
45
+ method: 'POST',
46
+ body: JSON.stringify({
47
+ payloads: Array.from(batched.keys())
48
+ }),
49
+ headers
50
+ });
55
51
 
56
- if (!response.ok) {
57
- throw new Error('Failed to execute batch query');
58
- }
52
+ if (response.redirect) {
53
+ await goto(response.redirect);
59
54
 
60
- const result = /** @type {RemoteFunctionResponse} */ (await response.json());
61
- if (result.type === 'error') {
62
- throw new HttpError(result.status ?? 500, result.error);
55
+ // settle all batched promises (with `undefined`, like a redirect
56
+ // from a non-batched query) so that callers don't hang forever
57
+ for (const resolvers of batched.values()) {
58
+ for (const { resolve } of resolvers) {
59
+ resolve(undefined);
60
+ }
63
61
  }
64
62
 
65
- if (result.type === 'redirect') {
66
- await goto(result.location);
67
- throw new Redirect(307, result.location);
68
- }
63
+ return;
64
+ }
69
65
 
70
- const results = devalue.parse(result.result, app.decoders);
66
+ const results = response._;
67
+ let i = 0;
71
68
 
72
- // Resolve individual queries
73
- // Maps guarantee insertion order so we can do it like this
74
- let i = 0;
69
+ for (const resolvers of batched.values()) {
70
+ const result = results[i];
75
71
 
76
- for (const resolvers of batched.values()) {
77
- for (const { resolve, reject } of resolvers) {
78
- if (results[i].type === 'error') {
79
- reject(new HttpError(results[i].status, results[i].error));
80
- } else {
81
- resolve(results[i].data);
82
- }
72
+ for (const { resolve, reject } of resolvers) {
73
+ if (result.type === 'error') {
74
+ reject(new HttpError(result.status, result.error));
75
+ } else {
76
+ resolve(result.data);
83
77
  }
84
- i++;
85
78
  }
86
- } catch (error) {
87
- // Reject all queries in the batch
88
- for (const resolver of batched.values()) {
89
- for (const { reject } of resolver) {
90
- reject(error);
91
- }
79
+ i++;
80
+ }
81
+ } catch (e) {
82
+ for (const resolvers of batched.values()) {
83
+ for (const { reject } of resolvers) {
84
+ reject(e);
92
85
  }
93
86
  }
94
- }, 0);
95
- });
87
+ }
88
+ }, 0);
96
89
  });
97
-
98
- return devalue.parse(serialized, app.decoders);
99
90
  });
100
91
  };
101
92
 
@@ -1,12 +1,10 @@
1
1
  /** @import { PromiseWithResolvers } from '../../../../utils/promise.js' */
2
- import { app } from '../../client.js';
3
- import * as devalue from 'devalue';
2
+ import { query_responses } from '../../client.js';
4
3
  import { HttpError, Redirect } from '@sveltejs/kit/internal';
5
4
  import { noop, once } from '../../../../utils/functions.js';
6
5
  import { with_resolvers } from '../../../../utils/promise.js';
7
6
  import { SharedIterator } from '../../../../utils/shared-iterator.js';
8
7
  import { tick } from 'svelte';
9
- import { unfriendly_hydratable } from '../../../shared.js';
10
8
  import { create_live_iterator } from './iterator.js';
11
9
 
12
10
  /**
@@ -87,9 +85,25 @@ export class LiveQuery {
87
85
  this.#resolve_first = resolve;
88
86
  this.#reject_first = reject;
89
87
 
90
- const serialized = unfriendly_hydratable(key, () => undefined);
91
- if (serialized !== undefined) {
92
- this.set(devalue.parse(serialized, app.decoders));
88
+ if (Object.hasOwn(query_responses, key)) {
89
+ const node = query_responses[key];
90
+ delete query_responses[key];
91
+
92
+ if (node.e) {
93
+ // the query failed during SSR — seed the failed state (mirroring `fail()`,
94
+ // minus its terminal `#done`), so the main loop still connects as usual
95
+ // and the query can recover
96
+ const error = new HttpError(node.e[0] ?? 500, node.e[1]);
97
+ this.#loading = false;
98
+ this.#error = error;
99
+
100
+ promise.catch(noop);
101
+ this.#reject_first?.(error);
102
+ this.#resolve_first = null;
103
+ this.#reject_first = null;
104
+ } else {
105
+ this.set(node.v);
106
+ }
93
107
  }
94
108
  }
95
109