@sveltejs/kit 2.57.1 → 2.59.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 (35) hide show
  1. package/package.json +2 -2
  2. package/src/exports/internal/remote-functions.js +1 -1
  3. package/src/exports/public.d.ts +107 -16
  4. package/src/runtime/app/paths/client.js +7 -0
  5. package/src/runtime/app/paths/server.js +7 -0
  6. package/src/runtime/app/server/remote/command.js +7 -6
  7. package/src/runtime/app/server/remote/form.js +2 -1
  8. package/src/runtime/app/server/remote/prerender.js +3 -4
  9. package/src/runtime/app/server/remote/query.js +327 -113
  10. package/src/runtime/app/server/remote/requested.js +127 -32
  11. package/src/runtime/app/server/remote/shared.js +89 -20
  12. package/src/runtime/client/client.js +92 -62
  13. package/src/runtime/client/ndjson.js +42 -0
  14. package/src/runtime/client/remote-functions/command.svelte.js +8 -3
  15. package/src/runtime/client/remote-functions/form.svelte.js +24 -6
  16. package/src/runtime/client/remote-functions/index.js +3 -1
  17. package/src/runtime/client/remote-functions/query-batch.svelte.js +105 -0
  18. package/src/runtime/client/remote-functions/query-live.svelte.js +636 -0
  19. package/src/runtime/client/remote-functions/query.svelte.js +48 -148
  20. package/src/runtime/client/remote-functions/shared.svelte.js +76 -23
  21. package/src/runtime/form-utils.js +33 -12
  22. package/src/runtime/server/page/index.js +26 -16
  23. package/src/runtime/server/page/load_data.js +4 -2
  24. package/src/runtime/server/page/render.js +21 -18
  25. package/src/runtime/server/remote.js +117 -9
  26. package/src/runtime/server/respond.js +11 -8
  27. package/src/runtime/server/utils.js +10 -0
  28. package/src/runtime/shared.js +3 -3
  29. package/src/runtime/utils.js +0 -1
  30. package/src/types/internal.d.ts +54 -14
  31. package/src/utils/page_nodes.js +1 -0
  32. package/src/utils/url.js +3 -3
  33. package/src/version.js +1 -1
  34. package/types/index.d.ts +182 -30
  35. package/types/index.d.ts.map +8 -4
@@ -1,53 +1,133 @@
1
- /** @import { RemoteQueryFunction, RequestedResult } from '@sveltejs/kit' */
2
- /** @import { MaybePromise, RemoteQueryInternals } from 'types' */
1
+ /** @import { RemoteLiveQuery, RemoteLiveQueryFunction, RemoteQuery, RemoteQueryFunction, RequestedResult, QueryRequestedResult, LiveQueryRequestedResult } from '@sveltejs/kit' */
2
+ /** @import { MaybePromise, RemoteAnyQueryInternals } from 'types' */
3
3
  import { get_request_store } from '@sveltejs/kit/internal/server';
4
4
  import { create_remote_key, parse_remote_arg } from '../../../shared.js';
5
5
  import { noop } from '../../../../utils/functions.js';
6
- import { mark_argument_validated } from './query.js';
7
6
 
