atagen

malice

public
Star0
Watch0
35a820422568f7c14365c307cb9fa13dcedce3da
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/pending.ts

973f8fbdaf80atagen2026-09-28 09:10 UTC1
import { get, writable } from 'svelte/store';
973f8fbdaf80atagen2026-09-28 09:10 UTC2
import { sendMessage } from './api';
6d6a4c34bdc1atagen2026-10-24 06:51 UTC3
import { formatBody } from './markdown';
973f8fbdaf80atagen2026-09-28 09:10 UTC4
import { timelines } from './store';
05a48e9e7942atagen2026-10-24 09:32 UTC5
import type { Emote } from './emotes';
cf23dbf4e77catagen2026-10-23 06:36 UTC6
import type { Mention } from './mentions';
df3c0f2b3da4atagen2026-10-22 13:07 UTC7
import type { ReplyReference, TimelineItem } from './types';
973f8fbdaf80atagen2026-09-28 09:10 UTC8
973f8fbdaf80atagen2026-09-28 09:10 UTC9
export interface PendingMessage {
973f8fbdaf80atagen2026-09-28 09:10 UTC10
  id: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC11
  body: string;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC12
  formatted: string | null;
df3c0f2b3da4atagen2026-10-22 13:07 UTC13
  replyTo: ReplyReference | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC14
  status: 'sending' | 'failed';
973f8fbdaf80atagen2026-09-28 09:10 UTC15
  error: string | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC16
  createdAt: number;
973f8fbdaf80atagen2026-09-28 09:10 UTC17
}
973f8fbdaf80atagen2026-09-28 09:10 UTC18
973f8fbdaf80atagen2026-09-28 09:10 UTC19
export const pendingByRoom = writable<Record<string, PendingMessage[]>>({});
973f8fbdaf80atagen2026-09-28 09:10 UTC20
973f8fbdaf80atagen2026-09-28 09:10 UTC21
interface PendingRecord extends PendingMessage {
973f8fbdaf80atagen2026-09-28 09:10 UTC22
  roomId: string;
cf23dbf4e77catagen2026-10-23 06:36 UTC23
  mentions: Mention[];
6d6a4c34bdc1atagen2026-10-24 06:51 UTC24
  markdown: boolean;
05a48e9e7942atagen2026-10-24 09:32 UTC25
  emotes: Emote[];
973f8fbdaf80atagen2026-09-28 09:10 UTC26
  knownIds: Set<string>;
973f8fbdaf80atagen2026-09-28 09:10 UTC27
}
973f8fbdaf80atagen2026-09-28 09:10 UTC28
973f8fbdaf80atagen2026-09-28 09:10 UTC29
const records = new Map<string, PendingRecord>();
973f8fbdaf80atagen2026-09-28 09:10 UTC30
const queues = new Map<string, Promise<void>>();
973f8fbdaf80atagen2026-09-28 09:10 UTC31
let nextSeq = 1;
973f8fbdaf80atagen2026-09-28 09:10 UTC32
973f8fbdaf80atagen2026-09-28 09:10 UTC33
function nextId(): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC34
  return `pending-${nextSeq++}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC35
}
973f8fbdaf80atagen2026-09-28 09:10 UTC36
973f8fbdaf80atagen2026-09-28 09:10 UTC37
function ownIdentities(items: TimelineItem[]): Set<string> {
973f8fbdaf80atagen2026-09-28 09:10 UTC38
  const out = new Set<string>();
973f8fbdaf80atagen2026-09-28 09:10 UTC39
  for (const item of items) {
973f8fbdaf80atagen2026-09-28 09:10 UTC40
    if (item.kind !== 'event' || !item.event.own) continue;
973f8fbdaf80atagen2026-09-28 09:10 UTC41
    const id = item.event.transaction_id ?? item.event.event_id;
973f8fbdaf80atagen2026-09-28 09:10 UTC42
    if (id) out.add(id);
973f8fbdaf80atagen2026-09-28 09:10 UTC43
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC44
  return out;
973f8fbdaf80atagen2026-09-28 09:10 UTC45
}
973f8fbdaf80atagen2026-09-28 09:10 UTC46
973f8fbdaf80atagen2026-09-28 09:10 UTC47
function toPublic(record: PendingRecord): PendingMessage {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC48
  return { id: record.id, body: record.body, formatted: record.formatted, replyTo: record.replyTo, status: record.status, error: record.error, createdAt: record.createdAt };
973f8fbdaf80atagen2026-09-28 09:10 UTC49
}
973f8fbdaf80atagen2026-09-28 09:10 UTC50
973f8fbdaf80atagen2026-09-28 09:10 UTC51
function roomList(roomId: string): PendingMessage[] {
973f8fbdaf80atagen2026-09-28 09:10 UTC52
  return get(pendingByRoom)[roomId] ?? [];
973f8fbdaf80atagen2026-09-28 09:10 UTC53
}
973f8fbdaf80atagen2026-09-28 09:10 UTC54
973f8fbdaf80atagen2026-09-28 09:10 UTC55
function setRoom(roomId: string, list: PendingMessage[]): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC56
  pendingByRoom.update((all) => ({ ...all, [roomId]: list }));
973f8fbdaf80atagen2026-09-28 09:10 UTC57
}
973f8fbdaf80atagen2026-09-28 09:10 UTC58
973f8fbdaf80atagen2026-09-28 09:10 UTC59
function addRecord(record: PendingRecord): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC60
  records.set(record.id, record);
973f8fbdaf80atagen2026-09-28 09:10 UTC61
  setRoom(record.roomId, [...roomList(record.roomId), toPublic(record)]);
973f8fbdaf80atagen2026-09-28 09:10 UTC62
}
973f8fbdaf80atagen2026-09-28 09:10 UTC63
973f8fbdaf80atagen2026-09-28 09:10 UTC64
function updateRecord(id: string, patch: Partial<Pick<PendingRecord, 'status' | 'error'>>): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC65
  const record = records.get(id);
973f8fbdaf80atagen2026-09-28 09:10 UTC66
  if (!record) return;
973f8fbdaf80atagen2026-09-28 09:10 UTC67
  Object.assign(record, patch);
973f8fbdaf80atagen2026-09-28 09:10 UTC68
  setRoom(record.roomId, roomList(record.roomId).map((p) => (p.id === id ? toPublic(record) : p)));
973f8fbdaf80atagen2026-09-28 09:10 UTC69
}
973f8fbdaf80atagen2026-09-28 09:10 UTC70
973f8fbdaf80atagen2026-09-28 09:10 UTC71
function removeRecord(id: string): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC72
  const record = records.get(id);
973f8fbdaf80atagen2026-09-28 09:10 UTC73
  if (!record) return;
973f8fbdaf80atagen2026-09-28 09:10 UTC74
  records.delete(id);
973f8fbdaf80atagen2026-09-28 09:10 UTC75
  setRoom(record.roomId, roomList(record.roomId).filter((p) => p.id !== id));
973f8fbdaf80atagen2026-09-28 09:10 UTC76
}
973f8fbdaf80atagen2026-09-28 09:10 UTC77
973f8fbdaf80atagen2026-09-28 09:10 UTC78
async function attemptSend(record: PendingRecord): Promise<void> {
973f8fbdaf80atagen2026-09-28 09:10 UTC79
  if (!records.has(record.id)) return;
973f8fbdaf80atagen2026-09-28 09:10 UTC80
  updateRecord(record.id, { status: 'sending', error: null });
973f8fbdaf80atagen2026-09-28 09:10 UTC81
  try {
05a48e9e7942atagen2026-10-24 09:32 UTC82
    await sendMessage(record.roomId, record.body, record.replyTo?.event_id ?? null, record.mentions, record.markdown, 'text', record.emotes);
973f8fbdaf80atagen2026-09-28 09:10 UTC83
  } catch (e) {
973f8fbdaf80atagen2026-09-28 09:10 UTC84
    updateRecord(record.id, { status: 'failed', error: e instanceof Error ? e.message : 'Message could not be sent' });
973f8fbdaf80atagen2026-09-28 09:10 UTC85
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC86
}
973f8fbdaf80atagen2026-09-28 09:10 UTC87
973f8fbdaf80atagen2026-09-28 09:10 UTC88
function enqueue(record: PendingRecord): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC89
  const previous = queues.get(record.roomId) ?? Promise.resolve();
973f8fbdaf80atagen2026-09-28 09:10 UTC90
  const next = previous.then(() => attemptSend(record));
973f8fbdaf80atagen2026-09-28 09:10 UTC91
  queues.set(record.roomId, next.catch(() => undefined));
973f8fbdaf80atagen2026-09-28 09:10 UTC92
}
973f8fbdaf80atagen2026-09-28 09:10 UTC93
6d6a4c34bdc1atagen2026-10-24 06:51 UTC94
export function queueSend(
6d6a4c34bdc1atagen2026-10-24 06:51 UTC95
  roomId: string,
6d6a4c34bdc1atagen2026-10-24 06:51 UTC96
  body: string,
6d6a4c34bdc1atagen2026-10-24 06:51 UTC97
  replyTo: ReplyReference | null = null,
6d6a4c34bdc1atagen2026-10-24 06:51 UTC98
  mentions: Mention[] = [],
05a48e9e7942atagen2026-10-24 09:32 UTC99
  markdown = false,
05a48e9e7942atagen2026-10-24 09:32 UTC100
  emotes: Emote[] = []
6d6a4c34bdc1atagen2026-10-24 06:51 UTC101
): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC102
  const record: PendingRecord = {
973f8fbdaf80atagen2026-09-28 09:10 UTC103
    id: nextId(),
973f8fbdaf80atagen2026-09-28 09:10 UTC104
    roomId,
973f8fbdaf80atagen2026-09-28 09:10 UTC105
    body,
05a48e9e7942atagen2026-10-24 09:32 UTC106
    formatted: formatBody(body, mentions, markdown, emotes),
df3c0f2b3da4atagen2026-10-22 13:07 UTC107
    replyTo,
cf23dbf4e77catagen2026-10-23 06:36 UTC108
    mentions,
6d6a4c34bdc1atagen2026-10-24 06:51 UTC109
    markdown,
05a48e9e7942atagen2026-10-24 09:32 UTC110
    emotes,
973f8fbdaf80atagen2026-09-28 09:10 UTC111
    status: 'sending',
973f8fbdaf80atagen2026-09-28 09:10 UTC112
    error: null,
973f8fbdaf80atagen2026-09-28 09:10 UTC113
    createdAt: Date.now(),
973f8fbdaf80atagen2026-09-28 09:10 UTC114
    knownIds: ownIdentities(get(timelines)[roomId] ?? [])
973f8fbdaf80atagen2026-09-28 09:10 UTC115
  };
973f8fbdaf80atagen2026-09-28 09:10 UTC116
  addRecord(record);
973f8fbdaf80atagen2026-09-28 09:10 UTC117
  enqueue(record);
973f8fbdaf80atagen2026-09-28 09:10 UTC118
}
973f8fbdaf80atagen2026-09-28 09:10 UTC119
973f8fbdaf80atagen2026-09-28 09:10 UTC120
export function retryPending(id: string): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC121
  const record = records.get(id);
973f8fbdaf80atagen2026-09-28 09:10 UTC122
  if (!record) return;
973f8fbdaf80atagen2026-09-28 09:10 UTC123
  record.knownIds = ownIdentities(get(timelines)[record.roomId] ?? []);
973f8fbdaf80atagen2026-09-28 09:10 UTC124
  enqueue(record);
973f8fbdaf80atagen2026-09-28 09:10 UTC125
}
973f8fbdaf80atagen2026-09-28 09:10 UTC126
973f8fbdaf80atagen2026-09-28 09:10 UTC127
export function discardPending(id: string): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC128
  removeRecord(id);
973f8fbdaf80atagen2026-09-28 09:10 UTC129
}
973f8fbdaf80atagen2026-09-28 09:10 UTC130
973f8fbdaf80atagen2026-09-28 09:10 UTC131
export function resetPending(): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC132
  records.clear();
973f8fbdaf80atagen2026-09-28 09:10 UTC133
  queues.clear();
973f8fbdaf80atagen2026-09-28 09:10 UTC134
  pendingByRoom.set({});
973f8fbdaf80atagen2026-09-28 09:10 UTC135
}
973f8fbdaf80atagen2026-09-28 09:10 UTC136
973f8fbdaf80atagen2026-09-28 09:10 UTC137
function reconcileRoom(roomId: string, items: TimelineItem[]): void {
973f8fbdaf80atagen2026-09-28 09:10 UTC138
  const list = roomList(roomId);
973f8fbdaf80atagen2026-09-28 09:10 UTC139
  if (list.length === 0) return;
973f8fbdaf80atagen2026-09-28 09:10 UTC140
  const ownItems = items.filter((item) => item.kind === 'event' && item.event.own) as Extract<
973f8fbdaf80atagen2026-09-28 09:10 UTC141
    TimelineItem,
973f8fbdaf80atagen2026-09-28 09:10 UTC142
    { kind: 'event' }
973f8fbdaf80atagen2026-09-28 09:10 UTC143
  >[];
973f8fbdaf80atagen2026-09-28 09:10 UTC144
  const claimed = new Set<string>();
973f8fbdaf80atagen2026-09-28 09:10 UTC145
  for (const pending of list) {
973f8fbdaf80atagen2026-09-28 09:10 UTC146
    const record = records.get(pending.id);
973f8fbdaf80atagen2026-09-28 09:10 UTC147
    if (!record) continue;
973f8fbdaf80atagen2026-09-28 09:10 UTC148
    const match = ownItems.find((item) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC149
      if (claimed.has(item.event.item_id)) return false;
973f8fbdaf80atagen2026-09-28 09:10 UTC150
      const id = item.event.transaction_id ?? item.event.event_id;
973f8fbdaf80atagen2026-09-28 09:10 UTC151
      if (id && record.knownIds.has(id)) return false;
973f8fbdaf80atagen2026-09-28 09:10 UTC152
      return item.event.content.kind === 'Message' && item.event.content.message.body === record.body;
973f8fbdaf80atagen2026-09-28 09:10 UTC153
    });
973f8fbdaf80atagen2026-09-28 09:10 UTC154
    if (match) {
973f8fbdaf80atagen2026-09-28 09:10 UTC155
      claimed.add(match.event.item_id);
973f8fbdaf80atagen2026-09-28 09:10 UTC156
      removeRecord(record.id);
973f8fbdaf80atagen2026-09-28 09:10 UTC157
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC158
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC159
}
973f8fbdaf80atagen2026-09-28 09:10 UTC160
973f8fbdaf80atagen2026-09-28 09:10 UTC161
timelines.subscribe((all) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC162
  for (const roomId of Object.keys(get(pendingByRoom))) {
973f8fbdaf80atagen2026-09-28 09:10 UTC163
    reconcileRoom(roomId, all[roomId] ?? []);
973f8fbdaf80atagen2026-09-28 09:10 UTC164
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC165
});