@cocreate/cron-jobs 1.4.3 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +367 -47
  2. package/package.json +2 -5
  3. package/src/index.js +362 -135
package/README.md CHANGED
@@ -1,91 +1,411 @@
1
- # CoCreate-cron-jobs
1
+ # @cocreate/cron-jobs
2
2
 
3
- A simple cron-jobs component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API. Take it for a spin in our [playground!](https://cocreate.app/docs/cron-jobs)
3
+ A high-performance, multi-tenant, zero-dependency background cron engine and task scheduler built for distributed cluster orchestration and automated event execution across the CoCreate ecosystem.
4
4
 
5
- ![min file size in bytes](https://img.badgesize.io/https://cdn.cocreate.app/cron-jobs/latest/CoCreate-cron-jobs.min.js?style=flat-square&label=minified&color=orange)
6
- ![gzip file size in bytes](https://img.badgesize.io/https://cdn.cocreate.app/cron-jobs/latest/CoCreate-cron-jobs.min.js?compression=gzip&style=flat-square&label=gzip&color=yellow)
7
- ![brotlifile size in bytes](https://img.badgesize.io/https://cdn.cocreate.app/cron-jobs/latest/CoCreate-cron-jobs.min.js?compression=brotli&style=flat-square&label=brotli)
8
- ![GitHub latest release](https://img.shields.io/github/v/release/CoCreate-app/CoCreate-cron-jobs?style=flat-square)
9
- ![GitHub](https://img.shields.io/github/license/CoCreate-app/CoCreate-cron-jobs?style=flat-square)
10
- ![GitHub](https://img.shields.io/static/v1?style=flat-square&label=&message=Hiring&color=blueviolet)
5
+ ---
11
6
 
12
- ![CoCreate-cron-jobs](https://cdn.cocreate.app/docs/CoCreate-cron-jobs.gif)
7
+ ## Documentation
13
8
 
14
- ## [Docs & Demo](https://cocreate.app/docs/cron-jobs)
9
+ For complete API references, deployment guides, scheduling examples, and architecture documentation, visit the CoCreate Cron Jobs documentation:
15
10
 
16
- For a complete guide and working demo refer to the [doumentation](https://cocreate.app/docs/cron-jobs)
11
+ https://cocreatejs.com/docs/cron-jobs
17
12
 
18
- ## CDN
13
+ ---
19
14
 
20
- ```html
21
- <script src="https://cdn.cocreate.app/cron-jobs/latest/CoCreate-cron-jobs.min.js"></script>
15
+ ## Table of Contents
16
+
17
+ - [Overview](#overview)
18
+ - [Key Features](#key-features)
19
+ - [Architecture & Execution Pipeline](#architecture--execution-pipeline)
20
+ - [Installation](#installation)
21
+ - [Quick Start](#quick-start)
22
+ - [Schedule Configuration & Formats](#schedule-configuration--formats)
23
+ - [Standard 5-Part Cron Expressions](#1-standard-5-part-cron-expressions)
24
+ - [Rich JSON Schedule Objects](#2-rich-json-schedule-objects)
25
+ - [Supported Schedule Attributes](#supported-schedule-attributes)
26
+ - [Cron Expression Parsing Engine](#cron-expression-parsing-engine)
27
+ - [Cluster Coordination & Job Assignment](#cluster-coordination--job-assignment)
28
+ - [Programmatic API Reference](#programmatic-api-reference)
29
+ - [Database Integration & CRUD Events](#database-integration--crud-events)
30
+ - [Environment Variables](#environment-variables)
31
+ - [Announcements](#announcements)
32
+ - [Roadmap](#roadmap)
33
+ - [How to Contribute](#how-to-contribute)
34
+ - [About](#about)
35
+ - [License](#license)
36
+
37
+ ---
38
+
39
+ # Overview
40
+
41
+ `@cocreate/cron-jobs` delivers an enterprise-grade background scheduling engine for `@cocreate/server`.
42
+
43
+ It supports both traditional 5-part Unix cron expressions and rich JSON schedule objects with advanced scheduling capabilities including time zones, skip dates, end-of-month execution, date ranges, and human-readable scheduling.
44
+
45
+ Built as a native, zero-dependency service, it combines real-time CRUD event listeners with a resilient polling engine to coordinate scheduled jobs safely across multi-server and multi-worker deployments using atomic job assignment.
46
+
47
+ ---
48
+
49
+ # Key Features
50
+
51
+ - **Zero Dependencies** — Built entirely with native Node.js APIs including `Date`, `Intl.DateTimeFormat`, and standard language features.
52
+ - **Dual Scheduling Syntax** — Supports traditional cron expressions and structured JSON schedule objects.
53
+ - **UTC-First Scheduling** — Performs scheduling calculations in UTC while supporting localized execution using IANA time zones.
54
+ - **End-of-Month Support** — Supports `"L"` and `"end-of-month"` keywords, including leap-year awareness.
55
+ - **Hybrid Scheduler** — Combines database polling with real-time CRUD event listeners for reliable execution.
56
+ - **Cluster Safe** — Coordinates execution using `organization_id`, `clusterId`, `serverId`, and `workerId` ownership to prevent duplicate execution.
57
+ - **Integrated Action Routing** — Executes internal server actions, webhooks, or WebSocket broadcasts through the CoCreate platform.
58
+
59
+ ---
60
+
61
+ # Architecture & Execution Pipeline
62
+
63
+ The scheduler continuously polls the platform database looking for jobs scheduled within the next five-minute execution window. Eligible jobs are atomically claimed before being scheduled in memory using precise `setTimeout()` timers.
64
+
65
+ ```text
66
+ Database / CRUD Event
67
+
68
+
69
+ Evaluate Next UTC Execution
70
+
71
+
72
+ Is execution within 5 minutes?
73
+
74
+ ┌────┴────┐
75
+ │ │
76
+ Yes No
77
+ │ │
78
+ Claim Job Persist
79
+ Schedule State
80
+ setTimeout()
22
81
  ```
23
82
 
24
- ```html
25
- <script src="https://cdn.cocreate.app/cron-jobs/latest/CoCreate-cron-jobs.min.css"></script>
83
+ When a scheduled timer executes:
84
+
85
+ ```text
86
+ Cron Job Triggered
87
+
88
+
89
+ Execute Action
90
+ (Webhook / WS / Internal Action)
91
+
92
+
93
+ Calculate Next Execution Time
94
+
95
+ ┌────┴────┐
96
+ │ │
97
+ Next Run Expired
98
+ │ │
99
+ Scheduled Completed
26
100
  ```
27
101
 
102
+ ---
103
+
104
+ # Installation
105
+
28
106
  ## NPM
29
107
 
30
- ```shell
31
- $ npm i @cocreate/cron-jobs
108
+ ```bash
109
+ npm install @cocreate/cron-jobs
110
+ ```
111
+
112
+ ## Yarn
113
+
114
+ ```bash
115
+ yarn add @cocreate/cron-jobs
116
+ ```
117
+
118
+ > **Note**
119
+ >
120
+ > When used alongside `@cocreate/server`, the cron service initializes automatically.
121
+
122
+ ---
123
+
124
+ # Quick Start
125
+
126
+ ## Calculate the Next Execution Time
127
+
128
+ ```javascript
129
+ import cronJobs from "@cocreate/cron-jobs";
130
+
131
+ const nextRun = cronJobs.getNextExecutionTime({
132
+ cronExpression: "0 8 * * 1-5",
133
+ timezone: "UTC"
134
+ });
135
+
136
+ console.log(nextRun);
137
+ ```
138
+
139
+ ## Create a Scheduled Job
140
+
141
+ ```javascript
142
+ await crud.send({
143
+ method: "object.create",
144
+ array: "cron-job",
145
+ organization_id: "org_102938",
146
+ object: [
147
+ {
148
+ organization_id: "org_102938",
149
+ action: "notifications.sendDigest",
150
+ active: true,
151
+ schedule: {
152
+ time: "09:00:00",
153
+ daysOfWeek: ["Monday", "Wednesday", "Friday"],
154
+ skipDates: ["2026-12-25"],
155
+ timezone: "America/New_York"
156
+ }
157
+ }
158
+ ]
159
+ });
160
+ ```
161
+
162
+ ---
163
+
164
+ # Schedule Configuration & Formats
165
+
166
+ The scheduler accepts either a standard cron expression or a structured JSON schedule object.
167
+
168
+ ## 1. Standard 5-Part Cron Expressions
169
+
170
+ ```json
171
+ {
172
+ "cronExpression": "*/15 8-17 * * MON-FRI",
173
+ "startTime": "2026-01-01T00:00:00.000Z",
174
+ "endTime": "2026-12-31T23:59:59.000Z",
175
+ "skipDates": ["2026-07-04"]
176
+ }
177
+ ```
178
+
179
+ ---
180
+
181
+ ## 2. Rich JSON Schedule Objects
182
+
183
+ ```json
184
+ {
185
+ "startTime": "2026-01-01T00:00:00.000Z",
186
+ "endTime": "2026-12-31T23:59:59.000Z",
187
+ "time": "14:30:00",
188
+ "daysOfWeek": ["Tuesday", "Thursday"],
189
+ "daysOfMonth": [1, 15, "L"],
190
+ "months": ["January", "June", "December"],
191
+ "skipDates": ["2026-11-26"],
192
+ "timezone": "UTC"
193
+ }
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Supported Schedule Attributes
199
+
200
+ | Property | Type | Description |
201
+ |----------|------|-------------|
202
+ | `cronExpression` | String | Standard 5-part cron expression. |
203
+ | `startTime` | String | Earliest execution time. |
204
+ | `endTime` | String | Expiration time. |
205
+ | `time` | String | Time of day (`HH:MM` or `HH:MM:SS`). |
206
+ | `daysOfWeek` | Array | Weekday names. |
207
+ | `daysOfMonth` | Array | Month days including `"L"` or `"end-of-month"`. |
208
+ | `endOfMonth` | Boolean | Execute only on the last day of the month. |
209
+ | `months` | Array | Month names. |
210
+ | `skipDates` | Array | ISO dates excluded from execution. |
211
+ | `timezone` | String | IANA timezone identifier. Defaults to `UTC`. |
212
+
213
+ ---
214
+
215
+ # Cron Expression Parsing Engine
216
+
217
+ Supported fields:
218
+
219
+ ```text
220
+ Minute Hour Day-of-Month Month Day-of-Week
221
+ ```
222
+
223
+ Supported syntax:
224
+
225
+ | Syntax | Example | Description |
226
+ |---------|---------|-------------|
227
+ | Wildcard | `*` | Every value |
228
+ | Value | `15` | Specific value |
229
+ | List | `1,15,30` | Multiple values |
230
+ | Range | `1-5` | Inclusive range |
231
+ | Step | `*/15` | Every N values |
232
+ | Aliases | `JAN`, `MON` | Month and weekday aliases |
233
+
234
+ ---
235
+
236
+ # Cluster Coordination & Job Assignment
237
+
238
+ To prevent duplicate execution, scheduled jobs are atomically assigned to a server and worker before execution.
239
+
240
+ Example assignment:
241
+
242
+ ```json
243
+ {
244
+ "status": "assigned",
245
+ "clusterId": "default-cluster",
246
+ "serverId": "srv_instance_84f92a",
247
+ "workerId": 2,
248
+ "organization_id": "org_30192"
249
+ }
250
+ ```
251
+
252
+ If a worker exits before execution completes, polling automatically detects stale assignments and safely reassigns work to another available worker.
253
+
254
+ ---
255
+
256
+ # Programmatic API Reference
257
+
258
+ ## getNextExecutionTime(schedule)
259
+
260
+ Calculates the next execution time.
261
+
262
+ ```javascript
263
+ import cronJobs from "@cocreate/cron-jobs";
264
+
265
+ const next = cronJobs.getNextExecutionTime({
266
+ cronExpression: "0 0 * * *"
267
+ });
268
+ ```
269
+
270
+ ---
271
+
272
+ ## scheduleCronJob(job)
273
+
274
+ Schedules a job in memory.
275
+
276
+ ```javascript
277
+ cronJobs.scheduleCronJob({
278
+ _id: "job_88a91b",
279
+ organization_id: "org_102",
280
+ nextExecutionTime: "2026-08-01T12:00:00.000Z",
281
+ schedule: {}
282
+ });
283
+ ```
284
+
285
+ ---
286
+
287
+ ## executeCronJob(job)
288
+
289
+ Immediately executes a scheduled job.
290
+
291
+ ```javascript
292
+ await cronJobs.executeCronJob(jobObject);
293
+ ```
294
+
295
+ ---
296
+
297
+ ## pollForCronJobs()
298
+
299
+ Scans the database for pending work.
300
+
301
+ ```javascript
302
+ await cronJobs.pollForCronJobs();
303
+ ```
304
+
305
+ ---
306
+
307
+ ## handleCrudEvent(data)
308
+
309
+ Updates in-memory schedules after CRUD operations.
310
+
311
+ ```javascript
312
+ process.emit("crud-event", {
313
+ array: "cron-job",
314
+ method: "object.update",
315
+ object: [updatedJob]
316
+ });
32
317
  ```
33
318
 
34
- ## yarn
319
+ ---
320
+
321
+ # Database Integration & CRUD Events
35
322
 
36
- ```shell
37
- $ yarn install @cocreate/cron-jobs
323
+ `@cocreate/cron-jobs` automatically listens for platform CRUD events using:
324
+
325
+ ```javascript
326
+ process.on("crud-event");
38
327
  ```
39
328
 
40
- # Table of Contents
329
+ ### Supported Events
330
+
331
+ | Event | Behavior |
332
+ |--------|----------|
333
+ | `object.create` | Calculates next execution time and schedules the job. |
334
+ | `object.update` | Recalculates the schedule and updates timers. |
335
+ | `object.delete` | Cancels timers and releases resources. |
336
+ | `active: false` | Stops scheduling and marks the job inactive. |
337
+
338
+ Organization updates also synchronize job summaries into the parent `organizations` collection.
41
339
 
42
- - [Table of Contents](#table-of-contents)
43
- - [Announcements](#announcements)
44
- - [Roadmap](#roadmap)
45
- - [How to Contribute](#how-to-contribute)
46
- - [About](#about)
47
- - [License](#license)
340
+ ---
48
341
 
49
- <a name="announcements"></a>
342
+ # Environment Variables
343
+
344
+ | Variable | Default | Description |
345
+ |----------|----------|-------------|
346
+ | `organization_id` | — | Platform administrative organization. |
347
+ | `SERVER_ID` | `default-server` | Unique server identifier. |
348
+ | `WORKER_ID` | `standalone` | Worker process identifier. |
349
+
350
+ ---
50
351
 
51
352
  # Announcements
52
353
 
53
- All updates to this library are documented in our [CHANGELOG](https://github.com/CoCreate-app/CoCreate-cron-jobs/blob/master/CHANGELOG.md) and [releases](https://github.com/CoCreate-app/CoCreate-cron-jobs/releases). You may also subscribe to email for releases and breaking changes.
354
+ Release notes, performance improvements, scheduling enhancements, and compatibility updates are available on the GitHub Releases page:
355
+
356
+ https://github.com/CoCreate-app/CoCreate-cron-jobs/releases
54
357
 
55
- <a name="roadmap"></a>
358
+ ---
56
359
 
57
360
  # Roadmap
58
361
 
59
- If you are interested in the future direction of this project, please take a look at our open [issues](https://github.com/CoCreate-app/CoCreate-cron-jobs/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-cron-jobs/pulls). We would love to hear your feedback.
362
+ Upcoming improvements include:
60
363
 
61
- <a name="about"></a>
364
+ - Runtime AST execution rules
365
+ - Distributed lock providers
366
+ - Built-in execution telemetry
367
+ - Retry policies with exponential backoff
368
+ - Enhanced monitoring and reporting
62
369
 
63
- # About
370
+ ---
64
371
 
65
- CoCreate-cron-jobs is guided and supported by the CoCreate Developer Experience Team.
372
+ # How to Contribute
66
373
 
67
- Please Email the Developer Experience Team [here](mailto:develop@cocreate.app) in case of any queries.
374
+ We welcome bug reports, feature requests, documentation improvements, and pull requests.
68
375
 
69
- CoCreate-cron-jobs is maintained and funded by CoCreate. The names and logos for CoCreate are trademarks of CoCreate, LLC.
376
+ Please review the contributing guide before submitting changes:
70
377
 
71
- <a name="contribute"></a>
378
+ - **Contributing Guide:** https://github.com/CoCreate-app/CoCreate-cron-jobs/blob/master/CONTRIBUTING.md
379
+ - **Issue Tracker:** https://github.com/CoCreate-app/CoCreate-cron-jobs/issues
380
+ - **Discussions:** https://github.com/CoCreate-app/CoCreate-cron-jobs/discussions
72
381
 
73
- # How to Contribute
382
+ ---
74
383
 
75
- We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/CoCreate-app/CoCreate-cron-jobs/blob/master/CONTRIBUTING.md) guide for details.
384
+ # About
385
+
386
+ `@cocreate/cron-jobs` is designed, built, and maintained by the CoCreate Developer Experience Team.
387
+
388
+ It provides the distributed scheduling and recurring task execution backbone for the CoCreate platform.
76
389
 
77
- We want this library to be community-driven, and CoCreate led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/CoCreate-app/CoCreate-cron-jobs/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-cron-jobs/pulls) or merely upvote or comment on existing issues or pull requests.
390
+ Learn more:
78
391
 
79
- We appreciate your continued support, thank you!
392
+ - Documentation: https://cocreatejs.com/docs/cron-jobs
393
+ - Website: https://cocreatejs.com
80
394
 
81
- <a name="license"></a>
395
+ ---
82
396
 
83
397
  # License
84
398
 
85
- This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
399
+ This software is dual-licensed under the GNU Affero General Public License v3 (AGPLv3) and a commercial license.
400
+
401
+ ### Open Source
402
+
403
+ For open-source and non-commercial projects, this software is available under the AGPLv3.
404
+
405
+ See the `LICENSE` file for details.
86
406
 
87
- - **Open Source Use**: For open-source projects and non-commercial use, this software is available under the AGPLv3. The AGPLv3 allows you to freely use, modify, and distribute this software, provided that all modifications and derivative works are also licensed under the AGPLv3. For the full license text, see the [LICENSE file](https://github.com/CoCreate-app/CoCreate-cron-jobs/blob/master/LICENSE).
407
+ ### Commercial
88
408
 
89
- - **Commercial Use**: For-profit companies and individuals intending to use this software for commercial purposes must obtain a commercial license. The commercial license is available when you sign up for an API key on our [website](https://cocreate.app). This license permits proprietary use and modification of the software without the copyleft requirements of the AGPLv3. It is ideal for integrating this software into proprietary commercial products and applications.
409
+ Organizations requiring proprietary use without AGPL obligations may obtain a commercial license from CoCreate.
90
410
 
91
- If you have not purchased a commercial license and intend to use this software for commercial purposes, you are required to sign up for an API key on our website.
411
+ https://cocreatejs.com/licenses
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/cron-jobs",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
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",
@@ -35,8 +35,5 @@
35
35
  "url": "https://github.com/sponsors/CoCreate-app"
36
36
  },
37
37
  "type": "module",
38
- "main": "./src/index.js",
39
- "dependencies": {
40
- "cron-parser": "^4.9.0"
41
- }
38
+ "main": "./src/index.js"
42
39
  }
package/src/index.js CHANGED
@@ -21,87 +21,299 @@
21
21
  // you must obtain a commercial license from CoCreate LLC.
22
22
  // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
23
23
 
24
- import cronParser from 'cron-parser';
25
24
  import server from '@cocreate/server';
26
25
 
27
- // ==========================================
28
- // Module-Level Sandbox State (ESM Singleton)
29
- // ==========================================
30
26
  let crud = null;
31
27
  let wsManager = null;
32
28
  const scheduledJobs = {};
33
29
 
34
30
  /**
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.
31
+ * Safe helper to retrieve localized or UTC date part names natively.
39
32
  */
40
- function getNextExecutionTime(schedule) {
41
- const now = new Date();
42
- let currentDateTime = now;
33
+ function getFormattedDatePart(date, type, timezone = 'UTC') {
34
+ return date.toLocaleString('en-US', { [type]: 'long', timeZone: timezone });
35
+ }
43
36
 
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);
37
+ /**
38
+ * Calculates the last day of the month for any given UTC year/month.
39
+ */
40
+ function getLastDayOfMonthUTC(year, monthIndex) {
41
+ // Setting day to 0 of monthIndex + 1 yields the last day of monthIndex
42
+ return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
43
+ }
44
+
45
+ /**
46
+ * Custom 5-Part Cron Field Parser (Zero external dependencies).
47
+ * Handles ranges (1-5), lists (1,2,3), wildcards (*), step values (* /15, 1-30/5), and string aliases.
48
+ */
49
+ function parseCronField(field, min, max, nameMap = {}) {
50
+ field = field.trim().toUpperCase();
51
+
52
+ // Replace string aliases if present (e.g., JAN-DEC or SUN-SAT)
53
+ for (const [name, val] of Object.entries(nameMap)) {
54
+ field = field.replace(new RegExp(name, 'g'), val);
47
55
  }
48
56
 
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
57
+ const allowed = new Set();
58
+ const parts = field.split(',');
59
+
60
+ for (const part of parts) {
61
+ if (part.includes('/')) {
62
+ const [rangeStr, stepStr] = part.split('/');
63
+ const step = parseInt(stepStr, 10) || 1;
64
+ let start = min;
65
+ let end = max;
66
+
67
+ if (rangeStr !== '*') {
68
+ if (rangeStr.includes('-')) {
69
+ const [s, e] = rangeStr.split('-').map(Number);
70
+ start = s;
71
+ end = e;
72
+ } else {
73
+ start = parseInt(rangeStr, 10);
74
+ }
75
+ }
76
+ for (let i = start; i <= end; i += step) {
77
+ if (i >= min && i <= max) allowed.add(i);
78
+ }
79
+ } else if (part.includes('-')) {
80
+ const [start, end] = part.split('-').map(Number);
81
+ for (let i = start; i <= end; i++) {
82
+ if (i >= min && i <= max) allowed.add(i);
83
+ }
84
+ } else if (part === '*') {
85
+ for (let i = min; i <= max; i++) allowed.add(i);
86
+ } else {
87
+ const val = parseInt(part, 10);
88
+ if (!isNaN(val) && val >= min && val <= max) {
89
+ allowed.add(val);
90
+ }
91
+ }
52
92
  }
93
+ return allowed;
94
+ }
53
95
 
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' });
96
+ /**
97
+ * Tokenizes standard 5-part cron expressions into match sets.
98
+ */
99
+ function parseCronExpression(expression) {
100
+ if (!expression || typeof expression !== 'string') return null;
101
+ const tokens = expression.trim().split(/\s+/);
102
+ if (tokens.length !== 5) return null;
103
+
104
+ const monthNames = { JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6, JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12 };
105
+ const dayNames = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 };
106
+
107
+ return {
108
+ minutes: parseCronField(tokens[0], 0, 59),
109
+ hours: parseCronField(tokens[1], 0, 23),
110
+ daysOfMonth: parseCronField(tokens[2], 1, 31),
111
+ months: parseCronField(tokens[3], 1, 12, monthNames),
112
+ daysOfWeek: parseCronField(tokens[4], 0, 6, dayNames)
113
+ };
114
+ }
57
115
 
58
- while (!schedule.daysOfWeek.includes(dayOfWeek)) {
59
- currentDateTime.setDate(currentDateTime.getDate() + 1); // Move to the next day
116
+ /**
117
+ * Checks if a specific UTC date instance matches a parsed 5-part cron object.
118
+ */
119
+ function matchesCron(parsedCron, date) {
120
+ const month = date.getUTCMonth() + 1; // 1-12
121
+ const dayOfMonth = date.getUTCDate(); // 1-31
122
+ const dayOfWeek = date.getUTCDay(); // 0-6
123
+ const hours = date.getUTCHours();
124
+ const minutes = date.getUTCMinutes();
125
+
126
+ if (!parsedCron.minutes.has(minutes)) return false;
127
+ if (!parsedCron.hours.has(hours)) return false;
128
+ if (!parsedCron.months.has(month)) return false;
129
+
130
+ const domHasAll = parsedCron.daysOfMonth.size === 31;
131
+ const dowHasAll = parsedCron.daysOfWeek.size === 7;
132
+
133
+ // Standard Cron rule: If both DOM and DOW are specified (neither is *), either matching triggers execution (OR logic).
134
+ if (!domHasAll && !dowHasAll) {
135
+ if (!parsedCron.daysOfMonth.has(dayOfMonth) && !parsedCron.daysOfWeek.has(dayOfWeek)) {
136
+ return false;
60
137
  }
138
+ } else {
139
+ if (!parsedCron.daysOfMonth.has(dayOfMonth)) return false;
140
+ if (!parsedCron.daysOfWeek.has(dayOfWeek)) return false;
61
141
  }
62
142
 
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
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Calculates the next execution time for standard 5-part cron strings natively.
148
+ */
149
+ function getNextCronExecution(cronExpression, startFromDate, skipDates = []) {
150
+ const parsed = parseCronExpression(cronExpression);
151
+ if (!parsed) return null;
152
+
153
+ let current = new Date(startFromDate.getTime());
154
+ current.setUTCSeconds(0, 0);
155
+ current.setUTCMinutes(current.getUTCMinutes() + 1); // Step forward to next minute
156
+
157
+ const maxSearchMinutes = 525600; // 1-year upper bound protection
158
+ let counter = 0;
159
+
160
+ while (counter < maxSearchMinutes) {
161
+ counter++;
162
+ const isoDateStr = current.toISOString().split('T')[0];
163
+
164
+ // Evaluate skipDates
165
+ if (skipDates && skipDates.includes(isoDateStr)) {
166
+ current.setUTCDate(current.getUTCDate() + 1);
167
+ current.setUTCHours(0, 0, 0, 0);
168
+ continue;
169
+ }
170
+
171
+ if (matchesCron(parsed, current)) {
172
+ return current.toISOString();
173
+ }
174
+
175
+ // Fast-forward month mismatch
176
+ const month = current.getUTCMonth() + 1;
177
+ if (!parsed.months.has(month)) {
178
+ current.setUTCMonth(current.getUTCMonth() + 1);
179
+ current.setUTCDate(1);
180
+ current.setUTCHours(0, 0, 0, 0);
181
+ continue;
182
+ }
183
+
184
+ // Fast-forward day mismatch if DOW is wildcard
185
+ const dom = current.getUTCDate();
186
+ if (parsed.daysOfWeek.size === 7 && !parsed.daysOfMonth.has(dom)) {
187
+ current.setUTCDate(current.getUTCDate() + 1);
188
+ current.setUTCHours(0, 0, 0, 0);
189
+ continue;
190
+ }
191
+
192
+ // Fast-forward hour mismatch
193
+ const hour = current.getUTCHours();
194
+ if (!parsed.hours.has(hour)) {
195
+ current.setUTCHours(current.getUTCHours() + 1);
196
+ current.setUTCMinutes(0, 0, 0);
197
+ continue;
67
198
  }
199
+
200
+ current.setUTCMinutes(current.getUTCMinutes() + 1);
201
+ }
202
+
203
+ return null;
204
+ }
205
+
206
+ /**
207
+ * Parses the schedule object and calculates the next execution time natively in UTC.
208
+ * Supports cronExpression, startTime, skipDates, time, daysOfWeek, daysOfMonth, endOfMonth, months, endTime, etc.
209
+ * @param {Object} schedule - The schedule object containing custom fields or cronExpression.
210
+ * @returns {string|null} nextExecutionTime - Calculated ISO timestamp in UTC, or null if endTime has passed.
211
+ */
212
+ function getNextExecutionTime(schedule) {
213
+ if (!schedule) return null;
214
+
215
+ const now = new Date();
216
+ let currentDateTime = new Date();
217
+
218
+ // 1. Establish UTC start baseline
219
+ if (schedule.startTime && new Date(schedule.startTime) > now) {
220
+ currentDateTime = new Date(schedule.startTime);
68
221
  }
69
222
 
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' });
223
+ const timezone = schedule.timezone || 'UTC';
73
224
 
74
- while (!schedule.months.includes(monthName)) {
75
- currentDateTime.setMonth(currentDateTime.getMonth() + 1); // Move to the next month
225
+ // 2. If standard 5-part cron expression exists, use native parser
226
+ if (schedule.cronExpression) {
227
+ const nextCronRun = getNextCronExecution(schedule.cronExpression, currentDateTime, schedule.skipDates);
228
+ if (nextCronRun && schedule.endTime && new Date(nextCronRun) > new Date(schedule.endTime)) {
229
+ return null;
76
230
  }
231
+ return nextCronRun;
77
232
  }
78
233
 
79
- // Apply time: Set the execution time of day if specified
234
+ // 3. Object-based custom schedule evaluation with safety bounds
235
+ let safetyCounter = 0;
236
+ const maxSafetyIterations = 1000;
237
+
238
+ // Apply time of day if set (formatted HH:MM or HH:MM:SS)
80
239
  if (schedule.time) {
81
- const [hours, minutes, seconds] = schedule.time.split(':');
82
- currentDateTime.setHours(hours, minutes, seconds);
83
- }
240
+ const [hours, minutes, seconds] = schedule.time.split(':').map(Number);
241
+ currentDateTime.setUTCHours(hours || 0, minutes || 0, seconds || 0, 0);
84
242
 
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
243
+ if (currentDateTime < now) {
244
+ currentDateTime.setUTCDate(currentDateTime.getUTCDate() + 1);
245
+ }
88
246
  }
89
247
 
90
- // Now run the cronParser to handle cronExpression (if present) or fallback to currentDateTime
91
- if (schedule.cronExpression) {
92
- try {
93
- const interval = cronParser.parseExpression(schedule.cronExpression, {
94
- currentDate: currentDateTime,
95
- tz: schedule.timezone || 'UTC'
248
+ while (safetyCounter < maxSafetyIterations) {
249
+ safetyCounter++;
250
+
251
+ const isoDateStr = currentDateTime.toISOString().split('T')[0];
252
+
253
+ // Check skipDates
254
+ if (schedule.skipDates && schedule.skipDates.includes(isoDateStr)) {
255
+ currentDateTime.setUTCDate(currentDateTime.getUTCDate() + 1);
256
+ continue;
257
+ }
258
+
259
+ // Check months
260
+ if (schedule.months && schedule.months.length > 0) {
261
+ const currentMonth = getFormattedDatePart(currentDateTime, 'month', timezone);
262
+ if (!schedule.months.includes(currentMonth)) {
263
+ currentDateTime.setUTCMonth(currentDateTime.getUTCMonth() + 1);
264
+ currentDateTime.setUTCDate(1);
265
+ continue;
266
+ }
267
+ }
268
+
269
+ // Check daysOfMonth and end-of-month / "L" keyword
270
+ if (schedule.daysOfMonth && schedule.daysOfMonth.length > 0) {
271
+ const currentDom = currentDateTime.getUTCDate();
272
+ const year = currentDateTime.getUTCFullYear();
273
+ const monthIdx = currentDateTime.getUTCMonth();
274
+ const lastDay = getLastDayOfMonthUTC(year, monthIdx);
275
+
276
+ const isAllowedDom = schedule.daysOfMonth.some((day) => {
277
+ if (day === 'L' || day === 'end-of-month') return currentDom === lastDay;
278
+ return Number(day) === currentDom;
96
279
  });
97
- return interval.next().toISOString();
98
- } catch (err) {
99
- console.error('[@cocreate/cron-jobs] Cron parsing failed:', err);
280
+
281
+ if (!isAllowedDom) {
282
+ currentDateTime.setUTCDate(currentDateTime.getUTCDate() + 1);
283
+ continue;
284
+ }
285
+ } else if (schedule.endOfMonth) {
286
+ // Direct object property for end-of-month scheduling
287
+ const currentDom = currentDateTime.getUTCDate();
288
+ const year = currentDateTime.getUTCFullYear();
289
+ const monthIdx = currentDateTime.getUTCMonth();
290
+ const lastDay = getLastDayOfMonthUTC(year, monthIdx);
291
+
292
+ if (currentDom !== lastDay) {
293
+ currentDateTime.setUTCDate(currentDateTime.getUTCDate() + 1);
294
+ continue;
295
+ }
296
+ }
297
+
298
+ // Check daysOfWeek
299
+ if (schedule.daysOfWeek && schedule.daysOfWeek.length > 0) {
300
+ const currentDayOfWeek = getFormattedDatePart(currentDateTime, 'weekday', timezone);
301
+ if (!schedule.daysOfWeek.includes(currentDayOfWeek)) {
302
+ currentDateTime.setUTCDate(currentDateTime.getUTCDate() + 1);
303
+ continue;
304
+ }
305
+ }
306
+
307
+ // Verify Expiration / EndTime
308
+ if (schedule.endTime && currentDateTime > new Date(schedule.endTime)) {
100
309
  return null;
101
310
  }
311
+
312
+ return currentDateTime.toISOString();
102
313
  }
103
314
 
104
- return currentDateTime.toISOString();
315
+ console.warn('[@cocreate/cron-jobs] Max safety iteration reached for schedule:', schedule);
316
+ return null;
105
317
  }
106
318
 
107
319
  /**
@@ -112,6 +324,7 @@ async function updateCronJob(object) {
112
324
  if (!crud) return;
113
325
  try {
114
326
  const cronJobs = Array.isArray(object) ? object : [object];
327
+ if (cronJobs.length === 0) return;
115
328
 
116
329
  for (const job of cronJobs) {
117
330
  await crud.send({
@@ -121,10 +334,10 @@ async function updateCronJob(object) {
121
334
  upsert: true,
122
335
  organization_id: job.organization_id
123
336
  });
124
- console.log(`[@cocreate/cron-jobs] Cron job for org ${job.organization_id} updated.`);
337
+ console.log(`[@cocreate/cron-jobs] Cron job ${job._id} for org ${job.organization_id} updated.`);
125
338
  }
126
339
 
127
- if (cronJobs.length > 0 && cronJobs[0].organization_id) {
340
+ if (cronJobs[0].organization_id) {
128
341
  const firstJob = cronJobs[0];
129
342
  await crud.send({
130
343
  method: 'object.update',
@@ -142,12 +355,36 @@ async function updateCronJob(object) {
142
355
  }
143
356
  }
144
357
 
358
+ /**
359
+ * Triggers the defined job action or payload webhook.
360
+ */
361
+ async function triggerJobAction(job) {
362
+ console.log(`[@cocreate/cron-jobs] Triggering pipeline action for job ${job._id}`);
363
+
364
+ try {
365
+ if (server?.lazyloader?.webhook) {
366
+ await server.lazyloader.webhook(job);
367
+ } else if (wsManager && job.action) {
368
+ await wsManager.broadcast({
369
+ target: job.action,
370
+ data: job
371
+ });
372
+ }
373
+ } catch (err) {
374
+ console.error(`[@cocreate/cron-jobs] Action execution failed for job ${job._id}:`, err);
375
+ }
376
+ }
377
+
145
378
  /**
146
379
  * Executes the scheduled cron job.
147
380
  * @param {Object} object - The cron-job object containing job details, schedule parameters, etc.
148
381
  */
149
382
  async function executeCronJob(object) {
150
- console.log(`[@cocreate/cron-jobs] Executing cron job for org ${object.organization_id}`);
383
+ console.log(`[@cocreate/cron-jobs] Executing cron job ${object._id} for org ${object.organization_id}`);
384
+
385
+ if (scheduledJobs[object._id]) {
386
+ delete scheduledJobs[object._id];
387
+ }
151
388
 
152
389
  try {
153
390
  if (!object.job && crud) {
@@ -158,20 +395,19 @@ async function executeCronJob(object) {
158
395
  organization_id: object.organization_id
159
396
  });
160
397
  if (data.object && data.object[0]) {
161
- object = data.object[0];
398
+ object = { ...data.object[0], ...object };
162
399
  }
163
- console.log('[@cocreate/cron-jobs] Fetched cron job data:', object);
164
400
  }
165
401
 
166
- // Target Action Pipeline execution placeholder:
167
- // TODO: execute cron using lazyloader.webhook() or server-native lazyloader routing
168
-
402
+ // Execute target payload / AST action
403
+ await triggerJobAction(object);
404
+
405
+ // Recalculate next execution window in UTC
169
406
  object.nextExecutionTime = getNextExecutionTime(object.schedule);
170
407
  if (!object.nextExecutionTime) {
171
408
  object.active = false;
172
409
  object.status = 'completed';
173
410
  } else {
174
- // Re-queue and release the lock for the next interval run
175
411
  object.status = 'scheduled';
176
412
  object.serverId = null;
177
413
  object.workerId = null;
@@ -184,10 +420,17 @@ async function executeCronJob(object) {
184
420
  }
185
421
 
186
422
  /**
187
- * Schedules a cron job to be executed at a specific datetime.
423
+ * Schedules a cron job in-memory to be executed at a specific UTC datetime.
188
424
  * @param {Object} object - The cron-job configuration footprint.
189
425
  */
190
426
  function scheduleCronJob(object) {
427
+ if (!object || !object._id || !object.nextExecutionTime) return;
428
+
429
+ if (scheduledJobs[object._id]) {
430
+ clearTimeout(scheduledJobs[object._id]);
431
+ delete scheduledJobs[object._id];
432
+ }
433
+
191
434
  const delay = new Date(object.nextExecutionTime) - new Date();
192
435
 
193
436
  if (delay <= 0) {
@@ -195,17 +438,9 @@ function scheduleCronJob(object) {
195
438
  return;
196
439
  }
197
440
 
198
- console.log(`[@cocreate/cron-jobs] Scheduling cron job for org ${object.organization_id} in ${delay / 1000} seconds`);
441
+ console.log(`[@cocreate/cron-jobs] Scheduling job ${object._id} for org ${object.organization_id} in ${(delay / 1000).toFixed(1)} seconds`);
199
442
 
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
- };
443
+ const scheduledObj = { ...object };
209
444
 
210
445
  scheduledJobs[object._id] = setTimeout(() => {
211
446
  executeCronJob(scheduledObj);
@@ -213,71 +448,71 @@ function scheduleCronJob(object) {
213
448
  }
214
449
 
215
450
  /**
216
- * Handles CRUD events to schedule cron jobs dynamically.
451
+ * Handles CRUD events to schedule cron jobs dynamically without array mutation bugs.
217
452
  * @param {Object} data - The data object emitted from CRUD operations.
218
453
  */
219
454
  async function handleCrudEvent(data) {
220
- if (data.array !== 'cron-job') {
221
- return; // Ignore non-cron-job events
455
+ if (!data || data.array !== 'cron-job' || !Array.isArray(data.object)) {
456
+ return;
222
457
  }
223
458
 
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];
459
+ const updatedJobs = [];
460
+
461
+ for (const job of data.object) {
462
+ if (!job || !job._id) continue;
228
463
 
229
- if (timeoutId) {
230
- clearTimeout(timeoutId); // Cancel the timeout if it exists
231
- delete scheduledJobs[data.object[i]._id]; // Remove the reference
464
+ if (data.method === 'object.delete' || job.active === false) {
465
+ if (scheduledJobs[job._id]) {
466
+ clearTimeout(scheduledJobs[job._id]);
467
+ delete scheduledJobs[job._id];
232
468
  }
233
469
 
234
- data.object.splice(i, 1);
235
- i--; // Adjust the index after removal
470
+ job.status = data.method === 'object.delete' ? 'deleted' : 'inactive';
471
+ updatedJobs.push(job);
236
472
  continue;
237
473
  }
238
474
 
239
- const nextExecutionTime = getNextExecutionTime(data.object[i].schedule);
475
+ const nextExecutionTime = getNextExecutionTime(job.schedule);
240
476
 
241
477
  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
478
  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]);
479
+ job.nextExecutionTime = nextExecutionTime;
480
+
481
+ if (timeUntilExecution <= 5 * 60 * 1000 && timeUntilExecution >= 0) {
482
+ if (!scheduledJobs[job._id]) {
483
+ job.status = 'assigned';
484
+ job.serverId = server?.instanceId || server?.serverId || 'default-server';
485
+ job.workerId = server?.workerId !== undefined ? server.workerId : 'standalone';
486
+ scheduleCronJob(job);
487
+ }
488
+ } else {
489
+ job.status = 'scheduled';
257
490
  }
491
+ updatedJobs.push(job);
258
492
  } else {
259
- data.object.splice(i, 1);
260
- i--; // Adjust the index after removal
493
+ job.active = false;
494
+ job.status = 'completed';
495
+ updatedJobs.push(job);
261
496
  }
262
497
  }
263
498
 
264
- await updateCronJob(data.object);
499
+ if (updatedJobs.length > 0) {
500
+ await updateCronJob(updatedJobs);
501
+ }
265
502
  }
266
503
 
267
504
  /**
268
505
  * 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
506
  */
271
507
  async function pollForCronJobs() {
272
508
  if (!crud) return;
273
- console.log(`[@cocreate/cron-jobs] [Worker ${server?.workerId}] Polling for active organizations' cron jobs...`);
509
+ console.log(`[@cocreate/cron-jobs] [WORKER ${server?.workerId ?? 'standalone'}] Polling for active organizations' cron jobs...`);
274
510
 
275
511
  const now = new Date();
276
512
  const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
277
- const oneMinuteAgo = new Date(now.getTime() - 1 * 60 * 1000); // 1 minute past due jobs
513
+ const oneMinuteAgo = new Date(now.getTime() - 1 * 60 * 1000);
278
514
 
279
515
  try {
280
- // Query organizations.
281
516
  const data = await crud.send({
282
517
  method: 'object.read',
283
518
  array: 'organizations',
@@ -286,16 +521,16 @@ async function pollForCronJobs() {
286
521
  $or: [
287
522
  {
288
523
  $and: [
289
- { 'cron-jobs.nextExecutionTime': { $elemMatch: { $lte: fiveMinutesFromNow, $gte: now } } }, // Match jobs in array within 5 minutes
290
- { 'cron-jobs.status': { $elemMatch: { $ne: 'assigned' } } }, // Not already claimed/assigned
291
- { 'cron-jobs.active': { $elemMatch: { $ne: false } } } // Is active
524
+ { 'cron-jobs.nextExecutionTime': { $elemMatch: { $lte: fiveMinutesFromNow, $gte: now } } },
525
+ { 'cron-jobs.status': { $elemMatch: { $ne: 'assigned' } } },
526
+ { 'cron-jobs.active': { $elemMatch: { $ne: false } } }
292
527
  ]
293
528
  },
294
529
  {
295
530
  $and: [
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
531
+ { 'cron-jobs.nextExecutionTime': { $elemMatch: { $lt: oneMinuteAgo } } },
532
+ { 'cron-jobs.status': { $elemMatch: { $eq: 'assigned' } } },
533
+ { 'cron-jobs.active': { $elemMatch: { $ne: false } } }
299
534
  ]
300
535
  }
301
536
  ]
@@ -306,27 +541,29 @@ async function pollForCronJobs() {
306
541
 
307
542
  if (!data || !data.object) return;
308
543
 
309
- for (let i = 0; i < data.object.length; i++) {
310
- const cronJobs = data.object[i].cronJobs;
311
- if (!cronJobs) continue;
544
+ for (const org of data.object) {
545
+ const cronJobs = org.cronJobs;
546
+ if (!cronJobs || !Array.isArray(cronJobs)) continue;
312
547
 
313
- const organizationId = data.object[i].organization_id;
548
+ const organizationId = org.organization_id || org._id;
549
+ const jobsToUpdate = [];
314
550
 
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;
551
+ for (const job of cronJobs) {
552
+ if (!job._id || scheduledJobs[job._id]) continue;
318
553
 
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;
554
+ job.status = 'assigned';
555
+ job.clusterId = 'default-cluster';
556
+ job.serverId = server?.instanceId || server?.serverId || 'default-server';
557
+ job.workerId = server?.workerId !== undefined ? server.workerId : 'standalone';
558
+ job.organization_id = organizationId;
325
559
 
326
- scheduleCronJob(cronJobs[j]);
560
+ scheduleCronJob(job);
561
+ jobsToUpdate.push(job);
327
562
  }
328
563
 
329
- await updateCronJob(cronJobs);
564
+ if (jobsToUpdate.length > 0) {
565
+ await updateCronJob(jobsToUpdate);
566
+ }
330
567
  }
331
568
  } catch (error) {
332
569
  console.error('[@cocreate/cron-jobs] Error polling for cron jobs:', error);
@@ -335,8 +572,6 @@ async function pollForCronJobs() {
335
572
 
336
573
  /**
337
574
  * 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
575
  */
341
576
  async function addToPlatformDB(organization_id, datetime) {
342
577
  if (!crud) return;
@@ -357,7 +592,6 @@ async function addToPlatformDB(organization_id, datetime) {
357
592
 
358
593
  /**
359
594
  * Finds and schedules the next cron job for the organization after one is executed.
360
- * @param {string} organization_id - The organization ID.
361
595
  */
362
596
  async function findNextCronJob(organization_id) {
363
597
  if (!crud) return;
@@ -386,34 +620,27 @@ async function findNextCronJob(organization_id) {
386
620
 
387
621
  /**
388
622
  * 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
623
  */
392
624
  async function init() {
393
625
  if (server && server.isInitialized && server.crud && server.wsManager) {
394
626
  crud = server.crud;
395
627
  wsManager = server.wsManager;
396
628
 
397
- // Handle incoming realtime CRUD operations to dynamically reschedule active jobs
398
629
  process.on('crud-event', (event) => {
399
630
  handleCrudEvent(event);
400
631
  });
401
-
402
- // Initial boot poll
632
+
403
633
  pollForCronJobs();
404
634
 
405
- // Setup the 5-minute interval cycle
406
635
  setInterval(() => {
407
636
  pollForCronJobs();
408
637
  }, 5 * 60 * 1000);
409
-
638
+
410
639
  } else {
411
- // If the server core isn't fully booted/initialized yet, retry in 500ms
412
640
  setTimeout(init, 500);
413
641
  }
414
642
  }
415
643
 
416
- // Start auto-activation monitoring sequence instantly upon import evaluation
417
644
  init();
418
645
 
419
646
  export default {