From 60fb389aebf0b4fb05c64aec151e4b4445405744 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Aug 2025 00:45:11 +0200 Subject: [PATCH 1/8] feat: add remote function `query.stream` - streams are deduplicated on the client, i.e. same resource+payload == same stream instance - a stream is kept open as long as it's either in a reactive context or someone iterates over it. Streams can close when noone listens anymore and open back up when someone does again - cannot iterate on the server right now, only retrieve the first value via promise - cannot refresh/override (doesn't make sense) --- .changeset/smart-nails-allow.md | 5 + .../20-core-concepts/60-remote-functions.md | 66 ++++ .../src/exports/internal/remote-functions.js | 1 + packages/kit/src/exports/public.d.ts | 10 + .../src/runtime/app/server/remote/query.js | 119 ++++++- .../client/remote-functions/query.svelte.js | 214 +++++++++++- .../client/remote-functions/shared.svelte.js | 3 + packages/kit/src/runtime/server/remote.js | 63 ++++ packages/kit/src/types/internal.d.ts | 2 +- .../basics/src/routes/remote/stream/+page.js | 1 + .../src/routes/remote/stream/+page.svelte | 35 ++ .../src/routes/remote/stream/stream.remote.js | 17 + .../kit/test/apps/basics/test/client.test.js | 29 ++ packages/kit/types/index.d.ts | 329 +++++++++++++----- 14 files changed, 809 insertions(+), 85 deletions(-) create mode 100644 .changeset/smart-nails-allow.md create mode 100644 packages/kit/test/apps/basics/src/routes/remote/stream/+page.js create mode 100644 packages/kit/test/apps/basics/src/routes/remote/stream/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/remote/stream/stream.remote.js diff --git a/.changeset/smart-nails-allow.md b/.changeset/smart-nails-allow.md new file mode 100644 index 000000000000..b51d57cf2bc7 --- /dev/null +++ b/.changeset/smart-nails-allow.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add remote function `query.stream` diff --git a/documentation/docs/20-core-concepts/60-remote-functions.md b/documentation/docs/20-core-concepts/60-remote-functions.md index 49319511579f..74a1af2b9592 100644 --- a/documentation/docs/20-core-concepts/60-remote-functions.md +++ b/documentation/docs/20-core-concepts/60-remote-functions.md @@ -213,6 +213,72 @@ export const getPost = query.batch(v.string(), async (slugs) => { ``` +## query.stream + +`query.stream` allows you to stream continuous data from the server to the client. + +```js +/// file: src/routes/time.remote.js +// @filename: ambient.d.ts +declare module '$lib/server/database' { + export function sql(strings: TemplateStringsArray, ...values: any[]): Promise; +} +// @filename: index.js +// ---cut--- +import { query } from '$app/server'; + +export const time = query.stream(async function* () { + while (true) { + yield new Date(); + await new Promise(r => setTimeout(r, 1000)) + } +}); +``` + +You can consume the stream like a promise or via the `current` property. In both cases, if it's used in a reactive context, it will automatically update to the latest version upon retrieving new data. + +```svelte + + + +

{await time()}

+

{time().current}

+``` + +Apart from that you can iterate over it like any other async iterable, including using `for await (...)`. + +```svelte + + + + + +{#each times as time} + {time} +{/each} +``` + +Stream requests to the same resource with the same payload are deduplicated, i.e. you cannot start the same stream multiple times in parallel and it to start from the beginning each time. + ## form The `form` function makes it easy to write data to the server. It takes a callback that receives the current [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)... diff --git a/packages/kit/src/exports/internal/remote-functions.js b/packages/kit/src/exports/internal/remote-functions.js index fe85f39b025f..e280d31e8389 100644 --- a/packages/kit/src/exports/internal/remote-functions.js +++ b/packages/kit/src/exports/internal/remote-functions.js @@ -17,6 +17,7 @@ export function validate_remote_functions(module, file) { type !== 'command' && type !== 'query' && type !== 'query.batch' && + type !== 'query.stream' && type !== 'prerender' ) { throw new Error( diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index c92f523a17b8..36fc9d2ac1b0 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1822,6 +1822,16 @@ export interface RemoteQueryOverride { release(): void; } +/** + * The return value of a remote `query.stream` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + */ +export type RemoteQueryStream = RemoteResource & AsyncIterable>; + +/** + * The return value of a remote `query.stream` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + */ +export type RemoteQueryStreamFunction = (arg: Input) => RemoteQueryStream; + /** * The return value of a remote `prerender` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation. */ diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index 55737a862570..94d0d2f33618 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -1,4 +1,4 @@ -/** @import { RemoteQuery, RemoteQueryFunction } from '@sveltejs/kit' */ +/** @import { RemoteQuery, RemoteQueryFunction, RemoteQueryStream, RemoteQueryStreamFunction } from '@sveltejs/kit' */ /** @import { RemoteInfo, MaybePromise } from 'types' */ /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ import { get_request_store } from '@sveltejs/kit/internal/server'; @@ -228,5 +228,120 @@ function batch(validate_or_fn, maybe_fn) { return wrapper; } -// Add batch as a property to the query function +/** + * Creates a streaming remote query. When called from the browser, the generator function will be invoked on the server and values will be streamed via Server-Sent Events. + * + * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.stream) for full documentation. + * + * @template Output + * @overload + * @param {() => Generator | AsyncGenerator} fn + * @returns {RemoteQueryStreamFunction} + * @since 2.36 + */ +/** + * Creates a streaming remote query. When called from the browser, the generator function will be invoked on the server and values will be streamed via Server-Sent Events. + * + * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.stream) for full documentation. + * + * @template Input + * @template Output + * @overload + * @param {'unchecked'} validate + * @param {(arg: Input) => Generator | AsyncGenerator} fn + * @returns {RemoteQueryStreamFunction} + * @since 2.36 + */ +/** + * Creates a streaming remote query. When called from the browser, the generator function will be invoked on the server and values will be streamed via Server-Sent Events. + * + * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.stream) for full documentation. + * + * @template {StandardSchemaV1} Schema + * @template Output + * @overload + * @param {Schema} schema + * @param {(arg: StandardSchemaV1.InferOutput) => Generator | AsyncGenerator} fn + * @returns {RemoteQueryStreamFunction, Output>} + * @since 2.36 + */ +/** + * @template Input + * @template Output + * @param {any} validate_or_fn + * @param {(arg?: Input) => Generator | AsyncGenerator} [maybe_fn] + * @returns {RemoteQueryStreamFunction} + * @since 2.36 + */ +/*@__NO_SIDE_EFFECTS__*/ +function stream(validate_or_fn, maybe_fn) { + /** @type {(arg?: Input) => Generator | AsyncGenerator} */ + const fn = maybe_fn ?? validate_or_fn; + + /** @type {(arg?: any) => MaybePromise} */ + const validate = create_validator(validate_or_fn, maybe_fn); + + /** @type {RemoteInfo} */ + const __ = { type: 'query.stream', id: '', name: '' }; + + /** @type {RemoteQueryStreamFunction & { __: RemoteInfo }} */ + const wrapper = (/** @type {Input} */ arg) => { + if (prerendering) { + throw new Error( + `Cannot call query.stream '${__.name}' while prerendering, as prerendered pages need static data. Use 'prerender' from $app/server instead` + ); + } + + const { event, state } = get_request_store(); + + /** @type {IteratorResult | undefined} */ + let first_value; + + const promise = (async () => { + // We only care about the generator when doing a remote request + if (event.isRemoteRequest) return; + + const generator = await run_remote_function(event, state, false, arg, validate, fn); + first_value = await generator.next(); + await generator.return(); + return first_value.done ? undefined : first_value.value; + })(); + + // Catch promise to avoid unhandled rejection + promise.catch(() => {}); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Object.assign(promise, { + async *[Symbol.asyncIterator]() { + if (event.isRemoteRequest) { + const generator = await run_remote_function(event, state, false, arg, validate, fn); + yield* generator; + } else { + // TODO how would we subscribe to the stream on the server while deduplicating calls and knowing when to stop? + throw new Error( + 'Cannot iterate over a stream on the server. This restriction may be lifted in a future version.' + ); + } + }, + get error() { + return undefined; + }, + get ready() { + return !!first_value; + }, + get current() { + return first_value?.value; + } + }); + + return /** @type {RemoteQueryStream} */ (promise); + }; + + Object.defineProperty(wrapper, '__', { value: __ }); + + return wrapper; +} + +// Add batch and stream as properties to the query function Object.defineProperty(query, 'batch', { value: batch, enumerable: true }); +Object.defineProperty(query, 'stream', { value: stream, enumerable: true }); diff --git a/packages/kit/src/runtime/client/remote-functions/query.svelte.js b/packages/kit/src/runtime/client/remote-functions/query.svelte.js index 871e552ba5ab..edd2940ce747 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -1,4 +1,4 @@ -/** @import { RemoteQueryFunction } from '@sveltejs/kit' */ +/** @import { RemoteQueryFunction, RemoteQueryStreamFunction } from '@sveltejs/kit' */ /** @import { RemoteFunctionResponse } from 'types' */ import { app_dir, base } from '__sveltekit/paths'; import { app, goto, remote_responses, started } from '../client.js'; @@ -97,8 +97,218 @@ function batch(id) { }); } -// Add batch as a property to the query function +/** + * @param {string} id + * @returns {RemoteQueryStreamFunction} + */ +function stream(id) { + return create_remote_function(id, (_, payload) => { + const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`; + return new QueryStream(url); + }); +} + +/** + * Query stream class that implements both Promise and AsyncIterable interfaces + * @template T + * @implements {Partial>} + */ +class QueryStream { + /** + * The promise next() and then/catch/finally methods return. Is reset after each message from the EventSource. + * @type {Promise} + */ + // @ts-expect-error TS doesn't see that we assign it in the constructor indirectly through function calls + #promise; + + /** + * The resolve function for the promise. + * @type {(value: any) => void} + */ + // @ts-expect-error TS doesn't see that we assign it in the constructor indirectly through function calls + #resolve; + + /** + * The reject function for the promise. + * @type {(error?: any) => void} + */ + // @ts-expect-error TS doesn't see that we assign it in the constructor indirectly through function calls + #reject; + + /** @type {any} */ + #current = $state.raw(); + + /** @type {boolean} */ + #ready = $state(false); + + /** @type {any} */ + #error = $state(); + + /** @type {EventSource | undefined} */ + #source; + + /** + * How many active async iterators are using this stream. + * If there are no active iterators, the EventSource is closed if it's unused. + * @type {number} */ + #count = 0; + + /** + * The URL of the EventSource. + * @type {string} + */ + #url; + + /** + * Becomes `true` when our query map deletes this stream, which means there's no reactive listener to it anymore. + * @type {boolean} + */ + #unused = false; + + /** + * @param {string} url + */ + constructor(url) { + this.#url = url; + this.#next(); + } + + #create_promise() { + this.#reject?.(); // in case there's a dangling listener + this.#promise = new Promise((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; + }); + } + + #next() { + if (this.#source && this.#source.readyState !== EventSource.CLOSED) return; + + this.#create_promise(); + this.#source = new EventSource(this.#url); + + const source = this.#source; + + /** @param {MessageEvent} event */ + const onMessage = (event) => { + this.#ready = true; + this.#error = undefined; + + const message = event.data; + + if (message === '[DONE]') { + source.close(); + this.#resolve({ done: true, value: undefined }); + return; + } + + const parsed = devalue.parse(message, app.decoders); + if (parsed && typeof parsed === 'object' && parsed.type === 'error') { + source.close(); + this.#reject((this.#error = new HttpError(parsed.status ?? 500, parsed.error))); + return; + } + + this.#current = parsed; + this.#resolve({ done: false, value: parsed }); + this.#create_promise(); + }; + + /** @param {Event} error */ + const onError = (error) => { + this.#error = error; + this.#reject(error); + }; + + this.#source.addEventListener('message', onMessage); + this.#source.addEventListener('error', onError); + } + + get then() { + this.#current; + + /** + * @param {any} resolve + * @param {any} reject + */ + return (resolve, reject) => { + // On first call we return the promise. In all other cases we don't want any delay and return the current value. + // The getter will self-invalidate when the next message is received. + if (!this.#ready) { + return this.#promise.then((v) => v.value).then(resolve, reject); + } else { + if (this.#error) { + return reject(this.#error); + } else { + return resolve(this.#current); + } + } + }; + } + + get catch() { + this.#current; + + return (/** @type {any} */ reject) => { + return this.then(undefined, reject); + }; + } + + get finally() { + this.#current; + + return (/** @type {any} */ fn) => { + return this.then( + () => fn(), + () => fn() + ); + }; + } + + get current() { + return this.#current; + } + + get ready() { + return this.#ready; + } + + get error() { + return this.#error; + } + + _dispose() { + this.#unused = true; + if (this.#count === 0) { + this.#source?.close(); + } + } + + [Symbol.asyncIterator]() { + // Restart the stream in case it was closed previously. + // Can happen if this is iterated over from a non-reactive context. + this.#next(); + this.#count++; + const that = this; + + return { + next() { + return that.#promise; + }, + return() { + that.#count--; + if (that.#count === 0 && that.#unused) { + that.#source?.close(); + } + return Promise.resolve({ done: true, value: undefined }); + } + }; + } +} + +// Add batch and stream as properties to the query function Object.defineProperty(query, 'batch', { value: batch, enumerable: true }); +Object.defineProperty(query, 'stream', { value: stream, enumerable: true }); /** * @template T diff --git a/packages/kit/src/runtime/client/remote-functions/shared.svelte.js b/packages/kit/src/runtime/client/remote-functions/shared.svelte.js index 7d94374f40b1..fc072b802b81 100644 --- a/packages/kit/src/runtime/client/remote-functions/shared.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/shared.svelte.js @@ -59,6 +59,7 @@ export function create_remote_function(id, create) { entry.count--; void tick().then(() => { if (!entry.count && entry === query_map.get(cache_key)) { + entry.resource._dispose?.(); query_map.delete(cache_key); } }); @@ -93,6 +94,7 @@ export function create_remote_function(id, create) { entry === query_map.get(cache_key) ) { // If no one is tracking this resource anymore, we can delete it from the cache + resource._dispose?.(); query_map.delete(cache_key); } }); @@ -100,6 +102,7 @@ export function create_remote_function(id, create) { .catch(() => { // error delete the resource from the cache // TODO is that correct? + resource._dispose?.(); query_map.delete(cache_key); }); } diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 369854d41edd..5b2fb17fad47 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -121,6 +121,69 @@ async function handle_remote_call_internal(event, state, options, manifest, id) ); } + if (info.type === 'query.stream') { + const payload = /** @type {string} */ ( + new URL(event.request.url).searchParams.get('payload') + ); + + const generator = with_request_store({ event, state }, () => + fn(parse_remote_arg(payload, transport)) + ); + + // Return a Server-Sent Events stream + let cancelled = false; + + return new Response( + new ReadableStream({ + async start(controller) { + const encoder = new TextEncoder(); + + try { + for await (const value of generator) { + if (cancelled) break; + const serialized = stringify(value, transport); + const chunk = `data: ${serialized}\n\n`; + controller.enqueue(encoder.encode(chunk)); + } + + // Send end marker + controller.enqueue(encoder.encode('data: [DONE]\n\n')); + controller.close(); + } catch (error) { + console.error(error); + // Send error and close + const errorData = await handle_error_and_jsonify(event, state, options, error); + const serialized = stringify( + { + type: 'error', + error: errorData, + status: + error instanceof HttpError || error instanceof SvelteKitError + ? error.status + : 500 + }, + transport + ); + const chunk = `data: ${serialized}\n\n`; + controller.enqueue(encoder.encode(chunk)); + controller.close(); + } + }, + + cancel() { + cancelled = true; + } + }), + { + headers: { + 'content-type': 'text/event-stream', + 'cache-control': 'private, no-store', + connection: 'keep-alive' + } + } + ); + } + const payload = info.type === 'prerender' ? prerender_args diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 6a4720bdbeb8..8233a2d8ea98 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -548,7 +548,7 @@ export type ValidatedKitConfig = Omit, 'adapter'> & export type RemoteInfo = | { - type: 'query' | 'query.batch' | 'command'; + type: 'query' | 'query.batch' | 'query.stream' | 'command'; id: string; name: string; } diff --git a/packages/kit/test/apps/basics/src/routes/remote/stream/+page.js b/packages/kit/test/apps/basics/src/routes/remote/stream/+page.js new file mode 100644 index 000000000000..5ee254f59b7a --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/stream/+page.js @@ -0,0 +1 @@ +export const ssr = false; // TODO once async SSR exists also test server diff --git a/packages/kit/test/apps/basics/src/routes/remote/stream/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/stream/+page.svelte new file mode 100644 index 000000000000..39da2a69e9c7 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/stream/+page.svelte @@ -0,0 +1,35 @@ + + + +{#if true} +

{#await time() then t}{t}{/await}

+{/if} + +{#if true} +

{time().current}

+{/if} + +{#if true} +

[{streamValues.join(', ')}]

+{/if} + + diff --git a/packages/kit/test/apps/basics/src/routes/remote/stream/stream.remote.js b/packages/kit/test/apps/basics/src/routes/remote/stream/stream.remote.js new file mode 100644 index 000000000000..dc37661ac76d --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/stream/stream.remote.js @@ -0,0 +1,17 @@ +import { command, query } from '$app/server'; + +let i = 0; +let p = Promise.withResolvers(); + +export const next = command(() => { + i++; + p.resolve(); + p = Promise.withResolvers(); +}); + +export const time = query.stream(async function* () { + while (i < 2) { + yield i; + await p.promise; + } +}); diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index c06dd870050f..2bbba54ca056 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1932,4 +1932,33 @@ test.describe('remote functions', () => { await page.waitForTimeout(100); // allow all requests to finish expect(request_count).toBe(1); }); + + test('query.stream works', async ({ page }) => { + let request_count = 0; + page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0)); + + await page.goto('/remote/stream'); + + await expect(page.locator('#time-promise')).toHaveText('0'); + await expect(page.locator('#time-resource')).toHaveText('0'); + await expect(page.locator('#time-stream')).toHaveText('[0]'); + + expect(request_count).toBe(1); // deduplicated time() stream + + await page.click('button'); + await expect(page.locator('#time-promise')).toHaveText('1'); + await expect(page.locator('#time-resource')).toHaveText('1'); + await expect(page.locator('#time-stream')).toHaveText('[0, 1]'); + + await page.waitForTimeout(100); // allow all requests to finish + expect(request_count).toBe(2); // just the next() command + + await page.click('button'); + await expect(page.locator('#time-promise')).toHaveText('1'); + await expect(page.locator('#time-resource')).toHaveText('1'); + await expect(page.locator('#time-stream')).toHaveText('[0, 1]'); + + await page.waitForTimeout(100); // allow all requests to finish + expect(request_count).toBe(3); // just the next() command + }); }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 1c322e087beb..d4b39be365f7 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -4,7 +4,11 @@ declare module '@sveltejs/kit' { import type { SvelteConfig } from '@sveltejs/vite-plugin-svelte'; import type { StandardSchemaV1 } from '@standard-schema/spec'; - import type { RouteId as AppRouteId, LayoutParams as AppLayoutParams, ResolvedPathname } from '$app/types'; + import type { + RouteId as AppRouteId, + LayoutParams as AppLayoutParams, + ResolvedPathname + } from '$app/types'; import type { Span } from '@opentelemetry/api'; /** * [Adapters](https://svelte.dev/docs/kit/adapters) are responsible for taking the production build and turning it into something that can be deployed to a platform of your choosing. @@ -269,7 +273,10 @@ declare module '@sveltejs/kit' { * @param name the name of the cookie * @param opts the options, passed directly to `cookie.serialize`. The `path` must match the path of the cookie you want to delete. See documentation [here](https://github.com/jshttp/cookie#cookieserializename-value-options) */ - delete: (name: string, opts: import('cookie').CookieSerializeOptions & { path: string }) => void; + delete: ( + name: string, + opts: import('cookie').CookieSerializeOptions & { path: string } + ) => void; /** * Serialize a cookie name-value pair into a `Set-Cookie` header string, but don't apply it to the response. @@ -1419,7 +1426,10 @@ declare module '@sveltejs/kit' { * but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. * @param input the html chunk and the info if this is the last chunk */ - transformPageChunk?: (input: { html: string; done: boolean }) => MaybePromise; + transformPageChunk?: (input: { + html: string; + done: boolean; + }) => MaybePromise; /** * Determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. * By default, none will be included. @@ -1798,6 +1808,29 @@ declare module '@sveltejs/kit' { release(): void; } + /** + * The return value of a remote `query.stream` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + */ + export type RemoteQueryStream = RemoteResource & + AsyncIterable> & { + /** + * On the client, this function will re-fetch the query from the server. + * + * On the server, this can be called in the context of a `command` or `form` and the refreshed data will accompany the action response back to the client. + * This prevents SvelteKit needing to refresh all queries on the page in a second server round-trip. + */ + refresh(): Promise; + /** + * Temporarily override the value of a query. This is used with the `updates` method of a [command](https://svelte.dev/docs/kit/remote-functions#command-Updating-queries) or [enhanced form submission](https://svelte.dev/docs/kit/remote-functions#form-enhance) to provide optimistic updates. + */ + withOverride(update: (current: Awaited) => Awaited): RemoteQueryOverride; + }; + + /** + * The return value of a remote `query.stream` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + */ + export type RemoteQueryStreamFunction = (arg: Input) => RemoteQueryStream; + /** * The return value of a remote `prerender` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation. */ @@ -1829,7 +1862,9 @@ declare module '@sveltejs/kit' { * A function that is invoked once the entry has been created. This is where you * should write the function to the filesystem and generate redirect manifests. */ - complete(entry: { generateManifest(opts: { relativePath: string }): string }): MaybePromise; + complete(entry: { + generateManifest(opts: { relativePath: string }): string; + }): MaybePromise; } // Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts @@ -2294,16 +2329,24 @@ declare module '@sveltejs/kit' { * @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling. * @throws {Error} If the provided status is invalid (not between 400 and 599). */ - export function error(status: number, body?: { - message: string; - } extends App.Error ? App.Error | string | undefined : never): never; + export function error( + status: number, + body?: { + message: string; + } extends App.Error + ? App.Error | string | undefined + : never + ): never; /** * Checks whether this is an error thrown by {@link error}. * @param status The status to filter for. * */ - export function isHttpError(e: unknown, status?: T): e is (HttpError_1 & { + export function isHttpError( + e: unknown, + status?: T + ): e is HttpError_1 & { status: T extends undefined ? never : T; - }); + }; /** * Redirect a request. When called during request handling, SvelteKit will return a redirect response. * Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it. @@ -2320,7 +2363,10 @@ declare module '@sveltejs/kit' { * @throws {Redirect} This error instructs SvelteKit to redirect to the specified location. * @throws {Error} If the provided status is invalid. * */ - export function redirect(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), location: string | URL): never; + export function redirect( + status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), + location: string | URL + ): never; /** * Checks whether this is a redirect thrown by {@link redirect}. * @param e The object to check. @@ -2372,20 +2418,31 @@ declare module '@sveltejs/kit' { wasNormalized: boolean; denormalize: (url?: string | URL) => URL; }; - export type LessThan = TNumber extends TArray["length"] ? TArray[number] : LessThan; - export type NumericRange = Exclude, LessThan>; + export type LessThan< + TNumber extends number, + TArray extends any[] = [] + > = TNumber extends TArray['length'] + ? TArray[number] + : LessThan; + export type NumericRange = Exclude< + TEnd | LessThan, + LessThan + >; export const VERSION: string; class HttpError_1 { - - constructor(status: number, body: { - message: string; - } extends App.Error ? (App.Error | string | undefined) : App.Error); + constructor( + status: number, + body: { + message: string; + } extends App.Error + ? App.Error | string | undefined + : App.Error + ); status: number; body: App.Error; toString(): string; } class Redirect_1 { - constructor(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string); status: 301 | 302 | 303 | 307 | 308 | 300 | 304 | 305 | 306; location: string; @@ -2472,13 +2529,20 @@ declare module '@sveltejs/kit/hooks' { } declare module '@sveltejs/kit/node' { - export function getRequest({ request, base, bodySizeLimit }: { - request: import("http").IncomingMessage; + export function getRequest({ + request, + base, + bodySizeLimit + }: { + request: import('http').IncomingMessage; base: string; bodySizeLimit?: number; }): Promise; - export function setResponse(res: import("http").ServerResponse, response: Response): Promise; + export function setResponse( + res: import('http').ServerResponse, + response: Response + ): Promise; /** * Converts a file on disk to a readable stream * @since 2.4.0 @@ -2503,7 +2567,7 @@ declare module '@sveltejs/kit/vite' { /** * Returns the SvelteKit Vite plugins. * */ - export function sveltekit(): Promise; + export function sveltekit(): Promise; export {}; } @@ -2551,7 +2615,10 @@ declare module '$app/forms' { * } * ``` * */ - export function deserialize | undefined, Failure extends Record | undefined>(result: string): import("@sveltejs/kit").ActionResult; + export function deserialize< + Success extends Record | undefined, + Failure extends Record | undefined + >(result: string): import('@sveltejs/kit').ActionResult; /** * This action enhances a `
` element that otherwise would work without JavaScript. * @@ -2575,14 +2642,23 @@ declare module '$app/forms' { * @param form_element The form element * @param submit Submit callback */ - export function enhance | undefined, Failure extends Record | undefined>(form_element: HTMLFormElement, submit?: import("@sveltejs/kit").SubmitFunction): { + export function enhance< + Success extends Record | undefined, + Failure extends Record | undefined + >( + form_element: HTMLFormElement, + submit?: import('@sveltejs/kit').SubmitFunction + ): { destroy(): void; }; /** * This action updates the `form` property of the current page with the given data and updates `page.status`. * In case of an error, it redirects to the nearest error page. * */ - export function applyAction | undefined, Failure extends Record | undefined>(result: import("@sveltejs/kit").ActionResult): Promise; + export function applyAction< + Success extends Record | undefined, + Failure extends Record | undefined + >(result: import('@sveltejs/kit').ActionResult): Promise; export {}; } @@ -2593,7 +2669,9 @@ declare module '$app/navigation' { * * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function afterNavigate(callback: (navigation: import("@sveltejs/kit").AfterNavigate) => void): void; + export function afterNavigate( + callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void + ): void; /** * A navigation interceptor that triggers before we navigate to a URL, whether by clicking a link, calling `goto(...)`, or using the browser back/forward controls. * @@ -2605,7 +2683,9 @@ declare module '$app/navigation' { * * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function beforeNavigate(callback: (navigation: import("@sveltejs/kit").BeforeNavigate) => void): void; + export function beforeNavigate( + callback: (navigation: import('@sveltejs/kit').BeforeNavigate) => void + ): void; /** * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations. * @@ -2615,7 +2695,9 @@ declare module '$app/navigation' { * * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function onNavigate(callback: (navigation: import("@sveltejs/kit").OnNavigate) => MaybePromise<(() => void) | void>): void; + export function onNavigate( + callback: (navigation: import('@sveltejs/kit').OnNavigate) => MaybePromise<(() => void) | void> + ): void; /** * If called when the page is being updated following a navigation (in `onMount` or `afterNavigate` or an action, for example), this disables SvelteKit's built-in scroll handling. * This is generally discouraged, since it breaks user expectations. @@ -2630,14 +2712,17 @@ declare module '$app/navigation' { * @param url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://svelte.dev/docs/kit/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. * @param {Object} opts Options related to the navigation * */ - export function goto(url: string | URL, opts?: { - replaceState?: boolean | undefined; - noScroll?: boolean | undefined; - keepFocus?: boolean | undefined; - invalidateAll?: boolean | undefined; - invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined; - state?: App.PageState | undefined; - }): Promise; + export function goto( + url: string | URL, + opts?: { + replaceState?: boolean | undefined; + noScroll?: boolean | undefined; + keepFocus?: boolean | undefined; + invalidateAll?: boolean | undefined; + invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined; + state?: App.PageState | undefined; + } + ): Promise; /** * Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated. * @@ -2664,7 +2749,9 @@ declare module '$app/navigation' { * Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run (unless disabled via the option argument). * Returns a `Promise` that resolves when the page is subsequently updated. * */ - export function refreshAll({ includeLoadFunctions }?: { + export function refreshAll({ + includeLoadFunctions + }?: { includeLoadFunctions?: boolean; }): Promise; /** @@ -2678,14 +2765,17 @@ declare module '$app/navigation' { * * @param href Page to preload * */ - export function preloadData(href: string): Promise<{ - type: "loaded"; - status: number; - data: Record; - } | { - type: "redirect"; - location: string; - }>; + export function preloadData(href: string): Promise< + | { + type: 'loaded'; + status: number; + data: Record; + } + | { + type: 'redirect'; + location: string; + } + >; /** * Programmatically imports the code for routes that haven't yet been fetched. * Typically, you might call this to speed up subsequent navigation. @@ -2787,7 +2877,14 @@ declare module '$app/paths' { } declare module '$app/server' { - import type { RequestEvent, RemoteCommand, RemoteForm, RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit'; + import type { + RequestEvent, + RemoteCommand, + RemoteForm, + RemotePrerenderFunction, + RemoteQueryFunction, + RemoteQueryStreamFunction + } from '@sveltejs/kit'; import type { StandardSchemaV1 } from '@standard-schema/spec'; /** * Read the contents of an imported asset from the filesystem @@ -2825,7 +2922,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function command(validate: "unchecked", fn: (arg: Input) => Output): RemoteCommand; + export function command( + validate: 'unchecked', + fn: (arg: Input) => Output + ): RemoteCommand; /** * Creates a remote command. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -2833,7 +2933,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function command(validate: Schema, fn: (arg: StandardSchemaV1.InferOutput) => Output): RemoteCommand, Output>; + export function command( + validate: Schema, + fn: (arg: StandardSchemaV1.InferOutput) => Output + ): RemoteCommand, Output>; /** * Creates a form object that can be spread onto a `` element. * @@ -2849,10 +2952,15 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender(fn: () => MaybePromise, options?: { - inputs?: RemotePrerenderInputsGenerator; - dynamic?: boolean; - } | undefined): RemotePrerenderFunction; + export function prerender( + fn: () => MaybePromise, + options?: + | { + inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean; + } + | undefined + ): RemotePrerenderFunction; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -2860,10 +2968,16 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender(validate: "unchecked", fn: (arg: Input) => MaybePromise, options?: { - inputs?: RemotePrerenderInputsGenerator; - dynamic?: boolean; - } | undefined): RemotePrerenderFunction; + export function prerender( + validate: 'unchecked', + fn: (arg: Input) => MaybePromise, + options?: + | { + inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean; + } + | undefined + ): RemotePrerenderFunction; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -2871,10 +2985,16 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise, options?: { - inputs?: RemotePrerenderInputsGenerator>; - dynamic?: boolean; - } | undefined): RemotePrerenderFunction, Output>; + export function prerender( + schema: Schema, + fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise, + options?: + | { + inputs?: RemotePrerenderInputsGenerator>; + dynamic?: boolean; + } + | undefined + ): RemotePrerenderFunction, Output>; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -2890,7 +3010,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function query(validate: "unchecked", fn: (arg: Input) => MaybePromise): RemoteQueryFunction; + export function query( + validate: 'unchecked', + fn: (arg: Input) => MaybePromise + ): RemoteQueryFunction; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -2898,7 +3021,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function query(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise): RemoteQueryFunction, Output>; + export function query( + schema: Schema, + fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise + ): RemoteQueryFunction, Output>; export namespace query { /** * Creates a batch query function that collects multiple calls and executes them in a single request @@ -2907,7 +3033,10 @@ declare module '$app/server' { * * @since 2.35 */ - function batch(validate: "unchecked", fn: (args: Input[]) => MaybePromise): RemoteQueryFunction; + function batch( + validate: 'unchecked', + fn: (args: Input[]) => MaybePromise + ): RemoteQueryFunction; /** * Creates a batch query function that collects multiple calls and executes them in a single request * @@ -2915,7 +3044,44 @@ declare module '$app/server' { * * @since 2.35 */ - function batch(schema: Schema, fn: (args: StandardSchemaV1.InferOutput[]) => MaybePromise): RemoteQueryFunction, Output>; + function batch( + schema: Schema, + fn: (args: StandardSchemaV1.InferOutput[]) => MaybePromise + ): RemoteQueryFunction, Output>; + /** + * Creates a streaming remote query. When called from the browser, the generator function will be invoked on the server and values will be streamed via Server-Sent Events. + * + * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + * + * @since 2.36 + */ + function stream( + fn: () => Generator | AsyncGenerator + ): RemoteQueryStreamFunction; + /** + * Creates a streaming remote query. When called from the browser, the generator function will be invoked on the server and values will be streamed via Server-Sent Events. + * + * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + * + * @since 2.36 + */ + function stream( + validate: 'unchecked', + fn: (arg: Input) => Generator | AsyncGenerator + ): RemoteQueryStreamFunction; + /** + * Creates a streaming remote query. When called from the browser, the generator function will be invoked on the server and values will be streamed via Server-Sent Events. + * + * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + * + * @since 2.36 + */ + function stream( + schema: Schema, + fn: ( + arg: StandardSchemaV1.InferOutput + ) => Generator | AsyncGenerator + ): RemoteQueryStreamFunction, Output>; } type RemotePrerenderInputsGenerator = () => MaybePromise; type MaybePromise = T | Promise; @@ -2960,19 +3126,21 @@ declare module '$app/state' { * On the server, values can only be read during rendering (in other words _not_ in e.g. `load` functions). In the browser, the values can be read at any time. * * */ - export const page: import("@sveltejs/kit").Page; + export const page: import('@sveltejs/kit').Page; /** * A read-only object representing an in-progress navigation, with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. * Values are `null` when no navigation is occurring, or during server rendering. * */ - export const navigating: import("@sveltejs/kit").Navigation | { - from: null; - to: null; - type: null; - willUnload: null; - delta: null; - complete: null; - }; + export const navigating: + | import('@sveltejs/kit').Navigation + | { + from: null; + to: null; + type: null; + willUnload: null; + delta: null; + complete: null; + }; /** * A read-only reactive value that's initially `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update `current` to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. * */ @@ -2986,11 +3154,10 @@ declare module '$app/state' { declare module '$app/stores' { export function getStores(): { - page: typeof page; - + navigating: typeof navigating; - + updated: typeof updated; }; /** @@ -3000,7 +3167,7 @@ declare module '$app/stores' { * * @deprecated Use `page` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const page: import("svelte/store").Readable; + export const page: import('svelte/store').Readable; /** * A readable store. * When navigating starts, its value is a `Navigation` object with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. @@ -3010,7 +3177,9 @@ declare module '$app/stores' { * * @deprecated Use `navigating` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const navigating: import("svelte/store").Readable; + export const navigating: import('svelte/store').Readable< + import('@sveltejs/kit').Navigation | null + >; /** * A readable store whose initial value is `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. * @@ -3018,12 +3187,12 @@ declare module '$app/stores' { * * @deprecated Use `updated` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const updated: import("svelte/store").Readable & { + export const updated: import('svelte/store').Readable & { check(): Promise; }; export {}; -}/** +} /** * It's possible to tell SvelteKit how to type objects inside your app by declaring the `App` namespace. By default, a new project will have a file called `src/app.d.ts` containing the following: * * ```ts @@ -3159,4 +3328,4 @@ declare module '$app/types' { export type Asset = ReturnType; } -//# sourceMappingURL=index.d.ts.map \ No newline at end of file +//# sourceMappingURL=index.d.ts.map From 3b54a4c7ad8aacebe475dd19732eb83feb92989c Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Aug 2025 15:37:03 +0200 Subject: [PATCH 2/8] make it treeshakeable --- packages/kit/src/exports/internal/remote-functions.js | 2 +- packages/kit/src/runtime/app/server/remote/query.js | 2 +- packages/kit/src/runtime/client/remote-functions/index.js | 2 +- .../kit/src/runtime/client/remote-functions/query.svelte.js | 4 +--- packages/kit/src/runtime/server/remote.js | 2 +- packages/kit/src/types/internal.d.ts | 5 ++++- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/exports/internal/remote-functions.js b/packages/kit/src/exports/internal/remote-functions.js index 37dbc4caea83..31ee2da0e2d5 100644 --- a/packages/kit/src/exports/internal/remote-functions.js +++ b/packages/kit/src/exports/internal/remote-functions.js @@ -17,7 +17,7 @@ export function validate_remote_functions(module, file) { type !== 'command' && type !== 'query' && type !== 'query_batch' && - type !== 'query.stream' && + type !== 'query_stream' && type !== 'prerender' ) { throw new Error( diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index c89dca48d437..02118e75defe 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -321,7 +321,7 @@ function stream(validate_or_fn, maybe_fn) { const validate = create_validator(validate_or_fn, maybe_fn); /** @type {RemoteInfo} */ - const __ = { type: 'query.stream', id: '', name: '' }; + const __ = { type: 'query_stream', id: '', name: '' }; /** @type {RemoteQueryStreamFunction & { __: RemoteInfo }} */ const wrapper = (/** @type {Input} */ arg) => { diff --git a/packages/kit/src/runtime/client/remote-functions/index.js b/packages/kit/src/runtime/client/remote-functions/index.js index 4b20cabddd92..2e23fb4b0e1a 100644 --- a/packages/kit/src/runtime/client/remote-functions/index.js +++ b/packages/kit/src/runtime/client/remote-functions/index.js @@ -1,4 +1,4 @@ export { command } from './command.svelte.js'; export { form } from './form.svelte.js'; export { prerender } from './prerender.svelte.js'; -export { query, query_batch } from './query.svelte.js'; +export { query, query_batch, query_stream } from './query.svelte.js'; diff --git a/packages/kit/src/runtime/client/remote-functions/query.svelte.js b/packages/kit/src/runtime/client/remote-functions/query.svelte.js index 87042458da0b..5bcdca500fd6 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -108,7 +108,7 @@ export function query_batch(id) { * @param {string} id * @returns {RemoteQueryStreamFunction} */ -function stream(id) { +export function query_stream(id) { return create_remote_function(id, (_, payload) => { const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`; return new QueryStream(url); @@ -313,8 +313,6 @@ class QueryStream { } } -Object.defineProperty(query, 'stream', { value: stream, enumerable: true }); - /** * @template T * @implements {Partial>} diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 12bf30040c69..eb23e6c47f1c 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -135,7 +135,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id) ); } - if (info.type === 'query.stream') { + if (info.type === 'query_stream') { const payload = /** @type {string} */ ( new URL(event.request.url).searchParams.get('payload') ); diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 9aad668b1c99..a6dc091c6d3e 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -548,7 +548,10 @@ export type ValidatedKitConfig = Omit, 'adapter'> & export type RemoteInfo = | { - type: 'query' | 'query.stream' | 'command'; + /** + * Corresponds to the name of the client-side exports (that's why we use underscores and not dots) + */ + type: 'query' | 'query_stream' | 'command'; id: string; name: string; } From 96ea81cb592b6e25a74ad50d10bf3f17485743a1 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Aug 2025 15:39:51 +0200 Subject: [PATCH 3/8] oops --- .../src/routes/remote/stream/stream.remote.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/kit/test/apps/basics/src/routes/remote/stream/stream.remote.js b/packages/kit/test/apps/basics/src/routes/remote/stream/stream.remote.js index dc37661ac76d..5ff4d262febc 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/stream/stream.remote.js +++ b/packages/kit/test/apps/basics/src/routes/remote/stream/stream.remote.js @@ -1,12 +1,23 @@ import { command, query } from '$app/server'; +// TODO 3.0 remove this once we support a high enough version of Node.js +function withResolvers() { + /** @type {any} */ + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + let i = 0; -let p = Promise.withResolvers(); +let p = withResolvers(); export const next = command(() => { i++; p.resolve(); - p = Promise.withResolvers(); + p = withResolvers(); }); export const time = query.stream(async function* () { From 9656a7f7616540e0b7622927c96c3aba843d35fe Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Aug 2025 15:45:55 +0200 Subject: [PATCH 4/8] regenerate types --- packages/kit/types/index.d.ts | 309 ++++++++++------------------------ 1 file changed, 87 insertions(+), 222 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index d4b39be365f7..3a66ffbfee0f 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -4,11 +4,7 @@ declare module '@sveltejs/kit' { import type { SvelteConfig } from '@sveltejs/vite-plugin-svelte'; import type { StandardSchemaV1 } from '@standard-schema/spec'; - import type { - RouteId as AppRouteId, - LayoutParams as AppLayoutParams, - ResolvedPathname - } from '$app/types'; + import type { RouteId as AppRouteId, LayoutParams as AppLayoutParams, ResolvedPathname } from '$app/types'; import type { Span } from '@opentelemetry/api'; /** * [Adapters](https://svelte.dev/docs/kit/adapters) are responsible for taking the production build and turning it into something that can be deployed to a platform of your choosing. @@ -273,10 +269,7 @@ declare module '@sveltejs/kit' { * @param name the name of the cookie * @param opts the options, passed directly to `cookie.serialize`. The `path` must match the path of the cookie you want to delete. See documentation [here](https://github.com/jshttp/cookie#cookieserializename-value-options) */ - delete: ( - name: string, - opts: import('cookie').CookieSerializeOptions & { path: string } - ) => void; + delete: (name: string, opts: import('cookie').CookieSerializeOptions & { path: string }) => void; /** * Serialize a cookie name-value pair into a `Set-Cookie` header string, but don't apply it to the response. @@ -1426,10 +1419,7 @@ declare module '@sveltejs/kit' { * but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. * @param input the html chunk and the info if this is the last chunk */ - transformPageChunk?: (input: { - html: string; - done: boolean; - }) => MaybePromise; + transformPageChunk?: (input: { html: string; done: boolean }) => MaybePromise; /** * Determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. * By default, none will be included. @@ -1811,20 +1801,7 @@ declare module '@sveltejs/kit' { /** * The return value of a remote `query.stream` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. */ - export type RemoteQueryStream = RemoteResource & - AsyncIterable> & { - /** - * On the client, this function will re-fetch the query from the server. - * - * On the server, this can be called in the context of a `command` or `form` and the refreshed data will accompany the action response back to the client. - * This prevents SvelteKit needing to refresh all queries on the page in a second server round-trip. - */ - refresh(): Promise; - /** - * Temporarily override the value of a query. This is used with the `updates` method of a [command](https://svelte.dev/docs/kit/remote-functions#command-Updating-queries) or [enhanced form submission](https://svelte.dev/docs/kit/remote-functions#form-enhance) to provide optimistic updates. - */ - withOverride(update: (current: Awaited) => Awaited): RemoteQueryOverride; - }; + export type RemoteQueryStream = RemoteResource & AsyncIterable>; /** * The return value of a remote `query.stream` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. @@ -1862,9 +1839,7 @@ declare module '@sveltejs/kit' { * A function that is invoked once the entry has been created. This is where you * should write the function to the filesystem and generate redirect manifests. */ - complete(entry: { - generateManifest(opts: { relativePath: string }): string; - }): MaybePromise; + complete(entry: { generateManifest(opts: { relativePath: string }): string }): MaybePromise; } // Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts @@ -2329,24 +2304,16 @@ declare module '@sveltejs/kit' { * @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling. * @throws {Error} If the provided status is invalid (not between 400 and 599). */ - export function error( - status: number, - body?: { - message: string; - } extends App.Error - ? App.Error | string | undefined - : never - ): never; + export function error(status: number, body?: { + message: string; + } extends App.Error ? App.Error | string | undefined : never): never; /** * Checks whether this is an error thrown by {@link error}. * @param status The status to filter for. * */ - export function isHttpError( - e: unknown, - status?: T - ): e is HttpError_1 & { + export function isHttpError(e: unknown, status?: T): e is (HttpError_1 & { status: T extends undefined ? never : T; - }; + }); /** * Redirect a request. When called during request handling, SvelteKit will return a redirect response. * Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it. @@ -2363,10 +2330,7 @@ declare module '@sveltejs/kit' { * @throws {Redirect} This error instructs SvelteKit to redirect to the specified location. * @throws {Error} If the provided status is invalid. * */ - export function redirect( - status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), - location: string | URL - ): never; + export function redirect(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), location: string | URL): never; /** * Checks whether this is a redirect thrown by {@link redirect}. * @param e The object to check. @@ -2418,31 +2382,20 @@ declare module '@sveltejs/kit' { wasNormalized: boolean; denormalize: (url?: string | URL) => URL; }; - export type LessThan< - TNumber extends number, - TArray extends any[] = [] - > = TNumber extends TArray['length'] - ? TArray[number] - : LessThan; - export type NumericRange = Exclude< - TEnd | LessThan, - LessThan - >; + export type LessThan = TNumber extends TArray["length"] ? TArray[number] : LessThan; + export type NumericRange = Exclude, LessThan>; export const VERSION: string; class HttpError_1 { - constructor( - status: number, - body: { - message: string; - } extends App.Error - ? App.Error | string | undefined - : App.Error - ); + + constructor(status: number, body: { + message: string; + } extends App.Error ? (App.Error | string | undefined) : App.Error); status: number; body: App.Error; toString(): string; } class Redirect_1 { + constructor(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string); status: 301 | 302 | 303 | 307 | 308 | 300 | 304 | 305 | 306; location: string; @@ -2529,20 +2482,13 @@ declare module '@sveltejs/kit/hooks' { } declare module '@sveltejs/kit/node' { - export function getRequest({ - request, - base, - bodySizeLimit - }: { - request: import('http').IncomingMessage; + export function getRequest({ request, base, bodySizeLimit }: { + request: import("http").IncomingMessage; base: string; bodySizeLimit?: number; }): Promise; - export function setResponse( - res: import('http').ServerResponse, - response: Response - ): Promise; + export function setResponse(res: import("http").ServerResponse, response: Response): Promise; /** * Converts a file on disk to a readable stream * @since 2.4.0 @@ -2567,7 +2513,7 @@ declare module '@sveltejs/kit/vite' { /** * Returns the SvelteKit Vite plugins. * */ - export function sveltekit(): Promise; + export function sveltekit(): Promise; export {}; } @@ -2615,10 +2561,7 @@ declare module '$app/forms' { * } * ``` * */ - export function deserialize< - Success extends Record | undefined, - Failure extends Record | undefined - >(result: string): import('@sveltejs/kit').ActionResult; + export function deserialize | undefined, Failure extends Record | undefined>(result: string): import("@sveltejs/kit").ActionResult; /** * This action enhances a `` element that otherwise would work without JavaScript. * @@ -2642,23 +2585,14 @@ declare module '$app/forms' { * @param form_element The form element * @param submit Submit callback */ - export function enhance< - Success extends Record | undefined, - Failure extends Record | undefined - >( - form_element: HTMLFormElement, - submit?: import('@sveltejs/kit').SubmitFunction - ): { + export function enhance | undefined, Failure extends Record | undefined>(form_element: HTMLFormElement, submit?: import("@sveltejs/kit").SubmitFunction): { destroy(): void; }; /** * This action updates the `form` property of the current page with the given data and updates `page.status`. * In case of an error, it redirects to the nearest error page. * */ - export function applyAction< - Success extends Record | undefined, - Failure extends Record | undefined - >(result: import('@sveltejs/kit').ActionResult): Promise; + export function applyAction | undefined, Failure extends Record | undefined>(result: import("@sveltejs/kit").ActionResult): Promise; export {}; } @@ -2669,9 +2603,7 @@ declare module '$app/navigation' { * * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function afterNavigate( - callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void - ): void; + export function afterNavigate(callback: (navigation: import("@sveltejs/kit").AfterNavigate) => void): void; /** * A navigation interceptor that triggers before we navigate to a URL, whether by clicking a link, calling `goto(...)`, or using the browser back/forward controls. * @@ -2683,9 +2615,7 @@ declare module '$app/navigation' { * * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function beforeNavigate( - callback: (navigation: import('@sveltejs/kit').BeforeNavigate) => void - ): void; + export function beforeNavigate(callback: (navigation: import("@sveltejs/kit").BeforeNavigate) => void): void; /** * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations. * @@ -2695,9 +2625,7 @@ declare module '$app/navigation' { * * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function onNavigate( - callback: (navigation: import('@sveltejs/kit').OnNavigate) => MaybePromise<(() => void) | void> - ): void; + export function onNavigate(callback: (navigation: import("@sveltejs/kit").OnNavigate) => MaybePromise<(() => void) | void>): void; /** * If called when the page is being updated following a navigation (in `onMount` or `afterNavigate` or an action, for example), this disables SvelteKit's built-in scroll handling. * This is generally discouraged, since it breaks user expectations. @@ -2712,17 +2640,14 @@ declare module '$app/navigation' { * @param url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://svelte.dev/docs/kit/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. * @param {Object} opts Options related to the navigation * */ - export function goto( - url: string | URL, - opts?: { - replaceState?: boolean | undefined; - noScroll?: boolean | undefined; - keepFocus?: boolean | undefined; - invalidateAll?: boolean | undefined; - invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined; - state?: App.PageState | undefined; - } - ): Promise; + export function goto(url: string | URL, opts?: { + replaceState?: boolean | undefined; + noScroll?: boolean | undefined; + keepFocus?: boolean | undefined; + invalidateAll?: boolean | undefined; + invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined; + state?: App.PageState | undefined; + }): Promise; /** * Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated. * @@ -2749,9 +2674,7 @@ declare module '$app/navigation' { * Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run (unless disabled via the option argument). * Returns a `Promise` that resolves when the page is subsequently updated. * */ - export function refreshAll({ - includeLoadFunctions - }?: { + export function refreshAll({ includeLoadFunctions }?: { includeLoadFunctions?: boolean; }): Promise; /** @@ -2765,17 +2688,14 @@ declare module '$app/navigation' { * * @param href Page to preload * */ - export function preloadData(href: string): Promise< - | { - type: 'loaded'; - status: number; - data: Record; - } - | { - type: 'redirect'; - location: string; - } - >; + export function preloadData(href: string): Promise<{ + type: "loaded"; + status: number; + data: Record; + } | { + type: "redirect"; + location: string; + }>; /** * Programmatically imports the code for routes that haven't yet been fetched. * Typically, you might call this to speed up subsequent navigation. @@ -2877,14 +2797,7 @@ declare module '$app/paths' { } declare module '$app/server' { - import type { - RequestEvent, - RemoteCommand, - RemoteForm, - RemotePrerenderFunction, - RemoteQueryFunction, - RemoteQueryStreamFunction - } from '@sveltejs/kit'; + import type { RequestEvent, RemoteCommand, RemoteForm, RemotePrerenderFunction, RemoteQueryFunction, RemoteQueryStreamFunction } from '@sveltejs/kit'; import type { StandardSchemaV1 } from '@standard-schema/spec'; /** * Read the contents of an imported asset from the filesystem @@ -2922,10 +2835,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function command( - validate: 'unchecked', - fn: (arg: Input) => Output - ): RemoteCommand; + export function command(validate: "unchecked", fn: (arg: Input) => Output): RemoteCommand; /** * Creates a remote command. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -2933,10 +2843,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function command( - validate: Schema, - fn: (arg: StandardSchemaV1.InferOutput) => Output - ): RemoteCommand, Output>; + export function command(validate: Schema, fn: (arg: StandardSchemaV1.InferOutput) => Output): RemoteCommand, Output>; /** * Creates a form object that can be spread onto a `` element. * @@ -2952,15 +2859,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender( - fn: () => MaybePromise, - options?: - | { - inputs?: RemotePrerenderInputsGenerator; - dynamic?: boolean; - } - | undefined - ): RemotePrerenderFunction; + export function prerender(fn: () => MaybePromise, options?: { + inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean; + } | undefined): RemotePrerenderFunction; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -2968,16 +2870,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender( - validate: 'unchecked', - fn: (arg: Input) => MaybePromise, - options?: - | { - inputs?: RemotePrerenderInputsGenerator; - dynamic?: boolean; - } - | undefined - ): RemotePrerenderFunction; + export function prerender(validate: "unchecked", fn: (arg: Input) => MaybePromise, options?: { + inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean; + } | undefined): RemotePrerenderFunction; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -2985,16 +2881,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender( - schema: Schema, - fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise, - options?: - | { - inputs?: RemotePrerenderInputsGenerator>; - dynamic?: boolean; - } - | undefined - ): RemotePrerenderFunction, Output>; + export function prerender(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise, options?: { + inputs?: RemotePrerenderInputsGenerator>; + dynamic?: boolean; + } | undefined): RemotePrerenderFunction, Output>; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3010,10 +2900,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function query( - validate: 'unchecked', - fn: (arg: Input) => MaybePromise - ): RemoteQueryFunction; + export function query(validate: "unchecked", fn: (arg: Input) => MaybePromise): RemoteQueryFunction; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3021,10 +2908,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function query( - schema: Schema, - fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise - ): RemoteQueryFunction, Output>; + export function query(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise): RemoteQueryFunction, Output>; export namespace query { /** * Creates a batch query function that collects multiple calls and executes them in a single request @@ -3033,10 +2917,7 @@ declare module '$app/server' { * * @since 2.35 */ - function batch( - validate: 'unchecked', - fn: (args: Input[]) => MaybePromise - ): RemoteQueryFunction; + function batch(validate: "unchecked", fn: (args: Input[]) => MaybePromise): RemoteQueryFunction; /** * Creates a batch query function that collects multiple calls and executes them in a single request * @@ -3044,44 +2925,31 @@ declare module '$app/server' { * * @since 2.35 */ - function batch( - schema: Schema, - fn: (args: StandardSchemaV1.InferOutput[]) => MaybePromise - ): RemoteQueryFunction, Output>; + function batch(schema: Schema, fn: (args: StandardSchemaV1.InferOutput[]) => MaybePromise): RemoteQueryFunction, Output>; /** * Creates a streaming remote query. When called from the browser, the generator function will be invoked on the server and values will be streamed via Server-Sent Events. * - * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.stream) for full documentation. * * @since 2.36 */ - function stream( - fn: () => Generator | AsyncGenerator - ): RemoteQueryStreamFunction; + function stream(fn: () => Generator | AsyncGenerator): RemoteQueryStreamFunction; /** * Creates a streaming remote query. When called from the browser, the generator function will be invoked on the server and values will be streamed via Server-Sent Events. * - * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.stream) for full documentation. * * @since 2.36 */ - function stream( - validate: 'unchecked', - fn: (arg: Input) => Generator | AsyncGenerator - ): RemoteQueryStreamFunction; + function stream(validate: "unchecked", fn: (arg: Input) => Generator | AsyncGenerator): RemoteQueryStreamFunction; /** * Creates a streaming remote query. When called from the browser, the generator function will be invoked on the server and values will be streamed via Server-Sent Events. * - * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation. + * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.stream) for full documentation. * * @since 2.36 */ - function stream( - schema: Schema, - fn: ( - arg: StandardSchemaV1.InferOutput - ) => Generator | AsyncGenerator - ): RemoteQueryStreamFunction, Output>; + function stream(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => Generator | AsyncGenerator): RemoteQueryStreamFunction, Output>; } type RemotePrerenderInputsGenerator = () => MaybePromise; type MaybePromise = T | Promise; @@ -3126,21 +2994,19 @@ declare module '$app/state' { * On the server, values can only be read during rendering (in other words _not_ in e.g. `load` functions). In the browser, the values can be read at any time. * * */ - export const page: import('@sveltejs/kit').Page; + export const page: import("@sveltejs/kit").Page; /** * A read-only object representing an in-progress navigation, with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. * Values are `null` when no navigation is occurring, or during server rendering. * */ - export const navigating: - | import('@sveltejs/kit').Navigation - | { - from: null; - to: null; - type: null; - willUnload: null; - delta: null; - complete: null; - }; + export const navigating: import("@sveltejs/kit").Navigation | { + from: null; + to: null; + type: null; + willUnload: null; + delta: null; + complete: null; + }; /** * A read-only reactive value that's initially `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update `current` to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. * */ @@ -3154,10 +3020,11 @@ declare module '$app/state' { declare module '$app/stores' { export function getStores(): { + page: typeof page; - + navigating: typeof navigating; - + updated: typeof updated; }; /** @@ -3167,7 +3034,7 @@ declare module '$app/stores' { * * @deprecated Use `page` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const page: import('svelte/store').Readable; + export const page: import("svelte/store").Readable; /** * A readable store. * When navigating starts, its value is a `Navigation` object with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. @@ -3177,9 +3044,7 @@ declare module '$app/stores' { * * @deprecated Use `navigating` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const navigating: import('svelte/store').Readable< - import('@sveltejs/kit').Navigation | null - >; + export const navigating: import("svelte/store").Readable; /** * A readable store whose initial value is `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. * @@ -3187,12 +3052,12 @@ declare module '$app/stores' { * * @deprecated Use `updated` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const updated: import('svelte/store').Readable & { + export const updated: import("svelte/store").Readable & { check(): Promise; }; export {}; -} /** +}/** * It's possible to tell SvelteKit how to type objects inside your app by declaring the `App` namespace. By default, a new project will have a file called `src/app.d.ts` containing the following: * * ```ts @@ -3328,4 +3193,4 @@ declare module '$app/types' { export type Asset = ReturnType; } -//# sourceMappingURL=index.d.ts.map +//# sourceMappingURL=index.d.ts.map \ No newline at end of file From 6665ed6eed19aa129049d65ed1a98d488ea10d97 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Aug 2025 22:40:35 +0200 Subject: [PATCH 5/8] thanks for nothing service workers --- documentation/docs/20-core-concepts/60-remote-functions.md | 2 ++ packages/kit/test/apps/basics/src/service-worker.js | 4 ++++ packages/kit/test/apps/options-2/src/service-worker.js | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/documentation/docs/20-core-concepts/60-remote-functions.md b/documentation/docs/20-core-concepts/60-remote-functions.md index d350504b52c6..fbba7304a56e 100644 --- a/documentation/docs/20-core-concepts/60-remote-functions.md +++ b/documentation/docs/20-core-concepts/60-remote-functions.md @@ -281,6 +281,8 @@ Apart from that you can iterate over it like any other async iterable, including Stream requests to the same resource with the same payload are deduplicated, i.e. you cannot start the same stream multiple times in parallel and it to start from the beginning each time. +> [!NOTE] Be careful when using `query.stream` in combination with service workers. Specifically, make sure to never pass the promise of a `ReadableStream` (which `query.stream` uses) to `event.respondWith(...)`, as the promise never settles. + ## form The `form` function makes it easy to write data to the server. It takes a callback that receives the current [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)... diff --git a/packages/kit/test/apps/basics/src/service-worker.js b/packages/kit/test/apps/basics/src/service-worker.js index fc3dbc27600f..e3a2aedd2b4b 100644 --- a/packages/kit/test/apps/basics/src/service-worker.js +++ b/packages/kit/test/apps/basics/src/service-worker.js @@ -25,6 +25,10 @@ self.addEventListener('fetch', (event) => { if (request.method !== 'GET' || request.headers.has('range')) return; + // Skip EventSource requests to prevent connection issues + const acceptHeader = request.headers.get('accept'); + if (acceptHeader && acceptHeader.includes('text/event-stream')) return; + const url = new URL(request.url); const cached = caches.match(request); diff --git a/packages/kit/test/apps/options-2/src/service-worker.js b/packages/kit/test/apps/options-2/src/service-worker.js index 5d2346ffc333..4c99909412a2 100644 --- a/packages/kit/test/apps/options-2/src/service-worker.js +++ b/packages/kit/test/apps/options-2/src/service-worker.js @@ -32,6 +32,10 @@ self.addEventListener('fetch', (event) => { if (request.method !== 'GET' || request.headers.has('range')) return; + // Skip EventSource requests to prevent connection issues + const acceptHeader = request.headers.get('accept'); + if (acceptHeader && acceptHeader.includes('text/event-stream')) return; + const url = new URL(request.url); const cached = caches.match(request); From 28a3564a60a1e9016d5bf6418203f4de24bf18b0 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Aug 2025 22:40:46 +0200 Subject: [PATCH 6/8] tweak readablestream implementation --- .../client/remote-functions/query.svelte.js | 4 +- packages/kit/src/runtime/server/remote.js | 39 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/kit/src/runtime/client/remote-functions/query.svelte.js b/packages/kit/src/runtime/client/remote-functions/query.svelte.js index 5bcdca500fd6..2450087b33bd 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -216,8 +216,8 @@ class QueryStream { return; } - this.#current = parsed; - this.#resolve({ done: false, value: parsed }); + this.#current = parsed.value; + this.#resolve({ done: false, value: parsed.value }); this.#create_promise(); }; diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index eb23e6c47f1c..e4b5bbf8bc2b 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -144,29 +144,32 @@ async function handle_remote_call_internal(event, state, options, manifest, id) fn(parse_remote_arg(payload, transport)) ); - // Return a Server-Sent Events stream + // Return a Server-Sent Events stream using the pull method to consume the async iterator let cancelled = false; + const encoder = new TextEncoder(); + const iterator = generator[Symbol.asyncIterator](); return new Response( new ReadableStream({ - async start(controller) { - const encoder = new TextEncoder(); - + async pull(controller) { try { - for await (const value of generator) { - if (cancelled) break; - const serialized = stringify(value, transport); - const chunk = `data: ${serialized}\n\n`; - controller.enqueue(encoder.encode(chunk)); + const { value, done } = await iterator.next(); + if (cancelled) return; + + if (done) { + // Send end marker + controller.enqueue(encoder.encode('data: [DONE]\n\n')); + controller.close(); + return; } - - // Send end marker - controller.enqueue(encoder.encode('data: [DONE]\n\n')); - controller.close(); + const serialized = stringify({ type: 'data', value }, transport); + const chunk = `data: ${serialized}\n\n`; + controller.enqueue(encoder.encode(chunk)); } catch (error) { - console.error(error); - // Send error and close const errorData = await handle_error_and_jsonify(event, state, options, error); + + if (cancelled) return; + const serialized = stringify( { type: 'error', @@ -186,13 +189,15 @@ async function handle_remote_call_internal(event, state, options, manifest, id) cancel() { cancelled = true; + if (iterator.return) { + iterator.return(); + } } }), { headers: { 'content-type': 'text/event-stream', - 'cache-control': 'private, no-store', - connection: 'keep-alive' + 'cache-control': 'private, no-store' } } ); From 531225cd0ad28b31e720e826938cda40b99c37ac Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Aug 2025 23:13:39 +0200 Subject: [PATCH 7/8] fix --- .../src/runtime/client/remote-functions/query.svelte.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/remote-functions/query.svelte.js b/packages/kit/src/runtime/client/remote-functions/query.svelte.js index 2450087b33bd..b210fadf4af7 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -186,6 +186,7 @@ class QueryStream { this.#resolve = resolve; this.#reject = reject; }); + this.#promise.catch(() => {}); // don't let unhandled rejections bubble up } #next() { @@ -244,10 +245,13 @@ class QueryStream { if (!this.#ready) { return this.#promise.then((v) => v.value).then(resolve, reject); } else { + // We return/reject right away instead of waiting on the promise, + // else we would end up in a constant pending state since the next + // promise is created right after the previous one is resolved. if (this.#error) { - return reject(this.#error); + return Promise.reject(this.#error).then(undefined, reject); } else { - return resolve(this.#current); + return Promise.resolve(this.#current).then(resolve); } } }; @@ -288,6 +292,7 @@ class QueryStream { this.#unused = true; if (this.#count === 0) { this.#source?.close(); + this.#reject?.(); } } From a649cf0095c0a77482dca0b4e3de6a271b5cfcbd Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 25 Aug 2025 21:36:07 +0200 Subject: [PATCH 8/8] don't share stream() --- .../20-core-concepts/60-remote-functions.md | 18 +++++++++++++++++- .../client/remote-functions/query.svelte.js | 14 ++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/documentation/docs/20-core-concepts/60-remote-functions.md b/documentation/docs/20-core-concepts/60-remote-functions.md index 6a968be35075..853314cab47b 100644 --- a/documentation/docs/20-core-concepts/60-remote-functions.md +++ b/documentation/docs/20-core-concepts/60-remote-functions.md @@ -289,7 +289,23 @@ Apart from that you can iterate over it like any other async iterable, including {/each} ``` -Stream requests to the same resource with the same payload are deduplicated, i.e. you cannot start the same stream multiple times in parallel and it to start from the beginning each time. +Unlike other `query` methods, stream requests to the same resource with the same payload are _not_ deduplicated. That means you can start the same stream multiple times in parallel and it will start from the beginning each time. + +```svelte + + + + +{#await stream()} +{#await stream()} + + +{await oneToTen()} +``` > [!NOTE] Be careful when using `query.stream` in combination with service workers. Specifically, make sure to never pass the promise of a `ReadableStream` (which `query.stream` uses) to `event.respondWith(...)`, as the promise never settles. diff --git a/packages/kit/src/runtime/client/remote-functions/query.svelte.js b/packages/kit/src/runtime/client/remote-functions/query.svelte.js index 51e5efd947af..d168ac44707b 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -6,6 +6,7 @@ import { tick } from 'svelte'; import { create_remote_function, remote_request } from './shared.svelte.js'; import * as devalue from 'devalue'; import { HttpError, Redirect } from '@sveltejs/kit/internal'; +import { stringify_remote_arg } from '../../shared.js'; /** * @param {string} id @@ -124,10 +125,11 @@ export function query_batch(id) { * @returns {RemoteQueryStreamFunction} */ export function query_stream(id) { - return create_remote_function(id, (_, payload) => { - const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`; + // @ts-expect-error [Symbol.toStringTag] missing + return (payload) => { + const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${stringify_remote_arg(payload, app.hooks.transport)}` : ''}`; return new QueryStream(url); - }); + }; } /** @@ -247,7 +249,7 @@ class QueryStream { this.#source.addEventListener('error', onError); } - get then() { + #then = $derived.by(() => { this.#current; /** @@ -270,6 +272,10 @@ class QueryStream { } } }; + }); + + get then() { + return this.#then; } get catch() {