summaryrefslogtreecommitdiff
path: root/ui/lib/outbox.svelte.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/lib/outbox.svelte.js')
-rw-r--r--ui/lib/outbox.svelte.js111
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();
+ }
+ }
+ }
+}