@sveltejs/kit 2.60.1 → 2.61.1

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 +8 -9
  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 +100 -36
  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 +60 -36
  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 +29 -6
  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 +27 -31
  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
 
@@ -448,21 +431,17 @@ function create_query_resource(__, payload, state, fn) {
448
431
  const value = is_immediate_refresh ? get_promise() : fn();
449
432
  return update_refresh_value(refresh_context, value, is_immediate_refresh);
450
433
  },
451
- run() {
452
- // 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
453
- // 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
454
- // concrete use case.
455
- if (!state.is_in_universal_load) {
456
- throw new Error(
457
- 'On the server, .run() can only be called in universal `load` functions. Anywhere else, just await the query directly'
458
- );
459
- }
460
- return get_response(__, payload, state, fn);
461
- },
462
434
  /** @param {any} value */
463
435
  set(value) {
464
436
  return update_refresh_value(get_refresh_context(__, 'set', payload), value);
465
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
+ },
466
445
  /** @type {Promise<any>['then']} */
467
446
  then(onfulfilled, onrejected) {
468
447
  return get_promise().then(onfulfilled, onrejected);
@@ -480,13 +459,21 @@ function create_query_resource(__, payload, state, fn) {
480
459
  * @param {RemoteQueryLiveInternals} __
481
460
  * @param {string} payload — the stringified raw argument (i.e. the cache key the client will use)
482
461
  * @param {RequestState} state
483
- * @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
484
464
  * @returns {RemoteLiveQuery<any>}
485
465
  */
486
- function create_live_query_resource(__, payload, state, get_first_value) {
466
+ function create_live_query_resource(__, payload, state, signal, get_generator) {
487
467
  /** @type {Promise<any> | null} */
488
468
  let promise = null;
489
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
+
490
477
  const get_promise = () => {
491
478
  return (promise ??= get_response(__, payload, state, get_first_value));
492
479
  };
@@ -540,19 +527,96 @@ function create_live_query_resource(__, payload, state, get_first_value) {
540
527
  reconnects.set(create_remote_key(__.id, payload), get_promise());
541
528
  return Promise.resolve();
542
529
  },
530
+ /** @ts-expect-error This method no longer exists */
543
531
  run() {
544
- 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
+ );
545
535
  },
546
536
  /** @type {Promise<any>['then']} */
547
537
  then(onfulfilled, onrejected) {
548
538
  return get_promise().then(onfulfilled, onrejected);
549
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
+ },
550
550
  get [Symbol.toStringTag]() {
551
551
  return 'LiveQueryResource';
552
552
  }
553
553
  };
554
554
  }
555
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
+
556
620
  // Add batch as a property to the query function
557
621
  Object.defineProperty(query, 'batch', { value: batch, enumerable: true });
558
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
+ }