I am making a web app which sends some requests to external APIs which can take some time to complete. The user types information in a form, which is received by a +page.server.ts file in the default form action. Once it is done processing the request, information is displayed to the user.
However, since this takes so long, I wanted to add some messages that I can send to the client to display while they wait to inform them of the status of their request.
Below is a pseudocode-style representation of the functionality I am looking for:
+page.server.js
export const actions = {
default: async ({ request }) => {
sendClientMessage("I'm processing your request!")
await processSomething()
sendClientMessage("Thing 1 done!")
await processOtherThing()
sendClientMessage("Thing 2 done!")
}
}
+page.svelte
<script>
onClientMessage((message) => {
console.log(`Got message from server: ${message}`)
}
</script>
I've seen people suggest using things like websockets and socket.io. This sounds sort of like what I want, but from what I've seen, these require some pretty complicated manual client management to avoid sending messages to every connected client. Is there any simple-ish library that achieves the results I want, or are these going to be my only options? I don't really need two-way communication as the only details the user sends are in the initial request.
Perhaps more importantly, is all of this effort worth it for relatively little functionality?
If anyone has any ideas or advice, it would be greatly appreciated. :-)
Actions do not support incremental updates and as of now, WebSocket support in SvelteKit is still being worked on.
You could use server-sent events via a +server
endpoint as long as your form data can be encoded to query parameters and the platform you want to deploy to does not fully buffer server responses (which would defeat the point of sending incremental updates).
The first argument to
Response
can be aReadableStream
, making it possible to stream large amounts of data or create server-sent events (unless deploying to platforms that buffer responses, like AWS Lambda).
Example for endpoint sending a progress update every second:
// routes/sse/api/+server.js
export const GET = ({ url }) => {
// Getting form data from the URL:
const input = url.searchParams.get('input');
const totalExpectedTicks = 3;
let progressTicks = 0;
let interval = null;
const bodyStream = new ReadableStream({
start(controller) {
function sendUpdate() {
const content = {
message: `Processing for ${input}`,
progress: progressTicks / totalExpectedTicks,
};
controller.enqueue(`data: ${JSON.stringify(content)}\n\n`);
}
sendUpdate();
interval = setInterval(() => {
progressTicks++;
sendUpdate();
if (progressTicks >= totalExpectedTicks) {
clearInterval(interval);
}
}, 1000);
},
cancel() {
// Happens if active connection is interrupted
clearInterval(interval);
},
});
return new Response(bodyStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
});
};
Using an EventSource
to receive the events on the page:
<script>
let updateState = $state(null);
function onSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = new URLSearchParams(formData).toString();
const eventSource = new EventSource('/sse/api?' + data, {
withCredentials: true,
});
eventSource.addEventListener('message', (event) => {
updateState = JSON.parse(event.data);
if (updateState.progress === 1) {
eventSource.close();
}
});
eventSource.addEventListener('error', (event) => {
// [handle errors]
});
}
</script>
<form onsubmit={onSubmit}>
<fieldset disabled={updateState != null && updateState.progress < 1}>
<input type="text" name="input" />
<button type="submit">Submit</button>
</fieldset>
</form>
{#if updateState}
<div>
Status: {updateState.message} <br>
Progress: {Math.round(updateState.progress * 100)}%
</div>
{/if}