@sveltejs/kit 3.0.0-next.1 → 3.0.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +33 -21
- package/src/constants.js +2 -0
- package/src/core/adapt/builder.js +32 -49
- package/src/core/adapt/index.js +1 -4
- package/src/core/config/index.js +137 -71
- package/src/core/config/options.js +236 -244
- package/src/core/config/types.d.ts +1 -1
- package/src/core/env.js +121 -5
- package/src/core/generate_manifest/index.js +12 -15
- package/src/core/postbuild/analyse.js +7 -19
- package/src/core/postbuild/crawl.js +22 -6
- package/src/core/postbuild/fallback.js +2 -1
- package/src/core/postbuild/prerender.js +114 -48
- package/src/core/postbuild/queue.js +2 -1
- package/src/core/sync/create_manifest_data/index.js +53 -50
- package/src/core/sync/sync.js +0 -2
- package/src/core/sync/write_client_manifest.js +8 -15
- package/src/core/sync/write_env.js +2 -1
- package/src/core/sync/write_non_ambient.js +12 -9
- package/src/core/sync/write_server.js +14 -15
- package/src/core/sync/write_tsconfig.js +16 -8
- package/src/core/sync/write_tsconfig_test/package.json +7 -0
- package/src/core/sync/write_types/index.js +28 -24
- package/src/core/utils.js +30 -5
- package/src/exports/env/index.js +77 -0
- package/src/exports/hooks/index.js +3 -9
- package/src/exports/hooks/sequence.js +3 -2
- package/src/exports/index.js +35 -13
- package/src/exports/internal/client.js +5 -0
- package/src/exports/internal/env.js +8 -5
- package/src/exports/internal/index.js +1 -90
- package/src/exports/internal/{event.js → server/event.js} +1 -2
- package/src/exports/internal/server/index.js +33 -0
- package/src/exports/internal/shared.js +89 -0
- package/src/exports/node/index.js +62 -15
- package/src/exports/params.js +68 -0
- package/src/exports/public.d.ts +363 -189
- package/src/exports/url.js +86 -0
- package/src/exports/vite/build/build_server.js +53 -59
- package/src/exports/vite/build/remote.js +18 -11
- package/src/exports/vite/build/utils.js +0 -8
- package/src/exports/vite/dev/index.js +92 -50
- package/src/exports/vite/index.js +903 -681
- package/src/exports/vite/module_ids.js +0 -2
- package/src/exports/vite/preview/index.js +32 -24
- package/src/exports/vite/static_analysis/index.js +2 -4
- package/src/exports/vite/static_analysis/types.d.ts +14 -0
- package/src/exports/vite/utils.js +40 -35
- package/src/runtime/app/env/internal.js +4 -4
- package/src/runtime/app/env/types.d.ts +1 -1
- package/src/runtime/app/environment/index.js +3 -3
- package/src/runtime/app/forms.js +24 -7
- package/src/runtime/app/paths/client.js +4 -10
- package/src/runtime/app/paths/internal/client.js +4 -2
- package/src/runtime/app/paths/internal/server.js +2 -23
- package/src/runtime/app/paths/public.d.ts +0 -28
- package/src/runtime/app/paths/server.js +10 -6
- package/src/runtime/app/server/index.js +1 -1
- package/src/runtime/app/server/remote/command.js +0 -3
- package/src/runtime/app/server/remote/form.js +27 -15
- package/src/runtime/app/server/remote/prerender.js +29 -36
- package/src/runtime/app/server/remote/query.js +101 -99
- package/src/runtime/app/server/remote/requested.js +22 -14
- package/src/runtime/app/server/remote/shared.js +30 -26
- package/src/runtime/app/state/client.js +1 -2
- package/src/runtime/app/stores.js +13 -76
- package/src/runtime/client/bundle.js +1 -1
- package/src/runtime/client/client-entry.js +3 -0
- package/src/runtime/client/client.js +411 -318
- package/src/runtime/client/entry.js +24 -3
- package/src/runtime/client/fetcher.js +3 -2
- package/src/runtime/client/ndjson.js +6 -33
- package/src/runtime/client/payload.js +17 -0
- package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
- package/src/runtime/client/remote-functions/command.svelte.js +7 -32
- package/src/runtime/client/remote-functions/form.svelte.js +183 -112
- package/src/runtime/client/remote-functions/prerender.svelte.js +29 -8
- package/src/runtime/client/remote-functions/query/index.js +7 -14
- package/src/runtime/client/remote-functions/query/instance.svelte.js +63 -18
- package/src/runtime/client/remote-functions/query/proxy.js +3 -3
- package/src/runtime/client/remote-functions/query-batch.svelte.js +60 -68
- package/src/runtime/client/remote-functions/query-live/instance.svelte.js +60 -18
- package/src/runtime/client/remote-functions/query-live/iterator.js +36 -55
- package/src/runtime/client/remote-functions/shared.svelte.js +88 -62
- package/src/runtime/client/sse.js +32 -0
- package/src/runtime/client/state.svelte.js +65 -49
- package/src/runtime/client/stream.js +39 -0
- package/src/runtime/client/types.d.ts +11 -7
- package/src/runtime/client/utils.js +0 -96
- package/src/runtime/components/root.svelte +66 -0
- package/src/runtime/env/dynamic/private.js +7 -0
- package/src/runtime/env/dynamic/public.js +7 -0
- package/src/runtime/env/static/private.js +6 -0
- package/src/runtime/env/static/public.js +6 -0
- package/src/runtime/form-utils.js +100 -22
- package/src/runtime/server/cookie.js +69 -52
- package/src/runtime/server/csrf.js +65 -0
- package/src/runtime/server/data/index.js +15 -19
- package/src/runtime/server/env_module.js +0 -5
- package/src/runtime/server/errors.js +135 -0
- package/src/runtime/server/fetch.js +1 -1
- package/src/runtime/server/index.js +22 -12
- package/src/runtime/server/internal.js +25 -0
- package/src/runtime/server/page/actions.js +43 -27
- package/src/runtime/server/page/data_serializer.js +10 -10
- package/src/runtime/server/page/index.js +12 -17
- package/src/runtime/server/page/load_data.js +2 -2
- package/src/runtime/server/page/render.js +114 -193
- package/src/runtime/server/page/respond_with_error.js +11 -11
- package/src/runtime/server/page/server_routing.js +27 -18
- package/src/runtime/server/remote-functions.js +596 -0
- package/src/runtime/server/respond.js +100 -68
- package/src/runtime/server/sourcemaps.js +183 -0
- package/src/runtime/server/utils.js +2 -134
- package/src/runtime/shared.js +83 -13
- package/src/runtime/telemetry/otel.js +1 -1
- package/src/runtime/types.d.ts +8 -0
- package/src/types/ambient-private.d.ts +2 -0
- package/src/types/ambient.d.ts +10 -5
- package/src/types/global-private.d.ts +13 -25
- package/src/types/internal.d.ts +95 -80
- package/src/types/private.d.ts +41 -1
- package/src/utils/error.js +24 -4
- package/src/utils/escape.js +9 -25
- package/src/utils/features.js +1 -1
- package/src/utils/fork.js +7 -2
- package/src/utils/import.js +6 -1
- package/src/utils/imports.js +83 -0
- package/src/utils/mime.js +9 -0
- package/src/utils/page_nodes.js +3 -5
- package/src/utils/params.js +66 -0
- package/src/utils/routing.js +90 -44
- package/src/utils/shared-iterator.js +5 -0
- package/src/utils/streaming.js +14 -4
- package/src/utils/url.js +20 -2
- package/src/version.js +1 -1
- package/types/index.d.ts +537 -557
- package/types/index.d.ts.map +24 -39
- package/src/core/sync/write_root.js +0 -148
- package/src/exports/internal/server.js +0 -22
- package/src/runtime/server/remote.js +0 -457
- package/src/runtime/shared-server.js +0 -7
- package/src/types/synthetic/$lib.md +0 -5
- /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 {
|
|
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
|
-
|
|
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()
|
|
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 =
|
|
151
|
+
this.#error = error;
|
|
122
152
|
this.#loading = false;
|
|
123
153
|
});
|
|
124
154
|
|
|
125
|
-
reject(
|
|
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
|
|
164
|
+
this.start();
|
|
135
165
|
return this.#then;
|
|
136
166
|
}
|
|
137
167
|
|
|
138
168
|
get catch() {
|
|
139
|
-
this
|
|
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
|
|
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
|
|
194
|
+
this.start();
|
|
165
195
|
return this.#current;
|
|
166
196
|
}
|
|
167
197
|
|
|
168
198
|
get error() {
|
|
169
|
-
this
|
|
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
|
|
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
|
|
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 {(
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
|
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 (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
26
|
+
if (batching.size > 1) return;
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
52
|
+
if (response.redirect) {
|
|
53
|
+
// Use internal version to allow redirects to external URLs
|
|
54
|
+
await _goto(response.redirect, {}, 0);
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
throw new Redirect(307, result.location);
|
|
68
|
-
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
69
66
|
|
|
70
|
-
|
|
67
|
+
const results = response._;
|
|
68
|
+
let i = 0;
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let i = 0;
|
|
70
|
+
for (const resolvers of batched.values()) {
|
|
71
|
+
const result = results[i];
|
|
75
72
|
|
|
76
|
-
for (const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
}
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
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 {
|
|
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 {
|
|
7
|
+
import { read_sse } from '../../sse.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* @
|
|
11
|
-
* @
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 (
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
67
|
+
await reader.cancel();
|
|
87
68
|
} catch {
|
|
88
69
|
// already closed
|
|
89
70
|
}
|