@sveltejs/kit 3.0.0-next.1 → 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 +3 -3
  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,12 +1,12 @@
1
1
  /** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest } from '@sveltejs/kit' */
2
- /** @import { RemoteFormInternals, RemoteFunctionResponse, RemoteInternals, RequestState, SSROptions } from 'types' */
2
+ /** @import { RemoteFormInternals, RemoteFunctionData, RemoteFunctionResponse, RemoteInternals, RequestState, SSROptions } from 'types' */
3
3
 
4
4
  import { json, error } from '@sveltejs/kit';
5
- import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
5
+ import { Redirect, SvelteKitError } from '@sveltejs/kit/internal';
6
6
  import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server';
7
7
  import { app_dir, base } from '$app/paths/internal/server';
8
8
  import { is_form_content_type } from '../../utils/http.js';
9
- import { parse_remote_arg, split_remote_key, stringify } from '../shared.js';
9
+ import { create_remote_key, parse_remote_arg, split_remote_key, stringify } from '../shared.js';
10
10
  import { handle_error_and_jsonify } from './utils.js';
11
11
  import { normalize_error } from '../../utils/error.js';
12
12
  import { check_incorrect_fail_use } from './page/actions.js';
@@ -14,6 +14,13 @@ import { DEV } from 'esm-env';
14
14
  import { record_span } from '../telemetry/record_span.js';
15
15
  import { deserialize_binary_form } from '../form-utils.js';
16
16
 
17
+ /**
18
+ * How long (in milliseconds) to wait after the last message was sent before
19
+ * sending a `: keep-alive` SSE comment, to prevent proxies/load balancers with
20
+ * an idle timeout from closing an otherwise-quiet `query.live` connection.
21
+ */
22
+ const KEEP_ALIVE_INTERVAL = 30_000;
23
+
17
24
  /** @type {typeof handle_remote_call_internal} */
