Skip to content

Rebuild file transfer as a self-contained core with resume - #171

Merged
ctoth merged 3 commits into
masterfrom
refactor/file-transfer-core
Oct 11, 2026
Merged

ctoth merged 3 commits into
masterfrom
refactor/file-transfer-core

Conversation

@ctoth

@ctoth ctoth commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces FileTransferManager, WebRTCService and FileTransferStore with a new core in src/fileTransfer/. The engine knows nothing of GMCP, WebRTC or IndexedDB; those sit behind a signalling, a link and a storage interface, and createFileTransfers.ts assembles them for the browser.

What changes for users

  • Several transfers at once. Each transfer has its own connection, and the panel shows a row with its own progress and Cancel button for each.
  • Resume. After a dropped link or a page reload, only the missing pieces are sent, and the receiver is not prompted again.
  • Truthful completion. The sender reports success once the receiver has verified and saved the file.
  • The pending-offer row shows the file size.
  • Offer notifications name the sender by player name, and are spoken when desktop notifications are not granted.

How it works

  • Content id: SHA-256 over the SHA-256 digests of consecutive 16 KiB pieces, so a file is hashed and verified one piece at a time.
  • Bounded memory: received pieces go straight to storage, and the sender keeps at most 64 pieces (1 MiB) outstanding until the receiver reports them stored.
  • One connection per transfer: ICE candidates carry the transfer id inside the GMCP candidate string so they can be routed.
  • Resume: the sender re-offers the same id marked as a resume; the receiver answers without prompting and says which pieces it holds.
  • Closing: the side that sends the last frame keeps the link open until the peer closes it.

Trust

  • No connection exists on the receiving side before the user accepts.
  • Name, size and content are checked against the accepted offer; the name shown and saved comes from the offer, with path components, control characters and bidi/format characters removed.
  • Undecided offers are capped per peer and expire; re-offers and confirmations are bounded; held ICE candidates are capped.
  • History keeps the last 50 finished transfers in the order they finished; an offer nobody accepted is not kept.

Client and UI

  • client.getFileTransfers() replaces getFileTransferManager(); client.onFileTransferOffer() reports each new incoming offer, and is what notifications and the sidebar now use.
  • RequestResend is removed from the Client.FileTransfer package.
  • The panel renders from engine snapshots (useTransfers, useTransferHistory).

Accessibility

  • History lines are spoken through the client's announcer, so they are heard whichever sidebar tab is showing.
  • Focus moves to the panel heading when the row that held it disappears.
  • The recipient combobox always has its listbox, marks the highlighted option as selected, and announces when nobody matches.
  • Rows are named groups; Cancel buttons say what they cancel; the progress bar says when it is connecting or verifying.
  • The file input is labelled; the open panel no longer clips at a fixed height.

Compatibility

The data-channel format, candidate tagging and content id are new, so this build and older builds cannot exchange files with each other.

Docs

docs/GMCP/packages/client-file-transfer.md describes the content id, the tagged candidate, the resume marker and the data-channel protocol.

Verification

  • npm run typecheck: clean.
  • npx vitest run: 135 files, 2,061 tests pass.
  • npm run build: the transfer code is its own lazily loaded chunk, createFileTransfers-*.js, 28.7 kB.

ctoth added 3 commits October 10, 2026 18:17
Replace FileTransferManager, WebRTCService and FileTransferStore with
src/fileTransfer/: an engine that knows nothing of GMCP, WebRTC or
IndexedDB, which sit behind a signalling, a link and a storage interface.

- One connection per transfer, so several run at once; the panel shows a
  row with its own progress and Cancel for each.
- Bounded memory: the content id is hashed piece by piece, received pieces
  go straight to storage, and the sender keeps at most 64 pieces out until
  the receiver reports them stored.
- Resume after a lost link or a reload sends only the missing pieces and
  does not prompt again.
- The sender reports success only once the receiver has verified and
  saved the file.
- No connection exists before the user accepts; name, size and content are
  checked against the accepted offer. Offers are capped per peer and
  expire, re-offers are bounded, names lose control and bidi characters.
- The data-channel format, candidate tagging and content id are new, so
  this build cannot exchange files with older ones. RequestResend is no
  longer sent.

Accessibility of the panel: history lines are spoken through the client's
announcer instead of a live region that is silent in a hidden tab; a new
offer names its sender and is spoken when no desktop notification shows;
focus goes to the panel heading when the row that held it disappears; the
recipient combobox always has its listbox, marks the highlighted option
and says when nobody matches; rows are named groups and the progress bar
says when it is connecting or verifying.
@ctoth
ctoth merged commit 09f4002 into master Oct 11, 2026
2 checks passed
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