@fluojs/cron 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -23,6 +23,8 @@ fluo 애플리케이션을 위한 데코레이터 기반 스케줄링 패키지
23
23
  npm install @fluojs/cron croner
24
24
  ```
25
25
 
26
+ `croner`는 `@fluojs/cron`이 사용하는 scheduler engine입니다. 애플리케이션과 배포 감사에서 runtime scheduler dependency ownership이 명확히 보이도록 패키지와 함께 설치하세요.
27
+
26
28
  ## 사용 시점
27
29
 
28
30
  - 정기적인 백그라운드 작업(예: 데이터베이스 정리, 리포트 생성)이 필요할 때 사용합니다.
@@ -141,6 +143,8 @@ class TaskManager {
141
143
 
142
144
  Registry는 `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`을 제공합니다. Timeout task는 한 번 실행된 뒤 비활성화되지만 registry에는 남아 있어 의도적으로 다시 활성화할 수 있습니다.
143
145
 
146
+ Dynamic cron 등록은 scheduler startup과 원자적으로 처리됩니다. Scheduler가 새 cron job을 거부하면 registry는 half-registered task를 남기지 않습니다. 실행 중인 cron expression update도 rollback-safe합니다. Rescheduling이 실패하면 이전 expression과 scheduled job이 그대로 유지됩니다. Cron task는 scheduler-level no-overlap protection과 fluo의 in-process running guard를 함께 사용하므로 같은 task instance가 overlapping tick으로 실행되지 않습니다.
147
+
144
148
  ### 제한된 종료
145
149
 
146
150
  `CronModule`은 애플리케이션 종료 시 실행 중인 작업을 제한된 타임아웃 안에서 drain합니다. 따라서 하나의 hung task 때문에 프로세스 종료가 영원히 막히지 않습니다.
@@ -177,6 +181,7 @@ singleton provider/controller만 스케줄링됩니다. Request-scoped 및 trans
177
181
  - `SCHEDULING_REGISTRY`: `SchedulingRegistry` 서비스를 위한 주입 토큰입니다.
178
182
  - `normalizeCronModuleOptions(...)`: module option과 기본값을 정규화합니다.
179
183
  - `createCronPlatformStatusSnapshot(...)`: health/readiness 통합을 위한 status snapshot을 생성합니다.
184
+ - 공개 타입: `CronModuleOptions`, `CronDistributedOptions`, `CronShutdownOptions`, `CronScheduleOptions`, `CronScheduler`, `CronScheduledJob`, `SchedulingRegistry`, `SchedulingTaskDescriptor`, `SchedulingTaskCallback`, `SchedulingTaskOptions`, `CronTaskOptions`, `IntervalTaskOptions`, `TimeoutTaskOptions`.
180
185
  - 메타데이터 헬퍼와 심볼: `defineSchedulingTaskMetadata`, `defineCronTaskMetadata`, `getSchedulingTaskMetadata`, `getCronTaskMetadata`, `getSchedulingTaskMetadataEntries`, `getCronTaskMetadataEntries`, `schedulingMetadataSymbol`, `cronMetadataSymbol`.
181
186
 
182
187
 
package/README.md CHANGED
@@ -23,6 +23,8 @@ Decorator-based scheduling for fluo applications with lifecycle-managed startup/
23
23
  npm install @fluojs/cron croner
24
24
  ```
25
25
 
26
+ `croner` is the scheduler engine used by `@fluojs/cron`. Install it alongside the package so lockfiles make the runtime scheduler dependency explicit for applications and deployment audits.
27
+
26
28
  ## When to Use
27
29
 
28
30
  - When you need to run periodic background tasks (e.g., database cleanup, report generation).
