@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
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Yields parsed JSON objects from a ReadableStream of newline-delimited JSON.
3
+ * Each yielded value is the raw `JSON.parse`'d object — callers handle deserialization.
4
+ * @param {ReadableStreamDefaultReader<Uint8Array>} reader
5
+ */
6
+ export async function* read_ndjson(reader) {
7
+ let done = false;
8
+ let buffer = '';
9
+ const decoder = new TextDecoder();
10
+
11
+ while (true) {
12
+ let split = buffer.indexOf('\n');
13
+ while (split !== -1) {
14
+ const line = buffer.slice(0, split).trim();
15
+ buffer = buffer.slice(split + 1);
16
+
17
+ if (line) {
18
+ yield JSON.parse(line);
19
+ }
20
+
21
+ split = buffer.indexOf('\n');
22
+ }
23
+
24
+ if (done) {
25
+ const line = buffer.trim();
26
+ if (line) {
27
+ yield JSON.parse(line);
28
+ }
29
+ return;
30
+ }
31
+
32
+ const chunk = await reader.read();
33
+ done = chunk.done;
34
+ if (chunk.value) {
35
+ buffer += decoder.decode(chunk.value, { stream: true });
36
+ }
37
+
38
+ if (done) {
39
+ buffer += decoder.decode();
40
+ }
41
+ }
42
+ }
@@ -8,7 +8,8 @@ import { stringify_remote_arg } from '../../shared.js';
8
8
  import {
9
9
  get_remote_request_headers,
10
10
  apply_refreshes,
11
- categorize_updates
11
+ categorize_updates,
12
+ apply_reconnections
12
13
  } from './shared.svelte.js';
13
14
 
