After reading the upload streaming article I thought that fetch could be used for bi-directional streaming using a single request, essentially WebTransport streams using Fetch.
So the connection remains persisten AFAICT, a dedicated streaming message channel. Where we can of course substitute streaming other data for echoing the input.
The only cost I observed was the request being stalled in Network panel of DevTools. I did a little reading on the subject matter and the opinions are wide ranging, so I thought I'd ask the experts
<!DOCTYPE html>
<html>
<head> </head>
<body>
<input type="text" />
<output></output>
<script>
async function halfDuplexStream() {
let controller;
const stream = new ReadableStream({
start(_) {
return (controller = _);
},
}).pipeThrough(new TextEncoderStream());
const output = document.querySelector('output');
const input = document.querySelector('input');
input.onchange = (e) => {
controller.enqueue(e.target.value);
};
input.onselect = (e) => {
input.value = '';
};
fetch('./?stream', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: stream,
duplex: 'half',
})
.then((r) =>
r.body.pipeThrough(new TextDecoderStream()).pipeTo(
new WritableStream({
write(value) {
output.textContent = value;
},
close() {
console.log('Stream closed');
},
abort(reason) {
console.log({ reason });
},
})
)
)
.then(console.log)
.catch(console.warn);
}
navigator.serviceWorker
.getRegistrations()
.then((r) => Promise.all(r.map((s) => s.unregister())))
.then(() =>
navigator.serviceWorker.register('sw.js', {
scope: './',
})
)
.then((s) => {
return new Promise((resolve) => {
navigator.serviceWorker.addEventListener(
'controllerchange',
(e) => {
console.log(e);
resolve();
},
{ once: true }
);
});
})
.then(halfDuplexStream)
.catch(console.error);
</script>
</body>
</html>
A ServiceWorker which can be substituted for Deno's internal serveTls server which implements Fetch, and ServiceWorker style server with half-duplex streaming
self.addEventListener('install', (event) => {
console.log(event);
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', async (event) => {
console.log(event);
event.waitUntil(self.clients.claim());
console.log(await self.clients);
});
onfetch = (e) => {
if (e.request.url.includes('stream')) {
let client = clients.get(e.clientId);
e.respondWith(
new Response(
e.request.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(
new TransformStream({
transform(value, c) {
c.enqueue(value.toUpperCase());
},
flush() {
console.log('flush');
},
})
)
.pipeThrough(new TextEncoderStream())
)
);
}
};