Skip to content

Move main to openspacelabs v11.0.2 (upstream v11.0.1 + staging-body sweep) - #14

Closed
thevwick wants to merge 134 commits into
mainfrom
chore/upgrade-to-openspacelabs-v11
Closed

thevwick wants to merge 134 commits into
mainfrom
chore/upgrade-to-openspacelabs-v11

Conversation

@thevwick

Copy link
Copy Markdown

Brings the fork up to date with openspacelabs upstream and puts our patch on main.

Contents

  • openspacelabs v11.0.1 (new architecture TurboModule line)
  • Our patch on top: sweep staged upload bodies that no entry claims at iOS relaunch
  • Tagged v11.0.2

Why
main has been sitting on the 2024 Vydia line while all current work happened on the v11 line. This makes main the branch to use.

Safety
The previous main is preserved on vydia-7x-legacy. Both apps pin by commit SHA, not branch, so neither Sitemate Mobile (c26be72) nor Dashpivot (b8ce380) is affected; those commits stay reachable on vydia-7x-legacy.

🤖 Generated with Claude Code

dmurphy5 and others added 27 commits October 6, 2026 16:35
vars is now any object or null, and data is any JSON-serializable value.
Generated OpenAPI request types and DTOs have optional fields, object-typed
values, and nullable strings, none of which satisfy a recursive Json type.
TypeScript cannot prove serializability for those shapes, so mutate()
validates at runtime: a cycle, a function, a Date, or a value that does not
serialize to an object is rejected with a named error. The 4 KB cap stays.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Native enqueue() is one synchronous disk write, so a promise that never
settles is a native bug. mutate() now races it against a timer, 10 s by
default (configure({ enqueueTimeoutMs })). On timeout it rejects with an
error naming the key and id and logs a warning, and delivery for that id
is not held open. If native did persist the entry, its outcome still
reaches the handlers, and a same-id retry resumes rather than duplicates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ative contract

From the JS interface review. The 4 KB vars cap was a leftover from the
old ctx field; the generated-ops design puts the request body in vars, so
the default is 1 MB and configure({ maxVarsBytes }) sets it. Meta gains
deliveries, native's count of how many times an outcome was delivered, so
an app can decide a poison policy; the library never gives up on its own.
The response parser receives vars, so a decoder that needs an id runs in
the parser path, where a throw is a terminal onError, not a replay.

The codegen spec comments now pin the contract the native slices
implement: enqueue resolves after every staged copy is on disk and rejects
with E_RUNNING, E_FILE_MISSING, or E_STORAGE; same-id rules for a changed
body; the exact set of rows getRequests returns; cancel of an unknown id
is a no-op; ackEvents is void and idempotent; updateHeaders bumps a header
generation; attempt events are live-only. RequestRow gains nextAttemptAt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Findings from the Android and iOS review that land on the JS side:

- vars and data cross the bridge as JSON strings (varsJson, dataJson).
  React Native on iOS drops null-valued object keys, so an object form
  loses fields such as { status: null }. A "null" body is a real body; a
  bodiless request omits data.
- A GET with a body is rejected at mutate(). DELETE with a body stays.
- E_INVALID joins the enqueue rejection codes for input native cannot
  send: non-http(s) URL, bad header names or values, GET with a body,
  parts that do not tile the moved file.
- deliveries counts deliveries that reached a JS listener; an outcome
  journaled with no listener starts at 0 and is not emitted live.
- A different url or method is a different body. attempts count the
  current generation. cancel of a settled entry also forgets its
  unacknowledged outcomes. updateHeaders also replaces same-named part
  headers. A paused entry past expiresAt settles at resume.
- AttemptEvent.outcome narrows to completed | error; pause, cancel, and
  supersede emit no attempt event.
- The watchdog text says a timeout means native did not answer, and that
  staging a large file body takes time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e written

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Android implements the v10 contract pinned in the codegen spec. The v9
chunked manifest store generalizes into QueueStore: one durable entry per
mutate() (id, key, vars, descriptor, staged body, state, attempts, bytes,
expiresAt, header generation, deliveries) written tmp+fsync+rename before
any attempt. BodyStaging writes JSON and multipart bodies to library files,
copies single-file bodies, and moves chunked sources as v9 did.

