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

Event emission path:
UploadSessioncalls its emit callback (provided byUploadEngineImpl)UploadEngineImpl.handleEvent()receives the event- For
UPLOAD_ABORTED: removes persistence + deletes session first - 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() (idle → initializing)
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() (uploading → paused)
Guarantees:
- Worker's
AbortControllerhas 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 callsession.resume(). The event is emitted immediately for UI feedback, while the actualpaused → uploadingtransition happens later when the worker'sinitialize()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) (committing → completed)
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
errorfield 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
sessionsMap - 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_ABORTEDis never followed byUPLOAD_COMPLETED
Consumers must tolerate:
- Missing intermediate
UPLOAD_PROGRESSevents - Late subscriber registration (events are not replayed)
- App reloads (subscribe again after rehydration)