8
7
  /**
9
8
  * In the context of a remote `command` or `form` request, returns an iterable
10
- * of the client-requested refreshes' validated arguments up to the supplied limit.
11
- * Arguments that fail validation or exceed the limit are recorded as failures in
9
+ * of `{ arg, query }` entries for the refreshes requested by the client, up to
10
+ * the supplied `limit`. Each `query` is a `RemoteQuery` bound to the original
11
+ * client-side cache key, so `refresh()` / `set()` propagate correctly even when
12
+ * the query's schema transforms the input. `arg` is the *validated* argument,
13
+ * i.e. the value after the schema has run (so `InferOutput<Schema>` for queries
14
+ * declared with a Standard Schema).
15
+ *
16
+ * Arguments that fail validation or exceed `limit` are recorded as failures in
12
17
  * the response to the client.
13
18
  *
14
19
  * @example
15
20
  * ```ts
16
21
  * import { requested } from '$app/server';
17
22
  *
18
- * for (const arg of requested(getPost, 5)) {
19
- * // it's safe to throw away this promise -- SvelteKit
20
- * // will await it for us and handle any errors by sending
21
- * // them to the client.
22
- * void getPost(arg).refresh();
23
+ * for (const { arg, query } of requested(getPost, 5)) {
24
+ * // `arg` is the validated argument; `query` is bound to the client's
25
+ * // cache key. It's safe to throw away this promise -- SvelteKit will
26
+ * // await it and forward any errors to the client.
27
+ * void query.refresh();
23
28
  * }
24
29
  * ```
25
30
  *
26
31
  * As a shorthand for the above, you can also call `refreshAll` on the result:
27
32
  *
33
+ * @example
28
34
  * ```ts
29
35
  * import { requested } from '$app/server';
30
36
  *
31
37
  * await requested(getPost, 5).refreshAll();
32
38
  * ```
33
39
  *
40
+ * Works with `query.batch` as well — refreshes for individual entries are
41
+ * collected into a single batched call.
42
+ *
43
+ * For live queries, the same applies, but with `reconnect` and `reconnectAll`.
44
+ *
45
+ * @template Input
46
+ * @template Output
47
+ * @template [Validated=Input]
48
+ * @overload
49
+ * @param {RemoteQueryFunction<Input, Output, Validated>} query
50
+ * @param {number} limit
51
+ * @returns {QueryRequestedResult<Validated, Output>}
52
+ */
53
+ /**
54
+ * In the context of a remote `command` or `form` request, returns an iterable
55
+ * of `{ arg, query }` entries for the reconnects requested by the client, up to
56
+ * the supplied `limit`. Each `query` is a `RemoteLiveQuery` bound to the original
57
+ * client-side cache key, so `reconnect()` propagates correctly even when
58
+ * the query's schema transforms the input. `arg` is the *validated* argument.
59
+ *
60
+ * Arguments that fail validation or exceed `limit` are recorded as failures in
61
+ * the response to the client.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { requested } from '$app/server';
66
+ *
67
+ * for (const { query } of requested(getPost, 5)) {
68
+ * void query.reconnect();
69
+ * }
70
+ * ```
71
+ *
72
+ * As a shorthand, you can also call `reconnectAll` on the result:
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { requested } from '$app/server';
77
+ *
78
+ * await requested(getPost, 5).reconnectAll();
79
+ * ```
80
+ *
81
+ * @template Input
82
+ * @template Output
83
+ * @template [Validated=Input]
84
+ * @overload
85
+ * @param {RemoteLiveQueryFunction<Input, Output, Validated>} query
86
+ * @param {number} limit
87
+ * @returns {LiveQueryRequestedResult<Validated, Output>}
88
+ */
89
+ /**
34
90
  * @template Input
35
91
  * @template Output
36
- * @param {RemoteQueryFunction<Input, Output>} query
37
- * @param {number} [limit=Infinity]
38
- * @returns {RequestedResult<Input>}
92
+ * @template [Validated=Input]
93
+ * @param {RemoteQueryFunction<Input, Output, Validated> | RemoteLiveQueryFunction<Input, Output, Validated>} query
94
+ * @param {number} limit
95
+ * @returns {RequestedResult<Validated, Output>}
39
96
  */
