Skip to content

v8 stack 4/5: iOS Swift TurboModule rewrite - #26

Closed
dmurphy5 wants to merge 1 commit into
dylan/v8-stack-3-androidfrom
dylan/v8-stack-4-ios
Closed

v8 stack 4/5: iOS Swift TurboModule rewrite#26
dmurphy5 wants to merge 1 commit into
dylan/v8-stack-3-androidfrom
dylan/v8-stack-4-ios

Conversation

@dmurphy5

@dmurphy5 dmurphy5 commented Jul 27, 2026

Copy link
Copy Markdown

The stack

Five PRs, each based on the one before it. This is 4/5.

PR Contents Base
1 #23 toolchain, dependencies, and the ai.openspace rename master
2 #24 TurboModule spec and the JS surface #23
3 #25 Android durable journal and TurboModule #24
4 #26 iOS Swift TurboModule rewrite 👈 #25
5 #27 docs and example app #26

#22 stays open and untouched as the single-diff view of the same change.
Its tree and this stack's tip are byte-identical (tree f8404b0), so merging
either one produces exactly the same master. Review whichever is easier to
follow; merge one and close the other.


Replaces the 445-line VydiaRNFileUploader.m with Swift, as a TurboModule.

Three files: RNBackgroundUpload owns the URLSession work, EventJournal
persists terminal outcomes, TaskMap survives the task↔upload id mapping across
a relaunch. Apple documents taskDescription as non-durable, so it can no longer
be that mapping.

Terminal outcomes are journaled before being emitted, matching Android, so an
upload that finishes while the app is dead survives to the next launch.
Responses are classified into a typed errorKind and honor acceptStatus.

Why two classes

This split is forced, not stylistic. The generated spec header is Obj-C++, so the
TurboModule shell (RNFileUploader.h/.mm) has to be .mm. But a consumer's
AppDelegate is usually plain .m and needs to reach
setBackgroundSessionCompletionHandler:forIdentifier:. That handler therefore
lives on the Swift class, which plain Obj-C can reach via @import, while every
header stays private so the umbrella never exposes Obj-C++ to a .m file.

Details worth reviewing

invalidate is identity-checked. React Native dispatches invalidate
asynchronously and gives up waiting after ten seconds, so a replacement module
can register itself first. Clearing unconditionally killed event delivery for the
whole process after a slow reload.

The module takes its own methodQueue. A TurboModule otherwise shares the
process-wide queue, where this module's synchronous journal and task-map disk I/O
would stall unrelated native modules — and React Native's own invalidate, which
would queue behind it. The legacy bridge gave every module its own queue; a
TurboModule has to ask.

safeEmit stays synchronous on the URLSession delegate queue: the generated
emitter already locks its state and hops to the JS thread, and deferring to the
main queue would open a window where the TurboModule is torn down before the
block runs. Its try/catch is load-bearing — emitting before the TurboModule is
constructed, or with no listeners attached, throws std::bad_function_call.

Also: cancelUpload uses a lock-guarded Bool rather than a mutable array
shared across queues; user-cancel intent is consumed on every terminal outcome so
a cancel can't be resurrected; header values are restricted to String and
NSNumber instead of being string-interpolated; progress defaults to 0 rather
than -1; and getAllUploads reports real task states.

The podspec moves to iOS 15.1, compiles Swift and Obj-C++, and calls
install_modules_dependencies for the New Architecture pods. The stale
VydiaRNFileUploader.xcodeproj and the unused Helper category are deleted.

Verified

Built and exercised on a physical iOS device inside Diana: field note create
online and offline, plus attachments. CI doesn't build iOS, and I didn't run a
per-slice iOS build — this content is byte-identical to the branch in #22, which
was device-verified.

Still unverified on device

Background continuation while suspended or terminated; system relaunch on
completion; force-quit producing cancelReason: 'system'; event delivery after a
JS reload.

Replaces the 445-line VydiaRNFileUploader.m with Swift, split across three
files: RNBackgroundUpload owns the URLSession work, EventJournal persists
terminal outcomes, TaskMap survives the task/upload id mapping across a
relaunch. Apple documents taskDescription as non-durable, so it can no longer
be the mapping.

The two-class split on this platform is forced, not stylistic. The generated
spec header is Obj-C++, so the TurboModule shell (RNFileUploader.h/.mm) has to
be .mm — but a consumer's AppDelegate is usually plain .m and needs to reach
setBackgroundSessionCompletionHandler:forIdentifier:. That handler therefore
lives on the Swift class, which plain Obj-C can reach via @import, while every
header stays private so the umbrella never exposes Obj-C++ to a .m file.

Terminal outcomes are journaled before being emitted, matching Android, so an
upload that finishes while the app is dead survives to the next launch.
Responses are classified into a typed errorKind and honor acceptStatus.

Details worth reviewing:

invalidate is identity-checked. React Native dispatches invalidate
asynchronously and gives up waiting after ten seconds, so a replacement module
can register itself first; clearing unconditionally killed event delivery for
the whole process after a slow reload.

The module takes its own methodQueue. A TurboModule otherwise shares the
process-wide queue, where this module's synchronous journal and task-map disk
I/O would stall unrelated native modules and React Native's own invalidate.

safeEmit stays synchronous on the URLSession delegate queue — the generated
emitter already locks its state and hops to the JS thread — and its try/catch
is load-bearing, since emitting before the TurboModule is constructed or with
no listeners attached throws std::bad_function_call.

Also: cancelUpload uses a lock-guarded Bool rather than a mutable array shared
across queues, user-cancel intent is consumed on every terminal outcome so a
cancel cannot be resurrected, header values are restricted to String and
NSNumber instead of being string-interpolated, progress defaults to 0 rather
than -1, and getAllUploads reports real task states.

The podspec moves to iOS 15.1, compiles Swift and Obj-C++, and calls
install_modules_dependencies for the New Architecture pods. The stale
VydiaRNFileUploader.xcodeproj and the unused Helper category are deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant