Skip to content

Commit 8bc49c6

Browse files
authored
Merge pull request #8 from chaqchase/feat/enhancements-cleanup
New features and improvements
2 parents aef9692 + b4393e5 commit 8bc49c6

File tree

18 files changed

+902
-368
lines changed

18 files changed

+902
-368
lines changed

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"commit": false,
55
"fixed": [],
66
"linked": [],
7-
"access": "restricted",
7+
"access": "public",
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
1010
"ignore": []

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# cronbake
22

3+
## 0.3.0
4+
5+
### Minor Changes
6+
7+
- Immediate/delayed first run: Add `immediate` and `delay` options so the first callback can run right away or after a configurable delay (e.g., `"10s"`).
8+
- Overrun protection: Add `overrunProtection` (default: true) to skip starting a new run if the previous execution is still running; tracks `skippedExecutions` in metrics.
9+
- Pluggable persistence: Refactor persistence to use providers. Add `FilePersistenceProvider` (JSON on disk) and `RedisPersistenceProvider` (single-key JSON via injected client). New `persistence.strategy` and `persistence.provider` options.
10+
11+
### Features
12+
13+
- New Cron options: `immediate`, `delay`, `overrunProtection`.
14+
- New metrics: `skippedExecutions`.
15+
- New persistence API: `PersistenceProvider` with `save/load` and types for persisted state.
16+
- Export providers from package API: `FilePersistenceProvider`, `RedisPersistenceProvider`.
17+
18+
### Fixes
19+
20+
- Parser: Correct `@on_<day>` mapping to Sunday=0…Saturday=6, fix `@every_<n>_months` to run on day 1 (`0 0 0 1 */n *`), and `@every_<n>_dayOfWeek` to `*/n` on day-of-week.
21+
- Types: Replace `Timer` with `ReturnType<typeof setTimeout|setInterval>` for portability.
22+
- Build: Replace `@/lib` path aliases in source with relative imports to avoid bundler resolution issues.
23+
- Packaging: Set `module` to `dist/index.js`, move `@changesets/cli` to devDependencies, ensure Changesets access is public.
24+
25+
### Tests
26+
27+
- Add tests for immediate/delayed first run and overrun protection.
28+
- Add provider-focused tests and shared test utilities for persistence (file and Redis via a fake client).
29+
330
## 0.2.0
431

532
### Minor Changes

README.md

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ Cronbake provides two scheduling modes for optimal performance:
4646
- **Calculated Timeouts** (default): More efficient scheduling using precise timeout calculations
4747
- **Polling Interval**: Traditional polling-based scheduling with configurable intervals
4848

49+
Additional scheduling controls:
50+
51+
- **Immediate/Delayed First Run**: Run the first callback immediately (`immediate: true`) or after a delay (`delay: '10s'`).
52+
- **Overrun Protection**: Skip new starts if a previous run is still executing (`overrunProtection: true`).
53+
4954
#### Cron Job Management
5055

5156
Cronbake provides a simple and intuitive interface for managing cron jobs. You can easily add, remove, start, stop, pause, resume, and destroy cron jobs using the `Baker` class.
@@ -77,7 +82,7 @@ Track detailed execution history and performance metrics including:
7782

7883
Cronbake supports job persistence across application restarts:
7984

80-
- Save job state to file system
85+
- Save job state to file system or Redis (pluggable providers)
8186
- Automatic restoration on startup
8287
- Configurable persistence options
8388