40
- export function requested(query, limit = Infinity) {
97
+ export function requested(query, limit) {
41
98
  const { state } = get_request_store();
42
- const internals = /** @type {RemoteQueryInternals | undefined} */ (/** @type {any} */ (query).__);
99
+ const internals = /** @type {RemoteAnyQueryInternals | undefined} */ (
100
+ /** @type {any} */ (query).__
101
+ );
43
102
 
44
- if (!internals || internals.type !== 'query') {
45
- throw new Error('requested(...) expects a query function created with query(...)');
103
+ if (
104
+ internals?.type !== 'query' &&
105
+ internals?.type !== 'query_batch' &&
106
+ internals?.type !== 'query_live'
107
+ ) {
108
+ throw new Error(
109
+ 'requested(...) expects a query function created with query(...), query.batch(...), or query.live(...)'
110
+ );
46
111
  }
47
112
 
113
+ // narrow-stable alias so generator closures below don't lose the narrowing
114
+ const __ = internals;
115
+
48
116
  const requested = state.remote.requested;
49
- const payloads = requested?.get(internals.id) ?? [];
50
- const refreshes = (state.remote.refreshes ??= {});
117
+ const payloads = requested?.get(__.id) ?? [];
118
+ // note: don't initialize these maps here -- they will be initialized by the
119
+ // command/form wrapper when we enter them, and if we initialize them here
120
+ // we will enable requested(...) in contexts where it shouldn't be allowed,
121
+ // such as load functions or other server functions
122
+ const refreshes = state.remote.refreshes;
123
+ const reconnects = state.remote.reconnects;
124
+ const store = __.type === 'query_live' ? reconnects : refreshes;
125
+
126
+ if (!store) {
127
+ throw new Error(
128
+ 'requested(...) can only be called in the context of a command/form remote function'
129
+ );
130
+ }
51
131
  const [selected, skipped] = split_limit(payloads, limit);
52
132
 
53
133
  /**
@@ -58,34 +138,34 @@ export function requested(query, limit = Infinity) {
58
138
  const promise = Promise.reject(error);
59
139
  promise.catch(noop);
60
140
 
61
- const key = create_remote_key(internals.id, payload);
62
- refreshes[key] = promise;
141
+ const key = create_remote_key(__.id, payload);
142
+ store.set(key, promise);
63
143
  };
64
144
 
65
145
  for (const payload of skipped) {
66
146
  record_failure(
67
147
  payload,
68
148
  new Error(
69
- `Requested refresh was rejected because it exceeded requested(${internals.name}, ${limit}) limit`
149
+ `Requested refresh was rejected because it exceeded requested(${__.name}, ${limit}) limit`
70
150
  )
71
151
  );
72
152
  }
73
153
 
74
- return {
154
+ const result = {
75
155
  *[Symbol.iterator]() {
76
156
  for (const payload of selected) {
77
157
  try {
78
158
  const parsed = parse_remote_arg(payload, state.transport);
79
- const validated = internals.validate(parsed);
159
+ const validated = __.validate(parsed);
80
160
 
81
161
  if (is_thenable(validated)) {
82
162
  throw new Error(
83
163
  // TODO improve
84
- `requested(${internals.name}, ${limit}) cannot be used with synchronous iteration because the query validator is async. Use \`for await ... of\` instead`
164
+ `requested(${__.name}, ${limit}) cannot be used with synchronous iteration because the query validator is async. Use \`for await ... of\` instead`
85
165
  );
86
166
  }
87
167
 
88
- yield mark_argument_validated(internals, state, validated);
168
+ yield { arg: validated, query: __.bind(payload, validated) };
89
169
  } catch (error) {
90
170
  record_failure(payload, error);
91
171
  continue;
@@ -96,20 +176,35 @@ export function requested(query, limit = Infinity) {
96
176
  yield* race_all(selected, async (payload) => {
97
177
  try {
98
178
  const parsed = parse_remote_arg(payload, state.transport);
99
- const validated = await internals.validate(parsed);
100
- return mark_argument_validated(internals, state, validated);
179
+ const validated = await __.validate(parsed);
180
+ return { arg: validated, query: __.bind(payload, validated) };
101
181
  } catch (error) {
102
182
  record_failure(payload, error);
103
- throw new Error(`Skipping ${internals.name}(${payload})`, { cause: error });
183
+ throw new Error(`Skipping ${__.name}(${payload})`, { cause: error });
104
184
  }
105
185
  });
106
186
  },
107
187
  async refreshAll() {
108
- for await (const arg of this) {
109
- void query(arg).refresh();
188
+ if (__.type === 'query_live') {
189
+ throw new Error('refreshAll() is invalid for live queries. Use reconnectAll() instead.');
190
+ }
191
+
192
+ for await (const { query } of result) {
193
+ void (/** @type {RemoteQuery<Output>} */ (query).refresh());
194
+ }
195
+ },
196
+ async reconnectAll() {
197
+ if (__.type !== 'query_live') {
198
+ throw new Error('reconnectAll() is invalid for regular queries. Use refreshAll() instead.');
199
+ }
200
+
201
+ for await (const { query } of result) {
202
+ void (/** @type {RemoteLiveQuery<Output>} */ (query).reconnect());
110
203
  }
111
204
  }
112
205
  };
206
+
207
+ return /** @type {RequestedResult<Validated, Output>} */ (/** @type {unknown} */ (result));
113
208
  }
