feat(sdk): file-backed fs adapter + setTiming GSAP-script sync; add sdk-playground#1423
Conversation
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Verified at HEAD 1604228.
Verdict: approve-with-concerns. Solid foundation — fs adapter rounds out a real implementation, setTiming → GSAP-script sync closes a real consistency bug, playground gives the stack a usable harness. A few load-bearing issues to address before downstream PRs lean on this; one is a contract-shape bug worth fixing here.
Concerns (load-bearing as foundation):
-
packages/sdk/src/engine/mutate.ts:289-296&343-355— comment misstates the runtime contract; the actual reason for the sync is consistency, not overwrite-prevention.- Comment claims: "the runtime re-stamps those attributes FROM GSAP positions on the next init — silently overwriting the edit." Both occurrences.
- The runtime stamping path at
packages/core/src/runtime/init.ts:1001and:1020guards withif (target.hasAttribute("data-start")) continue;— it does not overwrite already-stamped attrs. And the runtime stampsdata-start/data-duration, whilehandleSetTimingwritesdata-start/data-end— different attr pairs, so there's no overlap to overwrite anyway. - The real motivation is the symmetric concern: if SDK setTiming only edits attrs and leaves the GSAP
position/durationin the script untouched, the script is the source of truth at play time (timeline rebuilds from script), so trim edits would have zero playback effect. The script-rewrite is what actually makes the trim affect playback. - Foundational PR — future readers will inherit the wrong mental model. Fix the comment, or the runtime guard, but the two should agree.
-
packages/sdk-playground/src/fileAdapter.ts:35—listVersionsreturn shape violatesPersistAdaptercontract.- Returns
{ key, timestamp };PersistVersionEntry.content: stringis required (non-optional inpackages/sdk/src/adapters/types.ts:8). - The vite-config plugin upstream (
vite.config.ts:2308) dropscontenton the wire on purpose (lazy-load vialoadFrom) — sensible UX, but the contract type should reflect it: makecontent?: string(and update fs adapter to either include or omit consistently), or haveFileAdapter.listVersionsshape-check / accept a documented partial. - Currently this compiles only because the missing
contentisn't structurally caught at the cast site — but downstream consumers oflistVersions()who index.contentwill hitundefinedat runtime on the HTTP path. Since this is foundation, the contract decision needs to land here.
- Returns
-
packages/sdk/src/adapters/fs.ts:91-100(appendVersion) — race-unsafe under concurrent writes.- Version key =
String(Date.now()). Two writes within the same millisecond → second overwrites the first version snapshot (same path,writeFilereplaces). - Two
write()calls in flight interleave: the prune at the end of the second invocation readsreaddirmid-write — under/over-prune is possible. - Two concurrent writes to the main file race on
writeFile— final on-disk content is the last-finishing one, not the last-called one. - #1425 in your stack adds
flush()in-flight tracking, but the underlying write loop here isn't serialized either. For author-time UX (single human typing) it's fine; if anything ever drives this from concurrent dispatch (test rigs, multi-tab playground), you'll see corruption. - Fix shape: serialize per-path via a
Map<path, Promise<void>>chain, and disambiguate keys withDate.now() + "-" + counteror a monotonic seq. Worth doing in this PR since #1425 will land assuming an ordering it doesn't have.
- Version key =
Concerns (smaller):
-
mutate.ts:291— the "Pre-parse GSAP script once" comment is partly aspirational.updateAnimationInScriptre-parses withparseGsapAstinternally on every call (packages/core/src/parsers/gsapParser.ts:1142). For typicalids.length < 10this is fine; just don't carry the "pre-parsed once" claim into the design of future ops that batch larger sets. -
mutate.ts:347-349— selector match list is[data-hf-id="${id}"]/[data-hf-id='${id}']/#${id}. Misses*[data-hf-id="…"], attribute selectors with whitespace inside, descendant selectors, class-based targeting. Foundation set is enough for the playground demo, but worth either a TODO or restricting via anormalizeSelectorhelper that the GSAP parser owns — otherwise the matcher list will drift across files as ops are added. -
packages/sdk-playground/src/fileAdapter.ts:5—API = "/api/composition"is hardcoded. Path arg toread/write/listVersions/loadFromis silently ignored (_path). Fine for the single-composition demo, but thePersistAdaptercontract takes a path — if anyone ever spins up a multi-composition playground variant, this adapter quietly does the wrong thing. Either honor the path or rename the adapter to make the single-doc constraint loud.
Nits:
vite.config.ts:2299—compositionPlugin()allocates oneFsAdapterper plugin invocation. Fine for dev server but spawns a fresh handler array on every HMR reload of the config; defer to module-scoped singleton if HMR ever proves flaky.fs.ts:107—unlink(...).catch(() => {})silently swallows prune failures. Probably fine; emitting via thepersist:errorchannel would let the playground surface "version pruning failing" if disk fills up.main.ts:780— DEMO_HTML is a 17-line template literal. Move to a sibling.htmlfile imported?raw(same pattern asgsapRaw) for editor-syntax-highlighting parity.
Questions:
-
mutate.ts:359-364— the script-edit and the attr-edits go into the sameMutationResult.forward/inverse. Does the patch-applier guarantee order across both DOM-attr ops and the script-text op when replaying inverse? If the script-rewrite patch lands before the attr-revert on undo, intermediate frames would show inconsistent state. Probably fine because both are inside the same dispatch batch, but worth a one-line confirm. -
The
data-start/data-endvsdata-durationmismatch I noted above — is the SDK author-side moving the format from(start, duration)to(start, end)on purpose, while the runtime still emits the older shape? If so, the runtime stamping atinit.ts:1004-1005and:1024-1025will need a follow-up PR to match, and downstream tools that readdata-durationwill need an update plan.
What I didn't verify:
- I didn't run the new test suite — relied on the PR's "118 pass" claim. Worth confirming the new GSAP-sync path has a test that exercises the multi-id case (where
parsedGsap.locatedis reused across iterations ofids). - I didn't trace the playground's per-frame loop for hidden fetches; spot-check on
tick()showedrequestAnimationFrame-driven local playback only, but the 1461-linemain.tshas surface I didn't fully cover. - I didn't validate the vite-plugin connect middleware against streamed bodies —
readBodyaccumulates everything in memory which is fine for HTML compositions but unbounded against a hostile client. Author-time tool, so likely a non-issue.
Review by Rames D Jusso
miguel-heygen
left a comment
There was a problem hiding this comment.
Preflight blocker: oxfmt --check fails on packages/sdk-playground/index.html. This is the single root failure across all 13 PRs in the stack — fix formatting on that file to unblock CI. Lint (oxlint) is clean.
Strengths:
packages/sdk/src/adapters/fs.ts— cleaninflightWrites: Set<Promise<void>>pattern;flush()correctly snapshots the set before awaiting to avoid race with concurrent writes starting afterflush()is called.packages/sdk/src/engine/mutate.tshandleSetTiming— accumulating all tween updates into a singlegsapScriptChange()patch pair rather than one-per-tween is the right approach; avoids redundant patch events and keeps inverse/forward symmetric.- The
packages/sdk-playground/is a genuine exerciser — having a runnable example alongside the unit tests is good.
Blocker:
packages/sdk-playground/index.htmlfailsoxfmt --check. File introduced in this PR. Runbunx oxfmt packages/sdk-playground/index.htmland commit the result. Required CI gate.
Important:
packages/sdk/src/engine/mutate.tshandleSetTiming— GSAP selector matching uses rawidto build comparison strings ([data-hf-id="${id}"],#${id}). When F9 scoped ids land (e.g."hf-host/hf-leaf"), these selectors will never match GSAP tween blocks (GSAP script uses bare IDs). The setTiming DOM attribute update will succeed but the GSAP sync will silently no-op. Worth a follow-up ticket once F9 scoped IDs ship (see #1434).
Nit:
sdk-playground/— no.gitignorefornode_modules/in the package root. If bun installs deps inside, they could land in the tree accidentally.
Verdict: REQUEST CHANGES
Reasoning: oxfmt format check failure blocks merge for this and all downstream PRs. One-line fix: bunx oxfmt packages/sdk-playground/index.html.
— magi
1604228 to
def5462
Compare
6cca945 to
93e8fcd
Compare

Stacked on the acorn-parser/T6e branch.
What
packages/sdk/src/adapters/fs.ts.node:fs/promisesread/write, timestamped version history under.hf-versions/<path>/, prune tomaxVersions(20),listVersions/loadFrom. Emitspersist:errorin the canonical{ error: { message, cause } }shape.packages/sdk/src/engine/mutate.ts.setTimingon a GSAP composition stampeddata-start/data-endon the DOM node, but the runtime re-stamps those attributes from GSAP positions on the next init — silently overwriting the edit.handleSetTimingnow also rewrites the GSAP script (parseGsapScriptAcornForWrite+updateAnimationInScript), flushing both models in one patch pair so they stay in sync. DAW-style timeline trim depends on this.@hyperframes/sdk-playground(new, private) — interactive browser harness over the full op surface:setStyle/setText/setAttribute/removeElement/setVariableValue/find/selection()proxy/all 9 GSAP+label ops/setClassStyle. Live preview iframe with click-select + drag-to-reposition, DAW-style timeline trim viasetTiming, file-backed persistence with version history (Vite dev-server plugin + fetch adapter), undo/redo/can/getOverrides/flush, raw-HTML editor modal. README documents built vs planned.Test
bun run --cwd packages/sdk build+test— 118 passbun run --cwd packages/sdk-playground build— clean🤖 Generated with Claude Code