Files
wehub-resource-sync 85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

220 lines
8.2 KiB
Plaintext

---
title: "Persistence"
description: "Autosave composition edits through pluggable adapters — filesystem, memory, or your own storage backend."
---
When you pass a `persist` adapter to `openComposition`, the SDK subscribes to every `change` event and schedules an async write automatically. You never call a save method after each edit — the adapter handles it.
```typescript
import { openComposition } from "@hyperframes/sdk";
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
const adapter = createFsAdapter({ root: "./project" });
const comp = await openComposition(html, {
persist: adapter,
persistPath: "composition.html", // default; can be omitted
});
comp.setText("hf-title", "Draft title");
// Autosaved on the next tick — no explicit save call needed.
```
## Options
<ParamField path="persist" type="PersistAdapter">
The storage adapter. The SDK calls `adapter.write(persistPath, html)` after every change. Omit to skip persistence (headless / agent use).
</ParamField>
<ParamField path="persistPath" type="string">
The path key passed to the adapter on every write. Default: `"composition.html"`. Immutable for the session lifetime — changing the path mid-session is not supported.
</ParamField>
## Flushing pending writes
The persist queue coalesces rapid edits, so the last write may be deferred when your application closes. Call `flush()` to drain any pending write before exit:
```typescript
// Ensure the latest state is committed before the process exits.
await comp.flush();
comp.dispose();
```
`flush()` resolves when the in-flight write completes. If there is nothing pending it resolves immediately. It is a no-op when no `persist` adapter was provided.
## Handling write failures
Write errors are emitted as events rather than thrown exceptions — a failed autosave must not crash an interactive session. Subscribe with `comp.on('persist:error', …)` to handle them:
```typescript
comp.on("persist:error", ({ error }) => {
console.error("Autosave failed:", error.message, error.cause);
showToast("Could not save — retrying next change.");
});
```
The event carries `{ error: { message: string; hint?: string; cause?: unknown } }`. The SDK retries on the next `change` event, so transient errors recover automatically.
## Shipped adapters
### `createMemoryAdapter()` — tests and demos
An in-process store — no file I/O, no Node.js required. Use it in unit tests, demos, or browser sessions where you want version history without disk access.
```typescript
import { createMemoryAdapter } from "@hyperframes/sdk";
const adapter = createMemoryAdapter();
const comp = await openComposition(html, {
persist: adapter,
});
comp.setText("hf-title", "v1");
await comp.flush();
comp.setText("hf-title", "v2");
await comp.flush();
const versions = await adapter.listVersions("composition.html");
// versions[0] is newest — key "v2", versions[1] is "v1"
const v1Html = await adapter.loadFrom("composition.html", versions[1].key);
```
`createMemoryAdapter()` also exposes `injectFault(message)` — a test helper that makes the next write fire `persist:error` instead of committing. This lets you exercise your error-handling path without real I/O failures.
### `createFsAdapter(opts)` — Node.js local dev
Writes to the filesystem and keeps a rolling version history under `.hf-versions/`. Node.js only — do not use in browser builds.
<Note>
`createFsAdapter` is imported from the `@hyperframes/sdk/adapters/fs` **subpath**, not the main barrel like `createMemoryAdapter`. The fs adapter pulls in Node's `fs` module, so keeping it on a separate subpath lets browser bundles tree-shake it away — importing it from the root would drag `node:fs` into a browser build.
</Note>
```typescript
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
const adapter = createFsAdapter({
root: "./project", // required — directory for composition files
maxVersions: 20, // optional — versions to retain per file (default: 20)
});
```
<ParamField path="root" type="string">
Root directory. The adapter writes `{root}/{persistPath}` and keeps version files under `{root}/.hf-versions/{persistPath}/`.
</ParamField>
<ParamField path="maxVersions" type="number">
Maximum version snapshots to retain per file. Oldest are pruned automatically. Default: `20`.
</ParamField>
**Listing and restoring versions:**
```typescript
const versions = await adapter.listVersions("composition.html");
// [ { key: "1751234567890-0003", timestamp: 1751234567890 }, ... ]
// Entries are newest-first.
const olderHtml = await adapter.loadFrom("composition.html", versions[2].key);
if (olderHtml) {
// Re-open the old snapshot.
const restored = await openComposition(olderHtml, { persist: adapter });
}
```
Version keys are `{timestamp}-{counter}` strings. `listVersions` returns entries newest-first; `timestamp` is the Unix milliseconds at the time of the write.
## Implementing a custom adapter
Any object that satisfies the `PersistAdapter` interface works as a drop-in. The interface is:
```typescript
export 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;
}
```
**Contract:**
- `read()` returns `undefined` for a path that has never been written.
- `write()` is idempotent — a second write to the same path overwrites.
- `flush()` resolves when any queued writes are committed. Return `Promise.resolve()` if writes are synchronous.
- `listVersions()` returns entries newest-first. Entries without stored `content` are valid — `loadFrom` is called lazily by the consumer.
- `loadFrom()` returns `undefined` when the version key is not found.
- `on('persist:error', handler)` registers an error listener. Errors **must not** propagate as thrown exceptions — surface them through the event instead. Returns an unsubscribe function.
**Minimal example — an HTTP / S3-style adapter:**
```typescript
import type { PersistAdapter, PersistVersionEntry, PersistErrorEvent } from "@hyperframes/sdk";
export function createHttpAdapter(baseUrl: string): PersistAdapter {
const errorHandlers: Array<(e: PersistErrorEvent) => void> = [];
return {
async read(path) {
const res = await fetch(`${baseUrl}/${path}`);
if (res.status === 404) return undefined;
return res.text();
},
async write(path, content) {
try {
await fetch(`${baseUrl}/${path}`, {
method: "PUT",
body: content,
headers: { "Content-Type": "text/html" },
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
errorHandlers.forEach((h) => h({ error: { message, cause: err } }));
}
},
async flush() {
// No queuing — writes are immediate.
},
async listVersions(path): Promise<PersistVersionEntry[]> {
const res = await fetch(`${baseUrl}/${path}/versions`);
if (!res.ok) return [];
return res.json() as Promise<PersistVersionEntry[]>;
},
async loadFrom(path, versionKey) {
const res = await fetch(`${baseUrl}/${path}/versions/${versionKey}`);
if (!res.ok) return undefined;
return res.text();
},
on(event, handler) {
if (event !== "persist:error") return () => {};
errorHandlers.push(handler);
return () => {
const i = errorHandlers.indexOf(handler);
if (i !== -1) errorHandlers.splice(i, 1);
};
},
};
}
```
<Note>
`write()` must never throw — catch errors and emit them through `on('persist:error', …)` instead. The SDK trusts that writes are fire-and-forget from its side; thrown exceptions break the internal persist queue.
</Note>
<CardGroup cols={2}>
<Card title="Adapter reference" icon="plug" href="/sdk/reference/adapters">
Full `PersistAdapter`, `PersistVersionEntry`, and `PreviewAdapter` type definitions.
</Card>
<Card title="openComposition reference" icon="code" href="/sdk/reference/open-composition">
All `OpenCompositionOptions` fields including `persist`, `persistPath`, `preview`, and `overrides`.
</Card>
</CardGroup>