@sveltejs/kit 3.0.0-next.1 → 3.0.0-next.3
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/core/postbuild/analyse.js +0 -8
- package/src/core/postbuild/prerender.js +2 -0
- package/src/core/sync/create_manifest_data/index.js +24 -1
- package/src/core/sync/write_env.js +2 -1
- package/src/exports/internal/env.js +1 -1
- package/src/exports/public.d.ts +1 -1
- package/src/exports/vite/build/build_server.js +47 -58
- package/src/exports/vite/build/remote.js +18 -11
- package/src/exports/vite/build/utils.js +0 -8
- package/src/exports/vite/index.js +221 -217
- package/src/exports/vite/static_analysis/index.js +2 -4
- package/src/exports/vite/static_analysis/types.d.ts +14 -0
- package/src/exports/vite/utils.js +1 -12
- package/src/runtime/app/server/remote/command.js +0 -3
- package/src/runtime/app/server/remote/form.js +18 -13
- package/src/runtime/app/server/remote/prerender.js +28 -34
- package/src/runtime/app/server/remote/query.js +105 -94
- package/src/runtime/app/server/remote/requested.js +14 -10
- package/src/runtime/app/server/remote/shared.js +25 -19
- package/src/runtime/client/client.js +19 -13
- package/src/runtime/client/ndjson.js +6 -33
- package/src/runtime/client/remote-functions/command.svelte.js +7 -32
- package/src/runtime/client/remote-functions/form.svelte.js +62 -82
- package/src/runtime/client/remote-functions/prerender.svelte.js +14 -6
- package/src/runtime/client/remote-functions/query/index.js +6 -14
- package/src/runtime/client/remote-functions/query/instance.svelte.js +20 -0
- package/src/runtime/client/remote-functions/query/proxy.js +3 -3
- package/src/runtime/client/remote-functions/query-batch.svelte.js +59 -68
- package/src/runtime/client/remote-functions/query-live/instance.svelte.js +21 -6
- package/src/runtime/client/remote-functions/query-live/iterator.js +36 -55
- package/src/runtime/client/remote-functions/shared.svelte.js +76 -59
- package/src/runtime/client/sse.js +32 -0
- package/src/runtime/client/stream.js +38 -0
- package/src/runtime/server/page/render.js +23 -80
- package/src/runtime/server/page/server_routing.js +20 -15
- package/src/runtime/server/remote.js +296 -204
- package/src/runtime/server/respond.js +4 -2
- package/src/runtime/shared.js +83 -13
- package/src/types/global-private.d.ts +3 -3
- package/src/types/internal.d.ts +54 -35
- package/src/utils/error.js +12 -0
- package/src/version.js +1 -1
- package/types/index.d.ts +17 -7
- package/types/index.d.ts.map +5 -3
|
@@ -1,12 +1,9 @@
|
|
|
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 {
|
|
5
|
-
import { get_remote_request_headers, QUERY_FUNCTION_ID } from './shared.svelte.js';
|
|
3
|
+
import { goto } from '../client.js';
|
|
4
|
+
import { get_remote_request_headers, QUERY_FUNCTION_ID, remote_request } from './shared.svelte.js';
|
|
6
5
|
import { QueryProxy } from './query/proxy.js';
|
|
7
|
-
import
|
|
8
|
-
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
9
|
-
import { hydratable } from 'svelte';
|
|
6
|
+
import { HttpError } from '@sveltejs/kit/internal';
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* @param {string} id
|
|
@@ -18,84 +15,78 @@ export function query_batch(id) {
|
|
|
18
15
|
|
|
19
16
|
/** @type {RemoteQueryFunction<any, any>} */
|
|
20
17
|
const wrapper = (arg) => {
|
|
21
|
-
return new QueryProxy(id, arg, async (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
batching.set(payload, entry);
|
|
18
|
+
return new QueryProxy(id, arg, async (payload) => {
|
|
19
|
+
return await new Promise((resolve, reject) => {
|
|
20
|
+
// create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
|
|
21
|
+
// is invoked multiple times with the same payload, so we need to deduplicate here
|
|
22
|
+
const entry = batching.get(payload) ?? [];
|
|
23
|
+
entry.push({ resolve, reject });
|
|
24
|
+
batching.set(payload, entry);
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
if (batching.size > 1) return;
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
// Do this here, after await Svelte' reactivity context is gone.
|
|
29
|
+
// TODO is it possible to have batches of the same key
|
|
30
|
+
// but in different forks/async contexts and in the same macrotask?
|
|
31
|
+
// If so this would potentially be buggy
|
|
32
|
+
const headers = {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
...get_remote_request_headers()
|
|
35
|
+
};
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
// Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
|
|
38
|
+
// and flushes could reveal more queries that should be batched.
|
|
39
|
+
setTimeout(async () => {
|
|
40
|
+
const batched = batching;
|
|
41
|
+
batching = new Map();
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
try {
|
|
44
|
+
const response = await remote_request(`${base}/${app_dir}/remote/${id}`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
payloads: Array.from(batched.keys())
|
|
48
|
+
}),
|
|
49
|
+
headers
|
|
50
|
+
});
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
52
|
+
if (response.redirect) {
|
|
53
|
+
await goto(response.redirect);
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
// settle all batched promises (with `undefined`, like a redirect
|
|
56
|
+
// from a non-batched query) so that callers don't hang forever
|
|
57
|
+
for (const resolvers of batched.values()) {
|
|
58
|
+
for (const { resolve } of resolvers) {
|
|
59
|
+
resolve(undefined);
|
|
60
|
+
}
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw new Redirect(307, result.location);
|
|
68
|
-
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
69
65
|
|
|
70
|
-
|
|
66
|
+
const results = response._;
|
|
67
|
+
let i = 0;
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let i = 0;
|
|
69
|
+
for (const resolvers of batched.values()) {
|
|
70
|
+
const result = results[i];
|
|
75
71
|
|
|
76
|
-
for (const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
resolve(results[i].data);
|
|
82
|
-
}
|
|
72
|
+
for (const { resolve, reject } of resolvers) {
|
|
73
|
+
if (result.type === 'error') {
|
|
74
|
+
reject(new HttpError(result.status, result.error));
|
|
75
|
+
} else {
|
|
76
|
+
resolve(result.data);
|
|
83
77
|
}
|
|
84
|
-
i++;
|
|
85
78
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
for (const resolvers of batched.values()) {
|
|
83
|
+
for (const { reject } of resolvers) {
|
|
84
|
+
reject(e);
|
|
92
85
|
}
|
|
93
86
|
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
87
|
+
}
|
|
88
|
+
}, 0);
|
|
96
89
|
});
|
|
97
|
-
|
|
98
|
-
return devalue.parse(serialized, app.decoders);
|
|
99
90
|
});
|
|
100
91
|
};
|
|
101
92
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as devalue from 'devalue';
|
|
1
|
+
import { query_responses } from '../../client.js';
|
|
3
2
|
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
4
3
|
import { noop, once } from '../../../../utils/functions.js';
|
|
5
4
|
import { SharedIterator } from '../../../../utils/shared-iterator.js';
|
|
6
|
-
import {
|
|
5
|
+
import { tick } from 'svelte';
|
|
7
6
|
import { create_live_iterator } from './iterator.js';
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -84,9 +83,25 @@ export class LiveQuery {
|
|
|
84
83
|
this.#resolve_first = resolve;
|
|
85
84
|
this.#reject_first = reject;
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
if (Object.hasOwn(query_responses, key)) {
|
|
87
|
+
const node = query_responses[key];
|
|
88
|
+
delete query_responses[key];
|
|
89
|
+
|
|
90
|
+
if (node.e) {
|
|
91
|
+
// the query failed during SSR — seed the failed state (mirroring `fail()`,
|
|
92
|
+
// minus its terminal `#done`), so the main loop still connects as usual
|
|
93
|
+
// and the query can recover
|
|
94
|
+
const error = new HttpError(node.e[0] ?? 500, node.e[1]);
|
|
95
|
+
this.#loading = false;
|
|
96
|
+
this.#error = error;
|
|
97
|
+
|
|
98
|
+
promise.catch(noop);
|
|
99
|
+
this.#reject_first?.(error);
|
|
100
|
+
this.#resolve_first = null;
|
|
101
|
+
this.#reject_first = null;
|
|
102
|
+
} else {
|
|
103
|
+
this.set(node.v);
|
|
104
|
+
}
|
|
90
105
|
}
|
|
91
106
|
}
|
|
92
107
|
|
|
@@ -4,21 +4,28 @@ import { get_remote_request_headers, handle_side_channel_response } from '../sha
|
|
|
4
4
|
import * as devalue from 'devalue';
|
|
5
5
|
import { HttpError } from '@sveltejs/kit/internal';
|
|
6
6
|
import { noop } from '../../../../utils/functions.js';
|
|
7
|
-
import {
|
|
7
|
+
import { read_sse } from '../../sse.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* @
|
|
11
|
-
* @
|
|
10
|
+
* @template T
|
|
11
|
+
* @param {string} id
|
|
12
|
+
* @param {string} payload
|
|
13
|
+
* @param {AbortController} [controller]
|
|
14
|
+
* @param {() => void} [on_connect]
|
|
15
|
+
* @returns {AsyncGenerator<T>}
|
|
12
16
|
*/
|
|
13
|
-
async function
|
|
14
|
-
|
|
17
|
+
export async function* create_live_iterator(
|
|
18
|
+
id,
|
|
19
|
+
payload,
|
|
20
|
+
controller = new AbortController(),
|
|
21
|
+
on_connect = noop
|
|
22
|
+
) {
|
|
23
|
+
const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
throw new HttpError(500, 'Invalid query.live response');
|
|
21
|
-
}
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
headers: get_remote_request_headers(),
|
|
27
|
+
signal: controller.signal
|
|
28
|
+
});
|
|
22
29
|
|
|
23
30
|
if (!response.ok) {
|
|
24
31
|
const result = await response.json().catch(() => ({
|
|
@@ -30,60 +37,34 @@ async function get_stream_reader(response) {
|
|
|
30
37
|
throw new HttpError(result.status ?? response.status ?? 500, result.error);
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
if (
|
|
34
|
-
|
|
40
|
+
if (response.headers.get('content-type')?.includes('application/json')) {
|
|
41
|
+
// we can end up here if we e.g. redirect in `handle`
|
|
42
|
+
const result = await response.json();
|
|
43
|
+
await handle_side_channel_response(result);
|
|
44
|
+
throw new HttpError(500, 'Invalid query.live response');
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
|
|
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');
|
|
47
|
+
if (!response.body) {
|
|
48
|
+
throw new Error('Expected query.live response body to be a ReadableStream');
|
|
53
49
|
}
|
|
54
|
-
}
|
|
55
50
|
|
|
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;
|
|
51
|
+
const reader = response.body.getReader();
|
|
73
52
|
|
|
74
53
|
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
54
|
on_connect();
|
|
82
55
|
|
|
83
|
-
|
|
56
|
+
for await (const node of read_sse(reader)) {
|
|
57
|
+
if (node.type === 'result') {
|
|
58
|
+
yield devalue.parse(node.result, app.decoders);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await handle_side_channel_response(node);
|
|
63
|
+
throw new HttpError(500, 'Invalid query.live response');
|
|
64
|
+
}
|
|
84
65
|
} finally {
|
|
85
66
|
try {
|
|
86
|
-
await reader
|
|
67
|
+
await reader.cancel();
|
|
87
68
|
} catch {
|
|
88
69
|
// already closed
|
|
89
70
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/** @import { RemoteFunctionResponse,
|
|
1
|
+
/** @import { RemoteFunctionResponse, RemoteFunctionData, RemoteFunctionDataNode } from 'types' */
|
|
2
2
|
/** @import { RemoteQueryUpdate } from '@sveltejs/kit' */
|
|
3
|
+
/** @import { CacheEntry } from './cache.svelte.js' */
|
|
3
4
|
import * as devalue from 'devalue';
|
|
4
|
-
import { app, goto, live_query_map, query_map } from '../client.js';
|
|
5
|
+
import { app, goto, live_query_map, query_map, query_responses } from '../client.js';
|
|
5
6
|
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
6
7
|
import { untrack } from 'svelte';
|
|
7
8
|
import { create_remote_key, split_remote_key } from '../../shared.js';
|
|
@@ -77,6 +78,20 @@ export function pin_while_resolving(cache_map, cache, id, payload, then) {
|
|
|
77
78
|
/**
|
|
78
79
|
* @returns {{ 'x-sveltekit-pathname': string, 'x-sveltekit-search': string }}
|
|
79
80
|
*/
|
|
81
|
+
/**
|
|
82
|
+
* Unwraps a `RemoteFunctionDataNode` that was serialized during SSR,
|
|
83
|
+
* rethrowing serialized errors so the consuming resource ends up
|
|
84
|
+
* in the same failed state it had on the server
|
|
85
|
+
* @param {RemoteFunctionDataNode} node
|
|
86
|
+
*/
|
|
87
|
+
export function unwrap_node(node) {
|
|
88
|
+
if (node.e) {
|
|
89
|
+
throw new HttpError(node.e[0] ?? 500, node.e[1]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return node.v;
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
export function get_remote_request_headers() {
|
|
81
96
|
// This will be the correct value of the current or soon-current url,
|
|
82
97
|
// even in forks because it's state-based - therefore not using window.location.
|
|
@@ -93,15 +108,10 @@ export function get_remote_request_headers() {
|
|
|
93
108
|
|
|
94
109
|
/**
|
|
95
110
|
* @param {string} url
|
|
96
|
-
* @param {
|
|
111
|
+
* @param {RequestInit} [init]
|
|
97
112
|
*/
|
|
98
|
-
export async function remote_request(url,
|
|
99
|
-
const response = await fetch(url,
|
|
100
|
-
headers: {
|
|
101
|
-
'Content-Type': 'application/json',
|
|
102
|
-
...headers
|
|
103
|
-
}
|
|
104
|
-
});
|
|
113
|
+
export async function remote_request(url, init) {
|
|
114
|
+
const response = await fetch(url, init);
|
|
105
115
|
|
|
106
116
|
if (!response.ok) {
|
|
107
117
|
throw new HttpError(500, 'Failed to execute remote function');
|
|
@@ -109,9 +119,63 @@ export async function remote_request(url, headers) {
|
|
|
109
119
|
|
|
110
120
|
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
|
|
111
121
|
|
|
112
|
-
|
|
122
|
+
if (result.type === 'error') {
|
|
123
|
+
throw new HttpError(result.status ?? 500, result.error);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const data = /** @type {RemoteFunctionData} */ (
|
|
127
|
+
result.data ? devalue.parse(result.data, app.decoders) : {}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
*
|
|
132
|
+
* @param {string} key
|
|
133
|
+
* @param {CacheEntry<any> | undefined} entry
|
|
134
|
+
* @param {any} result
|
|
135
|
+
*/
|
|
136
|
+
function refresh(key, entry, result) {
|
|
137
|
+
if (entry?.resource) {
|
|
138
|
+
if (result.e) {
|
|
139
|
+
entry.resource.fail(new HttpError(result.e[0] ?? 500, result.e[1]));
|
|
140
|
+
} else {
|
|
141
|
+
entry.resource.set(result.v);
|
|
142
|
+
}
|
|
143
|
+
} else if (!result.e) {
|
|
144
|
+
// `query_responses` stores `{ v }`/`{ e }` nodes, not raw values.
|
|
145
|
+
// Errors are deliberately dropped here: they are responses to a specific
|
|
146
|
+
// refresh, not durable state a future resource should initialize with
|
|
147
|
+
query_responses[key] = result;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// update queries with refreshed data
|
|
152
|
+
if (data.q) {
|
|
153
|
+
for (const key in data.q) {
|
|
154
|
+
const parts = split_remote_key(key);
|
|
155
|
+
const entry = query_map.get(parts.id)?.get(parts.payload);
|
|
156
|
+
|
|
157
|
+
refresh(key, entry, data.q[key]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// reconnect live queries
|
|
162
|
+
if (data.l) {
|
|
163
|
+
for (const key in data.l) {
|
|
164
|
+
const parts = split_remote_key(key);
|
|
165
|
+
const entry = live_query_map.get(parts.id)?.get(parts.payload);
|
|
166
|
+
|
|
167
|
+
refresh(key, entry, data.l[key]);
|
|
113
168
|
|
|
114
|
-
|
|
169
|
+
// `fail()` is terminal, so only reconnect on the success path —
|
|
170
|
+
// reconnecting after a hard failure would wipe the error state and
|
|
171
|
+
// restart the stream (see commit 63a3e83 regression).
|
|
172
|
+
if (!data.l[key].e) {
|
|
173
|
+
void entry?.resource.reconnect();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return data;
|
|
115
179
|
}
|
|
116
180
|
|
|
117
181
|
/**
|
|
@@ -206,50 +270,3 @@ export function categorize_updates(updates) {
|
|
|
206
270
|
|
|
207
271
|
return { overrides, refreshes };
|
|
208
272
|
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* @template TResource
|
|
212
|
-
* @param {string} stringified_singleflight
|
|
213
|
-
* @param {Map<string, Map<string, { resource: TResource }>>} map
|
|
214
|
-
* @param {(resource: TResource, value: RemoteSingleflightEntry) => void} callback
|
|
215
|
-
*/
|
|
216
|
-
function apply_singleflight(stringified_singleflight, map, callback) {
|
|
217
|
-
const singleflight = /** @type {RemoteSingleflightMap} */ (
|
|
218
|
-
devalue.parse(stringified_singleflight, app.decoders)
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
for (const [key, value] of Object.entries(singleflight)) {
|
|
222
|
-
const parts = split_remote_key(key);
|
|
223
|
-
const entry = map.get(parts.id)?.get(parts.payload);
|
|
224
|
-
if (entry?.resource) {
|
|
225
|
-
callback(entry.resource, value);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Apply refresh data from the server to the relevant queries
|
|
232
|
-
*
|
|
233
|
-
* @param {string} stringified_refreshes
|
|
234
|
-
*/
|
|
235
|
-
export const apply_refreshes = (stringified_refreshes) => {
|
|
236
|
-
apply_singleflight(stringified_refreshes, query_map, (resource, value) => {
|
|
237
|
-
if (value.type === 'result') {
|
|
238
|
-
resource?.set(value.data);
|
|
239
|
-
} else {
|
|
240
|
-
resource?.fail(new HttpError(value.status ?? 500, value.error));
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
/** @param {string} stringified_reconnects */
|
|
246
|
-
export const apply_reconnections = (stringified_reconnects) => {
|
|
247
|
-
apply_singleflight(stringified_reconnects, live_query_map, (resource, value) => {
|
|
248
|
-
if (value.type === 'result') {
|
|
249
|
-
resource?.set(value.data);
|
|
250
|
-
void resource?.reconnect();
|
|
251
|
-
} else {
|
|
252
|
-
resource?.fail(new HttpError(value.status ?? 500, value.error));
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { read_stream } from './stream.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} block
|
|
5
|
+
* @returns {string | undefined}
|
|
6
|
+
*/
|
|
7
|
+
function parse_sse_event_data(block) {
|
|
8
|
+
const lines = block.split('\n');
|
|
9
|
+
let data = '';
|
|
10
|
+
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
if (line.startsWith('data:')) {
|
|
13
|
+
data += (data ? '\n' : '') + line.slice(5).trimStart();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return data || undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Yields parsed JSON objects from a ReadableStream of Server-Sent Events.
|
|
22
|
+
* Each yielded value is the raw `JSON.parse`'d object from a `data:` field.
|
|
23
|
+
* @param {ReadableStreamDefaultReader<Uint8Array>} reader
|
|
24
|
+
*/
|
|
25
|
+
export async function* read_sse(reader) {
|
|
26
|
+
for await (const block of read_stream(reader, '\n\n')) {
|
|
27
|
+
const data = parse_sse_event_data(block);
|
|
28
|
+
if (data) {
|
|
29
|
+
yield JSON.parse(data);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads from a stream, decoding it as text and yielding each block of content
|
|
3
|
+
* separated by `delimiter`. The trailing block (if any) is yielded once the
|
|
4
|
+
* stream closes.
|
|
5
|
+
* @param {ReadableStreamDefaultReader<Uint8Array>} reader
|
|
6
|
+
* @param {string} delimiter
|
|
7
|
+
*/
|
|
8
|
+
export async function* read_stream(reader, delimiter) {
|
|
9
|
+
let done = false;
|
|
10
|
+
let buffer = '';
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
|
|
13
|
+
while (true) {
|
|
14
|
+
let split = buffer.indexOf(delimiter);
|
|
15
|
+
while (split !== -1) {
|
|
16
|
+
yield buffer.slice(0, split);
|
|
17
|
+
buffer = buffer.slice(split + delimiter.length);
|
|
18
|
+
split = buffer.indexOf(delimiter);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (done) {
|
|
22
|
+
if (buffer) {
|
|
23
|
+
yield buffer;
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const chunk = await reader.read();
|
|
29
|
+
done = chunk.done;
|
|
30
|
+
if (chunk.value) {
|
|
31
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (done) {
|
|
35
|
+
buffer += decoder.decode();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|