@flowdot.ai/daemon 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +51 -0
  3. package/dist/goals/DependencyResolver.d.ts +54 -0
  4. package/dist/goals/DependencyResolver.js +329 -0
  5. package/dist/goals/ErrorRecovery.d.ts +133 -0
  6. package/dist/goals/ErrorRecovery.js +489 -0
  7. package/dist/goals/GoalApiClient.d.ts +81 -0
  8. package/dist/goals/GoalApiClient.js +743 -0
  9. package/dist/goals/GoalCache.d.ts +65 -0
  10. package/dist/goals/GoalCache.js +243 -0
  11. package/dist/goals/GoalCommsHandler.d.ts +150 -0
  12. package/dist/goals/GoalCommsHandler.js +378 -0
  13. package/dist/goals/GoalExporter.d.ts +164 -0
  14. package/dist/goals/GoalExporter.js +318 -0
  15. package/dist/goals/GoalImporter.d.ts +107 -0
  16. package/dist/goals/GoalImporter.js +345 -0
  17. package/dist/goals/GoalManager.d.ts +110 -0
  18. package/dist/goals/GoalManager.js +535 -0
  19. package/dist/goals/GoalReporter.d.ts +105 -0
  20. package/dist/goals/GoalReporter.js +534 -0
  21. package/dist/goals/GoalScheduler.d.ts +102 -0
  22. package/dist/goals/GoalScheduler.js +209 -0
  23. package/dist/goals/GoalValidator.d.ts +72 -0
  24. package/dist/goals/GoalValidator.js +657 -0
  25. package/dist/goals/MetaGoalEnforcer.d.ts +111 -0
  26. package/dist/goals/MetaGoalEnforcer.js +536 -0
  27. package/dist/goals/MilestoneBreaker.d.ts +74 -0
  28. package/dist/goals/MilestoneBreaker.js +348 -0
  29. package/dist/goals/PermissionBridge.d.ts +109 -0
  30. package/dist/goals/PermissionBridge.js +326 -0
  31. package/dist/goals/ProgressTracker.d.ts +113 -0
  32. package/dist/goals/ProgressTracker.js +324 -0
  33. package/dist/goals/ReviewScheduler.d.ts +106 -0
  34. package/dist/goals/ReviewScheduler.js +360 -0
  35. package/dist/goals/TaskExecutor.d.ts +116 -0
  36. package/dist/goals/TaskExecutor.js +370 -0
  37. package/dist/goals/TaskFeedback.d.ts +126 -0
  38. package/dist/goals/TaskFeedback.js +402 -0
  39. package/dist/goals/TaskGenerator.d.ts +75 -0
  40. package/dist/goals/TaskGenerator.js +329 -0
  41. package/dist/goals/TaskQueue.d.ts +84 -0
  42. package/dist/goals/TaskQueue.js +331 -0
  43. package/dist/goals/TaskSanitizer.d.ts +61 -0
  44. package/dist/goals/TaskSanitizer.js +464 -0
  45. package/dist/goals/errors.d.ts +116 -0
  46. package/dist/goals/errors.js +299 -0
  47. package/dist/goals/index.d.ts +24 -0
  48. package/dist/goals/index.js +23 -0
  49. package/dist/goals/types.d.ts +395 -0
  50. package/dist/goals/types.js +230 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.js +3 -0
  53. package/dist/loop/DaemonIPC.d.ts +67 -0
  54. package/dist/loop/DaemonIPC.js +358 -0
  55. package/dist/loop/IntervalParser.d.ts +39 -0
  56. package/dist/loop/IntervalParser.js +217 -0
  57. package/dist/loop/LoopDaemon.d.ts +123 -0
  58. package/dist/loop/LoopDaemon.js +1821 -0
  59. package/dist/loop/LoopExecutor.d.ts +93 -0
  60. package/dist/loop/LoopExecutor.js +326 -0
  61. package/dist/loop/LoopManager.d.ts +79 -0
  62. package/dist/loop/LoopManager.js +476 -0
  63. package/dist/loop/LoopScheduler.d.ts +69 -0
  64. package/dist/loop/LoopScheduler.js +329 -0
  65. package/dist/loop/LoopStore.d.ts +57 -0
  66. package/dist/loop/LoopStore.js +406 -0
  67. package/dist/loop/LoopValidator.d.ts +55 -0
  68. package/dist/loop/LoopValidator.js +603 -0
  69. package/dist/loop/errors.d.ts +115 -0
  70. package/dist/loop/errors.js +312 -0
  71. package/dist/loop/index.d.ts +11 -0
  72. package/dist/loop/index.js +10 -0
  73. package/dist/loop/notifications/Notifier.d.ts +28 -0
  74. package/dist/loop/notifications/Notifier.js +78 -0
  75. package/dist/loop/notifications/SlackNotifier.d.ts +28 -0
  76. package/dist/loop/notifications/SlackNotifier.js +203 -0
  77. package/dist/loop/notifications/TerminalNotifier.d.ts +18 -0
  78. package/dist/loop/notifications/TerminalNotifier.js +72 -0
  79. package/dist/loop/notifications/WebhookNotifier.d.ts +24 -0
  80. package/dist/loop/notifications/WebhookNotifier.js +123 -0
  81. package/dist/loop/notifications/index.d.ts +24 -0
  82. package/dist/loop/notifications/index.js +109 -0
  83. package/dist/loop/types.d.ts +280 -0
  84. package/dist/loop/types.js +222 -0
  85. package/package.json +92 -0
