1/** 2 * @factory/collaboration — the block type ↔ Yjs node mapping (TASK-0092 3 * R4: "the Phase-4 node set registered with the y-prosemirror mapping — each 4 * Phase-4 node → its Yjs counterpart; the mapping table is the single source 5 * for the codec's node list — a new block type added in Phase 4+ must 6 * extend the mapping or the round-trip test fails, the structural gate"). 7 * 8 * Derivation: the table is **built from `BLOCK_CONTENT_SCHEMAS`** 9 * (`@factory/shared`) at module load — the shared DTO is the single source 10 * of truth, so a block type added to the shared enum automatically extends 11 * this mapping (the structural gate test asserts the two agree exactly). 12 * 13 * Per-block-type split of `content` keys: 14 * - **text slots** — `text` / `language` / `caption` / `title` → a 15 * `Y.XmlText` named slot (CRDT-mergeable text — concurrent typing merges 16 * character-level); 17 * - **rows slot** — table `rows` → a `Y.Array` of `Y.Array<Y.XmlText>` 18 * (row/cell text merges; structural row moves are CRDT); 19 * - **attrs** — everything else (`fileId`, `url`, `height`, `width`, 20 * `databaseId`, `viewId`, `chartId`, `formId`, `sourceBlockId`, 21 * `pageId`, `direction`) → plain node attrs (atomic metadata — the 22 * doc §4.4 rule: record values / file metadata are not CRDT-ed). 23 */ 24 25import { 26 BLOCK_CONTENT_SCHEMAS, 27 blockTypeSchema, 28 type BlockType, 29} from "@factory/shared/dto"; 30import type { z } from "zod"; 31 32/** The Y.XmlElement type name for every block (one node type, attrs carry 33 * the block type — the phase-07 codec rule). */ 34export const BLOCK_Y_NODE_TYPE = "factoryBlock"; 35 36/** Content keys that become `Y.XmlText` named slots. */ 37export const TEXT_SLOT_KEYS: readonly string[] = ["text", "language", "caption", "title"]; 38 39/** The table rows content key (becomes a `Y.Array` named slot). */ 40export const ROWS_KEY = "rows"; 41 42export interface BlockYMapping { 43 readonly blockType: BlockType; 44 /** Y.XmlElement type name (always `factoryBlock`). */ 45 readonly yNodeType: string; 46 /** Content keys mapped to `Y.XmlText` slots. */ 47 readonly textSlots: readonly string[]; 48 /** True for `table` (the `rows` content key → a rows slot). */ 49 readonly hasRows: boolean; 50 /** Content keys mapped to plain node attrs. */ 51 readonly attrKeys: readonly string[]; 52} 53 54function schemaKeys(schema: z.ZodTypeAny): readonly string[] { 55 const shape = (schema as { shape?: Record<string, z.ZodTypeAny> }).shape; 56 if (shape === undefined || shape === null) return []; 57 return Object.keys(shape); 58} 59 60function mappingFor(type: BlockType): BlockYMapping { 61 const keys = schemaKeys(BLOCK_CONTENT_SCHEMAS[type]); 62 const textSlots = keys.filter((k) => TEXT_SLOT_KEYS.includes(k)); 63 const hasRows = keys.includes(ROWS_KEY); 64 const attrKeys = keys.filter((k) => !TEXT_SLOT_KEYS.includes(k) && k !== ROWS_KEY); 65 return { blockType: type, yNodeType: BLOCK_Y_NODE_TYPE, textSlots, hasRows, attrKeys }; 66} 67 68/** 69 * The full mapping table, keyed by block type. Built from 70 * `BLOCK_CONTENT_SCHEMAS` — one entry per block type (30), in the shared 71 * enum's declaration order. 72 */ 73export const BLOCK_Y_MAPPINGS: Readonly<Record<BlockType, BlockYMapping>> = Object.fromEntries( 74 blockTypeSchema.options.map((type) => [type, mappingFor(type)]), 75) as Readonly<Record<BlockType, BlockYMapping>>; 76 77/** Look up a block type's mapping (throws on an unknown type — the gate). */ 78export function mappingForType(type: string): BlockYMapping { 79 const mapping = BLOCK_Y_MAPPINGS[type as BlockType]; 80 if (mapping === undefined) { 81 throw new Error( 82 `mapping: block type "${type}" is not in the Yjs mapping table — ` + 83 `extend BLOCK_CONTENT_SCHEMAS (shared) to add it`, 84 ); 85 } 86 return mapping; 87} 88