-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathjobTracker.ts
More file actions
201 lines (189 loc) · 5.93 KB
/
jobTracker.ts
File metadata and controls
201 lines (189 loc) · 5.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
'use client';
/**
* Client-side job tracking for ModelSEED operations.
*
* Provides localStorage-based tracking of submitted jobs (reconstruct, gapfill,
* FBA, merge) so users can monitor job status across page reloads. Automatically
* manages a rolling list of the most recent jobs.
*/
export interface TrackedJob {
id: string;
kind: 'reconstruct' | 'copy' | 'gapfill' | 'fba' | 'merge';
label: string;
modelId?: string;
relatedRef?: string;
submittedAt: string;
}
const TRACKED_JOBS_STORAGE_KEY = 'modelseed:tracked-jobs';
/** Maximum number of jobs to keep in localStorage (prevents unbounded growth). */
const MAX_TRACKED_JOBS = 25;
/**
* Check if localStorage is available (client-side check).
*
* @returns True if window and localStorage are defined
*/
function canUseStorage(): boolean {
return typeof window !== 'undefined' && typeof localStorage !== 'undefined';
}
/**
* List all currently tracked jobs from localStorage.
*
* Returns jobs in reverse chronological order (most recent first).
* Safe to call in SSR context (returns empty array).
*
* @returns Array of tracked jobs, or empty array if none or in SSR
*
* @example
* ```typescript
* const jobs = listTrackedJobs();
* jobs.forEach(job => {
* console.log(`${job.label} (${job.kind}): ${job.id}`);
* });
* ```
*/
export function listTrackedJobs(): TrackedJob[] {
if (!canUseStorage()) return [];
try {
const raw = localStorage.getItem(TRACKED_JOBS_STORAGE_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw) as unknown;
return Array.isArray(parsed) ? parsed as TrackedJob[] : [];
} catch {
return [];
}
}
/**
* Write tracked jobs to localStorage with size limit.
*
* @param jobs - Jobs to persist (will be trimmed to MAX_TRACKED_JOBS)
*/
function writeTrackedJobs(jobs: TrackedJob[]): void {
if (!canUseStorage()) return;
localStorage.setItem(TRACKED_JOBS_STORAGE_KEY, JSON.stringify(jobs.slice(0, MAX_TRACKED_JOBS)));
}
/**
* Add or update a tracked job in localStorage.
*
* If a job with the same ID already exists, it will be replaced with the new data.
* Jobs are stored in reverse chronological order (most recent first).
* Automatically maintains a maximum of MAX_TRACKED_JOBS entries.
*
* @param job - Job to track
*
* @example
* ```typescript
* const job: TrackedJob = {
* id: 'job-12345',
* kind: 'reconstruct',
* label: 'Reconstruct E. coli model',
* modelId: 'model-789',
* submittedAt: new Date().toISOString()
* };
* trackJob(job);
* ```
*/
export function trackJob(job: TrackedJob): void {
const existing = listTrackedJobs().filter((item) => item.id !== job.id);
writeTrackedJobs([job, ...existing]);
}
/**
* Remove a job from the tracked jobs list.
*
* Use this when a job is dismissed or no longer needs tracking.
*
* @param jobId - ID of the job to remove
*
* @example
* ```typescript
* removeTrackedJob('job-12345');
* ```
*/
export function removeTrackedJob(jobId: string): void {
writeTrackedJobs(listTrackedJobs().filter((job) => job.id !== jobId));
}
/**
* Extract job ID from various API response structures.
*
* Handles different backend response formats by recursively searching for
* common job ID field names (id, job_id, jobId, task_id, taskId, uuid).
* Returns null if no recognizable ID is found.
*
* @param payload - API response payload (can be string, object, or array)
* @returns Extracted job ID string, or null if not found
*
* @example
* ```typescript
* const response = await submitReconstructJobFromApi(params);
* const jobId = extractTrackedJobId(response);
* if (jobId) trackJob({ id: jobId, kind: 'reconstruct', ... });
* ```
*/
export function extractTrackedJobId(payload: unknown): string | null {
if (!payload) return null;
if (typeof payload === 'string') return payload;
if (Array.isArray(payload)) {
// Recursively search array elements
for (const item of payload) {
const nested = extractTrackedJobId(item);
if (nested) return nested;
}
return null;
}
if (typeof payload === 'object') {
const candidate = payload as Record<string, unknown>;
const directKeys = ['id', 'job_id', 'jobId', 'task_id', 'taskId', 'uuid'];
for (const key of directKeys) {
const value = candidate[key];
if (typeof value === 'string' && value.length > 0) {
return value;
}
}
// Check nested 'result' field (common in JSON-RPC responses)
if ('result' in candidate) {
return extractTrackedJobId(candidate.result);
}
}
return null;
}
/**
* Check if a job status indicates terminal state (completed or failed).
*
* Terminal statuses mean the job is no longer running and polling should stop.
*
* @param status - Job status string (case-insensitive)
* @returns True if status indicates job has finished (success or failure)
*
* @example
* ```typescript
* if (isTerminalJobStatus(job.status)) {
* clearInterval(pollingInterval);
* console.log('Job finished');
* }
* ```
*/
export function isTerminalJobStatus(status: string | undefined): boolean {
if (!status) return false;
return ['completed', 'failed', 'error', 'cancelled', 'canceled', 'terminated'].includes(
status.toLowerCase(),
);
}
/**
* Check if a job status indicates active/running state.
*
* Active statuses mean the job is still in progress and polling should continue.
* Returns true if status is undefined (assume active until proven otherwise).
*
* @param status - Job status string
* @returns True if job is still running or status is unknown
*
* @example
* ```typescript
* if (isActiveJobStatus(job.status)) {
* console.log('Job is still running, continue polling');
* }
* ```
*/
export function isActiveJobStatus(status: string | undefined): boolean {
if (!status) return true;
return !isTerminalJobStatus(status);
}