> ## Documentation Index
> Fetch the complete documentation index at: https://hyperframes-canary-calibration-notes.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Adapters

> Persistence and preview adapter interfaces, contracts, and the built-in factory functions.

The SDK decouples editing sessions from storage and preview surfaces through two injectable interfaces: `PersistAdapter` and `PreviewAdapter`. Both ship with concrete factory functions you pass to `openComposition()`. You can also implement either interface directly for custom storage backends (S3, IndexedDB, HTTP) or custom preview surfaces.

## PersistAdapter

```typescript theme={null}
import type { PersistAdapter } from "@hyperframes/sdk";
```

Injectable storage adapter. Decouples the SDK from the underlying persistence mechanism so the same session code runs in tests (memory), local dev (filesystem), and production (cloud storage).

### Interface

```typescript theme={null}
interface PersistAdapter {
  read(path: string): Promise<string | undefined>;
  write(path: string, content: string): Promise<void>;
  flush(): Promise<void>;
  listVersions(path: string): Promise<PersistVersionEntry[]>;
  loadFrom(path: string, versionKey: string): Promise<string | undefined>;
  on(event: "persist:error", handler: (event: PersistErrorEvent) => void): () => void;
}
```

<ParamField path="read" type="(path: string) => Promise<string | undefined>">
  Returns the stored content for `path`, or `undefined` for a path that has never been written. Never throws for a missing path.
</ParamField>

<ParamField path="write" type="(path: string, content: string) => Promise<void>">
  Persists `content` at `path`. Idempotent — a second call with the same path overwrites the prior value. Write failures must not propagate as thrown exceptions; fire `persist:error` instead.
</ParamField>

<ParamField path="flush" type="() => Promise<void>">
  Forces any queued or in-flight writes to commit before resolving. Call before process exit or navigation to prevent data loss.
</ParamField>

<ParamField path="listVersions" type="(path: string) => Promise<PersistVersionEntry[]>">
  Returns the version history for `path` ordered newest-first. Returns an empty array when no versions exist. See `PersistVersionEntry` below.
</ParamField>

<ParamField path="loadFrom" type="(path: string, versionKey: string) => Promise<string | undefined>">
  Returns the HTML content for a specific version identified by `versionKey`. Returns `undefined` when the key does not exist.
</ParamField>

<ParamField path="on" type="(event: &#x22;persist:error&#x22;, handler) => () => void">
  Subscribes to write failures. Returns an unsubscribe function. Adapters must emit this event — not throw — when a write fails, so the session continues running even when storage is temporarily unavailable.
</ParamField>

### Contract summary

* `read()` returns `undefined` for a path that has never been written — never throws ENOENT or a 404 equivalent.
* `write()` is idempotent; a second write to the same path replaces the stored content.
* `flush()` resolves when any pending writes are committed to durable storage.
* `listVersions()` returns entries newest-first; `loadFrom()` uses the keys from those entries.
* Write errors are emitted via `on('persist:error')`, never thrown — the session keeps running.

### PersistVersionEntry

```typescript theme={null}
interface PersistVersionEntry {
  /** Opaque key identifying this version (adapter-defined format). */
  key: string;
  /** Full HTML content — may be omitted by adapters that load content lazily via loadFrom(). */
  content?: string;
  timestamp?: number;
}
```

The `key` is adapter-defined and opaque to callers — pass it directly to `loadFrom()`. The filesystem adapter encodes milliseconds and a counter into the key; the memory adapter uses an incrementing `"v1"`, `"v2"` … scheme.

***

## PreviewAdapter

```typescript theme={null}
import type { PreviewAdapter } from "@hyperframes/sdk";
```

Injectable preview surface adapter. Decouples the SDK from the host's rendering layer. The SDK is **not** in the 60fps draft loop: your pointer-move handler calls `applyDraft()` directly on the adapter at 60fps, and the SDK only gets involved once per gesture when `commitPreview()` fires to derive and dispatch the resulting op.

### Interface

```typescript theme={null}
interface PreviewAdapter {
  elementAtPoint(x: number, y: number, opts?: { atTime?: number }): ElementAtPointResult | null;
  applyDraft(id: string, props: DraftProps): void;
  commitPreview(): void;
  cancelPreview(): void;
  select(ids: string[], opts?: { additive?: boolean }): void;
  on(event: "selection", handler: (ids: string[]) => void): () => void;
  attachSync(comp: Composition): () => void;
}
```

<ParamField path="elementAtPoint" type="(x, y, opts?) => ElementAtPointResult | null">
  Synchronous hit-test at composition coordinates `(x, y)`. Returns the nearest `[data-hf-id]` element under the point, or `null` for a transparent hit (the composition root, an opacity-0 element, or nothing at all). Requires a same-origin iframe — cross-origin access throws a DOMException. The `atTime` option reflects GSAP state at the current playhead; seeking to a speculative time is not supported.
</ParamField>

<ParamField path="applyDraft" type="(id: string, props: DraftProps) => void">
  Visually translates the preview element at 60fps during a drag: sets the element's CSS `translate` to its pre-drag value composed with the accumulated delta. Works on GSAP-animated elements (a `translate` set after GSAP's first parse composes with the animated transform). The **SDK is not called here** — this is a direct write to the preview surface by your pointer-move handler. Switching `id` mid-drag reverts the previous element's draft first.
