@sveltejs/kit 3.0.0-next.2 → 3.0.0-next.21

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 (195) hide show
  1. package/package.json +62 -36
  2. package/src/cli.js +15 -12
  3. package/src/constants.js +6 -1
  4. package/src/core/adapt/builder.js +66 -72
  5. package/src/core/adapt/index.js +17 -6
  6. package/src/core/config/index.js +134 -80
  7. package/src/core/config/options.js +295 -273
  8. package/src/core/config/types.d.ts +1 -1
  9. package/src/core/env.js +146 -16
  10. package/src/core/generate_manifest/index.js +18 -15
  11. package/src/core/postbuild/analyse.js +8 -20
  12. package/src/core/postbuild/crawl.js +22 -6
  13. package/src/core/postbuild/entities.js +8 -2
  14. package/src/core/postbuild/fallback.js +4 -2
  15. package/src/core/postbuild/prerender.js +222 -70
  16. package/src/core/postbuild/queue.js +2 -1
  17. package/src/core/sync/create_manifest_data/conflict.js +1 -1
  18. package/src/core/sync/create_manifest_data/index.js +82 -62
  19. package/src/core/sync/sync.js +40 -31
  20. package/src/core/sync/ts.js +1 -1
  21. package/src/core/sync/utils.js +22 -2
  22. package/src/core/sync/{write_non_ambient.js → write_app_types.js} +107 -58
  23. package/src/core/sync/write_client_manifest.js +14 -29
  24. package/src/core/sync/write_env.js +7 -5
  25. package/src/core/sync/write_server.js +37 -45
  26. package/src/core/sync/write_tsconfig/index.js +274 -0
  27. package/src/core/sync/write_tsconfig/test-app/package.json +7 -0
  28. package/src/core/sync/write_tsconfig/utils.js +77 -0
  29. package/src/core/sync/write_tsconfig/validate.js +128 -0
  30. package/src/core/sync/write_types/index.js +127 -123
  31. package/src/core/utils.js +30 -5
  32. package/src/exports/env/index.js +77 -0
  33. package/src/exports/env/public.d.ts +55 -0
  34. package/src/exports/hooks/index.js +3 -9
  35. package/src/exports/hooks/public.d.ts +195 -0
  36. package/src/exports/hooks/sequence.js +9 -6
  37. package/src/exports/index.js +52 -29
  38. package/src/exports/internal/client.js +5 -0
  39. package/src/exports/internal/env.js +8 -5
  40. package/src/exports/internal/index.js +1 -90
  41. package/src/exports/internal/{event.js → server/event.js} +2 -3
  42. package/src/exports/internal/server/index.js +37 -0
  43. package/src/exports/internal/server/telemetry.js +95 -0
  44. package/src/exports/internal/shared.js +90 -0
  45. package/src/exports/node/index.js +64 -22
  46. package/src/exports/params/index.js +70 -0
  47. package/src/exports/params/public.d.ts +63 -0
  48. package/src/exports/public.d.ts +50 -1733
  49. package/src/exports/remote/index.js +11 -0
  50. package/src/exports/remote/public.d.ts +519 -0
  51. package/src/exports/url.js +86 -0
  52. package/src/exports/vite/build/build_server.js +54 -65
  53. package/src/exports/vite/build/remote.js +24 -19
  54. package/src/exports/vite/build/utils.js +0 -8
  55. package/src/exports/vite/dev/index.js +218 -146
  56. package/src/exports/vite/index.js +1590 -858
  57. package/src/exports/vite/module_ids.js +2 -2
  58. package/src/exports/vite/preview/index.js +40 -32
  59. package/src/exports/vite/public.d.ts +588 -0
  60. package/src/exports/vite/utils.js +84 -46
  61. package/src/pathname.js +55 -0
  62. package/src/runner.js +15 -0
  63. package/src/runtime/app/env/internal.js +4 -4
  64. package/src/runtime/app/env/types.d.ts +1 -1
  65. package/src/runtime/app/environment/index.js +3 -3
  66. package/src/runtime/app/{forms.js → forms/index.js} +73 -42
  67. package/src/runtime/app/forms/public.d.ts +2 -0
  68. package/src/runtime/app/forms/types.d.ts +56 -0
  69. package/src/runtime/app/internal/transport.js +53 -0
  70. package/src/runtime/app/manifest/index.js +1 -0
  71. package/src/runtime/app/{navigation.js → navigation/index.js} +2 -1
  72. package/src/runtime/app/navigation/public.d.ts +237 -0
  73. package/src/runtime/app/paths/client.js +37 -37
  74. package/src/runtime/app/paths/index.js +1 -1
  75. package/src/runtime/app/paths/internal/client.js +34 -2
  76. package/src/runtime/app/paths/internal/server.js +6 -23
  77. package/src/runtime/app/paths/internal.d.ts +3 -0
  78. package/src/runtime/app/paths/public.d.ts +1 -29
  79. package/src/runtime/app/paths/server.js +36 -17
  80. package/src/runtime/app/paths/types.d.ts +11 -19
  81. package/src/runtime/app/server/index.js +2 -2
  82. package/src/runtime/app/server/public.d.ts +201 -0
  83. package/src/runtime/app/server/remote/command.js +13 -11
  84. package/src/runtime/app/server/remote/form.js +61 -39
  85. package/src/runtime/app/server/remote/prerender.js +45 -45
  86. package/src/runtime/app/server/remote/query.js +106 -108
  87. package/src/runtime/app/server/remote/requested.js +27 -19
  88. package/src/runtime/app/server/remote/shared.js +76 -76
  89. package/src/runtime/app/service-worker/index.js +24 -0
  90. package/src/runtime/app/state/client.js +4 -2
  91. package/src/runtime/app/state/index.js +7 -5
  92. package/src/runtime/app/state/public.d.ts +72 -0
  93. package/src/runtime/app/state/server.js +3 -0
  94. package/src/runtime/app/stores.js +15 -78
  95. package/src/runtime/client/bundle.js +1 -1
  96. package/src/runtime/client/client-entry.js +3 -0
  97. package/src/runtime/client/client.js +1336 -648
  98. package/src/runtime/client/constants.js +3 -6
  99. package/src/runtime/client/entry.js +24 -3
  100. package/src/runtime/client/fetcher.js +25 -25
  101. package/src/runtime/client/ndjson.js +1 -1
  102. package/src/runtime/client/parse.js +1 -1
  103. package/src/runtime/client/payload.js +17 -0
  104. package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
  105. package/src/runtime/client/remote-functions/command.svelte.js +37 -61
  106. package/src/runtime/client/remote-functions/form.svelte.js +267 -178
  107. package/src/runtime/client/remote-functions/prerender.svelte.js +32 -11
  108. package/src/runtime/client/remote-functions/query/index.js +10 -17
  109. package/src/runtime/client/remote-functions/query/instance.svelte.js +64 -18
  110. package/src/runtime/client/remote-functions/query/proxy.js +5 -5
  111. package/src/runtime/client/remote-functions/query-batch.svelte.js +61 -70
  112. package/src/runtime/client/remote-functions/query-live/index.js +1 -1
  113. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +63 -21
  114. package/src/runtime/client/remote-functions/query-live/iterator.js +15 -12
  115. package/src/runtime/client/remote-functions/query-live/proxy.js +2 -12
  116. package/src/runtime/client/remote-functions/shared.svelte.js +90 -65
  117. package/src/runtime/client/snapshots.js +147 -0
  118. package/src/runtime/client/state.svelte.js +94 -55
  119. package/src/runtime/client/stream.js +3 -2
  120. package/src/runtime/client/types.d.ts +13 -9
  121. package/src/runtime/client/utils.js +22 -110
  122. package/src/runtime/components/root.svelte +56 -0
  123. package/src/runtime/env/dynamic/private.js +7 -0
  124. package/src/runtime/env/dynamic/public.js +7 -0
  125. package/src/runtime/env/static/private.js +6 -0
  126. package/src/runtime/env/static/public.js +6 -0
  127. package/src/runtime/form-utils.js +172 -61
  128. package/src/runtime/pathname.js +20 -32
  129. package/src/runtime/props.svelte.js +72 -0
  130. package/src/runtime/server/constants.js +0 -3
  131. package/src/runtime/server/cookie.js +72 -55
  132. package/src/runtime/server/csrf.js +65 -0
  133. package/src/runtime/server/data/index.js +49 -53
  134. package/src/runtime/server/dev.js +22 -0
  135. package/src/runtime/server/endpoint.js +6 -7
  136. package/src/runtime/server/env_module.js +0 -5
  137. package/src/runtime/server/errors.js +160 -0
  138. package/src/runtime/server/fetch.js +32 -39
  139. package/src/runtime/server/index.js +90 -64
  140. package/src/runtime/server/internal.js +71 -0
  141. package/src/runtime/server/page/actions.js +85 -64
  142. package/src/runtime/server/page/crypto.js +2 -2
  143. package/src/runtime/server/page/csp.js +88 -104
  144. package/src/runtime/server/page/data_serializer.js +24 -25
  145. package/src/runtime/server/page/index.js +36 -53
  146. package/src/runtime/server/page/load_data.js +50 -57
  147. package/src/runtime/server/page/render.js +173 -239
  148. package/src/runtime/server/page/respond_with_error.js +17 -31
  149. package/src/runtime/server/page/serialize_data.js +2 -13
  150. package/src/runtime/server/page/server_routing.js +85 -26
  151. package/src/runtime/server/remote-functions.js +640 -0
  152. package/src/runtime/server/respond.js +190 -131
  153. package/src/runtime/server/sourcemaps.js +183 -0
  154. package/src/runtime/server/state.js +53 -0
  155. package/src/runtime/server/utils.js +13 -155
  156. package/src/runtime/shared.js +20 -40
  157. package/src/runtime/utils.js +3 -0
  158. package/src/types/ambient-private.d.ts +11 -1
  159. package/src/types/ambient.d.ts +87 -36
  160. package/src/types/global-private.d.ts +25 -25
  161. package/src/types/internal.d.ts +163 -157
  162. package/src/types/private.d.ts +41 -1
  163. package/src/utils/error.js +28 -4
  164. package/src/utils/escape.js +9 -25
  165. package/src/utils/features.js +1 -1
  166. package/src/utils/filesystem.js +1 -23
  167. package/src/utils/fork.js +7 -2
  168. package/src/utils/hash.js +21 -0
  169. package/src/utils/http.js +8 -7
  170. package/src/utils/import.js +9 -2
  171. package/src/utils/imports.js +83 -0
  172. package/src/utils/mime.js +9 -0
  173. package/src/utils/page_nodes.js +6 -7
  174. package/src/utils/params.js +67 -0
  175. package/src/utils/regex.js +9 -0
  176. package/src/utils/routing.js +145 -73
  177. package/src/utils/shared-iterator.js +5 -0
  178. package/src/utils/streaming.js +14 -4
  179. package/src/utils/url.js +33 -2
  180. package/src/version.js +1 -1
  181. package/types/index.d.ts +2616 -2433
  182. package/types/index.d.ts.map +120 -106
  183. package/src/core/sync/write_ambient.js +0 -18
  184. package/src/core/sync/write_root.js +0 -148
  185. package/src/core/sync/write_tsconfig.js +0 -250
  186. package/src/exports/internal/server.js +0 -22
  187. package/src/exports/node/polyfills.js +0 -30
  188. package/src/runtime/server/app.js +0 -9
  189. package/src/runtime/server/remote.js +0 -457
  190. package/src/runtime/shared-server.js +0 -7
  191. package/src/runtime/telemetry/otel.js +0 -21
  192. package/src/runtime/telemetry/record_span.js +0 -65
  193. package/src/types/synthetic/$lib.md +0 -5
  194. /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
  195. /package/src/{runtime/telemetry/noop.js → telemetry.js} +0 -0