14
15
  /**
@@ -35,8 +36,8 @@ export function command(id) {
35
36
  // Increment pending count when command starts
36
37
  pending_count++;
37
38
 
38
- // Noone should call commands during rendering but belts and braces.
39
- // Do this here, after await Svelte' reactivity context is gone.
39
+ // No one should call commands during rendering, but this is belt and braces.
40
+ // Do this here, after Svelte's reactivity context is gone.
40
41
  const headers = {
41
42
  'Content-Type': 'application/json',
42
43
  ...get_remote_request_headers()
@@ -79,6 +80,10 @@ export function command(id) {
79
80
  apply_refreshes(result.refreshes);
80
81
  }
81
82
 
83
+ if (result.reconnects) {
84
+ apply_reconnections(result.reconnects);
85
+ }
86
+
82
87
  return devalue.parse(result.result, app.decoders);
83
88
  }
84
89
  } finally {
@@ -7,7 +7,7 @@ import { DEV } from 'esm-env';
7
7
  import { HttpError } from '@sveltejs/kit/internal';
8
8
  import { app, query_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
9
9
  import { tick } from 'svelte';
10
- import { apply_refreshes, categorize_updates } from './shared.svelte.js';
10
+ import { apply_refreshes, categorize_updates, apply_reconnections } from './shared.svelte.js';
11
11
  import { createAttachmentKey } from 'svelte/attachments';
12
12
  import {
13
13
  convert_formdata,
@@ -156,7 +156,6 @@ export function form(id) {
156
156
  }
157
157
 
158
158
  try {
159
- // eslint-disable-next-line @typescript-eslint/await-thenable -- `callback` is typed as returning `void` to allow returning e.g. `Promise<boolean>`
160
159
  await callback({
161
160
  form,
162
161
  data,
@@ -228,27 +227,46 @@ export function form(id) {
228
227
 
229
228
  // reset issues in case it's a redirect or error (but issues passed in that case)
230
229
  raw_issues = [];
230
+ result = undefined;
231
231
 
232
232
  if (form_result.type === 'result') {
233
233
  ({ issues: raw_issues = [], result } = devalue.parse(form_result.result, app.decoders));
234
234
  const succeeded = raw_issues.length === 0;
235
235
 
236
236
  if (succeeded) {
237
- if (form_result.refreshes) {
238
- apply_refreshes(form_result.refreshes);
239
- } else {
237
+ if (refreshes === null && !form_result.refreshes && !form_result.reconnects) {
240
238
  void invalidateAll();
239
+ } else {
240
+ if (form_result.refreshes) {
241
+ apply_refreshes(form_result.refreshes);
242
+ }
243
+ if (form_result.reconnects) {
244
+ apply_reconnections(form_result.reconnects);
245
+ }
241
246
  }
242
247
  }
243
248
 
244
249
  return succeeded;
245
250
  } else if (form_result.type === 'redirect') {
246
251
  const stringified_refreshes = form_result.refreshes ?? '';
252
+ const stringified_reconnects = form_result.reconnects ?? '';
247
253
  if (stringified_refreshes) {
248
254
  apply_refreshes(stringified_refreshes);
249
255
  }
256
+
257
+ if (stringified_reconnects) {
258
+ apply_reconnections(stringified_reconnects);
259
+ }
260
+
250
261
  // Use internal version to allow redirects to external URLs
251
- void _goto(form_result.location, { invalidateAll: !stringified_refreshes }, 0);
262
+ void _goto(
263
+ form_result.location,
264
+ {
265
+ invalidateAll:
266
+ refreshes === null && !stringified_refreshes && !stringified_reconnects
267
+ },
268
+ 0
269
+ );
252
270
  return true;
253
271
  } else {
254
272
  throw new HttpError(form_result.status ?? 500, form_result.error);
@@ -1,4 +1,6 @@
1
1
  export { command } from './command.svelte.js';
2
2
  export { form } from './form.svelte.js';
3
3
  export { prerender } from './prerender.svelte.js';
4
- export { query, query_batch } from './query.svelte.js';
4
+ export { query } from './query.svelte.js';
5
+ export { query_batch } from './query-batch.svelte.js';
6
+ export { query_live } from './query-live.svelte.js';
@@ -0,0 +1,105 @@
1
+ /** @import { RemoteQueryFunction } from '@sveltejs/kit' */
2
+ /** @import { RemoteFunctionResponse } from 'types' */
3
+ import { app_dir, base } from '$app/paths/internal/client';
4
+ import { app, goto } from '../client.js';
5
+ import { get_remote_request_headers, QUERY_FUNCTION_ID } from './shared.svelte.js';
6
+ import { QueryProxy } from './query.svelte.js';
7
+ import * as devalue from 'devalue';
8
+ import { HttpError, Redirect } from '@sveltejs/kit/internal';
9
+ import { unfriendly_hydratable } from '../../shared.js';
10
+
11
+ /**
12
+ * @param {string} id
13
+ * @returns {RemoteQueryFunction<any, any>}
14
+ */
15
+ export function query_batch(id) {
16
+ /** @type {Map<string, Array<{resolve: (value: any) => void, reject: (error: any) => void}>>} */
17
+ let batching = new Map();
18
+
19
+ /** @type {RemoteQueryFunction<any, any>} */
20
+ const wrapper = (arg) => {
21
+ return new QueryProxy(id, arg, async (key, payload) => {
22
+ const serialized = await unfriendly_hydratable(key, () => {
23
+ return new Promise((resolve, reject) => {
24
+ // create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
25
+ // is invoked multiple times with the same payload, so we need to deduplicate here
26
+ const entry = batching.get(payload) ?? [];
27
+ entry.push({ resolve, reject });
28
+ batching.set(payload, entry);
29
+
30
+ if (batching.size > 1) return;
31
+
32
+ // Do this here, after await Svelte' reactivity context is gone.
33
+ // TODO is it possible to have batches of the same key
34
+ // but in different forks/async contexts and in the same macrotask?
35
+ // If so this would potentially be buggy
36
+ const headers = {
37
+ 'Content-Type': 'application/json',
38
+ ...get_remote_request_headers()
39
+ };
40
+
41
+ // Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
42
+ // and flushes could reveal more queries that should be batched.
43
+ setTimeout(async () => {
44
+ const batched = batching;
45
+ batching = new Map();
46
+
47
+ try {
48
+ const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
49
+ method: 'POST',
50
+ body: JSON.stringify({
51
+ payloads: Array.from(batched.keys())
52
+ }),
53
+ headers
54
+ });
55
+
56
+ if (!response.ok) {
57
+ throw new Error('Failed to execute batch query');
58
+ }
59
+
60
+ const result = /** @type {RemoteFunctionResponse} */ (await response.json());
61
+ if (result.type === 'error') {
62
+ throw new HttpError(result.status ?? 500, result.error);
63
+ }
64
+
65
+ if (result.type === 'redirect') {
66
+ await goto(result.location);
67
+ throw new Redirect(307, result.location);
68
+ }
69
+
70
+ const results = devalue.parse(result.result, app.decoders);
71
+
72
+ // Resolve individual queries
73
+ // Maps guarantee insertion order so we can do it like this
74
+ let i = 0;
75
+
76
+ for (const resolvers of batched.values()) {
77
+ for (const { resolve, reject } of resolvers) {
78
+ if (results[i].type === 'error') {
79
+ reject(new HttpError(results[i].status, results[i].error));
80
+ } else {
81
+ resolve(results[i].data);
82
+ }
83
+ }
84
+ i++;
85
+ }
86
+ } catch (error) {
87
+ // Reject all queries in the batch
88
+ for (const resolver of batched.values()) {
89
+ for (const { reject } of resolver) {
90
+ reject(error);
91
+ }
92
+ }
93
+ }
94
+ }, 0);
95
+ });
96
+ });
97
+
98
+ return devalue.parse(serialized, app.decoders);
99
+ });
100
+ };
101
+
102
+ Object.defineProperty(wrapper, QUERY_FUNCTION_ID, { value: id });
103
+
104
+ return wrapper;
105
+ }