webspresso 0.0.98 → 0.0.100

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 (88) hide show
  1. package/.agents/ADMIN.md +11 -1
  2. package/.agents/AGENTS.md +2 -2
  3. package/.agents/AUTH.md +415 -0
  4. package/.agents/CLI.md +47 -0
  5. package/.agents/ERRORS.md +59 -0
  6. package/.agents/ORM.md +178 -4
  7. package/.agents/PLUGINS.md +113 -8
  8. package/.agents/QUEUE.md +92 -0
  9. package/.agents/ROUTING.md +160 -7
  10. package/.agents/STREAMING.md +35 -0
  11. package/AGENTS.md +2 -0
  12. package/README.md +145 -93
  13. package/bin/commands/add-docker.js +467 -0
  14. package/bin/commands/deploy-check.js +339 -0
  15. package/bin/webspresso.js +4 -0
  16. package/core/auth/index.js +123 -1
  17. package/core/auth/magic-link.js +170 -0
  18. package/core/auth/manager.js +477 -3
  19. package/core/auth/middleware.js +118 -5
  20. package/core/auth/oauth.js +517 -0
  21. package/core/auth/qrcode.js +499 -0
  22. package/core/auth/session-manager.js +255 -0
  23. package/core/auth/sessions.js +351 -0
  24. package/core/auth/tokens.js +1 -0
  25. package/core/auth/totp.js +507 -0
  26. package/core/auth/user-agent.js +161 -0
  27. package/core/orm/eager-loader.js +144 -10
  28. package/core/orm/index.js +10 -1
  29. package/core/orm/model.js +55 -1
  30. package/core/orm/query-builder.js +90 -4
  31. package/core/orm/repository.js +327 -16
  32. package/core/orm/scopes.js +3 -2
  33. package/core/orm/search.js +213 -0
  34. package/core/orm/types.js +6 -1
  35. package/core/queue/cron.js +544 -0
  36. package/core/queue/index.js +8 -1
  37. package/core/queue/manager.js +100 -1
  38. package/core/queue/scheduler.js +415 -0
  39. package/core/sse/index.js +19 -0
  40. package/core/sse/stream.js +430 -0
  41. package/core/ssr/stream.js +62 -2
  42. package/index.d.ts +794 -6
  43. package/index.js +48 -0
  44. package/package.json +7 -5
  45. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
  46. package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
  47. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  48. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  49. package/plugins/admin-panel/field-renderers/index.js +2 -0
  50. package/plugins/admin-panel/modules/admin-users.js +1 -1
  51. package/plugins/api-key/crypto.js +101 -0
  52. package/plugins/api-key/index.js +288 -0
  53. package/plugins/api-key/middleware.js +461 -0
  54. package/plugins/api-key/migration-template.js +48 -0
  55. package/plugins/api-key/model.js +50 -0
  56. package/plugins/api-key/scopes.js +125 -0
  57. package/plugins/audit-log/admin-component.js +487 -70
  58. package/plugins/audit-log/api-handlers.js +114 -0
  59. package/plugins/audit-log/index.js +24 -0
  60. package/plugins/content/client/inline-edit.js +2 -46
  61. package/plugins/cors.js +13 -2
  62. package/plugins/email/auth-bridge.js +66 -0
  63. package/plugins/email/bundled-templates.js +15 -0
  64. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  65. package/plugins/file-manager/admin-component.js +69 -53
  66. package/plugins/file-manager/index.js +25 -8
  67. package/plugins/index.js +8 -0
  68. package/plugins/oauth/index.js +338 -0
  69. package/plugins/queue/index.js +4 -0
  70. package/plugins/webhooks/admin-component.js +608 -0
  71. package/plugins/webhooks/api-handlers.js +237 -0
  72. package/plugins/webhooks/crypto.js +144 -0
  73. package/plugins/webhooks/dispatcher.js +708 -0
  74. package/plugins/webhooks/index.js +263 -0
  75. package/plugins/webhooks/matcher.js +113 -0
  76. package/plugins/webhooks/migration-template.js +75 -0
  77. package/plugins/webhooks/models.js +98 -0
  78. package/plugins/webhooks/sender.js +176 -0
  79. package/src/api/api-loader.js +1 -0
  80. package/src/bundle/index.js +2 -1
  81. package/src/discovery/file-route-parser.js +44 -1
  82. package/src/discovery/scan-routes.js +2 -2
  83. package/src/file-router.js +310 -104
  84. package/src/helpers.js +16 -1
  85. package/src/njk-frontmatter.js +3 -0
  86. package/src/routing/cascading.js +490 -0
  87. package/src/routing/middleware-resolver.js +92 -0
  88. package/src/server.js +6 -0
