1/** 2 * @factory/editor — shared structural types for the block-canvas kernel. 3 * 4 * These are the pure (tiptap-free) shapes the codec, autosave, slash menu and 5 * context menu operate on. The canonical wire model is `@factory/shared` 6 * (`BlockPayload`, `BlockOperation`); the read shape here adds the `version` 7 * (optimistic-concurrency counter, `blocks.version`) the autosave pipeline 8 * needs and which the insert/update payloads do not carry. 9 */ 10 11import type { BlockContentByType, BlockOperation, BlockType } from "@factory/shared/dto"; 12import type { Position } from "@factory/shared/positions"; 13 14/** 15 * A block as read from the API / hydrated into the editor (`blocks` row). 16 * `id` and `parentId` are always present once persisted; `parentId === null` 17 * is a top-level block. `version` is the optimistic-concurrency counter the 18 * batch endpoint advances per update (autosave conflict detection). 19 */ 20export interface BlockDto { 21 readonly id: string; 22 readonly type: BlockType; 23 /** Type-specific content (mirrors `BlockContentByType` for the block type). */ 24 readonly content: BlockContentByType[BlockType]; 25 /** Cross-cutting presentation (color, backgroundColor, checked, …). */ 26 readonly properties?: Record<string, unknown>; 27 readonly parentId: string | null; 28 /** Fractional (LexoRank-style) sibling position — never an array index. */ 29 readonly position: Position; 30 /** Optimistic-concurrency counter (monotonic per block, server-advanced). */ 31 readonly version: number; 32} 33 34/** A block ready to insert (server assigns `id`; client supplies the rest). */ 35export interface NewBlock { 36 readonly type: BlockType; 37 readonly content: Record<string, unknown>; 38 readonly properties?: Record<string, unknown>; 39 readonly parentId: string | null; 40 readonly position: Position; 41} 42 43/** An editor document: the block forest, unsorted (the codec sorts by position). */ 44export type BlockDoc = readonly BlockDto[]; 45 46/** The op set the autosave pipeline emits to `POST /v1/blocks` (wire format). */ 47export type EditorBlockOperation = BlockOperation; 48 49/** Save-state machine for the top-bar indicator (TASK-0047 R5). */ 50export type SaveState = 51 | "idle" 52 | "dirty" 53 | "saving" 54 | "saved" 55 | "error" 56 | "conflict"; 57 58/** The 409 the batch endpoint returns for a stale `version`. */ 59export interface ConflictInfo { 60 readonly id: string; 61 readonly actualVersion: number; 62 readonly detail?: string; 63} 64 65/** Minimal transport the autosave pipeline uses (injectable for tests). */ 66export interface BlocksTransport { 67 /** `POST /v1/blocks` — one transactional batch (≤ 500 ops). */ 68 sendBatch(operations: readonly EditorBlockOperation[]): Promise<void>; 69 /** `GET /v1/blocks/:id` — fresh row for conflict rebase. */ 70 fetchBlock(id: string): Promise<BlockDto>; 71} 72 73/** 74 * Injectable timer hooks (fake-timer friendly). The narrow callable shape — 75 * NOT `typeof globalThis.setTimeout`, whose Node overload set (returning 76 * `NodeJS.Timeout` + `__promisify__`) cannot be satisfied by a test fake. 77 * The runtime handles are `Timeout | number`; we treat them opaquely. 78 */ 79export type TimerSet = (fn: () => void, ms?: number) => ReturnType<typeof globalThis.setTimeout>; 80export type TimerClear = (handle: ReturnType<typeof globalThis.setTimeout>) => void; 81 82/** Autosave configuration (TASK-0047 R5–R6). */ 83export interface AutosaveConfig { 84 readonly pageId: string; 85 /** Flush debounce (spec: 800 ms). */ 86 readonly debounceMs?: number; 87 /** Chunk size for oversized flushes (spec: 200 ops). */ 88 readonly maxBatchOps?: number; 89 readonly transport: BlocksTransport; 90 readonly onStateChange?: (state: SaveState) => void; 91 readonly onConflict?: (info: ConflictInfo, retried: boolean) => void; 92 readonly onError?: (error: unknown) => void; 93 /** Injectable clock (defaults to Date.now) — fake-timer friendly. */ 94 readonly now?: () => number; 95 /** Injectable timer (defaults to global setTimeout/clearTimeout). */ 96 readonly setTimeout?: TimerSet; 97 readonly clearTimeout?: TimerClear; 98} 99 100/** Undo/redo step: one settle point of block-level changes. */ 101export interface HistoryStep { 102 readonly label: string; 103 readonly before: readonly BlockDto[]; 104 readonly after: readonly BlockDto[]; 105} 106