@@ -141,6 +143,8 @@ class TaskManager {
141
143
 
142
144
  The registry exposes `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, and `updateCronExpression`. Timeout tasks run once, then disable themselves while remaining in the registry so they can be re-enabled deliberately.
143
145
 
146
+ Dynamic cron registration is atomic with scheduler startup: if the scheduler rejects a new cron job, the registry does not retain a half-registered task. Updating a running cron expression is also rollback-safe. If rescheduling fails, the previous expression and scheduled job remain active. Cron tasks use both scheduler-level no-overlap protection and fluo's in-process running guard, so the same task instance will not run overlapping ticks.
147
+
144
148
  ### Bounded Shutdown
145
149
 
146
150
  `CronModule` drains active task executions during application shutdown with a bounded timeout so one hung task cannot block process termination forever.
@@ -177,6 +181,7 @@ Only singleton providers/controllers are scheduled. Request-scoped and transient
177
181
  - `SCHEDULING_REGISTRY`: Injection token for the `SchedulingRegistry` service.
178
182
  - `normalizeCronModuleOptions(...)`: Normalizes module options and defaults.
179
183
  - `createCronPlatformStatusSnapshot(...)`: Creates a status snapshot for health/readiness integrations.
184
+ - Public types: `CronModuleOptions`, `CronDistributedOptions`, `CronShutdownOptions`, `CronScheduleOptions`, `CronScheduler`, `CronScheduledJob`, `SchedulingRegistry`, `SchedulingTaskDescriptor`, `SchedulingTaskCallback`, `SchedulingTaskOptions`, `CronTaskOptions`, `IntervalTaskOptions`, and `TimeoutTaskOptions`.
180
185
  - Metadata helpers and symbols: `defineSchedulingTaskMetadata`, `defineCronTaskMetadata`, `getSchedulingTaskMetadata`, `getCronTaskMetadata`, `getSchedulingTaskMetadataEntries`, `getCronTaskMetadataEntries`, `schedulingMetadataSymbol`, `cronMetadataSymbol`.
181
186
 
182
187
 
package/dist/service.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Container } from '@fluojs/di';
2
- import { type ApplicationLogger, type CompiledModule, type OnApplicationBootstrap, type OnApplicationShutdown, type OnModuleDestroy } from '@fluojs/runtime';
2
+ import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown, OnModuleDestroy } from '@fluojs/runtime';
3
3
  import type { CronTaskOptions, IntervalTaskOptions, NormalizedCronModuleOptions, SchedulingRegistry, SchedulingTaskCallback, SchedulingTaskDescriptor, TimeoutTaskOptions } from './types.js';
4
4
  /**
5
5
  * Lifecycle-managed scheduler runtime for decorator-discovered and dynamic tasks.
@@ -106,8 +106,10 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
106
106
  private assertTaskNameAvailable;
107
107
  private scheduleEnabledTasks;
108
108
  private scheduleTask;
109
+ private createScheduledHandle;
109
110
  private completeTimeoutTask;
110
111
  private unscheduleTask;
112
+ private stopScheduledHandle;
111
113
  private handleTaskTick;
112
114
  private runTaskTick;
113
115
  private shouldUseDistributedExecution;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
package/dist/service.js CHANGED
@@ -176,10 +176,17 @@ class CronLifecycleService {
176
176
  if (task.enabled) {
177
177
  return true;
178
178
  }
179
- task.enabled = true;
180
179
  if (this.started) {
181
- this.scheduleTask(task);
180
+ task.enabled = true;
181
+ try {
182
+ this.scheduleTask(task);
183
+ } catch (error) {
184
+ task.enabled = false;
185
+ throw error;
186
+ }
187
+ return true;
182
188
  }
189
+ task.enabled = true;
183
190
  return true;
184
191
  }
185
192
 
@@ -237,12 +244,24 @@ class CronLifecycleService {
237
244
  if (task.descriptor.kind !== 'cron') {
238
245
  throw new Error(`updateCronExpression() supports only cron tasks. Received ${task.descriptor.kind} task "${name}".`);
239
246
  }
240
- task.descriptor.expression = expression;
241
247
  if (!task.enabled || !this.started) {
248
+ task.descriptor.expression = expression;
242
249
  return;
243
250
  }
244
- this.unscheduleTask(task);
245
- this.scheduleTask(task);
251
+ const previousExpression = task.descriptor.expression;
252
+ const previousHandle = task.scheduledHandle;
253
+ task.descriptor.expression = expression;
254
+ try {
255
+ const nextHandle = this.createScheduledHandle(task);
256
+ task.scheduledHandle = nextHandle;
257
+ if (previousHandle) {
258
+ this.stopScheduledHandle(previousHandle);
259
+ }
260
+ } catch (error) {
261
+ task.descriptor.expression = previousExpression;
262
+ task.scheduledHandle = previousHandle;
263
+ throw error;
264
+ }
246
265
  }
247
266
  async onApplicationBootstrap() {
248
267
  if (this.started) {
@@ -365,10 +384,10 @@ class CronLifecycleService {
365
384
  scheduledHandle: undefined,
366
385
  source
367
386
  };
368
- this.tasks.set(descriptor.taskName, task);
369
387
  if (this.started) {
370
- this.scheduleTask(task);
388
+ task.scheduledHandle = this.createScheduledHandle(task);
371
389
  }
390
+ this.tasks.set(descriptor.taskName, task);
372
391
  }
373
392
  assertTaskNameAvailable(taskName) {
374
393
  if (this.tasks.has(taskName)) {
@@ -386,42 +405,43 @@ class CronLifecycleService {
386
405
  if (!task.enabled || task.scheduledHandle) {
387
406
  return;
388
407
  }
408
+ task.scheduledHandle = this.createScheduledHandle(task);
409
+ }
410
+ createScheduledHandle(task) {
411
+ const taskName = task.descriptor.taskName;
389
412
  if (task.descriptor.kind === 'cron') {
390
413
  const expression = task.descriptor.expression;
391
414
  if (!expression) {
392
- throw new Error(`Cron task "${task.descriptor.taskName}" is missing a cron expression.`);
415
+ throw new Error(`Cron task "${taskName}" is missing a cron expression.`);
393
416
  }
394
- const scheduled = this.options.scheduler(expression, {
395
- name: task.descriptor.taskName,
417
+ return this.options.scheduler(expression, {
418
+ name: taskName,
396
419
  protect: true,
397
420
  timezone: task.descriptor.timezone
398
421
  }, async () => {
399
- await this.handleTaskTick(task.descriptor.taskName);
422
+ await this.handleTaskTick(taskName);
400
423
  });
401
- task.scheduledHandle = scheduled;
402
- return;
403
424
  }
404
425
  const ms = task.descriptor.ms;
405
426
  if (!ms) {
406
- throw new Error(`${task.descriptor.kind} task "${task.descriptor.taskName}" is missing interval duration.`);
427
+ throw new Error(`${task.descriptor.kind} task "${taskName}" is missing interval duration.`);
407
428
  }
408
429
  if (task.descriptor.kind === 'interval') {
409
430
  const timer = setInterval(() => {
410
- void this.handleTaskTick(task.descriptor.taskName);
431
+ void this.handleTaskTick(taskName);
411
432
  }, ms);
412
- task.scheduledHandle = {
433
+ return {
413
434
  stop: () => {
414
435
  clearInterval(timer);
415
436
  }
416
437
  };
417
- return;
418
438
  }
419
439
  const timer = setTimeout(() => {
420
- void this.handleTaskTick(task.descriptor.taskName).finally(() => {
421
- this.completeTimeoutTask(task.descriptor.taskName);
440
+ void this.handleTaskTick(taskName).finally(() => {
441
+ this.completeTimeoutTask(taskName);
422
442
  });
423
443
  }, ms);
424
- task.scheduledHandle = {
444
+ return {
425
445
  stop: () => {
426
446
  clearTimeout(timer);
427
447
  }
@@ -439,17 +459,20 @@ class CronLifecycleService {
439
459
  if (!task.scheduledHandle) {
440
460
  return;
441
461
  }
462
+ const scheduledHandle = task.scheduledHandle;
463
+ task.scheduledHandle = undefined;
464
+ this.stopScheduledHandle(scheduledHandle);
465
+ }
466
+ stopScheduledHandle(scheduledHandle) {
442
467
  try {
443
- task.scheduledHandle.stop();
468
+ scheduledHandle.stop();
444
469
  } catch (error) {
445
- this.logger.error('Failed to stop scheduled task during shutdown.', error, 'CronLifecycleService');
446
- } finally {
447
- task.scheduledHandle = undefined;
470
+ this.logger.error('Failed to stop scheduled task.', error, 'CronLifecycleService');
448
471
  }
449
472
  }
450
473
  async handleTaskTick(taskName) {
451
474
  const taskState = this.tasks.get(taskName);
452
- if (!taskState || !taskState.enabled || taskState.running) {
475
+ if (!taskState?.enabled || taskState.running) {
453
476
  return;
454
477
  }
455
478
  const task = this.runTaskTick(taskState.descriptor, taskState);
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "timeout",
10
10
  "distributed-lock"
11
11
  ],
12
- "version": "1.0.0",
12
+ "version": "1.0.1",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -37,10 +37,10 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "croner": "^8.1.2",
40
- "@fluojs/core": "^1.0.0",
41
- "@fluojs/di": "^1.0.0",
42
- "@fluojs/runtime": "^1.0.0",
43
- "@fluojs/redis": "^1.0.0"
40
+ "@fluojs/core": "^1.0.1",
41
+ "@fluojs/di": "^1.0.1",
42
+ "@fluojs/redis": "^1.0.0",
43
+ "@fluojs/runtime": "^1.0.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "vitest": "^3.2.4"