mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
* fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors * test(sdk,ci): smoke test + explicit sdk-tests CI gate Smoke test covers the full public surface: openComposition → setStyle/setText/dispatch(moveElement) → serialize applyPatches + ORIGIN_APPLY_PATCHES tagging batch() coalescing + transactional rollback on throw undo/redo round-trip persist adapter write + persist:error surfacing T3 embedded mode: override-set apply on open + getOverrides round-trip Adds sdk-tests CI job so SDK coverage is explicitly named and required — prevents a repeat of the demo-next vitest-never-ran incident. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): export adapter types, awaitable flush(), never-coalesce mode - Export PersistAdapter, PreviewAdapter, PersistVersionEntry from package root — callers can now write typed fakes without reaching into internals - Add flush(): Promise<void> to Composition interface + CompositionImpl — app-close handlers can await a clean drain of the persist queue - coalesceMs <= 0 disables coalescing entirely in createHistory — enables deterministic test scenarios without per-entry timestamp manipulation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(sdk): p2 edge cases — setText no-text-node, override-remove non-existent, flush in smoke - setText on element with no prior text node (firstTextIdx=-1 path) - applyOverrideSet null removal on non-existent prop is a no-op (no throw) - smoke persist test uses comp.flush() instead of setTimeout - can() JSDoc clarifies Phase 3b false-return is intentional feature-detection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: trigger regression suite * fix(ci): add packages/sdk/package.json to Dockerfile.test workspace copy bun install --frozen-lockfile fails in the regression Docker build because the lockfile references the sdk workspace member but its package.json was not copied into the image before the install step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
84 lines
2.3 KiB
TypeScript
84 lines
2.3 KiB
TypeScript
/**
|
|
* 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, PersistErrorEvent } 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;
|
|
/** Called when adapter.write() rejects. */
|
|
onError?: (e: PersistErrorEvent) => void;
|
|
}
|
|
|
|
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 (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
opts.onError?.({ error: { message, cause: err } });
|
|
}
|
|
});
|
|
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();
|
|
},
|
|
};
|
|
}
|