@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,636 @@
|
|
|
1
|
+
/** @import { RemoteLiveQuery, RemoteLiveQueryFunction } from '@sveltejs/kit' */
|
|
2
|
+
/** @import { PromiseWithResolvers } from '../../../utils/promise.js' */
|
|
3
|
+
import { app_dir, base } from '$app/paths/internal/client';
|
|
4
|
+
import { app, live_query_map } from '../client.js';
|
|
5
|
+
import {
|
|
6
|
+
get_remote_request_headers,
|
|
7
|
+
handle_side_channel_response,
|
|
8
|
+
is_in_effect,
|
|
9
|
+
QUERY_FUNCTION_ID,
|
|
10
|
+
QUERY_RESOURCE_KEY
|
|
11
|
+
} from './shared.svelte.js';
|
|
12
|
+
import * as devalue from 'devalue';
|
|
13
|
+
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
14
|
+
import { DEV } from 'esm-env';
|
|
15
|
+
import { noop, once } from '../../../utils/functions.js';
|
|
16
|
+
import { with_resolvers } from '../../../utils/promise.js';
|
|
17
|
+
import { tick } from 'svelte';
|
|
18
|
+
import { create_remote_key, stringify_remote_arg, unfriendly_hydratable } from '../../shared.js';
|
|
19
|
+
import { read_ndjson } from '../ndjson.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @template T
|
|
23
|
+
* @typedef {{
|
|
24
|
+
* count: number;
|
|
25
|
+
* resource: LiveQuery<T>;
|
|
26
|
+
* cleanup: () => void;
|
|
27
|
+
* }} RemoteLiveQueryCacheEntry
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} id
|
|
32
|
+
* @returns {RemoteLiveQueryFunction<any, any>}
|
|
33
|
+
*/
|
|
34
|
+
export function query_live(id) {
|
|
35
|
+
if (DEV) {
|
|
36
|
+
// If this reruns as part of HMR, refresh the query
|
|
37
|
+
const entries = live_query_map.get(id);
|
|
38
|
+
|
|
39
|
+
if (entries) {
|
|
40
|
+
for (const entry of entries.values()) {
|
|
41
|
+
void entry.resource.reconnect();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @type {RemoteLiveQueryFunction<any, any>} */
|
|
47
|
+
const wrapper = (arg) => /** @type {RemoteLiveQuery<any>} */ (new LiveQueryProxy(id, arg));
|
|
48
|
+
|
|
49
|
+
Object.defineProperty(wrapper, QUERY_FUNCTION_ID, { value: id });
|
|
50
|
+
|
|
51
|
+
return wrapper;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {Response} response
|
|
56
|
+
* @returns {Promise<ReadableStreamDefaultReader<Uint8Array>>}
|
|
57
|
+
*/
|
|
58
|
+
async function get_stream_reader(response) {
|
|
59
|
+
const content_type = response.headers.get('content-type') ?? '';
|
|
60
|
+
|
|
61
|
+
if (response.ok && content_type.includes('application/json')) {
|
|
62
|
+
// we can end up here if we e.g. redirect in `handle`
|
|
63
|
+
const result = await response.json();
|
|
64
|
+
await handle_side_channel_response(result);
|
|
65
|
+
throw new HttpError(500, 'Invalid query.live response');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const result = await response.json().catch(() => ({
|
|
70
|
+
type: 'error',
|
|
71
|
+
status: response.status,
|
|
72
|
+
error: response.statusText
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
throw new HttpError(result.status ?? response.status ?? 500, result.error);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!response.body) {
|
|
79
|
+
throw new Error('Expected query.live response body to be a ReadableStream');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return response.body.getReader();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Yields deserialized results from a ReadableStream of newline-delimited JSON
|
|
87
|
+
* @param {ReadableStreamDefaultReader<Uint8Array>} reader
|
|
88
|
+
*/
|
|
89
|
+
async function* read_live_ndjson(reader) {
|
|
90
|
+
for await (const node of read_ndjson(reader)) {
|
|
91
|
+
if (node.type === 'result') {
|
|
92
|
+
yield devalue.parse(node.result, app.decoders);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await handle_side_channel_response(node);
|
|
97
|
+
throw new HttpError(500, 'Invalid query.live response');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @template T
|
|
103
|
+
* @param {string} id
|
|
104
|
+
* @param {string} payload
|
|
105
|
+
* @param {AbortController} [controller]
|
|
106
|
+
* @param {() => void} [on_connect]
|
|
107
|
+
* @returns {AsyncGenerator<T>}
|
|
108
|
+
*/
|
|
109
|
+
export async function* create_live_iterator(
|
|
110
|
+
id,
|
|
111
|
+
payload,
|
|
112
|
+
controller = new AbortController(),
|
|
113
|
+
on_connect = noop
|
|
114
|
+
) {
|
|
115
|
+
const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
|
|
116
|
+
/** @type {ReadableStreamDefaultReader<Uint8Array> | null} */
|
|
117
|
+
let reader = null;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(url, {
|
|
121
|
+
headers: get_remote_request_headers(),
|
|
122
|
+
signal: controller.signal
|
|
123
|
+
});
|
|
124
|
+
reader = await get_stream_reader(response);
|
|
125
|
+
|
|
126
|
+
on_connect();
|
|
127
|
+
|
|
128
|
+
yield* read_live_ndjson(reader);
|
|
129
|
+
} finally {
|
|
130
|
+
try {
|
|
131
|
+
await reader?.cancel();
|
|
132
|
+
} catch {
|
|
133
|
+
// already closed
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @template T
|
|
140
|
+
* @implements {Promise<T>}
|
|
141
|
+
*/
|
|
142
|
+
export class LiveQuery {
|
|
143
|
+
#id;
|
|
144
|
+
#payload;
|
|
145
|
+
#loading = $state(true);
|
|
146
|
+
#ready = $state(false);
|
|
147
|
+
/** Is there a current connection to the server? */
|
|
148
|
+
#connected = $state(false);
|
|
149
|
+
/**
|
|
150
|
+
* Have we been told by the server that we have completed iteration and are done?
|
|
151
|
+
* When this is `true`, the only way to start live-updating again is to call `.reconnect()`.
|
|
152
|
+
*/
|
|
153
|
+
#done = $state(false);
|
|
154
|
+
/** @type {T | undefined} */
|
|
155
|
+
#raw = $state.raw();
|
|
156
|
+
/** @type {any} */
|
|
157
|
+
#error = $state.raw(undefined);
|
|
158
|
+
/** @type {Promise<void>} */
|
|
159
|
+
#promise;
|
|
160
|
+
/** @type {((value: void) => void) | null} */
|
|
161
|
+
#resolve_first = null;
|
|
162
|
+
/** @type {((reason?: any) => void) | null} */
|
|
163
|
+
#reject_first = null;
|
|
164
|
+
/**
|
|
165
|
+
* Interrupt the main loop, causing the current connection (if active) to be closed
|
|
166
|
+
* and unscheduling any future automatic reconnection attempts. Returns a promise that
|
|
167
|
+
* resolves when the main loop has fully stopped.
|
|
168
|
+
* @type {(() => Promise<void>) | null}
|
|
169
|
+
*/
|
|
170
|
+
#interrupt = null;
|
|
171
|
+
#attempt = 0;
|
|
172
|
+
|
|
173
|
+
/** @type {Promise<T>['then']} */
|
|
174
|
+
// @ts-expect-error TS doesn't understand that the promise returns something
|
|
175
|
+
#then = $derived.by(() => {
|
|
176
|
+
const p = /** @type {Promise<T>} */ (this.#promise);
|
|
177
|
+
|
|
178
|
+
return (resolve, reject) => {
|
|
179
|
+
const result = p.then(tick).then(() => /** @type {T} */ (this.#raw));
|
|
180
|
+
|
|
181
|
+
if (resolve || reject) {
|
|
182
|
+
return result.then(resolve, reject);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return result;
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} id
|
|
191
|
+
* @param {string} key
|
|
192
|
+
* @param {string} payload
|
|
193
|
+
*/
|
|
194
|
+
constructor(id, key, payload) {
|
|
195
|
+
this.#id = id;
|
|
196
|
+
this.#payload = payload;
|
|
197
|
+
|
|
198
|
+
// the semantics of awaiting a live query are a bit weird, but it's basically:
|
|
199
|
+
// - It's a promise that resolves to the first value from the server
|
|
200
|
+
// - Thereafter, it's a promise that immediately resolves to the current value
|
|
201
|
+
const { promise, resolve, reject } = with_resolvers();
|
|
202
|
+
this.#promise = $state.raw(promise);
|
|
203
|
+
this.#resolve_first = resolve;
|
|
204
|
+
this.#reject_first = reject;
|
|
205
|
+
|
|
206
|
+
const serialized = unfriendly_hydratable(key, () => undefined);
|
|
207
|
+
if (serialized !== undefined) {
|
|
208
|
+
this.set(devalue.parse(serialized, app.decoders));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {number} attempt
|
|
214
|
+
* @returns {number}
|
|
215
|
+
*/
|
|
216
|
+
static #calculate_retry_delay(attempt) {
|
|
217
|
+
const base_delay = Math.min(250 * 2 ** attempt, 10_000);
|
|
218
|
+
const jitter = base_delay * (Math.random() * 0.4 - 0.2);
|
|
219
|
+
return Math.max(0, Math.round(base_delay + jitter));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** @param {{ on_connect: () => void, on_connect_failed: (reason: any) => void }} [on_connect_handlers] */
|
|
223
|
+
async #main({ on_connect, on_connect_failed } = { on_connect: noop, on_connect_failed: noop }) {
|
|
224
|
+
// this means we're already running the main loop
|
|
225
|
+
if (this.#interrupt) return;
|
|
226
|
+
|
|
227
|
+
/** @type {PromiseWithResolvers<void>} */
|
|
228
|
+
const { promise: stopped, resolve: on_stop } = with_resolvers();
|
|
229
|
+
|
|
230
|
+
while (!this.#done) {
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
|
|
233
|
+
this.#interrupt = () => {
|
|
234
|
+
controller.abort();
|
|
235
|
+
return stopped;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const generator = create_live_iterator(this.#id, this.#payload, controller, () => {
|
|
239
|
+
this.#connected = true;
|
|
240
|
+
this.#attempt = 0;
|
|
241
|
+
on_connect();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const { done, value } = await generator.next();
|
|
246
|
+
|
|
247
|
+
// TODO how much special handling does this need?
|
|
248
|
+
// should we even try to reconnect if this is the case?
|
|
249
|
+
if (done && !this.#ready) {
|
|
250
|
+
throw new Error('Live query completed before yielding a value');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.set(value);
|
|
254
|
+
|
|
255
|
+
for await (const value of generator) {
|
|
256
|
+
this.set(value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.#done = true;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (controller.signal.aborted) break;
|
|
262
|
+
|
|
263
|
+
if (error instanceof Redirect) {
|
|
264
|
+
// goto() was already called by handle_side_channel_response.
|
|
265
|
+
// Reconnect promptly
|
|
266
|
+
// TODO this really needs to hook into the router so the reconnect can
|
|
267
|
+
// finish before applying the navigation
|
|
268
|
+
this.#attempt = 0;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!this.#ready) {
|
|
273
|
+
// If we haven't successfully connected and received a value yet, surface the error
|
|
274
|
+
this.fail(error);
|
|
275
|
+
this.#done = true;
|
|
276
|
+
on_connect_failed(error);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (error instanceof HttpError) {
|
|
281
|
+
// Server intentionally sent an error. Surface it and stop.
|
|
282
|
+
this.fail(error);
|
|
283
|
+
this.#done = true;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Network/transport error — transient.
|
|
288
|
+
// Preserve last good value (or keep initial promise pending).
|
|
289
|
+
|
|
290
|
+
if (typeof navigator !== 'undefined' && !navigator.onLine) break;
|
|
291
|
+
|
|
292
|
+
const delay = LiveQuery.#calculate_retry_delay(this.#attempt++);
|
|
293
|
+
/** @type {boolean} */
|
|
294
|
+
const interrupted = await new Promise((resolve) => {
|
|
295
|
+
this.#interrupt = () => {
|
|
296
|
+
resolve(true);
|
|
297
|
+
return stopped;
|
|
298
|
+
};
|
|
299
|
+
setTimeout(() => resolve(false), delay);
|
|
300
|
+
});
|
|
301
|
+
if (interrupted) break;
|
|
302
|
+
} finally {
|
|
303
|
+
this.#connected = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.#interrupt = null;
|
|
308
|
+
on_stop();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#on_online = () => {
|
|
312
|
+
if (this.#done) return;
|
|
313
|
+
if (this.#interrupt) return;
|
|
314
|
+
this.#main().catch(noop);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
#on_offline = () => {
|
|
318
|
+
void this.#interrupt?.();
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
#on_pagehide = () => {
|
|
322
|
+
void this.#interrupt?.();
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
#on_pageshow = (/** @type {PageTransitionEvent} */ e) => {
|
|
326
|
+
if (e.persisted) {
|
|
327
|
+
this.#on_online();
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
#start = once(() => {
|
|
332
|
+
if (typeof window !== 'undefined') {
|
|
333
|
+
window.addEventListener('online', this.#on_online);
|
|
334
|
+
window.addEventListener('offline', this.#on_offline);
|
|
335
|
+
window.addEventListener('pagehide', this.#on_pagehide);
|
|
336
|
+
window.addEventListener('beforeunload', this.#on_pagehide);
|
|
337
|
+
window.addEventListener('pageshow', this.#on_pageshow);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.#main().catch(noop);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
destroy() {
|
|
344
|
+
if (typeof window !== 'undefined') {
|
|
345
|
+
window.removeEventListener('online', this.#on_online);
|
|
346
|
+
window.removeEventListener('offline', this.#on_offline);
|
|
347
|
+
window.removeEventListener('pagehide', this.#on_pagehide);
|
|
348
|
+
window.removeEventListener('beforeunload', this.#on_pagehide);
|
|
349
|
+
window.removeEventListener('pageshow', this.#on_pageshow);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
void this.#interrupt?.();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
get then() {
|
|
356
|
+
this.#start();
|
|
357
|
+
return this.#then;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
get catch() {
|
|
361
|
+
this.#start();
|
|
362
|
+
this.#then;
|
|
363
|
+
return (/** @type {any} */ reject) => {
|
|
364
|
+
return this.#then(undefined, reject);
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
get finally() {
|
|
369
|
+
this.#start();
|
|
370
|
+
this.#then;
|
|
371
|
+
return (/** @type {any} */ fn) => {
|
|
372
|
+
return this.#then(
|
|
373
|
+
(value) => {
|
|
374
|
+
fn();
|
|
375
|
+
return value;
|
|
376
|
+
},
|
|
377
|
+
(error) => {
|
|
378
|
+
fn();
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
get current() {
|
|
386
|
+
this.#start();
|
|
387
|
+
return this.#raw;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
get error() {
|
|
391
|
+
this.#start();
|
|
392
|
+
return this.#error;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
get loading() {
|
|
396
|
+
this.#start();
|
|
397
|
+
return this.#loading;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
get ready() {
|
|
401
|
+
this.#start();
|
|
402
|
+
return this.#ready;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
get connected() {
|
|
406
|
+
this.#start();
|
|
407
|
+
return this.#connected;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
get done() {
|
|
411
|
+
this.#start();
|
|
412
|
+
return this.#done;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async reconnect() {
|
|
416
|
+
await this.#interrupt?.();
|
|
417
|
+
/** @type {PromiseWithResolvers<void>} */
|
|
418
|
+
const { promise, resolve: on_connect, reject: on_connect_failed } = with_resolvers();
|
|
419
|
+
promise.catch(noop);
|
|
420
|
+
this.#done = false;
|
|
421
|
+
this.#attempt = 0;
|
|
422
|
+
this.#main({ on_connect, on_connect_failed }).catch(noop);
|
|
423
|
+
await promise;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** @param {T} value */
|
|
427
|
+
set(value) {
|
|
428
|
+
this.#ready = true;
|
|
429
|
+
this.#loading = false;
|
|
430
|
+
this.#error = undefined;
|
|
431
|
+
this.#raw = value;
|
|
432
|
+
|
|
433
|
+
if (this.#resolve_first) {
|
|
434
|
+
this.#resolve_first();
|
|
435
|
+
this.#resolve_first = null;
|
|
436
|
+
this.#reject_first = null;
|
|
437
|
+
} else {
|
|
438
|
+
this.#promise = Promise.resolve();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** @param {unknown} error */
|
|
443
|
+
fail(error) {
|
|
444
|
+
this.#loading = false;
|
|
445
|
+
this.#error = error;
|
|
446
|
+
|
|
447
|
+
if (this.#reject_first) {
|
|
448
|
+
this.#reject_first(error);
|
|
449
|
+
this.#resolve_first = null;
|
|
450
|
+
this.#reject_first = null;
|
|
451
|
+
} else {
|
|
452
|
+
const promise = Promise.reject(error);
|
|
453
|
+
promise.catch(noop);
|
|
454
|
+
this.#promise = promise;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
get [Symbol.toStringTag]() {
|
|
459
|
+
return 'LiveQuery';
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* @template T
|
|
465
|
+
* @implements {Promise<T>}
|
|
466
|
+
*/
|
|
467
|
+
class LiveQueryProxy {
|
|
468
|
+
#key;
|
|
469
|
+
#id;
|
|
470
|
+
#payload;
|
|
471
|
+
#active = true;
|
|
472
|
+
#tracking = is_in_effect();
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* @param {string} id
|
|
476
|
+
* @param {any} arg
|
|
477
|
+
*/
|
|
478
|
+
constructor(id, arg) {
|
|
479
|
+
this.#id = id;
|
|
480
|
+
this.#payload = stringify_remote_arg(arg, app.hooks.transport);
|
|
481
|
+
this.#key = create_remote_key(id, this.#payload);
|
|
482
|
+
Object.defineProperty(this, QUERY_RESOURCE_KEY, { value: this.#key });
|
|
483
|
+
|
|
484
|
+
if (!this.#tracking) {
|
|
485
|
+
this.#active = false;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const entry = this.#get_or_create_cache_entry();
|
|
490
|
+
|
|
491
|
+
$effect.pre(() => () => {
|
|
492
|
+
const die = this.#release(entry);
|
|
493
|
+
void tick().then(die);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** @returns {RemoteLiveQueryCacheEntry<T>} */
|
|
498
|
+
#get_or_create_cache_entry() {
|
|
499
|
+
let query_instances = live_query_map.get(this.#id);
|
|
500
|
+
|
|
501
|
+
if (!query_instances) {
|
|
502
|
+
query_instances = new Map();
|
|
503
|
+
live_query_map.set(this.#id, query_instances);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let this_instance = query_instances.get(this.#payload);
|
|
507
|
+
|
|
508
|
+
if (!this_instance) {
|
|
509
|
+
const c = (this_instance = {
|
|
510
|
+
count: 0,
|
|
511
|
+
resource: /** @type {LiveQuery<T>} */ (/** @type {unknown} */ (null)),
|
|
512
|
+
cleanup: /** @type {() => void} */ (/** @type {unknown} */ (null))
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
c.cleanup = $effect.root(() => {
|
|
516
|
+
c.resource = new LiveQuery(this.#id, this.#key, this.#payload);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
query_instances.set(this.#payload, this_instance);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
this_instance.count += 1;
|
|
523
|
+
|
|
524
|
+
return this_instance;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* @param {RemoteLiveQueryCacheEntry<T>} entry
|
|
529
|
+
* @param {boolean} [deactivate]
|
|
530
|
+
*/
|
|
531
|
+
#release(entry, deactivate = true) {
|
|
532
|
+
this.#active &&= !deactivate;
|
|
533
|
+
entry.count -= 1;
|
|
534
|
+
|
|
535
|
+
return () => {
|
|
536
|
+
const query_instances = live_query_map.get(this.#id);
|
|
537
|
+
const this_instance = query_instances?.get(this.#payload);
|
|
538
|
+
|
|
539
|
+
if (this_instance?.count === 0) {
|
|
540
|
+
this_instance.resource.destroy();
|
|
541
|
+
this_instance.cleanup();
|
|
542
|
+
query_instances?.delete(this.#payload);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (query_instances?.size === 0) {
|
|
546
|
+
live_query_map.delete(this.#id);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#get_cached_query() {
|
|
552
|
+
if (!this.#tracking) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
'This live query was not created in a reactive context and is limited to calling `.run` and `.reconnect`.'
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!this.#active) {
|
|
559
|
+
throw new Error(
|
|
560
|
+
'This query instance is no longer active and can no longer be used for reactive state access. ' +
|
|
561
|
+
'This typically means you created the query in a tracking context and stashed it somewhere outside of a tracking context.'
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const cached = live_query_map.get(this.#id)?.get(this.#payload);
|
|
566
|
+
|
|
567
|
+
if (!cached) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
'No cached query found. This should be impossible. Please file a bug report.'
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return cached.resource;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#safe_get_cached_query() {
|
|
577
|
+
return live_query_map.get(this.#id)?.get(this.#payload)?.resource;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
get current() {
|
|
581
|
+
return this.#safe_get_cached_query()?.current;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
get error() {
|
|
585
|
+
return this.#safe_get_cached_query()?.error;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
get loading() {
|
|
589
|
+
return this.#safe_get_cached_query()?.loading ?? false;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
get ready() {
|
|
593
|
+
return this.#safe_get_cached_query()?.ready ?? false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
get connected() {
|
|
597
|
+
return this.#safe_get_cached_query()?.connected ?? false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
get done() {
|
|
601
|
+
return this.#safe_get_cached_query()?.done ?? false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
run() {
|
|
605
|
+
if (is_in_effect()) {
|
|
606
|
+
throw new Error(
|
|
607
|
+
'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'
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return create_live_iterator(this.#id, this.#payload);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
reconnect() {
|
|
615
|
+
return this.#safe_get_cached_query()?.reconnect() ?? Promise.resolve();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
get then() {
|
|
619
|
+
const cached = this.#get_cached_query();
|
|
620
|
+
return cached.then.bind(cached);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
get catch() {
|
|
624
|
+
const cached = this.#get_cached_query();
|
|
625
|
+
return cached.catch.bind(cached);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
get finally() {
|
|
629
|
+
const cached = this.#get_cached_query();
|
|
630
|
+
return cached.finally.bind(cached);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
get [Symbol.toStringTag]() {
|
|
634
|
+
return 'LiveQueryProxy';
|
|
635
|
+
}
|
|
636
|
+
}
|