@sveltejs/kit 3.0.0-next.0 → 3.0.0-next.10

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 (127) hide show
  1. package/package.json +33 -21
  2. package/src/core/adapt/builder.js +29 -43
  3. package/src/core/adapt/index.js +1 -4
  4. package/src/core/config/index.js +132 -71
  5. package/src/core/config/options.js +294 -244
  6. package/src/core/config/types.d.ts +1 -1
  7. package/src/core/env.js +121 -5
  8. package/src/core/generate_manifest/index.js +12 -15
  9. package/src/core/postbuild/analyse.js +7 -19
  10. package/src/core/postbuild/crawl.js +22 -6
  11. package/src/core/postbuild/prerender.js +42 -23
  12. package/src/core/sync/create_manifest_data/index.js +53 -50
  13. package/src/core/sync/sync.js +0 -2
  14. package/src/core/sync/write_client_manifest.js +8 -15
  15. package/src/core/sync/write_env.js +2 -1
  16. package/src/core/sync/write_non_ambient.js +12 -9
  17. package/src/core/sync/write_server.js +13 -14
  18. package/src/core/sync/write_tsconfig.js +16 -8
  19. package/src/core/sync/write_tsconfig_test/package.json +7 -0
  20. package/src/core/sync/write_types/index.js +28 -24
  21. package/src/core/utils.js +2 -2
  22. package/src/exports/env/index.js +12 -0
  23. package/src/exports/hooks/index.js +3 -9
  24. package/src/exports/hooks/sequence.js +3 -2
  25. package/src/exports/index.js +35 -13
  26. package/src/exports/internal/client.js +5 -0
  27. package/src/exports/internal/env.js +2 -2
  28. package/src/exports/internal/index.js +1 -90
  29. package/src/exports/internal/{event.js → server/event.js} +1 -2
  30. package/src/exports/internal/server/index.js +33 -0
  31. package/src/exports/internal/shared.js +89 -0
  32. package/src/exports/node/index.js +62 -15
  33. package/src/exports/params.js +63 -0
  34. package/src/exports/public.d.ts +323 -170
  35. package/src/exports/url.js +86 -0
  36. package/src/exports/vite/build/build_server.js +53 -59
  37. package/src/exports/vite/build/remote.js +18 -11
  38. package/src/exports/vite/build/utils.js +0 -8
  39. package/src/exports/vite/dev/index.js +42 -44
  40. package/src/exports/vite/index.js +810 -593
  41. package/src/exports/vite/preview/index.js +29 -22
  42. package/src/exports/vite/static_analysis/index.js +2 -4
  43. package/src/exports/vite/static_analysis/types.d.ts +14 -0
  44. package/src/exports/vite/utils.js +21 -36
  45. package/src/runtime/app/env/internal.js +4 -4
  46. package/src/runtime/app/env/types.d.ts +1 -1
  47. package/src/runtime/app/environment/index.js +6 -0
  48. package/src/runtime/app/forms.js +24 -7
  49. package/src/runtime/app/paths/client.js +4 -10
  50. package/src/runtime/app/paths/internal/client.js +4 -2
  51. package/src/runtime/app/paths/internal/server.js +2 -23
  52. package/src/runtime/app/paths/public.d.ts +0 -28
  53. package/src/runtime/app/paths/server.js +9 -5
  54. package/src/runtime/app/server/remote/command.js +0 -3
  55. package/src/runtime/app/server/remote/form.js +27 -15
  56. package/src/runtime/app/server/remote/prerender.js +29 -36
  57. package/src/runtime/app/server/remote/query.js +101 -99
  58. package/src/runtime/app/server/remote/requested.js +22 -14
  59. package/src/runtime/app/server/remote/shared.js +30 -26
  60. package/src/runtime/app/state/client.js +1 -2
  61. package/src/runtime/app/stores.js +13 -76
  62. package/src/runtime/client/bundle.js +1 -1
  63. package/src/runtime/client/client-entry.js +3 -0
  64. package/src/runtime/client/client.js +411 -318
  65. package/src/runtime/client/entry.js +24 -3
  66. package/src/runtime/client/fetcher.js +3 -2
  67. package/src/runtime/client/ndjson.js +6 -33
  68. package/src/runtime/client/payload.js +17 -0
  69. package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
  70. package/src/runtime/client/remote-functions/command.svelte.js +7 -32
  71. package/src/runtime/client/remote-functions/form.svelte.js +183 -112
  72. package/src/runtime/client/remote-functions/prerender.svelte.js +29 -8
  73. package/src/runtime/client/remote-functions/query/index.js +7 -14
  74. package/src/runtime/client/remote-functions/query/instance.svelte.js +63 -18
  75. package/src/runtime/client/remote-functions/query/proxy.js +3 -3
  76. package/src/runtime/client/remote-functions/query-batch.svelte.js +60 -68
  77. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +60 -18
  78. package/src/runtime/client/remote-functions/query-live/iterator.js +36 -55
  79. package/src/runtime/client/remote-functions/shared.svelte.js +88 -62
  80. package/src/runtime/client/sse.js +32 -0
  81. package/src/runtime/client/state.svelte.js +65 -49
  82. package/src/runtime/client/stream.js +38 -0
  83. package/src/runtime/client/types.d.ts +11 -7
  84. package/src/runtime/client/utils.js +0 -96
  85. package/src/runtime/components/root.svelte +66 -0
  86. package/src/runtime/env/dynamic/private.js +7 -0
  87. package/src/runtime/env/dynamic/public.js +7 -0
  88. package/src/runtime/env/static/private.js +6 -0
  89. package/src/runtime/env/static/public.js +6 -0
  90. package/src/runtime/form-utils.js +100 -22
  91. package/src/runtime/server/cookie.js +69 -52
  92. package/src/runtime/server/csrf.js +65 -0
  93. package/src/runtime/server/data/index.js +14 -18
  94. package/src/runtime/server/env_module.js +0 -5
  95. package/src/runtime/server/index.js +2 -2
  96. package/src/runtime/server/page/actions.js +41 -26
  97. package/src/runtime/server/page/index.js +9 -15
  98. package/src/runtime/server/page/load_data.js +1 -1
  99. package/src/runtime/server/page/render.js +112 -187
  100. package/src/runtime/server/page/respond_with_error.js +7 -8
  101. package/src/runtime/server/page/server_routing.js +27 -18
  102. package/src/runtime/server/remote.js +355 -216
  103. package/src/runtime/server/respond.js +97 -61
  104. package/src/runtime/server/utils.js +32 -9
  105. package/src/runtime/shared.js +83 -13
  106. package/src/runtime/telemetry/otel.js +1 -1
  107. package/src/runtime/types.d.ts +8 -0
  108. package/src/types/ambient.d.ts +10 -5
  109. package/src/types/global-private.d.ts +15 -22
  110. package/src/types/internal.d.ts +89 -74
  111. package/src/types/private.d.ts +33 -1
  112. package/src/utils/error.js +24 -4
  113. package/src/utils/import.js +6 -1
  114. package/src/utils/imports.js +83 -0
  115. package/src/utils/mime.js +9 -0
  116. package/src/utils/params.js +66 -0
  117. package/src/utils/routing.js +90 -44
  118. package/src/utils/shared-iterator.js +5 -0
  119. package/src/utils/streaming.js +14 -4
  120. package/src/utils/url.js +20 -2
  121. package/src/version.js +1 -1
  122. package/types/index.d.ts +467 -537
  123. package/types/index.d.ts.map +22 -39
  124. package/src/core/sync/write_root.js +0 -148
  125. package/src/exports/internal/server.js +0 -22
  126. package/src/types/synthetic/$lib.md +0 -5
  127. /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
