v10 slice 1: define()/mutate() JS layer, codegen spec, native stubs - #41
Draft
dmurphy5 wants to merge 2 commits into
Draft
v10 slice 1: define()/mutate() JS layer, codegen spec, native stubs#41dmurphy5 wants to merge 2 commits into
dmurphy5 wants to merge 2 commits into
Conversation
The public surface becomes a durable mutation registry modeled on TanStack Query's setMutationDefaults + mutate. A consumer calls define() once per request kind at boot, with a request builder, an optional response parser, and onSuccess/onError handlers. Call sites pass only variables to mutate(). The native queue owns durability, retry, and delivery. JS: src/registry.ts (define, mutate, descriptor validation, header merge, vars cap, ids), src/delivery.ts (journal replay after configure(), eventId dedupe, wait-for-mutate ordering, handler routing, ack after the handler's promise, 30 s warning, unhandled-key reporting), src/index.ts (createUploadClient), src/types.ts. 113 tests plus type tests. Codegen spec: enqueue, pause, resume, cancel, setWifiOnly, updateHeaders, synchronous getRequests, onState/onProgress/onAttempt/onSettled emitters. Removed: startUpload, startChunkedUpload, cancelUpload, removeUpload, getAllUploads, and the v9 per-outcome emitters. Native: both modules stub the new methods with E_NOT_IMPLEMENTED so the package compiles and CI passes alone. The v9 engines stay in place for the Android and iOS slices to wire up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review findings on the JS layer, each with a test: - Outcomes of one id now deliver in order, one handler at a time, through a per-id promise lane. Different ids stay concurrent. - A cancelled outcome acks before the definition lookup, so an entry whose key was renamed still frees its bytes. - A settled event without a string key, kind and id is dropped with one warning per eventId, neither acked nor emitted (a v9-shaped journal). - mutate() resolves with the entry id it tracked, not native's return. - Nested descriptor objects (retry, accept, android, part range) reject unknown keys and wrong value shapes, so a typo cannot silently fall back to the transient default. - Header merge matches names without regard to case; the descriptor's spelling and value win. - A throwing state listener is caught and warned, and does not block the other listeners or the ack. - CHANGELOG lists the removed UploadId type. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR is slice 1 of v10. It replaces the JS surface with a durable mutation registry. It contains no engine code.
The model is TanStack Query's
setMutationDefaults+mutate. A consumer callsdefine()once per request kind, at boot. The definition holds a request builder, an optional response parser, andonSuccess/onErrorhandlers. Call sites pass only variables tomutate(). The native queue owns durability, retry, and delivery. Handlers register every boot, so an outcome that lands while the app is dead still reaches its handler.What the JS layer does
define()registers a key. Types infer from the definition:varsfrom therequestparameter, theonSuccessdata type from theresponseparser. Without a parser,onSuccessreceives theRawResponse. A seconddefine()with the same key replaces the first and warns in dev, so hot reload works.mutate(vars, { id? })runsrequest(vars)one time, mergesconfigure().headers()under the descriptor's headers, validates the descriptor (one body kind;partsonly withfile; parts tile the file from byte 0;varsat most 4 KB), defaultsexpiresAtto now + 14 days, and calls nativeenqueue. It resolves when native has persisted the entry.configure(). It drains the native journal, then subscribes to live outcomes. It dedupes byeventId. Delivery for an id waits for the caller'smutate()promise. It routes to the definition, parses the body, calls the handler, awaits the promise, then acks. A handler that throws is not acked, so it runs again at next boot. A handler that runs past 30 s logs a warning. An outcome with no definition is reported on thestatefeed withreason: 'unhandled-key'and stays unacked.configure({ lifetimeMs, retry, headers, android })is called once at boot after everydefine().pause(),resume(),cancel(id),setWifiOnly(),updateHeaders(patch), and a synchronousgetRequests()for the boot rebuild of a UI projection.A chunked example
Chunked uploads are a request kind, not a separate API. A descriptor with
fileandpartsroutes to the chunked engine.chunkPlan()stays, so the part count told to the server and the parts array derive from one result.Codegen spec and native stubs
The spec gains
enqueue,pause,resume,cancel,setWifiOnly,updateHeaders, a synchronousgetRequests, and theonState,onProgress,onAttempt,onSettledemitters.configure,getUnacknowledgedEvents,ackEvents, andonNotificationstay. Removed:startUpload,startChunkedUpload,cancelUpload,removeUpload,getAllUploads, and the v9 per-outcome emitters.Both native modules stub the new methods. They reject with
E_NOT_IMPLEMENTED.getRequestsreturns an empty array. The v9 engines and their Kotlin unit tests stay in place, so the Android and iOS slices above this PR can wire them to the new spec. The package compiles and CI passes alone. Until the engine slices land, this branch cannot send a request.The example app still uses the v9 API. CI only lints it. The hardening slice updates it.
Removed from the public surface
startUpload,addListener('progress' | 'error' | 'completed' | 'cancelled'),getUnacknowledgedEvents,ackEvents,getAllUploads,cancelUpload,removeUpload, per-uploadwifiOnly, and the v9 event and option types. The CHANGELOG lists each with its replacement.Known limits
varstype must be atypealias, not aninterface, and arrays must be mutable. The recursiveJsonconstraint requires it. This is documented on theJsontype.file://normalization in JS. The engine slices accept both forms.Test Plan
What's required for testing (prerequisites)?
Node 20, yarn, JDK 17 for the Android unit tests.
What are the steps to reproduce (after prerequisites)?
The JS tests cover: define replace-on-duplicate, vars cap, one body kind, parts tiling, header merge order, expiresAt default, id passthrough, replay after configure, buffering during the drain, eventId dedupe, wait-for-mutate ordering, unknown key, completed with parser, truncated body, parser throw, error and cancelled routing, handler rejection keeps the entry unacked, the 30 s warning, per-id delivery order, cancelled ack without a definition, malformed-event drop, nested descriptor validation, case-insensitive header merge, a throwing state listener, getRequests filter, and listener mapping. Type tests prove vars and response inference and that a mistyped handler does not compile.
Compatibility
Checklist
README.md🤖 Generated with Claude Code