@adminforth/background-jobs 1.12.1 → 1.13.1

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 22,184 bytes received 172 bytes 44,712.00 bytes/sec
17
+ total size is 21,551 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,90 @@ 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
+ props.job.state[data.fieldName] = data.value;
138
+ if (jobStore.currentJob?.id === props.job.id) {
139
+ jobStore.updateCurrentJob({
140
+ state: {
141
+ ...props.job.state,
142
+ },
143
+ });
144
+ }
145
+ }
146
+
147
+ function handleTaskStateFieldUpdate(data: TaskStateFieldUpdate) {
148
+ if (data.jobId !== props.job.id || !jobTasks.value[data.taskIndex]) {
149
+ return;
150
+ }
151
+
152
+ jobTasks.value[data.taskIndex].state = {
153
+ ...jobTasks.value[data.taskIndex].state,
154
+ [data.fieldName]: data.value,
155
+ };
156
+ }
157
+
158
+ function subscribeToJobStateFields(fieldNames: string[]) {
159
+ return createStateFieldSubscription(
160
+ fieldNames,
161
+ (fieldName) => `/background-jobs-state-update/${props.job.id}/${encodeURIComponent(fieldName)}`,
162
+ handleJobStateFieldUpdate,
163
+ );
164
+ }
165
+
166
+ function subscribeToJobTaskFields(fieldNames: string[]) {
167
+ return createStateFieldSubscription(
168
+ fieldNames,
169
+ (fieldName) => `/background-jobs-task-state-update/${props.job.id}/${encodeURIComponent(fieldName)}`,
170
+ handleTaskStateFieldUpdate,
171
+ );
172
+ }
173
+
85
174
  async function cancelJob() {
86
175
  // Implement job cancellation logic here
87
176
  const isConfirmed = await adminforth.confirm({ message: t('Are you sure you want to cancel this job?') });
@@ -111,7 +200,7 @@ async function cancelJob() {
111
200
 
112
201
 
113
202
 
114
- async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{state: Record<string, any>, status: string}[]> {
203
+ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<JobTask[]> {
115
204
  try {
116
205
  const res = await callAdminForthApi({
117
206
  path: `/plugin/${props.meta.pluginInstanceId}/get-tasks`,
@@ -123,7 +212,12 @@ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{sta
123
212
  },
124
213
  });
125
214
  if (res.ok) {
126
- return res.data;
215
+ const tasks = res.data.tasks as JobTask[];
216
+ const startIndex = offset || 0;
217
+ for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
218
+ jobTasks.value[startIndex + taskIndex] = tasks[taskIndex];
219
+ }
220
+ return jobTasks.value.slice(startIndex, startIndex + tasks.length);
127
221
  } else {
128
222
  console.error('Error fetching job tasks:', res.error);
129
223
  return [];
@@ -147,6 +241,12 @@ watch(
147
241
  { immediate: true }
148
242
  );
149
243
 
244
+ onBeforeUnmount(() => {
245
+ for (const unsubscribe of Array.from(subscriptionCleanups)) {
246
+ unsubscribe();
247
+ }
248
+ });
249
+
150
250
 
151
251
 
152
- </script>
252
+ </script>
@@ -5,7 +5,7 @@
5
5
  class="p-4"
6
6
  v-for="job in props.jobs" :key="job.id"
7
7
  :beforeCloseFunction="onBeforeClose"
8
- :beforeOpenFunction="onBeforeOpen"
8
+ :beforeOpenFunction="() => onBeforeOpen(job)"
9
9
  removeFromDomOnClose
10
10
  >
11
11
  <template #trigger>
@@ -30,7 +30,8 @@
30
30
  </div>
31
31
  </template>
32
32
  <JobInfoPopup
33
- :job="job"
33
+ v-if="loadedJobs[job.id]"
34
+ :job="loadedJobs[job.id]"
34
35
  :meta="meta"
35
36
  :closeModal="closeModal"
36
37
  />
@@ -42,7 +43,7 @@
42
43
 
43
44
  <script setup lang="ts">
44
45
  import type { IJob } from './utils';
45
- import { getTimeAgoString } from '@/utils';
46
+ import { callAdminForthApi, getTimeAgoString } from '@/utils';
46
47
  import { ProgressBar, Modal } from '@/afcl';
47
48
  import JobInfoPopup from './JobInfoPopup.vue';
48
49
  import StateToIcon from './StateToIcon.vue';
@@ -78,9 +79,34 @@ const props = defineProps<{
78
79
 
79
80
 
80
81
  const isModalOpen = ref(false);
82
+ const loadedJobs = ref<Record<string, IJob>>({});
81
83
 
82
- function onBeforeOpen() {
84
+ async function onBeforeOpen(job: IJob) {
83
85
  props.closeDropdown();
86
+ try {
87
+ const res = await callAdminForthApi({
88
+ path: `/plugin/get-background-job-info`,
89
+ method: 'POST',
90
+ body: { jobId: job.id },
91
+ });
92
+
93
+ if (res?.ok && res.job) {
94
+ loadedJobs.value[job.id] = res.job;
95
+ return;
96
+ }
97
+
98
+ console.log('[background-jobs] failed to load full job info', {
99
+ jobId: job.id,
100
+ response: res,
101
+ });
102
+ } catch (error) {
103
+ console.log('[background-jobs] failed to load full job info', {
104
+ error,
105
+ jobId: job.id,
106
+ });
107
+ }
108
+
109
+ loadedJobs.value[job.id] = job;
84
110
  }
85
111
 
86
112
  function onBeforeClose() {
@@ -88,4 +114,4 @@ function onBeforeClose() {
88
114
  }
89
115
 
90
116
 
91
- </script>
117
+ </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,90 @@ 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
+ props.job.state[data.fieldName] = data.value;
138
+ if (jobStore.currentJob?.id === props.job.id) {
139
+ jobStore.updateCurrentJob({
140
+ state: {
141
+ ...props.job.state,
142
+ },
143
+ });
144
+ }
145
+ }
146
+
147
+ function handleTaskStateFieldUpdate(data: TaskStateFieldUpdate) {
148
+ if (data.jobId !== props.job.id || !jobTasks.value[data.taskIndex]) {
149
+ return;
150
+ }
151
+
152
+ jobTasks.value[data.taskIndex].state = {
153
+ ...jobTasks.value[data.taskIndex].state,
154
+ [data.fieldName]: data.value,
155
+ };
156
+ }
157
+
158
+ function subscribeToJobStateFields(fieldNames: string[]) {
159
+ return createStateFieldSubscription(
160
+ fieldNames,
161
+ (fieldName) => `/background-jobs-state-update/${props.job.id}/${encodeURIComponent(fieldName)}`,
162
+ handleJobStateFieldUpdate,
163
+ );
164
+ }
165
+
166
+ function subscribeToJobTaskFields(fieldNames: string[]) {
167
+ return createStateFieldSubscription(
168
+ fieldNames,
169
+ (fieldName) => `/background-jobs-task-state-update/${props.job.id}/${encodeURIComponent(fieldName)}`,
170
+ handleTaskStateFieldUpdate,
171
+ );
172
+ }
173
+
85
174
  async function cancelJob() {
86
175
  // Implement job cancellation logic here
87
176
  const isConfirmed = await adminforth.confirm({ message: t('Are you sure you want to cancel this job?') });
@@ -111,7 +200,7 @@ async function cancelJob() {
111
200
 
112
201
 
113
202
 
114
- async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{state: Record<string, any>, status: string}[]> {
203
+ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<JobTask[]> {
115
204
  try {
116
205
  const res = await callAdminForthApi({
117
206
  path: `/plugin/${props.meta.pluginInstanceId}/get-tasks`,
@@ -123,7 +212,12 @@ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{sta
123
212
  },
124
213
  });
125
214
  if (res.ok) {
126
- return res.data;
215
+ const tasks = res.data.tasks as JobTask[];
216
+ const startIndex = offset || 0;
217
+ for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
218
+ jobTasks.value[startIndex + taskIndex] = tasks[taskIndex];
219
+ }
220
+ return jobTasks.value.slice(startIndex, startIndex + tasks.length);
127
221
  } else {
128
222
  console.error('Error fetching job tasks:', res.error);
129
223
  return [];
@@ -147,6 +241,12 @@ watch(
147
241
  { immediate: true }
148
242
  );
149
243
 
244
+ onBeforeUnmount(() => {
245
+ for (const unsubscribe of Array.from(subscriptionCleanups)) {
246
+ unsubscribe();
247
+ }
248
+ });
249
+
150
250
 
151
251
 
152
- </script>
252
+ </script>
@@ -5,7 +5,7 @@
5
5
  class="p-4"
6
6
  v-for="job in props.jobs" :key="job.id"
7
7
  :beforeCloseFunction="onBeforeClose"
8
- :beforeOpenFunction="onBeforeOpen"
8
+ :beforeOpenFunction="() => onBeforeOpen(job)"
9
9
  removeFromDomOnClose
10
10
  >
11
11
  <template #trigger>
@@ -30,7 +30,8 @@
30
30
  </div>
31
31
  </template>
32
32
  <JobInfoPopup
33
- :job="job"
33
+ v-if="loadedJobs[job.id]"
34
+ :job="loadedJobs[job.id]"
34
35
  :meta="meta"
35
36
  :closeModal="closeModal"
36
37
  />
@@ -42,7 +43,7 @@
42
43
 
43
44
  <script setup lang="ts">
44
45
  import type { IJob } from './utils';
45
- import { getTimeAgoString } from '@/utils';
46
+ import { callAdminForthApi, getTimeAgoString } from '@/utils';
46
47
  import { ProgressBar, Modal } from '@/afcl';
47
48
  import JobInfoPopup from './JobInfoPopup.vue';
48
49
  import StateToIcon from './StateToIcon.vue';
@@ -78,9 +79,34 @@ const props = defineProps<{
78
79
 
79
80
 
80
81
  const isModalOpen = ref(false);
82
+ const loadedJobs = ref<Record<string, IJob>>({});
81
83
 
82
- function onBeforeOpen() {
84
+ async function onBeforeOpen(job: IJob) {
83
85
  props.closeDropdown();
86
+ try {
87
+ const res = await callAdminForthApi({
88
+ path: `/plugin/get-background-job-info`,
89
+ method: 'POST',
90
+ body: { jobId: job.id },
91
+ });
92
+
93
+ if (res?.ok && res.job) {
94
+ loadedJobs.value[job.id] = res.job;
95
+ return;
96
+ }
97
+
98
+ console.log('[background-jobs] failed to load full job info', {
99
+ jobId: job.id,
100
+ response: res,
101
+ });
102
+ } catch (error) {
103
+ console.log('[background-jobs] failed to load full job info', {
104
+ error,
105
+ jobId: job.id,
106
+ });
107
+ }
108
+
109
+ loadedJobs.value[job.id] = job;
84
110
  }
85
111
 
86
112
  function onBeforeClose() {
@@ -88,4 +114,4 @@ function onBeforeClose() {
88
114
  }
89
115
 
90
116
 
91
- </script>
117
+ </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.1",
3
+ "version": "1.13.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",