18
25
  export async function handle_remote_call(event, state, options, manifest, id) {
19
26
  return record_span({
@@ -41,10 +48,10 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
41
48
  const [hash, name, additional_args] = id.split('/');
42
49
  const remotes = manifest._.remotes;
43
50
 
44
- if (!remotes[hash]) error(404);
51
+ if (!Object.hasOwn(remotes, hash)) error(404);
45
52
 
46
53
  const module = await remotes[hash]();
47
- const fn = module.default[name];
54
+ const fn = Object.hasOwn(module.default, name) ? module.default[name] : undefined;
48
55
 
49
56
  if (!fn) error(404);
50
57
 
@@ -57,278 +64,400 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
57
64
  'sveltekit.remote.call.name': internals.name
58
65
  });
59
66
 
67
+ /** @type {HeadersInit | undefined} */
68
+ const headers = state.prerendering ? undefined : { 'cache-control': 'private, no-store' };
69
+
60
70
  try {
61
- if (internals.type === 'query_batch') {
62
- if (event.request.method !== 'POST') {
63
- throw new SvelteKitError(
64
- 405,
65
- 'Method Not Allowed',
66
- `\`query.batch\` functions must be invoked via POST request, not ${event.request.method}`
71
+ /** @type {RemoteFunctionData} */
72
+ const data = {};
73
+
74
+ switch (internals.type) {
75
+ case 'query_live': {
76
+ if (event.request.method !== 'GET') {
77
+ throw new SvelteKitError(
78
+ 405,
79
+ 'Method Not Allowed',
80
+ `\`query.live\` functions must be invoked via GET request, not ${event.request.method}`
81
+ );
82
+ }
83
+
84
+ const payload = /** @type {string} */ (
85
+ new URL(event.request.url).searchParams.get('payload')
67
86
  );
68
- }
69
87
 
70
- /** @type {{ payloads: string[] }} */
71
- const { payloads } = await event.request.json();
88
+ const generator = internals.run(event, state, parse_remote_arg(payload, transport));
89
+
90
+ const encoder = new TextEncoder();
91
+
92
+ let closed = false;
93
+
94
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
95
+ let keep_alive;
96
+
97
+ /**
98
+ * (Re)schedule the keep-alive comment. Called whenever a message is sent, so
99
+ * that a keep-alive is only emitted once `KEEP_ALIVE_INTERVAL` has elapsed
100
+ * without any other activity.
101
+ * @param {ReadableStreamDefaultController} controller
102
+ */
103
+ function schedule_keep_alive(controller) {
104
+ clearTimeout(keep_alive);
105
+ keep_alive = setTimeout(() => {
106
+ if (closed || event.request.signal.aborted) return;
107
+ // SSE comments (lines starting with `:`) are ignored by the client
108
+ controller.enqueue(encoder.encode(': keep-alive\n\n'));
109
+ schedule_keep_alive(controller);
110
+ }, KEEP_ALIVE_INTERVAL);
111
+ }
72
112
 
73
- const args = await Promise.all(
74
- payloads.map((payload) => parse_remote_arg(payload, transport))
75
- );
113
+ /**
114
+ * @param {ReadableStreamDefaultController} controller
115
+ * @param {any} payload
116
+ */
117
+ function send(controller, payload) {
118
+ controller.enqueue(encoder.encode('data: ' + JSON.stringify(payload) + '\n\n'));
119
+ schedule_keep_alive(controller);
120
+ }
76
121
 
77
- const results = await with_request_store({ event, state }, () =>
78
- internals.run(args, options)
79
- );
122
+ /** @type {string | undefined} */
123
+ let result = undefined;
80
124
 
81
- return json(
82
- /** @type {RemoteFunctionResponse} */ ({
83
- type: 'result',
84
- result: stringify(results, transport)
85
- })
86
- );
87
- }
125
+ async function cancel() {
126
+ if (closed) return;
127
+ closed = true;
128
+ clearTimeout(keep_alive);
129
+ await generator.return(undefined);
130
+ }
131
+
132
+ event.request.signal.addEventListener('abort', cancel, { once: true });
133
+
134
+ return new Response(
135
+ new ReadableStream({
136
+ start(controller) {
137
+ schedule_keep_alive(controller);
138
+ },
139
+ async pull(controller) {
140
+ if (event.request.signal.aborted) {
141
+ await cancel();
142
+ controller.close();
143
+ return;
144
+ }
145
+
146
+ try {
147
+ while (true) {
148
+ const { value, done } = await generator.next();
149
+
150
+ if (done) {
151
+ await cancel();
152
+ controller.close();
153
+ return;
154
+ }
155
+
156
+ // only send changed data
157
+ if (result !== (result = stringify(value, transport))) {
158
+ send(controller, {
159
+ type: 'result',
160
+ result
161
+ });
162
+
163
+ return;
164
+ }
165
+ }
166
+ } catch (error) {
167
+ if (!event.request.signal.aborted) {
168
+ if (error instanceof Redirect) {
169
+ send(controller, {
170
+ type: 'redirect',
171
+ location: error.location
172
+ });
173
+ } else {
174
+ const transformed = await handle_error_and_jsonify(
175
+ event,
176
+ state,
177
+ options,
178
+ error
179
+ );
180
+
181
+ send(controller, {
182
+ type: 'error',
183
+ error: transformed
184
+ });
185
+ }
186
+ }
88
187
 
89
- if (internals.type === 'form') {
90
- if (event.request.method !== 'POST') {
91
- throw new SvelteKitError(
92
- 405,
93
- 'Method Not Allowed',
94
- `\`form\` functions must be invoked via POST request, not ${event.request.method}`
188
+ await cancel();
189
+ controller.close();
190
+ }
191
+ },
192
+ cancel
193
+ }),
194
+ {
195
+ headers: {
196
+ 'cache-control': 'private, no-store',
197
+ 'content-type': 'text/event-stream'
198
+ }
199
+ }
95
200
  );
96
201
  }
97
202
 
98
- if (!is_form_content_type(event.request)) {
99
- throw new SvelteKitError(
100
- 415,
101
- 'Unsupported Media Type',
102
- `\`form\` functions expect form-encoded data — received ${event.request.headers.get(
103
- 'content-type'
104
- )}`
203
+ case 'query_batch': {
204
+ if (event.request.method !== 'POST') {
205
+ throw new SvelteKitError(
206
+ 405,
207
+ 'Method Not Allowed',
208
+ `\`query.batch\` functions must be invoked via POST request, not ${event.request.method}`
209
+ );
210
+ }
211
+
212
+ /** @type {{ payloads: string[] }} */
213
+ const { payloads } = await event.request.json();
214
+
215
+ const args = await Promise.all(
216
+ payloads.map((payload) => parse_remote_arg(payload, transport))
105
217
  );
106
- }
107
218
 
108
- const { data, meta, form_data } = await deserialize_binary_form(event.request);
109
- state.remote.requested = create_requested_map(meta.remote_refreshes);
219
+ data._ = await with_request_store({ event, state }, () => internals.run(args, options));
110
220
 
111
- // If this is a keyed form instance (created via form.for(key)), add the key to the form data (unless already set)
112
- // Note that additional_args will only be set if the form is not enhanced, as enhanced forms transfer the key inside `data`.
113
- if (additional_args && !('id' in data)) {
114
- data.id = JSON.parse(decodeURIComponent(additional_args));
221
+ break;
115
222
  }
116
223
 
117
- const fn = internals.fn;
118
- const result = await with_request_store({ event, state }, () => fn(data, meta, form_data));
224
+ case 'form': {
225
+ if (event.request.method !== 'POST') {
226
+ throw new SvelteKitError(
227
+ 405,
228
+ 'Method Not Allowed',
229
+ `\`form\` functions must be invoked via POST request, not ${event.request.method}`
230
+ );
231
+ }
119
232
 
120
- return json(
121
- /** @type {RemoteFunctionResponse} */ ({
122
- type: 'result',
123
- result: stringify(result, transport),
124
- refreshes: result.issues
125
- ? undefined
126
- : await serialize_singleflight(state.remote.refreshes),
127
- reconnects: result.issues
128
- ? undefined
129
- : await serialize_singleflight(state.remote.reconnects)
130
- })
131
- );
132
- }
233
+ if (!is_form_content_type(event.request)) {
234
+ throw new SvelteKitError(
235
+ 415,
236
+ 'Unsupported Media Type',
237
+ `\`form\` functions expect form-encoded data — received ${event.request.headers.get(
238
+ 'content-type'
239
+ )}`
240
+ );
241
+ }
133
242
 
134
- if (internals.type === 'command') {
135
- /** @type {{ payload: string, refreshes?: string[] }} */
136
- const { payload, refreshes } = await event.request.json();
137
- state.remote.requested = create_requested_map(refreshes);
138
- const arg = parse_remote_arg(payload, transport);
139
- const data = await with_request_store({ event, state }, () => fn(arg));
243
+ const { data: input, meta, form_data } = await deserialize_binary_form(event.request);
244
+ state.remote.requested = create_requested_map(meta.remote_refreshes);
140
245
 
141
- return json(
142
- /** @type {RemoteFunctionResponse} */ ({
143
- type: 'result',
144
- result: stringify(data, transport),
145
- refreshes: await serialize_singleflight(state.remote.refreshes),
146
- reconnects: await serialize_singleflight(state.remote.reconnects)
147
- })
148
- );
149
- }
246
+ // If this is a keyed form instance (created via form.for(key)), add the key to the form data (unless already set)
247
+ // Note that additional_args will only be set if the form is not enhanced, as enhanced forms transfer the key inside `data`.
248
+ if (additional_args && !('id' in input)) {
249
+ input.id = JSON.parse(decodeURIComponent(additional_args));
250
+ }
150
251
 
151
- if (internals.type === 'query_live') {
152
- if (event.request.method !== 'GET') {
153
- throw new SvelteKitError(
154
- 405,
155
- 'Method Not Allowed',
156
- `\`query.live\` functions must be invoked via GET request, not ${event.request.method}`
252
+ const fn = internals.fn;
253
+ data._ = await with_request_store(
254
+ { event, state: { ...state, is_in_remote_form_or_command: true } },
255
+ () => fn(input, meta, form_data)
157
256
  );
158
- }
159
-
160
- const payload = /** @type {string} */ (
161
- new URL(event.request.url).searchParams.get('payload')
162
- );
163
257
 
164
- const generator = internals.run(event, state, parse_remote_arg(payload, transport));
165
-
166
- const encoder = new TextEncoder();
258
+ if (data._.issues) {
259
+ // special case — don't serialize refreshes/reconnects
260
+ return json(
261
+ /** @type {RemoteFunctionResponse} */ ({
262
+ type: 'result',
263
+ data: stringify(data, transport)
264
+ }),
265
+ { headers }
266
+ );
267
+ }
167
268
 
168
- /**
169
- * @param {ReadableStreamDefaultController} controller
170
- * @param {any} payload
171
- */
172
- function send(controller, payload) {
173
- controller.enqueue(encoder.encode(JSON.stringify(payload) + '\n'));
269
+ break;
174
270
  }
175
271
 
176
- let closed = false;
272
+ case 'command': {
273
+ /** @type {{ payload: string, refreshes?: string[] }} */
274
+ const { payload, refreshes } = await event.request.json();
275
+ state.remote.requested = create_requested_map(refreshes);
276
+ const arg = parse_remote_arg(payload, transport);
177
277
 
178
- /** @type {string | undefined} */
179
- let result = undefined;
278
+ data._ = await with_request_store(
279
+ { event, state: { ...state, is_in_remote_form_or_command: true } },
280
+ () => fn(arg)
281
+ );
180
282
 
181
- async function cancel() {
182
- if (closed) return;
183
- closed = true;
184
- await generator.return(undefined);
283
+ break;
185
284
  }
186
285
 
187
- event.request.signal.addEventListener('abort', cancel, { once: true });
188
-
189
- return new Response(
190
- new ReadableStream({
191
- async pull(controller) {
192
- if (event.request.signal.aborted) {
193
- await cancel();
194
- controller.close();
195
- return;
196
- }
197
-
198
- try {
199
- while (true) {
200
- const { value, done } = await generator.next();
286
+ case 'prerender': {
287
+ data._ = await with_request_store({ event, state }, () =>
288
+ fn(parse_remote_arg(additional_args, transport))
289
+ );
201
290
 
202
- if (done) {
203
- await cancel();
204
- controller.close();
205
- return;
206
- }
291
+ break;
292
+ }
207
293
 
208
- // only send changed data
209
- if (result !== (result = stringify(value, transport))) {
210
- send(controller, {
211
- type: 'result',
212
- result
213
- });
294
+ case 'query': {
295
+ const payload = /** @type {string} */ (
296
+ // new URL(...) necessary because we're hiding the URL from the user in the event object
297
+ new URL(event.request.url).searchParams.get('payload')
298
+ );
214
299
 
215
- return;
216
- }
217
- }
218
- } catch (error) {
219
- if (!event.request.signal.aborted) {
220
- if (error instanceof Redirect) {
221
- send(controller, {
222
- type: 'redirect',
223
- location: error.location
224
- });
225
- } else {
226
- const status =
227
- error instanceof HttpError || error instanceof SvelteKitError
228
- ? error.status
229
- : 500;
230
-
231
- send(controller, {
232
- type: 'error',
233
- error: await handle_error_and_jsonify(event, state, options, error),
234
- status
235
- });
236
- }
237
- }
300
+ data._ = await with_request_store({ event, state }, () =>
301
+ fn(parse_remote_arg(payload, transport))
302
+ );
238
303
 
239
- await cancel();
240
- controller.close();
241
- }
242
- },
243
- cancel
244
- }),
245
- {
246
- headers: {
247
- 'cache-control': 'private, no-store',
248
- 'content-type': 'application/x-ndjson'
249
- }
250
- }
251
- );
304
+ break;
305
+ }
252
306
  }
253
307
 
254
- const payload =
255
- internals.type === 'prerender'
256
- ? additional_args
257
- : /** @type {string} */ (
258
- // new URL(...) necessary because we're hiding the URL from the user in the event object
259
- new URL(event.request.url).searchParams.get('payload')
260
- );
261
-
262
- const data = await with_request_store({ event, state }, () =>
263
- fn(parse_remote_arg(payload, transport))
264
- );
308
+ await collect_remote_data(data, event, state, options);
265
309
 
266
310
  return json(
267
311
  /** @type {RemoteFunctionResponse} */ ({
268
312
  type: 'result',
269
- result: stringify(data, transport)
270
- })
313
+ data: stringify(data, transport)
314
+ }),
315
+ { headers }
271
316
  );
272
317
  } catch (error) {
273
318
  if (error instanceof Redirect) {
319
+ const data = await collect_remote_data({ redirect: error.location }, event, state, options);
320
+
274
321
  return json(
275
322
  /** @type {RemoteFunctionResponse} */ ({
276
- type: 'redirect',
277
- location: error.location,
278
- refreshes: await serialize_singleflight(state.remote.refreshes),
279
- reconnects: await serialize_singleflight(state.remote.reconnects)
280
- })
323
+ type: 'result',
324
+ data: stringify(data, transport)
325
+ }),
326
+ { headers }
281
327
  );
282
328
  }
283
329
 
284
- const status =
285
- error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500;
330
+ const transformed = await handle_error_and_jsonify(event, state, options, error);
286
331
 
287
332
  return json(
288
333
  /** @type {RemoteFunctionResponse} */ ({
289
334
  type: 'error',
290
- error: await handle_error_and_jsonify(event, state, options, error),
291
- status
335
+ error: transformed
292
336
  }),
293
337
  {
294
338
  // By setting a non-200 during prerendering we fail the prerender process (unless handleHttpError handles it).
295
339
  // Errors at runtime will be passed to the client and are handled there
296
- status: state.prerendering ? status : undefined,
340
+ status: state.prerendering ? transformed.status : undefined,
297
341
  headers: {
298
342
  'cache-control': 'private, no-store'
299
343
  }
300
344
  }
301
345
  );
302
346
  }
347
+ }
303
348
 
304
- /** @param {Map<string, Promise<any>> | null} map */
305
- async function serialize_singleflight(map) {
306
- if (!map || map.size === 0) {
307
- return undefined;
308
- }
349
+ /**
350
+ * Collects all the query/prerender data that was retrieved
351
+ * during the request and adds it to `data`
352
+ * @param {RemoteFunctionData} data
353
+ * @param {RequestEvent} event
354
+ * @param {RequestState} state
355
+ * @param {SSROptions} options
356
+ */
357
+ export async function collect_remote_data(data, event, state, options) {
358
+ /**
359
+ *
360
+ * @param {unknown} error
361
+ * @returns {Promise<App.Error>}
362
+ */
363
+ function convert_error(error) {
364
+ // TODO 4.0 remove the `Promise.resolve(...)`
365
+ return Promise.resolve(handle_error_and_jsonify(event, state, options, error));
366
+ }
367
+
368
+ /** @type {Promise<any>[]} */
369
+ const promises = [];
370
+
371
+ if (state.remote.explicit) {
372
+ for (const [remote_key, { internals, fn }] of state.remote.explicit) {
373
+ // there were explicit refreshes/reconnects (via `refresh()`/`set()`/`reconnect()`),
374
+ // so the client should apply these single-flight updates instead of calling `invalidateAll()`
375
+ data.r = true;
309
376
 
310
- const results = await Promise.all(
311
- Array.from(map, async ([key, promise]) => {
312
- try {
313
- return [key, { type: 'result', data: await promise }];
314
- } catch (error) {
315
- const status =
316
- error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500;
317
-
318
- return [
319
- key,
320
- {
321
- type: 'error',
322
- status,
323
- error: await handle_error_and_jsonify(event, state, options, error)
377
+ const type = /** @type {'p' | 'q' | 'l'} */ (
378
+ internals.type === 'query_live' ? 'l' : internals.type[0]
379
+ );
380
+
381
+ // `fn` is deferred until now so the query runs after any state mutations
382
+ // in the command/form body. If the query was re-awaited in the meantime,
383
+ // `fn` returns the existing (fresh) cache entry rather than re-running.
384
+ // Kick off the query immediately and collect the promise so that multiple
385
+ // explicit refreshes run concurrently rather than serially.
386
+ const promise = fn();
387
+
388
+ promises.push(
389
+ promise.then(
390
+ (v) => {
391
+ ((data[type] ??= {})[remote_key] ??= {}).v = v;
392
+ },
393
+ async (e) => {
394
+ if (e instanceof Redirect) {
395
+ // already handled elsewhere
396
+ return;
324
397
  }
325
- ];
326
- }
327
- })
328
- );
329
398
 
330
- return stringify(Object.fromEntries(results), transport);
399
+ ((data[type] ??= {})[remote_key] ??= {}).e = await convert_error(e);
400
+ }
401
+ )
402
+ );
403
+ }
331
404
  }
405
+
406
+ await Promise.all(promises);
407
+
408
+ if (state.remote.implicit) {
409
+ for (const [internals, record] of state.remote.implicit) {
410
+ // Private (non-exported) remote functions have no `id` and must never be
411
+ // serialized into the response — otherwise their (potentially private) result
412
+ // would be shipped to the client under a malformed `undefined/...` key.
413
+ if (!internals.id) continue;
414
+
415
+ for (const key in record) {
416
+ // form outputs are registered under the client-side action id directly
417
+ const remote_key = internals.type === 'form' ? key : create_remote_key(internals.id, key);
418
+
419
+ const type = /** @type {'p' | 'q' | 'l' | 'f'} */ (
420
+ internals.type === 'query_live' ? 'l' : internals.type[0]
421
+ );
422
+
423
+ const promise = state.remote.data?.get(internals)?.[key] ?? record[key]();
424
+
425
+ // If the promise is still pending (e.g. the query was rendered in its loading
426
+ // state during SSR), omit it from the payload entirely so that the client
427
+ // fetches it itself — an entry without `v`/`e` would hydrate as `undefined`.
428
+ let resolved = true;
429
+
430
+ await Promise.race([
431
+ Promise.resolve(promise).then(
432
+ (v) => {
433
+ if (resolved) {
434
+ ((data[type] ??= {})[remote_key] ??= {}).v = v;
435
+ }
436
+ },
437
+ (e) => {
438
+ if (e instanceof Redirect) {
439
+ // already handled elsewhere
440
+ return;
441
+ }
442
+
443
+ if (resolved) {
444
+ promises.push(
445
+ convert_error(e).then((e) => {
446
+ ((data[type] ??= {})[remote_key] ??= {}).e = e;
447
+ })
448
+ );
449
+ }
450
+ }
451
+ ),
452
+ Promise.resolve().then(() => (resolved = false))
453
+ ]);
454
+ }
455
+ }
456
+ }
457
+
458
+ await Promise.all(promises);
459
+
460
+ return data;
332
461
  }
333
462
 
334
463
  /**
@@ -377,11 +506,16 @@ export async function handle_remote_form_post(event, state, manifest, id) {
377
506
  * @returns {Promise<ActionResult>}
378
507
  */
379
508
  async function handle_remote_form_post_internal(event, state, manifest, id) {
380
- const [hash, name, action_id] = id.split('/');
509
+ // `hash` and `name` can never contain a `/`, but the JSON-stringified key of a
510
+ // keyed (`form.for(key)`) instance can — rejoin the remaining segments
511
+ const [hash, name, ...rest] = id.split('/');
512
+ const action_id = rest.join('/');
381
513
  const remotes = manifest._.remotes;
382
- const module = await remotes[hash]?.();
514
+ const module = Object.hasOwn(remotes, hash) ? await remotes[hash]() : undefined;
383
515
 
384
- let form = /** @type {RemoteForm<any, any>} */ (module?.default[name]);
516
+ let form = /** @type {RemoteForm<any, any>} */ (
517
+ module && Object.hasOwn(module.default, name) ? module.default[name] : undefined
518
+ );
385
519
 
386
520
  if (!form) {
387
521
  event.setHeaders({
@@ -391,6 +525,7 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
391
525
  });
392
526
  return {
393
527
  type: 'error',
528
+ // We're lying a bit with the types here; this will be transformed into a proper App.Error object later
394
529
  error: new SvelteKitError(
395
530
  405,
396
531
  'Method Not Allowed',
@@ -413,7 +548,10 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
413
548
  data.id = JSON.parse(decodeURIComponent(action_id));
414
549
  }
415
550
 
416
- await with_request_store({ event, state }, () => fn(data, meta, form_data));
551
+ await with_request_store(
552
+ { event, state: { ...state, is_in_remote_form_or_command: true } },
553
+ () => fn(data, meta, form_data)
554
+ );
417
555
 
418
556
  // We don't want the data to appear on `let { form } = $props()`, which is why we're not returning it.
419
557
  // It is instead available on `myForm.result`, setting of which happens within the remote `form` function.
@@ -434,6 +572,7 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
434
572
 
435
573
  return {
436
574
  type: 'error',
575
+ // @ts-expect-error We're lying a bit with the types here; this will be transformed into a proper App.Error object later
437
576
  error: check_incorrect_fail_use(err)
438
577
  };
439
578
  }