114
209
 
115
210
  /**
@@ -157,7 +252,7 @@ async function* race_all(array, fn) {
157
252
  value: result
158
253
  }));
159
254
 
160
- promise.catch(noop);
255
+ promise.catch(() => pending.delete(promise));
161
256
  pending.add(promise);
162
257
  }
163
258
 
@@ -1,19 +1,14 @@
1
1
  /** @import { RequestEvent } from '@sveltejs/kit' */
2
- /** @import { ServerHooks, MaybePromise, RequestState, RemoteInternals, RequestStore } from 'types' */
2
+ /** @import { ServerHooks, MaybePromise, RequestState, RemoteInternals, RequestStore, RemoteLiveQueryUserFunctionReturnType } from 'types' */
3
3
  import { parse } from 'devalue';
4
4
  import { error } from '@sveltejs/kit';
5
5
  import { with_request_store, get_request_store } from '@sveltejs/kit/internal/server';
6
6
  import { noop } from '../../../../utils/functions.js';
7
- import {
8
- stringify_remote_arg,
9
- create_remote_key,
10
- stringify,
11
- unfriendly_hydratable
12
- } from '../../../shared.js';
7
+ import { create_remote_key, stringify, unfriendly_hydratable } from '../../../shared.js';
13
8
 
14
9
  /**
15
10
  * @param {any} validate_or_fn
16
- * @param {(arg?: any) => any} [maybe_fn]
11
+ * @param {((arg?: any) => any) | undefined} [maybe_fn]
17
12
  * @returns {(arg?: any) => MaybePromise<any>}
18
13
  */
