Migrations

Prev Next

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

Image

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:

  1. STATE_TRANSITIONS map in UploadStateMachine.ts
  2. UploadSession methods that call transition()
  3. UploadWorker catch blocks if new error paths
  4. Test file UploadStateMachine.test.ts
  5. 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.