@sveltejs/kit 3.0.0-next.1 → 3.0.0-next.3

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 (45) hide show
  1. package/package.json +2 -2
  2. package/src/core/postbuild/analyse.js +0 -8
  3. package/src/core/postbuild/prerender.js +2 -0
  4. package/src/core/sync/create_manifest_data/index.js +24 -1
  5. package/src/core/sync/write_env.js +2 -1
  6. package/src/exports/internal/env.js +1 -1
  7. package/src/exports/public.d.ts +1 -1
  8. package/src/exports/vite/build/build_server.js +47 -58
  9. package/src/exports/vite/build/remote.js +18 -11
  10. package/src/exports/vite/build/utils.js +0 -8
  11. package/src/exports/vite/index.js +221 -217
  12. package/src/exports/vite/static_analysis/index.js +2 -4
  13. package/src/exports/vite/static_analysis/types.d.ts +14 -0
  14. package/src/exports/vite/utils.js +1 -12
  15. package/src/runtime/app/server/remote/command.js +0 -3
  16. package/src/runtime/app/server/remote/form.js +18 -13
  17. package/src/runtime/app/server/remote/prerender.js +28 -34
  18. package/src/runtime/app/server/remote/query.js +105 -94
  19. package/src/runtime/app/server/remote/requested.js +14 -10
  20. package/src/runtime/app/server/remote/shared.js +25 -19
  21. package/src/runtime/client/client.js +19 -13
  22. package/src/runtime/client/ndjson.js +6 -33
  23. package/src/runtime/client/remote-functions/command.svelte.js +7 -32
  24. package/src/runtime/client/remote-functions/form.svelte.js +62 -82
  25. package/src/runtime/client/remote-functions/prerender.svelte.js +14 -6
  26. package/src/runtime/client/remote-functions/query/index.js +6 -14
  27. package/src/runtime/client/remote-functions/query/instance.svelte.js +20 -0
  28. package/src/runtime/client/remote-functions/query/proxy.js +3 -3
  29. package/src/runtime/client/remote-functions/query-batch.svelte.js +59 -68
  30. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +21 -6
  31. package/src/runtime/client/remote-functions/query-live/iterator.js +36 -55
  32. package/src/runtime/client/remote-functions/shared.svelte.js +76 -59
  33. package/src/runtime/client/sse.js +32 -0
  34. package/src/runtime/client/stream.js +38 -0
  35. package/src/runtime/server/page/render.js +23 -80
  36. package/src/runtime/server/page/server_routing.js +20 -15
  37. package/src/runtime/server/remote.js +296 -204
  38. package/src/runtime/server/respond.js +4 -2
  39. package/src/runtime/shared.js +83 -13
  40. package/src/types/global-private.d.ts +3 -3
  41. package/src/types/internal.d.ts +54 -35
  42. package/src/utils/error.js +12 -0
  43. package/src/version.js +1 -1
  44. package/types/index.d.ts +17 -7
  45. package/types/index.d.ts.map +5 -3
@@ -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,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 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,9 +1,8 @@
1
- import { app } from '../../client.js';
2
- import * as devalue from 'devalue';
1
+ import { query_responses } 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
  /**
@@ -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[0] ?? 500, node.e[1]);
95
+ this.#loading = false;
96
+ this.#error = error;
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
 
@@ -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
  }
@@ -1,7 +1,8 @@
1
- /** @import { RemoteFunctionResponse, RemoteSingleflightMap, RemoteSingleflightEntry } from 'types' */
1
+ /** @import { RemoteFunctionResponse, RemoteFunctionData, RemoteFunctionDataNode } from 'types' */
2
2
  /** @import { RemoteQueryUpdate } from '@sveltejs/kit' */
3
+ /** @import { CacheEntry } from './cache.svelte.js' */
3
4
  import * as devalue from 'devalue';
4
- import { app, goto, live_query_map, query_map } from '../client.js';
5
+ import { app, goto, live_query_map, query_map, query_responses } from '../client.js';
5
6
  import { HttpError, Redirect } from '@sveltejs/kit/internal';
6
7
  import { untrack } from 'svelte';
