psdev-task-manager 1.0.2 → 1.0.4
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/backend/elevated-modules.js +22 -0
- package/backend/index.js +4 -0
- package/backend/tasks/consts.js +25 -0
- package/backend/tasks/fieldTypeDetector.js +231 -0
- package/backend/tasks/index.js +9 -0
- package/backend/tasks/parentChildTasks/childScheduler.js +66 -0
- package/backend/tasks/parentChildTasks/handler.js +77 -0
- package/backend/tasks/parentChildTasks/index.js +6 -0
- package/backend/tasks/parentChildTasks/parentStatusManager.js +41 -0
- package/backend/tasks/parentChildTasks/utils.js +28 -0
- package/backend/tasks/resultsSaver.js +124 -0
- package/backend/tasks/taskManager.js +141 -0
- package/backend/tasks/utils.js +262 -0
- package/index.js +2 -5
- package/package.json +5 -1
- package/public/consts.js +29 -0
- package/public/index.js +6 -0
- package/public/performance/duration.js +14 -0
- package/public/performance/index.js +3 -0
- package/script.js +0 -7
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { items, collections } = require('@wix/data');
|
|
2
|
+
const { auth } = require('@wix/essentials');
|
|
3
|
+
|
|
4
|
+
// @wix/data does not support suppressAuth currently, so we need to elevate it
|
|
5
|
+
const wixData = {
|
|
6
|
+
insert: auth.elevate(items.insert),
|
|
7
|
+
update: auth.elevate(items.update),
|
|
8
|
+
bulkInsert: auth.elevate(items.bulkInsert),
|
|
9
|
+
query: auth.elevate(items.query),
|
|
10
|
+
save: auth.elevate(items.save),
|
|
11
|
+
remove: auth.elevate(items.remove),
|
|
12
|
+
get: auth.elevate(items.get),
|
|
13
|
+
//TODO: add other methods here as needed
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const wixCollections = {
|
|
17
|
+
getDataCollection: auth.elevate(collections.getDataCollection),
|
|
18
|
+
createDataCollection: auth.elevate(collections.createDataCollection),
|
|
19
|
+
updateDataCollection: auth.elevate(collections.updateDataCollection),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
module.exports = { wixData, wixCollections };
|
package/backend/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const TASK_STATUS = {
|
|
2
|
+
PENDING: 'pending',
|
|
3
|
+
IN_PROGRESS: 'in_progress',
|
|
4
|
+
SUCCESS: 'success',
|
|
5
|
+
FAILED: 'failed',
|
|
6
|
+
SKIPPED: 'skipped',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const TASK_TYPE = {
|
|
10
|
+
SCHEDULED: 'scheduled',
|
|
11
|
+
EVENT: 'event',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const TASKS_CONCURRENCY_LIMIT = 1;
|
|
15
|
+
const TASK_MAX_TRIES = 3;
|
|
16
|
+
|
|
17
|
+
const CRON_JOB_MAX_DURATION_SEC = 240; // 4 minutes is the velo limit for a cron job run @see https://wix.slack.com/archives/C02T75TDG2C/p1747750017241869
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
TASK_STATUS,
|
|
21
|
+
TASK_TYPE,
|
|
22
|
+
TASKS_CONCURRENCY_LIMIT,
|
|
23
|
+
TASK_MAX_TRIES,
|
|
24
|
+
CRON_JOB_MAX_DURATION_SEC,
|
|
25
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for detecting field types based on values
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a string is a valid URL
|
|
7
|
+
* @param {string} str - The string to check
|
|
8
|
+
* @returns {boolean} - Whether the string is a valid URL
|
|
9
|
+
*/
|
|
10
|
+
function isValidUrl(str) {
|
|
11
|
+
if (typeof str !== 'string') return false;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(str);
|
|
15
|
+
return ['http:', 'https:', 'ftp:', 'ftps:'].includes(url.protocol);
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a string represents a date/datetime
|
|
23
|
+
* @param {string} str - The string to check
|
|
24
|
+
* @returns {boolean} - Whether the string represents a valid date
|
|
25
|
+
*/
|
|
26
|
+
function isDateString(str) {
|
|
27
|
+
if (typeof str !== 'string') return false;
|
|
28
|
+
|
|
29
|
+
// Common date patterns
|
|
30
|
+
const datePatterns = [
|
|
31
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, // ISO 8601: 2025-06-24T11:26:00.000Z
|
|
32
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/, // ISO with timezone: 2025-06-24T11:26:00+01:00
|
|
33
|
+
/^\d{4}-\d{2}-\d{2}$/, // Date only: 2025-06-24
|
|
34
|
+
/^\d{2}\/\d{2}\/\d{4}$/, // US format: 06/24/2025
|
|
35
|
+
/^\d{2}-\d{2}-\d{4}$/, // Alternative format: 24-06-2025
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (!datePatterns.some(pattern => pattern.test(str))) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate that it's actually a valid date
|
|
43
|
+
const date = new Date(str);
|
|
44
|
+
return !isNaN(date.getTime());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks if a string represents an email address
|
|
49
|
+
* @param {string} str - The string to check
|
|
50
|
+
* @returns {boolean} - Whether the string is a valid email
|
|
51
|
+
*/
|
|
52
|
+
function isEmail(str) {
|
|
53
|
+
if (typeof str !== 'string') return false;
|
|
54
|
+
|
|
55
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
56
|
+
return emailPattern.test(str);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks if a string represents a phone number
|
|
61
|
+
* @param {string} str - The string to check
|
|
62
|
+
* @returns {boolean} - Whether the string looks like a phone number
|
|
63
|
+
*/
|
|
64
|
+
function isPhoneNumber(str) {
|
|
65
|
+
if (typeof str !== 'string') return false;
|
|
66
|
+
|
|
67
|
+
// Remove all non-alphanumeric characters for pattern matching
|
|
68
|
+
const cleanStr = str.replace(/[\s().-]/g, '');
|
|
69
|
+
|
|
70
|
+
// Basic phone number patterns (international and common formats)
|
|
71
|
+
const phonePatterns = [
|
|
72
|
+
/^\+\d{1,3}\d{6,14}$/, // International: +1234567890
|
|
73
|
+
/^\d{10}$/, // Simple 10 digits: 1234567890
|
|
74
|
+
/^\d{11}$/, // 11 digits with country code: 11234567890
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Also check original string for formatted patterns
|
|
78
|
+
const formattedPatterns = [
|
|
79
|
+
/^\+\d{1,3}\s?\(\d{3}\)\s?\d{3}[\s-]?\d{4}$/, // +1 (555) 123-4567
|
|
80
|
+
/^\(\d{3}\)\s?\d{3}[\s-]?\d{4}$/, // (555) 123-4567
|
|
81
|
+
/^\d{3}[\s-]?\d{3}[\s-]?\d{4}$/, // 555-123-4567 or 555 123 4567
|
|
82
|
+
/^\+\d{1,3}[\s-]?\d{6,14}$/, // +1-1234567890 or +1 1234567890
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
phonePatterns.some(pattern => pattern.test(cleanStr)) ||
|
|
87
|
+
formattedPatterns.some(pattern => pattern.test(str))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detects the appropriate Wix Data field type based on a value
|
|
93
|
+
* @param {*} value - The value to analyze
|
|
94
|
+
* @returns {string} - The Wix Data field type
|
|
95
|
+
*/
|
|
96
|
+
function detectFieldType(value) {
|
|
97
|
+
// Handle null/undefined
|
|
98
|
+
if (value === null || value === undefined) {
|
|
99
|
+
return 'TEXT'; // Default to TEXT for null values
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle primitive types first
|
|
103
|
+
if (typeof value === 'boolean') {
|
|
104
|
+
return 'BOOLEAN';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof value === 'number') {
|
|
108
|
+
return 'NUMBER';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (value instanceof Date) {
|
|
112
|
+
return 'DATETIME';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle arrays
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return 'ARRAY';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle objects (but not arrays or dates)
|
|
121
|
+
if (typeof value === 'object') {
|
|
122
|
+
return 'OBJECT';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle strings with special patterns
|
|
126
|
+
if (typeof value === 'string') {
|
|
127
|
+
// Check for empty strings
|
|
128
|
+
if (value.trim() === '') {
|
|
129
|
+
return 'TEXT';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for date strings first (most specific)
|
|
133
|
+
if (isDateString(value)) {
|
|
134
|
+
return 'DATETIME';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check for URLs
|
|
138
|
+
if (isValidUrl(value)) {
|
|
139
|
+
return 'URL';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for email addresses
|
|
143
|
+
if (isEmail(value)) {
|
|
144
|
+
return 'EMAIL';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check for phone numbers
|
|
148
|
+
if (isPhoneNumber(value)) {
|
|
149
|
+
return 'PHONE';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for very long text (might be better as rich text)
|
|
153
|
+
if (value.length > 500) {
|
|
154
|
+
return 'RICHTEXT';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Default to TEXT for other strings
|
|
158
|
+
return 'TEXT';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Fallback for unknown types
|
|
162
|
+
return 'TEXT';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Analyzes multiple values to determine the best field type
|
|
167
|
+
* Useful when you have an array of values and want to determine the most appropriate type
|
|
168
|
+
* @param {Array} values - Array of values to analyze
|
|
169
|
+
* @returns {string} - The most appropriate Wix Data field type
|
|
170
|
+
*/
|
|
171
|
+
function detectFieldTypeFromMultipleValues(values) {
|
|
172
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
173
|
+
return 'TEXT';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Filter out null/undefined values for analysis
|
|
177
|
+
const validValues = values.filter(v => v !== null && v !== undefined);
|
|
178
|
+
|
|
179
|
+
if (validValues.length === 0) {
|
|
180
|
+
return 'TEXT';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Get types for all valid values
|
|
184
|
+
const types = validValues.map(detectFieldType);
|
|
185
|
+
const uniqueTypes = [...new Set(types)];
|
|
186
|
+
|
|
187
|
+
// If all values have the same type, use that type
|
|
188
|
+
if (uniqueTypes.length === 1) {
|
|
189
|
+
return uniqueTypes[0];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle mixed types - prioritize more specific types
|
|
193
|
+
const typePriority = {
|
|
194
|
+
BOOLEAN: 8,
|
|
195
|
+
NUMBER: 7,
|
|
196
|
+
DATETIME: 6,
|
|
197
|
+
URL: 5,
|
|
198
|
+
EMAIL: 4,
|
|
199
|
+
PHONE: 3,
|
|
200
|
+
ARRAY: 2,
|
|
201
|
+
OBJECT: 2,
|
|
202
|
+
RICHTEXT: 1,
|
|
203
|
+
TEXT: 0,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Find the highest priority type that appears
|
|
207
|
+
const sortedTypes = uniqueTypes.sort((a, b) => (typePriority[b] || 0) - (typePriority[a] || 0));
|
|
208
|
+
|
|
209
|
+
// If we have mixed primitive types with TEXT, prefer the more specific type
|
|
210
|
+
// unless it's a very small percentage of the data
|
|
211
|
+
const primaryType = sortedTypes[0];
|
|
212
|
+
const primaryTypeCount = types.filter(t => t === primaryType).length;
|
|
213
|
+
const primaryTypePercentage = primaryTypeCount / types.length;
|
|
214
|
+
|
|
215
|
+
// If the primary type represents at least 70% of the data, use it
|
|
216
|
+
if (primaryTypePercentage >= 0.7) {
|
|
217
|
+
return primaryType;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Otherwise, default to TEXT for mixed data
|
|
221
|
+
return 'TEXT';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
isValidUrl,
|
|
226
|
+
isDateString,
|
|
227
|
+
isEmail,
|
|
228
|
+
isPhoneNumber,
|
|
229
|
+
detectFieldType,
|
|
230
|
+
detectFieldTypeFromMultipleValues,
|
|
231
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const { insertNewTask, bulkInsertTasks } = require('../utils');
|
|
2
|
+
|
|
3
|
+
const { createChildTaskPayload } = require('./utils');
|
|
4
|
+
|
|
5
|
+
const scheduleChildren = ({
|
|
6
|
+
parentTaskId,
|
|
7
|
+
parentTaskData,
|
|
8
|
+
expectedChildTasks,
|
|
9
|
+
scheduledChildren,
|
|
10
|
+
}) => {
|
|
11
|
+
const scheduleSingleChild = ({ taskName, taskData, parentTaskId }) => {
|
|
12
|
+
const childTask = createChildTaskPayload({
|
|
13
|
+
taskName,
|
|
14
|
+
taskData,
|
|
15
|
+
parentTaskId,
|
|
16
|
+
});
|
|
17
|
+
return insertNewTask(childTask);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const scheduleChildrenInBulk = ({
|
|
21
|
+
parentTaskId,
|
|
22
|
+
parentTaskData: { previousTaskOutput: _previousTaskOutput, ...restParentData },
|
|
23
|
+
childTasks,
|
|
24
|
+
}) => {
|
|
25
|
+
const tasksToInsert = childTasks.map(({ name, data }) =>
|
|
26
|
+
createChildTaskPayload({
|
|
27
|
+
taskName: name,
|
|
28
|
+
taskData: { ...restParentData, ...data },
|
|
29
|
+
parentTaskId,
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
//Host needs to keep in mind Velo limitations of bulkInsert 1000 items max and 500kb max item size
|
|
33
|
+
//Ref: https://dev.wix.com/docs/velo/apis/wix-data/bulk-insert
|
|
34
|
+
return bulkInsertTasks(tasksToInsert);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getPreviousChildOutput = childIndex => {
|
|
38
|
+
const isFirstChild = childIndex === 0 || !scheduledChildren?.length;
|
|
39
|
+
if (isFirstChild) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const lastScheduledChild = scheduledChildren[scheduledChildren.length - 1];
|
|
43
|
+
return lastScheduledChild?.data?.output;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
sequentially: childIndex =>
|
|
48
|
+
scheduleSingleChild({
|
|
49
|
+
taskName: expectedChildTasks[childIndex].name,
|
|
50
|
+
taskData: {
|
|
51
|
+
...parentTaskData,
|
|
52
|
+
...expectedChildTasks[childIndex].data,
|
|
53
|
+
previousTaskOutput: getPreviousChildOutput(childIndex),
|
|
54
|
+
},
|
|
55
|
+
parentTaskId,
|
|
56
|
+
}),
|
|
57
|
+
inBulk: () =>
|
|
58
|
+
scheduleChildrenInBulk({
|
|
59
|
+
parentTaskId,
|
|
60
|
+
parentTaskData,
|
|
61
|
+
childTasks: expectedChildTasks,
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
module.exports = { scheduleChildren };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const { getTaskConfig, getTaskScheduledChildren, getExpectedChildTasks } = require('../utils');
|
|
2
|
+
|
|
3
|
+
const { scheduleChildren } = require('./childScheduler');
|
|
4
|
+
const {
|
|
5
|
+
hasFailedChildAndMarkedParent,
|
|
6
|
+
hasUnfinishedChild,
|
|
7
|
+
areChildrenSucceededAndMarkedParent,
|
|
8
|
+
} = require('./parentStatusManager');
|
|
9
|
+
const { shouldScheduleNextChild: _shouldScheduleNextChild } = require('./utils');
|
|
10
|
+
const scheduleChildTasksAndUpdateParent = async (task, tasksConfig) => {
|
|
11
|
+
const { _id: parentTaskId, name: parentTaskName, data: parentTaskData } = task;
|
|
12
|
+
const parentTaskConfig = getTaskConfig(parentTaskName, tasksConfig);
|
|
13
|
+
const expectedChildTasks = getExpectedChildTasks(task, parentTaskConfig);
|
|
14
|
+
const scheduledChildren = await getTaskScheduledChildren(parentTaskId);
|
|
15
|
+
const childrenScheduler = scheduleChildren({
|
|
16
|
+
parentTaskId,
|
|
17
|
+
parentTaskData,
|
|
18
|
+
expectedChildTasks,
|
|
19
|
+
scheduledChildren,
|
|
20
|
+
});
|
|
21
|
+
if (!scheduledChildren.length) {
|
|
22
|
+
console.log(
|
|
23
|
+
`No scheduled children found for task with id ${parentTaskId}, started scheduling children`
|
|
24
|
+
);
|
|
25
|
+
if (parentTaskConfig.scheduleChildrenSequentially) {
|
|
26
|
+
console.log('scheduling children sequentially');
|
|
27
|
+
return childrenScheduler.sequentially(0);
|
|
28
|
+
} else {
|
|
29
|
+
console.log('scheduling children in bulk');
|
|
30
|
+
return childrenScheduler.inBulk();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (hasUnfinishedChild(scheduledChildren)) {
|
|
35
|
+
console.log(
|
|
36
|
+
`[Children Unfinished]: Found unfinished children for task with id ${parentTaskId}, quitting parent processing`
|
|
37
|
+
);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (await hasFailedChildAndMarkedParent(task, scheduledChildren)) {
|
|
42
|
+
console.log(
|
|
43
|
+
`[Children Failed]: Found Failed children for task with id ${parentTaskId}, quitting parent processing`
|
|
44
|
+
);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
await areChildrenSucceededAndMarkedParent({
|
|
50
|
+
parent: task,
|
|
51
|
+
scheduledChildren: scheduledChildren,
|
|
52
|
+
expectedChildrenCount: expectedChildTasks.length,
|
|
53
|
+
})
|
|
54
|
+
) {
|
|
55
|
+
console.log(
|
|
56
|
+
`[Children Completed Successfully]: All children for task with id ${parentTaskId} completed Successfully, quitting parent processing.`
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const shouldScheduleNextChild = _shouldScheduleNextChild({
|
|
61
|
+
parentTaskConfig,
|
|
62
|
+
scheduledChildren,
|
|
63
|
+
expectedChildTasks,
|
|
64
|
+
});
|
|
65
|
+
if (shouldScheduleNextChild) {
|
|
66
|
+
const toScheduleChildIndex = scheduledChildren.length;
|
|
67
|
+
console.log(
|
|
68
|
+
`[Sequential Scheduling]: Scheduling child with index ${toScheduleChildIndex} for task with id ${parentTaskId}`
|
|
69
|
+
);
|
|
70
|
+
return childrenScheduler.sequentially(toScheduleChildIndex);
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
`[scheduleChildTasksAndUpdateParent] - Something went wrong with task: ${task.name} this line should not be reached`
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
module.exports = { scheduleChildTasksAndUpdateParent };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { TASK_STATUS } = require('../consts');
|
|
2
|
+
const { markTask } = require('../utils');
|
|
3
|
+
|
|
4
|
+
const { isPermanentlyFailed, isCompletedSuccessfully, isUnfinishedTask } = require('./utils');
|
|
5
|
+
|
|
6
|
+
const hasFailedChildAndMarkedParent = async (parent, children) => {
|
|
7
|
+
const failedChild = children.find(child => isPermanentlyFailed(child));
|
|
8
|
+
if (failedChild) {
|
|
9
|
+
console.log('Child task failed, so marking parent as failed!!');
|
|
10
|
+
await markTask({
|
|
11
|
+
task: parent,
|
|
12
|
+
status: TASK_STATUS.FAILED,
|
|
13
|
+
error: failedChild.error,
|
|
14
|
+
});
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const hasUnfinishedChild = children => children.some(child => isUnfinishedTask(child));
|
|
21
|
+
|
|
22
|
+
const areChildrenSucceededAndMarkedParent = async ({
|
|
23
|
+
parent,
|
|
24
|
+
scheduledChildren,
|
|
25
|
+
expectedChildrenCount,
|
|
26
|
+
}) => {
|
|
27
|
+
const allChildrenSucceeded =
|
|
28
|
+
scheduledChildren.length === expectedChildrenCount &&
|
|
29
|
+
scheduledChildren.every(isCompletedSuccessfully);
|
|
30
|
+
if (allChildrenSucceeded) {
|
|
31
|
+
await markTask({ task: parent, status: TASK_STATUS.SUCCESS });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
hasFailedChildAndMarkedParent,
|
|
39
|
+
hasUnfinishedChild,
|
|
40
|
+
areChildrenSucceededAndMarkedParent,
|
|
41
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const { TASK_TYPE, TASK_STATUS, TASK_MAX_TRIES } = require('../consts');
|
|
2
|
+
|
|
3
|
+
const isCompletedSuccessfully = task =>
|
|
4
|
+
[TASK_STATUS.SUCCESS, TASK_STATUS.SKIPPED].includes(task.status);
|
|
5
|
+
|
|
6
|
+
const isPermanentlyFailed = task =>
|
|
7
|
+
task.status === TASK_STATUS.FAILED && task.amountOfRetries === TASK_MAX_TRIES;
|
|
8
|
+
const isFinishedTask = task => isCompletedSuccessfully(task) || isPermanentlyFailed(task);
|
|
9
|
+
|
|
10
|
+
const isUnfinishedTask = task => !isCompletedSuccessfully(task) && !isPermanentlyFailed(task);
|
|
11
|
+
const createChildTaskPayload = ({ taskName, taskData, parentTaskId }) => ({
|
|
12
|
+
name: taskName,
|
|
13
|
+
data: taskData,
|
|
14
|
+
type: TASK_TYPE.SCHEDULED,
|
|
15
|
+
parentTaskId,
|
|
16
|
+
});
|
|
17
|
+
const shouldScheduleNextChild = ({ parentTaskConfig, scheduledChildren, expectedChildTasks }) =>
|
|
18
|
+
parentTaskConfig.scheduleChildrenSequentially &&
|
|
19
|
+
scheduledChildren.length < expectedChildTasks.length;
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
createChildTaskPayload,
|
|
23
|
+
isPermanentlyFailed,
|
|
24
|
+
isCompletedSuccessfully,
|
|
25
|
+
isFinishedTask,
|
|
26
|
+
isUnfinishedTask,
|
|
27
|
+
shouldScheduleNextChild,
|
|
28
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const { wixData, wixCollections } = require('../elevated-modules');
|
|
2
|
+
|
|
3
|
+
const { detectFieldType } = require('./fieldTypeDetector');
|
|
4
|
+
const { createCollectionIfMissing } = require('./utils');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a results collection for a specific task if it doesn't exist
|
|
8
|
+
* @param {string} taskName - The name of the task
|
|
9
|
+
* @returns {Promise<string>} - The collection ID
|
|
10
|
+
*/
|
|
11
|
+
function createResultsCollectionIfMissing(taskName) {
|
|
12
|
+
const collectionId = `${taskName}_results`;
|
|
13
|
+
|
|
14
|
+
const resultsFields = [
|
|
15
|
+
{ key: 'taskId', type: 'TEXT' },
|
|
16
|
+
{ key: 'taskName', type: 'TEXT' },
|
|
17
|
+
{ key: 'status', type: 'TEXT' },
|
|
18
|
+
{ key: 'result', type: 'OBJECT' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
return createCollectionIfMissing(collectionId, resultsFields, null, 'Results');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adds fields dynamically to a results collection based on the result object structure
|
|
26
|
+
* @param {string} collectionId - The collection ID
|
|
27
|
+
* @param {Object} result - The result object to analyze for fields
|
|
28
|
+
*/
|
|
29
|
+
async function addDynamicFieldsToCollection(collectionId, result) {
|
|
30
|
+
if (!result || typeof result !== 'object') {
|
|
31
|
+
console.log(`[addDynamicFieldsToCollection] No valid result object to analyze for fields`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const collection = await wixCollections.getDataCollection(collectionId);
|
|
37
|
+
const existingFields = new Set(collection.fields.map(field => field.key));
|
|
38
|
+
const newFields = [];
|
|
39
|
+
|
|
40
|
+
Object.keys(result).forEach(key => {
|
|
41
|
+
if (!existingFields.has(key)) {
|
|
42
|
+
const value = result[key];
|
|
43
|
+
const fieldType = detectFieldType(value);
|
|
44
|
+
newFields.push({ key, type: fieldType });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (newFields.length > 0) {
|
|
49
|
+
console.log(
|
|
50
|
+
`[addDynamicFieldsToCollection] Adding ${newFields.length} new fields to collection ${collectionId}`
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const updatedFields = [...collection.fields, ...newFields];
|
|
54
|
+
await wixCollections.updateDataCollection({
|
|
55
|
+
_id: collectionId,
|
|
56
|
+
revision: collection.revision,
|
|
57
|
+
fields: updatedFields,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
console.log(
|
|
61
|
+
`[addDynamicFieldsToCollection] Successfully added new fields: ${newFields.map(f => f.key).join(', ')}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// If updating fields fails, we'll continue anyway - the result will still be saved in the 'result' OBJECT field
|
|
66
|
+
console.error(
|
|
67
|
+
`[addDynamicFieldsToCollection] Failed to add dynamic fields to collection ${collectionId}: ${error.message}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Saves task results to a dedicated collection
|
|
74
|
+
* @param {Object} task - The task object
|
|
75
|
+
* @param {string} status - The task status
|
|
76
|
+
* @returns {Promise<string>} - The inserted result ID
|
|
77
|
+
*/
|
|
78
|
+
async function saveTaskResultToAdditionalCollection(task, status, result) {
|
|
79
|
+
try {
|
|
80
|
+
const { _id: taskId, name: taskName } = task;
|
|
81
|
+
|
|
82
|
+
console.log(`[saveTaskResult] Saving result for task ${taskName}`);
|
|
83
|
+
|
|
84
|
+
const collectionId = await createResultsCollectionIfMissing(taskName);
|
|
85
|
+
|
|
86
|
+
await addDynamicFieldsToCollection(collectionId, result);
|
|
87
|
+
|
|
88
|
+
const dataToInsert = {
|
|
89
|
+
taskId,
|
|
90
|
+
taskName,
|
|
91
|
+
status,
|
|
92
|
+
result,
|
|
93
|
+
// Add individual fields from result for easier querying
|
|
94
|
+
...(result && typeof result === 'object' ? result : {}),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const insertedResult = await wixData.insert(collectionId, dataToInsert);
|
|
98
|
+
|
|
99
|
+
console.log(
|
|
100
|
+
`[saveTaskResult] Successfully saved result for task ${taskName} with ID ${insertedResult._id}`
|
|
101
|
+
);
|
|
102
|
+
return insertedResult._id;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const errMsg = `[saveTaskResult] Failed to save result for task ${task.name}: ${error.message}`;
|
|
105
|
+
console.error(errMsg);
|
|
106
|
+
throw new Error(errMsg);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Checks if a task configuration has results saving enabled
|
|
112
|
+
* @param {Object} taskConfig - The task configuration
|
|
113
|
+
* @returns {boolean} - Whether results saving is enabled
|
|
114
|
+
*/
|
|
115
|
+
function shouldSaveResultsToAdditionalCollection(taskConfig) {
|
|
116
|
+
return taskConfig && taskConfig.saveResults === true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
createResultsCollectionIfMissing,
|
|
121
|
+
addDynamicFieldsToCollection,
|
|
122
|
+
saveTaskResultToAdditionalCollection,
|
|
123
|
+
shouldSaveResultsToAdditionalCollection,
|
|
124
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const { withDuration } = require('../../public/performance/duration'); //relative path to needed file, so we won't break site due to frontend modules imports inside backend
|
|
2
|
+
|
|
3
|
+
const { TASK_STATUS, CRON_JOB_MAX_DURATION_SEC } = require('./consts');
|
|
4
|
+
const { scheduleChildTasksAndUpdateParent } = require('./parentChildTasks/handler');
|
|
5
|
+
const {
|
|
6
|
+
saveTaskResultToAdditionalCollection,
|
|
7
|
+
shouldSaveResultsToAdditionalCollection,
|
|
8
|
+
} = require('./resultsSaver');
|
|
9
|
+
const {
|
|
10
|
+
getTaskConfig,
|
|
11
|
+
markTask,
|
|
12
|
+
insertNewTask,
|
|
13
|
+
getScheduledTasks,
|
|
14
|
+
isParentTask,
|
|
15
|
+
getTasksToProcess,
|
|
16
|
+
bulkInsertTasks,
|
|
17
|
+
} = require('./utils');
|
|
18
|
+
|
|
19
|
+
function taskManager() {
|
|
20
|
+
const processTask = (task, tasksConfig) =>
|
|
21
|
+
withDuration(
|
|
22
|
+
'processTask',
|
|
23
|
+
async () => {
|
|
24
|
+
const taskName = task.name;
|
|
25
|
+
const didFailBefore = task.status === TASK_STATUS.FAILED;
|
|
26
|
+
let saveResults = false;
|
|
27
|
+
try {
|
|
28
|
+
console.log(`[processTask] for ${taskName} Started`);
|
|
29
|
+
const taskConfig = getTaskConfig(taskName, tasksConfig);
|
|
30
|
+
saveResults = shouldSaveResultsToAdditionalCollection(taskConfig);
|
|
31
|
+
await markTask({ task, status: TASK_STATUS.IN_PROGRESS });
|
|
32
|
+
if (isParentTask(task, taskConfig)) {
|
|
33
|
+
await scheduleChildTasksAndUpdateParent(task, tasksConfig);
|
|
34
|
+
//TODO: handle collecting children outputs and use it, e.g csv generation
|
|
35
|
+
return task; // we return here, as we let upper line handle parent task status update;
|
|
36
|
+
}
|
|
37
|
+
const identifier = await taskConfig.getIdentifier(task);
|
|
38
|
+
if (taskConfig.shouldSkipCheck(identifier)) {
|
|
39
|
+
console.log(`[${taskName}] Task for identifier ${identifier} will be skipped`);
|
|
40
|
+
await markTask({
|
|
41
|
+
task,
|
|
42
|
+
status: TASK_STATUS.SKIPPED,
|
|
43
|
+
error: `${taskName} skipped`,
|
|
44
|
+
});
|
|
45
|
+
console.log(`[${taskName}] Task for identifier ${identifier} is skipped`);
|
|
46
|
+
return task;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = await taskConfig.process(identifier);
|
|
50
|
+
//setting the processing output to the task data, so it can be used by the parent task if needed
|
|
51
|
+
task.data.output = result;
|
|
52
|
+
|
|
53
|
+
await markTask({ task, status: TASK_STATUS.SUCCESS });
|
|
54
|
+
|
|
55
|
+
if (saveResults) {
|
|
56
|
+
await saveTaskResultToAdditionalCollection(task, TASK_STATUS.SUCCESS, result);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(
|
|
60
|
+
`[processTask] for ${taskName} Task with identifier ${identifier} completed successfully`
|
|
61
|
+
);
|
|
62
|
+
return task;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const errMsg = `[processTask] for ${taskName} Task failed with error: ${error}`;
|
|
65
|
+
console.error(errMsg);
|
|
66
|
+
const amountOfRetries = didFailBefore ? task.amountOfRetries + 1 : task.amountOfRetries; // this is the first time it fails
|
|
67
|
+
await markTask({
|
|
68
|
+
task,
|
|
69
|
+
error,
|
|
70
|
+
status: TASK_STATUS.FAILED,
|
|
71
|
+
amountOfRetries,
|
|
72
|
+
});
|
|
73
|
+
if (saveResults) {
|
|
74
|
+
await saveTaskResultToAdditionalCollection(task, TASK_STATUS.FAILED, { error });
|
|
75
|
+
}
|
|
76
|
+
throw new Error(errMsg);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
console
|
|
80
|
+
);
|
|
81
|
+
/**
|
|
82
|
+
* @description Processing tasks based on how many the cron job tick can handle and running them sequentially to be as safe as possible
|
|
83
|
+
* */
|
|
84
|
+
const processTasksBasedOnVeloLimit = async (scheduledTasks, tasksConfig) => {
|
|
85
|
+
console.log(
|
|
86
|
+
`processTasksBasedOnVeloLimit: ${JSON.stringify(scheduledTasks)} ${JSON.stringify(tasksConfig)}`
|
|
87
|
+
);
|
|
88
|
+
const toProcessTasks = getTasksToProcess({
|
|
89
|
+
scheduledTasks,
|
|
90
|
+
tasksConfig,
|
|
91
|
+
maxDuration: CRON_JOB_MAX_DURATION_SEC,
|
|
92
|
+
});
|
|
93
|
+
console.log(`processTasksBasedOnVeloLimit: toProcessTasks: ${JSON.stringify(toProcessTasks)}`);
|
|
94
|
+
console.log(
|
|
95
|
+
`[processTasksBasedOnVeloLimit] started processing: ${toProcessTasks.length} tasks`
|
|
96
|
+
);
|
|
97
|
+
for (const task of toProcessTasks) {
|
|
98
|
+
// want to run tasks sequentially, but still don't want if one fails to stop others from running to gain the most from each job tick
|
|
99
|
+
try {
|
|
100
|
+
await processTask(task, tasksConfig);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(
|
|
103
|
+
`[processTasksBasedOnVeloLimit] failed to process task ${JSON.stringify(
|
|
104
|
+
task
|
|
105
|
+
)} with error: ${error}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
console.log(
|
|
110
|
+
`[processTasksBasedOnVeloLimit] finished processing: ${toProcessTasks.length} tasks`
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
const runScheduledTasks = async tasksConfig => {
|
|
114
|
+
try {
|
|
115
|
+
const scheduledTasks = await getScheduledTasks();
|
|
116
|
+
console.log(`runScheduledTasks: scheduledTasks: ${JSON.stringify(scheduledTasks)}`);
|
|
117
|
+
console.log(`runScheduledTasks: tasksConfig: ${JSON.stringify(tasksConfig)}`);
|
|
118
|
+
if (scheduledTasks.length) {
|
|
119
|
+
await processTasksBasedOnVeloLimit(scheduledTasks, tasksConfig);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(
|
|
122
|
+
'[runScheduledTasks] No Tasks to process',
|
|
123
|
+
JSON.stringify({ scheduledTasksCount: scheduledTasks.length })
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`[runScheduledTasks] failed with error: ${error}`);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
schedule: insertNewTask,
|
|
134
|
+
scheduleInBulk: bulkInsertTasks,
|
|
135
|
+
processTask,
|
|
136
|
+
runScheduledTasks: (...args) =>
|
|
137
|
+
withDuration('runScheduledTasks', () => runScheduledTasks(...args), console),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { taskManager };
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const { COLLECTIONS, COLLECTIONS_FIELDS } = require('../../public/consts'); // need relative import to not break site due to frontend modules imports inside backend
|
|
2
|
+
const { wixData, wixCollections } = require('../elevated-modules');
|
|
3
|
+
|
|
4
|
+
const { TASK_STATUS, TASK_TYPE, TASK_MAX_TRIES } = require('./consts');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generic function to create a collection if it doesn't exist
|
|
8
|
+
* @param {string} collectionId - The ID of the collection to create
|
|
9
|
+
* @param {Array} fields - Array of field definitions for the collection
|
|
10
|
+
* @param {Object} permissions - Permissions object for the collection
|
|
11
|
+
* @param {string} collectionType - Description of the collection type for logging
|
|
12
|
+
* @returns {Promise<string>} - The collection ID
|
|
13
|
+
*/
|
|
14
|
+
async function createCollectionIfMissing(
|
|
15
|
+
collectionId,
|
|
16
|
+
fields,
|
|
17
|
+
permissions = null,
|
|
18
|
+
collectionType = 'Collection'
|
|
19
|
+
) {
|
|
20
|
+
console.log(`[createCollectionIfMissing] Checking for collection with ID: ${collectionId}`);
|
|
21
|
+
|
|
22
|
+
const defaultPermissions = { insert: 'ADMIN', update: 'ADMIN', remove: 'ADMIN', read: 'ADMIN' };
|
|
23
|
+
const collectionPermissions = permissions || defaultPermissions;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// try to get the collection by id, if it doesn't exist, this will throw an error
|
|
27
|
+
await wixCollections.getDataCollection(collectionId);
|
|
28
|
+
console.log(
|
|
29
|
+
`[createCollectionIfMissing] ${collectionType} collection ${collectionId} already exists`
|
|
30
|
+
);
|
|
31
|
+
return collectionId;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
if (e.message.includes('WDE0025')) {
|
|
34
|
+
// an error that indicates that the collection does not exist.
|
|
35
|
+
console.log(
|
|
36
|
+
`[createCollectionIfMissing] ${collectionType} collection ${collectionId} does not exist. Creating...`
|
|
37
|
+
);
|
|
38
|
+
const createDataCollectionOptions = {
|
|
39
|
+
id: collectionId,
|
|
40
|
+
fields,
|
|
41
|
+
permissions: collectionPermissions,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (collectionType === 'singleItem') {
|
|
45
|
+
createDataCollectionOptions.plugins = [
|
|
46
|
+
{
|
|
47
|
+
type: "SINGLE_ITEM",
|
|
48
|
+
singleItemOptions: {},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
await wixCollections.createDataCollection(createDataCollectionOptions);
|
|
53
|
+
console.log(
|
|
54
|
+
`[createCollectionIfMissing] ${collectionType} collection ${collectionId} created successfully`
|
|
55
|
+
);
|
|
56
|
+
return collectionId;
|
|
57
|
+
} else {
|
|
58
|
+
// unexpected error
|
|
59
|
+
console.error(
|
|
60
|
+
`[createCollectionIfMissing] While trying to get collection with id: ${collectionId} an unexpected error occurred: ${e}`
|
|
61
|
+
);
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const getTaskConfig = (taskName, tasksConfig) => {
|
|
68
|
+
const taskConfig = Object.values(tasksConfig).find(t => t.name === taskName);
|
|
69
|
+
if (!taskConfig) {
|
|
70
|
+
throw new Error(`unknown task name ${taskName}`);
|
|
71
|
+
}
|
|
72
|
+
return taskConfig;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const updateTask = async task => {
|
|
76
|
+
try {
|
|
77
|
+
await wixData.update(COLLECTIONS.TASKS, task);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const errMsg = `[updateTask] for ${JSON.stringify(task)} failed with error: ${err.message}`;
|
|
80
|
+
console.error(errMsg);
|
|
81
|
+
throw new Error(errMsg);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const markTask = async ({ task, status, error, amountOfRetries }) => {
|
|
85
|
+
const toMarkTask = {
|
|
86
|
+
...task,
|
|
87
|
+
...(status && { status }),
|
|
88
|
+
...(error && { error: error.message ?? error }),
|
|
89
|
+
...(amountOfRetries !== undefined && { amountOfRetries }),
|
|
90
|
+
};
|
|
91
|
+
await updateTask(toMarkTask);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const validateTask = (task, methodName) => {
|
|
95
|
+
if (
|
|
96
|
+
typeof task.name !== 'string' ||
|
|
97
|
+
!task.name ||
|
|
98
|
+
typeof task.data !== 'object' ||
|
|
99
|
+
!task.data ||
|
|
100
|
+
typeof task.type !== 'string' ||
|
|
101
|
+
!task.type ||
|
|
102
|
+
(task.parentTaskId !== undefined &&
|
|
103
|
+
(typeof task.parentTaskId !== 'string' || !task.parentTaskId))
|
|
104
|
+
) {
|
|
105
|
+
throw new Error(`[${methodName}] Invalid params: ${JSON.stringify(task)}`);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const insertNewTask = async ({ name, data, type, parentTaskId }) => {
|
|
110
|
+
await createCollectionIfMissing(COLLECTIONS.TASKS, COLLECTIONS_FIELDS.TASKS);
|
|
111
|
+
try {
|
|
112
|
+
validateTask({ name, data, type, parentTaskId }, 'insertNewTask');
|
|
113
|
+
console.log('inserting pending task', name);
|
|
114
|
+
const insertedTask = await wixData.insert(COLLECTIONS.TASKS, {
|
|
115
|
+
name,
|
|
116
|
+
parentTaskId,
|
|
117
|
+
data,
|
|
118
|
+
status: TASK_STATUS.PENDING,
|
|
119
|
+
type,
|
|
120
|
+
amountOfRetries: 0,
|
|
121
|
+
});
|
|
122
|
+
console.log(`Task ${name} inserted successfully`);
|
|
123
|
+
return insertedTask._id;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const errMsg = `[insertNewTask] Failed for Task ${name} with error ${err.message}`;
|
|
126
|
+
console.error(errMsg);
|
|
127
|
+
throw new Error(errMsg);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const bulkInsertTasks = async tasks => {
|
|
131
|
+
await createCollectionIfMissing(COLLECTIONS.TASKS, COLLECTIONS_FIELDS.TASKS);
|
|
132
|
+
|
|
133
|
+
// Validate tasks before attempting database operations
|
|
134
|
+
const toInsertTasks = tasks.map(task => {
|
|
135
|
+
validateTask(task, 'bulkInsertTasks');
|
|
136
|
+
return {
|
|
137
|
+
...task,
|
|
138
|
+
status: TASK_STATUS.PENDING,
|
|
139
|
+
amountOfRetries: 0,
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const insertedTasks = await wixData.bulkInsert(COLLECTIONS.TASKS, toInsertTasks);
|
|
145
|
+
return insertedTasks.insertedItemIds;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const errMsg = `[bulkInsertTasks] Failed for Tasks with error ${err.message}`;
|
|
148
|
+
console.error(errMsg);
|
|
149
|
+
throw new Error(errMsg);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const getExpectedChildTasks = (task, taskConfig) => {
|
|
154
|
+
const childTasks =
|
|
155
|
+
typeof taskConfig.childTasks === 'function'
|
|
156
|
+
? taskConfig.childTasks(task)
|
|
157
|
+
: taskConfig.childTasks;
|
|
158
|
+
return childTasks;
|
|
159
|
+
};
|
|
160
|
+
const isParentTask = (task, taskConfig) => {
|
|
161
|
+
if (!('childTasks' in taskConfig)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const childTasks = getExpectedChildTasks(task, taskConfig);
|
|
165
|
+
if (!Array.isArray(childTasks) || childTasks.length === 0) {
|
|
166
|
+
throw new Error('taskConfig.childTasks must be a non-empty array if defined');
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
};
|
|
170
|
+
const filterScheduledTasksByStatus = (tasks, tasksConfig) => {
|
|
171
|
+
console.log(
|
|
172
|
+
`filterScheduledTasksByStatus: ${JSON.stringify(tasks)} ${JSON.stringify(tasksConfig)}`
|
|
173
|
+
);
|
|
174
|
+
const filtered = tasks.filter(task => {
|
|
175
|
+
if (task.status === TASK_STATUS.IN_PROGRESS) {
|
|
176
|
+
//Only include parent tasks that are in progress
|
|
177
|
+
console.log(`filterScheduledTasksByStatus: task: ${JSON.stringify(task)}`);
|
|
178
|
+
console.log(`filterScheduledTasksByStatus: tasksConfig: ${JSON.stringify(tasksConfig)}`);
|
|
179
|
+
return isParentTask(task, tasksConfig[task.name]);
|
|
180
|
+
}
|
|
181
|
+
return [TASK_STATUS.PENDING, TASK_STATUS.FAILED].includes(task.status);
|
|
182
|
+
});
|
|
183
|
+
console.log(`filterScheduledTasksByStatus: filtered: ${JSON.stringify(filtered)}`);
|
|
184
|
+
return filtered;
|
|
185
|
+
};
|
|
186
|
+
const getScheduledTasks = async () => {
|
|
187
|
+
try {
|
|
188
|
+
const baseQuery = wixData.query(COLLECTIONS.TASKS);
|
|
189
|
+
const scheduledTasksQuery = baseQuery
|
|
190
|
+
.eq('type', TASK_TYPE.SCHEDULED)
|
|
191
|
+
.hasSome('status', [TASK_STATUS.PENDING, TASK_STATUS.FAILED, TASK_STATUS.IN_PROGRESS]);
|
|
192
|
+
const eventBasedTasksQuery = baseQuery
|
|
193
|
+
.eq('type', TASK_TYPE.EVENT)
|
|
194
|
+
.eq('status', TASK_STATUS.FAILED);
|
|
195
|
+
return await scheduledTasksQuery
|
|
196
|
+
.or(eventBasedTasksQuery)
|
|
197
|
+
.lt('amountOfRetries', TASK_MAX_TRIES)
|
|
198
|
+
.ascending('_updatedDate')
|
|
199
|
+
.limit(1000)
|
|
200
|
+
.find()
|
|
201
|
+
.then(result => result.items);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error(`[getScheduledTasks] failed with error: ${error.message}`);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const getTaskScheduledChildren = async parentTaskId => {
|
|
207
|
+
try {
|
|
208
|
+
return await wixData
|
|
209
|
+
.query(COLLECTIONS.TASKS)
|
|
210
|
+
.eq('parentTaskId', parentTaskId)
|
|
211
|
+
.ascending('_createdDate')
|
|
212
|
+
.limit(1000)
|
|
213
|
+
.find()
|
|
214
|
+
.then(result => result.items);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error(
|
|
217
|
+
`[getTaskScheduledChildren] failed for task [${parentTaskId}] with error: ${error.message}`
|
|
218
|
+
);
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const getTasksToProcess = ({ scheduledTasks, tasksConfig, maxDuration }) => {
|
|
224
|
+
console.log('getTasksToProcess:', { scheduledTasks, tasksConfig, maxDuration });
|
|
225
|
+
const tasksToProcess = [];
|
|
226
|
+
let totalDuration = 0;
|
|
227
|
+
console.log(`tasksConfig is : ${JSON.stringify(tasksConfig)}`);
|
|
228
|
+
const filteredScheduledTasks = filterScheduledTasksByStatus(scheduledTasks, tasksConfig);
|
|
229
|
+
console.log(`filteredScheduledTasks is : ${JSON.stringify(filteredScheduledTasks)}`);
|
|
230
|
+
for (const task of filteredScheduledTasks) {
|
|
231
|
+
console.log(`task is : ${JSON.stringify(task)}`);
|
|
232
|
+
const taskConfig = getTaskConfig(task.name, tasksConfig);
|
|
233
|
+
console.log(`taskConfig is : ${JSON.stringify(taskConfig)}`);
|
|
234
|
+
const estimated = taskConfig?.estimatedDurationSec;
|
|
235
|
+
if (!estimated) {
|
|
236
|
+
const errMsg = `[getTasksToProcess] estimatedDurationSec is not defined for task ${task.name}`;
|
|
237
|
+
console.error(errMsg);
|
|
238
|
+
throw new Error(errMsg);
|
|
239
|
+
}
|
|
240
|
+
const buffered = estimated * 1.5; // 1.5x buffer to be safe
|
|
241
|
+
|
|
242
|
+
if (totalDuration + buffered > maxDuration) break;
|
|
243
|
+
totalDuration += buffered;
|
|
244
|
+
tasksToProcess.push(task);
|
|
245
|
+
}
|
|
246
|
+
console.log('getTasksToProcess: tasksToProcess:', tasksToProcess);
|
|
247
|
+
return tasksToProcess;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
createCollectionIfMissing,
|
|
252
|
+
getTaskConfig,
|
|
253
|
+
updateTask,
|
|
254
|
+
markTask,
|
|
255
|
+
insertNewTask,
|
|
256
|
+
bulkInsertTasks,
|
|
257
|
+
getScheduledTasks,
|
|
258
|
+
getTaskScheduledChildren,
|
|
259
|
+
isParentTask,
|
|
260
|
+
getTasksToProcess,
|
|
261
|
+
getExpectedChildTasks,
|
|
262
|
+
};
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "psdev-task-manager",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Task manager library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"task-manager"
|
|
@@ -19,5 +19,9 @@
|
|
|
19
19
|
"main": "index.js",
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "jest"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@wix/data": "^1.0.273",
|
|
25
|
+
"@wix/essentials": "^0.1.27"
|
|
22
26
|
}
|
|
23
27
|
}
|
package/public/consts.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const COLLECTIONS = {
|
|
2
|
+
TASKS: 'Tasks',
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
const COLLECTIONS_FIELDS = {
|
|
6
|
+
TASKS: [
|
|
7
|
+
{ key: 'name', type: 'TEXT' },
|
|
8
|
+
{ key: 'data', type: 'OBJECT' },
|
|
9
|
+
{ key: 'type', type: 'TEXT', enum: ['event', 'scheduled'] },
|
|
10
|
+
{
|
|
11
|
+
key: 'status',
|
|
12
|
+
type: 'TEXT',
|
|
13
|
+
enum: ['pending', 'in_progress', 'success', 'skipped', 'failed'],
|
|
14
|
+
},
|
|
15
|
+
{ key: 'error', type: 'TEXT' },
|
|
16
|
+
{ key: 'amountOfRetries', type: 'NUMBER' },
|
|
17
|
+
{
|
|
18
|
+
key: 'parentTaskId',
|
|
19
|
+
type: 'REFERENCE',
|
|
20
|
+
typeMetadata: { reference: { referencedCollectionId: 'Tasks' } },
|
|
21
|
+
},
|
|
22
|
+
{ key: 'Task Result', type: 'TEXT' },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
COLLECTIONS,
|
|
28
|
+
COLLECTIONS_FIELDS,
|
|
29
|
+
};
|
package/public/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
async function withDuration(functionName, func, logger) {
|
|
2
|
+
const start = Date.now();
|
|
3
|
+
const result = await func();
|
|
4
|
+
logger.log(
|
|
5
|
+
`${functionName} duration: ${
|
|
6
|
+
Date.now() - start
|
|
7
|
+
}ms and in human readble format: ${(Date.now() - start) / 1000} s`
|
|
8
|
+
);
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
withDuration,
|
|
14
|
+
};
|