@adminforth/background-jobs 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build.log CHANGED
@@ -11,5 +11,5 @@ custom/StateToIcon.vue
11
11
  custom/tsconfig.json
12
12
  custom/utils.ts
13
13
 
14
- sent 12,501 bytes received 134 bytes 25,270.00 bytes/sec
15
- total size is 12,016 speedup is 0.95
14
+ sent 12,535 bytes received 134 bytes 25,338.00 bytes/sec
15
+ total size is 12,050 speedup is 0.95
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="flex flex-col w-full min-w-96">
3
3
  <div class="flex items-center mb-1">
4
- <div class="flex flex-col items-start">
4
+ <div class="flex flex-col items-start justify-end h-12">
5
5
  <h2 class="text-lg font-semibold dark:text-white">{{ job.name }}</h2>
6
6
  <Tooltip>
7
7
  <p class="text-xs text-gray-600 dark:text-gray-200 h-full">{{ t('Created:') }} {{ getTimeAgoString(new Date(job.createdAt)) }}</p>
@@ -10,7 +10,7 @@
10
10
  </template>
11
11
  </Tooltip>
12
12
  </div>
13
- <div class="ml-auto flex flex-col items-start">
13
+ <div class="ml-auto flex flex-col items-start justify-end h-12">
14
14
  <div class="flex items-center">
15
15
  <p class=" text-gray-800 dark:text-white h-full"> {{ t('Progress:') }} <span class="font-semibold" >{{ job.progress }}%</span></p>
16
16
  <StateToIcon :job="job" />
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="flex flex-col w-full min-w-96">
3
3
  <div class="flex items-center mb-1">
4
- <div class="flex flex-col items-start">
4
+ <div class="flex flex-col items-start justify-end h-12">
5
5
  <h2 class="text-lg font-semibold dark:text-white">{{ job.name }}</h2>
6
6
  <Tooltip>
7
7
  <p class="text-xs text-gray-600 dark:text-gray-200 h-full">{{ t('Created:') }} {{ getTimeAgoString(new Date(job.createdAt)) }}</p>
@@ -10,7 +10,7 @@
10
10
  </template>
11
11
  </Tooltip>
12
12
  </div>
13
- <div class="ml-auto flex flex-col items-start">
13
+ <div class="ml-auto flex flex-col items-start justify-end h-12">
14
14
  <div class="flex items-center">
15
15
  <p class=" text-gray-800 dark:text-white h-full"> {{ t('Progress:') }} <span class="font-semibold" >{{ job.progress }}%</span></p>
16
16
  <StateToIcon :job="job" />
package/dist/index.js CHANGED
@@ -13,7 +13,6 @@ import pLimit from 'p-limit';
13
13
  import { Level } from 'level';
14
14
  import fs from 'fs/promises';
15
15
  import { Mutex } from 'async-mutex';
16
- const mutex = new Mutex();
17
16
  export default class BackgroundJobsPlugin extends AdminForthPlugin {
18
17
  constructor(options) {
19
18
  super(options, import.meta.url);
@@ -21,6 +20,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
21
20
  this.jobCustomComponents = {};
22
21
  this.jobParallelLimits = {};
23
22
  this.levelDbInstances = {};
23
+ this.jobStateMutexes = {};
24
24
  this.options = options;
25
25
  this.shouldHaveSingleInstancePerWholeApp = () => true;
26
26
  }
@@ -66,6 +66,8 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
66
66
  yield jobLevelDb.close();
67
67
  delete this.levelDbInstances[recordId];
68
68
  }
69
+ // cleanup per-job mutex as well
70
+ delete this.jobStateMutexes[recordId];
69
71
  //delete level db folder for the job
70
72
  yield fs.rm(levelDbPath, {
71
73
  recursive: true,
@@ -75,6 +77,12 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
75
77
  }));
76
78
  });
77
79
  }
80
+ cleanupJobMutexIfTerminalStatus(jobId, status) {
81
+ // Keep mutex while job is active to preserve atomicity between concurrent tasks.
82
+ if (status === 'DONE' || status === 'DONE_WITH_ERRORS' || status === 'CANCELLED') {
83
+ delete this.jobStateMutexes[jobId];
84
+ }
85
+ }
78
86
  checkIfFieldInResource(resourceConfig, fieldName, fieldString) {
79
87
  if (!fieldName) {
80
88
  throw new Error(`Field name for ${fieldString} is not provided. Please check your plugin options.`);
@@ -247,6 +255,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
247
255
  [this.options.finishedAtField]: (new Date()).toISOString(),
248
256
  });
249
257
  this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE', finishedAt: (new Date()).toISOString() });
258
+ this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE');
250
259
  }
251
260
  else if (failedTasks > 0) {
252
261
  yield this.adminforth.resource(this.getResourceId()).update(jobId, {
@@ -254,6 +263,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
254
263
  [this.options.finishedAtField]: (new Date()).toISOString(),
255
264
  });
256
265
  this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE_WITH_ERRORS' });
266
+ this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE_WITH_ERRORS');
257
267
  }
258
268
  });
259
269
  }
@@ -344,6 +354,26 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
344
354
  return JSON.parse(state);
345
355
  });
346
356
  }
