@fluojs/cron 1.0.0-beta.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/LICENSE +21 -0
- package/README.ko.md +183 -0
- package/README.md +183 -0
- package/dist/decorators.d.ts +41 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +116 -0
- package/dist/distributed-lock-manager.d.ts +38 -0
- package/dist/distributed-lock-manager.d.ts.map +1 -0
- package/dist/distributed-lock-manager.js +181 -0
- package/dist/expressions.d.ts +13 -0
- package/dist/expressions.d.ts.map +1 -0
- package/dist/expressions.js +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/metadata.d.ts +17 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +78 -0
- package/dist/module.d.ts +31 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +95 -0
- package/dist/scheduler.d.ts +3 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +8 -0
- package/dist/service.d.ts +118 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +528 -0
- package/dist/status.d.ts +32 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +107 -0
- package/dist/task-discovery.d.ts +8 -0
- package/dist/task-discovery.d.ts.map +1 -0
- package/dist/task-discovery.js +104 -0
- package/dist/task-runner.d.ts +15 -0
- package/dist/task-runner.d.ts.map +1 -0
- package/dist/task-runner.js +87 -0
- package/dist/tokens.d.ts +7 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +4 -0
- package/dist/types.d.ts +206 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { getRedisClientToken } from '@fluojs/redis';
|
|
2
|
+
const RELEASE_LOCK_SCRIPT = 'if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end';
|
|
3
|
+
const RENEW_LOCK_SCRIPT = 'if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("PEXPIRE", KEYS[1], ARGV[2]) else return 0 end';
|
|
4
|
+
export class CronDistributedLockManager {
|
|
5
|
+
ownedLockKeys = new Set();
|
|
6
|
+
redisClient;
|
|
7
|
+
lockOwnershipLosses = 0;
|
|
8
|
+
lockRenewalFailures = 0;
|
|
9
|
+
constructor(options, runtimeContainer, logger) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.runtimeContainer = runtimeContainer;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
get resolvedClient() {
|
|
15
|
+
return this.redisClient;
|
|
16
|
+
}
|
|
17
|
+
get ownedLocks() {
|
|
18
|
+
return this.ownedLockKeys.size;
|
|
19
|
+
}
|
|
20
|
+
get ownershipLosses() {
|
|
21
|
+
return this.lockOwnershipLosses;
|
|
22
|
+
}
|
|
23
|
+
get renewalFailures() {
|
|
24
|
+
return this.lockRenewalFailures;
|
|
25
|
+
}
|
|
26
|
+
async resolveClient() {
|
|
27
|
+
if (!this.options.distributed.enabled) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const redisToken = getRedisClientToken(this.options.distributed.clientName);
|
|
31
|
+
if (!this.runtimeContainer.has(redisToken)) {
|
|
32
|
+
throw new Error('Cron distributed mode requires the configured Redis client to be registered.');
|
|
33
|
+
}
|
|
34
|
+
const redisClient = await this.runtimeContainer.resolve(redisToken);
|
|
35
|
+
if (!hasRedisLockClient(redisClient)) {
|
|
36
|
+
throw new Error('Cron distributed mode requires the configured Redis client to implement set/eval lock operations.');
|
|
37
|
+
}
|
|
38
|
+
this.redisClient = redisClient;
|
|
39
|
+
}
|
|
40
|
+
reset() {
|
|
41
|
+
this.redisClient = undefined;
|
|
42
|
+
}
|
|
43
|
+
async tryAcquireLock(descriptor) {
|
|
44
|
+
const redis = this.redisClient;
|
|
45
|
+
if (!redis) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const result = await redis.set(descriptor.lockKey, this.options.distributed.ownerId, 'PX', descriptor.lockTtlMs, 'NX');
|
|
50
|
+
if (result === 'OK') {
|
|
51
|
+
this.ownedLockKeys.add(descriptor.lockKey);
|
|
52
|
+
}
|
|
53
|
+
return result === 'OK';
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.logger.error(`Failed to acquire distributed cron lock for ${descriptor.taskName}.`, error, 'CronLifecycleService');
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
startLockRenewalMonitor(descriptor) {
|
|
60
|
+
const renewalState = this.createLockRenewalState(descriptor.lockTtlMs);
|
|
61
|
+
const renewalTimer = setInterval(() => {
|
|
62
|
+
if (renewalState.stopped) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
renewalState.nextRenewalDueAt += renewalState.renewalIntervalMs;
|
|
66
|
+
renewalState.renewalChain = renewalState.renewalChain.then(async () => {
|
|
67
|
+
await this.runLockRenewalAttempt(descriptor, renewalState);
|
|
68
|
+
});
|
|
69
|
+
}, renewalState.renewalIntervalMs);
|
|
70
|
+
return {
|
|
71
|
+
getPostRunError: async () => {
|
|
72
|
+
this.queueDueLockRenewalAttempts(descriptor, renewalState);
|
|
73
|
+
await renewalState.renewalChain;
|
|
74
|
+
return renewalState.lockPostRunError;
|
|
75
|
+
},
|
|
76
|
+
stop: () => {
|
|
77
|
+
if (renewalState.stopped) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
renewalState.stopped = true;
|
|
81
|
+
clearInterval(renewalTimer);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async releaseLock(descriptor) {
|
|
86
|
+
await this.releaseLockKey(descriptor.lockKey, descriptor.taskName);
|
|
87
|
+
}
|
|
88
|
+
async releaseOwnedLocks() {
|
|
89
|
+
if (!this.redisClient || this.ownedLockKeys.size === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const lockKeys = Array.from(this.ownedLockKeys);
|
|
93
|
+
await Promise.all(lockKeys.map(async lockKey => {
|
|
94
|
+
await this.releaseLockKey(lockKey, lockKey);
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
createLockRenewalState(lockTtlMs) {
|
|
98
|
+
const renewalIntervalMs = Math.max(250, Math.floor(lockTtlMs / 2));
|
|
99
|
+
return {
|
|
100
|
+
lockPostRunError: undefined,
|
|
101
|
+
nextRenewalDueAt: Date.now() + renewalIntervalMs,
|
|
102
|
+
renewalChain: Promise.resolve(),
|
|
103
|
+
renewalIntervalMs,
|
|
104
|
+
stopped: false
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
queueDueLockRenewalAttempts(descriptor, renewalState) {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
while (now >= renewalState.nextRenewalDueAt) {
|
|
110
|
+
renewalState.nextRenewalDueAt += renewalState.renewalIntervalMs;
|
|
111
|
+
renewalState.renewalChain = renewalState.renewalChain.then(async () => {
|
|
112
|
+
await this.runLockRenewalAttempt(descriptor, renewalState);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async runLockRenewalAttempt(descriptor, renewalState) {
|
|
117
|
+
const outcome = await this.renewLock(descriptor);
|
|
118
|
+
if (outcome === 'ownership-lost') {
|
|
119
|
+
this.lockOwnershipLosses += 1;
|
|
120
|
+
}
|
|
121
|
+
if (outcome === 'renewal-failed') {
|
|
122
|
+
this.lockRenewalFailures += 1;
|
|
123
|
+
}
|
|
124
|
+
if (renewalState.lockPostRunError) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
renewalState.lockPostRunError = this.toLockPostRunError(outcome, descriptor.taskName);
|
|
128
|
+
}
|
|
129
|
+
toLockPostRunError(outcome, taskName) {
|
|
130
|
+
if (outcome === 'ownership-lost') {
|
|
131
|
+
return new Error(`Distributed cron lock ownership lost for ${taskName}.`);
|
|
132
|
+
}
|
|
133
|
+
if (outcome === 'renewal-failed') {
|
|
134
|
+
return new Error(`Distributed cron lock renewal failed for ${taskName}.`);
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
async renewLock(descriptor) {
|
|
139
|
+
const redis = this.redisClient;
|
|
140
|
+
if (!redis) {
|
|
141
|
+
return 'renewed';
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const result = await redis.eval(RENEW_LOCK_SCRIPT, 1, descriptor.lockKey, this.options.distributed.ownerId, String(descriptor.lockTtlMs));
|
|
145
|
+
if (typeof result === 'number' && result <= 0) {
|
|
146
|
+
this.logger.warn(`Distributed cron lock ownership was lost for ${descriptor.taskName}.`, 'CronLifecycleService');
|
|
147
|
+
return 'ownership-lost';
|
|
148
|
+
}
|
|
149
|
+
this.logger.log(`Renewed distributed cron lock for ${descriptor.taskName}.`, 'CronLifecycleService');
|
|
150
|
+
return 'renewed';
|
|
151
|
+
} catch (error) {
|
|
152
|
+
this.logger.error(`Failed to renew distributed cron lock for ${descriptor.taskName}.`, error, 'CronLifecycleService');
|
|
153
|
+
return 'renewal-failed';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async releaseLockKey(lockKey, taskName) {
|
|
157
|
+
const redis = this.redisClient;
|
|
158
|
+
if (!redis) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const result = await redis.eval(RELEASE_LOCK_SCRIPT, 1, lockKey, this.options.distributed.ownerId);
|
|
163
|
+
if (typeof result === 'number' && result <= 0) {
|
|
164
|
+
this.logger.warn(`Distributed cron lock for ${taskName} was already released or owned by another node.`, 'CronLifecycleService');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.logger.log(`Released distributed cron lock for ${taskName}.`, 'CronLifecycleService');
|
|
168
|
+
} catch (error) {
|
|
169
|
+
this.logger.error(`Failed to release distributed cron lock for ${taskName}.`, error, 'CronLifecycleService');
|
|
170
|
+
} finally {
|
|
171
|
+
this.ownedLockKeys.delete(lockKey);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function hasRedisLockClient(value) {
|
|
176
|
+
if (typeof value !== 'object' || value === null) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const client = value;
|
|
180
|
+
return typeof client.set === 'function' && typeof client.eval === 'function';
|
|
181
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const CronExpression: {
|
|
2
|
+
readonly EVERY_SECOND: "* * * * * *";
|
|
3
|
+
readonly EVERY_5_SECONDS: "*/5 * * * * *";
|
|
4
|
+
readonly EVERY_10_SECONDS: "*/10 * * * * *";
|
|
5
|
+
readonly EVERY_30_SECONDS: "*/30 * * * * *";
|
|
6
|
+
readonly EVERY_MINUTE: "0 * * * * *";
|
|
7
|
+
readonly EVERY_5_MINUTES: "0 */5 * * * *";
|
|
8
|
+
readonly EVERY_10_MINUTES: "0 */10 * * * *";
|
|
9
|
+
readonly EVERY_30_MINUTES: "0 */30 * * * *";
|
|
10
|
+
readonly EVERY_HOUR: "0 0 * * * *";
|
|
11
|
+
readonly EVERY_DAY_AT_MIDNIGHT: "0 0 0 * * *";
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=expressions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expressions.d.ts","sourceRoot":"","sources":["../src/expressions.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc;;;;;;;;;;;CAWjB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const CronExpression = {
|
|
2
|
+
EVERY_SECOND: '* * * * * *',
|
|
3
|
+
EVERY_5_SECONDS: '*/5 * * * * *',
|
|
4
|
+
EVERY_10_SECONDS: '*/10 * * * * *',
|
|
5
|
+
EVERY_30_SECONDS: '*/30 * * * * *',
|
|
6
|
+
EVERY_MINUTE: '0 * * * * *',
|
|
7
|
+
EVERY_5_MINUTES: '0 */5 * * * *',
|
|
8
|
+
EVERY_10_MINUTES: '0 */10 * * * *',
|
|
9
|
+
EVERY_30_MINUTES: '0 */30 * * * *',
|
|
10
|
+
EVERY_HOUR: '0 0 * * * *',
|
|
11
|
+
EVERY_DAY_AT_MIDNIGHT: '0 0 0 * * *'
|
|
12
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './decorators.js';
|
|
2
|
+
export * from './expressions.js';
|
|
3
|
+
export * from './metadata.js';
|
|
4
|
+
export * from './module.js';
|
|
5
|
+
export * from './status.js';
|
|
6
|
+
export { SCHEDULING_REGISTRY } from './tokens.js';
|
|
7
|
+
export * from './types.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type MetadataPropertyKey } from '@fluojs/core';
|
|
2
|
+
import type { CronTaskMetadata, SchedulingTaskMetadata } from './types.js';
|
|
3
|
+
export declare function defineSchedulingTaskMetadata(target: object, propertyKey: MetadataPropertyKey, metadata: SchedulingTaskMetadata): void;
|
|
4
|
+
export declare function defineCronTaskMetadata(target: object, propertyKey: MetadataPropertyKey, metadata: CronTaskMetadata): void;
|
|
5
|
+
export declare function getSchedulingTaskMetadata(target: object, propertyKey: MetadataPropertyKey): SchedulingTaskMetadata | undefined;
|
|
6
|
+
export declare function getCronTaskMetadata(target: object, propertyKey: MetadataPropertyKey): CronTaskMetadata | undefined;
|
|
7
|
+
export declare function getSchedulingTaskMetadataEntries(target: object): Array<{
|
|
8
|
+
metadata: SchedulingTaskMetadata;
|
|
9
|
+
propertyKey: MetadataPropertyKey;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function getCronTaskMetadataEntries(target: object): Array<{
|
|
12
|
+
metadata: CronTaskMetadata;
|
|
13
|
+
propertyKey: MetadataPropertyKey;
|
|
14
|
+
}>;
|
|
15
|
+
export declare const schedulingMetadataSymbol: symbol;
|
|
16
|
+
export declare const cronMetadataSymbol: symbol;
|
|
17
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAwD3E,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,mBAAmB,EAChC,QAAQ,EAAE,sBAAsB,GAC/B,IAAI,CAEN;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,mBAAmB,EAAE,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAEzH;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,mBAAmB,GAAG,sBAAsB,GAAG,SAAS,CAS9H;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,mBAAmB,GAAG,gBAAgB,GAAG,SAAS,CAIlH;AAED,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,MAAM,GACb,KAAK,CAAC;IAAE,QAAQ,EAAE,sBAAsB,CAAC;IAAC,WAAW,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAW/E;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,WAAW,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAIlI;AAED,eAAO,MAAM,wBAAwB,QAAgC,CAAC;AACtE,eAAO,MAAM,kBAAkB,QAAgC,CAAC"}
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ensureSymbolMetadataPolyfill, metadataSymbol } from '@fluojs/core/internal';
|
|
2
|
+
void ensureSymbolMetadataPolyfill();
|
|
3
|
+
const standardSchedulingMetadataKey = Symbol.for('fluo.cron.standard.task');
|
|
4
|
+
const schedulingMetadataStore = new WeakMap();
|
|
5
|
+
function cloneTaskMetadata(metadata) {
|
|
6
|
+
if (metadata.kind === 'cron') {
|
|
7
|
+
return {
|
|
8
|
+
expression: metadata.expression,
|
|
9
|
+
kind: 'cron',
|
|
10
|
+
options: {
|
|
11
|
+
...metadata.options
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (metadata.kind === 'interval') {
|
|
16
|
+
return {
|
|
17
|
+
kind: 'interval',
|
|
18
|
+
ms: metadata.ms,
|
|
19
|
+
options: {
|
|
20
|
+
...metadata.options
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
kind: 'timeout',
|
|
26
|
+
ms: metadata.ms,
|
|
27
|
+
options: {
|
|
28
|
+
...metadata.options
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function getStandardMetadataBag(target) {
|
|
33
|
+
return target[metadataSymbol];
|
|
34
|
+
}
|
|
35
|
+
function getStandardSchedulingMap(target) {
|
|
36
|
+
const constructor = target.constructor;
|
|
37
|
+
return constructor ? getStandardMetadataBag(constructor)?.[standardSchedulingMetadataKey] : undefined;
|
|
38
|
+
}
|
|
39
|
+
function getOrCreateSchedulingMap(target) {
|
|
40
|
+
let map = schedulingMetadataStore.get(target);
|
|
41
|
+
if (!map) {
|
|
42
|
+
map = new Map();
|
|
43
|
+
schedulingMetadataStore.set(target, map);
|
|
44
|
+
}
|
|
45
|
+
return map;
|
|
46
|
+
}
|
|
47
|
+
export function defineSchedulingTaskMetadata(target, propertyKey, metadata) {
|
|
48
|
+
getOrCreateSchedulingMap(target).set(propertyKey, cloneTaskMetadata(metadata));
|
|
49
|
+
}
|
|
50
|
+
export function defineCronTaskMetadata(target, propertyKey, metadata) {
|
|
51
|
+
defineSchedulingTaskMetadata(target, propertyKey, metadata);
|
|
52
|
+
}
|
|
53
|
+
export function getSchedulingTaskMetadata(target, propertyKey) {
|
|
54
|
+
const stored = schedulingMetadataStore.get(target)?.get(propertyKey);
|
|
55
|
+
const standard = getStandardSchedulingMap(target)?.get(propertyKey);
|
|
56
|
+
if (!stored && !standard) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return cloneTaskMetadata(stored ?? standard);
|
|
60
|
+
}
|
|
61
|
+
export function getCronTaskMetadata(target, propertyKey) {
|
|
62
|
+
const metadata = getSchedulingTaskMetadata(target, propertyKey);
|
|
63
|
+
return metadata?.kind === 'cron' ? metadata : undefined;
|
|
64
|
+
}
|
|
65
|
+
export function getSchedulingTaskMetadataEntries(target) {
|
|
66
|
+
const stored = schedulingMetadataStore.get(target) ?? new Map();
|
|
67
|
+
const standard = getStandardSchedulingMap(target) ?? new Map();
|
|
68
|
+
const keys = new Set([...stored.keys(), ...standard.keys()]);
|
|
69
|
+
return Array.from(keys).map(propertyKey => ({
|
|
70
|
+
metadata: getSchedulingTaskMetadata(target, propertyKey),
|
|
71
|
+
propertyKey
|
|
72
|
+
})).filter(entry => entry.metadata !== undefined);
|
|
73
|
+
}
|
|
74
|
+
export function getCronTaskMetadataEntries(target) {
|
|
75
|
+
return getSchedulingTaskMetadataEntries(target).filter(entry => entry.metadata.kind === 'cron');
|
|
76
|
+
}
|
|
77
|
+
export const schedulingMetadataSymbol = standardSchedulingMetadataKey;
|
|
78
|
+
export const cronMetadataSymbol = standardSchedulingMetadataKey;
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ModuleType } from '@fluojs/runtime';
|
|
2
|
+
import type { CronModuleOptions, NormalizedCronModuleOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes module options so the runtime can rely on concrete scheduler and lock settings.
|
|
5
|
+
*
|
|
6
|
+
* @param options Raw cron module options supplied by the application.
|
|
7
|
+
* @returns A normalized options object with concrete distributed defaults and scheduler implementation.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeCronModuleOptions(options?: CronModuleOptions): NormalizedCronModuleOptions;
|
|
10
|
+
/** Runtime module entrypoint for decorator-driven scheduling. */
|
|
11
|
+
export declare class CronModule {
|
|
12
|
+
/**
|
|
13
|
+
* Registers the scheduling registry and optional distributed locking defaults.
|
|
14
|
+
*
|
|
15
|
+
* @param options Scheduler and distributed-lock options for discovered and dynamic tasks.
|
|
16
|
+
* @returns A module definition that exports the {@link SCHEDULING_REGISTRY} token.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { Module } from '@fluojs/core';
|
|
21
|
+
* import { CronModule } from '@fluojs/cron';
|
|
22
|
+
*
|
|
23
|
+
* @Module({
|
|
24
|
+
* imports: [CronModule.forRoot({ distributed: true })],
|
|
25
|
+
* })
|
|
26
|
+
* export class AppModule {}
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
static forRoot(options?: CronModuleOptions): ModuleType;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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;AAkDjF;;;;;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;CAQ5D"}
|
package/dist/module.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { defineModule } from '@fluojs/runtime';
|
|
2
|
+
import { CronLifecycleService } from './service.js';
|
|
3
|
+
import { defaultCronScheduler } from './scheduler.js';
|
|
4
|
+
import { CRON_OPTIONS, SCHEDULING_REGISTRY } from './tokens.js';
|
|
5
|
+
const DEFAULT_CRON_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
6
|
+
function randomId() {
|
|
7
|
+
return `${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
8
|
+
}
|
|
9
|
+
function normalizeDistributedOptions(distributed) {
|
|
10
|
+
if (distributed === undefined || distributed === false) {
|
|
11
|
+
return {
|
|
12
|
+
clientName: undefined,
|
|
13
|
+
enabled: false,
|
|
14
|
+
keyPrefix: 'fluo:cron:lock',
|
|
15
|
+
lockTtlMs: 30_000,
|
|
16
|
+
ownerId: randomId()
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (distributed === true) {
|
|
20
|
+
return {
|
|
21
|
+
clientName: undefined,
|
|
22
|
+
enabled: true,
|
|
23
|
+
keyPrefix: 'fluo:cron:lock',
|
|
24
|
+
lockTtlMs: 30_000,
|
|
25
|
+
ownerId: randomId()
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
clientName: distributed.clientName,
|
|
30
|
+
enabled: distributed.enabled ?? true,
|
|
31
|
+
keyPrefix: distributed.keyPrefix ?? 'fluo:cron:lock',
|
|
32
|
+
lockTtlMs: distributed.lockTtlMs ?? 30_000,
|
|
33
|
+
ownerId: distributed.ownerId ?? randomId()
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function normalizeShutdownOptions(shutdown) {
|
|
37
|
+
const timeoutMs = shutdown?.timeoutMs ?? DEFAULT_CRON_SHUTDOWN_TIMEOUT_MS;
|
|
38
|
+
if (!Number.isFinite(timeoutMs) || !Number.isInteger(timeoutMs) || timeoutMs < 0) {
|
|
39
|
+
throw new Error('Cron shutdown timeoutMs must be a non-negative integer.');
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
timeoutMs
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalizes module options so the runtime can rely on concrete scheduler and lock settings.
|
|
48
|
+
*
|
|
49
|
+
* @param options Raw cron module options supplied by the application.
|
|
50
|
+
* @returns A normalized options object with concrete distributed defaults and scheduler implementation.
|
|
51
|
+
*/
|
|
52
|
+
export function normalizeCronModuleOptions(options = {}) {
|
|
53
|
+
return {
|
|
54
|
+
distributed: normalizeDistributedOptions(options.distributed),
|
|
55
|
+
scheduler: options.scheduler ?? defaultCronScheduler,
|
|
56
|
+
shutdown: normalizeShutdownOptions(options.shutdown)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function createCronProviders(options = {}) {
|
|
60
|
+
return [{
|
|
61
|
+
provide: CRON_OPTIONS,
|
|
62
|
+
useValue: normalizeCronModuleOptions(options)
|
|
63
|
+
}, {
|
|
64
|
+
provide: SCHEDULING_REGISTRY,
|
|
65
|
+
useClass: CronLifecycleService
|
|
66
|
+
}];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Runtime module entrypoint for decorator-driven scheduling. */
|
|
70
|
+
export class CronModule {
|
|
71
|
+
/**
|
|
72
|
+
* Registers the scheduling registry and optional distributed locking defaults.
|
|
73
|
+
*
|
|
74
|
+
* @param options Scheduler and distributed-lock options for discovered and dynamic tasks.
|
|
75
|
+
* @returns A module definition that exports the {@link SCHEDULING_REGISTRY} token.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* import { Module } from '@fluojs/core';
|
|
80
|
+
* import { CronModule } from '@fluojs/cron';
|
|
81
|
+
*
|
|
82
|
+
* @Module({
|
|
83
|
+
* imports: [CronModule.forRoot({ distributed: true })],
|
|
84
|
+
* })
|
|
85
|
+
* export class AppModule {}
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
static forRoot(options = {}) {
|
|
89
|
+
class CronModuleDefinition {}
|
|
90
|
+
return defineModule(CronModuleDefinition, {
|
|
91
|
+
exports: [SCHEDULING_REGISTRY],
|
|
92
|
+
providers: createCronProviders(options)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,eAAO,MAAM,oBAAoB,EAAE,aAMlC,CAAC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Container } from '@fluojs/di';
|
|
2
|
+
import { type ApplicationLogger, type CompiledModule, type OnApplicationBootstrap, type OnApplicationShutdown, type OnModuleDestroy } from '@fluojs/runtime';
|
|
3
|
+
import type { CronTaskOptions, IntervalTaskOptions, NormalizedCronModuleOptions, SchedulingRegistry, SchedulingTaskCallback, SchedulingTaskDescriptor, TimeoutTaskOptions } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Lifecycle-managed scheduler runtime for decorator-discovered and dynamic tasks.
|
|
6
|
+
*
|
|
7
|
+
* The service discovers scheduling decorators during bootstrap, coordinates
|
|
8
|
+
* optional distributed locks through Redis, and exposes runtime task management
|
|
9
|
+
* through {@link SchedulingRegistry}.
|
|
10
|
+
*/
|
|
11
|
+
export declare class CronLifecycleService implements SchedulingRegistry, OnApplicationBootstrap, OnApplicationShutdown, OnModuleDestroy {
|
|
12
|
+
private readonly options;
|
|
13
|
+
private readonly runtimeContainer;
|
|
14
|
+
private readonly compiledModules;
|
|
15
|
+
private readonly logger;
|
|
16
|
+
private readonly tasks;
|
|
17
|
+
private readonly activeTasks;
|
|
18
|
+
private readonly distributedLocks;
|
|
19
|
+
private readonly taskRunner;
|
|
20
|
+
private lifecycleState;
|
|
21
|
+
private started;
|
|
22
|
+
private shutdownPromise;
|
|
23
|
+
constructor(options: NormalizedCronModuleOptions, runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger);
|
|
24
|
+
/**
|
|
25
|
+
* Registers a cron task at runtime.
|
|
26
|
+
*
|
|
27
|
+
* @param name Stable task name used for lookup and distributed lock derivation.
|
|
28
|
+
* @param expression Cron expression validated before registration.
|
|
29
|
+
* @param callback Task body executed on matching cron ticks.
|
|
30
|
+
* @param options Optional hooks, distributed lock overrides, and timezone.
|
|
31
|
+
*/
|
|
32
|
+
addCron(name: string, expression: string, callback: SchedulingTaskCallback, options?: CronTaskOptions): void;
|
|
33
|
+
/**
|
|
34
|
+
* Registers a fixed-interval task at runtime.
|
|
35
|
+
*
|
|
36
|
+
* @param name Stable task name used for lookup and distributed lock derivation.
|
|
37
|
+
* @param ms Positive interval in milliseconds.
|
|
38
|
+
* @param callback Task body executed on each interval.
|
|
39
|
+
* @param options Optional hooks and distributed lock overrides.
|
|
40
|
+
*/
|
|
41
|
+
addInterval(name: string, ms: number, callback: SchedulingTaskCallback, options?: IntervalTaskOptions): void;
|
|
42
|
+
/**
|
|
43
|
+
* Registers a one-shot delayed task at runtime.
|
|
44
|
+
*
|
|
45
|
+
* @param name Stable task name used for lookup and distributed lock derivation.
|
|
46
|
+
* @param ms Positive delay in milliseconds before execution.
|
|
47
|
+
* @param callback Task body executed once after the delay.
|
|
48
|
+
* @param options Optional hooks and distributed lock overrides.
|
|
49
|
+
*/
|
|
50
|
+
addTimeout(name: string, ms: number, callback: SchedulingTaskCallback, options?: TimeoutTaskOptions): void;
|
|
51
|
+
/**
|
|
52
|
+
* Removes a registered task by name.
|
|
53
|
+
*
|
|
54
|
+
* @param name Task name to remove.
|
|
55
|
+
* @returns `true` when a task existed and was removed.
|
|
56
|
+
*/
|
|
57
|
+
remove(name: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Enables a task that was previously disabled.
|
|
60
|
+
*
|
|
61
|
+
* @param name Task name to enable.
|
|
62
|
+
* @returns `true` when the task exists after the operation.
|
|
63
|
+
*/
|
|
64
|
+
enable(name: string): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Disables a task without removing its descriptor.
|
|
67
|
+
*
|
|
68
|
+
* @param name Task name to disable.
|
|
69
|
+
* @returns `true` when the task exists after the operation.
|
|
70
|
+
*/
|
|
71
|
+
disable(name: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Looks up one task descriptor.
|
|
74
|
+
*
|
|
75
|
+
* @param name Task name to inspect.
|
|
76
|
+
* @returns The task descriptor, or `undefined` when not found.
|
|
77
|
+
*/
|
|
78
|
+
get(name: string): SchedulingTaskDescriptor | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Lists every known task descriptor.
|
|
81
|
+
*
|
|
82
|
+
* @returns All decorator-discovered and dynamically registered task descriptors.
|
|
83
|
+
*/
|
|
84
|
+
getAll(): SchedulingTaskDescriptor[];
|
|
85
|
+
/**
|
|
86
|
+
* Replaces the cron expression of one existing cron task.
|
|
87
|
+
*
|
|
88
|
+
* @param name Name of the cron task to update.
|
|
89
|
+
* @param expression New cron expression to validate and schedule.
|
|
90
|
+
*/
|
|
91
|
+
updateCronExpression(name: string, expression: string): void;
|
|
92
|
+
onApplicationBootstrap(): Promise<void>;
|
|
93
|
+
onApplicationShutdown(): Promise<void>;
|
|
94
|
+
onModuleDestroy(): Promise<void>;
|
|
95
|
+
createPlatformStatusSnapshot(): import("./status.js").CronPlatformStatusSnapshot;
|
|
96
|
+
private toSchedulingTaskDescriptor;
|
|
97
|
+
private shutdown;
|
|
98
|
+
private startLifecycle;
|
|
99
|
+
private validateDistributedLockConfiguration;
|
|
100
|
+
private handleStartupFailure;
|
|
101
|
+
private runShutdownLifecycle;
|
|
102
|
+
private registerDecoratorTasks;
|
|
103
|
+
private registerTask;
|
|
104
|
+
private assertTaskNameAvailable;
|
|
105
|
+
private scheduleEnabledTasks;
|
|
106
|
+
private scheduleTask;
|
|
107
|
+
private completeTimeoutTask;
|
|
108
|
+
private unscheduleTask;
|
|
109
|
+
private handleTaskTick;
|
|
110
|
+
private runTaskTick;
|
|
111
|
+
private shouldUseDistributedExecution;
|
|
112
|
+
private runDistributedTaskTick;
|
|
113
|
+
private waitForActiveTasks;
|
|
114
|
+
private drainActiveTasks;
|
|
115
|
+
private executeTask;
|
|
116
|
+
private stopAllScheduledTasks;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|