# 保存与恢复

以源码为权威，原子提交对应预览，并拒绝过期或已释放的编辑请求。

结构化宿主保存 `TextDiagramEmbed`：版本标识、稳定 id、标题、源码语言/正文/修订号以及对应 PNG。新草稿可以没有预览；保存后的对象应包含由同一份源码生成的预览。

## 源码与图片一起保存

`createTextDiagram` 创建 revision 为 0 的新对象。`renderTextDiagram` 产生源码快照，`rasterizeTextDiagram` 从快照生成 PNG，`prepareTextDiagramSave` 在原基线基础上提出一次 revision + 1 的完整对象。工作台已经管理这条流水线。

`readTextDiagram` 校验未知输入并返回不可变副本；不接受额外字段、访问器或非规范值。它验证对象结构与大小，**不会重新渲染以证明 PNG 内容等于源码**。不要把来自不可信输入的图片仅因通过结构检查就当作已验证的绘图结果。

`serializeTextDiagram` 提供稳定比较字符串；JSON 读取后再次经过 `readTextDiagram`，再交给宿主。

## 一个完整的本地宿主

假设用户打开图形进行编辑，工作台完成后需要把结果交还应用。应用既要保存新图，也要确认打开弹窗后原图没有被其他操作修改。

示例的 `createDiagramSession(initial, persist)` 为一张图维护已接受的版本。`read()` 读取当前图；`capture()` 捕获打开时的版本，并提供 `commit()` 接受结果、`release()` 结束本次编辑。`persist` 是应用提供的同步存储回调，例如写入本地存储；它抛出异常时不会更新当前修订，也不会报告成功。

| 文件 | 用途 |
| --- | --- |
| `recipes/session.ts` | 比较打开时与保存时的版本，调用存储，再更新当前图 |
| `shared/types.ts` | 定义编辑请求和 session 的类型，供宿主与工作台共用 |

### recipes/session.ts

```ts
import { readTextDiagram, serializeTextDiagram, type TextDiagramEmbed } from "@quirekit/text-diagram";
import type { DiagramSession } from "../shared/types";

/**
 * Creates a single-object host with compare-and-save semantics.
 * @param initial - Validated persisted object or new draft.
 * @param persist - Optional synchronous storage write; throwing leaves the baseline unchanged.
 * @returns A host whose requests cannot overwrite a newer accepted revision.
 */
export function createDiagramSession(initial: TextDiagramEmbed, persist?: (next: TextDiagramEmbed) => void): DiagramSession {
  let current = readTextDiagram(initial);
  return {
    /** @returns The immutable accepted revision. */
    read() { return current; },
    /** @returns A detached baseline with explicit commit and release callbacks. */
    capture() {
      const baseline = current, expected = serializeTextDiagram(baseline);
      let released = false;
      return {
        document: baseline,
        /** @param proposal - Source and preview proposed by the workbench. @returns Atomic host acceptance. */
        commit(proposal) {
          if (released) return "missing";
          if (serializeTextDiagram(current) !== expected) return "conflict";
          const next = readTextDiagram(proposal);
          if (next.id !== baseline.id || next.document.revision !== baseline.document.revision + 1 || !next.preview) {
            throw new TypeError("Invalid diagram save");
          }
          // Storage must succeed before publishing the new revision to the UI.
          persist?.(next);
          current = next;
          return "saved";
        },
        /** Closes this target without changing the accepted object. */
        release() { released = true; },
      };
    },
  };
}

```

### shared/types.ts

```ts
import type { TextDiagramEditRequest, TextDiagramEmbed, TextDiagramSnapshot, TextDiagramRaster } from "@quirekit/text-diagram";

/** Minimal synchronous host; durable storage belongs to its caller. */
export interface DiagramSession {
  /** @returns The last accepted immutable object. */
  read(): TextDiagramEmbed;
  /** @returns A new edit request capturing the current baseline. */
  capture(): TextDiagramEditRequest;
}

/** Two output formats generated from exactly the same source. */
export interface DiagramFiles {
  /** Sanitized vector snapshot. */
  readonly vector: TextDiagramSnapshot;
  /** Bounded PNG projection. */
  readonly raster: TextDiagramRaster;
}

```

工作台传入的对象已有对应预览。生产宿主还应在最终提交时确认目标仍存在、当前仍可编辑、目标位置映射有效。Classic/Block/MSFE 的集成各自负责这些编辑器语义；这个示例不实现文档协作或多标签页同步。

## 四种提交结果

| 返回值 | 含义 | UI 行为 |
| --- | --- | --- |
| `saved` | 宿主已原子接受源码与预览 | 通知保存成功 |
| `conflict` | 基线之后正文已变更 | 保留草稿，显示冲突 |
| `readonly` | 当前不允许写入 | 保留草稿，显示只读 |
| `missing` | 原目标已删除或释放 | 保留草稿，显示目标丢失 |

存储故障可以抛出异常，工作台显示失败并允许重试。此合同不包含异步服务端确认、自动合并或后端重试；这些属于宿主的持久化策略。

## Markdown 宿主

调用 `textDiagramMarkdown(language, source)` 生成标准围栏。函数会选择比源码中的反引号更长的围栏，防止正文意外闭合。Markdown 仅持有语言与源码，不保存 `id`、标题、PNG 或结构化修订；编辑与冲突校验由对应源码宿主处理。
