Skip to content

Commit b4393e5

Browse files
committed
refactor: standardize import statements and enhance README with immediate/delayed first run and overrun protection examples
1 parent 9cfc0dd commit b4393e5

4 files changed

Lines changed: 103 additions & 13 deletions

File tree

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/persistence.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ describe('Persistence Providers', () => {
1818
});
1919
await sleep(10);
2020
expect(baker2.getJobNames()).toContain('file-job');
21-
baker2.destroyAll();
22-
baker1.destroyAll();
21+
// Stop timers and persist final state before cleanup
22+
baker2.stopAll();
23+
baker1.stopAll();
24+
await baker2.saveState();
2325
cleanupFile(filePath);
2426
});
2527

lib/persistence/file.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,44 @@ import * as path from "path";
33
import { PersistedState, PersistenceProvider } from "./types";
44

55
class FilePersistenceProvider implements PersistenceProvider {
6+
private queue: Promise<void> = Promise.resolve();
7+
68
constructor(private filePath: string) {}
79

810
async save(state: PersistedState): Promise<void> {
911
const filePath = path.resolve(this.filePath);
1012
const dir = path.dirname(filePath);
13+
const tmp = `${filePath}.tmp`;
14+
1115
if (!fs.existsSync(dir)) {
1216
fs.mkdirSync(dir, { recursive: true });
1317
}
14-
await fs.promises.writeFile(filePath, JSON.stringify(state, null, 2), "utf8");
18+
19+
const write = async () => {
20+
const data = JSON.stringify(state, null, 2);
21+
// Atomic write: write to temp, then rename over destination
22+
await fs.promises.writeFile(tmp, data, "utf8");
23+
await fs.promises.rename(tmp, filePath);
24+
};
25+
26+
// Serialize writes to avoid interleaving
27+
this.queue = this.queue.then(write, write);
28+
return this.queue;
1529
}
1630

1731
async load(): Promise<PersistedState | null> {
1832
const filePath = path.resolve(this.filePath);
1933
if (!fs.existsSync(filePath)) return null;
20-
const data = await fs.promises.readFile(filePath, "utf8");
21-
const state = JSON.parse(data);
22-
if (!state || typeof state !== "object") return null;
23-
return state as PersistedState;
34+
try {
35+
const data = await fs.promises.readFile(filePath, "utf8");
36+
const state = JSON.parse(data);
37+
if (!state || typeof state !== "object") return null;
38+
return state as PersistedState;
39+
} catch {
40+
// If file is mid-write or corrupted, return null so caller can retry later
41+
return null;
42+
}
2443
}
2544
}
2645

2746
export { FilePersistenceProvider };
28-

0 commit comments

Comments
 (0)