Data Model

Prev Next

# Data Model

Type Boundary Diagram

Screenshot 2026-08-26 at 1.28.59 PM.png

Public Types

UploadSessionId

type UploadSessionId = string;

Format: upl_<timestamp>_<random6> (e.g., upl_1711100400000_k3f9x2).

Generated by UploadEngineImpl.generateUploadId(). Stable and never reused.

UploadContext

Describes where and why an upload was initiated. Write-once, read-only for the entire lifecycle.

type UploadContext = {
  source: 'account-avatar' | 'content-create' | 'content-details' | 'brand-settings' | string;

  contentType:
    | 'image'
    | 'video'
    | 'audio'
    | 'document'
    | 'closedCaptions'
    | 'brand'
    | 'brand-video'
    | 'androidKeystore'
    | 'firetvKeystore'
    | 'androidLauncherXML';

  entityId?: string; // contentId, brandId, userId, etc.
  dropZoneKey?: string; // originating drop zone (e.g. "_16x9Images")
  meta?: UploadMeta; // immutable intent metadata
};
Field Required Purpose
source Yes Logical origin — used for analytics, completion routing, UI projections
contentType Yes Backend-recognized content type passed to /init and /complete endpoints
entityId No Associated domain entity identifier
dropZoneKey No UI surface identifier (drop zone name)
meta No Immutable intent metadata for post-upload domain actions

The engine stores this object but never interprets it.

UploadMeta

type UploadMeta = Readonly<{
  intent: string; // e.g. "CREATE_VIDEO", "UPLOAD_BRAND_IMAGE"
  payload: Record<string, unknown>; // arbitrary serializable domain data
  owner?: UploadOwner; // content-level routing for UploadListener
}>;

type UploadOwner = {
  contentType: ContentTypeEnum | 'video'; // 'video' is special — backend doesn't accept 'vod'
  contentId: string;
};
  • Written once at start() — immutable thereafter
  • Survives UI unmounts, route changes, and page reloads (via persistence)
  • UploadListener reads intent + owner to route post-upload domain actions

UploadSessionSnapshot

The only data shape UI layers may consume. Produced by toPublicSnapshotAdapter().

type UploadSessionSnapshot = {
  uploadId: UploadSessionId;
  fileName: string;
  fileSize: number;
  state: 'uploading' | 'paused' | 'completed' | 'needs-attention' | 'aborted';
  progress: number; // 0–100, monotonic
  context: UploadContext;
  fileUrl?: string; // present only when state === 'completed'
  error?: {
    stage: 'upload' | 'completion';
    message: string;
  }; // present only when state === 'needs-attention'
};

UploadEvent

Discriminated union of 7 event types. See events.md for full contract.

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 };

UploadCompletionHandler<T>

type UploadCompletionHandler<T = unknown> = (args: {
  fileUrl: string;
  file: File;
  context: UploadContext;
}) => Promise<T>;

Best-effort. Failure here does not invalidate the upload.

UploadDomainProcessedEvent<DomainShape>

Emitted after domain actions complete (outside the engine, by UploadListener):

type UploadDomainProcessedEvent<DomainShape> = {
  owner: UploadOwner;
  uploadId: UploadSessionId;
  dropZoneKey?: string;
  entity: DomainShape;
};

Internal Types

:::warning
These types are not exported and may change without notice. UI/feature modules must never depend on them.
:::

InternalUploadState

The 10-state enum governing the state machine (see lifecycle.md):

type InternalUploadState =
  | 'idle'
  | 'initializing'
  | 'uploading'
  | 'paused'
  | 'retrying'
  | 'finalizing'
  | 'committing'
  | 'completed'
  | 'needs-attention'
  | 'aborted';

Internal → Public mapping:

Internal States Public State
idle, initializing, uploading, retrying, finalizing, committing 'uploading'
paused 'paused'
completed 'completed'
needs-attention 'needs-attention'
aborted 'aborted'

InternalUploadSnapshot

Full internal state held by UploadSession:

type InternalUploadSnapshot = {
  uploadId: UploadSessionId;
  file: File;
  state: InternalUploadState;
  progress: number;
  context: UploadContext;
  uploadServiceData?: {
    uploadId: string; // multipart upload ID from backend
    key: string; // S3 object key
    bucket: string; // S3 bucket
    prefixUrl: string; // URL prefix for final file URL
    chunkSize: number; // server-authoritative chunk size
    totalChunks: number; // ceil(fileSize / chunkSize)
  };
  completedParts: CompletedPart[];
  fileUrl?: string;
  error?: { stage: 'upload' | 'completion'; message: string };
  persistence?: { key: string; createdAt: number };
  chunkConfig?: { chunkSize: number; windowSize: number };
  internalFlags?: { enableParallelChunkProcessing?: boolean };
};

CompletedPart

type CompletedPart = {
  PartNumber: number;
  ETag: string;
};

Maps directly to AWS S3 multipart completion format. Sorted by PartNumber before sending to /complete.

PersistedUploadState

Serializable, tenant-scoped state saved to sessionStorage:

type PersistedUploadState = {
  key: string; // "<siteId>:<site>:<epoch>"
  tenant: { site: string; siteId: string };
  createdAt: number;
  uploadId: UploadSessionId;
  fileMeta: { name: string; size: number; type: string };
  context: UploadContext;
  uploadServiceData?: {
    /* same as InternalUploadSnapshot */
  };
  completedParts: CompletedPart[];
  progress: number;
  state: 'paused' | 'uploading' | 'needs-attention';
  chunkConfig?: { chunkSize: number; windowSize: number };
  internalFlags?: Record<string, unknown>;
};

:::info Persistence rules

  • Only paused and needs-attention states are rehydrated on app reload
  • File object is not persisted (not serializable) — rehydrated uploads create a placeholder
  • Persistence key format: <siteId>:<site>:<epoch> (e.g., acme123:ACME:1768765855488)
    :::

Data Adapters

The persistence.adapter.ts file provides four adapter functions bridging internal ↔ persisted ↔ public:

Adapter From To
toPersistedStateAdapter() InternalUploadSnapshot PersistedUploadState | null
fromPersistedStateAdapter() PersistedUploadState UploadSession
toPublicSnapshotAdapter() InternalUploadSnapshot UploadSessionSnapshot
mapInternalStateAdapter() InternalUploadState Public state string

Image

Events vs Snapshots

Events Snapshots
What What happened Current state
When Emitted once per transition Queried on demand
Shape UploadEvent (discriminated union) UploadSessionSnapshot
Use for Reactions (domain actions) Observation (UI rendering)
Replay No — do not reconstruct state from events Yes — always reflects latest