diff options
Diffstat (limited to 'ui/lib/outbox.svelte.js')
| -rw-r--r-- | ui/lib/outbox.svelte.js | 111 |
1 files changed, 111 insertions, 0 deletions
diff --git a/ui/lib/outbox.svelte.js b/ui/lib/outbox.svelte.js new file mode 100644 index 0000000..472c58b --- /dev/null +++ b/ui/lib/outbox.svelte.js @@ -0,0 +1,111 @@ +import { DateTime } from 'luxon'; +import * as msg from './state/remote/messages.svelte.js'; + +import * as api from './apiServer.js'; +import * as md from './markdown.js'; + +class PostToChannel { + constructor(channel, body) { + this.channel = channel; + this.body = body; + this.at = DateTime.now(); + this.renderedBody = md.render(body); + } + + toSkeleton(sender) { + return { + id: null, + at: this.at, + channel: this.channel, + sender, + body: this.body, + renderedBody: this.renderedBody + }; + } + + async send() { + return await api.retry(() => api.postToChannel(this.channel, this.body)); + } +} + +class DeleteMessage { + constructor(messageId) { + this.messageId = messageId; + } + + async send() { + return await api.retry(() => api.deleteMessage(this.messageId)); + } +} + +class CreateChannel { + constructor(name) { + this.name = name; + } + + async send() { + return await api.retry(() => api.createChannel(this.name)); + } +} + +export class Outbox { + pending = $state([]); + messages = $derived(this.pending.filter((operation) => operation instanceof PostToChannel)); + deleted = $derived(this.pending.filter((operation) => operation instanceof DeleteMessage)); + + static empty() { + return new Outbox([]); + } + + constructor(pending) { + this.pending = pending; + } + + enqueue(operation) { + this.pending.push(operation); + this.start(); + } + + createChannel(name) { + this.enqueue(new CreateChannel(name)); + } + + postToChannel(channel, body) { + this.enqueue(new PostToChannel(channel, body)); + } + + deleteMessage(messageId) { + this.enqueue(new DeleteMessage(messageId)); + } + + start() { + if (this.sending) { + return; + } + // This is a promise transform primarily to keep the management of `this.sending` in one place, + // rather than spreading it across multiple methods. + this.sending = this.drain().finally(() => { + this.sending = null; + + // If we encounter an exception processing the pending queue, it may have an operation left + // in it. If so, start over. The exception will still propagate out (though since nothing + // ever awaits the promise from this.sending, it'll ultimately leak out to the browser + // anyways). + if (this.pending.length > 0) { + this.start(); + } + }); + } + + async drain() { + while (this.pending.length > 0) { + const operation = this.pending[0]; + + try { + await operation.send(); + } finally { + this.pending.shift(); + } + } + } +} |
