@adminforth/background-jobs 1.12.0 → 1.13.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
@@ -13,5 +13,5 @@ custom/tsconfig.json
13
13
  custom/useBackgroundJobApi.ts
14
14
  custom/utils.ts
15
15
 
16
- sent 18,730 bytes received 172 bytes 37,804.00 bytes/sec
17
- total size is 18,097 speedup is 0.96
16
+ sent 21,425 bytes received 172 bytes 43,194.00 bytes/sec
17
+ total size is 20,792 speedup is 0.96
@@ -54,6 +54,8 @@
54
54
  :meta="job.customComponent"
55
55
  :getJobTasks="getJobTasks"
56
56
  :job="job"
57
+ :subscribeToJobStateFields="subscribeToJobStateFields"
58
+ :subscribeToJobTaskFields="subscribeToJobTaskFields"
57
59
  />
58
60
  </template>
59
61
 
@@ -67,12 +69,15 @@ import { getTimeAgoString, callAdminForthApi, getCustomComponent} from '@/utils'
67
69
  import { useI18n } from 'vue-i18n';
68
70
  import StateToIcon from './StateToIcon.vue';
69
71
  import { useAdminforth } from '@/adminforth';
70
- import { watch } from 'vue';
72
+ import { onBeforeUnmount, ref, watch } from 'vue';
73
+ import websocket from '@/websocket';
74
+ import { useBackgroundJobApi } from './useBackgroundJobApi';
71
75
 
72
76
 
73
77
  const { t } = useI18n();
74
78
 
75
79
  const adminforth = useAdminforth();
80
+ const jobStore = useBackgroundJobApi();
76
81
 
