API Reference

trackJob()

Starts tracking an async backend job at runtime — for jobs that didn't exist yet when your stores were registered. The realtime and polling transports share one interpreter, so onProgress, onComplete, and onFailure behave identically no matter which one is wired up.

Prerequisites
  • flux created via createFlux() — see api/create-flux
  • A state manager store (Zustand natively, or a Redux/Jotai adapter) to receive progress
1

Basic usage

Use flux.trackJob() the moment you first learn a job's ID — typically right after an offline-queued upload finally replays and your backend hands back a job_id in its response.
typescript
1// The canonical use case: an offline-queued upload whose job_id
2// doesn't exist yet when the form is submitted.
3
4// 1. User submits offline — the queue stores the intent + file reference
5// 2. On reconnect, your replay handler uploads the file and calls your backend
6// 3. The backend response finally has a job_id — only now can tracking start
7
8const unregister = flux.trackJob(jobId, videoStore, {
9 trackBy: 'job_id',
10 onProgress: (store, percent) => store.getState().setProgress(percent),
11 onComplete: (store, payload) => store.getState().setResult(payload),
12 onFailure: (store, error) => store.getState().setError(error),
13})
14
15// Cleanup if the component unmounts before the job finishes
16unregister()
Static vs dynamic job tracking

If a channel always carries job rows and you know that at registration time, use the jobTracker field on register() instead — see api/register. Reach for trackJob() specifically when the job ID doesn't exist until runtime.

2

Config reference

Only trackBy is required. It's the same JobTrackerConfig shape used by the static register() path.
typescript
1interface JobTrackerConfig {
2 trackBy: string // field on the job row that uniquely identifies it
3 onProgress?: (store: any, percent: number) => void
4 onComplete?: (store: any, payload: any) => void
5 onFailure?: (store: any, error: any) => void
6 pollFn?: () => Promise<any[]> // polling fallback — see step 6
7 pollIntervalMs?: number // default 3000ms
8 scope?: StorageScope // defaults to 'global' — see step 4
9}
3

Status and progress field detection

You don't need to tell Flux which column holds status or progress — a fixed priority order of common field names is checked automatically, and every terminal value fires its callback exactly once.
typescript
1// Status and progress fields are auto-detected, checked in this order —
2// no field-name config needed if your job rows use any of these:
3
4// Status: status, job_status, state, jobState
5// Progress: progress, percent, percent_complete
6
7// Terminal success values (fire onComplete, exactly once):
8// complete, completed, done, finished, success, succeeded
9
10// Terminal failure values (fire onFailure, exactly once):
11// failed, failure, error, cancelled, canceled
12
13// Internal tracking state is cleaned up immediately on whichever
14// terminal transition fires, so long sessions don't accumulate memory.
4

scope — set explicitly, never inherited

A statically registered jobTracker inherits scope from its parent store. A dynamically tracked job has no parent registration to inherit from at all — if this job belongs to a logged-in user's private flow, set scope: 'user' yourself so it's cleared by clearUserSession().
typescript
1// scope defaults to 'global'. Unlike a static jobTracker registered
2// via register(), a dynamically tracked job has no parent
3// StoreRegistration to inherit scope from — you must set it
4// explicitly if the job belongs to a private, logged-in user flow.
5
6const unregister = flux.trackJob(jobId, videoStore, {
7 trackBy: 'job_id',
8 scope: 'user', // so this job's state is cleared on logout
9 onProgress: (store, percent) => store.getState().setProgress(percent),
10 onComplete: (store, payload) => store.getState().setResult(payload),
11})
Explicit, not inferred

Flux deliberately doesn't try to guess scope by pattern-matching trackBy against existing registrations — values like job_id are generic and shared across unrelated trackers, which makes that kind of inference unreliable. See core-concepts/multi-tenant-scoping.

5

Dual transport, one interpreter

Whether updates arrive over realtime or polling, the exact same event interpreter processes them — your callbacks fire the same way regardless of transport.
typescript
1// The same interpreter handles events regardless of where they came from.
2// Realtime path: every event on any subscribed channel is checked against
3// dynamicJobs — this runs for ALL realtime events, not just ones you're
4// tracking, but is effectively free if dynamicJobs is empty.
5
6// Polling path: if you pass pollFn (see step 6), a JobPoller constructs
7// a minimal FluxNormalizedEvent from each polled row and feeds it
8// through the exact same processJobTrackerEvent() function.
9
10// onProgress / onComplete / onFailure fire identically either way —
11// your callbacks never need to know which transport is in use.
6

Polling fallback

Pass pollFn for jobs with no realtime channel backing them. Polling is visibility- and offline-aware, and never stacks requests on a slow connection.
typescript
1// pollFn is a fallback for jobs with no realtime table backing them.
2// It's called on the configured interval, and its results are fed
3// through the same interpreter as the realtime path.
4
5flux.trackJob(jobId, videoStore, {
6 trackBy: 'job_id',
7 pollFn: () => fetch('/api/jobs/status').then((r) => r.json()),
8 pollIntervalMs: 3000, // recommended: 1000-1500ms for urgent flows
9 // 3000ms default for standard ops
10 // 10000-30000ms for long-running jobs
11})
12
13// The effective interval is pollIntervalMs PLUS fetch duration —
14// the next poll only starts once the current request resolves,
15// so slow connections never stack up requests.
7

Cleanup

trackJob() returns an unregister function, the same shape as register(). A normal terminal transition already cleans itself up — you only need to call this for an early unmount.
typescript
1// trackJob() returns an unregister function, same shape as register().
2// Call it if the tracking component unmounts before the job resolves.
3const unregister = flux.trackJob(jobId, store, config)
4unregister()
5
6// You don't need to call it on a normal terminal transition —
7// onComplete / onFailure firing already triggers automatic cleanup.

For jobs known at registration time, see the jobTracker field in api/register. For the full offline-upload-to-tracked-job flow, see offline/upload-pattern and core-concepts/job-tracker.