Store Options
WIPEverything you pass to createUploadStore — config, plugins, hooks, fingerprinting, and validation.
Overview
createUploadStore is the single entry point for building a store. It normalizes config,
installs the default transport, wires plugins and hooks, optionally hydrates from persistence,
and returns a Store.UploadStore<M, C, P, R>.
import { createUploadStore } from '@gentleduck/upload/core'
export const store = createUploadStore<Intents, Cursors, Purpose, Result>({
api,
strategies,
config: {
maxConcurrentUploads: 3,
maxAttempts: 3,
progressThrottleMs: 100,
autoStart: ['avatar'],
maxItems: 100,
validation: {
avatar: { maxSizeBytes: 5 * 1024 * 1024, allowedTypes: ['image/*'] },
},
},
})
Store options
Store.Options<M, C, P, R> accepts:
| Option | Required | Description |
|---|---|---|
api | Yes | Backend contract — Contracts.Api.Me<M, P, R> |
strategies | Yes | Strategy registry with your strategies registered |
config | No | Engine config (defaults applied by resolveUploadConfig) |
transport | No | HTTP transport. Defaults to createXHRTransport() |
persistence | No | Adapter + options for crash-safe resume |
plugins | No | Array of Engine.Plugin modules |
hooks | No | Engine.Hooks for low-level observation |
fingerprint | No | Custom, synchronous (file) => Contracts.FingerprintFile |
validateFile | No | Extra validator run after built-in rules |
errorNormalizer | No | Map raw thrown values into Contracts.Errors.Error |
initialState | No | Pre-hydrated Engine.State (e.g. from persistence) |
Config
Everything under config is optional. resolveUploadConfig() fills the rest at construction
time, so the runtime always has a fully-specified Engine.Config.
| Field | Type | Default | Meaning |
|---|---|---|---|
maxConcurrentUploads | number | 3 | Files transferring at once (queued items wait) |
progressThrottleMs | number | 100 | Minimum gap between progress events |
maxAttempts | number | 3 | Hard cap on attempts per phase |
maxItems | number | 100 | Items kept in state before oldest terminal ones evict |
autoStart | readonly P[] | (purpose: P) => boolean | undefined | Auto-queue matching purposes after intent |
validation | Partial<Record<P, Contracts.ValidationRules>> | {} | Per-purpose rules |
retryPolicy | (ctx) => Engine.RetryDecision | undefined | Override default backoff |
completedItemTTL | number | undefined | Evict completed items after N ms |
strictMimeMatch | boolean | false | Sniff magic bytes and reject spoofed types |
checksumMaxSize | number | null | null | Size cap above which client checksums are skipped |
Validation rules
Rules are keyed by purpose and run during addFiles. Extensions are matched without a
leading dot and case-insensitively; MIME types support an image/* wildcard prefix.
const config = {
validation: {
avatar: {
maxFiles: 1,
maxSizeBytes: 5 * 1024 * 1024,
minSizeBytes: 1024,
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
allowedExtensions: ['jpg', 'jpeg', 'png', 'webp'],
},
document: {
maxSizeBytes: 100 * 1024 * 1024,
allowedTypes: ['application/pdf'],
},
},
}
When both allowedTypes and allowedExtensions are given, a file passes if it matches
either. Rejected files never enter the state machine — they emit file.rejected with a
Contracts.Validation.Rejection.
Retry policy
The default retries up to maxAttempts with exponential backoff. Override per-error with
retryPolicy, which returns an Engine.RetryDecision:
const config = {
retryPolicy: ({ phase, attempt, error }) => {
if (error.code === 'auth') return { retryable: false }
if (error.code === 'rate_limit' && 'retryAfterMs' in error && typeof error.retryAfterMs === 'number') {
return { retryable: true, delayMs: error.retryAfterMs }
}
return { retryable: true, delayMs: Math.min(500 * 2 ** (attempt - 1), 10_000) }
},
}
phase is 'intent' | 'upload' | 'complete'. Return { retryable: false } or
{ retryable: true, delayMs }. maxAttempts still caps everything.
Plugins
Plugins extend behavior without forking the engine. Each Engine.Plugin has a name and a
setup that receives a minimal proxy — on, off, dispatch, getSnapshot — enough to
observe and react, not enough to corrupt state.
import type { Engine } from '@gentleduck/upload/core'
const analytics: Engine.Plugin<Intents, Cursors, Purpose, Result> = {
name: 'analytics',
setup({ on, getSnapshot }) {
on('upload.completed', ({ localId, result }) => {
const item = getSnapshot().items.get(localId)
track('upload_completed', { fileId: result.fileId, purpose: item?.purpose })
})
on('upload.error', ({ error }) => {
track('upload_error', { code: error.code, message: error.message })
})
},
}
const store = createUploadStore({ api, strategies, plugins: [analytics] })
A plugin that throws in setup is caught and logged in development; it never breaks the store,
and later plugins still initialize.
Hooks
Hooks are lower-level than plugins. onInternalEvent fires after every reducer event with the
event and the resulting state — ideal for devtools and logging:
const store = createUploadStore({
api,
strategies,
hooks: {
onInternalEvent(event, state) {
if (process.env.NODE_ENV === 'development') console.log('[upload]', event.type, event)
},
},
})
Custom fingerprinting
By default the engine fingerprints on name + size + type + lastModified. Supply a
synchronous function for stronger identity (a precomputed checksum enables dedupe):
const store = createUploadStore({
api,
strategies,
fingerprint: (file) => ({
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
checksum: precomputedChecksums.get(file), // must be available synchronously
}),
})
It must stay synchronous so addFiles never blocks. To hash large files, compute the checksum
before adding and look it up here.
Custom error normalization
errorNormalizer converts raw throws (fetch TypeError, XHR errors, backend shapes) into a
Contracts.Errors.Error with a stable code, so retryPolicy and your UI always see one
shape. Without it, the engine's built-in normalizer classifies common HTTP/network failures.
Next
- Engine — phases, scheduling, retry mechanics.
- Contracts — the API, transport, and strategy shapes.
- Persistence — configure resume across reloads.