@@ -0,0 +1,476 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import msPkg from 'ms';
4
+ function parseDurationString(value) {
5
+ return msPkg(value);
6
+ }
7
+ import { DEFAULT_LOOP_OPTIONS, DEFAULT_LOOP_CONFIG, INITIAL_LOOP_STATS, } from './types.js';
8
+ import { LoopError, LoopNameTakenError, LoopInvalidStatusError, LoopLimitExceededError, LoopExpiredError, } from './errors.js';
9
+ import { LoopStore } from './LoopStore.js';
10
+ import { LoopScheduler } from './LoopScheduler.js';
11
+ import { LoopExecutor } from './LoopExecutor.js';
12
+ import { LoopValidator } from './LoopValidator.js';
13
+ import { IntervalParser } from './IntervalParser.js';
14
+ const noopLogger = {
15
+ debug: () => { },
16
+ info: () => { },
17
+ warn: () => { },
18
+ error: () => { },
19
+ };
20
+ export class LoopManager extends EventEmitter {
21
+ store;
22
+ scheduler;
23
+ executor;
24
+ validator;
25
+ intervalParser;
26
+ config;
27
+ logger;
28
+ initialized = false;
29
+ constructor(options = {}) {
30
+ super();
31
+ this.logger = options.logger ?? noopLogger;
32
+ this.config = {
33
+ ...DEFAULT_LOOP_CONFIG,
34
+ ...options.config,
35
+ daemon: {
36
+ ...DEFAULT_LOOP_CONFIG.daemon,
37
+ ...options.config?.daemon,
38
+ },
39
+ webhooks: {
40
+ ...DEFAULT_LOOP_CONFIG.webhooks,
41
+ ...options.config?.webhooks,
42
+ },
43
+ };
44
+ const minIntervalMs = parseDurationString(this.config.minInterval) ?? 60000;
45
+ const maxRunDurationMs = parseDurationString(this.config.maxRunDuration) ?? 30 * 60 * 1000;
46
+ this.store = new LoopStore({
47
+ baseDir: options.baseDir,
48
+ logger: this.logger,
49
+ });
50
+ const schedulerOptions = {
51
+ driftThreshold: 5000,
52
+ logger: this.logger,
53
+ };
54
+ this.scheduler = new LoopScheduler(schedulerOptions);
55
+ const executorOptions = {
56
+ defaultTimeoutMs: maxRunDurationMs,
57
+ getAgenticResponse: options.getAgenticResponse,
58
+ executeAction: options.executeAction,
59
+ logger: this.logger,
60
+ };
61
+ this.executor = new LoopExecutor(executorOptions);
62
+ this.validator = new LoopValidator({
63
+ maxPromptLength: 10000,
64
+ maxNameLength: 100,
65
+ }, { minIntervalMs });
66
+ this.intervalParser = new IntervalParser({
67
+ minIntervalMs,
68
+ });
69
+ this.setupEventHandlers();
70
+ }
71
+ async initialize() {
72
+ if (this.initialized) {
73
+ return;
74
+ }
75
+ this.logger.info('LOOP', 'Initializing LoopManager');
76
+ await this.store.initialize();
77
+ const loops = await this.store.getAllLoops();
78
+ let scheduledCount = 0;
79
+ for (const loop of loops) {
80
+ if (this.isLoopExpired(loop)) {
81
+ await this.handleExpiredLoop(loop);
82
+ continue;
83
+ }
84
+ if (loop.status === 'running') {
85
+ try {
86
+ this.scheduler.schedule(loop);
87
+ scheduledCount++;
88
+ }
89
+ catch (error) {
90
+ this.logger.error('LOOP', `Failed to schedule loop ${loop.id}`, {
91
+ error: error instanceof Error ? error.message : String(error),
92
+ });
93
+ }
94
+ }
95
+ }
96
+ this.logger.info('LOOP', `Initialized with ${loops.length} loops, ${scheduledCount} scheduled`);
97
+ this.initialized = true;
98
+ }
99
+ start() {
100
+ this.ensureInitialized();
101
+ this.scheduler.start();
102
+ this.logger.info('LOOP', 'LoopManager started');
103
+ }
104
+ stop() {
105
+ this.scheduler.stop();
106
+ this.executor.cancelAllExecutions();
107
+ this.logger.info('LOOP', 'LoopManager stopped');
108
+ }
109
+ async shutdown() {
110
+ this.logger.info('LOOP', 'Shutting down LoopManager');
111
+ this.stop();
112
+ this.removeAllListeners();
113
+ this.initialized = false;
114
+ }
115
+ async createLoop(input) {
116
+ this.ensureInitialized();
117
+ this.logger.debug('LOOP', 'Creating loop', { prompt: input.prompt.substring(0, 50) });
118
+ const validationResult = this.validator.validateCreateInput(input);
119
+ if (validationResult && validationResult.valid === false) {
120
+ const firstError = validationResult.errors?.[0];
121
+ if (firstError instanceof Error) {
122
+ throw firstError;
123
+ }
124
+ throw new LoopError('VALIDATION_ERROR', typeof firstError?.message === 'string' ? firstError.message : 'Invalid loop input');
125
+ }
126
+ const activeCount = await this.store.getActiveLoopCount();
127
+ if (activeCount >= this.config.maxConcurrentLoops) {
128
+ throw new LoopLimitExceededError(activeCount, this.config.maxConcurrentLoops);
129
+ }
130
+ if (input.name) {
131
+ const nameTaken = await this.store.isNameTaken(input.name);
132
+ if (nameTaken) {
133
+ throw new LoopNameTakenError(input.name);
134
+ }
135
+ }
136
+ const interval = this.intervalParser.parse(input.interval);
137
+ const expiresAt = this.calculateExpiration(input.expires);
138
+ const options = {
139
+ ...DEFAULT_LOOP_OPTIONS,
140
+ model: input.model ?? DEFAULT_LOOP_OPTIONS.model,
141
+ maxRuns: input.maxRuns ?? DEFAULT_LOOP_OPTIONS.maxRuns,
142
+ onError: input.onError ?? this.config.defaultOnError,
143
+ notify: input.notify ?? this.config.defaultNotify,
144
+ webhookUrl: input.webhookUrl ?? DEFAULT_LOOP_OPTIONS.webhookUrl,
145
+ quiet: input.quiet ?? DEFAULT_LOOP_OPTIONS.quiet,
146
+ };
147
+ const now = new Date();
148
+ const loop = {
149
+ id: uuidv4(),
150
+ name: input.name ?? null,
151
+ prompt: input.prompt,
152
+ interval,
153
+ status: input.dryRun ? 'paused' : 'running',
154
+ createdAt: now,
155
+ expiresAt,
156
+ options,
157
+ stats: { ...INITIAL_LOOP_STATS },
158
+ cwd: input.cwd ?? process.cwd(),
159
+ };
160
+ await this.store.createLoop(loop);
161
+ if (!input.dryRun) {
162
+ this.scheduler.schedule(loop);
163
+ }
164
+ this.logger.info('LOOP', `Created loop ${loop.id}`, {
165
+ name: loop.name,
166
+ interval: loop.interval.raw,
167
+ status: loop.status,
168
+ });
169
+ this.emit('created', loop);
170
+ if (!input.dryRun) {
171
+ this.emit('started', loop);
172
+ }
173
+ return loop;
174
+ }
175
+ async getLoop(loopId) {
176
+ this.ensureInitialized();
177
+ return this.store.getLoop(loopId);
178
+ }
179
+ async getLoopByName(name) {
180
+ this.ensureInitialized();
181
+ return this.store.getLoopByName(name);
182
+ }
183
+ async getAllLoops() {
184
+ this.ensureInitialized();
185
+ return this.store.getAllLoops();
186
+ }
187
+ async getLoopsByStatus(status) {
188
+ this.ensureInitialized();
189
+ return this.store.getLoopsByStatus(status);
190
+ }
191
+ async pauseLoop(loopId) {
192
+ this.ensureInitialized();
193
+ const loop = await this.store.getLoop(loopId);
194
+ if (loop.status !== 'running') {
195
+ throw new LoopInvalidStatusError(loopId, loop.status, 'pause', ['running']);
196
+ }
197
+ loop.status = 'paused';
198
+ await this.store.updateLoop(loop);
199
+ this.scheduler.pause(loopId);
200
+ this.executor.cancelAll(loopId);
201
+ this.logger.info('LOOP', `Paused loop ${loopId}`);
202
+ this.emit('paused', loop);
203
+ return loop;
204
+ }
205
+ async resumeLoop(loopId) {
206
+ this.ensureInitialized();
207
+ const loop = await this.store.getLoop(loopId);
208
+ if (loop.status !== 'paused') {
209
+ throw new LoopInvalidStatusError(loopId, loop.status, 'resume', ['paused']);
210
+ }
211
+ if (this.isLoopExpired(loop)) {
212
+ await this.handleExpiredLoop(loop);
213
+ throw new LoopExpiredError(loopId, loop.expiresAt);
214
+ }
215
+ const activeCount = await this.store.getActiveLoopCount();
216
+ if (activeCount >= this.config.maxConcurrentLoops) {
217
+ throw new LoopLimitExceededError(activeCount, this.config.maxConcurrentLoops);
218
+ }
219
+ loop.status = 'running';
220
+ loop.interval.nextRunAt = this.intervalParser.calculateNextRun(loop.interval);
221
+ await this.store.updateLoop(loop);
222
+ this.scheduler.resume(loopId);
223
+ this.logger.info('LOOP', `Resumed loop ${loopId}`);
224
+ this.emit('resumed', loop);
225
+ return loop;
226
+ }
227
+ async stopLoop(loopId) {
228
+ this.ensureInitialized();
229
+ const loop = await this.store.getLoop(loopId);
230
+ if (loop.status === 'stopped') {
231
+ throw new LoopInvalidStatusError(loopId, loop.status, 'stop', ['running', 'paused', 'error', 'expired']);
232
+ }
233
+ loop.status = 'stopped';
234
+ await this.store.updateLoop(loop);
235
+ this.scheduler.unschedule(loopId);
236
+ this.executor.cancelAll(loopId);
237
+ this.logger.info('LOOP', `Stopped loop ${loopId}`);
238
+ this.emit('stopped', loop);
239
+ return loop;
240
+ }
241
+ async deleteLoop(loopId) {
242
+ this.ensureInitialized();
243
+ await this.store.getLoop(loopId);
244
+ this.scheduler.unschedule(loopId);
245
+ this.executor.cancelAll(loopId);
246
+ await this.store.deleteLoop(loopId);
247
+ this.logger.info('LOOP', `Deleted loop ${loopId}`);
248
+ this.emit('deleted', loopId);
249
+ }
250
+ async runNow(loopId) {
251
+ this.ensureInitialized();
252
+ const loop = await this.store.getLoop(loopId);
253
+ if (loop.status === 'stopped' || loop.status === 'expired') {
254
+ throw new LoopInvalidStatusError(loopId, loop.status, 'run now', ['running', 'paused', 'error']);
255
+ }
256
+ if (this.isLoopExpired(loop)) {
257
+ await this.handleExpiredLoop(loop);
258
+ throw new LoopExpiredError(loopId, loop.expiresAt);
259
+ }
260
+ this.logger.info('LOOP', `Triggering immediate run for loop ${loopId}`);
261
+ const result = await this.executeLoop(loop);
262
+ return result.run;
263
+ }
264
+ async getRunHistory(loopId, limit) {
265
+ this.ensureInitialized();
266
+ await this.store.getLoop(loopId);
267
+ return this.store.getRunHistory(loopId, limit);
268
+ }
269
+ async getRun(loopId, runId) {
270
+ this.ensureInitialized();
271
+ return this.store.getRun(loopId, runId);
272
+ }
273
+ async getStats() {
274
+ this.ensureInitialized();
275
+ const allLoops = await this.store.getAllLoops();
276
+ let totalRuns = 0;
277
+ let successfulRuns = 0;
278
+ let failedRuns = 0;
279
+ const statusCounts = {
280
+ running: 0,
281
+ paused: 0,
282
+ stopped: 0,
283
+ expired: 0,
284
+ error: 0,
285
+ };
286
+ for (const loop of allLoops) {
287
+ statusCounts[loop.status]++;
288
+ totalRuns += loop.stats.totalRuns;
289
+ successfulRuns += loop.stats.successfulRuns;
290
+ failedRuns += loop.stats.failedRuns;
291
+ }
292
+ return {
293
+ totalLoops: allLoops.length,
294
+ activeLoops: statusCounts.running,
295
+ pausedLoops: statusCounts.paused,
296
+ errorLoops: statusCounts.error,
297
+ totalRuns,
298
+ successfulRuns,
299
+ failedRuns,
300
+ schedulerRunning: this.scheduler.isRunning(),
301
+ schedulerStats: this.scheduler.getStats(),
302
+ };
303
+ }
304
+ getConfig() {
305
+ return { ...this.config };
306
+ }
307
+ isInitialized() {
308
+ return this.initialized;
309
+ }
310
+ isRunning() {
311
+ return this.scheduler.isRunning();
312
+ }
313
+ setupEventHandlers() {
314
+ this.scheduler.on('execute', async (loop) => {
315
+ try {
316
+ await this.executeLoop(loop);
317
+ }
318
+ catch (error) {
319
+ this.logger.error('LOOP', `Error executing loop ${loop.id}`, {
320
+ error: error instanceof Error ? error.message : String(error),
321
+ });
322
+ this.emit('error', error instanceof Error ? error : new Error(String(error)), loop.id);
323
+ }
324
+ });
325
+ this.scheduler.on('expired', async (loopId) => {
326
+ try {
327
+ const loop = await this.store.getLoop(loopId);
328
+ await this.handleExpiredLoop(loop);
329
+ }
330
+ catch (error) {
331
+ this.logger.error('LOOP', `Error handling expired loop ${loopId}`, {
332
+ error: error instanceof Error ? error.message : String(error),
333
+ });
334
+ }
335
+ });
336
+ this.scheduler.on('error', (error, loopId) => {
337
+ this.logger.error('LOOP', 'Scheduler error', {
338
+ loopId,
339
+ error: error.message,
340
+ });
341
+ this.emit('error', error, loopId);
342
+ });
343
+ this.executor.on('start', (loopId, runId) => {
344
+ this.logger.debug('LOOP', `Run started: ${runId} for loop ${loopId}`);
345
+ });
346
+ this.executor.on('complete', async (loopId, result) => {
347
+ this.logger.debug('LOOP', `Run completed: ${result.run.id} for loop ${loopId}`, {
348
+ success: result.success,
349
+ duration: result.duration,
350
+ });
351
+ });
352
+ this.executor.on('error', (loopId, runId, error) => {
353
+ this.logger.error('LOOP', `Run error: ${runId} for loop ${loopId}`, {
354
+ error: error.message,
355
+ });
356
+ });
357
+ this.executor.on('timeout', (loopId, runId, timeoutMs) => {
358
+ this.logger.warn('LOOP', `Run timeout: ${runId} for loop ${loopId}`, {
359
+ timeoutMs,
360
+ });
361
+ });
362
+ }
363
+ async executeLoop(loop) {
364
+ this.logger.info('LOOP', `Executing loop ${loop.id}`, {
365
+ name: loop.name,
366
+ runNumber: loop.stats.totalRuns + 1,
367
+ });
368
+ const runId = uuidv4();
369
+ this.emit('runStarted', loop, runId);
370
+ const result = await this.executor.execute(loop);
371
+ await this.store.createRun(result.run);
372
+ await this.updateLoopStats(loop, result);
373
+ if (loop.options.maxRuns !== null && loop.stats.totalRuns >= loop.options.maxRuns) {
374
+ this.logger.info('LOOP', `Loop ${loop.id} reached max runs (${loop.options.maxRuns})`);
375
+ await this.stopLoop(loop.id);
376
+ return result;
377
+ }
378
+ if (!result.success) {
379
+ this.emit('runFailed', loop, result.error ? new Error(result.error.message) : new Error('Unknown error'));
380
+ if (loop.stats.consecutiveFailures >= this.config.errorThreshold) {
381
+ await this.handleErrorThreshold(loop);
382
+ return result;
383
+ }
384
+ }
385
+ else {
386
+ this.emit('runCompleted', loop, result);
387
+ }
388
+ await this.sendNotification(loop, result);
389
+ return result;
390
+ }
391
+ async updateLoopStats(loop, result) {
392
+ loop.stats.totalRuns++;
393
+ loop.stats.lastRunAt = result.run.startedAt;
394
+ loop.stats.lastRunDuration = result.duration;
395
+ if (result.success) {
396
+ loop.stats.successfulRuns++;
397
+ loop.stats.consecutiveFailures = 0;
398
+ loop.stats.lastRunStatus = 'success';
399
+ loop.stats.lastError = null;
400
+ }
401
+ else {
402
+ loop.stats.failedRuns++;
403
+ loop.stats.consecutiveFailures++;
404
+ loop.stats.lastRunStatus = 'error';
405
+ loop.stats.lastError = result.error?.message ?? 'Unknown error';
406
+ }
407
+ loop.interval.nextRunAt = this.intervalParser.calculateNextRun(loop.interval);
408
+ await this.store.updateLoop(loop);
409
+ }
410
+ async handleErrorThreshold(loop) {
411
+ const reason = `Exceeded error threshold (${this.config.errorThreshold} consecutive failures)`;
412
+ this.logger.warn('LOOP', `Auto-pausing loop ${loop.id}: ${reason}`);
413
+ switch (loop.options.onError) {
414
+ case 'pause':
415
+ loop.status = 'paused';
416
+ this.scheduler.pause(loop.id);
417
+ break;
418
+ case 'stop':
419
+ loop.status = 'stopped';
420
+ this.scheduler.unschedule(loop.id);
421
+ break;
422
+ case 'continue':
423
+ loop.stats.consecutiveFailures = 0;
424
+ break;
425
+ default:
426
+ break;
427
+ }
428
+ await this.store.updateLoop(loop);
429
+ if (loop.options.onError !== 'continue') {
430
+ this.emit('autoPaused', loop, reason);
431
+ }
432
+ }
433
+ async handleExpiredLoop(loop) {
434
+ this.logger.info('LOOP', `Loop ${loop.id} expired`);
435
+ loop.status = 'expired';
436
+ await this.store.updateLoop(loop);
437
+ this.scheduler.unschedule(loop.id);
438
+ this.executor.cancelAll(loop.id);
439
+ this.emit('expired', loop);
440
+ }
441
+ async sendNotification(loop, result) {
442
+ if (loop.options.notify === 'none') {
443
+ return;
444
+ }
445
+ if (!loop.options.quiet) {
446
+ const status = result.success ? '✓' : '✗';
447
+ const duration = `${(result.duration / 1000).toFixed(1)}s`;
448
+ this.logger.info('LOOP', `[${status}] Loop "${loop.name || loop.id}" completed in ${duration}`);
449
+ }
450
+ }
451
+ calculateExpiration(expires) {
452
+ const expiresStr = expires ?? this.config.defaultExpiration;
453
+ if (!expiresStr || expiresStr === 'never') {
454
+ return null;
455
+ }
456
+ const expiresMs = parseDurationString(expiresStr);
457
+ if (expiresMs === undefined) {
458
+ return null;
459
+ }
460
+ return new Date(Date.now() + expiresMs);
461
+ }
462
+ isLoopExpired(loop) {
463
+ if (!loop.expiresAt) {
464
+ return false;
465
+ }
466
+ return new Date() >= loop.expiresAt;
467
+ }
468
+ ensureInitialized() {
469
+ if (!this.initialized) {
470
+ throw new LoopError('MANAGER_NOT_INITIALIZED', 'LoopManager must be initialized before use. Call initialize() first.');
471
+ }
472
+ }
473
+ }
474
+ export function createLoopManager(options) {
475
+ return new LoopManager(options);
476
+ }
@@ -0,0 +1,69 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { Loop, LoopId, LoopConfig, Logger } from './types.js';
3
+ export interface SchedulerEvents {
4
+ execute: [loop: Loop];
5
+ scheduled: [loopId: LoopId, nextRunAt: Date];
6
+ unscheduled: [loopId: LoopId];
7
+ expired: [loopId: LoopId];
8
+ error: [error: Error, loopId?: LoopId];
9
+ }
10
+ export interface LoopSchedulerOptions {
11
+ config?: Partial<LoopConfig>;
12
+ timezone?: string;
13
+ maxTimerDelay?: number;
14
+ driftThreshold?: number;
15
+ logger?: Logger;
16
+ }
17
+ export type SchedulerOptions = LoopSchedulerOptions;
18
+ export interface SchedulerStats {
19
+ scheduledCount: number;
20
+ pausedCount: number;
21
+ executionCount: number;
22
+ expiredCount: number;
23
+ startedAt: Date | null;
24
+ lastExecutionAt: Date | null;
25
+ }
26
+ export declare class LoopScheduler extends EventEmitter {
27
+ private readonly scheduledLoops;
28
+ private readonly timezone;
29
+ private readonly maxTimerDelay;
30
+ private readonly driftThreshold;
31
+ private readonly logger;
32
+ private started;
33
+ private startedAt;
34
+ private executionCount;
35
+ private expiredCount;
36
+ private lastExecutionAt;
37
+ constructor(options?: LoopSchedulerOptions);
38
+ start(): void;
39
+ stop(): void;
40
+ isRunning(): boolean;
41
+ schedule(loop: Loop): void;
42
+ unschedule(loopId: LoopId): void;
43
+ update(loop: Loop): void;
44
+ pause(loopId: LoopId): void;
45
+ resume(loopId: LoopId): void;
46
+ triggerNow(loopId: LoopId): void;
47
+ isScheduled(loopId: LoopId): boolean;
48
+ getScheduled(loopId: LoopId): Loop | null;
49
+ getNextRunTime(loopId: LoopId): Date | null;
50
+ getScheduledLoopIds(): LoopId[];
51
+ getActiveLoopIds(): LoopId[];
52
+ getStats(): SchedulerStats;
53
+ private scheduleNextRun;
54
+ private executeLoop;
55
+ private calculateNextRunTime;
56
+ private applyDriftCorrection;
57
+ updateNextRunTime(loopId: LoopId, nextRunAt: Date): void;
58
+ clear(): void;
59
+ get size(): number;
60
+ }
61
+ export interface TypedLoopScheduler {
62
+ on<K extends keyof SchedulerEvents>(event: K, listener: (...args: SchedulerEvents[K]) => void): this;
63
+ once<K extends keyof SchedulerEvents>(event: K, listener: (...args: SchedulerEvents[K]) => void): this;
64
+ emit<K extends keyof SchedulerEvents>(event: K, ...args: SchedulerEvents[K]): boolean;
65
+ off<K extends keyof SchedulerEvents>(event: K, listener: (...args: SchedulerEvents[K]) => void): this;
66
+ removeListener<K extends keyof SchedulerEvents>(event: K, listener: (...args: SchedulerEvents[K]) => void): this;
67
+ }
68
+ export declare function createLoopScheduler(options?: LoopSchedulerOptions): LoopScheduler;
69
+ export declare function createAndStartScheduler(options?: LoopSchedulerOptions): LoopScheduler;