@sveltejs/kit 2.58.0 → 2.59.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.
- package/package.json +1 -1
- package/src/exports/internal/remote-functions.js +1 -1
- package/src/exports/public.d.ts +77 -10
- package/src/exports/vite/dev/index.js +1 -1
- 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/query.js +304 -75
- package/src/runtime/app/server/remote/requested.js +109 -20
- package/src/runtime/app/server/remote/shared.js +84 -9
- 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 +6 -1
- package/src/runtime/client/remote-functions/form.svelte.js +23 -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 +15 -115
- package/src/runtime/client/remote-functions/shared.svelte.js +76 -23
- package/src/runtime/form-utils.js +31 -10
- package/src/runtime/server/page/load_data.js +4 -2
- package/src/runtime/server/page/render.js +4 -1
- package/src/runtime/server/remote.js +117 -9
- package/src/runtime/server/respond.js +1 -0
- package/src/runtime/shared.js +2 -2
- package/src/runtime/utils.js +0 -1
- package/src/types/internal.d.ts +45 -13
- package/src/version.js +1 -1
- package/types/index.d.ts +137 -15
- package/types/index.d.ts.map +7 -4
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/** @import { RemoteQueryFunction } from '@sveltejs/kit' */
|
|
2
|
-
/** @import { RemoteFunctionResponse } from 'types' */
|
|
3
2
|
import { app_dir, base } from '$app/paths/internal/client';
|
|
4
|
-
import { app,
|
|
3
|
+
import { app, query_map, query_responses } from '../client.js';
|
|
5
4
|
import {
|
|
6
5
|
get_remote_request_headers,
|
|
6
|
+
is_in_effect,
|
|
7
7
|
QUERY_FUNCTION_ID,
|
|
8
8
|
QUERY_OVERRIDE_KEY,
|
|
9
9
|
QUERY_RESOURCE_KEY,
|
|
10
10
|
remote_request
|
|
11
11
|
} from './shared.svelte.js';
|
|
12
12
|
import * as devalue from 'devalue';
|
|
13
|
-
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
14
13
|
import { DEV } from 'esm-env';
|
|
15
14
|
import { noop } from '../../../utils/functions.js';
|
|
16
15
|
import { with_resolvers } from '../../../utils/promise.js';
|
|
@@ -26,18 +25,6 @@ import { create_remote_key, stringify_remote_arg, unfriendly_hydratable } from '
|
|
|
26
25
|
* }} RemoteQueryCacheEntry
|
|
27
26
|
*/
|
|
28
27
|
|
|
29
|
-
/**
|
|
30
|
-
* @returns {boolean} Returns `true` if we are in an effect
|
|
31
|
-
*/
|
|
32
|
-
function is_in_effect() {
|
|
33
|
-
try {
|
|
34
|
-
$effect.pre(noop);
|
|
35
|
-
return true;
|
|
36
|
-
} catch {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
28
|
/**
|
|
42
29
|
* @param {string} id
|
|
43
30
|
* @returns {RemoteQueryFunction<any, any>}
|
|
@@ -49,8 +36,7 @@ export function query(id) {
|
|
|
49
36
|
|
|
50
37
|
if (entries) {
|
|
51
38
|
for (const entry of entries.values()) {
|
|
52
|
-
|
|
53
|
-
void entry.resource.refresh?.();
|
|
39
|
+
void entry.resource.refresh();
|
|
54
40
|
}
|
|
55
41
|
}
|
|
56
42
|
}
|
|
@@ -73,102 +59,6 @@ export function query(id) {
|
|
|
73
59
|
return wrapper;
|
|
74
60
|
}
|
|
75
61
|
|
|
76
|
-
/**
|
|
77
|
-
* @param {string} id
|
|
78
|
-
* @returns {RemoteQueryFunction<any, any>}
|
|
79
|
-
*/
|
|
80
|
-
export function query_batch(id) {
|
|
81
|
-
/** @type {Map<string, Array<{resolve: (value: any) => void, reject: (error: any) => void}>>} */
|
|
82
|
-
let batching = new Map();
|
|
83
|
-
|
|
84
|
-
/** @type {RemoteQueryFunction<any, any>} */
|
|
85
|
-
const wrapper = (arg) => {
|
|
86
|
-
return new QueryProxy(id, arg, async (key, payload) => {
|
|
87
|
-
const serialized = await unfriendly_hydratable(key, () => {
|
|
88
|
-
return new Promise((resolve, reject) => {
|
|
89
|
-
// create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
|
|
90
|
-
// is invoked multiple times with the same payload, so we need to deduplicate here
|
|
91
|
-
const entry = batching.get(payload) ?? [];
|
|
92
|
-
entry.push({ resolve, reject });
|
|
93
|
-
batching.set(payload, entry);
|
|
94
|
-
|
|
95
|
-
if (batching.size > 1) return;
|
|
96
|
-
|
|
97
|
-
// Do this here, after await Svelte' reactivity context is gone.
|
|
98
|
-
// TODO is it possible to have batches of the same key
|
|
99
|
-
// but in different forks/async contexts and in the same macrotask?
|
|
100
|
-
// If so this would potentially be buggy
|
|
101
|
-
const headers = {
|
|
102
|
-
'Content-Type': 'application/json',
|
|
103
|
-
...get_remote_request_headers()
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
|
|
107
|
-
// and flushes could reveal more queries that should be batched.
|
|
108
|
-
setTimeout(async () => {
|
|
109
|
-
const batched = batching;
|
|
110
|
-
batching = new Map();
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
|
|
114
|
-
method: 'POST',
|
|
115
|
-
body: JSON.stringify({
|
|
116
|
-
payloads: Array.from(batched.keys())
|
|
117
|
-
}),
|
|
118
|
-
headers
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
if (!response.ok) {
|
|
122
|
-
throw new Error('Failed to execute batch query');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
|
|
126
|
-
if (result.type === 'error') {
|
|
127
|
-
throw new HttpError(result.status ?? 500, result.error);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (result.type === 'redirect') {
|
|
131
|
-
await goto(result.location);
|
|
132
|
-
throw new Redirect(307, result.location);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const results = devalue.parse(result.result, app.decoders);
|
|
136
|
-
|
|
137
|
-
// Resolve individual queries
|
|
138
|
-
// Maps guarantee insertion order so we can do it like this
|
|
139
|
-
let i = 0;
|
|
140
|
-
|
|
141
|
-
for (const resolvers of batched.values()) {
|
|
142
|
-
for (const { resolve, reject } of resolvers) {
|
|
143
|
-
if (results[i].type === 'error') {
|
|
144
|
-
reject(new HttpError(results[i].status, results[i].error));
|
|
145
|
-
} else {
|
|
146
|
-
resolve(results[i].data);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
i++;
|
|
150
|
-
}
|
|
151
|
-
} catch (error) {
|
|
152
|
-
// Reject all queries in the batch
|
|
153
|
-
for (const resolver of batched.values()) {
|
|
154
|
-
for (const { reject } of resolver) {
|
|
155
|
-
reject(error);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}, 0);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
return devalue.parse(serialized, app.decoders);
|
|
164
|
-
});
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
Object.defineProperty(wrapper, QUERY_FUNCTION_ID, { value: id });
|
|
168
|
-
|
|
169
|
-
return wrapper;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
62
|
/**
|
|
173
63
|
* The actual query instance. There should only ever be one active query instance per key.
|
|
174
64
|
*
|
|
@@ -274,6 +164,11 @@ export class Query {
|
|
|
274
164
|
resolve(undefined);
|
|
275
165
|
})
|
|
276
166
|
.catch((e) => {
|
|
167
|
+
// TODO: Our behavior here could be better:
|
|
168
|
+
// - We should not reject on redirects, but should hook into the router
|
|
169
|
+
// to ensure the query is properly refreshed before the navigation completes
|
|
170
|
+
// - Instead of failing on transport-level errors, we should probably do what
|
|
171
|
+
// LiveQuery does and preserve the last known good value and retry the connection
|
|
277
172
|
const idx = this.#latest.indexOf(resolve);
|
|
278
173
|
if (idx === -1) return;
|
|
279
174
|
|
|
@@ -290,10 +185,14 @@ export class Query {
|
|
|
290
185
|
}
|
|
291
186
|
|
|
292
187
|
get then() {
|
|
188
|
+
// TODO this should be unnecessary but due to the bug described
|
|
189
|
+
// in #start, we need to do this in some circumstances
|
|
190
|
+
this.#start();
|
|
293
191
|
return this.#then;
|
|
294
192
|
}
|
|
295
193
|
|
|
296
194
|
get catch() {
|
|
195
|
+
this.#start();
|
|
297
196
|
this.#then;
|
|
298
197
|
return (/** @type {any} */ reject) => {
|
|
299
198
|
return this.#then(undefined, reject);
|
|
@@ -301,6 +200,7 @@ export class Query {
|
|
|
301
200
|
}
|
|
302
201
|
|
|
303
202
|
get finally() {
|
|
203
|
+
this.#start();
|
|
304
204
|
this.#then;
|
|
305
205
|
return (/** @type {any} */ fn) => {
|
|
306
206
|
return this.#then(
|
|
@@ -410,7 +310,7 @@ export class Query {
|
|
|
410
310
|
* @template T
|
|
411
311
|
* @implements {Promise<T>}
|
|
412
312
|
*/
|
|
413
|
-
class QueryProxy {
|
|
313
|
+
export class QueryProxy {
|
|
414
314
|
#id;
|
|
415
315
|
#key;
|
|
416
316
|
#payload;
|
|
@@ -545,7 +445,7 @@ class QueryProxy {
|
|
|
545
445
|
/** @type {Query<T>['withOverride']} */
|
|
546
446
|
withOverride(fn) {
|
|
547
447
|
const entry = this.#get_or_create_cache_entry();
|
|
548
|
-
const override = entry.resource.withOverride(fn);
|
|
448
|
+
const override = /** @type {Query<T>} */ (entry.resource).withOverride(fn);
|
|
549
449
|
|
|
550
450
|
const release = /** @type {(() => void) & { [QUERY_OVERRIDE_KEY]: string }} */ (
|
|
551
451
|
() => {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
/** @import { RemoteFunctionResponse,
|
|
1
|
+
/** @import { RemoteFunctionResponse, RemoteSingleflightMap, RemoteSingleflightEntry } from 'types' */
|
|
2
2
|
/** @import { RemoteQueryUpdate } from '@sveltejs/kit' */
|
|
3
3
|
import * as devalue from 'devalue';
|
|
4
|
-
import { app, goto, query_map } from '../client.js';
|
|
4
|
+
import { app, goto, live_query_map, query_map } from '../client.js';
|
|
5
5
|
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
6
6
|
import { untrack } from 'svelte';
|
|
7
7
|
import { create_remote_key, split_remote_key } from '../../shared.js';
|
|
8
8
|
import { navigating, page } from '../state.svelte.js';
|
|
9
|
+
import { noop } from '../../../utils/functions.js';
|
|
9
10
|
|
|
10
11
|
/** Indicates a query function, as opposed to a query instance */
|
|
11
12
|
export const QUERY_FUNCTION_ID = Symbol('sveltekit.query_function_id');
|
|
@@ -14,6 +15,18 @@ export const QUERY_OVERRIDE_KEY = Symbol('sveltekit.query_override_key');
|
|
|
14
15
|
/** Indicates a query instance */
|
|
15
16
|
export const QUERY_RESOURCE_KEY = Symbol('sveltekit.query_resource_key');
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @returns {boolean} Returns `true` if we are in an effect
|
|
20
|
+
*/
|
|
21
|
+
export function is_in_effect() {
|
|
22
|
+
try {
|
|
23
|
+
$effect.pre(noop);
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
/**
|
|
18
31
|
* @returns {{ 'x-sveltekit-pathname': string, 'x-sveltekit-search': string }}
|
|
19
32
|
*/
|
|
@@ -49,16 +62,26 @@ export async function remote_request(url, headers) {
|
|
|
49
62
|
|
|
50
63
|
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
|
|
51
64
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
const resolved = await handle_side_channel_response(result);
|
|
66
|
+
|
|
67
|
+
return resolved.result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {RemoteFunctionResponse} response
|
|
72
|
+
* @returns {Promise<Extract<RemoteFunctionResponse, { type: 'result' }>>}
|
|
73
|
+
*/
|
|
74
|
+
export async function handle_side_channel_response(response) {
|
|
75
|
+
if (response.type === 'redirect') {
|
|
76
|
+
await goto(response.location);
|
|
77
|
+
throw new Redirect(307, response.location);
|
|
55
78
|
}
|
|
56
79
|
|
|
57
|
-
if (
|
|
58
|
-
throw new HttpError(
|
|
80
|
+
if (response.type === 'error') {
|
|
81
|
+
throw new HttpError(response.status ?? 500, response.error);
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
return
|
|
84
|
+
return response;
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
/**
|
|
@@ -81,10 +104,10 @@ export function categorize_updates(updates) {
|
|
|
81
104
|
if (typeof update === 'function') {
|
|
82
105
|
if (Object.hasOwn(update, QUERY_FUNCTION_ID)) {
|
|
83
106
|
// this is a query function (not instance), so we need to find all active instances
|
|
84
|
-
// of this
|
|
107
|
+
// of this function and request that they be refreshed/reconnected by the command handler
|
|
85
108
|
// @ts-expect-error
|
|
86
109
|
const id = /** @type {string} */ (update[QUERY_FUNCTION_ID]);
|
|
87
|
-
const entries = query_map.get(id);
|
|
110
|
+
const entries = query_map.get(id) ?? live_query_map.get(id);
|
|
88
111
|
|
|
89
112
|
if (entries) {
|
|
90
113
|
for (const payload of entries.keys()) {
|
|
@@ -112,6 +135,10 @@ export function categorize_updates(updates) {
|
|
|
112
135
|
overrides.push(/** @type {() => void} */ (update));
|
|
113
136
|
continue;
|
|
114
137
|
}
|
|
138
|
+
|
|
139
|
+
// this is just a regular function provided by some user integration, so we can just stash it in the overrides array
|
|
140
|
+
overrides.push(/** @type {() => void} */ (update));
|
|
141
|
+
continue;
|
|
115
142
|
}
|
|
116
143
|
|
|
117
144
|
if (
|
|
@@ -125,31 +152,57 @@ export function categorize_updates(updates) {
|
|
|
125
152
|
continue;
|
|
126
153
|
}
|
|
127
154
|
|
|
128
|
-
throw new Error(
|
|
155
|
+
throw new Error(
|
|
156
|
+
'updates() expects a query or live query function, query resource, or query override'
|
|
157
|
+
);
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
return { overrides, refreshes };
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* @param {string}
|
|
164
|
+
* @template TResource
|
|
165
|
+
* @param {string} stringified_singleflight
|
|
166
|
+
* @param {Map<string, Map<string, { resource: TResource }>>} map
|
|
167
|
+
* @param {(resource: TResource, value: RemoteSingleflightEntry) => void} callback
|
|
138
168
|
*/
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
169
|
+
function apply_singleflight(stringified_singleflight, map, callback) {
|
|
170
|
+
const singleflight = /** @type {RemoteSingleflightMap} */ (
|
|
171
|
+
devalue.parse(stringified_singleflight, app.decoders)
|
|
142
172
|
);
|
|
143
173
|
|
|
144
|
-
for (const [key, value] of
|
|
174
|
+
for (const [key, value] of Object.entries(singleflight)) {
|
|
145
175
|
const parts = split_remote_key(key);
|
|
176
|
+
const entry = map.get(parts.id)?.get(parts.payload);
|
|
177
|
+
if (entry?.resource) {
|
|
178
|
+
callback(entry.resource, value);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
146
182
|
|
|
147
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Apply refresh data from the server to the relevant queries
|
|
185
|
+
*
|
|
186
|
+
* @param {string} stringified_refreshes
|
|
187
|
+
*/
|
|
188
|
+
export const apply_refreshes = (stringified_refreshes) => {
|
|
189
|
+
apply_singleflight(stringified_refreshes, query_map, (resource, value) => {
|
|
190
|
+
if (value.type === 'result') {
|
|
191
|
+
resource?.set(value.data);
|
|
192
|
+
} else {
|
|
193
|
+
resource?.fail(new HttpError(value.status ?? 500, value.error));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
};
|
|
148
197
|
|
|
198
|
+
/** @param {string} stringified_reconnects */
|
|
199
|
+
export const apply_reconnections = (stringified_reconnects) => {
|
|
200
|
+
apply_singleflight(stringified_reconnects, live_query_map, (resource, value) => {
|
|
149
201
|
if (value.type === 'result') {
|
|
150
|
-
|
|
202
|
+
resource?.set(value.data);
|
|
203
|
+
void resource?.reconnect();
|
|
151
204
|
} else {
|
|
152
|
-
|
|
205
|
+
resource?.fail(new HttpError(value.status ?? 500, value.error));
|
|
153
206
|
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
207
|
+
});
|
|
208
|
+
};
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { DEV } from 'esm-env';
|
|
6
6
|
import * as devalue from 'devalue';
|
|
7
|
-
import {
|
|
7
|
+
import { text_encoder } from './utils.js';
|
|
8
8
|
import { SvelteKitError } from '@sveltejs/kit/internal';
|
|
9
9
|
|
|
10
|
+
const decoder = new TextDecoder();
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Sets a value in a nested object using a path string, mutating the original object
|
|
12
14
|
* @param {Record<string, any>} object
|
|
@@ -250,7 +252,7 @@ export async function deserialize_binary_form(request) {
|
|
|
250
252
|
const file_offsets_buffer = await get_buffer(HEADER_BYTES + data_length, file_offsets_length);
|
|
251
253
|
if (!file_offsets_buffer) throw deserialize_error('file offset table too short');
|
|
252
254
|
|
|
253
|
-
const parsed_offsets = JSON.parse(
|
|
255
|
+
const parsed_offsets = JSON.parse(decoder.decode(file_offsets_buffer));
|
|
254
256
|
|
|
255
257
|
if (
|
|
256
258
|
!Array.isArray(parsed_offsets) ||
|
|
@@ -265,7 +267,7 @@ export async function deserialize_binary_form(request) {
|
|
|
265
267
|
|
|
266
268
|
/** @type {Array<{ offset: number, size: number }>} */
|
|
267
269
|
const file_spans = [];
|
|
268
|
-
const [data, meta] = devalue.parse(
|
|
270
|
+
const [data, meta] = devalue.parse(decoder.decode(data_buffer), {
|
|
269
271
|
File: ([name, type, size, last_modified, index]) => {
|
|
270
272
|
if (
|
|
271
273
|
typeof name !== 'string' ||
|
|
@@ -458,7 +460,7 @@ class LazyFile {
|
|
|
458
460
|
});
|
|
459
461
|
}
|
|
460
462
|
async text() {
|
|
461
|
-
return
|
|
463
|
+
return decoder.decode(await this.arrayBuffer());
|
|
462
464
|
}
|
|
463
465
|
}
|
|
464
466
|
|
|
@@ -664,7 +666,7 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
|
|
|
664
666
|
if (prop === 'as') {
|
|
665
667
|
/**
|
|
666
668
|
* @param {string} type
|
|
667
|
-
* @param {
|
|
669
|
+
* @param {unknown} [input_value]
|
|
668
670
|
*/
|
|
669
671
|
const as_func = (type, input_value) => {
|
|
670
672
|
const is_array =
|
|
@@ -732,6 +734,23 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
|
|
|
732
734
|
}
|
|
733
735
|
}
|
|
734
736
|
|
|
737
|
+
if (type === 'checkbox' && !is_array) {
|
|
738
|
+
return Object.defineProperties(base_props, {
|
|
739
|
+
defaultChecked: {
|
|
740
|
+
enumerable: true,
|
|
741
|
+
get() {
|
|
742
|
+
return input_value;
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
checked: {
|
|
746
|
+
enumerable: true,
|
|
747
|
+
get() {
|
|
748
|
+
return get_value() ?? input_value;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
735
754
|
return Object.defineProperties(base_props, {
|
|
736
755
|
value: { value: input_value ?? 'on', enumerable: true },
|
|
737
756
|
checked: {
|
|
@@ -743,11 +762,7 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
|
|
|
743
762
|
return value === input_value;
|
|
744
763
|
}
|
|
745
764
|
|
|
746
|
-
|
|
747
|
-
return (value ?? []).includes(input_value);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
return value;
|
|
765
|
+
return (value ?? []).includes(input_value);
|
|
751
766
|
}
|
|
752
767
|
}
|
|
753
768
|
});
|
|
@@ -797,6 +812,12 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
|
|
|
797
812
|
|
|
798
813
|
// Handle all other input types (text, number, etc.)
|
|
799
814
|
return Object.defineProperties(base_props, {
|
|
815
|
+
defaultValue: {
|
|
816
|
+
enumerable: true,
|
|
817
|
+
get() {
|
|
818
|
+
return input_value;
|
|
819
|
+
}
|
|
820
|
+
},
|
|
800
821
|
value: {
|
|
801
822
|
enumerable: true,
|
|
802
823
|
get() {
|
|
@@ -4,7 +4,7 @@ import { disable_search, make_trackable } from '../../../utils/url.js';
|
|
|
4
4
|
import { validate_depends, validate_load_response } from '../../shared.js';
|
|
5
5
|
import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server';
|
|
6
6
|
import { record_span } from '../../telemetry/record_span.js';
|
|
7
|
-
import { base64_encode
|
|
7
|
+
import { base64_encode } from '../../utils.js';
|
|
8
8
|
import { NULL_BODY_STATUS } from '../constants.js';
|
|
9
9
|
import { get_node_type } from '../utils.js';
|
|
10
10
|
|
|
@@ -493,12 +493,14 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
|
|
|
493
493
|
async function stream_to_string(stream) {
|
|
494
494
|
let result = '';
|
|
495
495
|
const reader = stream.getReader();
|
|
496
|
+
const decoder = new TextDecoder();
|
|
496
497
|
while (true) {
|
|
497
498
|
const { done, value } = await reader.read();
|
|
498
499
|
if (done) {
|
|
500
|
+
result += decoder.decode();
|
|
499
501
|
break;
|
|
500
502
|
}
|
|
501
|
-
result +=
|
|
503
|
+
result += decoder.decode(value, { stream: true });
|
|
502
504
|
}
|
|
503
505
|
return result;
|
|
504
506
|
}
|
|
@@ -529,7 +529,10 @@ export async function render_response({
|
|
|
529
529
|
|
|
530
530
|
const store = internals.type === 'prerender' ? prerender : query;
|
|
531
531
|
|
|
532
|
-
if (
|
|
532
|
+
if (
|
|
533
|
+
event_state.remote.refreshes?.has(remote_key) ||
|
|
534
|
+
event_state.remote.reconnects?.has(remote_key)
|
|
535
|
+
) {
|
|
533
536
|
// This entry was refreshed/set by a command or form action.
|
|
534
537
|
// Always await it so the mutation result is serialized.
|
|
535
538
|
store[remote_key] = await entry.data;
|
|
@@ -121,7 +121,12 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
121
121
|
/** @type {RemoteFunctionResponse} */ ({
|
|
122
122
|
type: 'result',
|
|
123
123
|
result: stringify(result, transport),
|
|
124
|
-
refreshes: result.issues
|
|
124
|
+
refreshes: result.issues
|
|
125
|
+
? undefined
|
|
126
|
+
: await serialize_singleflight(state.remote.refreshes),
|
|
127
|
+
reconnects: result.issues
|
|
128
|
+
? undefined
|
|
129
|
+
: await serialize_singleflight(state.remote.reconnects)
|
|
125
130
|
})
|
|
126
131
|
);
|
|
127
132
|
}
|
|
@@ -137,11 +142,115 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
137
142
|
/** @type {RemoteFunctionResponse} */ ({
|
|
138
143
|
type: 'result',
|
|
139
144
|
result: stringify(data, transport),
|
|
140
|
-
refreshes: await
|
|
145
|
+
refreshes: await serialize_singleflight(state.remote.refreshes),
|
|
146
|
+
reconnects: await serialize_singleflight(state.remote.reconnects)
|
|
141
147
|
})
|
|
142
148
|
);
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
if (internals.type === 'query_live') {
|
|
152
|
+
if (event.request.method !== 'GET') {
|
|
153
|
+
throw new SvelteKitError(
|
|
154
|
+
405,
|
|
155
|
+
'Method Not Allowed',
|
|
156
|
+
`\`query.live\` functions must be invoked via GET request, not ${event.request.method}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const payload = /** @type {string} */ (
|
|
161
|
+
new URL(event.request.url).searchParams.get('payload')
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const generator = internals.run(event, state, parse_remote_arg(payload, transport));
|
|
165
|
+
|
|
166
|
+
const encoder = new TextEncoder();
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {ReadableStreamDefaultController} controller
|
|
170
|
+
* @param {any} payload
|
|
171
|
+
*/
|
|
172
|
+
function send(controller, payload) {
|
|
173
|
+
controller.enqueue(encoder.encode(JSON.stringify(payload) + '\n'));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let closed = false;
|
|
177
|
+
|
|
178
|
+
/** @type {string | undefined} */
|
|
179
|
+
let result = undefined;
|
|
180
|
+
|
|
181
|
+
async function cancel() {
|
|
182
|
+
if (closed) return;
|
|
183
|
+
closed = true;
|
|
184
|
+
await generator.return(undefined);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
event.request.signal.addEventListener('abort', cancel, { once: true });
|
|
188
|
+
|
|
189
|
+
return new Response(
|
|
190
|
+
new ReadableStream({
|
|
191
|
+
async pull(controller) {
|
|
192
|
+
if (event.request.signal.aborted) {
|
|
193
|
+
await cancel();
|
|
194
|
+
controller.close();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
while (true) {
|
|
200
|
+
const { value, done } = await generator.next();
|
|
201
|
+
|
|
202
|
+
if (done) {
|
|
203
|
+
await cancel();
|
|
204
|
+
controller.close();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// only send changed data
|
|
209
|
+
if (result !== (result = stringify(value, transport))) {
|
|
210
|
+
send(controller, {
|
|
211
|
+
type: 'result',
|
|
212
|
+
result
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (!event.request.signal.aborted) {
|
|
220
|
+
if (error instanceof Redirect) {
|
|
221
|
+
send(controller, {
|
|
222
|
+
type: 'redirect',
|
|
223
|
+
location: error.location
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
const status =
|
|
227
|
+
error instanceof HttpError || error instanceof SvelteKitError
|
|
228
|
+
? error.status
|
|
229
|
+
: 500;
|
|
230
|
+
|
|
231
|
+
send(controller, {
|
|
232
|
+
type: 'error',
|
|
233
|
+
error: await handle_error_and_jsonify(event, state, options, error),
|
|
234
|
+
status
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await cancel();
|
|
240
|
+
controller.close();
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
cancel
|
|
244
|
+
}),
|
|
245
|
+
{
|
|
246
|
+
headers: {
|
|
247
|
+
'cache-control': 'private, no-store',
|
|
248
|
+
'content-type': 'application/x-ndjson'
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
145
254
|
const payload =
|
|
146
255
|
internals.type === 'prerender'
|
|
147
256
|
? additional_args
|
|
@@ -166,7 +275,8 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
166
275
|
/** @type {RemoteFunctionResponse} */ ({
|
|
167
276
|
type: 'redirect',
|
|
168
277
|
location: error.location,
|
|
169
|
-
refreshes: await
|
|
278
|
+
refreshes: await serialize_singleflight(state.remote.refreshes),
|
|
279
|
+
reconnects: await serialize_singleflight(state.remote.reconnects)
|
|
170
280
|
})
|
|
171
281
|
);
|
|
172
282
|
}
|
|
@@ -191,16 +301,14 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
191
301
|
);
|
|
192
302
|
}
|
|
193
303
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const entries = Object.entries(refreshes);
|
|
198
|
-
if (entries.length === 0) {
|
|
304
|
+
/** @param {Map<string, Promise<any>> | null} map */
|
|
305
|
+
async function serialize_singleflight(map) {
|
|
306
|
+
if (!map || map.size === 0) {
|
|
199
307
|
return undefined;
|
|
200
308
|
}
|
|
201
309
|
|
|
202
310
|
const results = await Promise.all(
|
|
203
|
-
|
|
311
|
+
Array.from(map, async ([key, promise]) => {
|
|
204
312
|
try {
|
|
205
313
|
return [key, { type: 'result', data: await promise }];
|
|
206
314
|
} catch (error) {
|
|
@@ -152,6 +152,7 @@ export async function internal_respond(request, options, manifest, state) {
|
|
|
152
152
|
forms: null,
|
|
153
153
|
/** A map of remote function key to corresponding single-flight-mutation promise */
|
|
154
154
|
refreshes: null,
|
|
155
|
+
reconnects: null,
|
|
155
156
|
/** A map of remote function ID to payloads requested for refreshing by the client */
|
|
156
157
|
requested: null
|
|
157
158
|
},
|
package/src/runtime/shared.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @import { Transport } from '@sveltejs/kit' */
|
|
2
2
|
import * as devalue from 'devalue';
|
|
3
|
-
import { base64_decode, base64_encode,
|
|
3
|
+
import { base64_decode, base64_encode, text_encoder } from './utils.js';
|
|
4
4
|
import * as svelte from 'svelte';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -273,7 +273,7 @@ export function stringify_remote_arg(value, transport, sort = true) {
|
|
|
273
273
|
export function parse_remote_arg(string, transport) {
|
|
274
274
|
if (!string) return undefined;
|
|
275
275
|
|
|
276
|
-
const json_string =
|
|
276
|
+
const json_string = new TextDecoder().decode(
|
|
277
277
|
// no need to add back `=` characters, atob can handle it
|
|
278
278
|
base64_decode(string.replaceAll('-', '+').replaceAll('_', '/'))
|
|
279
279
|
);
|