@sveltejs/kit 2.60.0 → 2.61.0

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 +10 -11
  2. package/src/core/postbuild/analyse.js +1 -3
  3. package/src/core/sync/create_manifest_data/conflict.js +72 -0
  4. package/src/core/sync/create_manifest_data/index.js +1 -65
  5. package/src/core/sync/write_non_ambient.js +2 -2
  6. package/src/core/sync/write_types/index.js +1 -1
  7. package/src/exports/public.d.ts +23 -30
  8. package/src/exports/vite/build/build_server.js +36 -13
  9. package/src/exports/vite/dev/index.js +4 -2
  10. package/src/exports/vite/index.js +18 -16
  11. package/src/runtime/app/server/index.js +1 -2
  12. package/src/runtime/app/server/remote/form.js +10 -0
  13. package/src/runtime/app/server/remote/query.js +111 -44
  14. package/src/runtime/client/client.js +13 -8
  15. package/src/runtime/client/remote-functions/cache.svelte.js +157 -0
  16. package/src/runtime/client/remote-functions/form.svelte.js +235 -196
  17. package/src/runtime/client/remote-functions/index.js +2 -2
  18. package/src/runtime/client/remote-functions/prerender.svelte.js +1 -2
  19. package/src/runtime/client/remote-functions/query/cache.js +4 -0
  20. package/src/runtime/client/remote-functions/query/index.js +48 -0
  21. package/src/runtime/client/remote-functions/query/instance.svelte.js +249 -0
  22. package/src/runtime/client/remote-functions/query/proxy.js +156 -0
  23. package/src/runtime/client/remote-functions/query-batch.svelte.js +1 -1
  24. package/src/runtime/client/remote-functions/query-live/cache.js +4 -0
  25. package/src/runtime/client/remote-functions/query-live/index.js +31 -0
  26. package/src/runtime/client/remote-functions/{query-live.svelte.js → query-live/instance.svelte.js} +61 -310
  27. package/src/runtime/client/remote-functions/query-live/iterator.js +91 -0
  28. package/src/runtime/client/remote-functions/query-live/proxy.js +144 -0
  29. package/src/runtime/client/remote-functions/shared.svelte.js +53 -6
  30. package/src/runtime/client/utils.js +1 -1
  31. package/src/runtime/form-utils.js +7 -16
  32. package/src/runtime/server/index.js +2 -3
  33. package/src/runtime/server/page/actions.js +2 -9
  34. package/src/runtime/server/page/csp.js +3 -4
  35. package/src/runtime/server/page/render.js +13 -14
  36. package/src/runtime/server/respond.js +61 -38
  37. package/src/runtime/server/utils.js +23 -3
  38. package/src/types/global-private.d.ts +5 -0
  39. package/src/types/internal.d.ts +45 -8
  40. package/src/utils/routing.js +3 -1
  41. package/src/utils/shared-iterator.js +213 -0
  42. package/src/version.js +1 -1
  43. package/types/index.d.ts +28 -32
  44. package/types/index.d.ts.map +1 -1
  45. package/src/runtime/client/remote-functions/query.svelte.js +0 -512