</ParamField>

<ParamField path="commitPreview" type="() => void">
  Called once on pointer-up. Reads the accumulated draft delta, derives a `moveElement` op from it, dispatches it into the SDK, emits a patch event, and mirrors the committed position onto the live element (so it holds without a reload). This is the only moment the SDK becomes aware of a drag. If dispatch throws, the draft translate is reverted and the error propagates.
</ParamField>

<ParamField path="cancelPreview" type="() => void">
  Restores the element's pre-drag `translate` without dispatching any op. The model is never changed. Call this on `Escape` keydown or when a drag is aborted.
</ParamField>

<ParamField path="select" type="(ids: string[], opts?: { additive?: boolean }) => void">
  Sets the preview selection and fires `selectionchange` on the session. Pass `{ additive: true }` to merge `ids` into the current selection rather than replacing it.
</ParamField>

<ParamField path="on" type="(event: &#x22;selection&#x22;, handler: (ids: string[]) => void) => () => void">
  Fired when the preview host changes the selection (for example, the user clicks an element). Returns an unsubscribe function. In the current release, callers listen to the session's own `selectionchange` event instead — this hook is wired in a future stage.
</ParamField>

<ParamField path="attachSync" type="(comp: Composition) => () => void">
  Mirrors a composition's edits onto the adapter's own live document: an immediate full sync of the composition's current overrides, then a subscription that replays every future `patch` event — including undo/redo, since both fire through the same event with forward or inverse patches. Calling `attachSync` again while already attached detaches the previous subscription first. The full-override sync also re-runs on every iframe `load`, so a `srcdoc` navigation that races the attach (or drops patches committed during the load window) converges once the new document arrives. Returns an unsubscribe function.

  Script-tag patches (`/script/gsap` and any future `/script/*` path) are never mirrored — rewriting a live `<script>` tag's content doesn't re-execute it, and re-running GSAP setup from scratch would conflict with running timeline state. Every other patch kind (style, text, attribute, timing, hold, element add/remove, stylesheet, variable value, variable declaration) mirrors as-is.

  ```typescript theme={null}
  const adapter = createIframePreviewAdapter(iframe, dispatch);
  const comp = await openComposition(html, { preview: adapter });
  const detach = adapter.attachSync(comp);

  // later, if the host tears down the preview:
  detach();
  ```
</ParamField>

### ElementAtPointResult

```typescript theme={null}
interface ElementAtPointResult {
  id: string;
  tag: string;
}
```

The `id` is the element's `data-hf-id` value; `tag` is its lowercase tag name (e.g. `"div"`, `"img"`).

### DraftProps

```typescript theme={null}
interface DraftProps {
  dx?: number;
  dy?: number;
  width?: number;
  height?: number;
}
```

`dx` and `dy` are the accumulated drag deltas in composition pixels. `width` and `height` are defined in the interface for forward compatibility but are not yet wired to any op.

<Note>
  `ElementAtPointResult` and `DraftProps` are the structural shapes a `PreviewAdapter` produces and consumes. They are **not** re-exported from the `@hyperframes/sdk` barrel — you implement against these shapes rather than importing them.
</Note>

***

## Factory Functions

### createMemoryAdapter

```typescript theme={null}
import { createMemoryAdapter } from "@hyperframes/sdk";

function createMemoryAdapter(): PersistAdapter & { injectFault(message: string): void };
```

Returns a `PersistAdapter` backed by an in-process `Map`. Writes are synchronous; `flush()` is a no-op. Versions are keyed `"v1"`, `"v2"` … and stored in memory with full content.

The returned value also exposes `injectFault(message)` — a test helper that causes the **next** `write()` call to fire a `persist:error` event with `message` instead of committing. Use this in unit tests to verify your error-handling code path.

```typescript theme={null}
const persist = createMemoryAdapter();

const comp = await openComposition(html, { persist });
comp.setText("hf-title", "Hello");
await comp.flush();

const saved = await persist.read("composition.html");
```

<Note>
  `createMemoryAdapter()` is best suited for tests, demos, and ephemeral in-process sessions. For local development, use `createFsAdapter()` so edits survive restarts.
</Note>

***

### createFsAdapter

```typescript theme={null}
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";

function createFsAdapter(opts: FsAdapterOptions): PersistAdapter;
```

**Node.js only.** Returns a `PersistAdapter` that reads and writes files under a root directory. Import from the `@hyperframes/sdk/adapters/fs` subpath — this module uses Node `fs/promises` and is excluded from the browser-safe main bundle.

#### FsAdapterOptions

```typescript theme={null}
interface FsAdapterOptions {
  /** Root directory for composition files. */
  root: string;
  /** Max versions to keep per file. Default: 20. */
  maxVersions?: number;
}
```

<ParamField path="root" type="string" required>
  Absolute or relative path to the directory where composition files are written. Created with `mkdir -p` on first write.
</ParamField>

