Skip to content

Carry staged files to the hub over the tab-to-worker content lane - #30

Merged
LucaCappelletti94 merged 7 commits into
mainfrom
feat/r69-tab-worker-protocol
Sep 17, 2026
Merged

LucaCappelletti94 merged 7 commits into
mainfrom
feat/r69-tab-worker-protocol

Conversation

@LucaCappelletti94

@LucaCappelletti94 LucaCappelletti94 commented Sep 17, 2026

Copy link
Copy Markdown
Owner

A browser tab can now hand a file to the worker and have it reach the server together with the row that names it. The lane rides the existing tab-to-worker message transport as a new internal frame tag carrying a JSON control message and the bytes as an attached Blob, so the server protocol and every non-content path stay untouched.

Ordering is what makes pairing safe without a handshake per file. The tab posts the stage before committing the row, the port is FIFO, and the hub's shovel polls the internal lane first, so the hub always holds the blob by the time a mutation arrives. Pairing ignores table and column names entirely, scanning only the 32-byte blob values the changeset writes, and the hub re-hashes the staged bytes rather than trusting either declaration, so a row naming the wrong identity is refused and nothing uploads. The manifest, the outbox entry and the application row commit in one transaction through the client's bookkeeping-aware helper, and the commit tells the upload driver directly because a mutation that produces no upstream event would otherwise leave the entry asleep. Resolution answers wherever it arrives, from the worker's own store while a file is unsent, from a server ticket once it is uploaded, and as unavailable after both bounds.

A refused ticket retries on the content backoff, which is why the test fixture sleeps for real. A sleeper that resolves instantly turns that backoff into a busy loop that starves the worker, and the fixture caught a fake server minting a grant URL the write-endpoint split refuses, so both shapes are pinned in tests now.

Summary by Sourcery

Carry staged browser files over the worker content lane and commit, upload, and resolve them consistently with the mutations that reference them.

New Features:

  • Enable browser tabs to stage files through the worker and commit them atomically with rows that reference their content.
  • Add tab-to-worker content resolution for local bytes, server download tickets, and unavailable content.

Bug Fixes:

  • Reject staged mutations when the uploaded bytes do not match the identity declared by the row.
  • Prevent refused content tickets from causing busy-loop retries and ensure generated grants use valid upload endpoints.

Enhancements:

  • Add an internal browser transport lane for JSON content controls with attached Blob payloads while leaving the server protocol unchanged.
  • Pair staged content with mutations using ordered delivery and changeset identities, with bounded staging and resolution waits.
  • Expose client-side staging, transactional bookkeeping, and connection-aware content resolution APIs.

Build:

  • Include the file client crate in CI package checks and add web dependencies required for browser content handling.

Tests:

  • Add unit, integration, and browser relay coverage for atomic staging, identity mismatch rejection, local and remote resolution, unavailable content, ordering, and non-blocking ticket waits.

@sourcery-ai sourcery-ai Bot 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.

Sorry @LucaCappelletti94, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 5 days and 5 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: LucaCappelletti94/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 4daf044e-82b0-47c7-b486-9108c895262b


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-17T04:57:23.257771Z 41cdc0c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai

sourcery-ai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds a private, FIFO tab-to-worker content lane carrying JSON plus Blob attachments, stages and hashes files before mutations, atomically commits content bookkeeping with the naming row, and resolves bytes locally or through server tickets without changing the server sync protocol.

Sequence diagram for tab-to-worker staged content commit

sequenceDiagram
    actor Tab
    participant Lane as InternalContentLane
    participant Hub as RelayHub
    participant Archive as ContentArchive
    participant Replica as WorkerReplica
    participant Upload as ContentUpload

    Tab->>Lane: post_internal(ContentFrame::Stage, Blob)
    Lane->>Hub: FIFO Stage frame with Blob
    Tab->>Replica: with_conn(row, file_id)
    Replica->>Hub: mutation changeset
    Hub->>Hub: changeset_blob_values(changeset)
    Hub->>Archive: chunk_file(BlobSource, mime)
    Hub->>Archive: commit_staged(manifest, row)
    Archive->>Replica: put_manifest and enqueue
    Archive->>Replica: apply_changeset and record_tab_watermark
    Hub->>Upload: wake_content()