@@ -10,6 +10,8 @@ const path = require('path');
10
10
  const fs = require('fs');
11
11
  const { Job } = require('./job');
12
12
  const { MemoryQueueAdapter } = require('./adapters/memory');
13
+ const { CronExpression, CRON_ALIASES } = require('./cron');
14
+ const { TaskScheduler } = require('./scheduler');
13
15
 
14
16
  class QueueManager extends EventEmitter {
15
17
  /**
@@ -78,6 +80,12 @@ class QueueManager extends EventEmitter {
78
80
  this.startCleanup();
79
81
  }
80
82
 
83
+ // Initialize TaskScheduler for recurring cron tasks
84
+ this.scheduler = new TaskScheduler(this, {
85
+ autoStart: options.autoStart !== false,
86
+ ...(options.scheduler || {}),
87
+ });
88
+
81
89
  if (this.jobsDir && fs.existsSync(this.jobsDir)) {
82
90
  this.loadJobsFromDirectory(this.jobsDir);
83
91
  }
@@ -181,6 +189,9 @@ class QueueManager extends EventEmitter {
181
189
  this.isPaused = false;
182
190
  this._currentPollInterval = this.pollInterval;
183
191
  this._scheduleNextTick(0);
192
+ if (this.scheduler) {
193
+ this.scheduler.start();
194
+ }
184
195
  this.emit('start');
185
196
  return this;
186
197
  }
@@ -194,6 +205,9 @@ class QueueManager extends EventEmitter {
194
205
  clearTimeout(this._timer);
195
206
  this._timer = null;
196
207
  }
208
+ if (this.scheduler) {
209
+ this.scheduler.stop();
210
+ }
197
211
  this.emit('pause');
198
212
  return this;
199
213
  }
@@ -206,6 +220,9 @@ class QueueManager extends EventEmitter {
206
220
  this.isPaused = false;
207
221
  this._currentPollInterval = this.pollInterval;
208
222
  this._scheduleNextTick(0);
223
+ if (this.scheduler) {
224
+ this.scheduler.start();
225
+ }
209
226
  this.emit('resume');
210
227
  return this;
211
228
  }
@@ -218,6 +235,9 @@ class QueueManager extends EventEmitter {
218
235
  async drain(timeoutMs = 10000) {
219
236
  this.isDraining = true;
220
237
  this.pause();
238
+ if (this.scheduler) {
239
+ this.scheduler.stop();
240
+ }
221
241
 
222
242
  if (this.runningCount === 0) {
223
243
  this.isDraining = false;
@@ -245,10 +265,76 @@ class QueueManager extends EventEmitter {
245
265
  async stop() {
246
266
  this.pause();
247
267
  this.isRunning = false;
268
+ if (this.scheduler) {
269
+ this.scheduler.stop();
270
+ }
248
271
  this.stopCleanup();
249
272
  this.emit('stop');
250
273
  }
251
274
 
275
+ /**
276
+ * Schedule a recurring cron job
277
+ * @param {string|CronExpression} cronExpression - Cron pattern or alias (e.g. '0 0 * * *', '@daily')
278
+ * @param {string} name - Registered job name
279
+ * @param {any} [data={}] - Job data payload
280
+ * @param {Object} [options={}] - Schedule options (overlap, maxRuns, utc, runImmediately)
281
+ * @returns {Object} Registered schedule object
282
+ */
283
+ schedule(cronExpression, name, data = {}, options = {}) {
284
+ return this.scheduler.schedule(cronExpression, name, data, options);
285
+ }
286
+
287
+ /**
288
+ * Alias for schedule()
289
+ */
290
+ cron(cronExpression, name, data, options) {
291
+ return this.schedule(cronExpression, name, data, options);
292
+ }
293
+
294
+ /**
295
+ * Unschedule a recurring job by ID or job name
296
+ * @param {string} idOrJobName
297
+ * @returns {boolean}
298
+ */
299
+ unschedule(idOrJobName) {
300
+ return this.scheduler.unschedule(idOrJobName);
301
+ }
302
+
303
+ /**
304
+ * Get all registered schedules
305
+ * @returns {Array<Object>}
306
+ */
307
+ getSchedules() {
308
+ return this.scheduler.getSchedules();
309
+ }
310
+
311
+ /**
312
+ * Get single schedule by ID or job name
313
+ * @param {string} idOrJobName
314
+ * @returns {Object|null}
315
+ */
316
+ getSchedule(idOrJobName) {
317
+ return this.scheduler.getSchedule(idOrJobName);
318
+ }
319
+
320
+ /**
321
+ * Manually trigger a scheduled job immediately
322
+ * @param {string} idOrJobName
323
+ * @returns {Promise<Job|null>}
324
+ */
325
+ triggerSchedule(idOrJobName) {
326
+ return this.scheduler.trigger(idOrJobName);
327
+ }
328
+
329
+ /**
330
+ * Alias for triggerSchedule()
331
+ * @param {string} idOrJobName
332
+ * @returns {Promise<Job|null>}
333
+ */
334
+ triggerCron(idOrJobName) {
335
+ return this.scheduler.trigger(idOrJobName);
336
+ }
337
+
252
338
  /**
253
339
  * Delete completed or failed jobs older than a cutoff date
254
340
  * @param {Object} [options={}]
@@ -371,7 +457,10 @@ class QueueManager extends EventEmitter {
371
457
  if (entry.isDirectory()) {
372
458
  readRecursive(fullPath, basePrefix ? `${basePrefix}.${entry.name}` : entry.name);
373
459
  } else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.cjs'))) {
374
- const nameWithoutExt = entry.name.replace(/\.(c)?js$/, '');
460
+ const isCronFile = entry.name.endsWith('.cron.js') || entry.name.endsWith('.cron.cjs');
461
+ const nameWithoutExt = isCronFile
462
+ ? entry.name.replace(/\.cron\.(c)?js$/, '')
463
+ : entry.name.replace(/\.(c)?js$/, '');
375
464
  const jobName = basePrefix ? `${basePrefix}.${nameWithoutExt}` : nameWithoutExt;
376
465
 
377
466
  try {
@@ -381,6 +470,16 @@ class QueueManager extends EventEmitter {
381
470
 
382
471
  if (typeof handler === 'function') {
383
472
  this.define(jobName, handler, options);
473
+
474
+ const cronExpr = mod.cron || mod.schedule || (isCronFile ? (mod.expression || (CRON_ALIASES[`@${nameWithoutExt}`] ? `@${nameWithoutExt}` : null)) : null);
475
+ if (cronExpr) {
476
+ this.schedule(
477
+ cronExpr,
478
+ jobName,
479
+ mod.data || {},
480
+ { ...options, ...(mod.scheduleOptions || {}) }
481
+ );
482
+ }
384
483
  }
385
484
  } catch (err) {
386
485
  console.error(`[QueueManager] Failed to load job from ${fullPath}:`, err.message);
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Webspresso Task Scheduler Engine
3
+ * Manages recurring cron jobs, overlap prevention, and worker dispatching
4
+ * @module core/queue/scheduler
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { CronExpression } = require('./cron');
10
+
11
+ class TaskScheduler {
12
+ /**
13
+ * @param {import('./manager').QueueManager} queue - QueueManager instance
14
+ * @param {Object} [options]
15
+ * @param {boolean} [options.autoStart=true]
16
+ * @param {number} [options.maxIntervalMs=60000] - Maximum sleep interval in ms
17
+ */
18
+ constructor(queue, options = {}) {
19
+ if (!queue) {
20
+ throw new Error('TaskScheduler requires a QueueManager instance');
21
+ }
22
+
23
+ this.queue = queue;
24
+ this.maxIntervalMs = Math.max(100, options.maxIntervalMs || 60000);
25
+ this.isRunning = false;
26
+ this._timer = null;
27
+
28
+ /** @type {Map<string, Object>} */
29
+ this.schedules = new Map();
30
+
31
+ // Hook into queue events to track job lifecycle for overlap prevention
32
+ this._onJobCompleted = (job, result) => {
33
+ this._handleJobSettled(job, null, result);
34
+ };
35
+
36
+ this._onJobFailed = (job, err) => {
37
+ this._handleJobSettled(job, err, null);
38
+ };
39
+
40
+ this.queue.on('job:completed', this._onJobCompleted);
41
+ this.queue.on('job:failed', this._onJobFailed);
42
+
43
+ if (options.autoStart !== false && queue.isRunning) {
44
+ this.start();
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Register a new recurring scheduled task
50
+ * @param {string|CronExpression} cronExpression - Cron pattern or alias (e.g. '0 0 * * *', '@daily')
51
+ * @param {string} jobName - Name of the registered job in QueueManager
52
+ * @param {any} [data={}] - Payload passed to job on dispatch
53
+ * @param {Object} [options]
54
+ * @param {string} [options.id] - Optional custom schedule ID
55
+ * @param {boolean} [options.overlap=false] - Allow concurrent executions of the same schedule
56
+ * @param {boolean} [options.preventOverlap] - Inverted alias for overlap: false
57
+ * @param {number} [options.maxRuns] - Maximum number of runs before auto-unscheduling
58
+ * @param {boolean} [options.utc=false] - Evaluate schedule in UTC
59
+ * @param {string} [options.timezone] - IANA Timezone (e.g. 'Europe/Istanbul')
60
+ * @param {string} [options.tz] - Alias for timezone
61
+ * @param {boolean} [options.runImmediately=false] - Run immediately upon registration
62
+ * @returns {Object} Registered schedule object
63
+ */
64
+ schedule(cronExpression, jobName, data = {}, options = {}) {
65
+ if (typeof jobName !== 'string' || !jobName.trim()) {
66
+ throw new Error('Job name must be a non-empty string');
67
+ }
68
+
69
+ const cron = cronExpression instanceof CronExpression
70
+ ? cronExpression
71
+ : new CronExpression(cronExpression, {
72
+ utc: !!options.utc,
73
+ timezone: options.timezone || options.tz,
74
+ });
75
+
76
+ const id = options.id || `schedule:${jobName}:${cron.resolvedExpression}`;
77
+
78
+ const now = new Date();
79
+ const nextRunAt = cron.getNextDate(now);
80
+
81
+ const allowOverlap = options.overlap === true || (options.preventOverlap === false);
82
+
83
+ const scheduleObj = {
84
+ id,
85
+ name: jobName,
86
+ cronExpression: cron.rawExpression,
87
+ cron,
88
+ data,
89
+ options: {
90
+ overlap: allowOverlap,
91
+ preventOverlap: !allowOverlap,
92
+ maxRuns: typeof options.maxRuns === 'number' && options.maxRuns > 0 ? options.maxRuns : null,
93
+ runImmediately: !!options.runImmediately,
94
+ ...options,
95
+ },
96
+ runsCount: 0,
97
+ lastRunAt: null,
98
+ nextRunAt,
99
+ isRunning: false,
100
+ activeJobId: null,
101
+ activeJobIds: new Set(),
102
+ registeredAt: now,
103
+ };
104
+
105
+ this.schedules.set(id, scheduleObj);
106
+ this.queue.emit('schedule:registered', scheduleObj);
107
+
108
+ if (options.runImmediately) {
109
+ setImmediate(() => {
110
+ this.trigger(id).catch((err) => {
111
+ this.queue.emit('schedule:error', scheduleObj, err);
112
+ });
113
+ });
114
+ }
115
+
116
+ this._rescheduleTimer();
117
+ return scheduleObj;
118
+ }
119
+
120
+ /**
121
+ * Alias for schedule()
122
+ */
123
+ cron(cronExpression, jobName, data, options) {
124
+ return this.schedule(cronExpression, jobName, data, options);
125
+ }
126
+
127
+ /**
128
+ * Remove a scheduled task by ID or job name
129
+ * @param {string} idOrJobName
130
+ * @returns {boolean} True if removed, false if not found
131
+ */
132
+ unschedule(idOrJobName) {
133
+ if (this.schedules.has(idOrJobName)) {
134
+ const scheduleObj = this.schedules.get(idOrJobName);
135
+ this.schedules.delete(idOrJobName);
136
+ this.queue.emit('schedule:unregistered', scheduleObj);
137
+ this._rescheduleTimer();
138
+ return true;
139
+ }
140
+
141
+ const toRemove = [];
142
+ for (const [id, s] of this.schedules.entries()) {
143
+ if (s.name === idOrJobName) {
144
+ toRemove.push({ id, s });
145
+ }
146
+ }
147
+
148
+ if (toRemove.length === 0) return false;
149
+
150
+ for (const { id, s } of toRemove) {
151
+ this.schedules.delete(id);
152
+ this.queue.emit('schedule:unregistered', s);
153
+ }
154
+
155
+ this._rescheduleTimer();
156
+ return true;
157
+ }
158
+
159
+ /**
160
+ * Get all registered schedules
161
+ * @returns {Array<Object>}
162
+ */
163
+ getSchedules() {
164
+ return Array.from(this.schedules.values()).map((s) => ({
165
+ id: s.id,
166
+ name: s.name,
167
+ cronExpression: s.cronExpression,
168
+ data: s.data,
169
+ options: s.options,
170
+ runsCount: s.runsCount,
171
+ lastRunAt: s.lastRunAt,
172
+ nextRunAt: s.nextRunAt,
173
+ isRunning: s.isRunning,
174
+ activeJobId: s.activeJobId,
175
+ activeJobIds: Array.from(s.activeJobIds || []),
176
+ registeredAt: s.registeredAt,
177
+ }));
178
+ }
179
+
180
+ /**
181
+ * Get single schedule by ID or job name
182
+ * @param {string} idOrJobName
183
+ * @returns {Object|null}
184
+ */
185
+ getSchedule(idOrJobName) {
186
+ if (this.schedules.has(idOrJobName)) {
187
+ return this.schedules.get(idOrJobName);
188
+ }
189
+ for (const s of this.schedules.values()) {
190
+ if (s.name === idOrJobName) return s;
191
+ }
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * Manually trigger a schedule immediately
197
+ * @param {string} idOrJobName
198
+ * @returns {Promise<any>}
199
+ */
200
+ async trigger(idOrJobName) {
201
+ const s = this.getSchedule(idOrJobName);
202
+ if (!s) {
203
+ throw new Error(`Schedule not found: '${idOrJobName}'`);
204
+ }
205
+
206
+ const allowOverlap = s.options.overlap === true || s.options.preventOverlap === false;
207
+ if (!allowOverlap && s.isRunning) {
208
+ this.queue.emit('schedule:skipped', s, 'overlap');
209
+ return null;
210
+ }
211
+
212
+ s.isRunning = true;
213
+ s.lastRunAt = new Date();
214
+ s.runsCount++;
215
+
216
+ try {
217
+ const job = await this.queue.dispatch(s.name, s.data, s.options);
218
+ s.activeJobId = job.id;
219
+ if (!s.activeJobIds) s.activeJobIds = new Set();
220
+ s.activeJobIds.add(job.id);
221
+
222
+ this.queue.emit('schedule:triggered', s, job);
223
+
224
+ if (s.options.maxRuns && s.runsCount >= s.options.maxRuns) {
225
+ this.unschedule(s.id);
226
+ }
227
+
228
+ return job;
229
+ } catch (err) {
230
+ if (!s.activeJobIds || s.activeJobIds.size === 0) {
231
+ s.isRunning = false;
232
+ s.activeJobId = null;
233
+ }
234
+ throw err;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Start scheduler timer loop
240
+ */
241
+ start() {
242
+ if (this.isRunning) return this;
243
+ this.isRunning = true;
244
+ this._rescheduleTimer();
245
+ this.queue.emit('schedule:start');
246
+ return this;
247
+ }
248
+
249
+ /**
250
+ * Stop scheduler timer loop
251
+ */
252
+ stop() {
253
+ this.isRunning = false;
254
+ if (this._timer) {
255
+ clearTimeout(this._timer);
256
+ this._timer = null;
257
+ }
258
+ this.queue.emit('schedule:stop');
259
+ return this;
260
+ }
261
+
262
+ /**
263
+ * Pause alias for stop
264
+ */
265
+ pause() {
266
+ return this.stop();
267
+ }
268
+
269
+ /**
270
+ * Resume alias for start
271
+ */
272
+ resume() {
273
+ return this.start();
274
+ }
275
+
276
+ /**
277
+ * Clear all schedules and stop timer
278
+ */
279
+ clear() {
280
+ this.stop();
281
+ this.schedules.clear();
282
+ }
283
+
284
+ /**
285
+ * Cleanup event listeners
286
+ */
287
+ destroy() {
288
+ this.stop();
289
+ this.schedules.clear();
290
+ this.queue.removeListener('job:completed', this._onJobCompleted);
291
+ this.queue.removeListener('job:failed', this._onJobFailed);
292
+ }
293
+
294
+ /**
295
+ * Internal job settled handler for overlap tracking
296
+ * @private
297
+ */
298
+ _handleJobSettled(job, err, result) {
299
+ for (const s of this.schedules.values()) {
300
+ if (s.activeJobIds?.has(job.id) || s.activeJobId === job.id) {
301
+ if (s.activeJobIds) {
302
+ s.activeJobIds.delete(job.id);
303
+ }
304
+ if (!s.activeJobIds || s.activeJobIds.size === 0) {
305
+ s.isRunning = false;
306
+ s.activeJobId = null;
307
+ } else {
308
+ s.activeJobId = s.activeJobIds.values().next().value || null;
309
+ }
310
+ this.queue.emit('schedule:finished', s, job, err ? { error: err } : { result });
311
+ break;
312
+ }
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Re-evaluates next timer tick according to earliest due schedule
318
+ * @private
319
+ */
320
+ _rescheduleTimer() {
321
+ if (!this.isRunning) return;
322
+
323
+ if (this._timer) {
324
+ clearTimeout(this._timer);
325
+ this._timer = null;
326
+ }
327
+
328
+ if (this.schedules.size === 0) return;
329
+
330
+ const now = Date.now();
331
+ let earliest = Infinity;
332
+
333
+ for (const s of this.schedules.values()) {
334
+ if (s.nextRunAt) {
335
+ const time = s.nextRunAt.getTime();
336
+ if (time < earliest) {
337
+ earliest = time;
338
+ }
339
+ }
340
+ }
341
+
342
+ if (earliest === Infinity) return;
343
+
344
+ const delay = Math.max(0, Math.min(earliest - now, this.maxIntervalMs));
345
+
346
+ this._timer = setTimeout(() => this._tick(), delay);
347
+ }
348
+
349
+ /**
350
+ * Clock tick: evaluates due schedules and dispatches them
351
+ * @private
352
+ */
353
+ async _tick() {
354
+ if (!this.isRunning) return;
355
+
356
+ try {
357
+ const now = new Date();
358
+ const nowTime = now.getTime();
359
+
360
+ const dueList = [];
361
+ for (const s of this.schedules.values()) {
362
+ if (s.nextRunAt && s.nextRunAt.getTime() <= nowTime) {
363
+ dueList.push(s);
364
+ }
365
+ }
366
+
367
+ for (const s of dueList) {
368
+ if (!this.isRunning) break;
369
+
370
+ // Advance next run date immediately to avoid double tick
371
+ s.nextRunAt = s.cron.getNextDate(now);
372
+
373
+ const allowOverlap = s.options.overlap === true || s.options.preventOverlap === false;
374
+ if (!allowOverlap && s.isRunning) {
375
+ this.queue.emit('schedule:skipped', s, 'overlap');
376
+ s.lastRunAt = now;
377
+ continue;
378
+ }
379
+
380
+ s.isRunning = true;
381
+ s.lastRunAt = now;
382
+ s.runsCount++;
383
+
384
+ try {
385
+ const job = await this.queue.dispatch(s.name, s.data, s.options);
386
+ s.activeJobId = job.id;
387
+ if (!s.activeJobIds) s.activeJobIds = new Set();
388
+ s.activeJobIds.add(job.id);
389
+
390
+ this.queue.emit('schedule:triggered', s, job);
391
+ } catch (err) {
392
+ if (!s.activeJobIds || s.activeJobIds.size === 0) {
393
+ s.isRunning = false;
394
+ s.activeJobId = null;
395
+ }
396
+ this.queue.emit('schedule:error', s, err);
397
+ }
398
+
399
+ // Check maxRuns
400
+ if (s.options.maxRuns && s.runsCount >= s.options.maxRuns) {
401
+ this.unschedule(s.id);
402
+ }
403
+ }
404
+ } catch (err) {
405
+ this.queue.emit('schedule:error', null, err);
406
+ } finally {
407
+ this._rescheduleTimer();
408
+ }
409
+ }
410
+ }
411
+
412
+ module.exports = {
413
+ TaskScheduler,
414
+ };
415
+
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Webspresso Server-Sent Events (SSE) Module
3
+ * @module core/sse
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const {
9
+ ServerSentEventsStream,
10
+ createSseStream,
11
+ formatSseFrame,
12
+ } = require('./stream');
13
+
14
+ module.exports = {
15
+ ServerSentEventsStream,
16
+ createSseStream,
17
+ formatSseFrame,
18
+ sse: createSseStream,
19
+ };