--- title: "Adapters" description: "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 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 interface PersistAdapter { read(path: string): Promise; write(path: string, content: string): Promise; flush(): Promise; listVersions(path: string): Promise; loadFrom(path: string, versionKey: string): Promise; on(event: "persist:error", handler: (event: PersistErrorEvent) => void): () => void; } ``` Returns the stored content for `path`, or `undefined` for a path that has never been written. Never throws for a missing path. 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. Forces any queued or in-flight writes to commit before resolving. Call before process exit or navigation to prevent data loss. Returns the version history for `path` ordered newest-first. Returns an empty array when no versions exist. See `PersistVersionEntry` below. Returns the HTML content for a specific version identified by `versionKey`. Returns `undefined` when the key does not exist. 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. ### 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 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 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 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; } ``` 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. 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. 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. 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. Sets the preview selection and fires `selectionchange` on the session. Pass `{ additive: true }` to merge `ids` into the current selection rather than replacing it. 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. 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. Returns an unsubscribe function. Script-tag patches (`/script/gsap` and any future `/script/*` path) are never mirrored — rewriting a live `