Skip to main content

Design Decisions

WIP

Architecture decisions and design rationale behind @gentleduck/upload.

Architecture decisions and rationale behind the upload engine.

Typed Results End-to-End

The engine carries a typed completion result all the way through:

  • UploadApi.complete returns a typed result.
  • Internal events include the result.
  • upload.completed emits it.
  • Items in the completed phase keep it.

unknown never appears on the completion path, so UIs use results without casts.

Dedupe Without Fake Intents

When findByChecksum matches, the engine completes the item via a dedicated internal event (dedupe.ok) — no synthetic intent. State stays honest and no strategy assumptions sneak in.

Defaults At Construction

createUploadStore resolves config defaults and supplies a default transport (createXHRTransport) when none is given. Pass only the fields you care about — the runtime is always fully specified.

Core Does Not Depend On React

The reducer and runtime are React-free. React adapters live in src/react and only read the store interface. No import cycles, and the core works in Node.js, Vue, or vanilla JS.

Immutable State Updates

All state changes go through the reducer. Direct map mutations were removed to fix useSyncExternalStore snapshot identity issues. Internal events drive every change:

  • Files are added via files.added.
  • Fingerprints update via fingerprint.updated.
  • Cleanup returns a new state instead of mutating.

Single Event Emission Layer

State-derived public events emit from one place in the store runtime, not inside individual handlers. No duplicates, consistent semantics across the lifecycle.

Strategy Decoupling

Core defines strategy interfaces in core/contracts/strategy.types.ts. The strategies package implements the registry helper and the protocol logic. Core never imports strategy implementations directly.