WorkManager runs one worker per entry. WorkerOps applies the retry table
from the plan: accept rules with bodyIncludes, transient network/5xx/408/
429 with jittered backoff and nextAttemptAt, 401/403 parking with a header
generation that updateHeaders bumps, per-request exempt lists, expiry at
expiresAt with bytes kept. The transfer semaphore (4) and chunked window
(3) stay. Pause gates the queue; cancel settles a live entry and forgets a
settled one; ack is void and idempotent and forgets only a matching
generation. The journal writes every outcome before onSettled emits it;
onState carries a full RequestRow, onProgress is throttled 1 s / 10 min,
onAttempt is live-only. RequestIndex backs the synchronous getRequests.
First launch imports v9 journal entries as legacy rows and cancels v9 work.

Logic lives in JVM-testable classes (QueueController, WorkerOps,
EnqueueRules, EntryTransitions); the module and workers are thin shells.
217 unit tests, including crash-mid-write cases for the store.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reliability: begin() applies an own-generation journal record instead of
re-running, so a crash between the journal write and the store update
cannot double-send. A failed journal write on settle holds the record in
memory and retries; nothing is acked that was not written. cancel() stops
the work in a finally block, journals before it saves, and applies an
existing record of its generation rather than adding a second outcome.
Listening state and the deliveries count are decided under one journal
lock, and a live settle goes to the listener that drained, not the newest
module instance.

Contract: vars and data arrive as JSON text; "null" is a body; GET with a
body rejects E_INVALID. Attempt events are completed or error only; pause,
cancel, and supersede emit none. A settled entry's cancel forgets its
unacked outcomes. attempts reset only on reopen. Under pause only an
accepted response settles. A same-id enqueue over a legacy row adopts its
v9 manifest. The response body cap applies while streaming. A prune never
deletes an eventId a row or a new record names. Rows carry live bytesSent.

Platform: a headless run stopped at JobScheduler's 10-minute limit takes
the transient path with backoff. Header validation messages carry the
name and offset, never the value. README documents the headless limit and
the allowBackup requirement.

Simplification: one EntryRun attempt path shared by simple and chunked
transfers; classifyFailure is the only failure rule; dead code removed.
Tests: 289 (was 217), with a TransferHost seam for the run loop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The test cleared the listener with stopListening(listener), but from the
second iteration on the listener was the previous iteration's owner, so
nothing was cleared. When the settle won the race, the journal stamped one
live delivery and the drain counted a second one. CI failed about one run
in five. Now each iteration clears whatever listener is set and asserts
that none is set before the race starts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When the cancel record was journaled but the entry save then failed,
cancel() rejected with E_STORAGE. The cancel had already happened: the
worker stopped, the cancelled outcome went to JS, and the ack, the next
cancel(), or the boot sweep applies the record. A rejection told JS
that nothing changed.

Now cancel() resolves in that case, as iOS does. It rejects only when
the journal write fails, and then nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iOS implements the same contract over a background URLSession. QueueStore
generalizes the v9 chunked manifest into one durable entry per mutate(),
written with fsync before any task is created. BodyStaging writes JSON and
multipart bodies to files, copies single-file bodies, and moves chunked
sources, so every upload comes from a file as the background session
requires.

QueueCoordinator owns the entries: enqueue with the same-id rules and
E_RUNNING/E_FILE_MISSING/E_STORAGE codes, one task per attempt keyed by
(id, generation, attempt), TaskMap reconciliation at relaunch and through
handleEventsForBackgroundURLSession, the retry table with delayed tasks
for backoff, 401/403 parking with header generations, expiry, pause,
cancel, and forget-after-ack. The journal writes before onSettled emits;
onState, onProgress (throttled), and onAttempt follow the TypeScript
payload types. RequestIndex backs the synchronous getRequests. First
launch imports v9 journal entries as legacy rows.

