🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@fluojs/cron

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fluojs/cron - npm Package Compare versions

Comparing version
1.1.0
to
2.0.1
+1
-1
dist/decorators.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,eAAe,EAEf,mBAAmB,EAEnB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAGpB,KAAK,yBAAyB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAC;AACjG,KAAK,mBAAmB,GAAG,yBAAyB,CAAC;AAqCrD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,mBAAmB,CAoB3F;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,mBAAmB,CAgB3F;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,mBAAmB,CAgBzF"}
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,eAAe,EAEf,mBAAmB,EAEnB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAGpB,KAAK,yBAAyB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAC;AACjG,KAAK,mBAAmB,GAAG,yBAAyB,CAAC;AAyCrD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,mBAAmB,CAoB3F;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,mBAAmB,CAgB3F;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,mBAAmB,CAgBzF"}

@@ -29,2 +29,5 @@ import { ensureMetadataSymbol } from '@fluojs/core/internal';

}
if (context.static) {
throw new Error(`${decoratorName}() cannot be used on static methods.`);
}
}

@@ -31,0 +34,0 @@

@@ -35,3 +35,3 @@ import type { Container } from '@fluojs/di';

releaseLock(descriptor: CronTaskDescriptor): Promise<boolean>;
releaseOwnedLocks(excludedLockKeys?: ReadonlySet<string>): Promise<void>;
releaseOwnedLocks(excludedLockKeys?: ReadonlySet<string>, timeoutMs?: number): Promise<void>;
private createLockRenewalState;

@@ -38,0 +38,0 @@ private queueDueLockRenewalAttempts;

@@ -1,1 +0,1 @@

