Events

Prev Next

Events

Transport

All events flow through the global Signal bus on topic 'upload':

// UploadEvents.ts
import { Signal } from '@/lib/internal/Signal';

export const UPLOAD_SIGNAL_TOPIC = 'upload' as const;

export type UploadSignalPayload = {
  event: UploadEvent;
};

export function emitUploadEvent(event: UploadEvent): void {
  Signal.publish<UploadSignalPayload>(UPLOAD_SIGNAL_TOPIC, { event });
}

Subscribing:

import { Signal } from '@/lib/internal/Signal';
import type { UploadSignalPayload } from '@/lib/internal/UploadEngine/UploadEvents';

const unsub = Signal.subscribe<UploadSignalPayload>('upload', ({ event }) => {
  switch (event.type) {
    case 'UPLOAD_COMPLETED':
      console.log('File at:', event.fileUrl);
      break;
    case 'UPLOAD_NEEDS_ATTENTION':
      console.warn('Upload stuck:', event.reason);
      break;
  }
});

// cleanup
unsub();

Event Flow Diagram

Image

Event emission path:

  1. UploadSession calls its emit callback (provided by UploadEngineImpl)
  2. UploadEngineImpl.handleEvent() receives the event
  3. For UPLOAD_ABORTED: removes persistence + deletes session first
  4. Calls emitUploadEvent(event)Signal.publish('upload', { event })

Event Union (complete)

type UploadEvent =
  | { type: 'UPLOAD_STARTED'; uploadId: UploadSessionId }
  | { type: 'UPLOAD_PROGRESS'; uploadId: UploadSessionId; progress: number }
  | { type: 'UPLOAD_PAUSED'; uploadId: UploadSessionId }
  | { type: 'UPLOAD_RESUMED'; uploadId: UploadSessionId }
  | { type: 'UPLOAD_COMPLETED'; uploadId: UploadSessionId; fileUrl: string }
  | { type: 'UPLOAD_NEEDS_ATTENTION'; uploadId: UploadSessionId; reason: string }
  | { type: 'UPLOAD_ABORTED'; uploadId: UploadSessionId };

Event Definitions

UPLOAD_STARTED

Field Type Description
type 'UPLOAD_STARTED' Discriminant
uploadId UploadSessionId Stable session identity

Emitted by: session.startInitialization() (idleinitializing)

Guarantees:

  • Upload identity is stable from this point
  • Persistence metadata has been set (if tenant cookies available)
  • No bytes have necessarily been transferred yet

UPLOAD_PROGRESS

Field Type Description
type 'UPLOAD_PROGRESS' Discriminant
uploadId UploadSessionId Session identity
progress number 0–100, monotonic

Emitted by: session.updateProgress(progress) (inside UploadWorker.uploadChunk())

How progress is calculated:

const progress = Math.round((completedPartNumbers.size / totalChunks) * 100);

Guarantees:

  • Progress is monotonic — updateProgress() rejects non-increasing values
  • Progress is clamped to 0–100
  • Emitted per chunk completion (not per byte)

Consumers must tolerate:

  • Gaps between progress values (e.g., 0 → 20 → 40 → 100)
  • Variable frequency based on chunk size and network speed

UPLOAD_PAUSED

Field Type Description
type 'UPLOAD_PAUSED' Discriminant
uploadId UploadSessionId Session identity

Emitted by: session.pause() (uploadingpaused)

Guarantees:

  • Worker's AbortController has been aborted (in-flight requests cancelled)
  • State has been persisted to sessionStorage
  • Queue slot has been freed

UPLOAD_RESUMED

Field Type Description
type 'UPLOAD_RESUMED' Discriminant
uploadId UploadSessionId Session identity

Emitted by: UploadEngineImpl.resume() — emitted directly via emitUploadEvent(), not through the session

:::info Design note
resume() does not call session.resume(). The event is emitted immediately for UI feedback, while the actual paused → uploading transition happens later when the worker's initialize() runs.
:::

UPLOAD_COMPLETED

Field Type Description
type 'UPLOAD_COMPLETED' Discriminant
uploadId UploadSessionId Session identity
fileUrl string Durable URL: prefixUrl + key

Emitted by: session.complete(fileUrl) (committingcompleted)

Guarantees:

  • All chunks have been uploaded and verified by backend
  • completeMultipartUpload() has returned successfully
  • File is durably available at fileUrl
  • State is terminal — no further transitions

This is the authoritative signal for post-upload domain actions.

UPLOAD_NEEDS_ATTENTION

Field Type Description
type 'UPLOAD_NEEDS_ATTENTION' Discriminant
uploadId UploadSessionId Session identity
reason string Human-readable failure description

Emitted by: session.fail(stage, message) (any state → needs-attention)

Triggers:

  • Chunk upload retries exhausted (stage: 'upload')
  • Presigned URL fetch failed with non-retryable error (stage: 'upload')
  • completeMultipartUpload() retries exhausted (stage: 'completion')
  • Completion handler failed after retry (stage: 'completion')

Guarantees:

  • Automatic retries have been exhausted or the error is non-retryable
  • State has been persisted — user can resume or abort later
  • The error field on the snapshot contains { stage, message }

UPLOAD_ABORTED

Field Type Description
type 'UPLOAD_ABORTED' Discriminant
uploadId UploadSessionId Session identity

Emitted by: session.abort() (any non-terminal state → aborted)

Side effects in handleEvent():

  • Removes persistence data for this session
  • Deletes session from sessions Map
  • Then publishes to Signal bus

Guarantees:

  • Client-side work has stopped immediately
  • State is terminal — this event is never followed by completion
  • Session is removed from list() results

Event Timing Matrix

Event Emitted During Session Method State Transition
UPLOAD_STARTED start() startInitialization() idle → initializing
UPLOAD_PROGRESS Chunk uploads updateProgress() None (same state)
UPLOAD_PAUSED pause() pause() uploading → paused
UPLOAD_RESUMED resume() (direct emit) None (deferred)
UPLOAD_COMPLETED Worker completion complete(fileUrl) committing → completed
UPLOAD_NEEDS_ATTENTION Error handling fail(stage, msg) * → needs-attention
UPLOAD_ABORTED abort() abort() * → aborted

Events vs Snapshots vs Callbacks

Mechanism Durability Use For Reliability
Events (Signal) Broadcast once Domain reactions, analytics Authoritative
Snapshots (get(), list()) Queryable UI rendering Authoritative
Callbacks (onComplete, onError) Tied to UI lifetime Convenience notifications Best-effort

:::danger
Domain correctness must be driven by events, not callbacks. Callbacks may be lost if the originating UI unmounts.
:::

Delivery Guarantees

The engine guarantees:

  • Events for a single upload are emitted in logical lifecycle order
  • No event is emitted twice for the same state transition
  • UPLOAD_ABORTED is never followed by UPLOAD_COMPLETED

Consumers must tolerate:

  • Missing intermediate UPLOAD_PROGRESS events
  • Late subscriber registration (events are not replayed)
  • App reloads (subscribe again after rehydration)