@@ -1,4 +1,5 @@
1
- import { query_responses } from '../../client.js';
1
+ import { query_responses, handle_error } 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 { tick, untrack } from 'svelte';
@@ -36,7 +37,7 @@ export class Query {
36
37
  return this.#overrides.reduce((v, r) => r(v), /** @type {T} */ (this.#raw));
37
38
  });
38
39
 
39
- /** @type {any} */
40
+ /** @type {App.Error | undefined} */
40
41
  #error = $state.raw(undefined);
41
42
 
42
43
  /** @type {Promise<T>['then']} */
@@ -63,6 +64,17 @@ export class Query {
63
64
  constructor(key, fn) {
64
65
  this.#key = key;
65
66
  this.#fn = fn;
67
+
68
+ if (Object.hasOwn(query_responses, key)) {
69
+ const node = query_responses[key];
70
+ delete query_responses[key];
71
+
72
+ if (node.e) {
73
+ this.fail(new HttpError(node.e.status, node.e));
74
+ } else {
75
+ this.set(/** @type {T} */ (node.v));
76
+ }
77
+ }
66
78
  }
67
79
 
68
80
  #get_promise() {
@@ -70,12 +82,14 @@ export class Query {
70
82
  return /** @type {Promise<T>} */ (this.#promise);
71
83
  }
72
84
 
73
- #start() {
85
+ start() {
74
86
  // there is a really weird bug with untrack and writes and initializations
75
87
  // every time you see this comment, try removing the `tick.then` here and see
76
88
  // if all the tests still pass with the latest svelte version
77
89
  // if they do, congrats, you can remove tick.then
78
- void tick().then(() => this.#get_promise());
90
+ void tick()
91
+ .then(() => this.#get_promise())
92
+ .catch(noop);
79
93
  }
80
94
 
81
95
  #clear_pending() {
@@ -88,6 +102,11 @@ export class Query {
88
102
 
89
103
  const { promise, resolve, reject } = Promise.withResolvers();
90
104
 
105
+ // the rejection is surfaced via `.error` / the `then` getter for awaiting
106
+ // consumers — a purely reactive consumer (`.current`) attaches no handler,
107
+ // so make sure the stored promise can never become an unhandled rejection
108
+ promise.catch(noop);
109
+
91
110
  this.#latest.push(resolve);
92
111
 
93
112
  Promise.resolve(this.#fn())
@@ -107,22 +126,33 @@ export class Query {
107
126
 
108
127
  resolve(undefined);
109
128
  })
110
- .catch((e) => {
129
+ .catch(async (e) => {
111
130
  // TODO: Our behavior here could be better:
112
131
  // - We should not reject on redirects, but should hook into the router
113
132
  // to ensure the query is properly refreshed before the navigation completes
114
133
  // - Instead of failing on transport-level errors, we should probably do what
115
134
  // LiveQuery does and preserve the last known good value and retry the connection
135
+ if (this.#latest.indexOf(resolve) === -1) return;
136
+
137
+ const error = await handle_error(e, {
138
+ params: {},
139
+ route: { id: null },
140
+ url: new URL(location.href)
141
+ });
142
+
143
+ // Re-check after the async `handle_error` gap: a later request may have
144
+ // resolved/rejected while we were awaiting and superseded this one, so
145
+ // recompute the index and bail out if this request is no longer current
116
146
  const idx = this.#latest.indexOf(resolve);
117
147
  if (idx === -1) return;
118
148
 
119
149
  untrack(() => {
120
150
  this.#latest.splice(0, idx).forEach((r) => r(undefined));
121
- this.#error = e;
151
+ this.#error = error;
122
152
  this.#loading = false;
123
153
  });
124
154
 
125
- reject(e);
155
+ reject(new HttpError(error.status, error)); // so that transformError doesn't transform it again
126
156
  });
127
157
 
128
158
  return promise;
@@ -131,12 +161,12 @@ export class Query {
131
161
  get then() {
132
162
  // TODO this should be unnecessary but due to the bug described
133
163
  // in #start, we need to do this in some circumstances
134
- this.#start();
164
+ this.start();
135
165
  return this.#then;
136
166
  }
137
167
 
138
168
  get catch() {
139
- this.#start();
169
+ this.start();
140
170
  this.#then;
141
171
  return (/** @type {any} */ reject) => {
142
172
  return this.#then(undefined, reject);
@@ -144,7 +174,7 @@ export class Query {
144
174
  }
145
175
 
146
176
  get finally() {
147
- this.#start();
177
+ this.start();
148
178
  this.#then;
149
179
  return (/** @type {any} */ fn) => {
150
180
  return this.#then(
@@ -161,12 +191,12 @@ export class Query {
161
191
  }
162
192
 
163
193
  get current() {
164
- this.#start();
194
+ this.start();
165
195
  return this.#current;
166
196
  }
167
197
 
168
198
  get error() {
169
- this.#start();
199
+ this.start();
170
200
  return this.#error;
171
201
  }
172
202
 
@@ -174,7 +204,7 @@ export class Query {
174
204
  * Returns true if the resource is loading or reloading.
175
205
  */
176
206
  get loading() {
177
- this.#start();
207
+ this.start();
178
208
  return this.#loading;
179
209
  }
180
210
 
@@ -182,7 +212,7 @@ export class Query {
182
212
  * Returns true once the resource has been loaded for the first time.
183
213
  */
184
214
  get ready() {
185
- this.#start();
215
+ this.start();
186
216
  return this.#ready;
187
217
  }
188
218
 
@@ -198,6 +228,10 @@ export class Query {
198
228
  * @param {T} value
199
229
  */
200
230
  set(value) {
231
+ // normally consumed in the constructor, but make sure a leftover
232
+ // SSR record can never shadow the newly-set value
233
+ delete query_responses[this.#key];
234
+
201
235
  this.#clear_pending();
202
236
  this.#ready = true;
203
237
  this.#loading = false;
@@ -206,13 +240,15 @@ export class Query {
206
240
  this.#promise = Promise.resolve();
207
241
  }
208
242
 
209
- /**
210
- * @param {unknown} error
211
- */
243
+ /** @param {HttpError} error */
212
244
  fail(error) {
245
+ // normally consumed in the constructor, but make sure a leftover
246
+ // SSR record can never shadow the newly-set error
247
+ delete query_responses[this.#key];
248
+
213
249
  this.#clear_pending();
214
250
  this.#loading = false;
215
- this.#error = error;
251
+ this.#error = error.body;
216
252
 
217
253
  const promise = Promise.reject(error);
218
254
 
@@ -242,6 +278,15 @@ export class Query {
242
278
  return release;
243
279
  }
244
280
 
281
+ /**
282
+ * Reset ahead of a navigation that invalidates all, to force newly
283
+ * rendered queries to get fresh data
284
+ */
285
+ reset() {
286
+ this.#promise = null;
287
+ delete query_responses[this.#key];
288
+ }
289
+
245
290
  get [Symbol.toStringTag]() {
246
291
  return 'Query';
247
292
  }
@@ -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 { hydratable } from 'svelte';
6
+ import { HttpError } from '@sveltejs/kit/internal';
10
7
 
11
8
  /**
12
9
  * @param {string} id
@@ -18,84 +15,79 @@ 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 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
+ // Use internal version to allow redirects to external URLs
54
+ await _goto(response.redirect, {}, 0);
59
55
 
60
- const result = /** @type {RemoteFunctionResponse} */ (await response.json());
61
- if (result.type === 'error') {
62
- throw new HttpError(result.status ?? 500, result.error);
56
+ // settle all batched promises (with `undefined`, like a redirect
57
+ // from a non-batched query) so that callers don't hang forever
58
+ for (const resolvers of batched.values()) {
59
+ for (const { resolve } of resolvers) {
60
+ resolve(undefined);
61
+ }
63
62
  }
64
63
 
65
- if (result.type === 'redirect') {
66
- await goto(result.location);
67
- throw new Redirect(307, result.location);
68
- }
64
+ return;
65
+ }
69
66
 
70
- const results = devalue.parse(result.result, app.decoders);
67
+ const results = response._;
68
+ let i = 0;
71
69
 
72
- // Resolve individual queries
73
- // Maps guarantee insertion order so we can do it like this
74
- let i = 0;
70
+ for (const resolvers of batched.values()) {
71
+ const result = results[i];
75
72
 
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
- }
73
+ for (const { resolve, reject } of resolvers) {
74
+ if (result.type === 'error') {
75
+ reject(new HttpError(result.error.status, result.error));
76
+ } else {
77
+ resolve(result.data);
83
78
  }
84
- i++;
85
79
  }
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
- }
80
+ i++;
81
+ }
82
+ } catch (e) {
83
+ for (const resolvers of batched.values()) {
84
+ for (const { reject } of resolvers) {
85
+ reject(e);
92
86
  }
93
87
  }
94
- }, 0);
95
- });
88
+ }
89
+ }, 0);
96
90
  });
97
-
98
- return devalue.parse(serialized, app.decoders);
99
91
  });
100
92
  };
101
93
 
@@ -1,9 +1,8 @@
1
- import { app } from '../../client.js';
2
- import * as devalue from 'devalue';
1
+ import { query_responses, handle_error } from '../../client.js';
3
2
  import { HttpError, Redirect } from '@sveltejs/kit/internal';
4
3
  import { noop, once } from '../../../../utils/functions.js';
5
4
  import { SharedIterator } from '../../../../utils/shared-iterator.js';
6
- import { hydratable, tick } from 'svelte';
5
+ import { tick } from 'svelte';
7
6
  import { create_live_iterator } from './iterator.js';
8
7
 
9
8
  /**
@@ -25,7 +24,7 @@ export class LiveQuery {
25
24
  #done = $state(false);
26
25
  /** @type {T | undefined} */
27
26
  #raw = $state.raw();
28
- /** @type {any} */
27
+ /** @type {App.Error | undefined} */
29
28
  #error = $state.raw(undefined);
30
29
  /** @type {Promise<void>} */
31
30
  #promise;
@@ -84,9 +83,25 @@ export class LiveQuery {
84
83
  this.#resolve_first = resolve;
85
84
  this.#reject_first = reject;
86
85
 
87
- const serialized = hydratable(key, () => undefined);
88
- if (serialized !== undefined) {
89
- this.set(devalue.parse(serialized, app.decoders));
86
+ if (Object.hasOwn(query_responses, key)) {
87
+ const node = query_responses[key];
88
+ delete query_responses[key];
89
+
90
+ if (node.e) {
91
+ // the query failed during SSR — seed the failed state (mirroring `fail()`,
92
+ // minus its terminal `#done`), so the main loop still connects as usual
93
+ // and the query can recover
94
+ const error = new HttpError(node.e.status, node.e);
95
+ this.#loading = false;
96
+ this.#error = error.body;
97
+
98
+ promise.catch(noop);
99
+ this.#reject_first?.(error);
100
+ this.#resolve_first = null;
101
+ this.#reject_first = null;
102
+ } else {
103
+ this.set(node.v);
104
+ }
90
105
  }
91
106
  }
92
107
 
@@ -107,6 +122,7 @@ export class LiveQuery {
107
122
 
108
123
  /** @type {PromiseWithResolvers<void>} */
109
124
  const { promise: stopped, resolve: on_stop } = Promise.withResolvers();
125
+ let connected = false;
110
126
 
111
127
  while (!this.#done) {
112
128
  const controller = new AbortController();
@@ -119,16 +135,22 @@ export class LiveQuery {
119
135
  const generator = create_live_iterator(this.#id, this.#payload, controller, () => {
120
136
  this.#connected = true;
121
137
  this.#attempt = 0;
138
+ connected = true;
122
139
  on_connect();
123
140
  });
124
141
 
125
142
  try {
126
143
  const { done, value } = await generator.next();
127
144
 
128
- // TODO how much special handling does this need?
129
- // should we even try to reconnect if this is the case?
130
- if (done && !this.#ready) {
131
- throw new Error('Live query completed before yielding a value');
145
+ if (done) {
146
+ if (!this.#ready) {
147
+ throw new Error('Live query completed before yielding a value');
148
+ }
149
+ // stream completed without yielding (e.g. the generator returned
150
+ // immediately on reconnect) — keep the last good value
151
+ this.#done = true;
152
+ this.#fan_out.done();
153
+ break;
132
154
  }
133
155
 
134
156
  this.set(value);
@@ -153,7 +175,7 @@ export class LiveQuery {
153
175
 
154
176
  if (!this.#ready) {
155
177
  // If we haven't successfully connected and received a value yet, surface the error
156
- this.fail(error);
178
+ await this.#fail(error);
157
179
  on_connect_failed(error);
158
180
  break;
159
181
  }
@@ -185,6 +207,12 @@ export class LiveQuery {
185
207
  }
186
208
 
187
209
  this.#interrupt = null;
210
+ // If the loop exited without ever successfully connecting, settle the
211
+ // reconnect handshake so callers (e.g. `invalidateAll()`) never await
212
+ // a forever-pending promise.
213
+ if (!connected) {
214
+ on_connect_failed(this.#error ?? new Error('Live query connection was interrupted'));
215
+ }
188
216
  on_stop();
189
217
  }
190
218
 
@@ -328,10 +356,13 @@ export class LiveQuery {
328
356
  promise.catch(noop);
329
357
  this.#done = false;
330
358
  this.#attempt = 0;
331
- // The previous fan-out may have been closed by `done()`/`fail()`. Future
332
- // `for await` consumers need a fresh, open fan-out attached to the new
333
- // `#main` lifetime.
334
- this.#fan_out = new SharedIterator();
359
+ // Keep the existing fan-out open so active `for await` consumers
360
+ // continue receiving values from the new connection without interruption.
361
+ // Only replace it if it was already closed by a prior `done()`/`fail()`
362
+ // (e.g. reconnecting after a finite or hard-failed stream)
363
+ if (this.#fan_out.closed) {
364
+ this.#fan_out = new SharedIterator();
365
+ }
335
366
  this.#main({ on_connect, on_connect_failed }).catch(noop);
336
367
  await promise;
337
368
  }
@@ -354,10 +385,10 @@ export class LiveQuery {
354
385
  this.#fan_out.push(value);
355
386
  }
356
387
 
357
- /** @param {unknown} error */
388
+ /** @param {HttpError} error */
358
389
  fail(error) {
359
390
  this.#loading = false;
360
- this.#error = error;
391
+ this.#error = error.body;
361
392
  // `fail` is terminal — once a live query has hard-failed, the only way to start
362
393
  // streaming again is via `reconnect()`. Mark it done and abort any in-flight
363
394
  // request so that callers from outside the main loop (e.g. `apply_reconnections`)
@@ -366,6 +397,7 @@ export class LiveQuery {
366
397
  void this.#interrupt?.();
367
398
 
368
399
  if (this.#reject_first) {
400
+ this.#promise.catch(noop);
369
401
  this.#reject_first(error);
370
402
  this.#resolve_first = null;
371
403
  this.#reject_first = null;
@@ -378,6 +410,16 @@ export class LiveQuery {
378
410
  this.#fan_out.fail(error);
379
411
  }
380
412
 
413
+ /** @param {unknown} e */
414
+ async #fail(e) {
415
+ const error = await handle_error(e, {
416
+ params: {},
417
+ route: { id: null },
418
+ url: new URL(location.href)
419
+ });
420
+ this.fail(new HttpError(error.status, error));
421
+ }
422
+
381
423
  get [Symbol.toStringTag]() {
382
424
  return 'LiveQuery';
383
425
  }
@@ -4,21 +4,28 @@ import { get_remote_request_headers, handle_side_channel_response } from '../sha
4
4
  import * as devalue from 'devalue';
5
5
  import { HttpError } from '@sveltejs/kit/internal';
6
6
  import { noop } from '../../../../utils/functions.js';
7
- import { read_ndjson } from '../../ndjson.js';
7
+ import { read_sse } from '../../sse.js';
8
8
 
9
9
  /**
10
- * @param {Response} response
11
- * @returns {Promise<ReadableStreamDefaultReader<Uint8Array>>}
10
+ * @template T
11
+ * @param {string} id
12
+ * @param {string} payload
13
+ * @param {AbortController} [controller]
14
+ * @param {() => void} [on_connect]
15
+ * @returns {AsyncGenerator<T>}
12
16
  */
13
- async function get_stream_reader(response) {
14
- const content_type = response.headers.get('content-type') ?? '';
17
+ export async function* create_live_iterator(
18
+ id,
19
+ payload,
20
+ controller = new AbortController(),
21
+ on_connect = noop
22
+ ) {
23
+ const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
15
24
 
16
- if (response.ok && content_type.includes('application/json')) {
17
- // we can end up here if we e.g. redirect in `handle`
18
- const result = await response.json();
19
- await handle_side_channel_response(result);
20
- throw new HttpError(500, 'Invalid query.live response');
21
- }
25
+ const response = await fetch(url, {
26
+ headers: get_remote_request_headers(),
27
+ signal: controller.signal
28
+ });
22
29
 
23
30
  if (!response.ok) {
24
31
  const result = await response.json().catch(() => ({
@@ -30,60 +37,34 @@ async function get_stream_reader(response) {
30
37
  throw new HttpError(result.status ?? response.status ?? 500, result.error);
31
38
  }
32
39
 
33
- if (!response.body) {
34
- throw new Error('Expected query.live response body to be a ReadableStream');
40
+ if (response.headers.get('content-type')?.includes('application/json')) {
41
+ // we can end up here if we e.g. redirect in `handle`
42
+ const result = await response.json();
43
+ await handle_side_channel_response(result);
44
+ throw new HttpError(500, 'Invalid query.live response');
35
45
  }
36
46
 
37
- return response.body.getReader();
38
- }
39
-
40
- /**
41
- * Yields deserialized results from a ReadableStream of newline-delimited JSON
42
- * @param {ReadableStreamDefaultReader<Uint8Array>} reader
43
- */
44
- async function* read_live_ndjson(reader) {
45
- for await (const node of read_ndjson(reader)) {
46
- if (node.type === 'result') {
47
- yield devalue.parse(node.result, app.decoders);
48
- continue;
49
- }
50
-
51
- await handle_side_channel_response(node);
52
- throw new HttpError(500, 'Invalid query.live response');
47
+ if (!response.body) {
48
+ throw new Error('Expected query.live response body to be a ReadableStream');
53
49
  }
54
- }
55
50
 
56
- /**
57
- * @template T
58
- * @param {string} id
59
- * @param {string} payload
60
- * @param {AbortController} [controller]
61
- * @param {() => void} [on_connect]
62
- * @returns {AsyncGenerator<T>}
63
- */
64
- export async function* create_live_iterator(
65
- id,
66
- payload,
67
- controller = new AbortController(),
68
- on_connect = noop
69
- ) {
70
- const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
71
- /** @type {ReadableStreamDefaultReader<Uint8Array> | null} */
72
- let reader = null;
51
+ const reader = response.body.getReader();
73
52
 
74
53
  try {
75
- const response = await fetch(url, {
76
- headers: get_remote_request_headers(),
77
- signal: controller.signal
78
- });
79
- reader = await get_stream_reader(response);
80
-
81
54
  on_connect();
82
55
 
83
- yield* read_live_ndjson(reader);
56
+ for await (const node of read_sse(reader)) {
57
+ if (node.type === 'result') {
58
+ yield devalue.parse(node.result, app.decoders);
59
+ continue;
60
+ }
61
+
62
+ await handle_side_channel_response(node);
63
+ throw new HttpError(500, 'Invalid query.live response');
64
+ }
84
65
  } finally {
85
66
  try {
86
- await reader?.cancel();
67
+ await reader.cancel();
87
68
  } catch {
88
69
  // already closed
89
70
  }