The pure half of the module is a SwiftPM package (ios/Package.swift) so
`cd ios && swift test` runs 138 host-side tests, including crash-mid-write.
The podspec excludes the package files and test sources from the pod. The
example app builds for the simulator.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reliability: reconcile applies unacked same-generation records to a live
row before the task loop, so a failed entry save after the journal write
cannot re-issue a settled request. deliveries counts only deliveries that
reached a JS listener: journaled at 0 with no listener, not emitted live.
A chunked part with a pending replay holds its slot for the grace period.
A completion that lands before reconcile still advances the attempt
ordinal. A part-file build error settles error/file only when the blob is
missing or short; otherwise it refills after backoff. The background
completion handler releases as soon as the awaited replay lands, and the
grace timer only closes the wait it opened.

Contract: vars and data arrive as JSON text; NSNull handling for data is
gone; E_INVALID covers non-http(s) URLs, bad header names or values, and
GET with a body. url and method are part of the body fingerprint. attempts
reset only on reopen. A paused entry past expiresAt settles at resume; the
expiry check runs after classification. A same-id mutate on a waiting
retry retries now. updateHeaders patches part headers. Rows carry live
bytesSent; legacy rows report 0/0. Attempt events are completed or error.

Simplification: dead fields removed (TaskMap.Meta.accept, isChunkedPart,
fileUnreadable, lifetimeMs, descriptorJSON, allDormantManifests).
Tests: 170 (was 138), including relaunch and pending-replay cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner decision, matching Android: a cancel whose journal write fails
rejects with E_STORAGE and changes nothing; the caller may call again.
The hold-and-retry path stays for settle outcomes only. A cancel of a
settled entry now forgets atomically: the id directory is set aside by
one rename, the journal files are deleted, and a failure puts the row
back and rejects. A crash between the two steps is finished or undone at
launch. 178 tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A background URLSession raises an Objective-C exception, not an error,
when uploadTask(with:fromFile:) cannot read the file. Swift cannot catch
it, so the app ends. v9.0.1 fixed this for the v9 code (Sentry
DIANA-19VW). This applies the same guard to the v10 transport.

The transport now throws when the session cannot open the file. Each
caller decides what that means:
- A simple entry whose staged body is gone settles error 'file', as the
  missing-body check before it already does.
- A simple entry whose body still exists is unreadable for now (data
  protection while the device is locked). It goes back to queued and is
  issued again after a backoff.
- A chunked part follows the failed part-file rule: a short blob settles
  error 'file'; otherwise the window refills after a backoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The example app is the device test harness. It is rewritten on the v10
API: definitions at module scope (JSON POST, multipart, chunked with
chunkPlan, bodiless DELETE, no-vars GET), configure() with a headers
provider, a queue list from getRequests() and the state and progress
feeds, an attempt log, and controls for every step of the two device
scripts: cancel, pause, resume, wifiOnly, updateHeaders with a typed
value, same-id resume and replace, a short expiresAt, and a section of
outcomes journaled before this launch for the kill-and-relaunch check.
The local Express server answers by path segment (401, 404, 409, 503,
slow, oversize) and writes chunked parts at their offsets. The stale
Podfile.lock is regenerated for RN 0.84.1, the AppDelegate wires the
background completion handler, and the Android manifest declares and
requests POST_NOTIFICATIONS. The example README carries the device
script in UI terms.

Library: createUploadClient({ native }) accepts a fake TurboModule, and
react-native-background-upload/src/testing exports createFakeNative(),
an in-memory Spec with settle(), seeded rows, recorded calls, and ack
tracking, so a consumer tests definitions and handlers against the real
registry and delivery. CI gains a macOS job that runs swift test.

Docs: CHANGELOG 10.0.0 covers all slices; README gets an "Upgrading from
v9" section, iOS platform notes, a "Testing your definitions" section,
and a corrected AppDelegate snippet (@import for .m, header search path
for .mm). Version 10.0.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both changes come from the Diana readiness audit. Diana's Wi-Fi setting
applies to captures only, and Diana pauses captures only while field
note traffic keeps flowing. A queue-wide gate cannot express either.