Loading

Sequence diagram for content resolution

sequenceDiagram
    actor Tab
    participant Lane as InternalContentLane
    participant Hub as RelayHub
    participant Archive as ContentArchive
    participant Server

    Tab->>Lane: post_internal(ContentFrame::Resolve, file_id)
    Lane->>Hub: Resolve request
    Hub->>Archive: resolve_connection(file_id)
    alt local content available
        Archive-->>Hub: Resolved::Local(bytes)
        Hub-->>Lane: ResolveReply with Blob
    else server ticket available
        Archive->>Server: request_connection_or(Read)
        Server-->>Archive: read URL
        Archive-->>Hub: Resolved::Remote(url)
        Hub-->>Lane: ResolveReply with URL
    else unavailable or timeout
        Archive-->>Hub: Resolved::Unavailable
        Hub-->>Lane: ResolveReply unavailable
    end
    Lane-->>Tab: TabResolved
Loading

Flow diagram for staged content pairing and refusal

flowchart TD
    A[Tab hashes Blob to FileId]
    B["post_internal(Stage, Blob)"]
    C[Commit row mutation]
    D[Hub pairs staged Blob with 32-byte changeset value]
    E[Hub re-hashes and chunks Blob]
    F{Identity matches?}
    G[Atomic commit: manifest, outbox, row, watermark]
    H[Wake content upload]
    I[Reject mutation and roll back bookkeeping]

    A --> B --> C --> D --> E --> F
    F -->|yes| G --> H
    F -->|no| I
Loading

File-Level Changes

Change Details Files
Adds a browser tab-to-worker internal content lane that transports JSON control frames with optional Blob attachments.
  • Introduces a private internal frame tag and attached-message encoding/decoding.
  • Adds typed stage, resolve, and resolve-reply wire messages.
  • Exposes lane handles and inbound inboxes without altering codec/server protocol paths.
  • Connects the lane to tab intake and hub attachment/reply plumbing.
crates/connetto-web/src/content_wire.rs
crates/connetto-web/src/frames.rs
crates/connetto-web/src/content.rs
crates/connetto-web/src/lib.rs
crates/connetto-web/src/workers/intake.rs
crates/connetto-web/Cargo.toml
Implements ordered staging and atomic pairing of file content with the naming mutation.
  • Chunks staged Blob bytes into the encrypted store and computes the worker-side identity.
  • Pairs staged entries with 32-byte Blob values in inserted or updated changeset values, ignoring schema names and old images.
  • Commits the manifest, outbox entry, replica mutation, and tab watermark transactionally.
  • Re-hashes bytes and rejects identity mismatches or apply/bookkeeping failures without uploading.
  • Bounds staged-content retention by age and per-tab capacity, and explicitly wakes the upload driver after successful commits.
crates/connetto-file-client/src/error.rs
crates/connetto-file-client/src/lib.rs
crates/connetto-file-client/src/worker.rs
crates/connetto-web/src/relay.rs
Adds end-to-end content resolution across local worker storage, server tickets, and unavailable states.
  • Resolves unsent or retained local files from the encrypted chunk store.
  • Requests server read tickets when local resolution cannot answer and maps failures, cancellation, offline state, and timeout to Unavailable.
  • Returns local bytes as attached Blobs or remote URLs over the internal lane.
  • Preserves worker events observed during ticket requests for re-application.
crates/connetto-file-client/src/worker.rs
crates/connetto-web/src/content.rs
crates/connetto-web/src/relay.rs
crates/connetto-web/src/workers/helpers.rs
crates/connetto-web/src/workers.rs
Expands coverage for transactional staging, protocol ordering, resolution behavior, and retry timing.
  • Tests successful and refused atomic staged commits and local resolution in the file client.
  • Tests changeset identity extraction for inserts, updates, deletes, malformed data, and non-32-byte values.
  • Exercises real MessageChannel staging, mismatch refusal, unpaired staging, local bytes, remote tickets, and unavailable resolution.
  • Uses a real bounded sleeper to validate refused-ticket backoff and pins compatible grant URL behavior.