7
8
  import { create_remote_key, split_remote_key } from '../../shared.js';
@@ -77,6 +78,20 @@ export function pin_while_resolving(cache_map, cache, id, payload, then) {
77
78
  /**
78
79
  * @returns {{ 'x-sveltekit-pathname': string, 'x-sveltekit-search': string }}
79
80
  */
81
+ /**
82
+ * Unwraps a `RemoteFunctionDataNode` that was serialized during SSR,
83
+ * rethrowing serialized errors so the consuming resource ends up
84
+ * in the same failed state it had on the server
85
+ * @param {RemoteFunctionDataNode} node
86
+ */
87
+ export function unwrap_node(node) {
88
+ if (node.e) {
89
+ throw new HttpError(node.e[0] ?? 500, node.e[1]);
90
+ }
91
+
92
+ return node.v;
93
+ }
94
+
80
95
  export function get_remote_request_headers() {
81
96
  // This will be the correct value of the current or soon-current url,
82
97
  // even in forks because it's state-based - therefore not using window.location.
@@ -93,15 +108,10 @@ export function get_remote_request_headers() {
93
108
 
94
109
  /**
95
110
  * @param {string} url
96
- * @param {HeadersInit} headers
111
+ * @param {RequestInit} [init]
97
112
  */
98
- export async function remote_request(url, headers) {
99
- const response = await fetch(url, {
100
- headers: {
101
- 'Content-Type': 'application/json',
102
- ...headers
103
- }
104
- });
113
+ export async function remote_request(url, init) {
114
+ const response = await fetch(url, init);
105
115
 
106
116
  if (!response.ok) {
107
117
  throw new HttpError(500, 'Failed to execute remote function');
@@ -109,9 +119,63 @@ export async function remote_request(url, headers) {
109
119
 
110
120
  const result = /** @type {RemoteFunctionResponse} */ (await response.json());
111
121
 
112
- const resolved = await handle_side_channel_response(result);
122
+ if (result.type === 'error') {
123
+ throw new HttpError(result.status ?? 500, result.error);
124
+ }
125
+
126
+ const data = /** @type {RemoteFunctionData} */ (
127
+ result.data ? devalue.parse(result.data, app.decoders) : {}
128
+ );
129
+
130
+ /**
131
+ *
132
+ * @param {string} key
133
+ * @param {CacheEntry<any> | undefined} entry
134
+ * @param {any} result
135
+ */
136
+ function refresh(key, entry, result) {
137
+ if (entry?.resource) {
138
+ if (result.e) {
139
+ entry.resource.fail(new HttpError(result.e[0] ?? 500, result.e[1]));
140
+ } else {
141
+ entry.resource.set(result.v);
142
+ }
143
+ } else if (!result.e) {
144
+ // `query_responses` stores `{ v }`/`{ e }` nodes, not raw values.
145
+ // Errors are deliberately dropped here: they are responses to a specific
146
+ // refresh, not durable state a future resource should initialize with
147
+ query_responses[key] = result;
148
+ }
149
+ }
150
+
151
+ // update queries with refreshed data
152
+ if (data.q) {
153
+ for (const key in data.q) {
154
+ const parts = split_remote_key(key);
155
+ const entry = query_map.get(parts.id)?.get(parts.payload);
156
+
157
+ refresh(key, entry, data.q[key]);
158
+ }
159
+ }
160
+
161
+ // reconnect live queries
162
+ if (data.l) {
163
+ for (const key in data.l) {
164
+ const parts = split_remote_key(key);
165
+ const entry = live_query_map.get(parts.id)?.get(parts.payload);
166
+
167
+ refresh(key, entry, data.l[key]);
113
168
 
114
- return resolved.result;
169
+ // `fail()` is terminal, so only reconnect on the success path —
170
+ // reconnecting after a hard failure would wipe the error state and
171
+ // restart the stream (see commit 63a3e83 regression).
172
+ if (!data.l[key].e) {
173
+ void entry?.resource.reconnect();
174
+ }
175
+ }
176
+ }
177
+
178
+ return data;
115
179
  }
116
180
 
117
181
  /**
@@ -206,50 +270,3 @@ export function categorize_updates(updates) {
206
270
 
207
271
  return { overrides, refreshes };
208
272
  }
209
-
210
- /**
211
- * @template TResource
212
- * @param {string} stringified_singleflight
213
- * @param {Map<string, Map<string, { resource: TResource }>>} map
214
- * @param {(resource: TResource, value: RemoteSingleflightEntry) => void} callback
215
- */
216
- function apply_singleflight(stringified_singleflight, map, callback) {
217
- const singleflight = /** @type {RemoteSingleflightMap} */ (
218
- devalue.parse(stringified_singleflight, app.decoders)
219
- );
220
-
221
- for (const [key, value] of Object.entries(singleflight)) {
222
- const parts = split_remote_key(key);
223
- const entry = map.get(parts.id)?.get(parts.payload);
224
- if (entry?.resource) {
225
- callback(entry.resource, value);
226
- }
227
- }
228
- }
229
-
230
- /**
231
- * Apply refresh data from the server to the relevant queries
232
- *
233
- * @param {string} stringified_refreshes
234
- */
235
- export const apply_refreshes = (stringified_refreshes) => {
236
- apply_singleflight(stringified_refreshes, query_map, (resource, value) => {
237
- if (value.type === 'result') {
238
- resource?.set(value.data);
239
- } else {
240
- resource?.fail(new HttpError(value.status ?? 500, value.error));
241
- }
242
- });
243
- };
244
-
245
- /** @param {string} stringified_reconnects */
246
- export const apply_reconnections = (stringified_reconnects) => {
247
- apply_singleflight(stringified_reconnects, live_query_map, (resource, value) => {
248
- if (value.type === 'result') {
249
- resource?.set(value.data);
250
- void resource?.reconnect();
251
- } else {
252
- resource?.fail(new HttpError(value.status ?? 500, value.error));
253
- }
254
- });
255
- };
@@ -0,0 +1,32 @@
1
+ import { read_stream } from './stream.js';
2
+
3
+ /**
4
+ * @param {string} block
5
+ * @returns {string | undefined}
6
+ */
7
+ function parse_sse_event_data(block) {
8
+ const lines = block.split('\n');
9
+ let data = '';
10
+
11
+ for (const line of lines) {
12
+ if (line.startsWith('data:')) {
13
+ data += (data ? '\n' : '') + line.slice(5).trimStart();
14
+ }
15
+ }
16
+
17
+ return data || undefined;
18
+ }
19
+
20
+ /**
21
+ * Yields parsed JSON objects from a ReadableStream of Server-Sent Events.
22
+ * Each yielded value is the raw `JSON.parse`'d object from a `data:` field.
23
+ * @param {ReadableStreamDefaultReader<Uint8Array>} reader
24
+ */
25
+ export async function* read_sse(reader) {
26
+ for await (const block of read_stream(reader, '\n\n')) {
27
+ const data = parse_sse_event_data(block);
28
+ if (data) {
29
+ yield JSON.parse(data);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Reads from a stream, decoding it as text and yielding each block of content
3
+ * separated by `delimiter`. The trailing block (if any) is yielded once the
4
+ * stream closes.
5
+ * @param {ReadableStreamDefaultReader<Uint8Array>} reader
6
+ * @param {string} delimiter
7
+ */
8
+ export async function* read_stream(reader, delimiter) {
9
+ let done = false;
10
+ let buffer = '';
11
+ const decoder = new TextDecoder();
12
+
13
+ while (true) {
14
+ let split = buffer.indexOf(delimiter);
15
+ while (split !== -1) {
16
+ yield buffer.slice(0, split);
17
+ buffer = buffer.slice(split + delimiter.length);
18
+ split = buffer.indexOf(delimiter);
19
+ }
20
+
21
+ if (done) {
22
+ if (buffer) {
23
+ yield buffer;
24
+ }
25
+ return;
26
+ }
27
+
28
+ const chunk = await reader.read();
29
+ done = chunk.done;
30
+ if (chunk.value) {
31
+ buffer += decoder.decode(chunk.value, { stream: true });
32
+ }
33
+
34
+ if (done) {
35
+ buffer += decoder.decode();
36
+ }
37
+ }
38
+ }