# Save and restore

Keep source authoritative, commit its matching preview atomically and reject stale or released requests.

A structured host saves `TextDiagramEmbed`: protocol version, stable id, title, source language/text/revision and the matching PNG. A new draft may have no preview. A saved object should carry a preview generated from that exact source.

## Save source and image together

`createTextDiagram` creates revision 0. `renderTextDiagram` creates a source-bound snapshot, `rasterizeTextDiagram` generates its PNG, and `prepareTextDiagramSave` proposes a complete object with revision + 1. The workbench manages this pipeline already.

`readTextDiagram` validates unknown input and returns an immutable copy. It rejects extra fields, accessors and noncanonical values. It validates structure and size; **it does not re-render to prove that the PNG matches the source**. Structural validation alone does not make an untrusted image a verified diagram projection.

`serializeTextDiagram` provides a stable comparison string. After parsing JSON, validate it again with `readTextDiagram` before passing it to a host.

## A complete local host

When a user opens a diagram, the workbench eventually returns an edited result. The application must save it and check that another action has not changed the original while the dialog was open.

`createDiagramSession(initial, persist)` maintains the accepted version of one diagram. `read()` returns the current diagram. `capture()` records the opening version and supplies `commit()` to accept a result and `release()` to end that edit. The application supplies a synchronous `persist` callback, such as a local-storage write. If it throws, the accepted revision remains unchanged and success is not reported.

| File | Purpose |
| --- | --- |
| `recipes/session.ts` | Compare opening and current versions, call storage, then update the accepted diagram |
| `shared/types.ts` | Shared types for the edit request and 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;
}

```

The workbench supplies an object with a matching preview. A production host must also check that the target exists, remains editable and still maps to a valid position at commit time. Classic, Block and MSFE own those editor semantics. This example does not implement collaboration or cross-tab synchronization.

## Four acceptance results

| Result | Meaning | UI behavior |
| --- | --- | --- |
| `saved` | Host atomically accepted source and preview | Report success |
| `conflict` | Content changed after the captured baseline | Keep draft; explain conflict |
| `readonly` | Writes are no longer allowed | Keep draft; explain read-only state |
| `missing` | Target was removed or released | Keep draft; explain missing target |

Storage failures can throw. The workbench displays failure and allows retry. This contract does not provide asynchronous server confirmation, automatic merging or backend retry; those belong to the host's persistence strategy.

## Markdown hosts

Use `textDiagramMarkdown(language, source)` to generate a standard fence. It chooses a delimiter longer than any backtick run in the source, preventing an accidental closing fence. Markdown retains language and source, without a structured id, title, PNG or revision. Its source host owns editing and conflict detection.
