Skip to content

feat(sdk): file-backed fs adapter + setTiming GSAP-script sync; add sdk-playground#1423

Open
vanceingalls wants to merge 1 commit into
graphite-base/1423from
06-13-feat_sdk_playground_fs_adapter_settiming_gsap_sync
Open

feat(sdk): file-backed fs adapter + setTiming GSAP-script sync; add sdk-playground#1423
vanceingalls wants to merge 1 commit into
graphite-base/1423from
06-13-feat_sdk_playground_fs_adapter_settiming_gsap_sync

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

Stacked on the acorn-parser/T6e branch.

What

  • fs PersistAdapter implemented (was a stub) — packages/sdk/src/adapters/fs.ts. node:fs/promises read/write, timestamped version history under .hf-versions/<path>/, prune to maxVersions (20), listVersions/loadFrom. Emits persist:error in the canonical { error: { message, cause } } shape.
  • setTiming GSAP-script syncpackages/sdk/src/engine/mutate.ts. setTiming on a GSAP composition stamped data-start/data-end on the DOM node, but the runtime re-stamps those attributes from GSAP positions on the next init — silently overwriting the edit. handleSetTiming now 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 via setTiming, 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 pass
  • bun run --cwd packages/sdk-playground build — clean
  • oxlint + oxfmt clean; fallow audit passes (0 introduced findings)

🤖 Generated with Claude Code

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:1001 and :1020 guards with if (target.hasAttribute("data-start")) continue; — it does not overwrite already-stamped attrs. And the runtime stamps data-start/data-duration, while handleSetTiming writes data-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/duration in 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:35listVersions return shape violates PersistAdapter contract.

    • Returns { key, timestamp }; PersistVersionEntry.content: string is required (non-optional in packages/sdk/src/adapters/types.ts:8).
    • The vite-config plugin upstream (vite.config.ts:2308) drops content on the wire on purpose (lazy-load via loadFrom) — sensible UX, but the contract type should reflect it: make content?: string (and update fs adapter to either include or omit consistently), or have FileAdapter.listVersions shape-check / accept a documented partial.
    • Currently this compiles only because the missing content isn't structurally caught at the cast site — but downstream consumers of listVersions() who index .content will hit undefined at runtime on the HTTP path. Since this is foundation, the contract decision needs to land here.
  • 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, writeFile replaces).
    • Two write() calls in flight interleave: the prune at the end of the second invocation reads readdir mid-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 with Date.now() + "-" + counter or a monotonic seq. Worth doing in this PR since #1425 will land assuming an ordering it doesn't have.

Concerns (smaller):

  • mutate.ts:291 — the "Pre-parse GSAP script once" comment is partly aspirational. updateAnimationInScript re-parses with parseGsapAst internally on every call (packages/core/src/parsers/gsapParser.ts:1142). For typical ids.length < 10 this 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 a normalizeSelector helper that the GSAP parser owns — otherwise the matcher list will drift across files as ops are added.

  • packages/sdk-playground/src/fileAdapter.ts:5API = "/api/composition" is hardcoded. Path arg to read/write/listVersions/loadFrom is silently ignored (_path). Fine for the single-composition demo, but the PersistAdapter contract 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:2299compositionPlugin() allocates one FsAdapter per 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:107unlink(...).catch(() => {}) silently swallows prune failures. Probably fine; emitting via the persist:error channel 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 .html file imported ?raw (same pattern as gsapRaw) for editor-syntax-highlighting parity.

Questions:

  • mutate.ts:359-364 — the script-edit and the attr-edits go into the same MutationResult.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-end vs data-duration mismatch 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 at init.ts:1004-1005 and :1024-1025 will need a follow-up PR to match, and downstream tools that read data-duration will 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.located is reused across iterations of ids).
  • I didn't trace the playground's per-frame loop for hidden fetches; spot-check on tick() showed requestAnimationFrame-driven local playback only, but the 1461-line main.ts has surface I didn't fully cover.
  • I didn't validate the vite-plugin connect middleware against streamed bodies — readBody accumulates 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 miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 — clean inflightWrites: Set<Promise<void>> pattern; flush() correctly snapshots the set before awaiting to avoid race with concurrent writes starting after flush() is called.
  • packages/sdk/src/engine/mutate.ts handleSetTiming — accumulating all tween updates into a single gsapScriptChange() 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.html fails oxfmt --check. File introduced in this PR. Run bunx oxfmt packages/sdk-playground/index.html and commit the result. Required CI gate.

Important:

  • packages/sdk/src/engine/mutate.ts handleSetTiming — GSAP selector matching uses raw id to 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 .gitignore for node_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

@vanceingalls vanceingalls force-pushed the 06-13-feat_sdk_playground_fs_adapter_settiming_gsap_sync branch from 1604228 to def5462 Compare June 15, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants