Skip to main content

Store Options

WIP

Everything 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:

OptionRequiredDescription
apiYesBackend contract Contracts.Api.Me<M, P, R>
strategiesYesStrategy registry with your strategies registered
configNoEngine config (defaults applied by resolveUploadConfig)
transportNoHTTP transport. Defaults to createXHRTransport()
persistenceNoAdapter + options for crash-safe resume
pluginsNoArray of Engine.Plugin modules
hooksNoEngine.Hooks for low-level observation
fingerprintNoCustom, synchronous (file) => Contracts.FingerprintFile
validateFileNoExtra validator run after built-in rules
errorNormalizerNoMap raw thrown values into Contracts.Errors.Error
initialStateNoPre-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.

FieldTypeDefaultMeaning
maxConcurrentUploadsnumber3Files transferring at once (queued items wait)
progressThrottleMsnumber100Minimum gap between progress events
maxAttemptsnumber3Hard cap on attempts per phase
maxItemsnumber100Items kept in state before oldest terminal ones evict
autoStartreadonly P[] | (purpose: P) => booleanundefinedAuto-queue matching purposes after intent
validationPartial<Record<P, Contracts.ValidationRules>>{}Per-purpose rules
retryPolicy(ctx) => Engine.RetryDecisionundefinedOverride default backoff
completedItemTTLnumberundefinedEvict completed items after N ms
strictMimeMatchbooleanfalseSniff magic bytes and reject spoofed types
checksumMaxSizenumber | nullnullSize 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.

FAQ