feat(sdk): stage 7 step 1 — http persist adapter#1441
Conversation
88f067e to
db5032f
Compare
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Verified at HEAD db5032fb.
Stage 7 step 1 — net-new http persist adapter (+291/-0). Read this against #1423's fs adapter shape and #1425's flush() contract. Verdict: approve-with-concerns — concerns are stage-appropriate (this is wiring, not the cutover at #1452), but flagging now so #1450's shadow-parity run doesn't get confused by silent persist failures.
Concerns
-
No retry policy on transient failure (
packages/sdk/src/adapters/http.ts:38-50). A 5xx or network hiccup firespersist:erroronce and drops the write on the floor. The PR title says "persist adapter" andflush()is supposed to mean "all writes durable" per #1425's contract — but here, post-flush()you could still have lost writes that surfaced only as apersist:errorevent nobody happened to subscribe to. For #1450's shadow-parity comparison to be meaningful, the http path needs at-least-once semantics (retry-with-backoff for 5xx/network, give up only on 4xx). If the design intent is "let the caller handle retries via the error event," that should be documented at theHttpAdapterOptionsjsdoc — right now a reader assumes parity with fs adapter durability. -
Concurrent writes to the same path race on the server (
packages/sdk/src/adapters/http.ts:18-27). Two back-to-backwrite("comp.html", ...)calls each fire a PUT in parallel; whichever arrives at the backend last wins. The fs adapter implicitly serializes via the OS write queue per inode; http has no equivalent. For Studio's hot-loop dispatch (every keystroke in a style editor), this is a real corruption vector — the user types fast, two PUTs race, and the second-fired-but-first-arrived PUT clobbers the latest content. Suggest a per-path serialization queue (chain new writes onto the inflight promise for the same path). -
Auth strategy unstated (
packages/sdk/src/adapters/http.ts:30-37).fetch(url, ...)with no headers means the request relies entirely on ambient cookies. That's probably fine for the Studio-in-same-origin path, but if anyone ever wants this adapter from a CLI / external tool / cross-origin context, there's no injection point. Cheap fix: add an optionalheaders?: HeadersInit | (() => HeadersInit)toHttpAdapterOptions. Worth doing before this ships externally as a@hyperframes/sdk/adapters/httpsubpath export. -
listVersions/loadFromare silent no-ops (packages/sdk/src/adapters/http.ts:55-62). Returning[]/undefinedis fine if nothing in the SDK relies on them, butcanUndo/canRedofrom #1431 — does any code path consultlistVersionsto decide whether server-side history is browsable? If yes, the http adapter silently breaks that UX vs the fs adapter. Worth a one-line confirmation in the PR body that these are intentionally unimplemented and which SDK features degrade as a result.
Nits
private fireError(message, cause?)(line 80) — thecauseis stuffed into the event shape but never logged anywhere by default. Consider a defaultconsole.warnwhen no listener is registered, so a misconfigured caller doesn't silently lose write errors in dev.- Test at
http.test.ts:117usesvoid adapter.write(...)— fine, but the test relies onvi.fn's microtask ordering. Slightly fragile; not blocking.
What I didn't verify
- Whether the backend
PUT /api/projects/:id/files/:pathendpoint actually exists and acceptstext/plain— assumed from PR context (projectFilesUrlshape suggests an established route). - Whether #1450's shadow-parity mode tolerates persist mismatches gracefully — if it does, my retry concern downgrades to a nit.
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. The HTTP adapter implementation is clean.
Strengths:
packages/sdk/src/adapters/http.ts— inflight write tracking withSet<Promise<void>>+flush()is architecturally consistent with thefsadapter (#1425). Same proven pattern across both adapters.persist:errorevent surfaced for both non-OK HTTP responses and network errors — callers can recover or surface UI feedback without polling. TheAbortErrorskip (don't emit on deliberate cancellation) is correct.http.test.tscovers: successful write, non-OK response error, network error, flush drains in-flight — good contract coverage.
Important:
-
packages/sdk/src/adapters/http.tswrite()— no retry logic on transient 5xx or network errors. The adapter emitspersist:errorand the caller must handle retry if needed. This is consistent with the fs adapter design, but worth documenting in the JSDoc: callers should subscribe topersist:errorand decide whether to retry or surface a UI error. -
http.tswrite()— usesDELETEmethod (orPUT? — confirm) with the path as a URL segment. Ifpathcontains characters that are not URL-safe (e.g. spaces,%),encodeURIComponentshould be applied. Check whether the caller (useSdkSession) already ensures the path is URL-safe before passing to the adapter.
Nit:
http.test.ts— no test for concurrentwrite()calls where the second call starts before the first resolves;flush()should drain both. The fs adapter T13 suite covers this — consider mirroring it here for parity.
Verdict: APPROVE (pending #1423 oxfmt fix)
Reasoning: Correct inflight pattern, good error surfacing, consistent with the fs adapter contract.
— magi
4f11be8 to
f430c55
Compare
db5032f to
757ec4c
Compare
|
Thanks for the detailed reviews, @james-russo-rames-d-jusso and @miguel-heygen. Fixed in this revision:
Design choices (not changed):
|
0f6f62b to
ae58fa2
Compare
f430c55 to
3ec2adc
Compare
|
@james-russo-rames-d-jusso — all concerns addressed in post-review commits
|
d102376 to
1ded324
Compare
The base branch was changed.
Browser-fetch-based PersistAdapter for Studio REST file API. Uses fetch-only (no node:fs) so it is safe to bundle in Vite. - packages/sdk/src/adapters/http.ts — HttpAdapter class + createHttpAdapter factory - packages/sdk/src/adapters/http.test.ts — 14 unit tests (fetch mocked via vi.stubGlobal) - packages/sdk/src/index.ts — re-export createHttpAdapter + HttpAdapterOptions - packages/sdk/package.json — ./adapters/http subpath export (dev + publishConfig) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add per-path promise queue so rapid successive writes to the same composition file cannot race at the server. Different paths still write concurrently. Two new tests (RED→GREEN verified). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lush-drains-two test - Add optional headers?: HeadersInit | (() => HeadersInit) to HttpAdapterOptions for cross-origin / CLI / auth injection (function form refreshes on each PUT) - Document listVersions/loadFrom as intentional no-ops (server versioning not exposed) - Add flush-drains-two-concurrent-writes test to mirror fs adapter T13 coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
ae58fa2 to
9e58fd1
Compare
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Verified at HEAD 9e58fd11. R1 was at db5032fb. Verdict: approve-with-concerns (one R1 concern still open, one author-claim mismatch).
R1 finding verification
| R1 concern | Status at HEAD | Evidence |
|---|---|---|
| No retry on transient 5xx/network (TOP) | Resolved-differently as design choice | packages/sdk/src/adapters/http.ts:64-70 — still single-shot, persist:error then return. Author's stated contract: callers subscribe and decide. Matches fs adapter shape. Reasonable for stage 7 step 1; see open concern below. |
| Concurrent writes to same path race | ✅ Fixed | http.ts:23,39-50 — pathQueues: Map<string, Promise<void>> chains each new write onto the inflight promise for the same path. Verified by http.test.ts:204-230 "serializes concurrent writes to the same path" + :232-256 "does not block writes to different paths". Clean per-path serialization without cross-path contention. |
| Auth strategy unstated | ✅ Fixed | http.ts:10-15 adds headers?: HeadersInit | (() => HeadersInit) to HttpAdapterOptions. Lazy function form supported for token refresh (http.ts:57-58). Test coverage at http.test.ts:124-152 for both static and lazy. |
listVersions / loadFrom silent no-ops |
Partial | http.ts:77,82 got one-line JSDoc ("not exposed by this adapter; returns [] intentionally"). Documents the no-op but does NOT answer the R1 question — "which SDK features degrade?" (e.g. canUndo/canRedo from #1431, version-history UI). The PR body comment claims "explaining... which SDK features degrade" but the actual JSDoc doesn't list any features. Minor — defer to follow-up if version-browser UX is ever wired to the http path. |
Also confirmed magi's encodeURIComponent concern: applied on both read (:31) and write (:54) URL construction — was already there at R1, not a post-review fix.
Concerns
-
Author claim doesn't match diff (
packages/sdk/src/adapters/http.ts:38). The post-review comment states: "Added a JSDoc note on write() making this explicit: 'callers should subscribe to persist:error and decide whether to retry or surface a UI error.'" — butasync write()at HEAD has no JSDoc at all. Either the JSDoc commit got dropped during a rebase/save-and-push, or the comment is forward-looking. This matters specifically because retry was deferred as a design choice — without the docstring, the next reader still has to infer the contract frompersist:errorsemantics. Suggest a one-line JSDoc abovewrite()(and ideally onHttpAdapteritself) making the at-most-once + error-event contract explicit, especially given #1450's shadow-parity will be comparing http vs fs persist outcomes. -
Retry-as-caller-responsibility leaves #1450 with a comparison artifact (
packages/sdk/src/adapters/http.ts:64-70). I'll accept design-parity-with-fs as a legitimate resolution here — but worth confirming explicitly on the #1450 shadow-parity PR that the comparator either (a) tolerates http-onlypersist:errorevents as expected drift, or (b) coerces them to a retry before comparing. Otherwise a single transient backend 503 during a Studio session could produce a false-positive parity failure that masks real regressions. Out of scope to fix here; logging so it's not lost.
What I didn't verify
- Whether
useSdkSession(the documented caller) actually subscribes topersist:errorand surfaces or retries on UI — assumed from author's comment. - Whether #1450's shadow-parity comparator handles unilateral http errors gracefully (see second concern).
Review by Rames D Jusso
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-review of fix commits 91f99c0 and 9e58fd1 against my prior COMMENT review.
✅ Fixed
Per-path serialization (91f99c0 — http.ts:31-34): pathQueues: Map<string, Promise<void>> chains new writes for the same path onto the previous inflight promise. Exactly the pattern I asked for. Tests confirm: same-path write2 waits for write1; different-path writes start concurrently.
headers option (9e58fd1 — http.ts:9-17): headers?: HeadersInit | (() => HeadersInit) added to HttpAdapterOptions with lazy-function variant. Tests cover: static header merged into every PUT, function called once per write. The auth injection point I flagged is now there.
listVersions / loadFrom documented (9e58fd1 — http.ts:74,78): JSDoc comments now explicitly say server-side versioning is unimplemented intentionally. No silent surprise.
Concurrent flush test (9e58fd1 — http.test.ts:183-213): two concurrent different-path writes, flush waits for both — mirrors the gap I called out from the fs T13 suite.
Nit (carried forward, still optional)
fireError on no listener — still no console.warn as a fallback when no error listener is registered. Low urgency; the JSDoc now asks callers to subscribe, which is the next-best thing.
CI
All required checks pass on HEAD 9e58fd1. Windows pending is pre-existing.
Approval stands.
Verdict: APPROVE
Reasoning: All three substantive concerns from the prior round are addressed with correct implementations and tests.
— magi
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
R2 concerns addressed at HEAD
|

What
Adds
createHttpAdapter— a browser-nativePersistAdapterthat reads and writes composition files through the Studio dev-server's/api/projects/:id/files/...endpoints using the Fetch API. Exported as a subpath:@hyperframes/sdk/adapters/http.Why
The SDK's
PersistAdapterinterface previously had filesystem (fs) and in-memory (memory) implementations, both Node-only. Studio runs in the browser and needs to persist compositions back to the dev server. This adapter is the browser-compatible plug that letsopenCompositionwork in a Studio context without Node I/O.How
HttpAdapterimplementsPersistAdapter:read→ GET,write→ PUT with per-path queue to serialize concurrent writes to the same file (at-most-once in-flight per path)flush()waits for all in-flight queues to drainlistVersions/loadFromproxy the server's version history endpointson('persist:error')fires on network/non-2xx failures without throwing; callers can surface errors non-fatallyTest plan
http.test.ts: read/write round-trip with MSW, concurrent write serialization, persist:error event on 503, flush drains queuepersistAdapter.contract.test.ts) passes for the http adapter against a mock server