/** * Client-side inbox event emitter. * * The high-level `Inbox` orchestrator emits structural events — message * queued, delivered, polled, decrypted — so apps can drive UI badges, * push hooks, or telemetry without polling internal state. */ export interface InboxClientEventMap { /** * A new ciphertext entry was added to the outgoing queue. The push-hook * mentioned in the V3.6 spec dispatches off this event. */ 'inbox.message_queued': { recipientAddress: string; msgId: string; bytes: number; ttlSeconds: number; }; /** Server confirmed a queued blob landed. */ 'inbox.message_delivered': { recipientAddress: string; msgId: string; idempotent: boolean; }; /** Delivery attempt failed; will retry on next flush. */ 'inbox.message_failed': { recipientAddress: string; msgId: string; attempts: number; error: string; }; /** A poll cycle completed and pulled `count` blobs. */ 'inbox.poll_completed': { ownAddress: string; count: number; cursor: number }; /** Caller successfully decrypted and acked a blob. */ 'inbox.message_received': { senderHint: string | null; msgId: string; }; /** Caller failed to decrypt — typically tampering or stale ratchet. */ 'inbox.message_decrypt_failed': { msgId: string; error: string; }; } export type InboxClientEventName = keyof InboxClientEventMap; export type InboxClientEvent = { [K in InboxClientEventName]: { name: K; data: InboxClientEventMap[K]; timestamp: number }; }[InboxClientEventName]; export type InboxClientListener = (e: InboxClientEvent) => void; export class InboxClientEvents { private readonly listeners = new Set(); on(listener: InboxClientListener): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); } emit(name: K, data: InboxClientEventMap[K]): void { const event = { name, data, timestamp: Date.now() } as InboxClientEvent; for (const l of this.listeners) { try { l(event); } catch (err) { console.error('[Shade] Inbox client listener threw:', err); } } } }