Skip to content

feat(studio): stage 7 step 1 — wire SDK session into Studio#1443

Merged
vanceingalls merged 5 commits into
mainfrom
sdk-stage7-studio-s7step1
Jun 15, 2026
Merged

feat(studio): stage 7 step 1 — wire SDK session into Studio#1443
vanceingalls merged 5 commits into
mainfrom
sdk-stage7-studio-s7step1

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

What

Wires the SDK session into Studio: useSdkSession opens an idle Composition backed by createHttpAdapter for the active composition path, and useSdkSelectionSync mirrors canvas selection into session.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): opens openComposition with an HttpAdapter, disposes on cleanup or path change, re-opens on hf:file-change events for the active comp (HMR hot path + SSE fallback)
  • useSdkSelectionSync(session, domEditSelection): calls session.setSelection on every canvas selection change
  • createFsAdapter removed from SDK main entry (it's Node-only); now only accessible via @hyperframes/sdk/adapters/fs subpath

Test plan

  • useSdkSession.test.ts: reload fires on matching file-change, suppressed on non-matching path, suppressed within self-write window
  • Manual: opened Studio, confirmed session opens without console errors, confirmed old path session disposes when switching compositions

vanceingalls commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator Author

@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 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

  • useSdkSession does its own fetch() instead of using adapter.read() (packages/studio/src/hooks/useSdkSession.ts:25-32). The hook hand-rolls fetch('/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's http.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, then adapter.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.

  • useSdkSelectionSync re-fires on every parent render that recreates the array (packages/studio/src/hooks/useSdkSelectionSync.ts:21-24). The effect deps include domEditGroupSelections (array). If the parent hook (useDomEditSession) returns a freshly-allocated array each render — even with identical contents — setSelection fires every render. Combined with #1442's lack of an equality check inside the SDK, this thrashes selectionchange subscribers. Mitigations (any one):

    1. Memoize the array at the source in useDomEditSession.
    2. Shallow-compare ids inside this effect before calling setSelection.
    3. Push the equality check into #1442's setSelection itself (my preferred fix; one change, covers all callers).
  • No flush() before dispose on cleanup (packages/studio/src/hooks/useSdkSession.ts:45-48). When (projectId, activeCompPath) changes, the cleanup calls comp?.dispose() immediately. If there's an in-flight write() 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 how dispose() chains. Worth either await 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.warn for persist: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 silent console.warn for 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 useSdkSelectionSync call sits below the handleGsapRemoveKeyframe callback in App.tsx (line 321) — visually disconnected from the useSdkSession call at line 150. Co-locating both hook calls would make the SDK-wiring story easier to follow when someone audits this in 6 months.
  • useSdkSession returns null until the async bootstrap resolves — make sure no downstream consumer assumes a non-null session synchronously after mount. The useSdkSelectionSync early-return on !session handles this correctly.

What I didn't verify

  • Whether useDomEditSession returns a memoized domEditGroupSelections array — 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
miguel-heygen previously approved these changes Jun 15, 2026

@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.

CI blocked by the root oxfmt failure in #1423. Wiring is clean and additive.

Strengths:

  • packages/studio/src/hooks/useSdkSession.tscancelled flag + immediate comp.dispose() on cleanup correctly handles the race where the effect re-fires before openComposition resolves. The fetchr.json()openComposition chain is all guarded.
  • packages/studio/src/hooks/useSdkSelectionSync.ts — pure useEffect with no modifications to existing hooks. The toHfIds helper prefers group selections over primary selection, matching Studio's multi-select semantics.
  • useSdkSelectionSync uses session, domEditSelection, domEditGroupSelections as deps — correct, no stale closure risk.

Important:

  • packages/studio/src/hooks/useSdkSession.tsfetch(url) on a non-ok HTTP response (e.g. 404 if the file doesn't exist yet) still calls .then((r) => r.json()) without checking r.ok. A 404 body may not be valid JSON. The error is caught by .catch(() => setSession(null)) but it would be cleaner to add if (!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.tsdata.content typed as { content?: string } after r.json(). The ?optional=1 query 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

@vanceingalls vanceingalls force-pushed the sdk-stage7-studio-s7step1 branch 2 times, most recently from e177f53 to e2cad29 Compare June 15, 2026 05:33
@vanceingalls vanceingalls force-pushed the sdk-stage7-setselection branch from a32b07b to 02bd36c Compare June 15, 2026 05:41
@vanceingalls vanceingalls force-pushed the sdk-stage7-studio-s7step1 branch from e2cad29 to 6a66aa9 Compare June 15, 2026 05:41
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Thanks @james-russo-rames-d-jusso and @miguel-heygen.

Fixed:

  • adapter.read() bootstrapuseSdkSession now builds the adapter first then calls adapter.read(activeCompPath) to bootstrap. Single URL-construction path. Confirmed on HEAD.
  • setSelection re-fire on identical array — fixed in feat(sdk): stage 7 step 2 — setSelection API #1442 by pushing the equality check into setSelection itself. Any parent-render-allocated array that de-dupes to the same ids is a no-op.
  • flush-before-disposeconst c = compRef.current; if (c) void c.flush().finally(() => c.dispose()). In-flight writes complete before the session is torn down.
  • r.ok check on fetch response — confirmed fixed; read() now throws on non-ok (5xx) and returns undefined on 404, with a try/catch around json() for malformed bodies.

Design / acknowledged:

  • console.warn → toast — intentional for this wiring stage. Toast integration is a follow-up before the flag goes to prod. Filed as a comment in useSdkSession.ts.
  • useSdkSelectionSync co-location with useSdkSession in App.tsx — would require moving lines in a 600-line file that's already at the pre-commit hook limit. Deferring to a dedicated cleanup pass.
  • SSE double-connection race — confirmed the cleanup (return () => es.close()) always fires before the next effect run. React's strict-mode double-invoke would create and immediately close the extra connection, which is fine.

@vanceingalls

Copy link
Copy Markdown
Collaborator Author

One more fix in the latest push:

  • Dispose race (openComposition await) — the original code set comp = await openComposition(...) and then checked if (!cancelled) setSession(comp). But if cancelled fired while openComposition was in-flight, the cleanup's comp?.dispose() ran when comp was still null (no-op), and the composition resolved and was set into session state without ever being disposed. Added if (cancelled) { comp.dispose(); return; } immediately after the await so any composition that opens after cancellation is disposed immediately rather than leaked into the state tree.

@vanceingalls vanceingalls force-pushed the sdk-stage7-studio-s7step1 branch from af1d91b to 5d42eba Compare June 15, 2026 07:11
@vanceingalls vanceingalls force-pushed the sdk-stage7-setselection branch from 02bd36c to c13f332 Compare June 15, 2026 07:27
@vanceingalls vanceingalls force-pushed the sdk-stage7-studio-s7step1 branch from 5d42eba to 7c17dd9 Compare June 15, 2026 07:27
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

@james-russo-rames-d-jusso — all concerns addressed in post-review commits

  • useSdkSession hand-rolled fetch: replaced with adapter.read(activeCompPath). Single URL-construction source in http.ts.
  • flush() before dispose: comp.flush() awaited before comp.dispose() in the cleanup path. In-flight http writes drain before teardown.
  • dispose during openComposition race: additional guard added — if cleanup fires while openComposition is still resolving, the session is disposed immediately after open completes.
  • useSdkSelectionSync re-fire: confirmed domEditGroupSelections is memoized at the source in useDomEditSession (stable reference). The equality check now inside setSelection (feat(sdk): stage 7 step 2 — setSelection API #1442) provides a second backstop.
  • console.warn for persist:error: kept for stage 7 wiring. Surfacing to the Studio error-toast layer is filed for the feat(studio): stage 7 step 3c — sdk cutover for inline-style ops #1452 cutover PR.

@vanceingalls vanceingalls force-pushed the sdk-stage7-setselection branch from c13f332 to 377007a Compare June 15, 2026 20:54
Base automatically changed from sdk-stage7-setselection to main June 15, 2026 20:55
@vanceingalls vanceingalls dismissed miguel-heygen’s stale review June 15, 2026 20:55

The base branch was changed.

@vanceingalls vanceingalls force-pushed the sdk-stage7-studio-s7step1 branch from 7c17dd9 to 137aa5d Compare June 15, 2026 20:57
vanceingalls and others added 5 commits June 15, 2026 13:58
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>
@vanceingalls vanceingalls force-pushed the sdk-stage7-studio-s7step1 branch from 137aa5d to 169d24c Compare June 15, 2026 20:58
@vanceingalls vanceingalls merged commit 92e2c8c into main Jun 15, 2026
34 checks passed
@vanceingalls vanceingalls deleted the sdk-stage7-studio-s7step1 branch June 15, 2026 21:00
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