<ParamField path="maxVersions" type="number">
  Maximum number of historical versions retained per file. Oldest versions are pruned automatically when the limit is exceeded. Defaults to `20`.
</ParamField>

The adapter writes the current composition at `{root}/{path}` and stores version snapshots in `{root}/.hf-versions/{path}/`. Version keys encode `Date.now()` and a monotonic counter (`"1750000000000-0001"`), so `listVersions()` returns them newest-first by lexicographic descending sort.

```typescript theme={null}
import { openComposition } from "@hyperframes/sdk";
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";

const comp = await openComposition(html, {
  persist: createFsAdapter({ root: "./project", maxVersions: 50 }),
  persistPath: "index.html",
});

comp.setText("hf-title", "Saved");
await comp.flush();

// List saved versions
const adapter = createFsAdapter({ root: "./project" });
const versions = await adapter.listVersions("index.html");
const previous = await adapter.loadFrom("index.html", versions[1].key);
```

<Warning>
  `createFsAdapter` uses Node.js `fs/promises`. Do not import it in browser or edge environments — import from `@hyperframes/sdk/adapters/fs` (the subpath) so bundlers can tree-shake it.
</Warning>

***

### createHeadlessAdapter

```typescript theme={null}
import { createHeadlessAdapter } from "@hyperframes/sdk";

function createHeadlessAdapter(): PreviewAdapter;
```

Returns a no-op `PreviewAdapter` for headless use: agents, CI pipelines, and server-side rendering. All methods are stubs — `elementAtPoint` always returns `null`, `applyDraft` and `commitPreview` are no-ops, and the `"selection"` event never fires.

Pass this adapter when you open a composition for programmatic editing and do not need a live preview surface.

```typescript theme={null}
import { openComposition, createHeadlessAdapter } from "@hyperframes/sdk";

const comp = await openComposition(html, {
  preview: createHeadlessAdapter(),
});
```

<Note>
  `openComposition` does not create a preview adapter automatically. Omit `preview` when no preview
  surface is needed, or pass `createHeadlessAdapter()` when an explicit no-op adapter makes shared
  code clearer.
</Note>

***

### createIframePreviewAdapter

```typescript theme={null}
import { createIframePreviewAdapter } from "@hyperframes/sdk";

function createIframePreviewAdapter(
  iframe: HTMLIFrameElement,
  dispatch?: (op: EditOp) => void,
): PreviewAdapter;
```

Returns a `PreviewAdapter` that bridges the SDK to a same-origin `<iframe>` containing the composition. Provides real hit-testing via `elementsFromPoint` (z-stack aware), draft drag support, and selection management.

**Requirements:**

* The iframe must be same-origin (e.g. a `srcdoc` or `blob:` URL). Cross-origin access to `contentDocument` throws a `DOMException`.
* Pass your session's `dispatch` callback to enable `commitPreview()` — without it, pointer-up is a no-op on the model.

**Image-alpha hit-testing:** For `<img>` elements, the adapter samples the alpha channel of the pixel under the pointer using an `OffscreenCanvas`. Transparent pixels fall through to the element behind. Cross-origin images that taint the canvas are treated as opaque (safe fallback, logged once per src).

```typescript theme={null}
import { openComposition, createIframePreviewAdapter } from "@hyperframes/sdk";

const iframe = document.querySelector<HTMLIFrameElement>("#preview-frame")!;
// Optional, and dispatched with `?.` — the callback cannot fire before
// openComposition() resolves, but strict TypeScript cannot prove that and
// rejects a definite `let` captured before assignment (TS2454).
let comp: Awaited<ReturnType<typeof openComposition>> | undefined;
const preview = createIframePreviewAdapter(iframe, (op) => comp?.dispatch(op));
comp = await openComposition(html, { preview });

// Hit-test at pointer position
const hit = preview.elementAtPoint(pointerX, pointerY);
if (hit) {
  preview.select([hit.id]);

  // Drag: call applyDraft at 60fps, commitPreview on pointer-up
  preview.applyDraft(hit.id, { dx: 12, dy: -5 });
  preview.commitPreview();
}
```

***

## Export Map

| Symbol                                                    | Imported from                   |
| --------------------------------------------------------- | ------------------------------- |
| `PersistAdapter`, `PreviewAdapter`, `PersistVersionEntry` | `@hyperframes/sdk` (types only) |
| `createMemoryAdapter`                                     | `@hyperframes/sdk`              |
| `createHeadlessAdapter`                                   | `@hyperframes/sdk`              |
| `createIframePreviewAdapter`, `resolveNearestHfElement`   | `@hyperframes/sdk`              |
| `createFsAdapter`, `FsAdapterOptions`                     | `@hyperframes/sdk/adapters/fs`  |

<CardGroup cols={2}>
  <Card title="Persistence Guide" icon="floppy-disk" href="/sdk/guides/persistence">
    How to wire adapters into openComposition, handle errors, and restore versions.
  </Card>

  <Card title="Canvas Integration" icon="browser" href="/sdk/guides/canvas-integration">
    Building a visual editor canvas with the iframe preview adapter and hit-testing.
  </Card>
</CardGroup>
