Migrations
This document defines how the Upload Engine may evolve over time without breaking correctness.
Migrations exist to preserve invariants while allowing the system to grow.
Any change described here must be deliberate, explicit, and reviewable.
Migration Philosophy
The engine prioritizes:
- backward compatibility
- resumability guarantees
- invariant preservation
Breaking uploads in progress is unacceptable.
Current Contracts That Constrain Migrations
Public Surface (from index.ts)
These are the types exposed to consumers. Removing or changing them is a breaking change:
export { UploadEngine } from './UploadEngine';
export type {
UploadContext, // Upload metadata — industry, category, etc.
UploadMeta, // Generic intent payload
StartUploadOptions, // Input to start()
UploadSessionSnapshot, // Read-only view of upload state
UploadEvent, // 7-variant event union
UploadCompletionHandler, // onComplete callback type
UploadSessionId, // Branded string ID
} from './UploadTypes';
Persistence Format (from UploadTypes.ts:210-244)
Persisted uploads must survive page reload. The current PersistedUploadState shape is:
type PersistedUploadState = {
key: string; // tenant-scoped: <siteId>:<site>:<timestamp>
tenant: { site: string; siteId: string };
createdAt: number;
uploadId: UploadSessionId;
fileMeta: { name: string; size: number; type: string };
context: UploadContext;
uploadServiceData?: { uploadId; key; bucket; prefixUrl; chunkSize; totalChunks };
completedParts: CompletedPart[];
progress: number;
state: 'paused' | 'uploading' | 'needs-attention';
chunkConfig?: { chunkSize: number; windowSize: number };
internalFlags?: Record<string, unknown>;
};
State Machine (from UploadStateMachine.ts)
The 9-state machine and its transitions are a hard contract:
idle → initializing → uploading → completing → completed
uploading → paused → resuming → uploading
uploading → aborting → aborted
uploading → failed
completing → completion_failed
Safe Migration Patterns
Adding New API Options
| Change | Allowed? | Requirements |
|---|---|---|
Add optional field to StartUploadOptions |
✅ | Default preserves current behavior |
| Add optional parameter to public methods | ✅ | Absence must not change semantics |
| Remove existing option | ❌ | Breaking change |
| Change meaning of existing option | ❌ | Breaking change |
Adding New UploadContext Fields
| Change | Allowed? | Requirements |
|---|---|---|
Add optional field to UploadContext |
✅ | Engine must treat as opaque |
Add new keys inside meta.payload |
✅ | Listeners must validate defensively |
Make engine branch on UploadContext contents |
❌ | Violates Invariant #12 |
Mutate context after start() |
❌ | Violates Invariant #11 |
Event Contract Evolution
| Change | Allowed? | Requirements |
|---|---|---|
| Add new event type to union | ✅ | Existing events retain semantics; listeners must tolerate unknown types |
| Add optional field to event payload | ✅ | Existing fields unchanged |
| Remove event payload field | ❌ | Requires major version bump |
| Change event semantics | ❌ | Requires major version bump + migration plan |
Snapshot Shape Changes
| Change | Allowed? | Requirements |
|---|---|---|
Add optional field to UploadSessionSnapshot |
✅ | UI must tolerate unknown fields |
| Remove field from snapshot | ❌ | Breaking change |
| Change state value semantics | ❌ | Breaking change |
Persistence Migrations
Persistence migrations are the most sensitive because they affect uploads that are already in progress across page reloads.
Safe Persistence Changes
| Change | Requirements |
|---|---|
Add optional field to PersistedUploadState |
Existing persisted uploads must rehydrate safely with undefined |
Add internalFlags key |
Already supported — internalFlags?: Record<string, unknown> |
| Introduce persistence version key | Requires migration function in rehydrateFromPersistence() |
Forbidden Persistence Changes
| Change | Why |
|---|---|
| Invalidate existing persisted uploads | Would lose user's in-progress uploads on deploy |
| Require re-upload of completed chunks | Violates resumability guarantee (Invariant #8) |
| Change key format without migration | Would orphan existing entries in sessionStorage |
| Change state union without migration | Would make fromPersistedStateAdapter() return invalid state |
Migration Strategy

State Machine Migrations
Adding New States
| Change | Allowed? | Requirements |
|---|---|---|
| Add non-terminal state | ✅ | Existing transitions remain valid |
| Add terminal state | ✅ | Must add to isTerminalState() |
| Change existing terminal → non-terminal | ❌ | Violates Invariant #15 (abort is terminal) |
| Remove existing transition | ❌ | Could break in-progress uploads |
Adding New Transitions
Must update:
STATE_TRANSITIONSmap inUploadStateMachine.tsUploadSessionmethods that calltransition()UploadWorkercatch blocks if new error paths- Test file
UploadStateMachine.test.ts - This document's state machine diagram
Adapter Layer Migrations
The persistence.adapter.ts file maps between internal and public types. Changes here must update four adapters simultaneously:
| Adapter | Direction | Must Update When |
|---|---|---|
toPersistedStateAdapter |
Internal → Persisted | Persistence format changes |
fromPersistedStateAdapter |
Persisted → Internal | Persistence format changes |
toPublicSnapshotAdapter |
Internal → Public | Snapshot shape changes |
mapInternalStateAdapter |
Internal state → Public state | State enum changes |
:::warning
Changing one adapter without the others will cause data loss or type errors at runtime.
:::
Migration Testing Requirements
Any migration must include:
| Test Category | What to Verify |
|---|---|
| Backward compatibility | Old format rehydrates correctly |
| Forward compatibility | New code handles old persisted data |
| Roundtrip | Save → reload → resume produces identical behavior |
| Invariant preservation | All 20 invariants still hold after migration |
| Adapter symmetry | fromPersisted(toPersisted(state)) ≈ state |
If a migration cannot be tested safely, it must not ship.