77
82
  const props = defineProps<{
78
83
  job: IJob;
@@ -82,6 +87,88 @@ const props = defineProps<{
82
87
  closeModal: () => void;
83
88
  }>();
84
89
 
90
+ type JobTask = {
91
+ state: Record<string, any>;
92
+ status: string;
93
+ };
94
+
95
+ type JobStateFieldUpdate = {
96
+ jobId: string;
97
+ fieldName: string;
98
+ value: any;
99
+ };
100
+
101
+ type TaskStateFieldUpdate = JobStateFieldUpdate & {
102
+ taskIndex: number;
103
+ };
104
+
105
+ const jobTasks = ref<JobTask[]>([]);
106
+ const subscriptionCleanups = new Set<() => void>();
107
+
108
+ function getUniqueFieldNames(fieldNames: string[]): string[] {
109
+ return Array.from(new Set(fieldNames.filter((fieldName) => typeof fieldName === 'string' && fieldName.length > 0)));
110
+ }
111
+
112
+ function createStateFieldSubscription(
113
+ fieldNames: string[],
114
+ pathFactory: (fieldName: string) => string,
115
+ callback: (data: any) => void,
116
+ ) {
117
+ const paths = getUniqueFieldNames(fieldNames).map(pathFactory);
118
+ for (const path of paths) {
119
+ websocket.subscribe(path, callback);
120
+ }
121
+
122
+ const unsubscribe = () => {
123
+ for (const path of paths) {
124
+ websocket.unsubscribe(path);
125
+ }
126
+ subscriptionCleanups.delete(unsubscribe);
127
+ };
128
+ subscriptionCleanups.add(unsubscribe);
129
+ return unsubscribe;
130
+ }
131
+
132
+ function handleJobStateFieldUpdate(data: JobStateFieldUpdate) {
133
+ if (data.jobId !== props.job.id) {
134
+ return;
135
+ }
136
+
137
+ jobStore.updateCurrentJob({
138
+ state: {
139
+ ...props.job.state,
140
+ [data.fieldName]: data.value,
141
+ },
142
+ });
143
+ }
144
+
145
+ function handleTaskStateFieldUpdate(data: TaskStateFieldUpdate) {
146
+ if (data.jobId !== props.job.id || !jobTasks.value[data.taskIndex]) {
147
+ return;
148
+ }
149
+
150
+ jobTasks.value[data.taskIndex].state = {
151
+ ...jobTasks.value[data.taskIndex].state,
152
+ [data.fieldName]: data.value,
153
+ };
154
+ }
155
+
156
+ function subscribeToJobStateFields(fieldNames: string[]) {
157
+ return createStateFieldSubscription(
158
+ fieldNames,
159
+ (fieldName) => `/background-jobs-state-update/${props.job.id}/${encodeURIComponent(fieldName)}`,
160
+ handleJobStateFieldUpdate,
161
+ );
162
+ }
163
+
164
+ function subscribeToJobTaskFields(fieldNames: string[]) {
165
+ return createStateFieldSubscription(
166
+ fieldNames,
167
+ (fieldName) => `/background-jobs-task-state-update/${props.job.id}/${encodeURIComponent(fieldName)}`,
168
+ handleTaskStateFieldUpdate,
169
+ );
170
+ }
171
+
85
172
  async function cancelJob() {
86
173
  // Implement job cancellation logic here
87
174
  const isConfirmed = await adminforth.confirm({ message: t('Are you sure you want to cancel this job?') });
@@ -111,7 +198,7 @@ async function cancelJob() {
111
198
 
112
199
 
113
200
 
114
- async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{state: Record<string, any>, status: string}[]> {
201
+ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<JobTask[]> {
115
202
  try {
116
203
  const res = await callAdminForthApi({
117
204
  path: `/plugin/${props.meta.pluginInstanceId}/get-tasks`,
@@ -123,7 +210,12 @@ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{sta
123
210
  },
124
211
  });
125
212
  if (res.ok) {
126
- return res.data;
213
+ const tasks = res.data.tasks as JobTask[];
214
+ const startIndex = offset || 0;
215
+ for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
216
+ jobTasks.value[startIndex + taskIndex] = tasks[taskIndex];
217
+ }
218
+ return jobTasks.value.slice(startIndex, startIndex + tasks.length);
127
219
  } else {
128
220
  console.error('Error fetching job tasks:', res.error);
129
221
  return [];
@@ -147,6 +239,12 @@ watch(
147
239
  { immediate: true }
148
240
  );
149
241
 
242
+ onBeforeUnmount(() => {
243
+ for (const unsubscribe of Array.from(subscriptionCleanups)) {
244
+ unsubscribe();
245
+ }
246
+ });
247
+
150
248
 
151
249
 
152
- </script>
250
+ </script>
@@ -54,6 +54,8 @@
54
54
  :meta="job.customComponent"
55
55
  :getJobTasks="getJobTasks"
56
56
  :job="job"
57
+ :subscribeToJobStateFields="subscribeToJobStateFields"
58
+ :subscribeToJobTaskFields="subscribeToJobTaskFields"
57
59
  />
58
60
  </template>
59
61
 
@@ -67,12 +69,15 @@ import { getTimeAgoString, callAdminForthApi, getCustomComponent} from '@/utils'
67
69
  import { useI18n } from 'vue-i18n';
68
70
  import StateToIcon from './StateToIcon.vue';
69
71
  import { useAdminforth } from '@/adminforth';
70
- import { watch } from 'vue';
72
+ import { onBeforeUnmount, ref, watch } from 'vue';
73
+ import websocket from '@/websocket';
74
+ import { useBackgroundJobApi } from './useBackgroundJobApi';
71
75
 
72
76
 
73
77
  const { t } = useI18n();
74
78
 
75
79
  const adminforth = useAdminforth();
80
+ const jobStore = useBackgroundJobApi();
76
81
 
77
82
  const props = defineProps<{
78
83
  job: IJob;
@@ -82,6 +87,88 @@ const props = defineProps<{
82
87
  closeModal: () => void;
83
88
  }>();
84
89
 
90
+ type JobTask = {
91
+ state: Record<string, any>;
92
+ status: string;
93
+ };
94
+
95
+ type JobStateFieldUpdate = {
96
+ jobId: string;
97
+ fieldName: string;
98
+ value: any;
99
+ };
100
+
101
+ type TaskStateFieldUpdate = JobStateFieldUpdate & {
102
+ taskIndex: number;
103
+ };
104
+
105
+ const jobTasks = ref<JobTask[]>([]);
106
+ const subscriptionCleanups = new Set<() => void>();
107
+
108
+ function getUniqueFieldNames(fieldNames: string[]): string[] {
109
+ return Array.from(new Set(fieldNames.filter((fieldName) => typeof fieldName === 'string' && fieldName.length > 0)));
110
+ }
111
+
112
+ function createStateFieldSubscription(
113
+ fieldNames: string[],
114
+ pathFactory: (fieldName: string) => string,
115
+ callback: (data: any) => void,
116
+ ) {
117
+ const paths = getUniqueFieldNames(fieldNames).map(pathFactory);
118
+ for (const path of paths) {
119
+ websocket.subscribe(path, callback);
120
+ }
121
+
122
+ const unsubscribe = () => {
123
+ for (const path of paths) {
124
+ websocket.unsubscribe(path);
125
+ }
126
+ subscriptionCleanups.delete(unsubscribe);
127
+ };
128
+ subscriptionCleanups.add(unsubscribe);
129
+ return unsubscribe;
130
+ }
131
+
132
+ function handleJobStateFieldUpdate(data: JobStateFieldUpdate) {
133
+ if (data.jobId !== props.job.id) {
134
+ return;
135
+ }
136
+
137
+ jobStore.updateCurrentJob({
138
+ state: {
139
+ ...props.job.state,
140
+ [data.fieldName]: data.value,
141
+ },
142
+ });
143
+ }
144
+
145
+ function handleTaskStateFieldUpdate(data: TaskStateFieldUpdate) {
146
+ if (data.jobId !== props.job.id || !jobTasks.value[data.taskIndex]) {
147
+ return;
148
+ }
149
+
150
+ jobTasks.value[data.taskIndex].state = {
151
+ ...jobTasks.value[data.taskIndex].state,
152
+ [data.fieldName]: data.value,
153
+ };
154
+ }
155
+
156
+ function subscribeToJobStateFields(fieldNames: string[]) {
157
+ return createStateFieldSubscription(
158
+ fieldNames,
159
+ (fieldName) => `/background-jobs-state-update/${props.job.id}/${encodeURIComponent(fieldName)}`,
160
+ handleJobStateFieldUpdate,
161
+ );
162
+ }
163
+
164
+ function subscribeToJobTaskFields(fieldNames: string[]) {
165
+ return createStateFieldSubscription(
166
+ fieldNames,
167
+ (fieldName) => `/background-jobs-task-state-update/${props.job.id}/${encodeURIComponent(fieldName)}`,
168
+ handleTaskStateFieldUpdate,
169
+ );
170
+ }
171
+
85
172
  async function cancelJob() {
86
173
  // Implement job cancellation logic here
87
174
  const isConfirmed = await adminforth.confirm({ message: t('Are you sure you want to cancel this job?') });
@@ -111,7 +198,7 @@ async function cancelJob() {
111
198
 
112
199
 
113
200
 
114
- async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{state: Record<string, any>, status: string}[]> {
201
+ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<JobTask[]> {
115
202
  try {
116
203
  const res = await callAdminForthApi({
117
204
  path: `/plugin/${props.meta.pluginInstanceId}/get-tasks`,
@@ -123,7 +210,12 @@ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{sta
123
210
  },
124
211
  });
125
212
  if (res.ok) {
126
- return res.data;
213
+ const tasks = res.data.tasks as JobTask[];
214
+ const startIndex = offset || 0;
215
+ for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
216
+ jobTasks.value[startIndex + taskIndex] = tasks[taskIndex];
217
+ }
218
+ return jobTasks.value.slice(startIndex, startIndex + tasks.length);
127
219
  } else {
128
220
  console.error('Error fetching job tasks:', res.error);
129
221
  return [];
@@ -147,6 +239,12 @@ watch(
147
239
  { immediate: true }
148
240
  );
149
241
 
242
+ onBeforeUnmount(() => {
243
+ for (const unsubscribe of Array.from(subscriptionCleanups)) {
244
+ unsubscribe();
245
+ }
246
+ });
247
+
150
248
 
151
249
 
152
- </script>
250
+ </script>
package/dist/index.js CHANGED
@@ -13,6 +13,9 @@ 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
+ function encodeStateFieldName(fieldName) {
17
+ return encodeURIComponent(fieldName);
18
+ }
16
19
  export default class BackgroundJobsPlugin extends AdminForthPlugin {
17
20
  constructor(options) {
18
21
  super(options, import.meta.url);
@@ -170,6 +173,23 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
170
173
  return { failedTasks, succeededTasks };
171
174
  });
172
175
  }
176
+ publishJobStateField(jobId, fieldName, value) {
177
+ this.adminforth.websocket.publish(`/background-jobs-state-update/${jobId}/${encodeStateFieldName(fieldName)}`, {
178
+ jobId,
179
+ fieldName,
180
+ value,
181
+ });
182
+ }
183
+ publishTaskStateFields(jobId, taskIndex, state) {
184
+ for (const [fieldName, value] of Object.entries(state)) {
185
+ this.adminforth.websocket.publish(`/background-jobs-task-state-update/${jobId}/${encodeStateFieldName(fieldName)}`, {
186
+ jobId,
187
+ taskIndex,
188
+ fieldName,
189
+ value,
190
+ });
191
+ }
192
+ }
173
193
  triggerOnAllTasksDone(onAllTasksDone, levelDb, jobId) {
174
194
  return __awaiter(this, void 0, void 0, function* () {
175
195
  if (!onAllTasksDone) {
@@ -270,8 +290,8 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
270
290
  }
271
291
  //define the setTaskStateField and getTaskStateField functions to pass to the task
272
292
  const setTaskStateField = (state) => __awaiter(this, void 0, void 0, function* () {
273
- this.adminforth.websocket.publish(`/background-jobs-task-update/${jobId}`, { taskIndex, state });
274
293
  yield this.setLevelDbTaskStateField(jobLevelDb, taskIndex.toString(), state);
294
+ this.publishTaskStateFields(jobId, taskIndex, state);
275
295
  });
276
296
  const getTaskStateField = () => __awaiter(this, void 0, void 0, function* () {
277
297
  return yield this.getLevelDbTaskStateField(jobLevelDb, taskIndex.toString());
@@ -397,10 +417,10 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
397
417
  const state = jobRecord[this.options.stateField];
398
418
  const parsedState = JSON.parse(state);
399
419
  parsedState[key] = value;
400
- this.adminforth.websocket.publish(`/background-jobs`, { jobId, state: parsedState });
401
420
  yield this.adminforth.resource(this.getResourceId()).update(jobId, {
402
421
  [this.options.stateField]: JSON.stringify(parsedState),
403
422
  });
423
+ this.publishJobStateField(jobId, key, value);
404
424
  });
405
425
  }
406
426
  getJobField(jobId, key) {
package/index.ts CHANGED
@@ -21,6 +21,10 @@ type taskType = {
21
21
  skip?: boolean;
22
22
  state: Record<string, any>;
23
23
  }
24
+
25
+ function encodeStateFieldName(fieldName: string): string {
26
+ return encodeURIComponent(fieldName);
27
+ }
24
28
 
25
29
  export default class BackgroundJobsPlugin extends AdminForthPlugin {
26
30
  options: PluginOptions;
@@ -184,6 +188,25 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
184
188
  return { failedTasks, succeededTasks };
185
189
  }
186
190
 
191
+ private publishJobStateField(jobId: string, fieldName: string, value: any) {
192
+ this.adminforth.websocket.publish(`/background-jobs-state-update/${jobId}/${encodeStateFieldName(fieldName)}`, {
193
+ jobId,
194
+ fieldName,
195
+ value,
196
+ });
197
+ }
198
+
199
+ private publishTaskStateFields(jobId: string, taskIndex: number, state: Record<string, any>) {
200
+ for (const [fieldName, value] of Object.entries(state)) {
201
+ this.adminforth.websocket.publish(`/background-jobs-task-state-update/${jobId}/${encodeStateFieldName(fieldName)}`, {
202
+ jobId,
203
+ taskIndex,
204
+ fieldName,
205
+ value,
206
+ });
207
+ }
208
+ }
209
+
187
210
  private async triggerOnAllTasksDone(onAllTasksDone: onAllTasksDoneType | undefined, levelDb: Level, jobId: string) {
188
211
  if (!onAllTasksDone) {
189
212
  return;
@@ -306,8 +329,8 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
306
329
 
307
330
  //define the setTaskStateField and getTaskStateField functions to pass to the task
308
331
  const setTaskStateField = async (state: Record<string, any>) => {
309
- this.adminforth.websocket.publish(`/background-jobs-task-update/${jobId}`, { taskIndex, state });
310
332
  await this.setLevelDbTaskStateField(jobLevelDb, taskIndex.toString(), state);
333
+ this.publishTaskStateFields(jobId, taskIndex, state);
311
334
  }
312
335
  const getTaskStateField = async () => {
313
336
  return await this.getLevelDbTaskStateField(jobLevelDb, taskIndex.toString());
@@ -435,10 +458,10 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
435
458
  const state = jobRecord[this.options.stateField];
436
459
  const parsedState = JSON.parse(state);
437
460
  parsedState[key] = value;
438
- this.adminforth.websocket.publish(`/background-jobs`, { jobId, state: parsedState });
439
461
  await this.adminforth.resource(this.getResourceId()).update(jobId, {
440
462
  [this.options.stateField]: JSON.stringify(parsedState),
441
463
  });
464
+ this.publishJobStateField(jobId, key, value);
442
465
  }
443
466
 
444
467
  public async getJobField(jobId: string, key: string) {
@@ -629,4 +652,4 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
629
652
  });
630
653
  }
631
654
 
632
- }
655
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/background-jobs",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "description": "Background jobs plugin for AdminForth to run and manage long tasks from resources",
26
26
  "devDependencies": {
27
27
  "@types/node": "latest",
28
- "adminforth": "^2.50.0",
28
+ "adminforth": "^2.70.0",
29
29
  "semantic-release": "^24.2.1",
30
30
  "semantic-release-slack-bot": "^4.0.2",
31
31
  "typescript": "^5.7.3"