@@ -156,6 +161,27 @@ const everyMinuteJob = baker.add({
156161
baker.bakeAll();
157162
```
158163

164+
#### Immediate/Delayed First Run and Overrun Protection
165+
166+
```typescript
167+
import Baker from 'cronbake';
168+
169+
const baker = Baker.create();
170+
171+
baker.add({
172+
name: 'fast-job',
173+
cron: '@every_10_seconds',
174+
immediate: true, // run the first time right away
175+
delay: '2s', // but wait 2 seconds before that first run
176+
overrunProtection: true, // skip overlaps if a previous run still executes
177+
callback: async () => {
178+
// Do work
179+
},
180+
});
181+
182+
baker.bakeAll();
183+
```
184+
159185
#### Advanced Configuration
160186

161187
You can configure the Baker with advanced options:
@@ -254,6 +280,35 @@ await baker.saveState();
254280
await baker.restoreState();
255281
```
256282

283+
#### Persistence Providers (File and Redis)
284+
285+
Cronbake uses pluggable providers for persistence. A file provider is included by default. A Redis provider is available; you inject your Redis client.
286+
287+
```typescript
288+
import { Baker, FilePersistenceProvider, RedisPersistenceProvider } from 'cronbake';
289+
290+
// File-based
291+
const bakerFile = Baker.create({
292+
persistence: {
293+
enabled: true,
294+
strategy: 'file',
295+
provider: new FilePersistenceProvider('./cronbake-state.json'),
296+
autoRestore: true,
297+
},
298+
});
299+
300+
// Redis-based (provide your own client implementing get/set)
301+
const redisProvider = new RedisPersistenceProvider({ client: redisClient, key: 'cronbake:state' });
302+
const bakerRedis = Baker.create({
303+
persistence: {
304+
enabled: true,
305+
strategy: 'redis',
306+
provider: redisProvider,
307+
autoRestore: true,
308+
},
309+
});
310+
```
311+
257312
### Baker Methods
258313

259314
| Method | Description |
@@ -309,6 +364,8 @@ const job = Cron.create({
309364
console.error('Job failed:', error.message);
310365
},
311366
priority: 10,
367+
immediate: false,
368+
overrunProtection: true,
312369
});
313370

314371
// Start the cron job
@@ -326,8 +383,11 @@ const nextExecution = job.nextExecution();
326383
// Get metrics and history
327384
const metrics = job.getMetrics();
328385
const history = job.getHistory();
386+
// Metrics include skippedExecutions when overrun protection skips overlaps
329387
```
330388

389+
390+
331391
Cronbake also provides utility functions for parsing cron expressions, getting the next or previous execution times, and validating cron expressions.
332392

333393
```typescript
@@ -370,4 +430,4 @@ Contributions are welcome! If you find any issues or have suggestions for improv
370430

371431
## License
372432

373-
Cronbake is released under the [MIT License](./LICENSE).
433+
Cronbake is released under the [MIT License](./LICENSE).

index.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Baker from '@/lib';
1+
import Baker from "./lib";
22

33
export {
44
type CronOptions,
@@ -17,7 +17,17 @@ export {
1717
type OnDayStrType,
1818
type day,
1919
type unit,
20-
} from '@/lib';
20+
} from "./lib";
2121

22-
export { Cron, Baker, CronParser } from '@/lib';
22+
export { Cron, Baker, CronParser } from "./lib";
2323
export default Baker;
24+
25+
export {
26+
FilePersistenceProvider,
27+
RedisPersistenceProvider,
28+
PersistedJob,
29+
PersistedState,
30+
PersistenceProvider,
31+
RedisLikeClient,
32+
RedisProviderOptions,
33+
} from "./lib/persistence";

lib/baker.ts

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import Cron from '@/lib/cron';
2-
import {
3-
CronOptions,
4-
IBaker,
5-
IBakerOptions,
6-
ICron,
7-
Status,
8-
ExecutionHistory,
1+
import Cron from './cron';
2+
import {
3+
CronOptions,
4+
IBaker,
5+
IBakerOptions,
6+
ICron,
7+
Status,
8+
ExecutionHistory,
99
JobMetrics,
1010
SchedulerConfig,
11-
PersistenceOptions
12-
} from '@/lib/types';
13-
import * as fs from 'fs';
14-
import * as path from 'path';
11+
PersistenceOptions,
12+
} from './types';
13+
import { FilePersistenceProvider } from './persistence/file';
14+
import type { PersistenceProvider } from './persistence/types';
1515

1616
/**
1717
* A class that implements the `IBaker` interface and provides methods to manage cron jobs.
@@ -20,6 +20,7 @@ class Baker implements IBaker {
2020
private crons: Map<string, ICron> = new Map();
2121
private config: SchedulerConfig;
2222
private persistence: PersistenceOptions;
23+
private persistenceProvider?: PersistenceProvider;
2324
private enableMetrics: boolean;
2425
private onError?: (error: Error, jobName: string) => void;
2526

@@ -34,7 +35,20 @@ class Baker implements IBaker {
3435
enabled: options.persistence?.enabled ?? false,
3536
filePath: options.persistence?.filePath ?? './cronbake-state.json',
3637
autoRestore: options.persistence?.autoRestore ?? true,
38+
strategy: options.persistence?.strategy ?? 'file',
39+
provider: options.persistence?.provider,
40+
redis: options.persistence?.redis,
3741
};
42+
43+
if (this.persistence.enabled) {
44+
if (this.persistence.provider) {
45+
this.persistenceProvider = this.persistence.provider;
46+
} else if (this.persistence.strategy === 'file') {
47+
this.persistenceProvider = new FilePersistenceProvider(this.persistence.filePath!);
48+
} else if (this.persistence.strategy === 'redis') {
49+
throw new Error('Redis persistence selected but no provider supplied. Pass persistence.provider or use FilePersistenceProvider.');
50+
}
51+
}
3852

3953
this.enableMetrics = options.enableMetrics ?? true;
4054
this.onError = options.onError;
@@ -225,58 +239,40 @@ class Baker implements IBaker {
225239
}
226240

227241
async saveState(): Promise<void> {
228-
if (!this.persistence.enabled) return;
229-
242+
if (!this.persistence.enabled || !this.persistenceProvider) return;
230243
try {
231244
const state = {
245+
version: 1,
232246
timestamp: new Date().toISOString(),
233247
jobs: Array.from(this.crons.entries()).map(([name, cron]) => ({
234248
name,
235-
cron: cron.cron,
249+
cron: String(cron.cron),
236250
status: cron.getStatus(),
237251
priority: cron.priority,
238252
metrics: this.enableMetrics ? cron.getMetrics() : undefined,
239253
history: this.enableMetrics ? cron.getHistory() : undefined,
240254
})),
241255
config: this.config,
242-
};
243-
244-
const filePath = path.resolve(this.persistence.filePath!);
245-
const dir = path.dirname(filePath);
246-
247-
if (!fs.existsSync(dir)) {
248-
fs.mkdirSync(dir, { recursive: true });
249-
}
250-
251-
await fs.promises.writeFile(filePath, JSON.stringify(state, null, 2), 'utf8');
256+
} as const;
257+
await this.persistenceProvider.save(state);
252258
} catch (error) {
253259
throw new Error(`Failed to save state: ${error}`);
254260
}
255261
}
256262

257263
async restoreState(): Promise<void> {
258-
if (!this.persistence.enabled) return;
259-
264+
if (!this.persistence.enabled || !this.persistenceProvider) return;
260265
try {
261-
const filePath = path.resolve(this.persistence.filePath!);
262-
263-
if (!fs.existsSync(filePath)) {
264-
return;
265-
}
266-
267-
const data = await fs.promises.readFile(filePath, 'utf8');
268-
const state = JSON.parse(data);
269-
266+
const state = await this.persistenceProvider.load();
267+
if (!state) return;
270268
if (!state.jobs || !Array.isArray(state.jobs)) {
271-
throw new Error('Invalid state file format');
269+
throw new Error('Invalid state format');
272270
}
273-
274271
for (const jobData of state.jobs) {
275272
if (!jobData.name || !jobData.cron) {
276273
console.warn('Skipping invalid job data:', jobData);
277274
continue;
278275
}
279-
280276
try {
281277
const options: CronOptions = {
282278
name: jobData.name,
@@ -287,13 +283,11 @@ class Baker implements IBaker {
287283
priority: jobData.priority,
288284
start: jobData.status === 'running',
289285
};
290-
291286
this.add(options);
292287
} catch (error) {
293288
console.warn(`Failed to restore job '${jobData.name}':`, error);
294289
}
295290
}
296-
297291
console.log(`Restored ${state.jobs.length} cron jobs from persistence`);
298292
} catch (error) {
299293
throw new Error(`Failed to restore state: ${error}`);
@@ -308,4 +302,4 @@ class Baker implements IBaker {
308302
}
309303
}
310304

311-
export default Baker;
305+
export default Baker;

0 commit comments

Comments
 (0)