@quark-fw/plugin-cron 0.1.19 → 0.1.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quark-fw/plugin-cron",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.ts",
6
6
  "type": "module",
@@ -23,12 +23,12 @@
23
23
  "access": "public"
24
24
  },
25
25
  "dependencies": {
26
- "@quark-fw/atom": "0.1.19",
27
- "@quark-fw/plugin": "0.1.19"
26
+ "@quark-fw/atom": "0.1.21",
27
+ "@quark-fw/plugin": "0.1.21"
28
28
  },
29
29
  "peerDependencies": {
30
- "@quark-fw/core": "0.1.19",
31
- "@quark-fw/plugin-redis": "0.1.19",
30
+ "@quark-fw/core": "0.1.21",
31
+ "@quark-fw/plugin-redis": "0.1.21",
32
32
  "ioredis": "^5.9.3"
33
33
  },
34
34
  "devDependencies": {
@@ -78,6 +78,22 @@ describe("CronScheduler", () => {
78
78
  expect((await cron.status())[0]!.stale).toBe(true);
79
79
  });
80
80
 
81
+ test("не запускавшаяся задача протухает только спустя maxAge после регистрации", async () => {
82
+ const { client } = fakeRedis();
83
+ let now = at("2026-10-06T12:00:00Z");
84
+ const cron = new CronScheduler({
85
+ client: () => client,
86
+ now: () => now,
87
+ prefix: "t:",
88
+ });
89
+ cron.schedule("job", "0 3 * * *", async () => {}, {
90
+ maxAgeMs: 3_600_000,
91
+ });
92
+ expect((await cron.status())[0]!.stale).toBe(false);
93
+ now = at("2026-10-06T13:30:00Z");
94
+ expect((await cron.status())[0]!.stale).toBe(true);
95
+ });
96
+
81
97
  test("лок в Redis: второй инстанс не запускает ту же задачу", async () => {
82
98
  const { client } = fakeRedis();
83
99
  const a = new CronScheduler({ client: () => client, prefix: "t:" });
@@ -43,7 +43,11 @@ export class CronScheduler {
43
43
  private readonly onError: (job: string, error: unknown) => void;
44
44
  private readonly jobs = new Map<
45
45
  string,
46
- CronJob & { fields: CronFields; firedMinute?: string }
46
+ CronJob & {
47
+ fields: CronFields;
48
+ firedMinute?: string;
49
+ registeredAt: number;
50
+ }
47
51
  >();
48
52
  private readonly running = new Set<string>();
49
53
  private timer: NodeJS.Timeout | undefined;
@@ -78,6 +82,7 @@ export class CronScheduler {
78
82
  handler,
79
83
  options: { lockTtlMs: DEFAULT_LOCK_TTL, ...options },
80
84
  fields: parseCron(expression),
85
+ registeredAt: this.now().getTime(),
81
86
  });
82
87
  if (options.runOnStart) void this.run(name);
83
88
  }
@@ -189,12 +194,17 @@ export class CronScheduler {
189
194
  this.onError(job.name, error);
190
195
  }
191
196
  const lastSuccessAt = raw.lastSuccessAt || null;
197
+ // протухла: успех старше maxAge, а если успеха ещё не было — задача
198
+ // зарегистрирована дольше maxAge назад (свежий контейнер не виноват,
199
+ // что расписание ещё не наступило)
192
200
  const stale =
193
201
  job.options.maxAgeMs === undefined
194
202
  ? undefined
195
- : !lastSuccessAt ||
196
- this.now().getTime() -
197
- new Date(lastSuccessAt).getTime() >
203
+ : lastSuccessAt
204
+ ? this.now().getTime() -
205
+ new Date(lastSuccessAt).getTime() >
206
+ job.options.maxAgeMs
207
+ : this.now().getTime() - job.registeredAt >
198
208
  job.options.maxAgeMs;
199
209
  result.push({
200
210
  name: job.name,