feat(studio): stage 7 step 1 — wire SDK session into Studio#1443
Conversation
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Verified at HEAD 6112421e.
Stage 7 step 1 (Studio) — wire SDK session into Studio (+90/-3). Small but load-bearing: instantiates an SDK Composition alongside the existing Studio dispatch path, mirrors selection over. Existing dispatch still authoritative (correct for the "wire, not cutover" framing). Verdict: approve-with-concerns.
Concerns
-
useSdkSessiondoes its ownfetch()instead of usingadapter.read()(packages/studio/src/hooks/useSdkSession.ts:25-32). The hook hand-rollsfetch('/api/projects/{id}/files/{path}?optional=1')to bootstrap the HTML, then constructs the http adapter separately. That duplicates the URL-construction logic from #1441'shttp.ts:23(also?optional=1, same path shape). If the backend URL contract ever changes, #1441 gets updated, this won't. Cheap fix: build the adapter first, thenadapter.read(activeCompPath)to bootstrap. Single source of truth, and exercises the adapter's read path in the wiring stage where shadow-parity is being validated. -
useSdkSelectionSyncre-fires on every parent render that recreates the array (packages/studio/src/hooks/useSdkSelectionSync.ts:21-24). The effect deps includedomEditGroupSelections(array). If the parent hook (useDomEditSession) returns a freshly-allocated array each render — even with identical contents —setSelectionfires every render. Combined with #1442's lack of an equality check inside the SDK, this thrashesselectionchangesubscribers. Mitigations (any one):- Memoize the array at the source in
useDomEditSession. - Shallow-compare ids inside this effect before calling
setSelection. - Push the equality check into #1442's
setSelectionitself (my preferred fix; one change, covers all callers).
- Memoize the array at the source in
-
No
flush()before dispose on cleanup (packages/studio/src/hooks/useSdkSession.ts:45-48). When(projectId, activeCompPath)changes, the cleanup callscomp?.dispose()immediately. If there's an in-flightwrite()from the http adapter (e.g. the user just edited a style and then switched compositions fast), that write may be aborted mid-flight — depending on howdispose()chains. Worth eitherawait comp.flush()before dispose, or a comment confirming dispose drains. The fs adapter never had this risk because writes were synchronous-ish; the http adapter changes the failure surface. -
console.warnforpersist:error(useSdkSession.ts:38-40). Fine for stage 7 wiring, but this should reach the user somehow before #1452 cutover — when SDK writes are the only path, a silentconsole.warnfor a 503 means the user's edit just vanished and they don't know. Surface to the existing Studio error-toast layer, or at minimum file a follow-up.
Nits
- The
useSdkSelectionSynccall sits below thehandleGsapRemoveKeyframecallback inApp.tsx(line 321) — visually disconnected from theuseSdkSessioncall at line 150. Co-locating both hook calls would make the SDK-wiring story easier to follow when someone audits this in 6 months. useSdkSessionreturnsnulluntil the async bootstrap resolves — make sure no downstream consumer assumes a non-null session synchronously after mount. TheuseSdkSelectionSyncearly-return on!sessionhandles this correctly.
What I didn't verify
- Whether
useDomEditSessionreturns a memoizeddomEditGroupSelectionsarray — that's the load-bearing question for the re-fire concern. Worth a 30-second look from the author before merging. - Whether
Composition.dispose()awaits in-flight writes internally (would moot the flush-before-dispose concern).
Review by Rames D Jusso
miguel-heygen
left a comment
There was a problem hiding this comment.
CI blocked by the root oxfmt failure in #1423. Wiring is clean and additive.
Strengths:
packages/studio/src/hooks/useSdkSession.ts—cancelledflag + immediatecomp.dispose()on cleanup correctly handles the race where the effect re-fires beforeopenCompositionresolves. Thefetch→r.json()→openCompositionchain is all guarded.packages/studio/src/hooks/useSdkSelectionSync.ts— pureuseEffectwith no modifications to existing hooks. ThetoHfIdshelper prefers group selections over primary selection, matching Studio's multi-select semantics.useSdkSelectionSyncusessession, domEditSelection, domEditGroupSelectionsas deps — correct, no stale closure risk.
Important:
packages/studio/src/hooks/useSdkSession.ts—fetch(url)on a non-ok HTTP response (e.g. 404 if the file doesn't exist yet) still calls.then((r) => r.json())without checkingr.ok. A 404 body may not be valid JSON. The error is caught by.catch(() => setSession(null))but it would be cleaner to addif (!r.ok) throw new Error(...)before.json()to make the error path explicit. (This is addressed in #1452 — confirm it's already in the stack's head commit or flag as still needed.)
Nit:
packages/studio/src/hooks/useSdkSession.ts—data.contenttyped as{ content?: string }afterr.json(). The?optional=1query param implies the server may return{}or similar when the file doesn't exist. A test for the "missing file → no session" path would be valuable.
Verdict: APPROVE (pending #1423 oxfmt fix)
Reasoning: Clean additive wiring. The non-ok fetch path is covered by the catch block; the explicit r.ok check noted above is a style improvement patched in #1452.
— magi
e177f53 to
e2cad29
Compare
a32b07b to
02bd36c
Compare
e2cad29 to
6a66aa9
Compare
|
Thanks @james-russo-rames-d-jusso and @miguel-heygen. Fixed:
Design / acknowledged:
|
|
One more fix in the latest push:
|
af1d91b to
5d42eba
Compare
02bd36c to
c13f332
Compare
5d42eba to
7c17dd9
Compare
|
@james-russo-rames-d-jusso — all concerns addressed in post-review commits
|
c13f332 to
377007a
Compare
The base branch was changed.
7c17dd9 to
137aa5d
Compare
Creates useSdkSession hook: fetches active composition HTML, opens an SDK Composition backed by createHttpAdapter, disposes on comp/project change. Session is idle (no dispatch routed yet) — Step 3 wires edit ops through it. Also removes createFsAdapter from SDK main entry (Node-only; subpath-only: @hyperframes/sdk/adapters/fs). Required for Studio typecheck to pass when importing @hyperframes/sdk — fs.ts uses node:fs/promises which Studio's tsconfig does not include. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useSdkSelectionSync: effect that calls session.setSelection(hfIds) whenever domEditSelection or domEditGroupSelections changes. Maps each entry's hfId; skips entries without one. Pure additive — no existing hook modified. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Build the HttpAdapter first, then call adapter.read(activeCompPath) instead of duplicating URL construction with a raw fetch. Eliminates the /files/encode duplication already in HttpAdapter.read(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
Reviewer found a race: if the effect cleanup runs while openComposition is awaited, comp is null so cleanup is a no-op, but the composition is then set and never disposed. Add an explicit check after the await so any composition opened after cancellation is disposed immediately. Also wire the missing useSdkSession call in App.tsx (sdkSession was referenced but never declared — pre-existing typecheck failure), move the stableRenderQueue memo into useRenderQueue so App.tsx stays under the 600-line architecture gate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
137aa5d to
169d24c
Compare

What
Wires the SDK session into Studio:
useSdkSessionopens an idleCompositionbacked bycreateHttpAdapterfor the active composition path, anduseSdkSelectionSyncmirrors canvas selection intosession.setSelection. The session is idle — no ops route through it yet (that's step 3).Why
Step 1 of Studio's SDK migration. The session needs to exist and stay in sync before we can route any edits through it. Opening it early also surfaces any initialization or lifecycle issues (dispose on path change, race between open and cleanup) before production traffic depends on it.
How
useSdkSession(projectId, activeCompPath): opensopenCompositionwith anHttpAdapter, disposes on cleanup or path change, re-opens onhf:file-changeevents for the active comp (HMR hot path + SSE fallback)useSdkSelectionSync(session, domEditSelection): callssession.setSelectionon every canvas selection changecreateFsAdapterremoved from SDK main entry (it's Node-only); now only accessible via@hyperframes/sdk/adapters/fssubpathTest plan
useSdkSession.test.ts: reload fires on matching file-change, suppressed on non-matching path, suppressed within self-write window