RequestDescriptor.wifiOnly overrides the queue setting for that entry;
an entry that does not set it follows setWifiOnly, so toggling the
setting still moves queued captures. Both natives evaluate the
constraint per attempt and persist the per-entry value.

pause(scope) and resume(scope) take { keys?: string[] }. No keys means
the global gate, as before. Keys add to or remove from a persisted set.
An entry is paused when the gate is on or its key is in the set; each
entry that moves emits one state event; a scoped resume does not free
an entry the other gate still holds; an entry enqueued into a paused
scope starts paused. Pause produces no outcome and no attempt event.

The fake native in src/testing models both. Tests: JS 189, Android 307,
iOS 201.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The promotion PR says the script measures the time from mutate() to the
first send, which decides the deferred in-process fast path. The step
now exists, on both platforms, using the log clocks the harness prints.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
## Description

A chunked upload keeps 3 part tasks in the iOS background session. It
creates the next part task when one finishes, and that usually happens
while the app is in the background. Apple's docs say the system starts a
task created in the background at its own discretion and ignores
`isDiscretionary = false`. So after the first 3 parts, a large file can
wait for the system, for example until the device charges.

This adds `ios: { queueAllParts: true }` to the request descriptor. With
it, the library creates every part task at `mutate()`. A `mutate()` in
the foreground then leaves iOS nothing to defer. Without it, nothing
changes.

Before: a 40-part capture had 3 part tasks in the session, and parts 4
to 40 were created later in the background.
After, with `queueAllParts`: all 40 part tasks are in the session after
`mutate()`.

## What to look at

- `ChunkedCoordinator.refill` uses every part as the window when the
entry sets the flag.
- `QueueEntry.queueAllParts` is optional, so entries saved by 10.0.0
decode with the window of 3.

## Decisions and trade-offs

- **Each part task has its own part file.** With every part queued, the
upload needs the file's size again in free space. The README and the
type doc say so.
- **A failed part is retried as a new task.** A retry created in the
background is still at the system's discretion. The README says so.
- **iOS only, under `ios`.** Android runs parts in a worker, not a
background session. Android ignores the field.
- **The flag without `parts` is rejected in JS.** It would do nothing,
and the library rejects fields that would be dropped in silence.

## Test Plan

- `swift test` in `ios/`: 210 tests pass. New tests cover every part at
enqueue, `false` keeping 3, a relaunch that recreates lost parts past 3,
a parser reject, and decoding an entry saved without the field.
- `yarn jest`: 191 tests pass, with new tests for forwarding and
validation. `tsc` is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01QFiCVD9pHXvcx712ubCLpR

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDL4Nr8RSovs72UACtNu82
## Description

iOS starts a task created while the app is in the background at its own
discretion and ignores `isDiscretionary = false` for it ([Apple
docs](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/isdiscretionary)).
10.1.0's `ios.queueAllParts` only fixed this for parts after the first
3, and only for new uploads. Retries, a `mutate()` from a background
wake, and tasks a relaunch rebuilds are still created in the background,
and uploads already queued stay deferred.

This does two things:

- **Every part task at `mutate()`.** The window of 3 and
`ios.queueAllParts` are gone. There is no opt-out.
- **Replace background tasks on activation.** Each task records in the
TaskMap whether the app was active when it was created. When the app
becomes active, every task created while it was not, that has not sent a
body byte, is cancelled as superseded and created again. A simple
request keeps its attempt ordinal and request id. Both kinds keep what
is left of a backoff.

Tasks from earlier versions have no tag, so they count as background
tasks. The first activation after the upgrade replaces them, which
unsticks uploads already waiting on the system without any app change.

## What to look at

- `QueueCoordinator.replaceBackgroundTasks`,
`ChunkedCoordinator.replace`, and `issue(replacing:)`.
- "Active" means `didBecomeActive` to `willResignActive`, set without a
hop to the coordinator queue. A task created while the app is inactive
but visible counts as background, so the worst case is one extra
replacement.
- If a replaced task lands before its cancel takes effect, the existing
paths handle it: a part is accepted and its replacement stopped, and a
simple request settles and its replacement is cancelled. There are tests
for both.

