@sveltejs/kit 2.55.0 → 2.56.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 +3 -4
- package/src/core/postbuild/analyse.js +3 -3
- package/src/core/postbuild/prerender.js +9 -6
- package/src/core/sync/write_tsconfig.js +3 -1
- package/src/exports/internal/remote-functions.js +2 -2
- package/src/exports/public.d.ts +39 -13
- package/src/exports/vite/build/build_server.js +24 -4
- package/src/exports/vite/build/build_service_worker.js +16 -6
- package/src/exports/vite/build/utils.js +18 -3
- package/src/exports/vite/index.js +336 -327
- package/src/runtime/app/paths/server.js +1 -1
- package/src/runtime/app/server/index.js +1 -1
- package/src/runtime/app/server/remote/command.js +12 -7
- package/src/runtime/app/server/remote/form.js +14 -14
- package/src/runtime/app/server/remote/index.js +1 -0
- package/src/runtime/app/server/remote/prerender.js +8 -7
- package/src/runtime/app/server/remote/query.js +141 -66
- package/src/runtime/app/server/remote/requested.js +172 -0
- package/src/runtime/app/server/remote/shared.js +32 -10
- package/src/runtime/client/client.js +45 -20
- package/src/runtime/client/remote-functions/command.svelte.js +39 -16
- package/src/runtime/client/remote-functions/form.svelte.js +41 -24
- package/src/runtime/client/remote-functions/prerender.svelte.js +105 -76
- package/src/runtime/client/remote-functions/query.svelte.js +408 -138
- package/src/runtime/client/remote-functions/shared.svelte.js +95 -94
- package/src/runtime/components/svelte-5/error.svelte +2 -0
- package/src/runtime/form-utils.js +3 -7
- package/src/runtime/server/endpoint.js +0 -1
- package/src/runtime/server/page/actions.js +2 -1
- package/src/runtime/server/page/load_data.js +3 -1
- package/src/runtime/server/page/render.js +38 -15
- package/src/runtime/server/remote.js +65 -50
- package/src/runtime/server/respond.js +17 -3
- package/src/runtime/server/utils.js +0 -12
- package/src/runtime/shared.js +233 -5
- package/src/types/global-private.d.ts +4 -4
- package/src/types/internal.d.ts +80 -44
- package/src/utils/css.js +0 -3
- package/src/utils/escape.js +15 -3
- package/src/version.js +1 -1
- package/types/index.d.ts +68 -14
- package/types/index.d.ts.map +6 -1
|
@@ -1,16 +1,41 @@
|
|
|
1
1
|
/** @import { RemoteQueryFunction } from '@sveltejs/kit' */
|
|
2
2
|
/** @import { RemoteFunctionResponse } from 'types' */
|
|
3
3
|
import { app_dir, base } from '$app/paths/internal/client';
|
|
4
|
-
import { app, goto, query_map,
|
|
5
|
-
import { tick } from 'svelte';
|
|
4
|
+
import { app, goto, query_map, query_responses } from '../client.js';
|
|
6
5
|
import {
|
|
7
|
-
create_remote_function,
|
|
8
6
|
get_remote_request_headers,
|
|
7
|
+
QUERY_FUNCTION_ID,
|
|
8
|
+
QUERY_OVERRIDE_KEY,
|
|
9
|
+
QUERY_RESOURCE_KEY,
|
|
9
10
|
remote_request
|
|
10
11
|
} from './shared.svelte.js';
|
|
11
12
|
import * as devalue from 'devalue';
|
|
12
13
|
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
13
14
|
import { DEV } from 'esm-env';
|
|
15
|
+
import { with_resolvers } from '../../../utils/promise.js';
|
|
16
|
+
import { tick, untrack } from 'svelte';
|
|
17
|
+
import { create_remote_key, stringify_remote_arg, unfriendly_hydratable } from '../../shared.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @template T
|
|
21
|
+
* @typedef {{
|
|
22
|
+
* count: number;
|
|
23
|
+
* resource: Query<T>;
|
|
24
|
+
* cleanup: () => void;
|
|
25
|
+
* }} RemoteQueryCacheEntry
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @returns {boolean} Returns `true` if we are in an effect
|
|
30
|
+
*/
|
|
31
|
+
function is_in_effect() {
|
|
32
|
+
try {
|
|
33
|
+
$effect.pre(() => {});
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
14
39
|
|
|
15
40
|
/**
|
|
16
41
|
* @param {string} id
|
|
@@ -19,144 +44,152 @@ import { DEV } from 'esm-env';
|
|
|
19
44
|
export function query(id) {
|
|
20
45
|
if (DEV) {
|
|
21
46
|
// If this reruns as part of HMR, refresh the query
|
|
22
|
-
|
|
23
|
-
|
|
47
|
+
const entries = query_map.get(id);
|
|
48
|
+
|
|
49
|
+
if (entries) {
|
|
50
|
+
for (const entry of entries.values()) {
|
|
24
51
|
// use optional chaining in case a prerender function was turned into a query
|
|
25
|
-
entry.resource.refresh?.();
|
|
52
|
+
void entry.resource.refresh?.();
|
|
26
53
|
}
|
|
27
54
|
}
|
|
28
55
|
}
|
|
29
56
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return remote_responses[cache_key];
|
|
34
|
-
}
|
|
35
|
-
|
|
57
|
+
/** @type {RemoteQueryFunction<any, any>} */
|
|
58
|
+
const wrapper = (arg) => {
|
|
59
|
+
return new QueryProxy(id, arg, async (key, payload) => {
|
|
36
60
|
const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
|
|
37
61
|
|
|
38
|
-
const
|
|
39
|
-
|
|
62
|
+
const serialized = await unfriendly_hydratable(key, () =>
|
|
63
|
+
remote_request(url, get_remote_request_headers())
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return devalue.parse(serialized, app.decoders);
|
|
40
67
|
});
|
|
41
|
-
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
Object.defineProperty(wrapper, QUERY_FUNCTION_ID, { value: id });
|
|
71
|
+
|
|
72
|
+
return wrapper;
|
|
42
73
|
}
|
|
43
74
|
|
|
44
75
|
/**
|
|
45
76
|
* @param {string} id
|
|
46
|
-
* @returns {
|
|
77
|
+
* @returns {RemoteQueryFunction<any, any>}
|
|
47
78
|
*/
|
|
48
79
|
export function query_batch(id) {
|
|
49
80
|
/** @type {Map<string, Array<{resolve: (value: any) => void, reject: (error: any) => void}>>} */
|
|
50
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- we don't need reactivity for this
|
|
51
81
|
let batching = new Map();
|
|
52
82
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (!response.ok) {
|
|
96
|
-
throw new Error('Failed to execute batch query');
|
|
97
|
-
}
|
|
83
|
+
/** @type {RemoteQueryFunction<any, any>} */
|
|
84
|
+
const wrapper = (arg) => {
|
|
85
|
+
return new QueryProxy(id, arg, async (key, payload) => {
|
|
86
|
+
const serialized = await unfriendly_hydratable(key, () => {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
// create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
|
|
89
|
+
// is invoked multiple times with the same payload, so we need to deduplicate here
|
|
90
|
+
const entry = batching.get(payload) ?? [];
|
|
91
|
+
entry.push({ resolve, reject });
|
|
92
|
+
batching.set(payload, entry);
|
|
93
|
+
|
|
94
|
+
if (batching.size > 1) return;
|
|
95
|
+
|
|
96
|
+
// Do this here, after await Svelte' reactivity context is gone.
|
|
97
|
+
// TODO is it possible to have batches of the same key
|
|
98
|
+
// but in different forks/async contexts and in the same macrotask?
|
|
99
|
+
// If so this would potentially be buggy
|
|
100
|
+
const headers = {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
...get_remote_request_headers()
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
|
|
106
|
+
// and flushes could reveal more queries that should be batched.
|
|
107
|
+
setTimeout(async () => {
|
|
108
|
+
const batched = batching;
|
|
109
|
+
batching = new Map();
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
payloads: Array.from(batched.keys())
|
|
116
|
+
}),
|
|
117
|
+
headers
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error('Failed to execute batch query');
|
|
122
|
+
}
|
|
98
123
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
|
|
125
|
+
if (result.type === 'error') {
|
|
126
|
+
throw new HttpError(result.status ?? 500, result.error);
|
|
127
|
+
}
|
|
103
128
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
129
|
+
if (result.type === 'redirect') {
|
|
130
|
+
await goto(result.location);
|
|
131
|
+
throw new Redirect(307, result.location);
|
|
132
|
+
}
|
|
108
133
|
|
|
109
|
-
|
|
134
|
+
const results = devalue.parse(result.result, app.decoders);
|
|
110
135
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
// Resolve individual queries
|
|
137
|
+
// Maps guarantee insertion order so we can do it like this
|
|
138
|
+
let i = 0;
|
|
114
139
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
for (const resolvers of batched.values()) {
|
|
141
|
+
for (const { resolve, reject } of resolvers) {
|
|
142
|
+
if (results[i].type === 'error') {
|
|
143
|
+
reject(new HttpError(results[i].status, results[i].error));
|
|
144
|
+
} else {
|
|
145
|
+
resolve(results[i].data);
|
|
146
|
+
}
|
|
121
147
|
}
|
|
148
|
+
i++;
|
|
122
149
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
reject(error);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Reject all queries in the batch
|
|
152
|
+
for (const resolver of batched.values()) {
|
|
153
|
+
for (const { reject } of resolver) {
|
|
154
|
+
reject(error);
|
|
155
|
+
}
|
|
130
156
|
}
|
|
131
157
|
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
158
|
+
}, 0);
|
|
159
|
+
});
|
|
134
160
|
});
|
|
161
|
+
|
|
162
|
+
return devalue.parse(serialized, app.decoders);
|
|
135
163
|
});
|
|
136
|
-
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
Object.defineProperty(wrapper, QUERY_FUNCTION_ID, { value: id });
|
|
167
|
+
|
|
168
|
+
return wrapper;
|
|
137
169
|
}
|
|
138
170
|
|
|
139
171
|
/**
|
|
172
|
+
* The actual query instance. There should only ever be one active query instance per key.
|
|
173
|
+
*
|
|
140
174
|
* @template T
|
|
141
|
-
* @implements {
|
|
175
|
+
* @implements {Promise<T>}
|
|
142
176
|
*/
|
|
143
177
|
export class Query {
|
|
144
178
|
/** @type {string} */
|
|
145
|
-
|
|
179
|
+
#key;
|
|
146
180
|
|
|
147
|
-
#init = false;
|
|
148
181
|
/** @type {() => Promise<T>} */
|
|
149
182
|
#fn;
|
|
150
183
|
#loading = $state(true);
|
|
151
|
-
/** @type {Array<() => void>} */
|
|
184
|
+
/** @type {Array<(value: undefined) => void>} */
|
|
152
185
|
#latest = [];
|
|
153
186
|
|
|
154
187
|
/** @type {boolean} */
|
|
155
188
|
#ready = $state(false);
|
|
156
189
|
/** @type {T | undefined} */
|
|
157
190
|
#raw = $state.raw();
|
|
158
|
-
/** @type {Promise<void>} */
|
|
159
|
-
#promise;
|
|
191
|
+
/** @type {Promise<void> | null} */
|
|
192
|
+
#promise = $state.raw(null);
|
|
160
193
|
/** @type {Array<(old: T) => T>} */
|
|
161
194
|
#overrides = $state([]);
|
|
162
195
|
|
|
@@ -168,21 +201,17 @@ export class Query {
|
|
|
168
201
|
return this.#overrides.reduce((v, r) => r(v), /** @type {T} */ (this.#raw));
|
|
169
202
|
});
|
|
170
203
|
|
|
204
|
+
/** @type {any} */
|
|
171
205
|
#error = $state.raw(undefined);
|
|
172
206
|
|
|
173
207
|
/** @type {Promise<T>['then']} */
|
|
174
208
|
// @ts-expect-error TS doesn't understand that the promise returns something
|
|
175
209
|
#then = $derived.by(() => {
|
|
176
|
-
const p = this.#
|
|
210
|
+
const p = this.#get_promise();
|
|
177
211
|
this.#overrides.length;
|
|
178
212
|
|
|
179
213
|
return (resolve, reject) => {
|
|
180
|
-
const result = (
|
|
181
|
-
await p;
|
|
182
|
-
// svelte-ignore await_reactivity_loss
|
|
183
|
-
await tick();
|
|
184
|
-
return /** @type {T} */ (this.#current);
|
|
185
|
-
})();
|
|
214
|
+
const result = p.then(tick).then(() => /** @type {T} */ (this.#current));
|
|
186
215
|
|
|
187
216
|
if (resolve || reject) {
|
|
188
217
|
return result.then(resolve, reject);
|
|
@@ -197,34 +226,34 @@ export class Query {
|
|
|
197
226
|
* @param {() => Promise<T>} fn
|
|
198
227
|
*/
|
|
199
228
|
constructor(key, fn) {
|
|
200
|
-
this
|
|
229
|
+
this.#key = key;
|
|
201
230
|
this.#fn = fn;
|
|
202
|
-
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#get_promise() {
|
|
234
|
+
void untrack(() => (this.#promise ??= this.#run()));
|
|
235
|
+
return /** @type {Promise<T>} */ (this.#promise);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#start() {
|
|
239
|
+
// there is a really weird bug with untrack and writes and initializations
|
|
240
|
+
// every time you see this comment, try removing the `tick.then` here and see
|
|
241
|
+
// if all the tests still pass with the latest svelte version
|
|
242
|
+
// if they do, congrats, you can remove tick.then
|
|
243
|
+
void tick().then(() => this.#get_promise());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#clear_pending() {
|
|
247
|
+
this.#latest.forEach((r) => r(undefined));
|
|
248
|
+
this.#latest.length = 0;
|
|
203
249
|
}
|
|
204
250
|
|
|
205
251
|
#run() {
|
|
206
|
-
|
|
207
|
-
if (this.#init) {
|
|
208
|
-
this.#loading = true;
|
|
209
|
-
} else {
|
|
210
|
-
this.#init = true;
|
|
211
|
-
}
|
|
252
|
+
this.#loading = true;
|
|
212
253
|
|
|
213
|
-
|
|
214
|
-
/** @type {() => void} */
|
|
215
|
-
let resolve;
|
|
216
|
-
/** @type {(e?: any) => void} */
|
|
217
|
-
let reject;
|
|
218
|
-
/** @type {Promise<void>} */
|
|
219
|
-
const promise = new Promise((res, rej) => {
|
|
220
|
-
resolve = res;
|
|
221
|
-
reject = rej;
|
|
222
|
-
});
|
|
254
|
+
const { promise, resolve, reject } = with_resolvers();
|
|
223
255
|
|
|
224
|
-
this.#latest.push(
|
|
225
|
-
// @ts-expect-error it's defined at this point
|
|
226
|
-
resolve
|
|
227
|
-
);
|
|
256
|
+
this.#latest.push(resolve);
|
|
228
257
|
|
|
229
258
|
Promise.resolve(this.#fn())
|
|
230
259
|
.then((value) => {
|
|
@@ -232,21 +261,27 @@ export class Query {
|
|
|
232
261
|
const idx = this.#latest.indexOf(resolve);
|
|
233
262
|
if (idx === -1) return;
|
|
234
263
|
|
|
235
|
-
this
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
// Untrack this to not trigger mutation validation errors which can occur if you do e.g. $derived({ a: await queryA(), b: await queryB() })
|
|
265
|
+
untrack(() => {
|
|
266
|
+
this.#latest.splice(0, idx).forEach((r) => r(undefined));
|
|
267
|
+
this.#ready = true;
|
|
268
|
+
this.#loading = false;
|
|
269
|
+
this.#raw = value;
|
|
270
|
+
this.#error = undefined;
|
|
271
|
+
});
|
|
240
272
|
|
|
241
|
-
resolve();
|
|
273
|
+
resolve(undefined);
|
|
242
274
|
})
|
|
243
275
|
.catch((e) => {
|
|
244
276
|
const idx = this.#latest.indexOf(resolve);
|
|
245
277
|
if (idx === -1) return;
|
|
246
278
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
279
|
+
untrack(() => {
|
|
280
|
+
this.#latest.splice(0, idx).forEach((r) => r(undefined));
|
|
281
|
+
this.#error = e;
|
|
282
|
+
this.#loading = false;
|
|
283
|
+
});
|
|
284
|
+
|
|
250
285
|
reject(e);
|
|
251
286
|
});
|
|
252
287
|
|
|
@@ -281,10 +316,12 @@ export class Query {
|
|
|
281
316
|
}
|
|
282
317
|
|
|
283
318
|
get current() {
|
|
319
|
+
this.#start();
|
|
284
320
|
return this.#current;
|
|
285
321
|
}
|
|
286
322
|
|
|
287
323
|
get error() {
|
|
324
|
+
this.#start();
|
|
288
325
|
return this.#error;
|
|
289
326
|
}
|
|
290
327
|
|
|
@@ -292,6 +329,7 @@ export class Query {
|
|
|
292
329
|
* Returns true if the resource is loading or reloading.
|
|
293
330
|
*/
|
|
294
331
|
get loading() {
|
|
332
|
+
this.#start();
|
|
295
333
|
return this.#loading;
|
|
296
334
|
}
|
|
297
335
|
|
|
@@ -299,6 +337,7 @@ export class Query {
|
|
|
299
337
|
* Returns true once the resource has been loaded for the first time.
|
|
300
338
|
*/
|
|
301
339
|
get ready() {
|
|
340
|
+
this.#start();
|
|
302
341
|
return this.#ready;
|
|
303
342
|
}
|
|
304
343
|
|
|
@@ -306,7 +345,7 @@ export class Query {
|
|
|
306
345
|
* @returns {Promise<void>}
|
|
307
346
|
*/
|
|
308
347
|
refresh() {
|
|
309
|
-
delete
|
|
348
|
+
delete query_responses[this.#key];
|
|
310
349
|
return (this.#promise = this.#run());
|
|
311
350
|
}
|
|
312
351
|
|
|
@@ -314,6 +353,7 @@ export class Query {
|
|
|
314
353
|
* @param {T} value
|
|
315
354
|
*/
|
|
316
355
|
set(value) {
|
|
356
|
+
this.#clear_pending();
|
|
317
357
|
this.#ready = true;
|
|
318
358
|
this.#loading = false;
|
|
319
359
|
this.#error = undefined;
|
|
@@ -321,21 +361,251 @@ export class Query {
|
|
|
321
361
|
this.#promise = Promise.resolve();
|
|
322
362
|
}
|
|
323
363
|
|
|
364
|
+
/**
|
|
365
|
+
* @param {unknown} error
|
|
366
|
+
*/
|
|
367
|
+
fail(error) {
|
|
368
|
+
this.#clear_pending();
|
|
369
|
+
this.#loading = false;
|
|
370
|
+
this.#error = error;
|
|
371
|
+
|
|
372
|
+
const promise = Promise.reject(error);
|
|
373
|
+
|
|
374
|
+
promise.catch(() => {});
|
|
375
|
+
this.#promise = promise;
|
|
376
|
+
}
|
|
377
|
+
|
|
324
378
|
/**
|
|
325
379
|
* @param {(old: T) => T} fn
|
|
380
|
+
* @returns {(() => void) & { [QUERY_OVERRIDE_KEY]: string }}
|
|
326
381
|
*/
|
|
327
382
|
withOverride(fn) {
|
|
328
383
|
this.#overrides.push(fn);
|
|
329
384
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
release: () => {
|
|
385
|
+
const release = /** @type {(() => void) & { [QUERY_OVERRIDE_KEY]: string }} */ (
|
|
386
|
+
() => {
|
|
333
387
|
const i = this.#overrides.indexOf(fn);
|
|
334
388
|
|
|
335
389
|
if (i !== -1) {
|
|
336
390
|
this.#overrides.splice(i, 1);
|
|
337
391
|
}
|
|
338
392
|
}
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
Object.defineProperty(release, QUERY_OVERRIDE_KEY, { value: this.#key });
|
|
396
|
+
|
|
397
|
+
return release;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
get [Symbol.toStringTag]() {
|
|
401
|
+
return 'Query';
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Manages the caching layer between the user and the actual {@link Query} instance. This is the thing
|
|
407
|
+
* the developer actually gets to interact with in their application code.
|
|
408
|
+
*
|
|
409
|
+
* @template T
|
|
410
|
+
* @implements {Promise<T>}
|
|
411
|
+
*/
|
|
412
|
+
class QueryProxy {
|
|
413
|
+
#id;
|
|
414
|
+
#key;
|
|
415
|
+
#payload;
|
|
416
|
+
#fn;
|
|
417
|
+
#active = true;
|
|
418
|
+
/**
|
|
419
|
+
* Whether this proxy was created in a tracking context.
|
|
420
|
+
* @readonly
|
|
421
|
+
*/
|
|
422
|
+
#tracking = is_in_effect();
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @param {string} id
|
|
426
|
+
* @param {any} arg
|
|
427
|
+
* @param {(key: string, payload: string) => Promise<T>} fn
|
|
428
|
+
*/
|
|
429
|
+
constructor(id, arg, fn) {
|
|
430
|
+
this.#id = id;
|
|
431
|
+
this.#payload = stringify_remote_arg(arg, app.hooks.transport);
|
|
432
|
+
this.#key = create_remote_key(id, this.#payload);
|
|
433
|
+
Object.defineProperty(this, QUERY_RESOURCE_KEY, { value: this.#key });
|
|
434
|
+
this.#fn = fn;
|
|
435
|
+
|
|
436
|
+
if (!this.#tracking) {
|
|
437
|
+
this.#active = false;
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const entry = this.#get_or_create_cache_entry();
|
|
442
|
+
|
|
443
|
+
$effect.pre(() => () => {
|
|
444
|
+
const die = this.#release(entry);
|
|
445
|
+
void tick().then(die);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** @returns {RemoteQueryCacheEntry<T>} */
|
|
450
|
+
#get_or_create_cache_entry() {
|
|
451
|
+
let query_instances = query_map.get(this.#id);
|
|
452
|
+
|
|
453
|
+
if (!query_instances) {
|
|
454
|
+
query_instances = new Map();
|
|
455
|
+
query_map.set(this.#id, query_instances);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let this_instance = query_instances.get(this.#payload);
|
|
459
|
+
|
|
460
|
+
if (!this_instance) {
|
|
461
|
+
const c = (this_instance = {
|
|
462
|
+
count: 0,
|
|
463
|
+
resource: /** @type {Query<T>} */ (/** @type {unknown} */ (null)),
|
|
464
|
+
cleanup: /** @type {() => void} */ (/** @type {unknown} */ (null))
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
c.cleanup = $effect.root(() => {
|
|
468
|
+
c.resource = new Query(this.#key, () => this.#fn(this.#key, this.#payload));
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
query_instances.set(this.#payload, this_instance);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
this_instance.count += 1;
|
|
475
|
+
|
|
476
|
+
return this_instance;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* @param {RemoteQueryCacheEntry<T>} entry
|
|
481
|
+
* @param {boolean} [deactivate]
|
|
482
|
+
* @returns
|
|
483
|
+
*/
|
|
484
|
+
#release(entry, deactivate = true) {
|
|
485
|
+
this.#active &&= !deactivate;
|
|
486
|
+
entry.count -= 1;
|
|
487
|
+
|
|
488
|
+
return () => {
|
|
489
|
+
const query_instances = query_map.get(this.#id);
|
|
490
|
+
const this_instance = query_instances?.get(this.#payload);
|
|
491
|
+
|
|
492
|
+
if (this_instance?.count === 0) {
|
|
493
|
+
this_instance.cleanup();
|
|
494
|
+
query_instances?.delete(this.#payload);
|
|
495
|
+
}
|
|
496
|
+
if (query_instances?.size === 0) {
|
|
497
|
+
query_map.delete(this.#id);
|
|
498
|
+
}
|
|
339
499
|
};
|
|
340
500
|
}
|
|
501
|
+
|
|
502
|
+
#get_cached_query() {
|
|
503
|
+
// TODO iterate on error messages
|
|
504
|
+
if (!this.#tracking) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
'This query was not created in a reactive context and is limited to calling `.run`, `.refresh`, and `.set`.'
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!this.#active) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
'This query instance is no longer active and can no longer be used for reactive state access. ' +
|
|
513
|
+
'This typically means you created the query in a tracking context and stashed it somewhere outside of a tracking context.'
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const cached = query_map.get(this.#id)?.get(this.#payload);
|
|
518
|
+
|
|
519
|
+
if (!cached) {
|
|
520
|
+
// The only case where `this.#active` can be `true` is when we've added an entry to `query_map`, and the
|
|
521
|
+
// only way that entry can get removed is if this instance (and all others) have been deactivated.
|
|
522
|
+
// So if we get here, someone (us, check git blame and point fingers) did `entry.count -= 1` improperly.
|
|
523
|
+
throw new Error(
|
|
524
|
+
'No cached query found. This should be impossible. Please file a bug report.'
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return cached.resource;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#safe_get_cached_query() {
|
|
532
|
+
return query_map.get(this.#id)?.get(this.#payload)?.resource;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
get current() {
|
|
536
|
+
return this.#get_cached_query().current;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
get error() {
|
|
540
|
+
return this.#get_cached_query().error;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
get loading() {
|
|
544
|
+
return this.#get_cached_query().loading;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
get ready() {
|
|
548
|
+
return this.#get_cached_query().ready;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
run() {
|
|
552
|
+
if (is_in_effect()) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
'On the client, .run() can only be called outside render, e.g. in universal `load` functions and event handlers. In render, await the query directly'
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (Object.hasOwn(query_responses, this.#key)) {
|
|
559
|
+
return Promise.resolve(query_responses[this.#key]);
|
|
560
|
+
}
|
|
561
|
+
return this.#fn(this.#key, this.#payload);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
refresh() {
|
|
565
|
+
return this.#safe_get_cached_query()?.refresh() ?? Promise.resolve();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** @type {Query<T>['set']} */
|
|
569
|
+
set(value) {
|
|
570
|
+
this.#safe_get_cached_query()?.set(value);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** @type {Query<T>['withOverride']} */
|
|
574
|
+
withOverride(fn) {
|
|
575
|
+
const entry = this.#get_or_create_cache_entry();
|
|
576
|
+
const override = entry.resource.withOverride(fn);
|
|
577
|
+
|
|
578
|
+
const release = /** @type {(() => void) & { [QUERY_OVERRIDE_KEY]: string }} */ (
|
|
579
|
+
() => {
|
|
580
|
+
override();
|
|
581
|
+
this.#release(entry, false)();
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
Object.defineProperty(release, QUERY_OVERRIDE_KEY, { value: override[QUERY_OVERRIDE_KEY] });
|
|
586
|
+
|
|
587
|
+
return release;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** @type {Query<T>['then']} */
|
|
591
|
+
get then() {
|
|
592
|
+
const cached = this.#get_cached_query();
|
|
593
|
+
return cached.then.bind(cached);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** @type {Query<T>['catch']} */
|
|
597
|
+
get catch() {
|
|
598
|
+
const cached = this.#get_cached_query();
|
|
599
|
+
return cached.catch.bind(cached);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** @type {Query<T>['finally']} */
|
|
603
|
+
get finally() {
|
|
604
|
+
const cached = this.#get_cached_query();
|
|
605
|
+
return cached.finally.bind(cached);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
get [Symbol.toStringTag]() {
|
|
609
|
+
return 'QueryProxy';
|
|
610
|
+
}
|
|
341
611
|
}
|