import { type Cell, computed, Default, handler, NAME, pattern, type PerSpace, type PerUser, Stream, UI, type VNode, Writable, } from "commonfabric"; /** * Convergence-storm fixture — the structural skeleton of * `profile-group-chat/main.tsx` with the profile wish removed, for the * multi-runtime convergence tests (convergence-storm.test.ts). * * Keeps exactly the pieces the multiplayer pathology needs: * - a `PerSpace` message array every session appends to (the contended doc); * - message objects that carry a live `Cell` link (`authorProfile`), so each * pushed element is hoisted into its own entity — the durable docs another * session discovers via link and must materialize (the wedge reads these * at seq 0); * - a shared computed over the whole list (`authors`), materialized as a * space-scoped result doc whose commit carries reads of the message docs * (the poison-carrier class); * - a `PerUser` profile cell standing in for the wish result. */ export interface StormProfile { name?: string; } export interface StormMessage { /** Live link to the sender's per-user profile cell (forces element→doc). */ // PerUser declaration required by the scope-isolation write guard: // per-reader resolution is the deliberate repro shape here. The property is // optional because another reader's scoped target may be unavailable. authorProfile?: PerUser>; author: string; body: string; n: number; } const DEFAULT_MESSAGES: StormMessage[] = []; const DEFAULT_PROFILE: StormProfile = { name: "" }; export interface PostEvent { author?: string; body?: string; n?: number; } const post = handler; profile: Cell; }>((event, { messages, profile }) => { messages.push({ authorProfile: profile, author: event?.author ?? "", body: event?.body ?? "", n: event?.n ?? 0, }); }); export interface ConvergenceChatInput { messages?: PerSpace>; profile?: PerUser>; } export interface ConvergenceChatOutput { [NAME]: string; [UI]: VNode; messages: PerSpace>; profile: PerUser>; messageCount: number; authors: string[]; post: Stream; } export default pattern( ({ messages, profile }) => { const messageCount = messages.length; // Whole-list aggregate materialized as a shared result doc — its commit // reads every message element (and via authorProfile, the message docs). const authors = computed(() => { const out: string[] = []; for (const m of messages ?? []) { if (m && m.author && !out.includes(m.author)) out.push(m.author); } return out; }); return { [NAME]: "Convergence storm fixture", [UI]: (
{messageCount} messages
), messages, profile, messageCount, authors, post: post({ messages, profile }), }; }, );