357
+ updateJobFieldsAtomicly(jobId, updateFunction) {
358
+ return __awaiter(this, void 0, void 0, function* () {
359
+ if (!jobId) {
360
+ throw new Error('updateJobFieldsAtomicly: jobId is required');
361
+ }
362
+ if (typeof updateFunction !== 'function') {
363
+ throw new Error('updateJobFieldsAtomicly: updateFunction must be a function');
364
+ }
365
+ // Ensure updates are atomic per jobId.
366
+ // Different jobs are not blocked by each other.
367
+ let mutex = this.jobStateMutexes[jobId];
368
+ if (!mutex) {
369
+ mutex = new Mutex();
370
+ this.jobStateMutexes[jobId] = mutex;
371
+ }
372
+ return mutex.runExclusive(() => __awaiter(this, void 0, void 0, function* () {
373
+ yield updateFunction();
374
+ }));
375
+ });
376
+ }
347
377
  processAllUnfinishedJobs() {
348
378
  return __awaiter(this, void 0, void 0, function* () {
349
379
  const resourceId = this.getResourceId();
package/index.ts CHANGED
@@ -5,9 +5,7 @@ import { afLogger } from "adminforth";
5
5
  import pLimit from 'p-limit';
6
6
  import { Level } from 'level';
7
7
  import fs from 'fs/promises';
8
- import {Mutex, MutexInterface, Semaphore, SemaphoreInterface, withTimeout} from 'async-mutex';
9
-
10
- const mutex = new Mutex();
8
+ import { Mutex } from 'async-mutex';
11
9
 
12
10
  type TaskStatus = 'SCHEDULED' | 'IN_PROGRESS' | 'DONE' | 'FAILED';
13
11
  type setStateFieldParams = (state: Record<string, any>) => void;
@@ -24,6 +22,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
24
22
  private jobCustomComponents: Record<string, AdminForthComponentDeclarationFull> = {};
25
23
  private jobParallelLimits: Record<string, number> = {};
26
24
  private levelDbInstances: Record<string, Level> = {};
25
+ private jobStateMutexes: Record<string, Mutex> = {};
27
26
 
28
27
  constructor(options: PluginOptions) {
29
28
  super(options, import.meta.url);
@@ -73,6 +72,9 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
73
72
  delete this.levelDbInstances[recordId];
74
73
  }
75
74
 
75
+ // cleanup per-job mutex as well
76
+ delete this.jobStateMutexes[recordId];
77
+
76
78
  //delete level db folder for the job
77
79
  await fs.rm(levelDbPath, {
78
80
  recursive: true,
@@ -83,6 +85,13 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
83
85
  })
84
86
  }
85
87
 
88
+ private cleanupJobMutexIfTerminalStatus(jobId: string, status: string) {
89
+ // Keep mutex while job is active to preserve atomicity between concurrent tasks.
90
+ if (status === 'DONE' || status === 'DONE_WITH_ERRORS' || status === 'CANCELLED') {
91
+ delete this.jobStateMutexes[jobId];
92
+ }
93
+ }
94
+
86
95
  private checkIfFieldInResource(resourceConfig: AdminForthResource, fieldName: string, fieldString?: string) {
87
96
  if (!fieldName) {
88
97
  throw new Error(`Field name for ${fieldString} is not provided. Please check your plugin options.`);
@@ -279,12 +288,14 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
279
288
  [this.options.finishedAtField]: (new Date()).toISOString(),
280
289
  })
281
290
  this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE', finishedAt: (new Date()).toISOString() });
291
+ this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE');
282
292
  } else if (failedTasks > 0) {
283
293
  await this.adminforth.resource(this.getResourceId()).update(jobId, {
284
294
  [this.options.statusField]: 'DONE_WITH_ERRORS',
285
295
  [this.options.finishedAtField]: (new Date()).toISOString(),
286
296
  })
287
297
  this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE_WITH_ERRORS' });
298
+ this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE_WITH_ERRORS');
288
299
  }
289
300
  }
290
301
 
@@ -372,6 +383,27 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
372
383
  return JSON.parse(state);
373
384
  }
374
385
 
386
+ public async updateJobFieldsAtomicly(jobId: string, updateFunction: () => Promise<void>) {
387
+ if (!jobId) {
388
+ throw new Error('updateJobFieldsAtomicly: jobId is required');
389
+ }
390
+ if (typeof updateFunction !== 'function') {
391
+ throw new Error('updateJobFieldsAtomicly: updateFunction must be a function');
392
+ }
393
+
394
+ // Ensure updates are atomic per jobId.
395
+ // Different jobs are not blocked by each other.
396
+ let mutex = this.jobStateMutexes[jobId];
397
+ if (!mutex) {
398
+ mutex = new Mutex();
399
+ this.jobStateMutexes[jobId] = mutex;
400
+ }
401
+
402
+ return mutex.runExclusive(async () => {
403
+ await updateFunction();
404
+ });
405
+ }
406
+
375
407
  private async processAllUnfinishedJobs() {
376
408
  const resourceId = this.getResourceId();
377
409
  const unprocessedJobs = await this.adminforth.resource(resourceId).list(Filters.EQ(this.options.statusField, 'IN_PROGRESS'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/background-jobs",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",