crates/connetto-file-client/src/worker.rs
crates/connetto-web/src/relay.rs
crates/connetto-web/tests/content_relay.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41cdc0ce08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3376 to +3378
if tab.staged.len() >= MAX_STAGED_CONTENT {
tab.staged.pop_front();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve staged blobs instead of silently evicting them

When a tab has more than eight stages outstanding—for example, concurrent TabContent::stage calls while connection serialization delays their mutations—this branch silently discards the oldest blob. Its later mutation is then handled as an ordinary mutation because take_staged finds nothing, so the row is committed and forwarded without a manifest or upload entry, leaving content permanently unavailable despite staging appearing successful. Apply backpressure or reject the incoming stage rather than evicting an unpaired entry.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c5041ab. A full staging buffer now refuses the incoming file and logs a warning instead of evicting the oldest, so every accepted stage survives to its pairing.

Comment thread crates/connetto-web/src/relay.rs Outdated
Comment on lines +3393 to +3399
let (answer, observed) = content
.resolve_connection(
worker,
FileId::from_bytes(file_id),
sleep_ms(RESOLVE_WAIT_MS),
)
.await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep ticket waits out of the single hub event handler

When the content server is slow or does not answer a read ticket, this await occupies the hub's sole event-handling task until the 15-second timeout. During that interval no queued mutations, pings, stages, or other tab requests are serviced, and an in-progress upload being driven by the same handler also stops being polled; repeated resolves for unknown IDs can therefore keep every attached tab stalled. Drive the ticket wait as resumable state while continuing to service the hub event queue.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c5041ab. The hub no longer parks the event loop on a ticket answer. ContentArchive::start_resolve_connection sends the request and hands back a PendingConnectionResolve, the hub queues it with a deadline, settles it in handle_worker_event through the routed route_connection_event, and sweeps expired waits on its cycle, so mutations and other resolves keep flowing while one ticket is outstanding.

Comment on lines +359 to +360
let buffer = JsFuture::from(blob.array_buffer()).await?;
Ok(Uint8Array::new(&buffer).to_vec())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hash staged blobs without copying the whole file

For a large blob, especially the supported Video class, array_buffer() materializes the entire file in JavaScript memory and to_vec() immediately copies it again into Wasm linear memory solely to compute the identity. This produces a peak allocation of roughly twice the file size in addition to the blob backing store and can terminate the tab for otherwise valid large uploads; hash streamed blob slices instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c5041ab. stage now hashes through 4 MiB Blob.slice windows with FileIdHasher::update, so peak memory is one window and the digest is unchanged.

@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.47205% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.79%. Comparing base (2743777) to head (da11e09).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
crates/connetto-file-client/src/worker.rs 84.36% 28 Missing and 15 partials ⚠️
crates/connetto-file-client/src/ticket.rs 90.90% 0 Missing and 4 partials ⚠️
crates/connetto-file-client/src/error.rs 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #30      +/-   ##
==========================================
+ Coverage   81.24%   82.79%   +1.55%     
==========================================
  Files         115      114       -1     
  Lines       25806    25247     -559     
  Branches    25806    25247     -559     
==========================================
- Hits        20965    20903      -62     
+ Misses       3630     3132     -498     
- Partials     1211     1212       +1     
Flag Coverage Δ
client 64.16% <ø> (-0.05%) ⬇️
rest 55.86% <84.47%> (+0.78%) ⬆️
server 49.99% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Resolves now wait as hub state: the ticket request goes out, the cycle
keeps serving tabs and mutations, and the answer arrives through the
normal event path or an expiry sweep. Staged blobs hash in 4 MiB
windows instead of whole-file copies, and a full staging buffer refuses
the incoming file instead of evicting one a mutation may still pair
with.
@LucaCappelletti94
LucaCappelletti94 merged commit 3f28765 into main Sep 17, 2026
52 checks passed
@sonarqubecloud

Copy link
Copy Markdown

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