@cocreate/cron-jobs 1.2.1 → 1.4.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.
@@ -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@v3
26
- - name: Setup Node.js
27
- uses: actions/setup-node@v3
28
+ uses: actions/checkout@v4
28
29
  with:
29
- node-version: 14
30
- - name: Semantic Release
31
- uses: cycjimmy/semantic-release-action@v3
32
- id: semantic
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
- extra_plugins: |
35
- @semantic-release/changelog
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
- @semantic-release/github
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.GITHUB }}"
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,33 @@
1
+ ## [1.4.1](https://github.com/CoCreate-app/CoCreate-cron-jobs/compare/v1.4.0...v1.4.1) (2026-07-17)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * bump dependencies ([451a7a3](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/451a7a3c0d66927317089705a61f155541a58417))
7
+
8
+ # [1.4.0](https://github.com/CoCreate-app/CoCreate-cron-jobs/compare/v1.3.0...v1.4.0) (2026-07-17)
9
+
10
+
11
+ ### Features
12
+
13
+ * update automated workflow and release configuration for improved semantic release handling ([25d37c4](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/25d37c4fd2d2fb3ed40de34556fe13db627baa3a))
14
+
15
+ # [1.3.0](https://github.com/CoCreate-app/CoCreate-cron-jobs/compare/v1.2.1...v1.3.0) (2026-07-14)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * remove unnecessary console logs in init function for cleaner output ([50dac43](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/50dac43f973b7b90144233522d8a950ec6ce5057))
21
+ * removed post install ([101e9b3](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/101e9b379fba70b711b46bb4a046c892bdf21c2a))
22
+ * semantic version handling. Reorganize .gitignore for improved clarity and structure ([f211fa3](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/f211fa30c0c6dc3f90576ad28e15856f5abc45bd))
23
+ * update .gitignore to include package-lock.json and pnpm-lock.yaml ([4ca6545](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/4ca65451c8bd2f9d3c6562fc4908f54c69f3e1f5))
24
+ * update module export to ES6 syntax in release.config.js ([96909f4](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/96909f4deff179f2cb02170816680a8248fcf96e))
25
+
26
+
27
+ ### Features
28
+
29
+ * add cron-parser dependency and implement cron job scheduling logic ([b80a44d](https://github.com/CoCreate-app/CoCreate-cron-jobs/commit/b80a44d0dd2ec4b7f7d41e171b9babacb0a1e13a))
30
+
1
31
  ## [1.2.1](https://github.com/CoCreate-app/CoCreate-cron-jobs/compare/v1.2.0...v1.2.1) (2025-05-01)
2
32
 
3
33
 
package/package.json CHANGED
@@ -1,18 +1,10 @@
1
1
  {
2
2
  "name": "@cocreate/cron-jobs",
3
- "version": "1.2.1",
3
+ "version": "1.4.1",
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",
@@ -24,10 +16,9 @@
24
16
  "access": "public"
25
17
  },
26
18
  "scripts": {
27
- "start": "npx webpack --config webpack.config.js",
28
- "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); } }\""
19
+ "start": "webpack --config webpack.config.js",
20
+ "build": "webpack --mode=production --config webpack.config.js",
21
+ "dev": "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
- module.exports = {
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/git",
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
- * CoCreateCronJobs class is responsible for managing cron jobs for organizations.
25
- * It listens to CRUD events and polls the platform DB to schedule and execute cron jobs.
26
- *
27
- * Example Object:
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
- class CoCreateCronJobs {
86
- /**
87
- * Creates an instance of CoCreateCronJobs.
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
- // Start polling every 5 minutes
100
- setInterval(() => {
101
- this.pollForCronJobs();
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
- * Handles CRUD events to schedule cron jobs.
107
- * @param {Object} data - The data object emitted from CRUD operations.
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
- * Parses the schedule object and calculates the next execution time.
158
- * Takes into account cronExpression, startTime, skipDates, time, daysOfWeek, daysOfMonth, months, endTime, etc.
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
- while (!schedule.daysOfWeek.includes(dayOfWeek)) {
182
- currentDateTime.setDate(currentDateTime.getDate() + 1); // Move to the next day
183
- }
58
+ while (!schedule.daysOfWeek.includes(dayOfWeek)) {
59
+ currentDateTime.setDate(currentDateTime.getDate() + 1); // Move to the next day
184
60
  }
61
+ }
185
62
 
186
- // Apply daysOfMonth: Ensure the next execution is on a valid day of the month
187
- if (schedule.daysOfMonth) {
188
- while (!schedule.daysOfMonth.includes(currentDateTime.getDate())) {
189
- currentDateTime.setDate(currentDateTime.getDate() + 1); // Move to the next day
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
- // Apply months: Ensure the next execution falls within one of the allowed months
194
- if (schedule.months) {
195
- const monthName = currentDateTime.toLocaleString('en-US', { month: 'long', timeZone: schedule.timezone || 'UTC' });
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
- while (!schedule.months.includes(monthName)) {
198
- currentDateTime.setMonth(currentDateTime.getMonth() + 1); // Move to the next month
199
- }
74
+ while (!schedule.months.includes(monthName)) {
75
+ currentDateTime.setMonth(currentDateTime.getMonth() + 1); // Move to the next month
200
76
  }
77
+ }
201
78
 
202
- // Apply time: Set the execution time of day if specified
203
- if (schedule.time) {
204
- const [hours, minutes, seconds] = schedule.time.split(':');
205
- currentDateTime.setHours(hours, minutes, seconds);
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
- // Apply endTime: Ensure the next execution time does not exceed the endTime
209
- if (schedule.endTime && new Date(currentDateTime) > new Date(schedule.endTime)) {
210
- return null; // No further executions after endTime
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
- // Now run the cronParser to handle cronExpression (if present) or fallback to currentDateTime
214
- if (schedule.cronExpression) {
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
- * Schedules a cron job to be executed at a specific datetime.
227
- *
228
- * @param {string} organization_id - The organization ID.
229
- * @param {string} datetime - The time at which the job should be executed.
230
- */
231
- scheduleCronJob(object) {
232
- const delay = new Date(object.nextExecutionTime) - new Date();
233
-
234
- if (delay <= 0) {
235
- this.executeCronJob(object);
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
- console.log(`Scheduling cron job for org ${object.organization_id} in ${delay / 1000} seconds`);
240
-
241
- // TODO: get clusterId, serverId and workerId
242
- object = {
243
- _id: object._id,
244
- nextExecutionTime: object.nextExecutionTime,
245
- status: object.status,
246
- clusterId: 'id',
247
- severId: 'id',
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
- this.scheduledJobs[object._id] = setTimeout(() => {
253
- this.executeCronJob(object);
254
- }, delay);
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
- * Executes the scheduled cron job.
259
- *
260
- * @param {Object} schedule - The cron-job object containing job, schedule, etc.
261
- */
262
- async executeCronJob(object) {
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
- if (!object.job) {
266
- let data = await this.crud.send({
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 = data.object[0]
273
- console.log('Fetched cron job data:', object);
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
- // TODO: execute cron using lazyloader.webhook()
277
- object.nextExecutionTime = this.getNextExecutionTime(object.schedule);
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
- this.updateCronJob(object);
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
- * Polls the platform DB for cron jobs that are scheduled or potentially failed.
289
- */
290
- pollForCronJobs() {
291
- console.log('Polling for cron jobs...');
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
- const now = new Date();
294
- const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
295
- const oneMinuteAgo = new Date(now.getTime() - 1 * 60 * 1000); // 1 minute past due jobs
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
- this.crud.send({
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' } } }, // Assigned but not started
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
- this.crud.send({
359
- method: 'object.update',
360
- array: 'cron-jobs',
361
- object,
362
- upsert: true,
363
- organization_id: object.organization_id
364
- }).then(() => {
365
- console.log(`Cron job for org ${object.organization_id} updated.`);
366
- }).catch(error => {
367
- console.error(`Error updating cron job for org ${object.organization_id}:`, error);
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
- const cronJobs = object
373
- this.crud.send({
374
- method: 'object.update',
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
- * Adds a cron job reference to the platform database.
390
- *
391
- * @param {string} organization_id - The organization ID.
392
- * @param {string} datetime - The datetime of the cron job to add to the platform DB.
393
- */
394
- addToPlatformDB(organization_id, datetime) {
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
- this.crud.send({
366
+ try {
367
+ const nextJob = await crud.send({
398
368
  method: 'object.read',
399
- array: 'organizations',
400
- object: { _id: organization_id, nextCron: datetime },
401
- organization_id: process.env.organization_id
402
- }).then(() => {
403
- console.log(`Platform DB updated for org ${organization_id}`);
404
- }).catch(error => {
405
- console.error(`Error updating platform DB for org ${organization_id}:`, error);
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
- * Finds and schedules the next cron job for the organization after one is executed.
411
- *
412
- * @param {string} organization_id - The organization ID.
413
- */
414
- findNextCronJob(organization_id) {
415
- console.log(`Finding next cron job for org ${organization_id}`);
416
-
417
- this.crud.read({
418
- collection: 'cron-job',
419
- query: { organization_id },
420
- sort: { datetime: 1 },
421
- limit: 1
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
- module.exports = CoCreateCronJobs;
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
+ };