@@ -14,6 +14,7 @@ import {
14
14
  import { handle_error_and_jsonify } from '../../../server/utils.js';
15
15
  import { HttpError, SvelteKitError } from '@sveltejs/kit/internal';
16
16
  import { noop } from '../../../../utils/functions.js';
17
+ import { SharedIterator } from '../../../../utils/shared-iterator.js';
17
18
 
18
19
  /**
19
20
  * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call.
@@ -153,24 +154,6 @@ function live(validate_or_fn, maybe_fn) {
153
154
  const run = (event, state, get_input) =>
154
155
  run_remote_generator(event, state, false, get_input, fn, __.name);
155
156
 
156
- /**
157
- * @param {any} generator
158
- * @returns {Promise<any>}
159
- */
160
- const first_value = async (generator) => {
161
- try {
162
- const { value, done } = await generator.next();
163
-
164
- if (done) {
165
- throw new Error(`query.live '${__.name}' did not yield a value`);
166
- }
167
-
168
- return value;
169
- } finally {
170
- await generator.return(undefined);
171
- }
172
- };
173
-
174
157
  /** @type {RemoteQueryLiveInternals} */
175
158
  const __ = {
176
159
  type: 'query_live',
@@ -181,8 +164,8 @@ function live(validate_or_fn, maybe_fn) {
181
164
  bind(payload, validated_arg) {
182
165
  const { event, state } = get_request_store();
183
166
 
184
- return create_live_query_resource(__, payload, state, () =>
185
- first_value(run(event, state, () => validated_arg))
167
+ return create_live_query_resource(__, payload, state, event.request.signal, () =>
168
+ run(event, state, () => validated_arg)
186
169
  );
187
170
  }
188
171
  };
@@ -198,8 +181,8 @@ function live(validate_or_fn, maybe_fn) {
198
181
  const { event, state } = get_request_store();
199
182
  const payload = stringify_remote_arg(arg, state.transport);
200
183
 
201
- return create_live_query_resource(__, payload, state, () =>
202
- first_value(run(event, state, () => validate(arg)))
184
+ return create_live_query_resource(__, payload, state, event.request.signal, () =>
185
+ run(event, state, () => validate(arg))
203
186
  );
204
187
  };
205
188
 
@@ -250,9 +233,6 @@ function batch(validate_or_fn, maybe_fn) {
250
233
  /** @type {(arg?: any) => MaybePromise<Input>} */
251
234
  const validate = create_validator(validate_or_fn, maybe_fn);
252
235
 
253
- /** @type {Map<string, { get_validated: () => MaybePromise<any>, resolvers: Array<{resolve: (value: any) => void, reject: (error: any) => void}> }>} */
254
- let batching = new Map();
255
-
256
236
  /**
257
237
  * Enqueues a single call into the current batch (creating one if necessary)
258
238
  * and returns a promise that resolves with the result for this entry.
@@ -265,23 +245,29 @@ function batch(validate_or_fn, maybe_fn) {
265
245
  const { event, state } = get_request_store();
266
246
 
267
247
  return new Promise((resolve, reject) => {
268
- const entry = batching.get(payload);
248
+ const batches = (state.remote.batches ??=
249
+ /** @type {NonNullable<typeof state.remote.batches>} */ (new Map()));
250
+ let batched = batches.get(__.id);
251
+ if (!batched) {
252
+ batched = new Map();
253
+ batches.set(__.id, batched);
254
+ }
255
+ const entry = batched.get(payload);
269
256
 
270
257
  if (entry) {
271
258
  entry.resolvers.push({ resolve, reject });
272
259
  return;
273
260
  }
274
261
 
275
- batching.set(payload, {
262
+ batched.set(payload, {
276
263
  get_validated,
277
264
  resolvers: [{ resolve, reject }]
278
265
  });
279
266
 
280
- if (batching.size > 1) return;
267
+ if (batched.size > 1) return;
281
268
 
282
269
  setTimeout(async () => {
283
- const batched = batching;
284
- batching = new Map();
270
+ batches.delete(__.id);
285
271
  const entries = Array.from(batched.values());
286
272
 
287
273
  try {
@@ -445,21 +431,17 @@ function create_query_resource(__, payload, state, fn) {
445
431
  const value = is_immediate_refresh ? get_promise() : fn();
446
432
  return update_refresh_value(refresh_context, value, is_immediate_refresh);
447
433
  },
448
- run() {
449
- // potential TODO: if we want to be able to run queries at the top level of modules / outside of the request context, we could technically remove
450
- // the requirement that `state` is defined, but that's kind of an annoying change to make, so we're going to wait on that until we have any sort of
451
- // concrete use case.
452
- if (!state.is_in_universal_load) {
453
- throw new Error(
454
- 'On the server, .run() can only be called in universal `load` functions. Anywhere else, just await the query directly'
455
- );
456
- }
457
- return get_response(__, payload, state, fn);
458
- },
459
434
  /** @param {any} value */
460
435
  set(value) {
461
436
  return update_refresh_value(get_refresh_context(__, 'set', payload), value);
462
437
  },
438
+ // TODO 3.0 remove this
439
+ // @ts-expect-error This method no longer exists
440
+ run() {
441
+ throw new Error(
442
+ `\`myQuery().run()\` has been removed — please replace it with \`myQuery()\`. See https://github.com/sveltejs/kit/pull/15779 for more details`
443
+ );
444
+ },
463
445
  /** @type {Promise<any>['then']} */
464
446
  then(onfulfilled, onrejected) {
465
447
  return get_promise().then(onfulfilled, onrejected);
@@ -477,13 +459,21 @@ function create_query_resource(__, payload, state, fn) {
477
459
  * @param {RemoteQueryLiveInternals} __
478
460
  * @param {string} payload — the stringified raw argument (i.e. the cache key the client will use)
479
461
  * @param {RequestState} state
480
- * @param {() => Promise<any>} get_first_value
462
+ * @param {AbortSignal} signal the request signal; aborts in-flight iteration when the client disconnects
463
+ * @param {() => AsyncGenerator<any, void, void>} get_generator
481
464
  * @returns {RemoteLiveQuery<any>}
482
465
  */
483
- function create_live_query_resource(__, payload, state, get_first_value) {
466
+ function create_live_query_resource(__, payload, state, signal, get_generator) {
484
467
  /** @type {Promise<any> | null} */
485
468
  let promise = null;
486
469
 
470
+ const get_first_value = async () => {
471
+ for await (const value of get_generator()) {
472
+ return value;
473
+ }
474
+ throw new Error(`query.live '${__.name}' did not yield a value`);
475
+ };
476
+
487
477
  const get_promise = () => {
488
478
  return (promise ??= get_response(__, payload, state, get_first_value));
489
479
  };
@@ -537,19 +527,96 @@ function create_live_query_resource(__, payload, state, get_first_value) {
537
527
  reconnects.set(create_remote_key(__.id, payload), get_promise());
538
528
  return Promise.resolve();
539
529
  },
530
+ /** @ts-expect-error This method no longer exists */
540
531
  run() {
541
- throw new Error('Cannot call .run() on a live query on the server');
532
+ throw new Error(
533
+ '`.run()` has been removed from live queries. Use `for await (const value of liveQuery())` instead.'
534
+ );
542
535
  },
543
536
  /** @type {Promise<any>['then']} */
544
537
  then(onfulfilled, onrejected) {
545
538
  return get_promise().then(onfulfilled, onrejected);
546
539
  },
540
+ [Symbol.asyncIterator]() {
541
+ const key = create_remote_key(__.id, payload);
542
+ const cache = (state.remote.live_iterators ??= new Map());
543
+ let cached = cache.get(key);
544
+ if (!cached) {
545
+ cached = create_shared_live_iterator(signal, get_generator);
546
+ cache.set(key, cached);
547
+ }
548
+ return cached.subscribe();
549
+ },
547
550
  get [Symbol.toStringTag]() {
548
551
  return 'LiveQueryResource';
549
552
  }
550
553
  };
551
554
  }
552
555
 
556
+ /**
557
+ * Wraps a lazily-created live-query generator so that multiple `for await`
558
+ * consumers within the same request share one underlying iteration. The first
559
+ * subscriber starts the generator; values are broadcast to all subscribers
560
+ * via a `SharedIterator`. When the last subscriber unsubscribes, the generator
561
+ * is closed via `generator.return(undefined)`.
562
+ *
563
+ * If `signal` aborts (typically because the client has disconnected), the
564
+ * pump is torn down and any in-flight `next()` calls on consumer iterators
565
+ * resolve with `{ done: true }`, so suspended `for await` loops unwind
566
+ * cleanly rather than leaking.
567
+ *
568
+ * @param {AbortSignal} signal
569
+ * @param {() => AsyncGenerator<any, void, void>} get_generator
570
+ */
571
+ function create_shared_live_iterator(signal, get_generator) {
572
+ return new SharedIterator((instance) => {
573
+ // Don't bother starting the pump if the request has already been
574
+ // aborted between cache creation and first subscription.
575
+ if (signal.aborted) {
576
+ instance.done();
577
+ return noop;
578
+ }
579
+
580
+ const generator = get_generator();
581
+
582
+ // Set to `true` when we deliberately close the generator (because every
583
+ // subscriber has unsubscribed, or the request was aborted). The pump's
584
+ // `generator.next()` will reject as a result; we use this flag to swallow that
585
+ // abort error rather than surfacing it through `instance.fail()`.
586
+ let aborted = false;
587
+
588
+ const close = () => {
589
+ aborted = true;
590
+ void generator.return().catch(noop);
591
+ };
592
+
593
+ // On request abort, tear down the pump and notify subscribers. `done()` is
594
+ // used (rather than `fail()`) because an aborted request is a normal
595
+ // termination — there's no error to surface to user code that's already
596
+ // been disconnected from the client.
597
+ signal.addEventListener('abort', () => (close(), instance.done()), { once: true });
598
+
599
+ void (async () => {
600
+ try {
601
+ while (true) {
602
+ const result = await generator.next();
603
+ if (result.done) {
604
+ instance.done();
605
+ return;
606
+ }
607
+ instance.push(result.value);
608
+ }
609
+ } catch (error) {
610
+ if (!aborted) instance.fail(error);
611
+ } finally {
612
+ close();
613
+ }
614
+ })();
615
+
616
+ return close;
617
+ });
618
+ }
619
+
553
620
  // Add batch as a property to the query function
554
621
  Object.defineProperty(query, 'batch', { value: batch, enumerable: true });
555
622
  Object.defineProperty(query, 'live', { value: live, enumerable: true });
@@ -1,5 +1,6 @@
1
- /** @import { RemoteQueryCacheEntry } from './remote-functions/query.svelte.js' */
2
- /** @import { RemoteLiveQueryCacheEntry } from './remote-functions/query-live.svelte.js' */
1
+ /** @import { CacheEntry } from './remote-functions/cache.svelte.js' */
2
+ /** @import { Query } from './remote-functions/query/instance.svelte.js' */
3
+ /** @import { LiveQuery } from './remote-functions/query-live/instance.svelte.js' */
3
4
  import { BROWSER, DEV } from 'esm-env';
4
5
  import * as svelte from 'svelte';
5
6
  import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
@@ -302,13 +303,13 @@ const preload_tokens = new Set();
302
303
  export let pending_invalidate;
303
304
 
304
305
  /**
305
- * @type {Map<string, Map<string, RemoteQueryCacheEntry<any>>>}
306
+ * @type {Map<string, Map<string, CacheEntry<Query<any>>>>}
306
307
  * A map of query id -> payload -> query internals for all active queries.
307
308
  */
308
309
  export const query_map = new Map();
309
310
 
310
311
  /**
311
- * @type {Map<string, Map<string, RemoteLiveQueryCacheEntry<any>>>}
312
+ * @type {Map<string, Map<string, CacheEntry<LiveQuery<any>>>>}
312
313
  * A map of id -> payload -> live query internals for all active queries.
313
314
  */
314
315
  export const live_query_map = new Map();
@@ -658,7 +659,8 @@ async function _preload_code(url) {
658
659
  * @param {boolean} hydrate
659
660
  */
660
661
  async function initialize(result, target, hydrate) {
661
- if (DEV && result.state.error && document.querySelector('vite-error-overlay')) return;
662
+ if (__SVELTEKIT_DEV__ && result.state.error && document.querySelector('vite-error-overlay'))
663
+ return;
662
664
 
663
665
  /** @type {import('@sveltejs/kit').NavigationEvent} */
664
666
  const nav = {
@@ -672,8 +674,11 @@ async function initialize(result, target, hydrate) {
672
674
  nav
673
675
  };
674
676
 
675
- const style = document.querySelector('style[data-sveltekit]');
676
- if (style) style.remove();
677
+ // Removes the style node we used to avoid FOUC during development
678
+ if (__SVELTEKIT_DEV__) {
679
+ const style = document.querySelector('style[data-sveltekit]');
680
+ if (style) style.remove();
681
+ }
677
682
 
678
683
  update(/** @type {import('@sveltejs/kit').Page} */ (result.props.page));
679
684
 
@@ -2621,7 +2626,7 @@ function _start_router() {
2621
2626
  });
2622
2627
 
2623
2628
  // @ts-expect-error this isn't supported everywhere yet
2624
- if (!navigator.connection?.saveData) {
2629
+ if (!navigator.connection?.saveData && !/2g/.test(navigator.connection?.effectiveType)) {
2625
2630
  setup_preload();
2626
2631
  }
2627
2632
 
@@ -0,0 +1,157 @@
1
+ import { tick } from 'svelte';
2
+ import { once } from '../../../utils/functions.js';
3
+
4
+ /**
5
+ * @template R
6
+ * @typedef {object} CacheEntry
7
+ * @property {number} proxy_count The number of live proxy instances referencing this
8
+ * entry. The entry is eligible for eviction when this hits zero.
9
+ * @property {R} resource The actual reactive resource (Query or LiveQuery).
10
+ * @property {() => void} cleanup Tears down the `$effect.root` that owns the resource.
11
+ * Run when the entry is evicted.
12
+ */
13
+
14
+ /**
15
+ * @template R
16
+ * @typedef {{ entry: CacheEntry<R>, id: string, payload: string }} ProxyFinalizerToken
17
+ */
18
+
19
+ /**
20
+ * Cache controller bound to a specific cache map and resource teardown function. Owns the
21
+ * eviction scheduling and FinalizationRegistry for its cache.
22
+ *
23
+ * Methods are defined as arrow-function class fields so they can be destructured and
24
+ * re-exported without losing their `this` binding.
25
+ *
26
+ * @template R
27
+ */
28
+ export class CacheController {
29
+ /** @type {Map<string, Map<string, CacheEntry<R>>>} */
30
+ #cache_map;
31
+
32
+ /** @type {((resource: R) => void) | undefined} */
33
+ #destroy_resource;
34
+
35
+ /**
36
+ * The held value points at the cache entry the proxy is contributing to. When the
37
+ * proxy is GC'd, we decrement that entry's `proxy_count` and schedule a deferred
38
+ * eviction check.
39
+ *
40
+ * @type {FinalizationRegistry<ProxyFinalizerToken<R>>}
41
+ */
42
+ #proxy_finalizer = new FinalizationRegistry(({ entry, id, payload }) => {
43
+ this.deref(entry, id, payload);
44
+ });
45
+
46
+ /**
47
+ * @param {Map<string, Map<string, CacheEntry<R>>>} cache_map
48
+ * @param {(resource: R) => void} [destroy_resource] Optional teardown hook called on
49
+ * the resource itself before the cache entry's `$effect.root` cleanup runs. Used by
50
+ * live queries to detach event listeners and abort connections.
51
+ */
52
+ constructor(cache_map, destroy_resource) {
53
+ this.#cache_map = cache_map;
54
+ this.#destroy_resource = destroy_resource;
55
+ }
56
+
57
+ /**
58
+ * Get-or-create the cache entry for `(id, payload)`. The resource is constructed
59
+ * inside an `$effect.root`, the cleanup of which is stored on the entry.
60
+ *
61
+ * @param {string} id
62
+ * @param {string} payload
63
+ * @param {() => R} create_resource
64
+ * @returns {CacheEntry<R>}
65
+ */
66
+ ensure_entry = (id, payload, create_resource) => {
67
+ let entries = this.#cache_map.get(id);
68
+
69
+ if (!entries) {
70
+ entries = new Map();
71
+ this.#cache_map.set(id, entries);
72
+ }
73
+
74
+ let entry = entries.get(payload);
75
+
76
+ if (!entry) {
77
+ const c = /** @type {CacheEntry<R>} */ ({
78
+ proxy_count: 0,
79
+ resource: /** @type {R} */ (/** @type {unknown} */ (null)),
80
+ cleanup: /** @type {() => void} */ (/** @type {unknown} */ (null))
81
+ });
82
+
83
+ c.cleanup = $effect.root(() => {
84
+ c.resource = create_resource();
85
+ });
86
+
87
+ entry = c;
88
+ entries.set(payload, entry);
89
+ }
90
+
91
+ return entry;
92
+ };
93
+
94
+ /**
95
+ * Register a reference to a resource cache entry using an anchor object with the FinalizationRegistry.
96
+ * When the anchor object is garbage collected, the held value's `entry.proxy_count` is decremented
97
+ * and a deferred eviction check is scheduled for `(id, payload)`.
98
+ *
99
+ * @param {object} anchor
100
+ * @param {CacheEntry<R>} entry
101
+ * @param {string} id
102
+ * @param {string} payload
103
+ */
104
+ ref = (anchor, entry, id, payload) => {
105
+ entry.proxy_count++;
106
+ this.#proxy_finalizer.register(anchor, { entry, id, payload });
107
+ };
108
+
109
+ /**
110
+ * Manually reference this cache entry. Danger: This entry will never be cleaned up unless the returned callback is called.
111
+ *
112
+ * @param {CacheEntry<R>} entry
113
+ * @param {string} id
114
+ * @param {string} payload
115
+ */
116
+ manual_ref = (entry, id, payload) => {
117
+ entry.proxy_count++;
118
+ return once(() => this.deref(entry, id, payload));
119
+ };
120
+
121
+ /**
122
+ * Dereference this cache entry. If the entry's `proxy_count` hits zero, schedule a deferred eviction check.
123
+ *
124
+ * @param {CacheEntry<R>} entry
125
+ * @param {string} id
126
+ * @param {string} payload
127
+ */
128
+ deref = (entry, id, payload) => {
129
+ entry.proxy_count--;
130
+ void tick().then(() => {
131
+ const entry = this.#cache_map.get(id)?.get(payload);
132
+ if (!entry || entry.proxy_count > 0) return;
133
+ this.#evict(id, payload);
134
+ });
135
+ };
136
+
137
+ /**
138
+ * Tear down the cache entry for `(id, payload)` if it exists. Runs the optional
139
+ * resource teardown and the entry's `$effect.root` cleanup, then removes the entry
140
+ * from the cache map.
141
+ *
142
+ * @param {string} id
143
+ * @param {string} payload
144
+ */
145
+ #evict = (id, payload) => {
146
+ const entries = this.#cache_map.get(id);
147
+ const entry = entries?.get(payload);
148
+ if (!entry) return;
149
+
150
+ this.#destroy_resource?.(entry.resource);
151
+ entry.cleanup();
152
+ entries?.delete(payload);
153
+ if (entries && entries.size === 0) {
154
+ this.#cache_map.delete(id);
155
+ }
156
+ };
157
+ }