import type { InboxItem, InboxState, Message, SearchResult, Ticket, TicketState, } from "@lynkd/messaging-inbox-sdk"; import * as seed from "./data"; /** * In-memory mutable store. Next.js dev/prod route handlers share one Node * process, so a module singleton persists across requests for the session. * This is the ONLY stateful piece of the "backend" — swap it for the IIOS * client and the BFF routes stay identical. */ class MockStore { users = clone(seed.users); channels = clone(seed.channels); messages = clone(seed.messages); inbox = clone(seed.inbox); tickets = clone(seed.tickets); notifications = clone(seed.notifications); availability = clone(seed.availability); private idc = 10000; me() { return this.users.find((u) => u.id === seed.me.id) ?? this.users[0]; } bootstrap() { return { me: this.me(), workspace: seed.workspace, workspaces: seed.workspaces, users: this.users, sections: seed.sections, channels: this.channels, }; } addMember(channelId: string, userId: string) { const c = this.channels.find((x) => x.id === channelId); if (c && !c.memberIds.includes(userId)) c.memberIds.push(userId); return c ?? null; } removeMember(channelId: string, userId: string) { const c = this.channels.find((x) => x.id === channelId); if (c) c.memberIds = c.memberIds.filter((id) => id !== userId); return c ?? null; } markRead(channelId: string) { const c = this.channels.find((x) => x.id === channelId); if (c) { c.unreadCount = 0; c.mentionCount = 0; } return c ?? null; } editMessage(messageId: string, body: string): Message | null { const m = this.messages.find((x) => x.id === messageId); if (!m) return null; m.body = body; m.editedTs = Date.now(); return m; } deleteMessage(messageId: string): boolean { const before = this.messages.length; this.messages = this.messages.filter((m) => m.id !== messageId && m.parentId !== messageId); return this.messages.length !== before; } savedMessages(): Message[] { return this.messages.filter((m) => m.isSaved).sort((a, b) => b.ts - a.ts); } threadParents(): Message[] { return this.messages.filter((m) => (m.replyCount ?? 0) > 0).sort((a, b) => (b.lastReplyTs ?? b.ts) - (a.lastReplyTs ?? a.ts)); } setStatus(text: string | undefined, emoji: string | undefined, clearAt?: number) { const me = this.me(); me.statusText = text; me.statusEmoji = emoji; me.statusClearAt = clearAt; return me; } updateMe(patch: { name?: string; title?: string; avatarColor?: string; avatarUrl?: string | null; presence?: string }) { const me = this.me(); if (patch.name !== undefined) me.name = patch.name; if (patch.title !== undefined) me.title = patch.title; if (patch.avatarColor !== undefined) me.avatarColor = patch.avatarColor; if (patch.avatarUrl !== undefined) me.avatarUrl = patch.avatarUrl ?? undefined; if (patch.presence !== undefined) me.presence = patch.presence as any; return me; } channelMessages(channelId: string): Message[] { return this.messages .filter((m) => m.channelId === channelId && !m.parentId) .sort((a, b) => a.ts - b.ts); } thread(channelId: string, parentId: string) { const parent = this.messages.find((m) => m.id === parentId) ?? null; const replies = this.messages .filter((m) => m.parentId === parentId) .sort((a, b) => a.ts - b.ts); return { parent, replies }; } send( channelId: string, body: string, opts: { parentId?: string; scheduledFor?: number; attachments?: any[]; threadRootId?: string | null } = {}, ): Message { const msg: Message = { id: `m_new_${this.idc++}`, channelId, authorId: seed.me.id, ts: Date.now(), body, reactions: [], deliveryState: "sent", parentId: opts.parentId ?? null, scheduledFor: opts.scheduledFor, attachments: opts.attachments && opts.attachments.length ? opts.attachments : undefined, threadRootId: opts.threadRootId ?? undefined, }; this.messages.push(msg); if (opts.parentId) { const parent = this.messages.find((m) => m.id === opts.parentId); if (parent) { parent.replyCount = (parent.replyCount ?? 0) + 1; parent.lastReplyTs = msg.ts; parent.replyUserIds = Array.from(new Set([...(parent.replyUserIds ?? []), seed.me.id])); } } const ch = this.channels.find((c) => c.id === channelId); if (ch) ch.lastActivityTs = msg.ts; return msg; } createChannel(input: { name: string; kind?: string; topic?: string; memberIds?: string[] }) { const kind = (input.kind as any) || "channel"; const isDm = kind === "dm" || kind === "group_dm"; const id = `${isDm ? "d" : "c"}_new_${this.idc++}`; const channel: any = { id, kind, name: input.name.replace(/^#/, "").trim() || "new-channel", topic: input.topic, memberIds: Array.from(new Set([seed.me.id, ...(input.memberIds ?? [])])), sectionId: isDm ? "dms" : "channels", unreadCount: 0, lastActivityTs: Date.now(), }; this.channels.push(channel); return channel; } react(messageId: string, emoji: string): Message | null { const msg = this.messages.find((m) => m.id === messageId); if (!msg) return null; msg.reactions = msg.reactions ?? []; const r = msg.reactions.find((x) => x.emoji === emoji); const uid = seed.me.id; if (r) { if (r.userIds.includes(uid)) { r.userIds = r.userIds.filter((id) => id !== uid); if (r.userIds.length === 0) msg.reactions = msg.reactions.filter((x) => x.emoji !== emoji); } else { r.userIds.push(uid); } } else { msg.reactions.push({ emoji, userIds: [uid] }); } return msg; } pin(messageId: string): Message | null { const msg = this.messages.find((m) => m.id === messageId); if (!msg) return null; msg.isPinned = !msg.isPinned; return msg; } save(messageId: string): Message | null { const msg = this.messages.find((m) => m.id === messageId); if (!msg) return null; msg.isSaved = !msg.isSaved; return msg; } listInbox(state?: InboxState): InboxItem[] { const s = state ?? "OPEN"; return this.inbox.filter((i) => i.state === s).sort((a, b) => b.ts - a.ts); } patchInbox(id: string, state: InboxState): InboxItem | null { const item = this.inbox.find((i) => i.id === id); if (!item) return null; item.state = state; return item; } listTickets(scope?: string): Ticket[] { let list = this.tickets; if (scope === "mine") list = list.filter((t) => t.requesterId === seed.me.id || t.assigneeId === seed.me.id); if (scope === "assigned") list = list.filter((t) => t.assigneeId === seed.me.id); return [...list].sort((a, b) => b.updatedTs - a.updatedTs); } getTicket(id: string): Ticket | null { return this.tickets.find((t) => t.id === id) ?? null; } patchTicket(id: string, state: TicketState): Ticket | null { const t = this.tickets.find((x) => x.id === id); if (!t) return null; t.state = state; t.updatedTs = Date.now(); if (state === "RESOLVED" || state === "CLOSED") t.slaBreached = false; return t; } replyTicket(id: string, body: string): Ticket | null { const t = this.tickets.find((x) => x.id === id); if (!t) return null; t.messages = t.messages ?? []; t.messages.push({ id: `tm_new_${this.idc++}`, channelId: `t_${id}`, authorId: seed.me.id, ts: Date.now(), body, deliveryState: "sent", }); t.updatedTs = Date.now(); if (t.state === "NEW") t.state = "OPEN"; t.slaBreached = false; return t; } search(q: string): SearchResult[] { const query = q.toLowerCase().trim(); if (!query) return []; const out: SearchResult[] = []; for (const c of this.channels) { if (c.name.toLowerCase().includes(query) || c.topic?.toLowerCase().includes(query)) { out.push({ id: c.id, type: "channel", title: c.kind === "channel" ? `#${c.name}` : c.name, subtitle: c.topic, channelId: c.id }); } } for (const u of this.users) { if (u.name.toLowerCase().includes(query) || u.handle.includes(query)) { out.push({ id: u.id, type: "person", title: u.name, subtitle: u.title }); } } for (const msg of this.messages) { if (msg.body.toLowerCase().includes(query)) { out.push({ id: msg.id, type: "message", title: msg.body.slice(0, 80), subtitle: `in ${this.channelLabel(msg.channelId)}`, channelId: msg.channelId, ts: msg.ts, parentId: msg.parentId ?? undefined }); } } for (const t of this.tickets) { if (t.subject.toLowerCase().includes(query) || t.ref.toLowerCase().includes(query)) { out.push({ id: t.id, type: "ticket", title: `${t.ref} · ${t.subject}`, subtitle: t.state }); } } return out.slice(0, 20); } private channelLabel(id: string): string { const c = this.channels.find((x) => x.id === id); if (!c) return "conversation"; return c.kind === "channel" || c.kind === "private" ? `#${c.name}` : c.name; } } function clone(v: T): T { return JSON.parse(JSON.stringify(v)); } // preserve the singleton across HMR reloads in dev (bump the key when the // store's shape/methods change so HMR rebuilds it instead of reusing a stale one) const g = globalThis as unknown as { __lynkdStore_v3?: MockStore }; export const store = g.__lynkdStore_v3 ?? (g.__lynkdStore_v3 = new MockStore());