Files
heygen-com--hyperframes/docs/sdk/guides/undo-redo-and-patches.mdx
T
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

244 lines
8.6 KiB
Plaintext

---
title: "Undo, Redo & Patches"
description: "Use the SDK's built-in undo stack, subscribe to patch events, and integrate with a host application's own history."
---
The SDK ships undo/redo by default for standalone sessions and emits RFC 6902 JSON patches on every change. Patch events are the integration seam for host application history, collaborative editing, audit logs, and embedded override mode.
## Built-in Undo and Redo
Standalone sessions (those without an `overrides` option) automatically attach a history module. You call `undo()` and `redo()` directly on the composition, and guard UI state with `canUndo()` and `canRedo()`.
```typescript
import { openComposition } from "@hyperframes/sdk";
const comp = await openComposition(html);
comp.setText("hf-title", "Draft A");
comp.setText("hf-title", "Draft B");
comp.canUndo(); // true
comp.undo(); // reverts "Draft B" → "Draft A"
comp.redo(); // reapplies "Draft B"
comp.canRedo(); // false — already at tip
```
<Tip>
`canUndo()` and `canRedo()` return `false` when the stack is empty and also when `history: false` was passed to `openComposition`. Check them before showing undo/redo controls.
</Tip>
### Opting out of the built-in history
Pass `history: false` when the host application owns the undo stack and you want the SDK to emit patches without building a parallel internal stack:
```typescript
const comp = await openComposition(html, {
overrides: storedOverrides,
history: false,
});
// comp.undo() / comp.redo() are no-ops — host drives history via applyPatches()
```
<Note>
`history: false` only disables the SDK undo stack. Persistence (auto-save) is independent — passing `history: false` does not suppress disk writes.
</Note>
### Coalescing rapid edits
The history module coalesces patch events into one undo entry when two conditions are met: the same op types and same element paths are touched within a configurable window. This prevents a slider drag from flooding the stack with hundreds of individual entries.
The default coalesce window is 300 ms. Override it in `openComposition`:
```typescript
const comp = await openComposition(html, {
coalesceMs: 500, // ms — increase for slower interactions
});
```
For more on the coalesce contract and the `HistoryOptions` type, see [Utilities](/sdk/reference/utilities).
---
## Patch Events
Every committed change emits a `patch` event. Subscribe to mirror SDK edits into your host state, audit log, or collaboration channel.
```typescript
const off = comp.on("patch", ({ formatVersion, patches, inversePatches, origin, opTypes }) => {
if (formatVersion !== 1) throw new Error("Unexpected patch format version");
saveToDB({ patches, inversePatches, origin, opTypes });
});
// Unsubscribe when done
off();
```
### PatchEvent fields
<ResponseField name="formatVersion" type="1">
Always `1` in the current SDK. Check this and reject unknown versions — a bump means a breaking change.
</ResponseField>
<ResponseField name="patches" type="JsonPatchOp[]">
Forward patches that were applied: the add/remove/replace ops describing how the document changed.
</ResponseField>
<ResponseField name="inversePatches" type="JsonPatchOp[]">
Inverse patches that undo this change. Store these alongside `patches` to support host-owned undo.
</ResponseField>
<ResponseField name="origin" type="unknown">
The origin tag that was passed to `dispatch()` or `batch()`. Use it to route or filter events.
</ResponseField>
<ResponseField name="opTypes" type="string[]">
Semantic names of the operations that produced this event (e.g. `["setStyle"]`). Useful for analytics and history labels. Not versioned — treat as informational.
</ResponseField>
### JsonPatchOp
The SDK emits and accepts only the add/remove/replace subset of RFC 6902:
```typescript
interface JsonPatchOp {
op: "add" | "remove" | "replace";
path: string; // JSON Pointer (RFC 6901), e.g. "/elements/hf-title/style/color"
value?: unknown;
}
```
The SDK never emits `move`, `copy`, or `test` ops, and `applyPatches()` ignores them if passed.
---
## Origin Model
Every mutation is tagged with an origin string. The SDK defines two built-in origins:
| Constant | Value | When used |
|----------|-------|-----------|
| `ORIGIN_LOCAL` | `"local"` | Default for all `dispatch()` and `batch()` calls |
| `ORIGIN_APPLY_PATCHES` | `"@hyperframes/sdk:applyPatches"` | Automatically applied by `applyPatches()` |
Both are exported from `@hyperframes/sdk`.
### Setting a custom origin
Pass `origin` in the options to `dispatch()` or `batch()` to tag the event for downstream routing:
```typescript
import { ORIGIN_LOCAL } from "@hyperframes/sdk";
comp.dispatch(
{ type: "setStyle", target: "hf-title", styles: { color: "#0EA5E9" } },
{ origin: "ui:color-picker" },
);
comp.batch(() => {
comp.setText("hf-cta", "Buy Now");
comp.setStyle("hf-cta", { backgroundColor: "#22C55E" });
}, { origin: "template-wizard" });
```
---
## Host-Owned History with applyPatches
When `history: false`, the host maintains its own undo stack from the patch events the SDK emits. To replay an undo step back into the SDK, call `applyPatches()` with the inverse patches from that stack entry:
```typescript
// Host undo stack entry (built from PatchEvent)
type HostEntry = {
patches: JsonPatchOp[];
inversePatches: JsonPatchOp[];
};
let history: HostEntry[] = [];
comp.on("patch", ({ patches, inversePatches, origin }) => {
// Skip — this is the SDK replaying a host-initiated undo
if (origin === ORIGIN_APPLY_PATCHES) return;
history.push({ patches, inversePatches });
});
function hostUndo() {
const entry = history.pop();
if (!entry) return;
comp.applyPatches(entry.inversePatches);
}
```
### Preventing undo loops
`applyPatches()` auto-tags its patch event with `ORIGIN_APPLY_PATCHES`. Your listener **must** skip that origin or you create an infinite loop:
```typescript
import { ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";
comp.on("patch", ({ patches, inversePatches, origin }) => {
if (origin === ORIGIN_APPLY_PATCHES) return; // <-- required guard
history.push({ patches, inversePatches });
});
```
<Warning>
Omitting the `ORIGIN_APPLY_PATCHES` guard causes every `applyPatches()` call to push a new entry onto the host stack, which triggers another `applyPatches()`, and so on. Always check `origin` first.
</Warning>
`ORIGIN_APPLY_PATCHES` is a namespaced string (`"@hyperframes/sdk:applyPatches"`) rather than a `Symbol` so it survives `postMessage`, structured clone, and JSON round-trips — important when the host forwards patch events across frames or workers.
### applyPatches with a custom origin
If you want downstream listeners to receive an explicit marker different from the default:
```typescript
comp.applyPatches(entry.inversePatches, { origin: "host:undo" });
```
Any origin other than `ORIGIN_APPLY_PATCHES` will enter the history module if `history` is still attached. Use `history: false` or a `trackedOrigins` filter when running host-owned history to avoid double-stacking.
---
## Embedded Override Mode Integration
In embedded (T3) mode, the host stores only the sparse override delta for each user. Pair `history: false` with `applyPatches()` to let the host drive undo while the SDK updates the composition state:
```typescript
import { openComposition, ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";
const comp = await openComposition(baseTemplateHtml, {
overrides: storedUserOverrides,
history: false,
});
let hostStack: HostEntry[] = [];
comp.on("patch", ({ patches, inversePatches, origin }) => {
if (origin === ORIGIN_APPLY_PATCHES) return;
hostStack.push({ patches, inversePatches });
// Persist the updated override set
saveOverrides(comp.getOverrides());
});
function undo() {
const entry = hostStack.pop();
if (entry) comp.applyPatches(entry.inversePatches);
}
```
See [Embedded Override Mode](/sdk/guides/embedded-override-mode) for the full T3 integration pattern.
---
<CardGroup cols={2}>
<Card title="Composition API" icon="code" href="/sdk/reference/composition">
Full method signatures for `undo`, `redo`, `canUndo`, `canRedo`, `on`, `applyPatches`, `dispatch`, and `batch`.
</Card>
<Card title="Types" icon="brackets-curly" href="/sdk/reference/types">
`PatchEvent`, `JsonPatchOp`, `ORIGIN_APPLY_PATCHES`, `ORIGIN_LOCAL`, and related types.
</Card>
<Card title="Utilities" icon="wrench" href="/sdk/reference/utilities">
`HistoryOptions`, `coalesceMs`, `trackedOrigins`, and `createHistory` internals.
</Card>
<Card title="Embedded Override Mode" icon="layer-group" href="/sdk/guides/embedded-override-mode">
Full T3 embedded integration with host-owned history.
</Card>
</CardGroup>