Skip to content

feat(main): stream downloads through a first-party service worker (DSPX-159) - #1098

Draft
dmihalcik-virtru wants to merge 4 commits into
mainfrom
DSPX-159
Draft

dmihalcik-virtru wants to merge 4 commits into
mainfrom
DSPX-159

Conversation

@dmihalcik-virtru

@dmihalcik-virtru dmihalcik-virtru commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

What

This replaces StreamSaver in the sample web-app with a first-party streaming download service worker (DSPX-159).

  • web-app/public/sw.js is rewritten. The page registers each download over a MessagePort. A hidden iframe then opens <scope>__download__/<id>/<name>, and the worker answers that request with the stream as an attachment. The worker asks for one chunk each time its stream pulls, so backpressure from the browser's download manager reaches back to the SDK.
  • web-app/src/downloadSink.ts is the page side, toFile(). It sends a 10 s keepalive while streaming, and errors and cancels propagate in both directions. If no worker answers within 5 s, it falls back to an in-memory Blob.
  • Save As uses the native showSaveFilePicker. It is disabled, with a tooltip, where the browser lacks it.
  • Removed: streamsaver, @types/streamsaver, and native-file-system-adapter.

Why

  • StreamSaver routes plaintext through a third-party page. It loads jimmywarting.github.io/StreamSaver.js/mitm.html plus a service worker on that origin. It is also unmaintained.
  • The app's existing sw.js could not have worked. Its origin check compared against 'http://localhost:65432/', and an origin never has a trailing slash. It also acknowledged chunks when it queued them, not when the stream pulled them, so nothing bounded its queue. native-file-system-adapter's Firefox fallback depended on it.

Findings

  • The service-worker path works in Chromium, Firefox and WebKit. The output is byte-exact in all three.
  • Memory stays flat in Chromium and in real Firefox. Chromium: 0.34 → 0.35 GiB on 4 GiB. Stock Firefox 158, run with no automation: about 640 MiB on an unthrottled 8 GiB.
  • The worker survives long downloads. A 342 s paced download completed on Chromium and Firefox, past Firefox's 5-minute worker limit.
  • DSPX-5084's runner crash was Playwright, not StreamSaver. Playwright's bundled Firefox (Juggler) keeps every response body in its parent process. The BiDi moz-firefox channel does the same through network.addDataCollector. Details are on DSPX-5084.
  • WebKit buffers when the source is far faster than the disk (7.9 GiB peak on 16 GiB unthrottled). It stays flat at realistic rates. It isn't known yet whether this is WebKit or Playwright's inspector; tracked in DSPX-5246.

The design and full measurements are in spec/DSPX-159.md.

Test changes

  • New tests/tests/sink.spec.ts. It needs no platform. It imports downloadSink.ts from the dev server and checks:
    • byte-exact output;
    • that an abort fails the download rather than truncating it;
    • a large download, sized with SINK_TEST_BYTES and paced with SINK_TEST_MIBPS, with browser memory sampled.
  • New tests/playwright.moz-firefox.config.ts runs the suite on stock Firefox over WebDriver BiDi. It is only usable for huge on machines with plenty of RAM; see above.
  • New downloadPath() in acts.ts. Stock Firefox ignores Playwright's download folder, so specs now find the file with this helper.
  • huge.spec.ts now runs on WebKit too. It still skips Playwright's bundled Firefox.

How to test

cd web-app && npm test                    # 69 tests pass locally
npm run dev                                # http://localhost:65432
cd tests && PLAYWRIGHT_TESTS_TO_RUN=sink npx playwright test
# a large file, slowed so the download outlives the service worker timeouts:
SINK_TEST_BYTES=12884901888 SINK_TEST_MIBPS=32 PLAYWRIGHT_TESTS_TO_RUN=sink npx playwright test --project chromium

By hand: in Firefox, encrypt a multi-GiB file with the Download sink, and watch about:processes. Memory should stay flat.

Not yet verified

  • roundtrip.spec.ts and huge.spec.ts against a platform. My local platform went down partway through testing. CI's roundtrip job should cover roundtrip, and large-tests.yaml (a manual run) covers huge on Chromium and WebKit.
  • Real Safari.

Risk

  • Sample app only; no SDK changes. The service worker now intercepts only __download__/ URLs under its scope. Every other fetch goes to the network untouched.
  • Updating the worker interrupts downloads in progress. skipWaiting() replaces the old worker right away, which kills any download it is streaming. That is acceptable for a sample app, and it is noted in the spec.
  • UI change: the Save As radio is disabled on browsers without the File System Access API (Firefox, Safari).

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
…PX-159)

Replace StreamSaver, which routed downloads through a service worker on
jimmywarting.github.io, with our own worker in web-app/public/sw.js. The
page sends output to the worker over a MessagePort, one chunk per pull, and
a hidden iframe opens the streamed attachment response. Backpressure from
the browser's download manager reaches back to the SDK, so memory stays flat
at any file size.

The existing sw.js could not have worked: its origin check never matched,
and it acknowledged chunks on enqueue, so nothing bounded its queue.

Save As now uses the native showSaveFilePicker and is disabled where the
browser lacks it, so native-file-system-adapter is gone too.

Tests:
- Add tests/sink.spec.ts, which exercises the sink with no platform and can
  run a large, paced download with SINK_TEST_BYTES and SINK_TEST_MIBPS.
- Add playwright.moz-firefox.config.ts to run the suite on stock Firefox
  over WebDriver BiDi. Stock Firefox ignores Playwright's download folder,
  so specs now find files with downloadPath() from acts.ts.
- huge.spec.ts now runs on WebKit too. It still skips Playwright's bundled
  Firefox, which keeps every response body in memory (DSPX-5084).

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Describe the design as built, consolidate the prototype findings, list
follow-ups (DSPX-5084, DSPX-5246), tick the verified acceptance criteria,
and record PR #1098.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@github-actions

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
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