@stonyx/cron 0.2.1-alpha.50 → 0.2.1-alpha.51
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/README.md +45 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,51 @@ When a job is executed, its next trigger time is updated, and it is re-inserted
|
|
|
37
37
|
|
|
38
38
|
> `MinHeap` is also exported as a public subpath (`@stonyx/cron/min-heap`) and can be imported directly for advanced usage.
|
|
39
39
|
|
|
40
|
+
## CronService
|
|
41
|
+
|
|
42
|
+
The default export above is `Cron`: a fire-and-forget interval registry. `@stonyx/cron/service` is a separate, heavier class for jobs that need CRUD, persistence, a run log and error backoff. It is not a drop-in replacement and the two do not share a scheduler.
|
|
43
|
+
|
|
44
|
+
The two classes agree on the guarantee — the same job is never run concurrently with itself, and different jobs may overlap — but not on the mechanism or on what you can observe. `Cron` invokes callbacks fire-and-forget and reports a skipped run only through the `config.cron`-gated log. `CronService` **awaits** `onJobDue`, its return value shapes `status`/`error`/`summary`, and a refused run comes back to the caller as a value that no log setting can suppress — it is not logged.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import CronService from '@stonyx/cron/service';
|
|
48
|
+
|
|
49
|
+
const service = new CronService();
|
|
50
|
+
service.onJobDue = async (job) => ({ status: 'ok', summary: 'done' });
|
|
51
|
+
|
|
52
|
+
await service.start();
|
|
53
|
+
const job = await service.add({ name: 'Nightly', schedule: { kind: 'every', everyMs: 86_400_000 }, payload: { kind: 'agentTurn', message: 'go' } });
|
|
54
|
+
|
|
55
|
+
const result = await service.run(job.id, 'force');
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### The `run()` contract
|
|
59
|
+
|
|
60
|
+
`run(id, mode)` resolves with an `ExecuteResult`. It **never** invokes the callback twice for one job, and it will refuse rather than queue:
|
|
61
|
+
|
|
62
|
+
| `status` | `reason` | Meaning |
|
|
63
|
+
| :---------- | :------------------ | :----------------------------------------------------------------------------- |
|
|
64
|
+
| `'ok'` | — | The callback resolved. `summary` and `durationMs` are set. |
|
|
65
|
+
| `'error'` | — | The callback threw or rejected. `error` carries the message; backoff is applied. |
|
|
66
|
+
| `'skipped'` | `'not due'` | `mode` was `'due'` and the job's next run time has not arrived. Use `'force'` to run anyway. |
|
|
67
|
+
| `'skipped'` | `'already running'` | A previous invocation of **this** job has not settled. The call is refused, not queued, and nothing is logged. |
|
|
68
|
+
| `'skipped'` | `'removed'` | The job was removed between the lookup and the claim. The callback did not fire. |
|
|
69
|
+
|
|
70
|
+
`run()` throws (rather than returning a result) when `id` is not a registered job: `Error: Job not found: <id>`.
|
|
71
|
+
|
|
72
|
+
**Concurrency.** A job is bounded to one in-flight invocation on every path — manual `run()` and the timer both claim it first. **Different** jobs are not bounded: the callback is deliberately invoked outside the internal lock, so N concurrent `run()` calls on N distinct jobs produce N concurrent callbacks. The scheduler itself never generates that fan-out (its timer path invokes a due batch sequentially); only a caller can. If you drive `run()` from a request handler, bound it on your side. Taking the callback out of the lock is what stops a callback that never settles from blocking `add`/`update`/`remove`; restoring the bound by putting it back would restore that deadlock.
|
|
73
|
+
|
|
74
|
+
### Breaking changes in this line
|
|
75
|
+
|
|
76
|
+
Four consumer-visible changes landed with the phase split (#34). All are measured against the emitted `dist/service.d.ts`:
|
|
77
|
+
|
|
78
|
+
1. **`ExecuteResult.reason` narrowed** from `string` to `'not due' | 'already running' | 'removed'`, and gained the `'removed'` member. Comparing it against a literal outside the union, or `switch`ing on one, is now a compile error (`TS2367` / `TS2678`). Assigning it into `string | undefined` and spreading it are unaffected. The type is exported as `SkipReason`.
|
|
79
|
+
2. **`CronService` is nominally typed.** It carries ECMAScript hard-private members, so the declarations emit `#private;` and a structurally hand-built test double no longer assigns to `CronService` (`TS2741: Property '#private' is missing`). The break is one-directional: `class X extends CronService` still compiles, and assigning a real `CronService` to your own hand-written interface still compiles. **Migration:** declare your own interface and depend on that instead of a `CronService`-typed mock.
|
|
80
|
+
3. **`claimJob`, `settleJob` and `executeClaimed` are not published.** They were never a supported API; a claim taken without its matching settle strands the job permanently.
|
|
81
|
+
4. **`run()` no longer serializes across jobs** — see the concurrency note above.
|
|
82
|
+
|
|
83
|
+
`SkipReason`, `ExecuteResult`, `JobDueResult`, `ServiceStatus`, `ListOptions` and `OnJobDueCallback` are all exported from `@stonyx/cron/service`, so an exhaustive handler over `reason` is expressible.
|
|
84
|
+
|
|
40
85
|
## Configuration
|
|
41
86
|
|
|
42
87
|
Optionally, logging and debugging can be enabled through `config.cron`:
|