firstly 0.7.2 → 0.7.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # firstly
2
2
 
3
+ ## 0.7.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#315](https://github.com/jycouet/firstly/pull/315) [`3403d6c`](https://github.com/jycouet/firstly/commit/3403d6cffa657ad04acdac26024bdf6e989df7b4) Thanks [@jycouet](https://github.com/jycouet)! - cron: quieter logs by default - a tick only logs `done in Xms` when it took at least `logs.ended` ms (default 100; `true` = always, `false` = never). `starting` and `result` lines are opt-in, `logs.setup` can silence the registration line. If `onTick` throws, the run is now stored as `failed` (error in `result`), always logged, and no longer leaks the concurrency slot.
8
+
3
9
  ## 0.7.2
4
10
 
5
11
  ### Patch Changes
@@ -1,4 +1,4 @@
1
- declare const statuses: readonly ["starting", "ended", "skipped"];
1
+ declare const statuses: readonly ["starting", "ended", "skipped", "failed"];
2
2
  type StatusType = (typeof statuses)[number];
3
3
  export declare class Cron {
4
4
  id?: string;
package/esm/cron/Cron.js CHANGED
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { Entity, Fields } from 'remult';
8
8
  import { Roles_Cron } from './Roles_Cron';
9
- const statuses = ['starting', 'ended', 'skipped'];
9
+ const statuses = ['starting', 'ended', 'skipped', 'failed'];
10
10
  let Cron = class Cron {
11
11
  id;
12
12
  topic;
@@ -41,9 +41,17 @@ export type CronJobParams = {
41
41
  onTick: CronOnTick;
42
42
  topic: string;
43
43
  concurrent?: number;
44
+ /**
45
+ * Defaults: a tick logs `done in Xms` only when it took at least `ended` ms
46
+ * (default 100, `true` = always, `false` = never) + a `setup done` line at registration.
47
+ * `starting` & `result` are opt-in (full history incl. results is stored in `_ff_crons`).
48
+ * Failures and concurrency skips are always logged.
49
+ */
44
50
  logs?: {
51
+ setup?: boolean;
45
52
  starting?: boolean;
46
- ended?: boolean;
53
+ result?: boolean;
54
+ ended?: boolean | number;
47
55
  };
48
56
  start?: boolean;
49
57
  runOnInit?: boolean;
@@ -30,6 +30,8 @@ export const cronTime = {
30
30
  */
31
31
  every_friday_morning: '11 5 * * 5',
32
32
  };
33
+ const DEFAULT_ENDED_MIN_MS = 100;
34
+ const fmtDuration = (ms) => (ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`);
33
35
  /**
34
36
  * usage:
35
37
  *
@@ -59,47 +61,55 @@ export const cron = (jobsInfos) => {
59
61
  key: 'cron',
60
62
  entities: [Cron],
61
63
  });
62
- const logJobs = (topic, job, message, with_metadata = true, isSuccess = true) => {
63
- const l = [];
64
- l.push(magenta(`[${topic}]`));
65
- l.push(message);
66
- if (with_metadata) {
67
- // If the job is "stopped", there will still be a next date, but it will not fire it. The job has to start.
68
- l.push(`(${job.isActive ? green('running') : red('stopped')}, next at ${yellow(job.nextDate().toISO())})`);
69
- }
70
- if (isSuccess) {
71
- log.success(l.join(' '));
72
- }
73
- else {
74
- log.info(l.join(' '));
75
- }
76
- };
77
64
  m.initApi = async () => {
78
65
  jobsInfos.forEach((infos) => {
79
66
  const { topic, runOnInit, logs, concurrent, onTick: originalOnTick, ...params } = infos;
80
67
  const concurrentToUse = concurrent ?? 1;
68
+ const prefix = magenta(`[${topic}]`);
69
+ const endedMinMs = logs?.ended === false
70
+ ? Infinity
71
+ : logs?.ended === true
72
+ ? 0
73
+ : (logs?.ended ?? DEFAULT_ENDED_MIN_MS);
81
74
  // Create a wrapper that converts the return type to void for CronJob
82
75
  const wrappedOnTick = async () => {
83
- if (jobs[topic].concurrentInProgress < concurrentToUse) {
84
- jobs[topic].concurrentInProgress = jobs[topic].concurrentInProgress + 1;
85
- const rCron = await repo(Cron).insert({ topic });
86
- if (logs?.starting === undefined || logs?.starting === true) {
87
- logJobs(topic, job, 'starting...', false, false);
88
- }
76
+ if (jobs[topic].concurrentInProgress >= concurrentToUse) {
77
+ await repo(Cron).insert({ topic, status: 'skipped' });
78
+ log.info(`${prefix} skipped because of concurrent limit (${yellow(concurrentToUse.toString())})`);
79
+ return;
80
+ }
81
+ jobs[topic].concurrentInProgress++;
82
+ const startedAt = Date.now();
83
+ const rCron = await repo(Cron).insert({ topic });
84
+ if (logs?.starting) {
85
+ log.info(`${prefix} starting...`);
86
+ }
87
+ try {
89
88
  const res = await originalOnTick();
90
- log.info(`[${topic}] result:`, res);
91
89
  rCron.result = res;
92
90
  rCron.endedAt = new Date();
93
91
  rCron.status = 'ended';
94
92
  await repo(Cron).save(rCron);
95
- if (logs?.ended === undefined || logs?.ended === true) {
96
- logJobs(topic, job, 'done');
93
+ const ms = Date.now() - startedAt;
94
+ if (ms >= endedMinMs) {
95
+ const msg = `${prefix} done in ${fmtDuration(ms)}`;
96
+ if (logs?.result) {
97
+ log.success(msg, res);
98
+ }
99
+ else {
100
+ log.success(msg);
101
+ }
97
102
  }
98
- jobs[topic].concurrentInProgress = jobs[topic].concurrentInProgress - 1;
99
103
  }
100
- else {
101
- await repo(Cron).insert({ topic, status: 'skipped' });
102
- logJobs(topic, job, `skipped because of concurrent limit (${yellow(concurrentToUse.toString())})`, false, false);
104
+ catch (error) {
105
+ rCron.result = { error: error instanceof Error ? error.message : String(error) };
106
+ rCron.endedAt = new Date();
107
+ rCron.status = 'failed';
108
+ await repo(Cron).save(rCron);
109
+ log.error(`${prefix} failed after ${fmtDuration(Date.now() - startedAt)}`, error);
110
+ }
111
+ finally {
112
+ jobs[topic].concurrentInProgress--;
103
113
  }
104
114
  };
105
115
  // Use type assertion to bypass complex generic type issues
@@ -108,7 +118,10 @@ export const cron = (jobsInfos) => {
108
118
  onTick: wrappedOnTick,
109
119
  });
110
120
  jobs[topic] = { job, concurrentInProgress: 0 };
111
- logJobs(topic, job, 'setup done');
121
+ if (logs?.setup !== false) {
122
+ // A stopped job still reports a next date, it just won't fire it.
123
+ log.success(`${prefix} setup done (${job.isActive ? green('running') : red('stopped')}, next at ${yellow(job.nextDate().toISO())})`);
124
+ }
112
125
  // If not it will be done too early
113
126
  if (runOnInit) {
114
127
  job.fireOnTick();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firstly",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
5
  "description": "Firstly, an opinionated Remult setup!",
6
6
  "funding": "https://github.com/sponsors/jycouet",