mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete (#1325)
* feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete * fix(sdk): address review — live-DOM query cache, single parse, style parse dedup - getElements/getElement/find now walk the live linkedom DOM via buildRoots with a lazily-built cache invalidated on dispatch/applyPatches — no serialize→ensureHfIds→parseHTML round trip per query - openComposition parses once (parseMutable); dropped discarded _doc constructor param and the redundant buildDocument call - document.ts buildElement reuses model.ts getElementStyles — removes duplicated parseInlineStyles (also fixes custom-prop camelCase mangling) - JSDoc note: empty batch() still fires change handlers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sdk): restore full public exports now session/document modules exist index.ts re-exports document/session/history/persist-queue (trimmed in the engine-layer PR to keep it self-contained); drops the temporary fallow suppressions whose consumers now exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sdk): coalesce history by patch paths; replay override-set on open Adversarial-review findings F1 + F2: - history: coalescing now requires identical patch paths in addition to op types + origin + window. Previously two rapid setStyle calls on DIFFERENT elements merged into one entry carrying the second forward + first inverse — undo then reverted the wrong element and stranded the latest edit. Slider drags on one property still coalesce. - T3 init: openComposition({ overrides }) now replays the stored override-set onto the freshly-parsed base before exposing the session (new keyToPath inverse mapping + applyOverrideSet). Previously the overrides were copied into the map but never applied — reopening an embedded composition showed and serialized the base template. - examples: GSAP calls now feature-detect with can() (Phase 3b ops throw UnsupportedOpError as of the engine-layer fix); UnsupportedOpError re-exported from the package entry. - 8 new session tests: coalesce same-path / cross-element / cross-prop, override round-trip (style/text/attr/timing/removal/restore-base). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sdk): transactional batch rollback, sorted coalesce key, root-priority unify Round-2 review (Rames/Miguel) on the session layer: - batch() is now transactional: on throw, accumulated inverse patches are replayed in reverse and the override-set snapshot restored — the model is exactly as it was at batch entry. Previously a throwing batch left the DOM partially mutated with no patch trail, no history entry, no recovery path. 2 new tests (model unchanged + undo is no-op after throwing batch). - history coalesce key sorts opTypes — same op-type set coalesces regardless of dispatch order within a batch. - applyPatches comment documents that emitted PatchEvents carry an empty inversePatches array (hosts keep their own inverse log). - document.ts extractDimensions/extractDuration now use the engine's findRoot — dimension extraction and mutations agree on the root element ([data-hf-root] > #stage > first child). Dimensions prefer the runtime's data-width/data-height forced-override attrs, falling back to inline style. - ownText documented: snapshot .text is trimmed display text; setText writes verbatim. Deferred to follow-up (acknowledged, not ship-blocking): persist-queue flush error surfacing, debounce window, path default, history ring-buffer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
22bb6737c5
commit
7010edac85
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Optional persist queue module (F5 layering).
|
||||
*
|
||||
* Subscribes to 'change' events and schedules async writes via a PersistAdapter.
|
||||
* One in-flight write at a time; latest state always wins (last-write-wins coalescing).
|
||||
*
|
||||
* Wired automatically by openComposition() in standalone (T1/T2) mode.
|
||||
* T3 (embedded) hosts own persistence — do not use this module.
|
||||
*/
|
||||
|
||||
import type { Composition } from "./types.js";
|
||||
import type { PersistAdapter } from "./adapters/types.js";
|
||||
|
||||
export interface PersistQueueModule {
|
||||
/** Force an immediate write (e.g. before app close). */
|
||||
flush(): Promise<void>;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface PersistQueueOptions {
|
||||
/** Adapter path to write to. Default: "composition.html" */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export function createPersistQueue(
|
||||
session: Composition,
|
||||
adapter: PersistAdapter,
|
||||
opts: PersistQueueOptions = {},
|
||||
): PersistQueueModule {
|
||||
const path = opts.path ?? "composition.html";
|
||||
let pendingWrite: ReturnType<typeof setTimeout> | null = null;
|
||||
// Promise-chain mutex: each write chains onto the prior, preventing concurrent writes.
|
||||
let writeChain: Promise<void> = Promise.resolve();
|
||||
let disposed = false;
|
||||
|
||||
function scheduleWrite(): void {
|
||||
if (pendingWrite !== null) clearTimeout(pendingWrite);
|
||||
pendingWrite = setTimeout(() => {
|
||||
pendingWrite = null;
|
||||
void doWrite();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function doWrite(): Promise<void> {
|
||||
if (disposed) return Promise.resolve();
|
||||
const content = session.serialize();
|
||||
writeChain = writeChain.then(async () => {
|
||||
if (disposed) return;
|
||||
try {
|
||||
await adapter.write(path, content);
|
||||
} catch {
|
||||
// error already surfaced via persist:error on the adapter
|
||||
}
|
||||
});
|
||||
return writeChain;
|
||||
}
|
||||
|
||||
const unsubscribe = session.on("change", () => {
|
||||
scheduleWrite();
|
||||
});
|
||||
|
||||
return {
|
||||
async flush(): Promise<void> {
|
||||
if (pendingWrite !== null) {
|
||||
clearTimeout(pendingWrite);
|
||||
pendingWrite = null;
|
||||
}
|
||||
await doWrite();
|
||||
},
|
||||
|
||||
dispose(): void {
|
||||
disposed = true;
|
||||
if (pendingWrite !== null) {
|
||||
clearTimeout(pendingWrite);
|
||||
pendingWrite = null;
|
||||
}
|
||||
unsubscribe();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user