/// /** * ============================================================================ * BUG REPRO: Nested array elements are undefined when reading cross-session data * ============================================================================ * * SUMMARY: * When one browser session sets a cell's data, and another browser session * reads that same cell, the nested array elements resolve as `undefined`. * * OBSERVED BEHAVIOR: * - otherData: {label: 'Player 2', items: Array(2)} ✓ object resolves * - otherData.items: [undefined, undefined] ✗ array elements undefined! * - otherData.items[0]: undefined ✗ * - otherData.items[0]?.id: undefined ✗ * * EXPECTED BEHAVIOR: * - otherData.items[0]: {id: 10, name: 'P2-Ship1', start: {...}, hits: [...]} * - otherData.items[0]?.id: 10 * * KEY CONDITIONS TO TRIGGER: * 1. Parent pattern owns cells with Writable> * 2. Child pattern receives cells as Writable (no Default wrapper) * 3. Cell data is SET by one browser session (piece instance) * 4. Cell data is READ by a DIFFERENT browser session (piece instance) * 5. Data contains nested arrays with objects * * STEPS TO REPRODUCE: * 1. Deploy this pattern: * CT_API_URL=http://localhost:8000 CT_IDENTITY=./claude.key \ * deno task ct piece new packages/patterns/battleship/multiplayer/repro-minimal.tsx \ * --root packages/patterns/battleship --space gideon * * 2. Open the piece URL in Browser Tab 1 * 3. Click "Join as P1" → navigates to Child pattern * 4. Open the SAME piece URL in Browser Tab 2 (new session) * 5. Click "Join as P2" → this sets data2 cell * 6. Go back to Tab 1 (P1's view) * 7. Click "Check OTHER Data" * 8. Observe console: items array has length 2 but elements are undefined * * NOTE: "Check MY Data" works correctly because myData was set in the same session. */ import { action, computed, Default, handler, NAME, navigateTo, pattern, UI, Writable, } from "commontools"; // ============================================================================ // Types - Nested structure similar to battleship's Ship type // ============================================================================ interface Coordinate { row: number; col: number; } /** Item with nested object (start) and nested array (hits) - like Ship in battleship */ interface Item { id: number; name: string; start: Coordinate; // Nested object hits: boolean[]; // Nested array } /** Container holds an array of Items - the items array elements become undefined */ interface Container { label: string; items: Item[]; // <-- BUG: elements of this array are undefined when read cross-session } // ============================================================================ // Child Pattern - receives cells WITHOUT Default<> wrapper // ============================================================================ interface ChildInput { /** Cell set by THIS session - works correctly */ myData: Writable; /** Cell set by OTHER session - BUG: nested array elements are undefined */ otherData: Writable; whichPlayer: 1 | 2; } const Child = pattern( ({ myData, otherData, whichPlayer }) => { /** * CHECK OTHER DATA - This demonstrates the bug! * When otherData was set by a different browser session, * the array elements are undefined even though the array has length > 0. */ const checkOther = action(() => { console.log(`[Child P${whichPlayer}] Checking OTHER player's data...`); const other = otherData.get(); console.log("[Child] otherData:", other); console.log("[Child] otherData.items:", other?.items); console.log("[Child] otherData.items.length:", other?.items?.length); if (other?.items && other.items.length > 0) { // BUG: These all log undefined even though items.length is 2 console.log("[Child] otherData.items[0]:", other.items[0]); console.log("[Child] otherData.items[0]?.id:", other.items[0]?.id); console.log( "[Child] otherData.items[0]?.start:", other.items[0]?.start, ); console.log( "[Child] otherData.items[0]?.start?.row:", other.items[0]?.start?.row, ); } }); /** * CHECK MY DATA - This works correctly! * When myData was set by THIS browser session, everything resolves properly. */ const checkMine = action(() => { console.log(`[Child P${whichPlayer}] Checking MY data...`); const mine = myData.get(); console.log("[Child] myData:", mine); console.log("[Child] myData.items:", mine?.items); console.log("[Child] myData.items.length:", mine?.items?.length); if (mine?.items && mine.items.length > 0) { // WORKS: These all resolve correctly console.log("[Child] myData.items[0]:", mine.items[0]); console.log("[Child] myData.items[0]?.id:", mine.items[0]?.id); } }); return { [NAME]: computed(() => `Child P${whichPlayer}`), [UI]: (

Child Pattern - Player {whichPlayer}

checkMine.send()}> Check MY Data checkOther.send()}> Check OTHER Data
), }; }, ); // ============================================================================ // Parent Pattern - owns cells WITH Default<> wrappers (type mismatch with child) // ============================================================================ /** * Parent uses Writable> but child receives Writable. * This type mismatch may be related to the bug. */ interface ParentInput { data1: Writable>; data2: Writable>; } let nav: | (( myData: Writable, otherData: Writable, whichPlayer: 1 | 2, ) => unknown) | null = null; const joinAsP1 = handler< void, { data1: Writable; data2: Writable } >((_e, { data1, data2 }) => { const items = [ { id: 1, name: "P1-Ship1", start: { row: 0, col: 0 }, hits: [false, false, false], }, { id: 2, name: "P1-Ship2", start: { row: 2, col: 3 }, hits: [false, false], }, ]; console.log("[Parent] P1 joining with items:", items); data1.set({ label: "Player 1", items }); console.log("[Parent] P1 navigating to child..."); if (nav) return nav(data1, data2, 1); // My data = data1, Other = data2 }); const joinAsP2 = handler< void, { data1: Writable; data2: Writable } >((_e, { data1, data2 }) => { const items = [ { id: 10, name: "P2-Ship1", start: { row: 5, col: 5 }, hits: [false, false, false, false], }, { id: 11, name: "P2-Ship2", start: { row: 7, col: 1 }, hits: [false, false, false], }, ]; console.log("[Parent] P2 joining with items:", items); data2.set({ label: "Player 2", items }); console.log("[Parent] P2 navigating to child..."); if (nav) return nav(data2, data1, 2); // My data = data2, Other = data1 }); const Parent = pattern(({ data1, data2 }) => ({ [NAME]: "Two-Cell Repro", [UI]: (

Two-Cell Repro (like Battleship)

Join as one player, then check the OTHER player's data

Join as P1 Join as P2
), })); nav = (myData, otherData, whichPlayer) => navigateTo(Child({ myData, otherData, whichPlayer })); export default Parent;