@@ -1,9 +1,8 @@
1
- import { app } from '../../client.js';
2
- import * as devalue from 'devalue';
3
- import { HttpError, Redirect } from '@sveltejs/kit/internal';
1
+ import { query_responses, handle_error } from '../../client.js';
2
+ import { HttpError, Redirect, HandledHttpError } 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 HandledHttpError(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,14 +175,14 @@ 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
  }
160
182
 
161
183
  if (error instanceof HttpError) {
162
- // Server intentionally sent an error. Surface it and stop.
163
- this.fail(error);
184
+ // Server or client intentionally produced an error. Surface it and stop.
185
+ await this.#fail(error);
164
186
  break;
165
187
  }
166
188
 
@@ -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 HandledHttpError(error));
421
+ }
422
+
381
423
  get [Symbol.toStringTag]() {
382
424
  return 'LiveQuery';
383
425
  }
@@ -1,8 +1,10 @@
1
- import { app_dir, base } from '$app/paths/internal/client';
1
+ /** @import { RemoteFunctionResponse } from 'types' */
2
+ import { app_dir, base } from '#app/paths';
2
3
  import { app } from '../../client.js';
3
- import { get_remote_request_headers, handle_side_channel_response } from '../shared.svelte.js';
4
+ import { notify_version } from '../../state.svelte.js';
5
+ import { handle_side_channel_response } from '../shared.svelte.js';
4
6
  import * as devalue from 'devalue';