## Decisions and trade-offs

- **Breaking:** `mutate()` with `ios` now rejects as an unknown field.
Diana's PR that sets `ios.queueAllParts` needs to drop it.
- **Disk:** every part file exists at once, so a chunked upload needs
the file's size again in free space until the parts are sent.
- **Only unstarted tasks are replaced.** A task with `countOfBytesSent >
0` is never cut off. A simple request with an empty body is left alone,
because 0 bytes sent can't tell whether it went.
- **Still a gap:** while the app stays in the background, a retry waits
for the system until the next activation. iOS doesn't let us close that.

## Test Plan

- `yarn test`: 190 pass. `tsc` is clean.
- Swift host tests run in CI (no Swift toolchain where this was
written). New `CoordinatorForegroundTests` cover replacement of simple
and part tasks, tasks kept when created active or already sending,
resign-active tagging, backoff carry-over, a replaced task that lands
anyway, paused entries, and a relaunch that replaces untagged tasks from
an older build. Chunked tests were updated for a task per part.
- Not yet run on a device.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01FuzycfW82c1DKeSSwzuc9w)_

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDL4Nr8RSovs72UACtNu82
Vydia#54 was squash-merged before its last two commits were pushed, so they
never reached master. This PR carries them, rebased onto 11.0.0.

**Review fixes for replacing background tasks.** Replacement now runs 2
s after the app becomes active and skips tasks that have a response. A
superseded simple task whose body landed completes the entry, even from
an older attempt. Reconcile never adopts a superseded task, and progress
only counts from the task that owns it.

