@cocreate/cron-jobs 1.2.1 → 1.4.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/.github/workflows/automated.yml +25 -38
- package/CHANGELOG.md +23 -0
- package/package.json +4 -11
- package/release.config.js +12 -4
- package/src/index.js +322 -327
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
name: Automated Workflow
|
|
2
|
+
|
|
2
3
|
on:
|
|
3
4
|
push:
|
|
4
5
|
branches:
|
|
5
6
|
- master
|
|
7
|
+
|
|
6
8
|
jobs:
|
|
7
9
|
about:
|
|
8
10
|
runs-on: ubuntu-latest
|
|
@@ -18,52 +20,37 @@ jobs:
|
|
|
18
20
|
with:
|
|
19
21
|
direction: overwrite-github
|
|
20
22
|
githubToken: "${{ secrets.GITHUB }}"
|
|
23
|
+
|
|
21
24
|
release:
|
|
22
25
|
runs-on: ubuntu-latest
|
|
23
26
|
steps:
|
|
24
27
|
- name: Checkout
|
|
25
|
-
uses: actions/checkout@
|
|
26
|
-
- name: Setup Node.js
|
|
27
|
-
uses: actions/setup-node@v3
|
|
28
|
+
uses: actions/checkout@v4
|
|
28
29
|
with:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
fetch-depth: 0 # Required so semantic-release can trace git tags/history
|
|
31
|
+
|
|
32
|
+
- name: Setup Node.js
|
|
33
|
+
uses: actions/setup-node@v4
|
|
33
34
|
with:
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
node-version: 22
|
|
36
|
+
|
|
37
|
+
- name: Install Semantic Release & Plugins
|
|
38
|
+
# Installs semantic-release and its plugins on the runner
|
|
39
|
+
run: |
|
|
40
|
+
npm install -g semantic-release \
|
|
41
|
+
@semantic-release/changelog \
|
|
42
|
+
@semantic-release/npm \
|
|
43
|
+
@semantic-release/github \
|
|
36
44
|
@semantic-release/git
|
|
37
|
-
|
|
45
|
+
|
|
46
|
+
- name: Run Semantic Release (Native)
|
|
47
|
+
id: semantic
|
|
48
|
+
# This will automatically pick up your export default config file in the repository root
|
|
49
|
+
run: npx semantic-release
|
|
38
50
|
env:
|
|
39
|
-
GITHUB_TOKEN: "${{ secrets.
|
|
51
|
+
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
40
52
|
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
|
53
|
+
|
|
41
54
|
outputs:
|
|
42
55
|
new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
|
|
43
|
-
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
|
44
|
-
upload:
|
|
45
|
-
runs-on: ubuntu-latest
|
|
46
|
-
needs: release
|
|
47
|
-
if: needs.release.outputs.new_release_published == 'true'
|
|
48
|
-
env:
|
|
49
|
-
VERSION: "${{ needs.release.outputs.new_release_version }}"
|
|
50
|
-
steps:
|
|
51
|
-
- name: Checkout
|
|
52
|
-
uses: actions/checkout@v3
|
|
53
|
-
- name: Setup Node.js
|
|
54
|
-
uses: actions/setup-node@v3
|
|
55
|
-
with:
|
|
56
|
-
node-version: 16
|
|
57
|
-
- name: Set npm registry auth
|
|
58
|
-
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
59
|
-
- name: Install dependencies
|
|
60
|
-
run: yarn install
|
|
61
|
-
|
|
62
|
-
- name: Set Environment Variables
|
|
63
|
-
run: |
|
|
64
|
-
echo "organization_id=${{ secrets.COCREATE_ORGANIZATION_ID }}" >> $GITHUB_ENV
|
|
65
|
-
echo "key=${{ secrets.COCREATE_KEY }}" >> $GITHUB_ENV
|
|
66
|
-
echo "host=${{ secrets.COCREATE_HOST }}" >> $GITHUB_ENV
|
|
67
|
-
- name: CoCreate Upload
|
|
68
|
-
run: coc upload
|
|
69
|
-
|
|
56
|
+
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
# [1.4.0](https://github.com/CoCreate-app/CoCreate-cron-jobs/compare/v1.3.0...v1.4.0) (2026-07-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* update automated workflow and release configuration for improved semantic release handling ([25d37c4](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/25d37c4fd2d2fb3ed40de34556fe13db627baa3a))
|
|
7
|
+
|
|
8
|
+
# [1.3.0](https://github.com/CoCreate-app/CoCreate-cron-jobs/compare/v1.2.1...v1.3.0) (2026-07-14)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* remove unnecessary console logs in init function for cleaner output ([50dac43](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/50dac43f973b7b90144233522d8a950ec6ce5057))
|
|
14
|
+
* removed post install ([101e9b3](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/101e9b379fba70b711b46bb4a046c892bdf21c2a))
|
|
15
|
+
* semantic version handling. Reorganize .gitignore for improved clarity and structure ([f211fa3](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/f211fa30c0c6dc3f90576ad28e15856f5abc45bd))
|
|
16
|
+
* update .gitignore to include package-lock.json and pnpm-lock.yaml ([4ca6545](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/4ca65451c8bd2f9d3c6562fc4908f54c69f3e1f5))
|
|
17
|
+
* update module export to ES6 syntax in release.config.js ([96909f4](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/96909f4deff179f2cb02170816680a8248fcf96e))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* add cron-parser dependency and implement cron job scheduling logic ([b80a44d](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/b80a44d0dd2ec4b7f7d41e171b9babacb0a1e13a))
|
|
23
|
+
|
|
1
24
|
## [1.2.1](https://github.com/CoCreate-app/CoCreate-cron-jobs/compare/v1.2.0...v1.2.1) (2025-05-01)
|
|
2
25
|
|
|
3
26
|
|
package/package.json
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/cron-jobs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A simple cron-jobs component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cron-jobs",
|
|
7
|
-
"cocreate",
|
|
8
|
-
"low-code-framework",
|
|
9
|
-
"no-code-framework",
|
|
10
|
-
"cocreatejs",
|
|
11
|
-
"cocreatejs-component",
|
|
12
|
-
"cocreate-framework",
|
|
13
|
-
"no-code",
|
|
14
7
|
"low-code",
|
|
15
|
-
"collaborative-framework",
|
|
16
8
|
"realtime",
|
|
17
9
|
"realtime-framework",
|
|
18
10
|
"collaboration",
|
|
@@ -26,8 +18,7 @@
|
|
|
26
18
|
"scripts": {
|
|
27
19
|
"start": "npx webpack --config webpack.config.js",
|
|
28
20
|
"build": "npx webpack --mode=production --config webpack.config.js",
|
|
29
|
-
"dev": "npx webpack --config webpack.config.js --watch"
|
|
30
|
-
"postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('coc --version', { stdio: 'ignore' }); } catch (error) { try { execSync('npm install -g @cocreate/cli', { stdio: 'inherit' }); console.log('Installed \"@cocreate/cli\" globally.'); } catch (error) { console.error('Failed to install \"@cocreate/cli\" globally:', error); } }\""
|
|
21
|
+
"dev": "npx webpack --config webpack.config.js --watch"
|
|
31
22
|
},
|
|
32
23
|
"repository": {
|
|
33
24
|
"type": "git",
|
|
@@ -43,8 +34,10 @@
|
|
|
43
34
|
"type": "GitHub Sponsors ❤",
|
|
44
35
|
"url": "https://github.com/sponsors/CoCreate-app"
|
|
45
36
|
},
|
|
37
|
+
"type": "module",
|
|
46
38
|
"main": "./src/index.js",
|
|
47
39
|
"dependencies": {
|
|
40
|
+
"cron-parser": "^4.9.0",
|
|
48
41
|
"node-html-parser": "^6.1.13"
|
|
49
42
|
}
|
|
50
43
|
}
|
package/release.config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export default {
|
|
2
2
|
dryRun: false,
|
|
3
3
|
branches: ["master"],
|
|
4
4
|
plugins: [
|
|
@@ -10,12 +10,20 @@ module.exports = {
|
|
|
10
10
|
changelogFile: "CHANGELOG.md",
|
|
11
11
|
},
|
|
12
12
|
],
|
|
13
|
-
"@semantic-release/npm",
|
|
14
|
-
"@semantic-release/github",
|
|
13
|
+
"@semantic-release/npm",
|
|
15
14
|
[
|
|
16
|
-
"@semantic-release/
|
|
15
|
+
"@semantic-release/github",
|
|
17
16
|
{
|
|
17
|
+
successComment: false,
|
|
18
|
+
failTitle: false,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"@semantic-release/git",
|
|
23
|
+
{
|
|
24
|
+
// Only stage and commit the changelog and package.json
|
|
18
25
|
assets: ["CHANGELOG.md", "package.json"],
|
|
26
|
+
message: "chore(release): ${nextRelease.version} [skip ci]",
|
|
19
27
|
},
|
|
20
28
|
],
|
|
21
29
|
],
|
package/src/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* You should have received a copy of the GNU Affero General Public License
|
|
15
15
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*
|
|
16
17
|
********************************************************************************/
|
|
17
18
|
|
|
18
19
|
// Commercial Licensing Information:
|
|
@@ -20,281 +21,264 @@
|
|
|
20
21
|
// you must obtain a commercial license from CoCreate LLC.
|
|
21
22
|
// For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
|
|
22
23
|
|
|
24
|
+
import cronParser from 'cron-parser';
|
|
25
|
+
import server from '@cocreate/server';
|
|
26
|
+
|
|
27
|
+
// ==========================================
|
|
28
|
+
// Module-Level Sandbox State (ESM Singleton)
|
|
29
|
+
// ==========================================
|
|
30
|
+
let crud = null;
|
|
31
|
+
let wsManager = null;
|
|
32
|
+
const scheduledJobs = {};
|
|
33
|
+
|
|
23
34
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @typedef {Object} CronJobEvent
|
|
30
|
-
* @property {string} organization_id - The ID of the organization for which the cron job is scheduled.
|
|
31
|
-
* @property {string} nextExecutionTime - The ISO 8601 timestamp for the next time the cron job is scheduled to execute.
|
|
32
|
-
* @property {Object} [schedule] - The scheduling information, including cronExpression, interval, and time-related settings.
|
|
33
|
-
* @property {string} [status] - The current status of the cron job, e.g., 'assigned', 'completed', 'failed'.
|
|
34
|
-
* @property {boolean} [active] - Whether the cron job is active or paused.
|
|
35
|
-
* @property {Object} [job] - The job that is to be executed, which could be $crud, $api, or other specific operations.
|
|
36
|
-
* @property {Object} [retryPolicy] - Retry settings for failed jobs, including retries and backoff strategy.
|
|
37
|
-
* @property {Object} [log] - An optional log of the cron job's last execution details, including status and message.
|
|
38
|
-
*
|
|
39
|
-
* Example Object:
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* {
|
|
43
|
-
* "organization_id": "652c8d62679eca03e0b116a7",
|
|
44
|
-
* "nextExecutionTime": "2024-09-10T02:00:00Z",
|
|
45
|
-
* "active": true, // Whether the cron job is active or paused
|
|
46
|
-
* "status": "assigned",
|
|
47
|
-
* "schedule": {
|
|
48
|
-
* "cronExpression": "0 2 * * *", // Standard cron expression (for Unix-based scheduling)
|
|
49
|
-
* "startBoundary": "2024-09-10T02:00:00Z", // The first scheduled execution time
|
|
50
|
-
* "endBoundary": "2025-09-10T02:00:00Z", // The end time after which no further executions will occur
|
|
51
|
-
* "interval": "P1D", // ISO 8601 format for repeating interval (e.g., repeat every day)
|
|
52
|
-
* "daysOfWeek": ["Monday", "Wednesday"], // Days of the week when the job should run
|
|
53
|
-
* "daysOfMonth": [1, 15], // Specific days of the month for execution
|
|
54
|
-
* "months": ["January", "July"], // Specific months for execution
|
|
55
|
-
* "skipDates": ["2024-12-25"], // Dates to skip execution
|
|
56
|
-
* "timezone": "UTC", // Timezone for scheduling
|
|
57
|
-
* "time": "02:00:00" // Specific time of day when the job should run
|
|
58
|
-
* },
|
|
59
|
-
* "job": {
|
|
60
|
-
* "$crud": {
|
|
61
|
-
* "method": "object.update",
|
|
62
|
-
* "array": "users",
|
|
63
|
-
* "query": { "stripe.account_id": "acc_123456" },
|
|
64
|
-
* "object": { "stripe.balance": 1000 }
|
|
65
|
-
* }
|
|
66
|
-
* },
|
|
67
|
-
* "retryPolicy": {
|
|
68
|
-
* "retries": 3, // Number of retries for failed jobs
|
|
69
|
-
* "retryInterval": "PT10M" // Interval between retries in ISO 8601 format
|
|
70
|
-
* },
|
|
71
|
-
* "backoffStrategy": {
|
|
72
|
-
* "type": "exponential", // Type of backoff strategy (exponential, linear, etc.)
|
|
73
|
-
* "initialDelay": "PT5M" // Initial delay before the first retry
|
|
74
|
-
* },
|
|
75
|
-
* "executionTimeout": "PT2H", // Timeout for job execution (ISO 8601 format)
|
|
76
|
-
* "log": {
|
|
77
|
-
* "timestamp": "2024-09-09T02:00:00Z",
|
|
78
|
-
* "status": "completed",
|
|
79
|
-
* "message": "Job executed successfully"
|
|
80
|
-
* }
|
|
81
|
-
* }
|
|
82
|
-
*
|
|
83
|
-
* @class CoCreateCronJobs
|
|
35
|
+
* Parses the schedule object and calculates the next execution time.
|
|
36
|
+
* Takes into account cronExpression, startTime, skipDates, time, daysOfWeek, daysOfMonth, months, endTime, etc.
|
|
37
|
+
* @param {Object} schedule - The schedule object containing cronExpression, startTime, etc.
|
|
38
|
+
* @returns {string|null} nextExecutionTime - Calculated execution timestamp in ISO format, or null if endTime has passed.
|
|
84
39
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
* @param {Object} crud - The CRUD module used to handle data operations.
|
|
89
|
-
*/
|
|
90
|
-
constructor(crud) {
|
|
91
|
-
this.crud = crud;
|
|
92
|
-
this.scheduledJobs = {};
|
|
93
|
-
|
|
94
|
-
// Listen for CRUD events related to cron jobs
|
|
95
|
-
process.on('crud-event', (event) => {
|
|
96
|
-
this.handleCrudEvent(event);
|
|
97
|
-
});
|
|
40
|
+
function getNextExecutionTime(schedule) {
|
|
41
|
+
const now = new Date();
|
|
42
|
+
let currentDateTime = now;
|
|
98
43
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}, 5 * 60 * 1000);
|
|
44
|
+
// Use startTime if it's greater than now, otherwise use now
|
|
45
|
+
if (schedule.startTime && new Date(schedule.startTime) > now) {
|
|
46
|
+
currentDateTime = new Date(schedule.startTime);
|
|
103
47
|
}
|
|
104
48
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*/
|
|
109
|
-
handleCrudEvent(data) {
|
|
110
|
-
// Check if the array is 'cron-job'
|
|
111
|
-
if (data.array !== 'cron-job') {
|
|
112
|
-
return; // Ignore non-cron-job events
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Iterate over the jobs in the data.object array
|
|
116
|
-
for (let i = 0; i < data.object.length; i++) {
|
|
117
|
-
if (data.method === 'object.delete' || data.object[i].active === false) {
|
|
118
|
-
const timeoutId = this.scheduledJobs[data.object[i]._id];
|
|
119
|
-
|
|
120
|
-
if (timeoutId) {
|
|
121
|
-
clearTimeout(timeoutId); // Cancel the timeout if it exists
|
|
122
|
-
delete this.scheduledJobs[data.object[i]._id]; // Remove the reference
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
data.object.splice(i, 1);
|
|
126
|
-
i--; // Adjust the index after removal
|
|
127
|
-
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const nextExecutionTime = this.getNextExecutionTime(data.object[i].schedule);
|
|
132
|
-
|
|
133
|
-
if (nextExecutionTime) {
|
|
134
|
-
// TODO: Compare executionTime if equal continue
|
|
135
|
-
if (this.scheduledJobs[data.object[i]._id])
|
|
136
|
-
continue
|
|
137
|
-
|
|
138
|
-
const timeUntilExecution = new Date(nextExecutionTime) - new Date();
|
|
139
|
-
|
|
140
|
-
data.object[i].nextExecutionTime = nextExecutionTime
|
|
141
|
-
|
|
142
|
-
// If the next execution is within the next 5 minutes, schedule the job
|
|
143
|
-
if (timeUntilExecution <= 5 * 60 * 1000 && timeUntilExecution > 0) {
|
|
144
|
-
data.object[i].status = 'assigned'
|
|
145
|
-
this.scheduleCronJob(data.object[i]);
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
data.object.splice(i, 1);
|
|
149
|
-
i--; // Adjust the index after removal
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
this.updateCronJob(data.object);
|
|
49
|
+
// Apply skipDates: Skip the date if it matches any in skipDates
|
|
50
|
+
if (schedule.skipDates && schedule.skipDates.includes(currentDateTime.toISOString().split('T')[0])) {
|
|
51
|
+
currentDateTime = new Date(currentDateTime.getTime() + 24 * 60 * 60 * 1000); // Skip to the next day
|
|
154
52
|
}
|
|
155
53
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*
|
|
160
|
-
* @param {Object} schedule - The schedule object containing cronExpression, startTime, etc.
|
|
161
|
-
* @returns {string|null} nextExecutionTime - The calculated next execution time in ISO 8601 format, or null if endTime has passed.
|
|
162
|
-
*/
|
|
163
|
-
getNextExecutionTime(schedule) {
|
|
164
|
-
const now = new Date();
|
|
165
|
-
let currentDateTime = now;
|
|
166
|
-
|
|
167
|
-
// Use startTime if it's greater than now, otherwise use now
|
|
168
|
-
if (schedule.startTime && new Date(schedule.startTime) > now) {
|
|
169
|
-
currentDateTime = new Date(schedule.startTime);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Apply skipDates: Skip the date if it matches any in skipDates
|
|
173
|
-
if (schedule.skipDates && schedule.skipDates.includes(currentDateTime.toISOString().split('T')[0])) {
|
|
174
|
-
currentDateTime = new Date(currentDateTime.getTime() + 24 * 60 * 60 * 1000); // Skip to the next day
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Apply daysOfWeek: If the current day is not in the allowed days, skip to the next valid day
|
|
178
|
-
if (schedule.daysOfWeek) {
|
|
179
|
-
const dayOfWeek = currentDateTime.toLocaleString('en-US', { weekday: 'long', timeZone: schedule.timezone || 'UTC' });
|
|
54
|
+
// Apply daysOfWeek: If the current day is not in the allowed days, skip to the next valid day
|
|
55
|
+
if (schedule.daysOfWeek) {
|
|
56
|
+
const dayOfWeek = currentDateTime.toLocaleString('en-US', { weekday: 'long', timeZone: schedule.timezone || 'UTC' });
|
|
180
57
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
58
|
+
while (!schedule.daysOfWeek.includes(dayOfWeek)) {
|
|
59
|
+
currentDateTime.setDate(currentDateTime.getDate() + 1); // Move to the next day
|
|
184
60
|
}
|
|
61
|
+
}
|
|
185
62
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
63
|
+
// Apply daysOfMonth: Ensure the next execution is on a valid day of the month
|
|
64
|
+
if (schedule.daysOfMonth) {
|
|
65
|
+
while (!schedule.daysOfMonth.includes(currentDateTime.getDate())) {
|
|
66
|
+
currentDateTime.setDate(currentDateTime.getDate() + 1); // Move to the next day
|
|
191
67
|
}
|
|
68
|
+
}
|
|
192
69
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
70
|
+
// Apply months: Ensure the next execution falls within one of the allowed months
|
|
71
|
+
if (schedule.months) {
|
|
72
|
+
const monthName = currentDateTime.toLocaleString('en-US', { month: 'long', timeZone: schedule.timezone || 'UTC' });
|
|
196
73
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
74
|
+
while (!schedule.months.includes(monthName)) {
|
|
75
|
+
currentDateTime.setMonth(currentDateTime.getMonth() + 1); // Move to the next month
|
|
200
76
|
}
|
|
77
|
+
}
|
|
201
78
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
79
|
+
// Apply time: Set the execution time of day if specified
|
|
80
|
+
if (schedule.time) {
|
|
81
|
+
const [hours, minutes, seconds] = schedule.time.split(':');
|
|
82
|
+
currentDateTime.setHours(hours, minutes, seconds);
|
|
83
|
+
}
|
|
207
84
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
85
|
+
// Apply endTime: Ensure the next execution time does not exceed the endTime
|
|
86
|
+
if (schedule.endTime && new Date(currentDateTime) > new Date(schedule.endTime)) {
|
|
87
|
+
return null; // No further executions after endTime
|
|
88
|
+
}
|
|
212
89
|
|
|
213
|
-
|
|
214
|
-
|
|
90
|
+
// Now run the cronParser to handle cronExpression (if present) or fallback to currentDateTime
|
|
91
|
+
if (schedule.cronExpression) {
|
|
92
|
+
try {
|
|
215
93
|
const interval = cronParser.parseExpression(schedule.cronExpression, {
|
|
216
94
|
currentDate: currentDateTime,
|
|
217
95
|
tz: schedule.timezone || 'UTC'
|
|
218
96
|
});
|
|
219
97
|
return interval.next().toISOString();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('[@cocreate/cron-jobs] Cron parsing failed:', err);
|
|
100
|
+
return null;
|
|
220
101
|
}
|
|
221
|
-
|
|
222
|
-
return currentDateTime.toISOString();
|
|
223
102
|
}
|
|
224
103
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
104
|
+
return currentDateTime.toISOString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Marks the cron job status inside the platform database.
|
|
109
|
+
* Normalizes input array payloads to preserve safety across organization contexts.
|
|
110
|
+
*/
|
|
111
|
+
async function updateCronJob(object) {
|
|
112
|
+
if (!crud) return;
|
|
113
|
+
try {
|
|
114
|
+
const cronJobs = Array.isArray(object) ? object : [object];
|
|
238
115
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
workerId: 'id',
|
|
249
|
-
organization_id: object.organization_id
|
|
116
|
+
for (const job of cronJobs) {
|
|
117
|
+
await crud.send({
|
|
118
|
+
method: 'object.update',
|
|
119
|
+
array: 'cron-jobs',
|
|
120
|
+
object: [job],
|
|
121
|
+
upsert: true,
|
|
122
|
+
organization_id: job.organization_id
|
|
123
|
+
});
|
|
124
|
+
console.log(`[@cocreate/cron-jobs] Cron job for org ${job.organization_id} updated.`);
|
|
250
125
|
}
|
|
251
126
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
127
|
+
if (cronJobs.length > 0 && cronJobs[0].organization_id) {
|
|
128
|
+
const firstJob = cronJobs[0];
|
|
129
|
+
await crud.send({
|
|
130
|
+
method: 'object.update',
|
|
131
|
+
array: 'organizations',
|
|
132
|
+
object: {
|
|
133
|
+
_id: firstJob.organization_id,
|
|
134
|
+
cronJobs
|
|
135
|
+
},
|
|
136
|
+
organization_id: process.env.organization_id
|
|
137
|
+
});
|
|
138
|
+
console.log(`[@cocreate/cron-jobs] Cron job list for org ${firstJob.organization_id} mapped.`);
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('[@cocreate/cron-jobs] Error updating cron job records:', error);
|
|
255
142
|
}
|
|
143
|
+
}
|
|
256
144
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
console.log(`Executing cron job for org ${object.organization_id}`);
|
|
145
|
+
/**
|
|
146
|
+
* Executes the scheduled cron job.
|
|
147
|
+
* @param {Object} object - The cron-job object containing job details, schedule parameters, etc.
|
|
148
|
+
*/
|
|
149
|
+
async function executeCronJob(object) {
|
|
150
|
+
console.log(`[@cocreate/cron-jobs] Executing cron job for org ${object.organization_id}`);
|
|
264
151
|
|
|
265
|
-
|
|
266
|
-
|
|
152
|
+
try {
|
|
153
|
+
if (!object.job && crud) {
|
|
154
|
+
const data = await crud.send({
|
|
267
155
|
method: 'object.read',
|
|
268
156
|
array: 'cron-job',
|
|
269
157
|
object: { _id: object._id },
|
|
270
158
|
organization_id: object.organization_id
|
|
271
|
-
})
|
|
272
|
-
object
|
|
273
|
-
|
|
159
|
+
});
|
|
160
|
+
if (data.object && data.object[0]) {
|
|
161
|
+
object = data.object[0];
|
|
162
|
+
}
|
|
163
|
+
console.log('[@cocreate/cron-jobs] Fetched cron job data:', object);
|
|
274
164
|
}
|
|
275
165
|
|
|
276
|
-
//
|
|
277
|
-
|
|
166
|
+
// Target Action Pipeline execution placeholder:
|
|
167
|
+
// TODO: execute cron using lazyloader.webhook() or server-native lazyloader routing
|
|
168
|
+
|
|
169
|
+
object.nextExecutionTime = getNextExecutionTime(object.schedule);
|
|
278
170
|
if (!object.nextExecutionTime) {
|
|
279
|
-
object.active = false
|
|
280
|
-
object.status = 'completed'
|
|
171
|
+
object.active = false;
|
|
172
|
+
object.status = 'completed';
|
|
173
|
+
} else {
|
|
174
|
+
// Re-queue and release the lock for the next interval run
|
|
175
|
+
object.status = 'scheduled';
|
|
176
|
+
object.serverId = null;
|
|
177
|
+
object.workerId = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await updateCronJob(object);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(`[@cocreate/cron-jobs] Error executing cron job ${object._id}:`, error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Schedules a cron job to be executed at a specific datetime.
|
|
188
|
+
* @param {Object} object - The cron-job configuration footprint.
|
|
189
|
+
*/
|
|
190
|
+
function scheduleCronJob(object) {
|
|
191
|
+
const delay = new Date(object.nextExecutionTime) - new Date();
|
|
192
|
+
|
|
193
|
+
if (delay <= 0) {
|
|
194
|
+
executeCronJob(object);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(`[@cocreate/cron-jobs] Scheduling cron job for org ${object.organization_id} in ${delay / 1000} seconds`);
|
|
199
|
+
|
|
200
|
+
const scheduledObj = {
|
|
201
|
+
_id: object._id,
|
|
202
|
+
nextExecutionTime: object.nextExecutionTime,
|
|
203
|
+
status: object.status,
|
|
204
|
+
clusterId: 'default-cluster',
|
|
205
|
+
serverId: server?.instanceId || server?.serverId || 'default-server',
|
|
206
|
+
workerId: server?.workerId !== undefined ? server.workerId : 'standalone',
|
|
207
|
+
organization_id: object.organization_id
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
scheduledJobs[object._id] = setTimeout(() => {
|
|
211
|
+
executeCronJob(scheduledObj);
|
|
212
|
+
}, delay);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handles CRUD events to schedule cron jobs dynamically.
|
|
217
|
+
* @param {Object} data - The data object emitted from CRUD operations.
|
|
218
|
+
*/
|
|
219
|
+
async function handleCrudEvent(data) {
|
|
220
|
+
if (data.array !== 'cron-job') {
|
|
221
|
+
return; // Ignore non-cron-job events
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Iterate over the jobs in the data.object array
|
|
225
|
+
for (let i = 0; i < data.object.length; i++) {
|
|
226
|
+
if (data.method === 'object.delete' || data.object[i].active === false) {
|
|
227
|
+
const timeoutId = scheduledJobs[data.object[i]._id];
|
|
228
|
+
|
|
229
|
+
if (timeoutId) {
|
|
230
|
+
clearTimeout(timeoutId); // Cancel the timeout if it exists
|
|
231
|
+
delete scheduledJobs[data.object[i]._id]; // Remove the reference
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
data.object.splice(i, 1);
|
|
235
|
+
i--; // Adjust the index after removal
|
|
236
|
+
continue;
|
|
281
237
|
}
|
|
282
238
|
|
|
283
|
-
|
|
239
|
+
const nextExecutionTime = getNextExecutionTime(data.object[i].schedule);
|
|
240
|
+
|
|
241
|
+
if (nextExecutionTime) {
|
|
242
|
+
// If already scheduled for the exact same block, skip duplicate scheduling
|
|
243
|
+
if (scheduledJobs[data.object[i]._id]) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const timeUntilExecution = new Date(nextExecutionTime) - new Date();
|
|
248
|
+
data.object[i].nextExecutionTime = nextExecutionTime;
|
|
249
|
+
|
|
250
|
+
// If the next execution is within the next 5 minutes, schedule the job
|
|
251
|
+
if (timeUntilExecution <= 5 * 60 * 1000 && timeUntilExecution > 0) {
|
|
252
|
+
// Claim the job locally and coordinate across nodes/cores
|
|
253
|
+
data.object[i].status = 'assigned';
|
|
254
|
+
data.object[i].serverId = server?.instanceId || server?.serverId || 'default-server';
|
|
255
|
+
data.object[i].workerId = server?.workerId !== undefined ? server.workerId : 'standalone';
|
|
256
|
+
scheduleCronJob(data.object[i]);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
data.object.splice(i, 1);
|
|
260
|
+
i--; // Adjust the index after removal
|
|
261
|
+
}
|
|
284
262
|
}
|
|
285
263
|
|
|
264
|
+
await updateCronJob(data.object);
|
|
265
|
+
}
|
|
286
266
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Polls the platform DB for cron jobs that are scheduled or potentially failed.
|
|
269
|
+
* Any worker process hosting active sockets/procedures for an organization can safely execute this.
|
|
270
|
+
*/
|
|
271
|
+
async function pollForCronJobs() {
|
|
272
|
+
if (!crud) return;
|
|
273
|
+
console.log(`[@cocreate/cron-jobs] [Worker ${server?.workerId}] Polling for active organizations' cron jobs...`);
|
|
292
274
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
275
|
+
const now = new Date();
|
|
276
|
+
const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
|
|
277
|
+
const oneMinuteAgo = new Date(now.getTime() - 1 * 60 * 1000); // 1 minute past due jobs
|
|
296
278
|
|
|
297
|
-
|
|
279
|
+
try {
|
|
280
|
+
// Query organizations.
|
|
281
|
+
const data = await crud.send({
|
|
298
282
|
method: 'object.read',
|
|
299
283
|
array: 'organizations',
|
|
300
284
|
filter: {
|
|
@@ -303,131 +287,142 @@ class CoCreateCronJobs {
|
|
|
303
287
|
{
|
|
304
288
|
$and: [
|
|
305
289
|
{ 'cron-jobs.nextExecutionTime': { $elemMatch: { $lte: fiveMinutesFromNow, $gte: now } } }, // Match jobs in array within 5 minutes
|
|
306
|
-
{ 'cron-jobs.status': { $elemMatch: { $ne: 'assigned' } } }, // Not already assigned
|
|
290
|
+
{ 'cron-jobs.status': { $elemMatch: { $ne: 'assigned' } } }, // Not already claimed/assigned
|
|
307
291
|
{ 'cron-jobs.active': { $elemMatch: { $ne: false } } } // Is active
|
|
308
292
|
]
|
|
309
293
|
},
|
|
310
294
|
{
|
|
311
295
|
$and: [
|
|
312
|
-
{ 'cron-jobs.nextExecutionTime': { $elemMatch: { $lt: oneMinuteAgo } } }, // Past due by 1 minute
|
|
313
|
-
{ 'cron-jobs.status': { $elemMatch: { $eq: 'assigned' } } }, //
|
|
314
|
-
{ 'cron-jobs.active': { $elemMatch: { $ne: false } } } // Is active
|
|
296
|
+
{ 'cron-jobs.nextExecutionTime': { $elemMatch: { $lt: oneMinuteAgo } } }, // Past due by 1 minute (possible crash recovery)
|
|
297
|
+
{ 'cron-jobs.status': { $elemMatch: { $eq: 'assigned' } } }, // Marked assigned but execution failed to clear/update it
|
|
298
|
+
{ 'cron-jobs.active': { $elemMatch: { $ne: false } } } // Is active
|
|
315
299
|
]
|
|
316
300
|
}
|
|
317
301
|
]
|
|
318
302
|
}
|
|
319
303
|
},
|
|
320
304
|
organization_id: process.env.organization_id
|
|
321
|
-
}).then(data => {
|
|
322
|
-
for (let i = 0; i < data.object.length; i++) {
|
|
323
|
-
const cronJobs = data.object[i].cronJobs;
|
|
324
|
-
if (!cronJobs)
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
// Check each cron job in the organization's `cron-jobs` array
|
|
328
|
-
for (let j = 0; j < cronJobs; j++) {
|
|
329
|
-
if (this.scheduledJobs[cronJobs[j]._id])
|
|
330
|
-
return
|
|
331
|
-
|
|
332
|
-
cronJobs[j].status = 'assigned'
|
|
333
|
-
|
|
334
|
-
// TODO: get clusterId, serverId and workerId
|
|
335
|
-
cronJobs[j].clusterId = 'id'
|
|
336
|
-
cronJobs[j].severId = 'id'
|
|
337
|
-
cronJobs[j].workerId = 'id'
|
|
338
|
-
cronJobs[j].organization_id = data.object[i].organization_id
|
|
339
|
-
|
|
340
|
-
this.scheduleCronJob(cronJobs[j]);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
this.updateCronJob(cronJobs);
|
|
344
|
-
}
|
|
345
|
-
}).catch(error => {
|
|
346
|
-
console.error('Error polling for cron jobs:', error);
|
|
347
305
|
});
|
|
348
|
-
}
|
|
349
306
|
|
|
350
|
-
|
|
351
|
-
* Marks the cron job as failed after it is detected as past due and not started.
|
|
352
|
-
*/
|
|
353
|
-
updateCronJob(object) {
|
|
354
|
-
if (!Array.isArray(object))
|
|
355
|
-
object = [object]
|
|
307
|
+
if (!data || !data.object) return;
|
|
356
308
|
|
|
357
|
-
for (let i = 0; i < object.length; i++) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
309
|
+
for (let i = 0; i < data.object.length; i++) {
|
|
310
|
+
const cronJobs = data.object[i].cronJobs;
|
|
311
|
+
if (!cronJobs) continue;
|
|
312
|
+
|
|
313
|
+
const organizationId = data.object[i].organization_id;
|
|
314
|
+
|
|
315
|
+
// Check each cron job in the organization's `cron-jobs` array
|
|
316
|
+
for (let j = 0; j < cronJobs.length; j++) {
|
|
317
|
+
if (scheduledJobs[cronJobs[j]._id]) continue;
|
|
318
|
+
|
|
319
|
+
// Set assignment properties to claim this job cluster-wide
|
|
320
|
+
cronJobs[j].status = 'assigned';
|
|
321
|
+
cronJobs[j].clusterId = 'default-cluster';
|
|
322
|
+
cronJobs[j].serverId = server?.instanceId || server?.serverId || 'default-server';
|
|
323
|
+
cronJobs[j].workerId = server?.workerId !== undefined ? server.workerId : 'standalone';
|
|
324
|
+
cronJobs[j].organization_id = organizationId;
|
|
369
325
|
|
|
326
|
+
scheduleCronJob(cronJobs[j]);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
await updateCronJob(cronJobs);
|
|
370
330
|
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error('[@cocreate/cron-jobs] Error polling for cron jobs:', error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
371
335
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Adds a cron job reference to the platform database.
|
|
338
|
+
* @param {string} organization_id - The organization ID.
|
|
339
|
+
* @param {string} datetime - The datetime of the cron job to add to the platform DB.
|
|
340
|
+
*/
|
|
341
|
+
async function addToPlatformDB(organization_id, datetime) {
|
|
342
|
+
if (!crud) return;
|
|
343
|
+
console.log(`[@cocreate/cron-jobs] Adding cron job reference to platform DB for org ${organization_id}`);
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
await crud.send({
|
|
347
|
+
method: 'object.read',
|
|
375
348
|
array: 'organizations',
|
|
376
|
-
object: {
|
|
377
|
-
_id: object.organization_id,
|
|
378
|
-
cronJobs
|
|
379
|
-
},
|
|
349
|
+
object: { _id: organization_id, nextCron: datetime },
|
|
380
350
|
organization_id: process.env.organization_id
|
|
381
|
-
}).then(() => {
|
|
382
|
-
console.log(`Cron job for org ${object.organization_id} updated.`);
|
|
383
|
-
}).catch(error => {
|
|
384
|
-
console.error(`Error updating cron job for org ${object.organization_id}:`, error);
|
|
385
351
|
});
|
|
352
|
+
console.log(`[@cocreate/cron-jobs] Platform DB updated for org ${organization_id}`);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(`[@cocreate/cron-jobs] Error updating platform DB for org ${organization_id}:`, error);
|
|
386
355
|
}
|
|
356
|
+
}
|
|
387
357
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
console.log(`Adding cron job reference to platform DB for org ${organization_id}`);
|
|
358
|
+
/**
|
|
359
|
+
* Finds and schedules the next cron job for the organization after one is executed.
|
|
360
|
+
* @param {string} organization_id - The organization ID.
|
|
361
|
+
*/
|
|
362
|
+
async function findNextCronJob(organization_id) {
|
|
363
|
+
if (!crud) return;
|
|
364
|
+
console.log(`[@cocreate/cron-jobs] Finding next cron job for org ${organization_id}`);
|
|
396
365
|
|
|
397
|
-
|
|
366
|
+
try {
|
|
367
|
+
const nextJob = await crud.send({
|
|
398
368
|
method: 'object.read',
|
|
399
|
-
array: '
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
369
|
+
array: 'cron-job',
|
|
370
|
+
$filter: {
|
|
371
|
+
query: { organization_id },
|
|
372
|
+
sort: [{ key: 'datetime', direction: 'asc' }],
|
|
373
|
+
limit: 1
|
|
374
|
+
},
|
|
375
|
+
organization_id
|
|
406
376
|
});
|
|
377
|
+
|
|
378
|
+
if (nextJob && nextJob.object && nextJob.object.length > 0) {
|
|
379
|
+
const { datetime } = nextJob.object[0];
|
|
380
|
+
scheduleCronJob({ ...nextJob.object[0], nextExecutionTime: datetime });
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error(`[@cocreate/cron-jobs] Error finding next cron job for org ${organization_id}:`, error);
|
|
407
384
|
}
|
|
385
|
+
}
|
|
408
386
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}).then(nextJob => {
|
|
423
|
-
if (nextJob.length > 0) {
|
|
424
|
-
const { datetime } = nextJob[0];
|
|
425
|
-
this.scheduleCronJob(organization_id, datetime);
|
|
426
|
-
}
|
|
427
|
-
}).catch(error => {
|
|
428
|
-
console.error(`Error finding next cron job for org ${organization_id}:`, error);
|
|
387
|
+
/**
|
|
388
|
+
* Self-Activating Auto-Initialization Loop.
|
|
389
|
+
* Runs on static module import, polling background state to detect when the
|
|
390
|
+
* parent server core has fully executed its async initialization blocks.
|
|
391
|
+
*/
|
|
392
|
+
async function init() {
|
|
393
|
+
if (server && server.isInitialized && server.crud && server.wsManager) {
|
|
394
|
+
crud = server.crud;
|
|
395
|
+
wsManager = server.wsManager;
|
|
396
|
+
|
|
397
|
+
// Handle incoming realtime CRUD operations to dynamically reschedule active jobs
|
|
398
|
+
process.on('crud-event', (event) => {
|
|
399
|
+
handleCrudEvent(event);
|
|
429
400
|
});
|
|
401
|
+
|
|
402
|
+
// Initial boot poll
|
|
403
|
+
pollForCronJobs();
|
|
404
|
+
|
|
405
|
+
// Setup the 5-minute interval cycle
|
|
406
|
+
setInterval(() => {
|
|
407
|
+
pollForCronJobs();
|
|
408
|
+
}, 5 * 60 * 1000);
|
|
409
|
+
|
|
410
|
+
} else {
|
|
411
|
+
// If the server core isn't fully booted/initialized yet, retry in 500ms
|
|
412
|
+
setTimeout(init, 500);
|
|
430
413
|
}
|
|
431
414
|
}
|
|
432
415
|
|
|
433
|
-
|
|
416
|
+
// Start auto-activation monitoring sequence instantly upon import evaluation
|
|
417
|
+
init();
|
|
418
|
+
|
|
419
|
+
export default {
|
|
420
|
+
init,
|
|
421
|
+
getNextExecutionTime,
|
|
422
|
+
executeCronJob,
|
|
423
|
+
scheduleCronJob,
|
|
424
|
+
handleCrudEvent,
|
|
425
|
+
pollForCronJobs,
|
|
426
|
+
addToPlatformDB,
|
|
427
|
+
findNextCronJob
|
|
428
|
+
};
|