@sveltejs/kit 2.60.1 → 2.61.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 +8 -9
- package/src/core/postbuild/analyse.js +1 -3
- package/src/core/sync/create_manifest_data/conflict.js +72 -0
- package/src/core/sync/create_manifest_data/index.js +1 -65
- package/src/core/sync/write_non_ambient.js +2 -2
- package/src/core/sync/write_types/index.js +1 -1
- package/src/exports/public.d.ts +23 -30
- package/src/exports/vite/build/build_server.js +36 -13
- package/src/exports/vite/dev/index.js +4 -2
- package/src/exports/vite/index.js +18 -16
- package/src/runtime/app/server/index.js +1 -2
- package/src/runtime/app/server/remote/form.js +10 -0
- package/src/runtime/app/server/remote/query.js +100 -36
- package/src/runtime/client/client.js +13 -8
- package/src/runtime/client/remote-functions/cache.svelte.js +157 -0
- package/src/runtime/client/remote-functions/form.svelte.js +235 -196
- package/src/runtime/client/remote-functions/index.js +2 -2
- package/src/runtime/client/remote-functions/prerender.svelte.js +1 -2
- package/src/runtime/client/remote-functions/query/cache.js +4 -0
- package/src/runtime/client/remote-functions/query/index.js +48 -0
- package/src/runtime/client/remote-functions/query/instance.svelte.js +249 -0
- package/src/runtime/client/remote-functions/query/proxy.js +156 -0
- package/src/runtime/client/remote-functions/query-batch.svelte.js +1 -1
- package/src/runtime/client/remote-functions/query-live/cache.js +4 -0
- package/src/runtime/client/remote-functions/query-live/index.js +31 -0
- package/src/runtime/client/remote-functions/{query-live.svelte.js → query-live/instance.svelte.js} +61 -310
- package/src/runtime/client/remote-functions/query-live/iterator.js +91 -0
- package/src/runtime/client/remote-functions/query-live/proxy.js +144 -0
- package/src/runtime/client/remote-functions/shared.svelte.js +53 -6
- package/src/runtime/client/utils.js +1 -1
- package/src/runtime/form-utils.js +7 -16
- package/src/runtime/server/index.js +2 -3
- package/src/runtime/server/page/actions.js +2 -9
- package/src/runtime/server/page/csp.js +3 -4
- package/src/runtime/server/page/render.js +13 -14
- package/src/runtime/server/respond.js +60 -36
- package/src/runtime/server/utils.js +23 -3
- package/src/types/global-private.d.ts +5 -0
- package/src/types/internal.d.ts +29 -6
- package/src/utils/routing.js +3 -1
- package/src/utils/shared-iterator.js +213 -0
- package/src/version.js +1 -1
- package/types/index.d.ts +27 -31
- package/types/index.d.ts.map +1 -1
- package/src/runtime/client/remote-functions/query.svelte.js +0 -512
package/src/runtime/client/remote-functions/{query-live.svelte.js → query-live/instance.svelte.js}
RENAMED
|
@@ -1,143 +1,18 @@
|
|
|
1
|
-
/** @import {
|
|
2
|
-
|
|
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';
|
|
1
|
+
/** @import { PromiseWithResolvers } from '../../../../utils/promise.js' */
|
|
2
|
+
import { app } from '../../client.js';
|
|
12
3
|
import * as devalue from 'devalue';
|
|
13
4
|
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
5
|
+
import { noop, once } from '../../../../utils/functions.js';
|
|
6
|
+
import { with_resolvers } from '../../../../utils/promise.js';
|
|
7
|
+
import { SharedIterator } from '../../../../utils/shared-iterator.js';
|
|
17
8
|
import { tick } from 'svelte';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
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
|
-
}
|
|
9
|
+
import { unfriendly_hydratable } from '../../../shared.js';
|
|
10
|
+
import { create_live_iterator } from './iterator.js';
|
|
137
11
|
|
|
138
12
|
/**
|
|
139
13
|
* @template T
|
|
140
14
|
* @implements {Promise<T>}
|
|
15
|
+
* @implements {AsyncIterable<T>}
|
|
141
16
|
*/
|
|
142
17
|
export class LiveQuery {
|
|
143
18
|
#id;
|
|
@@ -170,6 +45,15 @@ export class LiveQuery {
|
|
|
170
45
|
#interrupt = null;
|
|
171
46
|
#attempt = 0;
|
|
172
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Fan-out for `for await` consumers attached to this LiveQuery's shared
|
|
50
|
+
* stream. New subscribers see the most-recently-emitted value (if any) as
|
|
51
|
+
* their first yield. Subsequent yields fire whenever `set()` is called.
|
|
52
|
+
*
|
|
53
|
+
* @type {SharedIterator<T>}
|
|
54
|
+
*/
|
|
55
|
+
#fan_out = new SharedIterator();
|
|
56
|
+
|
|
173
57
|
/** @type {Promise<T>['then']} */
|
|
174
58
|
// @ts-expect-error TS doesn't understand that the promise returns something
|
|
175
59
|
#then = $derived.by(() => {
|
|
@@ -257,6 +141,7 @@ export class LiveQuery {
|
|
|
257
141
|
}
|
|
258
142
|
|
|
259
143
|
this.#done = true;
|
|
144
|
+
this.#fan_out.done();
|
|
260
145
|
} catch (error) {
|
|
261
146
|
if (controller.signal.aborted) break;
|
|
262
147
|
|
|
@@ -272,7 +157,6 @@ export class LiveQuery {
|
|
|
272
157
|
if (!this.#ready) {
|
|
273
158
|
// If we haven't successfully connected and received a value yet, surface the error
|
|
274
159
|
this.fail(error);
|
|
275
|
-
this.#done = true;
|
|
276
160
|
on_connect_failed(error);
|
|
277
161
|
break;
|
|
278
162
|
}
|
|
@@ -280,7 +164,6 @@ export class LiveQuery {
|
|
|
280
164
|
if (error instanceof HttpError) {
|
|
281
165
|
// Server intentionally sent an error. Surface it and stop.
|
|
282
166
|
this.fail(error);
|
|
283
|
-
this.#done = true;
|
|
284
167
|
break;
|
|
285
168
|
}
|
|
286
169
|
|
|
@@ -349,9 +232,38 @@ export class LiveQuery {
|
|
|
349
232
|
window.removeEventListener('pageshow', this.#on_pageshow);
|
|
350
233
|
}
|
|
351
234
|
|
|
235
|
+
this.#fan_out.done();
|
|
236
|
+
|
|
352
237
|
void this.#interrupt?.();
|
|
353
238
|
}
|
|
354
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Iterate the stream of values yielded by this live query. Multiple
|
|
242
|
+
* iterators share the underlying connection; the most-recently emitted value
|
|
243
|
+
* (if any) is yielded first to each new iterator, mirroring the semantics
|
|
244
|
+
* of awaiting the query directly.
|
|
245
|
+
*
|
|
246
|
+
* Backpressure note: if values arrive faster than the consumer drains, only
|
|
247
|
+
* the latest pending value is kept. This matches the reactive `.current`
|
|
248
|
+
* semantics — live streams are not event logs.
|
|
249
|
+
*
|
|
250
|
+
* @returns {AsyncGenerator<T, void, void>}
|
|
251
|
+
*/
|
|
252
|
+
[Symbol.asyncIterator]() {
|
|
253
|
+
this.#start();
|
|
254
|
+
|
|
255
|
+
// Seed the new iterator with the current value (if any) so the first
|
|
256
|
+
// `.next()` resolves synchronously — mirroring `await liveQuery()`
|
|
257
|
+
// semantics. If the query has hard-failed, `#fan_out` is already closed
|
|
258
|
+
// with the terminal error and `subscribe()` returns an iterator whose
|
|
259
|
+
// first `.next()` rejects.
|
|
260
|
+
return this.#fan_out.subscribe(
|
|
261
|
+
this.#ready && this.#error === undefined
|
|
262
|
+
? { initial_value: { value: /** @type {T} */ (this.#raw) } }
|
|
263
|
+
: undefined
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
355
267
|
get then() {
|
|
356
268
|
this.#start();
|
|
357
269
|
return this.#then;
|
|
@@ -419,6 +331,10 @@ export class LiveQuery {
|
|
|
419
331
|
promise.catch(noop);
|
|
420
332
|
this.#done = false;
|
|
421
333
|
this.#attempt = 0;
|
|
334
|
+
// The previous fan-out may have been closed by `done()`/`fail()`. Future
|
|
335
|
+
// `for await` consumers need a fresh, open fan-out attached to the new
|
|
336
|
+
// `#main` lifetime.
|
|
337
|
+
this.#fan_out = new SharedIterator();
|
|
422
338
|
this.#main({ on_connect, on_connect_failed }).catch(noop);
|
|
423
339
|
await promise;
|
|
424
340
|
}
|
|
@@ -437,12 +353,20 @@ export class LiveQuery {
|
|
|
437
353
|
} else {
|
|
438
354
|
this.#promise = Promise.resolve();
|
|
439
355
|
}
|
|
356
|
+
|
|
357
|
+
this.#fan_out.push(value);
|
|
440
358
|
}
|
|
441
359
|
|
|
442
360
|
/** @param {unknown} error */
|
|
443
361
|
fail(error) {
|
|
444
362
|
this.#loading = false;
|
|
445
363
|
this.#error = error;
|
|
364
|
+
// `fail` is terminal — once a live query has hard-failed, the only way to start
|
|
365
|
+
// streaming again is via `reconnect()`. Mark it done and abort any in-flight
|
|
366
|
+
// request so that callers from outside the main loop (e.g. `apply_reconnections`)
|
|
367
|
+
// don't leave the loop spinning.
|
|
368
|
+
this.#done = true;
|
|
369
|
+
void this.#interrupt?.();
|
|
446
370
|
|
|
447
371
|
if (this.#reject_first) {
|
|
448
372
|
this.#reject_first(error);
|
|
@@ -453,184 +377,11 @@ export class LiveQuery {
|
|
|
453
377
|
promise.catch(noop);
|
|
454
378
|
this.#promise = promise;
|
|
455
379
|
}
|
|
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
380
|
|
|
489
|
-
|
|
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);
|
|
381
|
+
this.#fan_out.fail(error);
|
|
631
382
|
}
|
|
632
383
|
|
|
633
384
|
get [Symbol.toStringTag]() {
|
|
634
|
-
return '
|
|
385
|
+
return 'LiveQuery';
|
|
635
386
|
}
|
|
636
387
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { app_dir, base } from '$app/paths/internal/client';
|
|
2
|
+
import { app } from '../../client.js';
|
|
3
|
+
import { get_remote_request_headers, handle_side_channel_response } from '../shared.svelte.js';
|
|
4
|
+
import * as devalue from 'devalue';
|
|
5
|
+
import { HttpError } from '@sveltejs/kit/internal';
|
|
6
|
+
import { noop } from '../../../../utils/functions.js';
|
|
7
|
+
import { read_ndjson } from '../../ndjson.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {Response} response
|
|
11
|
+
* @returns {Promise<ReadableStreamDefaultReader<Uint8Array>>}
|
|
12
|
+
*/
|
|
13
|
+
async function get_stream_reader(response) {
|
|
14
|
+
const content_type = response.headers.get('content-type') ?? '';
|
|
15
|
+
|
|
16
|
+
if (response.ok && content_type.includes('application/json')) {
|
|
17
|
+
// we can end up here if we e.g. redirect in `handle`
|
|
18
|
+
const result = await response.json();
|
|
19
|
+
await handle_side_channel_response(result);
|
|
20
|
+
throw new HttpError(500, 'Invalid query.live response');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const result = await response.json().catch(() => ({
|
|
25
|
+
type: 'error',
|
|
26
|
+
status: response.status,
|
|
27
|
+
error: response.statusText
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
throw new HttpError(result.status ?? response.status ?? 500, result.error);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!response.body) {
|
|
34
|
+
throw new Error('Expected query.live response body to be a ReadableStream');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return response.body.getReader();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Yields deserialized results from a ReadableStream of newline-delimited JSON
|
|
42
|
+
* @param {ReadableStreamDefaultReader<Uint8Array>} reader
|
|
43
|
+
*/
|
|
44
|
+
async function* read_live_ndjson(reader) {
|
|
45
|
+
for await (const node of read_ndjson(reader)) {
|
|
46
|
+
if (node.type === 'result') {
|
|
47
|
+
yield devalue.parse(node.result, app.decoders);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await handle_side_channel_response(node);
|
|
52
|
+
throw new HttpError(500, 'Invalid query.live response');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @template T
|
|
58
|
+
* @param {string} id
|
|
59
|
+
* @param {string} payload
|
|
60
|
+
* @param {AbortController} [controller]
|
|
61
|
+
* @param {() => void} [on_connect]
|
|
62
|
+
* @returns {AsyncGenerator<T>}
|
|
63
|
+
*/
|
|
64
|
+
export async function* create_live_iterator(
|
|
65
|
+
id,
|
|
66
|
+
payload,
|
|
67
|
+
controller = new AbortController(),
|
|
68
|
+
on_connect = noop
|
|
69
|
+
) {
|
|
70
|
+
const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
|
|
71
|
+
/** @type {ReadableStreamDefaultReader<Uint8Array> | null} */
|
|
72
|
+
let reader = null;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
headers: get_remote_request_headers(),
|
|
77
|
+
signal: controller.signal
|
|
78
|
+
});
|
|
79
|
+
reader = await get_stream_reader(response);
|
|
80
|
+
|
|
81
|
+
on_connect();
|
|
82
|
+
|
|
83
|
+
yield* read_live_ndjson(reader);
|
|
84
|
+
} finally {
|
|
85
|
+
try {
|
|
86
|
+
await reader?.cancel();
|
|
87
|
+
} catch {
|
|
88
|
+
// already closed
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { app, live_query_map } from '../../client.js';
|
|
2
|
+
import { pin_in_effect, pin_while_resolving, QUERY_RESOURCE_KEY } from '../shared.svelte.js';
|
|
3
|
+
import { create_remote_key, stringify_remote_arg } from '../../../shared.js';
|
|
4
|
+
import { LiveQuery } from './instance.svelte.js';
|
|
5
|
+
import { cache } from './cache.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @template T
|
|
9
|
+
* @implements {Promise<T>}
|
|
10
|
+
* @implements {AsyncIterable<T>}
|
|
11
|
+
*/
|
|
12
|
+
export class LiveQueryProxy {
|
|
13
|
+
#key;
|
|
14
|
+
#id;
|
|
15
|
+
#payload;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} id
|
|
19
|
+
* @param {any} arg
|
|
20
|
+
*/
|
|
21
|
+
constructor(id, arg) {
|
|
22
|
+
this.#id = id;
|
|
23
|
+
this.#payload = stringify_remote_arg(arg, app.hooks.transport);
|
|
24
|
+
this.#key = create_remote_key(id, this.#payload);
|
|
25
|
+
Object.defineProperty(this, QUERY_RESOURCE_KEY, { value: this.#key });
|
|
26
|
+
|
|
27
|
+
// Capture key/payload in locals so the create_resource closure doesn't capture
|
|
28
|
+
// `this` (the LiveQueryProxy), which would prevent the FinalizationRegistry from
|
|
29
|
+
// observing the proxy as unreachable and so leak the first proxy for a given key.
|
|
30
|
+
const key = this.#key;
|
|
31
|
+
const payload = this.#payload;
|
|
32
|
+
const entry = cache.ensure_entry(this.#id, payload, () => new LiveQuery(id, key, payload));
|
|
33
|
+
|
|
34
|
+
cache.ref(this, entry, this.#id, payload);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#get_cache_entry() {
|
|
38
|
+
const cached = live_query_map.get(this.#id)?.get(this.#payload);
|
|
39
|
+
|
|
40
|
+
if (!cached) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'No cached query found. This should be impossible. Please file a bug report.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cached;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#get_cached_query() {
|
|
50
|
+
return this.#get_cache_entry().resource;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get current() {
|
|
54
|
+
return this.#get_cached_query().current;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get error() {
|
|
58
|
+
return this.#get_cached_query().error;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get loading() {
|
|
62
|
+
return this.#get_cached_query().loading;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get ready() {
|
|
66
|
+
return this.#get_cached_query().ready;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get connected() {
|
|
70
|
+
return this.#get_cached_query().connected;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get done() {
|
|
74
|
+
return this.#get_cached_query().done;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @deprecated Use `for await (const value of liveQuery())` instead.
|
|
79
|
+
* @returns {AsyncGenerator<T>}
|
|
80
|
+
*/
|
|
81
|
+
run() {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'`.run()` has been removed from live queries. Use `for await (const value of liveQuery())` instead.'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
reconnect() {
|
|
88
|
+
return this.#get_cached_query().reconnect();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @returns {AsyncGenerator<T, void, void>}
|
|
93
|
+
*/
|
|
94
|
+
async *[Symbol.asyncIterator]() {
|
|
95
|
+
const entry = this.#get_cache_entry();
|
|
96
|
+
const release = cache.manual_ref(entry, this.#id, this.#payload);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
yield* entry.resource;
|
|
100
|
+
} finally {
|
|
101
|
+
release();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get then() {
|
|
106
|
+
pin_in_effect(live_query_map, cache, this.#id, this.#payload);
|
|
107
|
+
const cached = this.#get_cached_query();
|
|
108
|
+
return pin_while_resolving(
|
|
109
|
+
live_query_map,
|
|
110
|
+
cache,
|
|
111
|
+
this.#id,
|
|
112
|
+
this.#payload,
|
|
113
|
+
cached.then.bind(cached)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get catch() {
|
|
118
|
+
pin_in_effect(live_query_map, cache, this.#id, this.#payload);
|
|
119
|
+
const cached = this.#get_cached_query();
|
|
120
|
+
return pin_while_resolving(
|
|
121
|
+
live_query_map,
|
|
122
|
+
cache,
|
|
123
|
+
this.#id,
|
|
124
|
+
this.#payload,
|
|
125
|
+
cached.catch.bind(cached)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get finally() {
|
|
130
|
+
pin_in_effect(live_query_map, cache, this.#id, this.#payload);
|
|
131
|
+
const cached = this.#get_cached_query();
|
|
132
|
+
return pin_while_resolving(
|
|
133
|
+
live_query_map,
|
|
134
|
+
cache,
|
|
135
|
+
this.#id,
|
|
136
|
+
this.#payload,
|
|
137
|
+
cached.finally.bind(cached)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get [Symbol.toStringTag]() {
|
|
142
|
+
return 'LiveQueryProxy';
|
|
143
|
+
}
|
|
144
|
+
}
|