@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.
- package/package.json +2 -2
- package/src/exports/internal/remote-functions.js +1 -1
- package/src/exports/public.d.ts +107 -16
- package/src/runtime/app/paths/client.js +7 -0
- package/src/runtime/app/paths/server.js +7 -0
- package/src/runtime/app/server/remote/command.js +7 -6
- package/src/runtime/app/server/remote/form.js +2 -1
- package/src/runtime/app/server/remote/prerender.js +3 -4
- package/src/runtime/app/server/remote/query.js +327 -113
- package/src/runtime/app/server/remote/requested.js +127 -32
- package/src/runtime/app/server/remote/shared.js +89 -20
- package/src/runtime/client/client.js +92 -62
- package/src/runtime/client/ndjson.js +42 -0
- package/src/runtime/client/remote-functions/command.svelte.js +8 -3
- package/src/runtime/client/remote-functions/form.svelte.js +24 -6
- package/src/runtime/client/remote-functions/index.js +3 -1
- package/src/runtime/client/remote-functions/query-batch.svelte.js +105 -0
- package/src/runtime/client/remote-functions/query-live.svelte.js +636 -0
- package/src/runtime/client/remote-functions/query.svelte.js +48 -148
- package/src/runtime/client/remote-functions/shared.svelte.js +76 -23
- package/src/runtime/form-utils.js +33 -12
- package/src/runtime/server/page/index.js +26 -16
- package/src/runtime/server/page/load_data.js +4 -2
- package/src/runtime/server/page/render.js +21 -18
- package/src/runtime/server/remote.js +117 -9
- package/src/runtime/server/respond.js +11 -8
- package/src/runtime/server/utils.js +10 -0
- package/src/runtime/shared.js +3 -3
- package/src/runtime/utils.js +0 -1
- package/src/types/internal.d.ts +54 -14
- package/src/utils/page_nodes.js +1 -0
- package/src/utils/url.js +3 -3
- package/src/version.js +1 -1
- package/types/index.d.ts +182 -30
- 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
|
-
//
|
|
39
|
-
// Do this here, after
|
|
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(
|
|
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
|
|
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
|
+
}
|