{"version":3,"file":"distributed-lock-manager.d.ts","sourceRoot":"","sources":["../src/distributed-lock-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAElF,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CAC7G;AAED,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,eAAe,IAAI,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IAC9C,IAAI,IAAI,IAAI,CAAC;CACd;AAoDD,yFAAyF;AACzF,qBAAa,0BAA0B;IAQnC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,mBAAmB,CAAK;gBAGb,OAAO,EAAE,2BAA2B,EACpC,gBAAgB,EAAE,SAAS,EAC3B,MAAM,EAAE,iBAAiB;IAG5C,IAAI,cAAc,IAAI,eAAe,GAAG,SAAS,CAEhD;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,eAAe,IAAI,OAAO,CAM7B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBpC,KAAK,IAAI,IAAI;IAKP,cAAc,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAkCtE,uBAAuB,CAAC,UAAU,EAAE,kBAAkB,GAAG,kBAAkB;IA8BrE,WAAW,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7D,iBAAiB,CAAC,gBAAgB,GAAE,WAAW,CAAC,MAAM,CAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBzF,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,2BAA2B;YAcrB,qBAAqB;IAqBnC,OAAO,CAAC,kBAAkB;YAYZ,SAAS;YA2CT,cAAc;YAsCd,wBAAwB;IAmBtC,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,qBAAqB;CAG9B"}
{"version":3,"file":"distributed-lock-manager.d.ts","sourceRoot":"","sources":["../src/distributed-lock-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAElF,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CAC7G;AAED,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,eAAe,IAAI,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IAC9C,IAAI,IAAI,IAAI,CAAC;CACd;AA+ED,yFAAyF;AACzF,qBAAa,0BAA0B;IAQnC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,mBAAmB,CAAK;gBAGb,OAAO,EAAE,2BAA2B,EACpC,gBAAgB,EAAE,SAAS,EAC3B,MAAM,EAAE,iBAAiB;IAG5C,IAAI,cAAc,IAAI,eAAe,GAAG,SAAS,CAEhD;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,eAAe,IAAI,OAAO,CAM7B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBpC,KAAK,IAAI,IAAI;IAKP,cAAc,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAkCtE,uBAAuB,CAAC,UAAU,EAAE,kBAAkB,GAAG,kBAAkB;IA8BrE,WAAW,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7D,iBAAiB,CAAC,gBAAgB,GAAE,WAAW,CAAC,MAAM,CAAa,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7G,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,2BAA2B;YAcrB,qBAAqB;IAqBnC,OAAO,CAAC,kBAAkB;YAYZ,SAAS;YA2CT,cAAc;YAyCd,wBAAwB;IAmBtC,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,qBAAqB;CAG9B"}

@@ -19,2 +19,22 @@ /** Minimal Redis command surface required for distributed cron locks. */

}
function createLockReleaseTimeoutError(timeoutMs) {
return new Error(`Distributed cron lock release timed out after ${String(timeoutMs)}ms.`);
}
async function withTimeout(operation, timeoutMs) {
if (timeoutMs === undefined) {
return await operation;
}
let timeoutHandle;
try {
return await Promise.race([operation, new Promise((_resolve, reject) => {
timeoutHandle = setTimeout(() => {
reject(createLockReleaseTimeoutError(timeoutMs));
}, timeoutMs);
})]);
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
}
async function resolveRedisPeerModule() {

@@ -111,3 +131,3 @@ try {

});
}, renewalState.renewalIntervalMs);
}, renewalState.renewalIntervalMs).unref();
return {

@@ -131,3 +151,3 @@ getPostRunError: async () => {

}
async releaseOwnedLocks(excludedLockKeys = new Set()) {
async releaseOwnedLocks(excludedLockKeys = new Set(), timeoutMs) {
if (!this.redisClient || this.ownedLockKeys.size === 0) {

@@ -141,3 +161,3 @@ return;

await Promise.all(lockKeys.map(async lockKey => {
await this.releaseLockKey(lockKey, lockKey);
await this.releaseLockKey(lockKey, lockKey, timeoutMs);
}));

@@ -207,3 +227,3 @@ }

}
async releaseLockKey(lockKey, taskName) {
async releaseLockKey(lockKey, taskName, timeoutMs) {
const redis = this.redisClient;

@@ -214,3 +234,3 @@ if (!redis) {

try {
const result = await redis.eval(RELEASE_LOCK_SCRIPT, 1, lockKey, this.options.distributed.ownerId);
const result = await withTimeout(redis.eval(RELEASE_LOCK_SCRIPT, 1, lockKey, this.options.distributed.ownerId), timeoutMs);
if (typeof result === 'number' && result <= 0) {

@@ -217,0 +237,0 @@ this.markLockIoAvailable();

@@ -1,1 +0,1 @@

{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKhE,OAAO,KAAK,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAwDjF;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,iBAAsB,GAAG,2BAA2B,CAMvG;AAeD,iEAAiE;AACjE,qBAAa,UAAU;IACrB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU;CAS5D"}
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKhE,OAAO,KAAK,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAoGjF;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,iBAAsB,GAAG,2BAA2B,CAMvG;AAeD,iEAAiE;AACjE,qBAAa,UAAU;IACrB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU;CAS5D"}
import { defineModule } from '@fluojs/runtime';
import { defaultCronScheduler } from './scheduler.js';
import { CronLifecycleService } from './service.js';
import { defaultCronScheduler } from './scheduler.js';
import { CRON_OPTIONS, SCHEDULING_REGISTRY } from './tokens.js';

@@ -13,2 +13,30 @@ const DEFAULT_CRON_SHUTDOWN_TIMEOUT_MS = 10_000;

}
function normalizeRedisClientName(clientName) {
if (clientName === undefined) {
return undefined;
}
const normalizedClientName = clientName.trim();
if (normalizedClientName.length === 0) {
throw new Error('Cron distributed clientName must be a non-empty string when provided.');
}
return normalizedClientName;
}
function assertValidDistributedLockTtlMs(lockTtlMs) {
if (!Number.isFinite(lockTtlMs) || !Number.isInteger(lockTtlMs) || lockTtlMs < 1_000) {
throw new Error('Cron distributed lockTtlMs must be a positive integer greater than or equal to 1000ms.');
}
}
function normalizeDistributedOwnerId(ownerId) {
if (ownerId === undefined) {
return randomId();
}
if (typeof ownerId !== 'string') {
throw new Error('Cron distributed ownerId must be a string when provided.');
}
const normalizedOwnerId = ownerId.trim();
if (normalizedOwnerId.length === 0) {
throw new Error('Cron distributed ownerId must be a non-empty string when provided.');
}
return normalizedOwnerId;
}
function normalizeDistributedOptions(distributed) {

@@ -33,9 +61,13 @@ if (distributed === undefined || distributed === false) {

}
return {
clientName: distributed.clientName,
const normalizedDistributed = {
clientName: normalizeRedisClientName(distributed.clientName),
enabled: distributed.enabled ?? true,
keyPrefix: distributed.keyPrefix ?? 'fluo:cron:lock',
lockTtlMs: distributed.lockTtlMs ?? 30_000,
ownerId: distributed.ownerId ?? randomId()
ownerId: normalizeDistributedOwnerId(distributed.ownerId)
};
if (normalizedDistributed.enabled) {
assertValidDistributedLockTtlMs(normalizedDistributed.lockTtlMs);
}
return normalizedDistributed;
}

@@ -42,0 +74,0 @@ function normalizeShutdownOptions(shutdown) {

@@ -91,2 +91,3 @@ import type { Container } from '@fluojs/di';

* @param expression New cron expression to validate and schedule.
* @throws When validation, replacement scheduling, or previous-handle shutdown fails.
*/

@@ -99,2 +100,3 @@ updateCronExpression(name: string, expression: string): void;

* @param ms New positive interval in milliseconds.
* @throws When validation, replacement scheduling, or previous-handle shutdown fails.
*/

@@ -112,2 +114,4 @@ updateIntervalMs(name: string, ms: number): void;

private handleStartupFailure;
private completeStartupFailureCleanupAfterActiveTasks;
private resetDistributedLocksAfterStartupFailure;
private runShutdownLifecycle;

@@ -114,0 +118,0 @@ private getRunningDistributedLockKeys;

@@ -1,1 +0,1 @@

{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EAChB,MAAM,iBAAiB,CAAC;AASzB,OAAO,KAAK,EAEV,eAAe,EACf,mBAAmB,EACnB,2BAA2B,EAC3B,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAiEpB;;;;;;GAMG;AACH,qBACa,oBACX,YAAW,kBAAkB,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,eAAe;IAY3F,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAbzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;IACxD,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAqB;IAChE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiB;IAC5C,OAAO,CAAC,cAAc,CAAmF;IACzG,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,eAAe,CAA4B;gBAGhC,OAAO,EAAE,2BAA2B,EACpC,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB;IAM5C;;;;;;;OAOG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,EAAE,OAAO,GAAE,eAAoB,GAAG,IAAI;IAuBhH;;;;;;;OAOG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,EAAE,OAAO,GAAE,mBAAwB,GAAG,IAAI;IAsBhH;;;;;;;OAOG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,EAAE,OAAO,GAAE,kBAAuB,GAAG,IAAI;IAsB9G;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAY7B;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IA4B7B;;;;;OAKG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAgB9B;;;;;OAKG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,wBAAwB,GAAG,SAAS;IAMvD;;;;OAIG;IACH,MAAM,IAAI,wBAAwB,EAAE;IAIpC;;;;;OAKG;IACH,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAqC5D;;;;;OAKG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAqC1C,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,4BAA4B;IA8B5B,OAAO,CAAC,0BAA0B;YAkBpB,QAAQ;YAYR,0CAA0C;YAQ1C,cAAc;IAQ5B,OAAO,CAAC,oCAAoC;IAQ5C,OAAO,CAAC,oBAAoB;YAOd,oBAAoB;IAmBlC,OAAO,CAAC,6BAA6B;IAIrC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,YAAY;IAsBpB,OAAO,CAAC,uBAAuB;IAM/B,OAAO,CAAC,oBAAoB;IAQ5B,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,qBAAqB;IAsD7B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,mBAAmB;YAQb,cAAc;YAmBd,WAAW;IASzB,OAAO,CAAC,6BAA6B;YAIvB,sBAAsB;YA0BtB,kBAAkB;YA2BlB,gBAAgB;YAMhB,WAAW;IAYzB,OAAO,CAAC,qBAAqB;CAK9B"}
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}

@@ -12,3 +12,3 @@ let _initClass;

import { createCronPlatformStatusSnapshot } from './status.js';
import { createLockKey, discoverCronTaskDescriptors } from './task-discovery.js';
import { assertValidSchedulingTaskName, createLockKey, discoverCronTaskDescriptors, resolveSchedulingTaskName } from './task-discovery.js';
import { CronTaskRunner } from './task-runner.js';

@@ -21,14 +21,4 @@ import { CRON_OPTIONS } from './tokens.js';

}
function assertValidTaskName(name) {
if (name.trim().length === 0) {
throw new Error('Scheduling task name must be a non-empty string.');
}
}
function resolveDynamicTaskName(name, optionName) {
assertValidTaskName(name);
if (optionName !== undefined) {
assertValidTaskName(optionName);
return optionName;
}
return name;
return resolveSchedulingTaskName(name, optionName);
}

@@ -256,2 +246,3 @@ function assertValidMs(ms, context) {

* @param expression New cron expression to validate and schedule.
* @throws When validation, replacement scheduling, or previous-handle shutdown fails.
*/

@@ -273,10 +264,14 @@ updateCronExpression(name, expression) {

const previousHandle = task.scheduledHandle;
let nextHandle;
task.descriptor.expression = expression;
try {
const nextHandle = this.createScheduledHandle(task);
task.scheduledHandle = nextHandle;
nextHandle = this.createScheduledHandle(task);
if (previousHandle) {
this.stopScheduledHandle(previousHandle);
previousHandle.stop();
}
task.scheduledHandle = nextHandle;
} catch (error) {
if (nextHandle) {
this.stopScheduledHandle(nextHandle);
}
task.descriptor.expression = previousExpression;

@@ -293,2 +288,3 @@ task.scheduledHandle = previousHandle;

* @param ms New positive interval in milliseconds.
* @throws When validation, replacement scheduling, or previous-handle shutdown fails.
*/

@@ -310,10 +306,14 @@ updateIntervalMs(name, ms) {

const previousHandle = task.scheduledHandle;
let nextHandle;
task.descriptor.ms = ms;
try {
const nextHandle = this.createScheduledHandle(task);
task.scheduledHandle = nextHandle;
nextHandle = this.createScheduledHandle(task);
if (previousHandle) {
this.stopScheduledHandle(previousHandle);
previousHandle.stop();
}
task.scheduledHandle = nextHandle;
} catch (error) {
if (nextHandle) {
this.stopScheduledHandle(nextHandle);
}
task.descriptor.ms = previousMs;

@@ -334,3 +334,3 @@ task.scheduledHandle = previousHandle;

this.lifecycleState = 'failed';
this.handleStartupFailure();
await this.handleStartupFailure();
throw error;

@@ -372,3 +372,3 @@ }

toSchedulingTaskDescriptor(task) {
return {
return Object.freeze({
distributed: task.descriptor.distributed,

@@ -387,3 +387,3 @@ enabled: task.enabled,

timezone: task.descriptor.timezone
};
});
}

@@ -403,7 +403,7 @@ async shutdown() {

}
await this.distributedLocks.releaseOwnedLocks();
await this.distributedLocks.releaseOwnedLocks(new Set(), this.options.shutdown.timeoutMs);
}
async startLifecycle() {
this.validateDistributedLockConfiguration();
await this.distributedLocks.resolveClient();
this.validateDistributedLockConfiguration();
this.registerDecoratorTasks();

@@ -419,8 +419,27 @@ this.started = true;

}
handleStartupFailure() {
async handleStartupFailure() {
this.started = false;
this.stopAllScheduledTasks();
const startupRollbackTimedOut = await this.waitForActiveTasks();
if (startupRollbackTimedOut) {
this.logger.warn(`Cron startup rollback timed out after ${String(this.options.shutdown.timeoutMs)}ms with ${String(this.activeTasks.size)} active task(s) still pending.`, 'CronLifecycleService');
}
await this.distributedLocks.releaseOwnedLocks(startupRollbackTimedOut ? this.getRunningDistributedLockKeys() : new Set(), this.options.shutdown.timeoutMs);
this.tasks.clear();
this.distributedLocks.reset();
if (this.activeTasks.size > 0) {
void this.completeStartupFailureCleanupAfterActiveTasks();
return;
}
this.resetDistributedLocksAfterStartupFailure();
}
async completeStartupFailureCleanupAfterActiveTasks() {
await this.drainActiveTasks();
await this.distributedLocks.releaseOwnedLocks(new Set(), this.options.shutdown.timeoutMs);
this.resetDistributedLocksAfterStartupFailure();
}
resetDistributedLocksAfterStartupFailure() {
if (this.distributedLocks.ownedLocks === 0) {
this.distributedLocks.reset();
}
}
async runShutdownLifecycle() {

@@ -434,3 +453,3 @@ this.lifecycleState = 'stopping';

}
await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set());
await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set(), this.options.shutdown.timeoutMs);
this.lifecycleState = 'stopped';

@@ -448,4 +467,5 @@ }

registerTask(descriptor, source) {
assertValidSchedulingTaskName(descriptor.taskName);
this.assertTaskNameAvailable(descriptor.taskName);
if (descriptor.distributed) {
if (this.options.distributed.enabled && descriptor.distributed) {
assertValidLockTtlMs(descriptor.lockTtlMs);

@@ -588,3 +608,3 @@ }

if (!released && this.lifecycleState === 'stopped') {
await this.distributedLocks.releaseOwnedLocks();
await this.distributedLocks.releaseOwnedLocks(new Set(), this.options.shutdown.timeoutMs);
}

@@ -591,0 +611,0 @@ }

@@ -1,2 +0,2 @@

import { type MetadataPropertyKey } from '@fluojs/core';
import type { MetadataPropertyKey } from '@fluojs/core';
import type { ApplicationLogger, CompiledModule } from '@fluojs/runtime';

@@ -21,2 +21,16 @@ import type { CronTaskDescriptor, NormalizedCronModuleOptions } from './types.js';

/**
* Asserts that a scheduling task name can be used as a registry key.
*
* @param name Scheduling task name supplied by a decorator or registry call.
*/
export declare function assertValidSchedulingTaskName(name: string): void;
/**
* Resolves the effective scheduling task name while preserving authored names.
*
* @param defaultName Name derived from the decorated target or registry argument.
* @param optionName Optional name override supplied in scheduling options.
* @returns The effective task name used by the scheduler registry.
*/
export declare function resolveSchedulingTaskName(defaultName: string, optionName?: string): string;
/**
* Method key to name.

@@ -23,0 +37,0 @@ *

@@ -1,1 +0,1 @@

{"version":3,"file":"task-discovery.d.ts","sourceRoot":"","sources":["../src/task-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAc,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AASlF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,CAEtE;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,iBAAiB,GACxB,kBAAkB,EAAE,CA4DtB"}
{"version":3,"file":"task-discovery.d.ts","sourceRoot":"","sources":["../src/task-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAS,MAAM,cAAc,CAAC;AAG/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AASlF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAIhE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAS1F;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,CAEtE;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,iBAAiB,GACxB,kBAAkB,EAAE,CA+DtB"}

@@ -26,2 +26,29 @@ import { getClassDiMetadata } from '@fluojs/core/internal';

/**
* Asserts that a scheduling task name can be used as a registry key.
*
* @param name Scheduling task name supplied by a decorator or registry call.
*/
export function assertValidSchedulingTaskName(name) {
if (name.trim().length === 0) {
throw new Error('Scheduling task name must be a non-empty string.');
}
}
/**
* Resolves the effective scheduling task name while preserving authored names.
*
* @param defaultName Name derived from the decorated target or registry argument.
* @param optionName Optional name override supplied in scheduling options.
* @returns The effective task name used by the scheduler registry.
*/
export function resolveSchedulingTaskName(defaultName, optionName) {
assertValidSchedulingTaskName(defaultName);
if (optionName !== undefined) {
assertValidSchedulingTaskName(optionName);
return optionName;
}
return defaultName;
}
/**
* Method key to name.

@@ -57,3 +84,3 @@ *

const methodName = methodKeyToName(entry.propertyKey);
const taskName = entry.metadata.options.name ?? buildDefaultTaskName(candidate.targetType.name, methodName);
const taskName = resolveSchedulingTaskName(buildDefaultTaskName(candidate.targetType.name, methodName), entry.metadata.options.name);
const seenMethods = seen.get(candidate.targetType) ?? new Set();

@@ -60,0 +87,0 @@ const lockTtlMs = entry.metadata.options.lockTtlMs ?? options.distributed.lockTtlMs;

@@ -12,3 +12,3 @@ {

],
"version": "1.1.0",
"version": "2.0.1",
"private": false,

@@ -41,8 +41,8 @@ "license": "MIT",

"croner": "^8.1.2",
"@fluojs/core": "^1.0.3",
"@fluojs/di": "^1.1.0",
"@fluojs/runtime": "^1.1.8"
"@fluojs/core": "^1.1.0",
"@fluojs/di": "^2.0.0",
"@fluojs/runtime": "^2.0.1"
},
"peerDependencies": {
"@fluojs/redis": "^1.0.2"
"@fluojs/redis": "^1.1.0"
},

@@ -56,3 +56,3 @@ "peerDependenciesMeta": {

"vitest": "^3.2.4",
"@fluojs/redis": "^1.0.2"
"@fluojs/redis": "^1.1.0"
},

@@ -59,0 +59,0 @@ "scripts": {

@@ -13,2 +13,3 @@ # @fluojs/cron

- [공통 패턴](#공통-패턴)
- [NestJS Cron 옵션 마이그레이션](#nestjs-cron-옵션-마이그레이션)
- [분산 락 사용하기](#분산-락-사용하기)

@@ -24,6 +25,6 @@ - [동적 스케줄링](#동적-스케줄링)

```bash
npm install @fluojs/cron croner
npm install @fluojs/cron
```
`croner`는 `@fluojs/cron`이 사용하는 scheduler engine입니다. 애플리케이션과 배포 감사에서 runtime scheduler dependency ownership이 명확히 보이도록 패키지와 함께 설치하세요.
`@fluojs/cron`이 `croner`를 runtime dependency로 소유하므로 소비자가 scheduler engine을 직접 설치할 필요가 없습니다.

@@ -46,3 +47,3 @@ `@fluojs/redis`는 Redis distributed locking을 활성화할 때만 필요합니다. Non-distributed scheduling 경로는 package import, module registration, bootstrap, status snapshot 생성 중 Redis integration을 로드하지 않습니다.

Scheduling decorator는 public instance method에만 적용됩니다. NestJS에서 사용하던 private scheduled method, static helper, legacy decorator metadata 가정 뒤에 숨은 method name을 그대로 옮기지 마세요. 공개 provider/controller method를 노출하고 private 구현 세부사항은 그 method 뒤에 두세요.
Scheduling decorator는 public instance method에만 적용됩니다. NestJS에서 사용하던 private scheduled method, static helper, legacy decorator metadata 가정 뒤에 숨은 method name을 그대로 옮기지 마세요. 공개 provider/controller method를 노출하고 private 구현 세부사항은 그 method 뒤에 두세요. 명시적인 decorator `name` 값은 non-empty string이어야 하며, dynamic registry validation contract와 동일하게 검증됩니다.

@@ -79,2 +80,18 @@ ```typescript

### NestJS Cron 옵션 마이그레이션
NestJS `@Cron()` 옵션은 `CronTaskOptions`에 그대로 전달할 수 없습니다. NestJS `timeZone`을 fluo `timezone`으로 바꾸세요:
```typescript
// NestJS
@Cron('0 9 * * *', { timeZone: 'Asia/Seoul', waitForCompletion: true })
// fluo
@Cron('0 9 * * *', { timezone: 'Asia/Seoul' })
```
`waitForCompletion`을 복사하거나 overlap flag를 만들지 마세요. fluo는 두 옵션을 모두 노출하지 않으며, 모든 cron task에 scheduler-level no-overlap protection과 in-process running guard를 함께 적용합니다. 같은 task instance가 아직 실행 중일 때 다음 tick이 도착하면 fluo는 새 실행을 queue하지 않고 해당 tick을 건너뜁니다. 따라서 NestJS에서 `waitForCompletion: true`였던 task는 마이그레이션할 때 이 옵션을 생략합니다. NestJS task가 `waitForCompletion`을 생략하거나 `false`로 설정해 의도적으로 overlapping run에 의존했다면 fluo에서 overlap을 활성화할 수 있다고 가정하지 말고 application-owned queue 또는 worker 뒤로 작업을 재설계하세요.
이 guard는 한 application process 안의 같은 task instance만 보호합니다. 여러 application instance가 같은 task를 동시에 실행하지 않아야 한다면 [분산 락 사용하기](#분산-락-사용하기)를 적용하세요.
### 분산 락 사용하기

@@ -104,7 +121,7 @@

`distributed.clientName`을 생략하면 위의 기본 Redis 등록을 계속 사용합니다. 분산 락에 기본 Redis가 아닌 다른 연결을 쓰려면 `RedisModule.forRoot({ name, ... })`로 등록한 이름을 `distributed.clientName`에 지정하세요.
`distributed.clientName`을 생략하면 위의 기본 Redis 등록을 계속 사용합니다. 분산 락에 기본 Redis가 아닌 다른 연결을 쓰려면 `RedisModule.forRoot({ name, ... })`로 등록한 이름을 `distributed.clientName`에 지정하세요. fluo는 module option normalization 중 configured client name을 trim하고, lifecycle 또는 status reporting이 Redis dependency name을 사용하기 전에 blank 값을 거부합니다.
`distributed.lockTtlMs`는 `1_000ms` 이상이어야 합니다. fluo는 최소 지원 경계인 `1_000ms`를 포함해 TTL이 만료되기 전에 Redis 락을 갱신합니다.
`distributed.lockTtlMs`는 `1_000ms` 이상이어야 합니다. Distributed locking이 활성화된 경우 fluo는 Redis를 load, resolve, probe하기 전에 module option normalization 중 module-level TTL을 검증합니다. Task-level `lockTtlMs` override는 module distributed mode와 해당 task의 distributed locking이 모두 활성화된 경우에만 검증됩니다. Module 또는 task locking이 비활성화되어 있으면 사용되지 않는 TTL이 distributed 최소값보다 낮다는 이유만으로 실패하지 않습니다. fluo는 활성 TTL이 만료되기 전에 Redis 락을 갱신하며, 최소 지원 경계인 `1_000ms`도 포함됩니다.
각 scheduler instance는 platform-neutral 기본 `distributed.ownerId`를 사용합니다. 배포 환경에 더 강한 stable-owner 규칙이 있을 때만 `distributed.ownerId`를 명시적으로 지정하세요. Lock release는 task 실행 뒤 `finally` 경로에서 수행됩니다. Redis release가 실패하면 fluo는 status snapshot의 local ownership을 유지하고 shutdown 중 다시 release를 시도합니다. Redis가 다른 owner의 key라고 응답하면 fencing이 이미 다른 곳으로 이동한 것이므로 local ownership을 정리합니다. Redis TTL과 renewal timing은 drift 영향을 받는 coordination primitive이지 강한 fencing token 자체는 아니므로, stale work가 위험한 long-running job은 idempotent하게 작성하고 application-level fencing을 함께 사용해야 합니다.
각 scheduler instance는 platform-neutral 기본 `distributed.ownerId`를 사용합니다. 배포 환경에 더 강한 stable-owner 규칙이 있을 때만 `distributed.ownerId`를 명시적으로 지정하세요. `distributed.ownerId`를 제공한 경우 fluo는 module option normalization 중에 값을 trim하고, scheduler 또는 Redis lifecycle setup 전에 blank 또는 non-string 값을 거부합니다. 따라서 유효하지 않거나 빈 owner 식별자가 Redis lock ownership 상태로 들어갈 수 없습니다. Lock release는 task 실행 뒤 `finally` 경로에서 수행됩니다. Distributed tick이 이미 실행 중인 상태에서 bootstrap이 나중에 실패하면 startup rollback은 해당 active task가 drain되어 락을 release할 수 있을 때까지 Redis lock client를 유지합니다. Redis release가 실패하면 fluo는 status snapshot의 local ownership을 유지하고 shutdown 중 다시 release를 시도합니다. Redis가 다른 owner의 key라고 응답하면 fencing이 이미 다른 곳으로 이동한 것이므로 local ownership을 정리합니다. Redis TTL과 renewal timing은 drift 영향을 받는 coordination primitive이지 강한 fencing token 자체는 아니므로, stale work가 위험한 long-running job은 idempotent하게 작성하고 application-level fencing을 함께 사용해야 합니다.

@@ -157,5 +174,5 @@ ```typescript

Registry는 `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`, `updateIntervalMs`를 제공합니다. 첫 번째 `name` 인자는 기본 registry key이며, `options.name`을 전달하면 dynamic task의 실제 registry key, scheduler metadata name, 기본 distributed lock key가 이를 사용해 decorator naming semantics와 일치합니다. `get`과 `getAll`은 live `CronJob` handle이 아니라 read-only `SchedulingTaskDescriptor` 값을 반환합니다. Timeout task는 한 번 실행된 뒤 비활성화되지만 registry에는 남아 있어 의도적으로 다시 활성화할 수 있습니다.
Registry는 `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`, `updateIntervalMs`를 제공합니다. 첫 번째 `name` 인자는 기본 registry key이며, `options.name`을 전달하면 dynamic task의 실제 registry key, scheduler metadata name, 기본 distributed lock key가 이를 사용해 decorator naming semantics와 일치합니다. Registry, decorator, dynamic `options.name` task name은 non-empty string이어야 합니다. Blank dynamic override name은 scheduler 또는 registry state를 남기기 전에 거부됩니다. `get`과 `getAll`은 live `CronJob` handle이나 mutable registry state가 아니라 immutable `SchedulingTaskDescriptor` snapshot을 반환합니다. Timeout task는 한 번 실행된 뒤 비활성화되지만 registry에는 남아 있어 의도적으로 다시 활성화할 수 있습니다.
Dynamic cron 등록은 scheduler startup과 원자적으로 처리됩니다. Scheduler가 새 cron job을 거부하면 registry는 half-registered task를 남기지 않습니다. 실행 중인 cron expression 또는 interval cadence update도 rollback-safe합니다. Rescheduling이 실패하면 이전 expression 또는 interval milliseconds와 scheduled handle이 그대로 유지됩니다. Cron task는 scheduler-level no-overlap protection과 fluo의 in-process running guard를 함께 사용하므로 같은 task instance가 overlapping tick으로 실행되지 않습니다.
Dynamic cron 등록은 scheduler startup과 원자적으로 처리됩니다. Scheduler가 새 cron job을 거부하면 registry는 half-registered task를 남기지 않습니다. 실행 중인 cron expression 또는 interval cadence update도 rollback-safe합니다. 이전 scheduled handle의 stop이 성공해야만 replacement를 commit합니다. Replacement scheduling이 실패하거나 이전 handle을 stop할 수 없으면 fluo는 provisional replacement를 stop하고 이전 expression 또는 interval milliseconds와 handle을 복원한 뒤 failure를 다시 throw하므로 duplicate schedule을 조용히 남기지 않습니다. Cron task는 scheduler-level no-overlap protection과 fluo의 in-process running guard를 함께 사용하므로 같은 task instance가 overlapping tick으로 실행되지 않습니다.

@@ -166,3 +183,3 @@ ### 제한된 종료

기본적으로 shutdown drain은 최대 `10_000ms` 동안 기다립니다. 이 시간이 지나면 스케줄러는 경고 로그를 남기고 hung task가 끝나기를 더 기다리지 않은 채 종료를 계속합니다. 분산 락을 사용하는 경우 아직 실행 중인 작업이 보유한 락은 timeout 시점에 즉시 해제하지 않습니다. 해당 작업이 정상적으로 끝날 때까지 락 소유권을 유지하거나, 프로세스가 종료된 뒤 Redis TTL로 만료되게 두어 원래 작업이 아직 실행 중인데 다른 노드가 같은 작업을 시작하지 않도록 합니다.
기본적으로 shutdown drain은 최대 `10_000ms` 동안 기다립니다. 이 시간이 지나면 스케줄러는 경고 로그를 남기고 hung task가 끝나기를 더 기다리지 않은 채 종료를 계속합니다. 같은 `shutdown.timeoutMs` 경계는 shutdown 중 Redis owned-lock release I/O에도 적용되므로, 멈춘 Redis release가 process termination을 무기한 막지 못합니다. 분산 락을 사용하는 경우 아직 실행 중인 작업이 보유한 락은 timeout 시점에 즉시 해제하지 않습니다. 해당 작업이 정상적으로 끝날 때까지 락 소유권을 유지하거나, 프로세스가 종료된 뒤 Redis TTL로 만료되게 둡니다. fluo는 lock renewal timer에 `unref()`를 호출하므로 다른 작업이 Node.js event loop를 활성 상태로 유지하는 동안에는 갱신을 계속하지만 timer 자체만으로 process를 유지하지 않으며, task가 settle되면 timer를 clear합니다. Release I/O 자체가 timeout되면 fluo는 Redis가 release를 확인하거나 다른 owner가 key를 보유한다고 응답할 때까지 local owned-lock visibility/status를 보존하고 ownership을 지우지 않습니다. 이렇게 원래 작업이 아직 실행 중인데 다른 노드가 같은 작업을 시작하지 않도록 합니다.

@@ -169,0 +186,0 @@ ```typescript

@@ -13,2 +13,3 @@ # @fluojs/cron

- [Common Patterns](#common-patterns)
- [Migrating NestJS Cron Options](#migrating-nestjs-cron-options)
- [Distributed Locking](#distributed-locking)

@@ -24,6 +25,6 @@ - [Dynamic Scheduling](#dynamic-scheduling)

```bash
npm install @fluojs/cron croner
npm install @fluojs/cron
```
`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.
`@fluojs/cron` owns `croner` as a runtime dependency, so consumers do not need to install the scheduler engine directly.

@@ -46,3 +47,3 @@ `@fluojs/redis` is needed only when Redis distributed locking is enabled. Non-distributed scheduling paths do not load the Redis integration during package import, module registration, bootstrap, or status snapshot creation.

Scheduling decorators apply to public instance methods only. Do not migrate NestJS private scheduled methods, static helpers, or method names that are hidden behind legacy decorator metadata assumptions as-is; expose a public provider/controller method and keep any private implementation details behind that method.
Scheduling decorators apply to public instance methods only. Do not migrate NestJS private scheduled methods, static helpers, or method names that are hidden behind legacy decorator metadata assumptions as-is; expose a public provider/controller method and keep any private implementation details behind that method. Explicit decorator `name` values must be non-empty strings, matching the dynamic registry validation contract.

@@ -79,2 +80,18 @@ ```typescript

### Migrating NestJS Cron Options
NestJS `@Cron()` options are not a drop-in `CronTaskOptions` object. Rename NestJS `timeZone` to fluo `timezone`:
```typescript
// NestJS
@Cron('0 9 * * *', { timeZone: 'Asia/Seoul', waitForCompletion: true })
// fluo
@Cron('0 9 * * *', { timezone: 'Asia/Seoul' })
```
Do not copy `waitForCompletion` or invent an overlap flag. fluo does not expose either option: every cron task uses scheduler-level no-overlap protection plus an in-process running guard. If another tick arrives while the same task instance is still running, fluo skips that tick instead of queueing another run. A NestJS task with `waitForCompletion: true` therefore omits the option when migrated. If the NestJS task left `waitForCompletion` unset or set it to `false` and intentionally depended on overlapping runs, redesign that work behind an application-owned queue or worker rather than expecting fluo to enable overlap.
This guard covers one task instance in one application process. Use [Distributed Locking](#distributed-locking) when multiple application instances must not run the same task concurrently.
### Distributed Locking

@@ -104,7 +121,7 @@

Leave `distributed.clientName` unset to keep using the default Redis registration above. To use a non-default Redis connection for distributed locks, set `distributed.clientName` to the name registered through `RedisModule.forRoot({ name, ... })`.
Leave `distributed.clientName` unset to keep using the default Redis registration above. To use a non-default Redis connection for distributed locks, set `distributed.clientName` to the name registered through `RedisModule.forRoot({ name, ... })`. fluo trims the configured client name during module option normalization and rejects blank values before lifecycle or status reporting uses the Redis dependency name.
`distributed.lockTtlMs` must stay at or above `1_000ms`. fluo renews the Redis lock before that TTL expires, including the minimum supported `1_000ms` boundary.
`distributed.lockTtlMs` must stay at or above `1_000ms`. When distributed locking is enabled, fluo validates that module-level TTL during option normalization before loading, resolving, or probing Redis. Task-level `lockTtlMs` overrides are validated only when module distributed mode and that task's distributed locking are both enabled. Disabled module or task locking does not fail solely because an inactive TTL is below the distributed minimum. fluo renews the Redis lock before the active TTL expires, including the minimum supported `1_000ms` boundary.
Each scheduler instance uses a platform-neutral default `distributed.ownerId`; set `distributed.ownerId` explicitly only when your deployment has a stronger stable-owner convention. Lock release runs in a `finally` path after task execution. If Redis release fails, fluo keeps local ownership in status snapshots and retries during shutdown; if Redis reports that another owner holds the key, local ownership is cleared because fencing has already moved elsewhere. Redis TTL and renewal timing are still drift-sensitive coordination primitives rather than hard fencing tokens, so long-running jobs should remain idempotent and use application-level fencing when stale work would be unsafe.
Each scheduler instance uses a platform-neutral default `distributed.ownerId`; set `distributed.ownerId` explicitly only when your deployment has a stronger stable-owner convention. When `distributed.ownerId` is provided, fluo trims it during module option normalization and rejects blank or non-string values before scheduler or Redis lifecycle setup, so invalid or empty owner identifiers cannot enter Redis lock ownership state. Lock release runs in a `finally` path after task execution. If bootstrap later fails while a distributed tick is already running, startup rollback keeps the Redis lock client available until that active task can drain and release its lock. If Redis release fails, fluo keeps local ownership in status snapshots and retries during shutdown; if Redis reports that another owner holds the key, local ownership is cleared because fencing has already moved elsewhere. Redis TTL and renewal timing are still drift-sensitive coordination primitives rather than hard fencing tokens, so long-running jobs should remain idempotent and use application-level fencing when stale work would be unsafe.

@@ -157,5 +174,5 @@ ```typescript

The registry exposes `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`, and `updateIntervalMs`. The first `name` argument is the default registry key; passing `options.name` overrides the actual registry key, scheduler metadata name, and default distributed lock key for dynamic tasks so dynamic registration matches decorator naming semantics. `get` and `getAll` return read-only `SchedulingTaskDescriptor` values, not live `CronJob` handles. Timeout tasks run once, then disable themselves while remaining in the registry so they can be re-enabled deliberately.
The registry exposes `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`, and `updateIntervalMs`. The first `name` argument is the default registry key; passing `options.name` overrides the actual registry key, scheduler metadata name, and default distributed lock key for dynamic tasks so dynamic registration matches decorator naming semantics. Registry, decorator, and dynamic `options.name` task names must be non-empty strings; blank dynamic override names are rejected before scheduler or registry state is retained. `get` and `getAll` return immutable `SchedulingTaskDescriptor` snapshots, not live `CronJob` handles or mutable registry state. Timeout tasks run once, then disable themselves while remaining in the registry so they can be re-enabled deliberately.
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 or interval cadence is also rollback-safe. If rescheduling fails, the previous expression or interval milliseconds and scheduled handle 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.
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 or interval cadence is also rollback-safe. A replacement is committed only after the previous scheduled handle stops successfully. If replacement scheduling fails or the previous handle cannot be stopped, fluo stops the provisional replacement, restores the previous expression or interval milliseconds and handle, and rethrows the failure instead of silently retaining duplicate schedules. 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.

@@ -166,3 +183,3 @@ ### Bounded Shutdown

By default the shutdown drain waits up to `10_000ms`. If that timeout expires, the scheduler logs a warning and continues shutdown without waiting for the hung task to settle. When distributed locking is enabled, locks held by still-running tasks are not eagerly released on timeout; they remain owned by that task until it settles normally, or until Redis expires the lock after the process exits. This prevents another node from starting the same job while the original task is still running.
By default the shutdown drain waits up to `10_000ms`. If that timeout expires, the scheduler logs a warning and continues shutdown without waiting for the hung task to settle. The same `shutdown.timeoutMs` boundary also applies to Redis owned-lock release I/O during shutdown, so a stuck Redis release cannot block process termination indefinitely. When distributed locking is enabled, locks held by still-running tasks are not eagerly released on timeout; they remain owned by that task until it settles normally, or until Redis expires the lock after the process exits. fluo calls `unref()` on lock renewal timers so they continue renewing while other work keeps the Node.js event loop active without retaining the process by themselves, and it clears them when the task settles. If release I/O itself times out, fluo preserves local owned-lock visibility/status and does not clear ownership until Redis confirms release or reports that another owner holds the key. This prevents another node from starting the same job while the original task is still running.

@@ -169,0 +186,0 @@ ```typescript