5
- import { HttpError } from '@sveltejs/kit/internal';
7
+ import { HttpError, HandledHttpError } from '@sveltejs/kit/internal';
6
8
  import { noop } from '../../../../utils/functions.js';
7
9
  import { read_sse } from '../../sse.js';
8
10
 
@@ -23,25 +25,26 @@ export async function* create_live_iterator(
23
25
  const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
24
26
 
25
27
  const response = await fetch(url, {
26
- headers: get_remote_request_headers(),
27
28
  signal: controller.signal
28
29
  });
29
30
 
31
+ // detect new deployments from the response header
32
+ notify_version(response.headers.get('x-sveltekit-version'));
33
+
30
34
  if (!response.ok) {
31
- const result = await response.json().catch(() => ({
32
- type: 'error',
33
- status: response.status,
34
- error: response.statusText
35
- }));
35
+ /** @type {RemoteFunctionResponse | undefined} */
36
+ const result = await response.json().catch(() => undefined);
36
37
 
37
- throw new HttpError(result.status ?? response.status ?? 500, result.error);
38
+ throw result?.type === 'error'
39
+ ? new HandledHttpError(result.error)
40
+ : new HttpError({ status: response.status, message: response.statusText });
38
41
  }
39
42
 
40
43
  if (response.headers.get('content-type')?.includes('application/json')) {
41
44
  // we can end up here if we e.g. redirect in `handle`
42
45
  const result = await response.json();
43
46
  await handle_side_channel_response(result);
44
- throw new HttpError(500, 'Invalid query.live response');
47
+ throw new HttpError({ status: 500, message: 'Invalid query.live response' });
45
48
  }
46
49
 
47
50
  if (!response.body) {
@@ -60,7 +63,7 @@ export async function* create_live_iterator(
60
63
  }
61
64
 
62
65
  await handle_side_channel_response(node);
63
- throw new HttpError(500, 'Invalid query.live response');
66
+ throw new HttpError({ status: 500, message: 'Invalid query.live response' });
64
67
  }
65
68
  } finally {
66
69
  try {
@@ -1,4 +1,4 @@
1
- import { app, live_query_map } from '../../client.js';
1
+ import { live_query_map } from '../../client.js';
2
2
  import { pin_in_effect, pin_while_resolving, QUERY_RESOURCE_KEY } from '../shared.svelte.js';
3
3
  import { create_remote_key, stringify_remote_arg } from '../../../shared.js';
4
4
  import { LiveQuery } from './instance.svelte.js';
@@ -20,7 +20,7 @@ export class LiveQueryProxy {
20
20
  */
21
21
  constructor(id, arg) {
22
22
  this.#id = id;
23
- this.#payload = stringify_remote_arg(arg, app.hooks.transport);
23
+ this.#payload = stringify_remote_arg(arg);
24
24
  this.#key = create_remote_key(id, this.#payload);
25
25
  Object.defineProperty(this, QUERY_RESOURCE_KEY, { value: this.#key });
26
26
 
@@ -74,16 +74,6 @@ export class LiveQueryProxy {
74
74
  return this.#get_cached_query().done;
75
75
  }
76
76
 
77
- /**
78
- * @deprecated Use `for await (const value of liveQuery())` instead.
79
- * @returns {AsyncGenerator<T>}
80
- */
81
- run() {
82
- throw new Error(
83
- '`.run()` has been removed from live queries. Use `for await (const value of liveQuery())` instead.'
84
- );
85
- }
86
-
87
77
  reconnect() {
88
78
  return this.#get_cached_query().reconnect();
89
79
  }
@@ -1,11 +1,12 @@
1
- /** @import { RemoteFunctionResponse, RemoteSingleflightMap, RemoteSingleflightEntry } from 'types' */
2
- /** @import { RemoteQueryUpdate } from '@sveltejs/kit' */
1
+ /** @import { RemoteFunctionResponse, RemoteFunctionData, RemoteFunctionDataNode } from 'types' */
2
+ /** @import { RemoteQueryUpdate } from '@sveltejs/kit/remote' */
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 { HttpError, Redirect } from '@sveltejs/kit/internal';
5
+ import { app, _goto, live_query_map, query_map, query_responses } from '../client.js';
6
+ import { HttpError, Redirect, HandledHttpError } from '@sveltejs/kit/internal';
6
7
  import { untrack } from 'svelte';
7
8
  import { create_remote_key, split_remote_key } from '../../shared.js';
8
- import { navigating, page } from '../state.svelte.js';
9
+ import { navigating, page, notify_version } from '../state.svelte.js';
9
10
 
10
11
  /** Indicates a query function, as opposed to a query instance */
11
12
  export const QUERY_FUNCTION_ID = Symbol('sveltekit.query_function_id');
@@ -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 HandledHttpError(node.e);
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,25 +108,81 @@ 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);
115
+ const status = response.status;
116
+
117
+ // detect new deployments from the response header
118
+ notify_version(response.headers.get('x-sveltekit-version'));
105
119
 
106
120
  if (!response.ok) {
107
- throw new HttpError(500, 'Failed to execute remote function');
121
+ const result = await response.json().catch(() => undefined);
122
+
123
+ throw result?.type === 'error'
124
+ ? new HandledHttpError({ status, ...result.error })
125
+ : new HttpError({ status, message: response.statusText });
108
126
  }
109
127
 
110
128
  const result = /** @type {RemoteFunctionResponse} */ (await response.json());
111
129
 
112
- const resolved = await handle_side_channel_response(result);
130
+ if (result.type === 'error') {
131
+ throw new HandledHttpError(result.error);
132
+ }
133
+
134
+ const data = /** @type {RemoteFunctionData} */ (
135
+ result.data ? devalue.parse(result.data, app.decoders) : {}
136
+ );
137
+
138
+ /**
139
+ * @param {string} key
140
+ * @param {CacheEntry | undefined} entry
141
+ * @param {any} result
142
+ */
143
+ function refresh(key, entry, result) {
144
+ if (entry?.resource) {
145
+ if (result.e) {
146
+ entry.resource.fail(new HandledHttpError(result.e));
147
+ } else {
148
+ entry.resource.set(result.v);
149
+ }
150
+ } else if (!result.e) {
151
+ // `query_responses` stores `{ v }`/`{ e }` nodes, not raw values.
152
+ // Errors are deliberately dropped here: they are responses to a specific
153
+ // refresh, not durable state a future resource should initialize with
154
+ query_responses[key] = result;
155
+ }
156
+ }
157
+
158
+ // update queries with refreshed data
159
+ if (data.q) {
160
+ for (const key in data.q) {
161
+ const parts = split_remote_key(key);
162
+ const entry = query_map.get(parts.id)?.get(parts.payload);
163
+
164
+ refresh(key, entry, data.q[key]);
165
+ }
166
+ }
167
+
168
+ // reconnect live queries
169
+ if (data.l) {
170
+ for (const key in data.l) {
171
+ const parts = split_remote_key(key);
172
+ const entry = live_query_map.get(parts.id)?.get(parts.payload);
113
173
 
114
- return resolved.result;
174
+ refresh(key, entry, data.l[key]);
175
+
176
+ // `fail()` is terminal, so only reconnect on the success path —
177
+ // reconnecting after a hard failure would wipe the error state and
178
+ // restart the stream (see commit 63a3e83 regression).
179
+ if (!data.l[key].e) {
180
+ void entry?.resource.reconnect();
181
+ }
182
+ }
183
+ }
184
+
185
+ return data;
115
186
  }
116
187
 
117
188
  /**
@@ -120,12 +191,13 @@ export async function remote_request(url, headers) {
120
191
  */
121
192
  export async function handle_side_channel_response(response) {
122
193
  if (response.type === 'redirect') {
123
- await goto(response.location);
194
+ // Use internal version to allow redirects to external URLs
195
+ await _goto(response.location);
124
196
  throw new Redirect(307, response.location);
125
197
  }
126
198
 
127
199
  if (response.type === 'error') {
128
- throw new HttpError(response.status ?? 500, response.error);
200
+ throw new HandledHttpError(response.error);
129
201
  }
130
202
 
131
203
  return response;
@@ -206,50 +278,3 @@ export function categorize_updates(updates) {
206
278
 
207
279
  return { overrides, refreshes };
208
280
  }
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,147 @@
1
+ import { BROWSER, DEV } from 'esm-env';
2
+ import { onMount } from 'svelte';
3
+ import * as storage from './session-storage.js';
4
+ import { NAVIGATION_SNAPSHOT_KEY } from './constants.js';
5
+ import { hash } from '../../utils/hash.js';
6
+ import { parse, stringify } from '#app/internal/transport';
7
+
8
+ /**
9
+ * @typedef {{ id: string; capture: () => any; restore: (value: any) => void; reset?: () => void }} SnapshotRegistration
10
+ */
11
+
12
+ // Function snapshots use history indexes so shallow entries have distinct state.
13
+ /** @type {Record<string, Record<string, string>>} */
14
+ const navigation_snapshots = storage.get(NAVIGATION_SNAPSHOT_KEY) ?? {};
15
+
16
+ /** @type {Set<SnapshotRegistration>} */
17
+ const snapshot_registrations = new Set();
18
+
19
+ /**
20
+ * The registrations mounted right now. Take before navigating and pass to
21
+ * `restore_navigation_snapshot` — only these are eligible for `reset`.
22
+ * @returns {Set<SnapshotRegistration>}
23
+ */
24
+ export function current_registrations() {
25
+ return new Set(snapshot_registrations);
26
+ }
27
+
28
+ // injected by client.js: the live history index while the router is idle, null mid-navigation
29
+ /** @type {() => number | null} */
30
+ let get_idle_history_index = () => null;
31
+
32
+ /** @param {() => number | null} fn */
33
+ export function init_snapshots(fn) {
34
+ get_idle_history_index = fn;
35
+ }
36
+
37
+ /** @param {number} index */
38
+ export function capture_navigation_snapshot(index) {
39
+ if (snapshot_registrations.size === 0) {
40
+ delete navigation_snapshots[index];
41
+ return;
42
+ }
43
+
44
+ navigation_snapshots[index] = Object.fromEntries(
45
+ Array.from(snapshot_registrations, ({ id, capture }) => [id, stringify(capture())])
46
+ );
47
+ }
48
+
49
+ /**
50
+ * @param {number} index
51
+ * @param {Set<SnapshotRegistration>} [reset_registrations]
52
+ */
53
+ export function restore_navigation_snapshot(index, reset_registrations) {
54
+ const values = navigation_snapshots[index];
55
+
56
+ for (const registration of snapshot_registrations) {
57
+ if (values && Object.hasOwn(values, registration.id)) {
58
+ registration.restore(parse(values[registration.id]));
59
+ } else if (reset_registrations?.has(registration)) {
60
+ registration.reset?.();
61
+ }
62
+ }
63
+ }
64
+
65
+ /** @param {number} index */
66
+ export function delete_navigation_snapshot(index) {
67
+ delete navigation_snapshots[index];
68
+ }
69
+
70
+ /** @param {number} index */
71
+ export function persist_navigation_snapshots(index) {
72
+ capture_navigation_snapshot(index);
73
+ storage.set(NAVIGATION_SNAPSHOT_KEY, navigation_snapshots);
74
+ }
75
+
76
+ /**
77
+ * @param {string | undefined} stack
78
+ * @returns {string}
79
+ */
80
+ function callsite_id(stack) {
81
+ let frames = stack?.split('\n') ?? [];
82
+ if (frames[0]?.trim() === 'Error') frames = frames.slice(1);
83
+
84
+ // only the callsite frame is stable: frames above it differ between hydration and
85
+ // re-mounting, and Vite query strings (stripped here) change on module invalidation
86
+ const frame = frames[1]?.replace(/\?[^)\s]*(?=:\d+:\d+\)?$)/, '');
87
+
88
+ if (!frame) {
89
+ throw new Error('Could not generate a snapshot id from the stack trace. Pass an `id` instead.');
90
+ }
91
+
92
+ return hash(frame);
93
+ }
94
+
95
+ /**
96
+ * A lifecycle function that captures state before navigating and restores it when traversing history.
97
+ *
98
+ * By default, the snapshot `id` is generated from the call site. Pass an explicit `id` to keep snapshots stable across deployments or distinguish multiple uses of a shared helper.
99
+ *
100
+ * The optional `reset` callback runs on navigations where there is no captured value to restore, such as when a new history entry is created. Captured values are serialized with the app's transport hook.
101
+ *
102
+ * `snapshot` must be called during a component initialization. It remains active as long as the component is mounted.
103
+ * @template T
104
+ * @param {{ id?: string; capture: () => T; restore: (value: T) => void; reset?: () => void }} options
105
+ * @returns {void}
106
+ */
107
+ export function snapshot(options) {
108
+ if (!BROWSER) return;
109
+
110
+ let id = options.id;
111
+ if (id === undefined) {
112
+ // restore any lowered third-party limit, else every callsite collapses to one id
113
+ const limit = Error.stackTraceLimit;
114
+ const lowered = typeof limit === 'number' && limit < 3;
115
+ if (lowered) Error.stackTraceLimit = 3;
116
+ const stack = new Error().stack;
117
+ if (lowered) Error.stackTraceLimit = limit;
118
+
119
+ id = callsite_id(stack);
120
+ }
121
+
122
+ const registration = { ...options, id };
123
+
124
+ onMount(() => {
125
+ if (DEV && Array.from(snapshot_registrations).some((existing) => existing.id === id)) {
126
+ throw new Error(
127
+ options.id === undefined
128
+ ? 'snapshot() was called multiple times from the same call site. Pass a unique `id` to distinguish the instances.'
129
+ : `A snapshot with id "${options.id}" is already registered. Pass a unique \`id\`.`
130
+ );
131
+ }
132
+
133
+ const index = get_idle_history_index();
134
+ if (index !== null) {
135
+ const values = navigation_snapshots[index];
136
+ if (values && Object.hasOwn(values, id)) {
137
+ registration.restore(parse(values[id]));
138
+ }
139
+ }
140
+
141
+ snapshot_registrations.add(registration);
142
+
143
+ return () => {
144
+ snapshot_registrations.delete(registration);
145
+ };
146
+ });
147
+ }