/** * @factory/editor — shared structural types for the block-canvas kernel. * * These are the pure (tiptap-free) shapes the codec, autosave, slash menu and * context menu operate on. The canonical wire model is `@factory/shared` * (`BlockPayload`, `BlockOperation`); the read shape here adds the `version` * (optimistic-concurrency counter, `blocks.version`) the autosave pipeline * needs and which the insert/update payloads do not carry. */ import type { BlockContentByType, BlockOperation, BlockType } from "@factory/shared/dto"; import type { Position } from "@factory/shared/positions"; /** * A block as read from the API / hydrated into the editor (`blocks` row). * `id` and `parentId` are always present once persisted; `parentId === null` * is a top-level block. `version` is the optimistic-concurrency counter the * batch endpoint advances per update (autosave conflict detection). */ export interface BlockDto { readonly id: string; readonly type: BlockType; /** Type-specific content (mirrors `BlockContentByType` for the block type). */ readonly content: BlockContentByType[BlockType]; /** Cross-cutting presentation (color, backgroundColor, checked, …). */ readonly properties?: Record; readonly parentId: string | null; /** Fractional (LexoRank-style) sibling position — never an array index. */ readonly position: Position; /** Optimistic-concurrency counter (monotonic per block, server-advanced). */ readonly version: number; } /** A block ready to insert (server assigns `id`; client supplies the rest). */ export interface NewBlock { readonly type: BlockType; readonly content: Record; readonly properties?: Record; readonly parentId: string | null; readonly position: Position; } /** An editor document: the block forest, unsorted (the codec sorts by position). */ export type BlockDoc = readonly BlockDto[]; /** The op set the autosave pipeline emits to `POST /v1/blocks` (wire format). */ export type EditorBlockOperation = BlockOperation; /** Save-state machine for the top-bar indicator (TASK-0047 R5). */ export type SaveState = | "idle" | "dirty" | "saving" | "saved" | "error" | "conflict"; /** The 409 the batch endpoint returns for a stale `version`. */ export interface ConflictInfo { readonly id: string; readonly actualVersion: number; readonly detail?: string; } /** Minimal transport the autosave pipeline uses (injectable for tests). */ export interface BlocksTransport { /** `POST /v1/blocks` — one transactional batch (≤ 500 ops). */ sendBatch(operations: readonly EditorBlockOperation[]): Promise; /** `GET /v1/blocks/:id` — fresh row for conflict rebase. */ fetchBlock(id: string): Promise; } /** * Injectable timer hooks (fake-timer friendly). The narrow callable shape — * NOT `typeof globalThis.setTimeout`, whose Node overload set (returning * `NodeJS.Timeout` + `__promisify__`) cannot be satisfied by a test fake. * The runtime handles are `Timeout | number`; we treat them opaquely. */ export type TimerSet = (fn: () => void, ms?: number) => ReturnType; export type TimerClear = (handle: ReturnType) => void; /** Autosave configuration (TASK-0047 R5–R6). */ export interface AutosaveConfig { readonly pageId: string; /** Flush debounce (spec: 800 ms). */ readonly debounceMs?: number; /** Chunk size for oversized flushes (spec: 200 ops). */ readonly maxBatchOps?: number; readonly transport: BlocksTransport; readonly onStateChange?: (state: SaveState) => void; readonly onConflict?: (info: ConflictInfo, retried: boolean) => void; readonly onError?: (error: unknown) => void; /** Injectable clock (defaults to Date.now) — fake-timer friendly. */ readonly now?: () => number; /** Injectable timer (defaults to global setTimeout/clearTimeout). */ readonly setTimeout?: TimerSet; readonly clearTimeout?: TimerClear; } /** Undo/redo step: one settle point of block-level changes. */ export interface HistoryStep { readonly label: string; readonly before: readonly BlockDto[]; readonly after: readonly BlockDto[]; }