19
14
  export function create_validator(validate_or_fn, maybe_fn) {
@@ -36,6 +31,7 @@ export function create_validator(validate_or_fn, maybe_fn) {
36
31
  return async (arg) => {
37
32
  // Get event before async validation to ensure it's available in server environments without AsyncLocalStorage, too
38
33
  const { event, state } = get_request_store();
34
+
39
35
  // access property and call method in one go to preserve potential this context
40
36
  const result = await validate_or_fn['~standard'].validate(arg);
41
37
 
@@ -68,19 +64,18 @@ export function create_validator(validate_or_fn, maybe_fn) {
68
64
  *
69
65
  * @template {MaybePromise<any>} T
70
66
  * @param {RemoteInternals} internals
71
- * @param {any} arg
67
+ * @param {string} payload — the stringified raw argument (i.e. the cache key the client will use)
72
68
  * @param {RequestState} state
73
69
  * @param {() => Promise<T>} get_result
74
70
  * @returns {Promise<T>}
75
71
  */
76
- export async function get_response(internals, arg, state, get_result) {
72
+ export async function get_response(internals, payload, state, get_result) {
77
73
  // wait a beat, in case `myQuery().set(...)` or `myQuery().refresh()` is immediately called
78
74
  // eslint-disable-next-line @typescript-eslint/await-thenable
79
75
  await 0;
80
76
 
81
77
  const cache = get_cache(internals, state);
82
- const key = stringify_remote_arg(arg, state.transport);
83
- const entry = (cache[key] ??= {
78
+ const entry = (cache[payload] ??= {
84
79
  serialize: false,
85
80
  data: get_result()
86
81
  });
@@ -88,7 +83,7 @@ export async function get_response(internals, arg, state, get_result) {
88
83
  entry.serialize ||= !!state.is_in_universal_load;
89
84
 
90
85
  if (state.is_in_render && internals.id) {
91
- const remote_key = create_remote_key(internals.id, key);
86
+ const remote_key = create_remote_key(internals.id, payload);
92
87
 
93
88
  Promise.resolve(entry.data)
94
89
  .then((value) => {
@@ -115,17 +110,13 @@ export function parse_remote_response(data, transport) {
115
110
  }
116
111
 
117
112
  /**
118
- * Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`.
119
- * @template T
120
113
  * @param {RequestEvent} event
121
114
  * @param {RequestState} state
122
115
  * @param {boolean} allow_cookies
123
- * @param {() => any} get_input
124
- * @param {(arg?: any) => T} fn
116
+ * @returns {RequestStore}
125
117
  */
126
- export async function run_remote_function(event, state, allow_cookies, get_input, fn) {
127
- /** @type {RequestStore} */
128
- const store = {
118
+ function derive_remote_function_event(event, state, allow_cookies) {
119
+ return {
129
120
  event: {
130
121
  ...event,
131
122
  setHeaders: () => {
@@ -162,12 +153,90 @@ export async function run_remote_function(event, state, allow_cookies, get_input
162
153
  is_in_remote_function: true
163
154
  }
164
155
  };
156
+ }
157
+
158
+ /**
159
+ * Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`.
160
+ * @template T
161
+ * @param {RequestEvent} event
162
+ * @param {RequestState} state
163
+ * @param {boolean} allow_cookies
164
+ * @param {() => any} get_input
165
+ * @param {(arg?: any) => T} fn
166
+ */
167
+ export async function run_remote_function(event, state, allow_cookies, get_input, fn) {
168
+ const store = derive_remote_function_event(event, state, allow_cookies);
165
169
 
166
170
  // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function
167
171
  const input = await with_request_store(store, get_input);
168
172
  return with_request_store(store, () => fn(input));
169
173
  }
170
174
 
175
+ /**
176
+ * Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`.
177
+ * @template T
178
+ * @param {RequestEvent} event
179
+ * @param {RequestState} state
180
+ * @param {boolean} allow_cookies
181
+ * @param {() => any} get_input
182
+ * @param {(arg?: any) => RemoteLiveQueryUserFunctionReturnType<T>} fn
183
+ * @param {string} name
184
+ */
185
+ export async function* run_remote_generator(event, state, allow_cookies, get_input, fn, name) {
186
+ const store = derive_remote_function_event(event, state, allow_cookies);
187
+
188
+ // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function / calls to next
189
+ const input = await with_request_store(store, get_input);
190
+ const source = await with_request_store(store, () => fn(input));
191
+ const iterator = to_iterator(source, name);
192
+ let done = false;
193
+
194
+ try {
195
+ while (true) {
196
+ // the code of a generator function is basically chopped apart at each
197
+ // yield, and each part is an invocation of `.next`. So, to provide
198
+ // access to the request context in generator functions, we have to
199
+ // provide it to every invocation of `.next`. (It's more obvious that
200
+ // this is necessary with plain iterators.)
201
+ const result = await with_request_store(store, () => iterator.next());
202
+ if (result.done) {
203
+ done = true;
204
+ return result.value;
205
+ }
206
+ yield result.value;
207
+ }
208
+ } finally {
209
+ if (!done && typeof iterator.return === 'function') {
210
+ await with_request_store(store, () => iterator.return?.(undefined));
211
+ }
212
+ }
213
+ }
214
+
215
+ /**
216
+ * @template T
217
+ * @param {Awaited<RemoteLiveQueryUserFunctionReturnType<T>>} source
218
+ * @param {string} name
219
+ * @returns {Iterator<T> | AsyncIterator<T>}
220
+ */
221
+ function to_iterator(source, name) {
222
+ // intentionally using `in` because these could be inherited
223
+ if ('next' in source && typeof source.next === 'function') {
224
+ return source;
225
+ }
226
+
227
+ if (Symbol.asyncIterator in source && typeof source[Symbol.asyncIterator] === 'function') {
228
+ return source[Symbol.asyncIterator]();
229
+ }
230
+
231
+ if (Symbol.iterator in source && typeof source[Symbol.iterator] === 'function') {
232
+ return source[Symbol.iterator]();
233
+ }
234
+
235
+ throw new Error(
236
+ `query.live '${name}' must return an Iterator, Iterable, AsyncIterator or AsyncIterable`
237
+ );
238
+ }
239
+
171
240
  /**
172
241
  * @param {RemoteInternals} internals
173
242
  * @param {RequestState} state
@@ -1,4 +1,5 @@
1
1
  /** @import { RemoteQueryCacheEntry } from './remote-functions/query.svelte.js' */
2
+ /** @import { RemoteLiveQueryCacheEntry } from './remote-functions/query-live.svelte.js' */
2
3
  import { BROWSER, DEV } from 'esm-env';
3
4
  import * as svelte from 'svelte';
4
5
  import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
@@ -44,6 +45,7 @@ import { compact } from '../../utils/array.js';
44
45
  import {
45
46
  INVALIDATED_PARAM,
46
47
  TRAILING_SLASH_PARAM,
48
+ create_remote_key,
47
49
  validate_depends,
48
50
  validate_load_response
49
51
  } from '../shared.js';
@@ -52,7 +54,7 @@ import { writable } from 'svelte/store';
52
54
  import { page, update, navigating } from './state.svelte.js';
53
55
  import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
54
56
  import { noop_span } from '../telemetry/noop.js';
55
- import { text_decoder } from '../utils.js';
57
+ import { read_ndjson } from './ndjson.js';
56
58
 
57
59
  export { load_css };
58
60
  const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
@@ -305,6 +307,12 @@ export let pending_invalidate;
305
307
  */
306
308
  export const query_map = new Map();
307
309
 
310
+ /**
311
+ * @type {Map<string, Map<string, RemoteLiveQueryCacheEntry<any>>>}
312
+ * A map of id -> payload -> live query internals for all active queries.
313
+ */
314
+ export const live_query_map = new Map();
315
+
308
316
  /**
309
317
  * @param {import('./types.js').SvelteKitApp} _app
310
318
  * @param {HTMLElement} _target
@@ -409,12 +417,23 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
409
417
  discard_load_cache();
410
418
 
411
419
  // Rerun queries
420
+ /** @type {Map<string, Promise<void>>} */
421
+ const live_query_reconnects = new Map();
412
422
  if (force_invalidation) {
413
- query_map.forEach((entries) => {
414
- entries.forEach(({ resource }) => {
415
- void resource.refresh?.();
416
- });
417
- });
423
+ for (const entries of query_map.values()) {
424
+ for (const { resource } of entries.values()) {
425
+ void resource.refresh();
426
+ }
427
+ }
428
+
429
+ for (const [query_id, entries] of live_query_map) {
430
+ for (const [payload, { resource }] of entries) {
431
+ const key = create_remote_key(query_id, payload);
432
+ const promise = resource.reconnect();
433
+ promise.catch(noop);
434
+ live_query_reconnects.set(key, promise);
435
+ }
436
+ }
418
437
  }
419
438
 
420
439
  if (include_load_functions) {
@@ -443,12 +462,26 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
443
462
  reset_invalidation();
444
463
  }
445
464
 
465
+ // only wait for promises that are connected to queries that still exist
466
+ /** @type {Promise<any>[]} */
467
+ const promises = [];
468
+ for (const entries of query_map.values()) {
469
+ for (const { resource } of entries.values()) {
470
+ promises.push(resource);
471
+ }
472
+ }
473
+ for (const [query_id, entries] of live_query_map) {
474
+ for (const payload of entries.keys()) {
475
+ const key = create_remote_key(query_id, payload);
476
+ const promise = live_query_reconnects.get(key);
477
+ if (promise) {
478
+ promises.push(promise);
479
+ }
480
+ }
481
+ }
482
+
446
483
  // Don't use allSettled yet because it's too new
447
- await Promise.all(
448
- [...query_map.values()].flatMap((entries) =>
449
- [...entries.values()].map(({ resource }) => resource)
450
- )
451
- ).catch(noop);
484
+ await Promise.all(promises).catch(noop);
452
485
  }
453
486
 
454
487
  function reset_invalidation() {
@@ -485,8 +518,10 @@ function persist_state() {
485
518
  * @param {{}} [nav_token]
486
519
  */
487
520
  export async function _goto(url, options, redirect_count, nav_token) {
488
- /** @type {string[]} */
521
+ /** @type {Set<string>} */
489
522
  let query_keys;
523
+ /** @type {Set<string>} */
524
+ let live_query_keys;
490
525
 
491
526
  // Clear preload cache when invalidateAll is true to ensure fresh data
492
527
  // after form submissions or explicit invalidations
@@ -506,12 +541,18 @@ export async function _goto(url, options, redirect_count, nav_token) {
506
541
  accept: () => {
507
542
  if (options.invalidateAll) {
508
543
  force_invalidation = true;
509
- query_keys = [];
510
- query_map.forEach((entries, id) => {
544
+ query_keys = new Set();
545
+ for (const [id, entries] of query_map) {
511
546
  for (const payload of entries.keys()) {
512
- query_keys.push(id + '/' + payload);
547
+ query_keys.add(create_remote_key(id, payload));
513
548
  }
514
- });
549
+ }
550
+ live_query_keys = new Set();
551
+ for (const [id, entries] of live_query_map) {
552
+ for (const payload of entries.keys()) {
553
+ live_query_keys.add(create_remote_key(id, payload));
554
+ }
555
+ }
515
556
  }
516
557
 
517
558
  if (options.invalidate) {
@@ -527,13 +568,20 @@ export async function _goto(url, options, redirect_count, nav_token) {
527
568
  .tick()
528
569
  .then(svelte.tick)
529
570
  .then(() => {
530
- query_map.forEach((entries, id) => {
531
- entries.forEach(({ resource }, payload) => {
532
- if (query_keys?.includes(id + '/' + payload)) {
533
- void resource.refresh?.();
571
+ for (const [id, entries] of query_map) {
572
+ for (const [payload, { resource }] of entries) {
573
+ if (query_keys?.has(create_remote_key(id, payload))) {
574
+ void resource.refresh();
534
575
  }
535
- });
536
- });
576
+ }
577
+ }
578
+ for (const [id, entries] of live_query_map) {
579
+ for (const [payload, { resource }] of entries) {
580
+ if (live_query_keys?.has(create_remote_key(id, payload))) {
581
+ void resource.reconnect();
582
+ }
583
+ }
584
+ }
537
585
  });
538
586
  }
539
587
  }
@@ -3035,49 +3083,31 @@ async function load_data(url, invalid) {
3035
3083
  });
3036
3084
  }
3037
3085
 
3038
- let text = '';
3039
-
3040
- while (true) {
3041
- // Format follows ndjson (each line is a JSON object) or regular JSON spec
3042
- const { done, value } = await reader.read();
3043
- if (done && !text) break;
3044
-
3045
- text += !value && text ? '\n' : text_decoder.decode(value, { stream: true }); // no value -> final chunk -> add a new line to trigger the last parse
3046
-
3047
- while (true) {
3048
- const split = text.indexOf('\n');
3049
- if (split === -1) {
3050
- break;
3051
- }
3052
-
3053
- const node = JSON.parse(text.slice(0, split));
3054
- text = text.slice(split + 1);
3086
+ for await (const node of read_ndjson(reader)) {
3087
+ if (node.type === 'redirect') {
3088
+ return resolve(node);
3089
+ }
3055
3090
 
3056
- if (node.type === 'redirect') {
3057
- return resolve(node);
3058
- }
3091
+ if (node.type === 'data') {
3092
+ // This is the first (and possibly only, if no pending promises) chunk
3093
+ node.nodes?.forEach((/** @type {any} */ node) => {
3094
+ if (node?.type === 'data') {
3095
+ node.uses = deserialize_uses(node.uses);
3096
+ node.data = deserialize(node.data);
3097
+ }
3098
+ });
3059
3099
 
3060
- if (node.type === 'data') {
3061
- // This is the first (and possibly only, if no pending promises) chunk
3062
- node.nodes?.forEach((/** @type {any} */ node) => {
3063
- if (node?.type === 'data') {
3064
- node.uses = deserialize_uses(node.uses);
3065
- node.data = deserialize(node.data);
3066
- }
3067
- });
3100
+ resolve(node);
3101
+ } else if (node.type === 'chunk') {
3102
+ // This is a subsequent chunk containing deferred data
3103
+ const { id, data, error } = node;
3104
+ const deferred = /** @type {import('types').Deferred} */ (deferreds.get(id));
3105
+ deferreds.delete(id);
3068
3106
 
3069
- resolve(node);
3070
- } else if (node.type === 'chunk') {
3071
- // This is a subsequent chunk containing deferred data
3072
- const { id, data, error } = node;
3073
- const deferred = /** @type {import('types').Deferred} */ (deferreds.get(id));
3074
- deferreds.delete(id);
3075
-
3076
- if (error) {
3077
- deferred.reject(deserialize(error));
3078
- } else {
3079
- deferred.fulfil(deserialize(data));
3080
- }
3107
+ if (error) {
3108
+ deferred.reject(deserialize(error));
3109
+ } else {
3110
+ deferred.fulfil(deserialize(data));
3081
3111
  }
3082
3112
  }
3083
3113
  }