**Batch the entry and TaskMap writes for part tasks.** With every part
queued at mutate(), each part task saved the whole entry (which holds
every part's URL) and each cancel rewrote the whole TaskMap. A 200-part
upload did hundreds of full-file writes on the coordinator queue, which
delegate callbacks wait on. Now `enqueueParts` builds the part files and
saves the entry once before any task exists,
`cancelTasks(keys:purpose:)` writes every purpose in one TaskMap write,
and part replacements are grouped per entry. Each task still writes its
TaskMap entry before it resumes.

A commit moves the CHANGELOG notes from the released 11.0.0 section to
Unreleased.

**Fixes from review of this PR.** A replaced simple task whose body
landed while the entry was parked for auth now completes the entry. Only
the latest scheduled replace runs, so a quick background-and-return
still waits the full 2 s. A part left without a task when `enqueueParts`
stops mid-batch keeps its remaining backoff on refill. Each has a test
that fails without the fix.

`swift test` (233) and `yarn test` (190) pass locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018kyo6onjB1N53r2W6XJHWd

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kyo6onjB1N53r2W6XJHWd
Device testing of Sitemate Mobile on an iOS simulator killed the app while
mutate() was writing an 8.4 MB body for a new id. The half-written
body-*.tmp was still on disk two relaunches later. BodyStaging removes its
tmp on every failure path, but a process death between creating the tmp
and saving entry.json leaves it, and nothing reclaimed a directory with no
row: QueueStore.sweep(entry) only ran after a successful enqueue of that id.

reconcileAll now sweeps once every grace wait has ended, right after it
releases the background completion handlers:
- Every non-legacy entry in the index gets QueueStore.sweep(entry), the
  same sweep enqueue runs after a save. It keeps the entry's bodyPath and
  its current-incarnation part files, and removes other bodies, .tmp files,
  and stale-incarnation part files. That covers a same-id replace that
  died before its save or before its sweep. This generalises the
  ChunkedCoordinator.reconcile precedent (store helpers decide orphans from
  what reconcile knows is live) instead of adding a parallel scheme.
- New QueueStore.sweepEntryless(): a directory with no entry.json at all
  loses body-*, part-* and *.tmp files, then the directory itself if
  rmdir finds it empty.

Safety:
- Live and recoverable entries keep their files: a claimed directory is
  one with an entry.json, and an entry's referenced body is never removed.
  The sweep waits for reconcile's grace waits, so an entry awaiting a
  daemon replay is swept only after that replay has settled it or it has
  been re-issued, and its body survives either way.
- In-flight writes: staging, saves and part-file writes all run on the
  coordinator's serial queue, as does the sweep, so no write of this
  process is ever half done when it runs.
- Left alone deliberately: a blob-* or v9 blob in an entry-less directory
  (the caller's file was moved, not copied, into it, and adoptableBlob lets
  a same-id retry resume from it), any directory holding a v9
  manifest.json (a dormant v9 upload), a directory whose entry.json exists
  but is unreadable, and legacy rows.
- A failed removal is logged with NSLog and skipped. The sweep runs after
  issuing, so it can never fail or delay an upload. sweep(entry) now logs
  instead of using try?.

Tests: an orphan body with no owning entry is removed, and a read-only
orphan directory is skipped without blocking the upload. A live entry
keeps its body while its stray bodies go. An entry in its grace window
keeps its body, nothing is swept until the grace ends, and a body staged
by this process during the wait survives. A store test covers what
sweepEntryless keeps. The three coordinator tests fail with the sweep
call removed.

Adopting v11.0.1 (this branch is from openspacelabs-main at v11.0.1;
4 commits after v10.1.0: d3f38f3, 7a4b186, 3741d9d, 45495c8):
- Breaking API: `ios` is gone from RequestDescriptor. `ios.queueAllParts`
  is removed, not renamed. mutate() with any `ios` key rejects with
  'unknown descriptor field "ios"' in JS validation and never reaches
  native. Nothing replaces it: every chunked part task is now always
  created at mutate(). No other option, export or event changed, and
  Android is untouched.
- Behaviour, iOS only:
  * Every part of a chunked upload is queued at once, so the upload needs
    the file's size again in free space until its parts are sent. Paced
    by httpMaximumConnectionsPerHost = 4 per session.
  * The TaskMap tags each task with whether the app was active when it was
    created. 2 s after didBecomeActive, every background-created task
    that has sent no body byte and has no response is cancelled as
    superseded and created again. A simple request keeps its attempt
    ordinal and request id, and both kinds keep their remaining backoff.
    Tasks from older builds have no tag, so the first activation after
    the upgrade replaces them, which unsticks uploads iOS had deferred.
  * Reconcile never adopts a superseded task. A superseded simple task
    whose body landed completes its entry instead of being re-sent.
  * Part-task entry and TaskMap writes are batched per mutate()/replace.
  * Known gap: while the app stays in the background, a retry still waits
    for the system until the next activation.
  The library wires didBecomeActive and willResignActive itself. No
  AppDelegate or Podfile change is needed beyond the usual pod install
  for changed native code.
- Dashpivot Mobile (branch fm/dpm-upload-poc, POC at 7b6e4d73e): must drop
  `ios: { queueAllParts: true }` at
  app/sync/uploads/asset-upload-queue.ts:52, or every large-asset mutate()
  rejects. It also pins openspacelabs/react-native-background-upload
  directly, not the site-mate fork, so the bump should point it at this
  fork. Large chunked assets need the extra free space noted above.
- Sitemate Mobile (SMM-3293, origin/fix-26.16/SMM-3293-durable-upload-outcomes,
  pinned to the fork at e364399): uses no `ios` field and no `parts`, so
  only the pin changes. It gains the foreground replacement automatically.
- Both apps need a native rebuild (pod install) for the bump. Neither app
  is changed here.
Keeps the v11.0.2 tree as-is. The previous main (Vydia 7.x) history is retained
through this merge and on vydia-7x-legacy; both apps pin by SHA so neither is
affected.
@thevwick thevwick self-assigned this Oct 26, 2026
@thevwick

Copy link
Copy Markdown
Author

Superseded. The v11 line now lives in a proper fork of openspacelabs at site-mate/react-native-background-upload-osl, where upstream is a remote and their releases merge normally. Importing their history into this repo left two unrelated lineages sharing only an ancient ancestor, which is what made this PR need a history rewrite in the first place.

This repo keeps serving the SHAs both apps pin today (SMM c26be72, DPM b8ce380); nothing in production moves.

@thevwick thevwick closed this Oct 26, 2026
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.

7 participants