anbaric 1.23.2 → 1.24.0

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 CHANGED
@@ -266,6 +266,37 @@ re-evaluates its transitions, so the input drives it on and the wait clears once
266
266
  it moves to another state. Actions placed after an `Await` in the same state do
267
267
  not run when the job resumes.
268
268
 
269
+ Some work is started by the clock rather than by a person or an event. The
270
+ **`JobRunScheduler`** starts jobs on a timetable — give it a machine and a
271
+ `Schedule` and it does the rest:
272
+
273
+ ```ts
274
+ import {JobRunScheduler, Schedule} from "anbaric";
275
+
276
+ // 02:00 every day
277
+ JobRunScheduler.instance().schedule(
278
+ reconciliation,
279
+ new Schedule(Schedule.everyDay(), [{ hours: 2, minutes: 0 }]),
280
+ );
281
+
282
+ // weekdays at 09:00 and 17:30
283
+ JobRunScheduler.instance().schedule(
284
+ digest,
285
+ new Schedule(Schedule.daysOfWeek([1, 2, 3, 4, 5]), [{ hours: 9, minutes: 0 }, { hours: 17, minutes: 30 }]),
286
+ );
287
+ ```
288
+
289
+ The day test is a predicate, so anything expressible in code is a schedule.
290
+ Runs are **planned ahead and stored** rather than discovered at the last moment,
291
+ so a restart keeps the timetable and a scheduler that was down starts the runs
292
+ it missed instead of skipping them; each job carries the run it belongs to in
293
+ `scheduledFor`. Machines due at the same moment are spread by a small random
294
+ offset, applied when the run is planned so the stored time is the real one.
295
+ Locally the plan is in memory; deployed it lives in the platform database, where
296
+ claiming a due run is atomic — so several instances of your app can run the
297
+ scheduler and each run still starts exactly once. See
298
+ [Scheduled runs](anbaric/docs/features/scheduled-runs.md).
299
+
269
300
  Everything is pluggable through env-driven factories: locally (no env vars)
270
301
  you get in-memory persistence and queueing; deployed, the same factories talk
271
302
  to the platform automatically. The same applies to `JsonStoreFactory`
package/docs/README.md CHANGED
@@ -21,6 +21,7 @@ What the framework gives you, one capability at a time.
21
21
  - [State machines](features/state-machines.md) — defining and running a workflow
22
22
  - [Actions and actors](features/actions-and-actors.md) — who does the work, and how
23
23
  - [Awaiting input](features/awaiting-input.md) — pausing a job for a human or external system
24
+ - [Scheduled runs](features/scheduled-runs.md) — starting jobs on a timetable
24
25
  - [AI agents](features/ai-agents.md) — letting a model drive a state
25
26
  - [Documents and secrets](features/documents-and-secrets.md) — the JSON and secret stores
26
27
  - [The SQL store](features/sql-store.md) — a relational database for structured data
@@ -29,6 +29,7 @@ defaulting to a local implementation otherwise).
29
29
  | --- | --- | --- |
30
30
  | `ANBARIC_JOB_PERSISTENCE_TYPE` | where jobs are stored | in-memory |
31
31
  | `ANBARIC_QUEUE_TYPE` | the job queue | in-memory |
32
+ | `ANBARIC_JOB_RUN_SCHEDULE_PERSISTENCE_TYPE` | where planned [scheduled runs](../features/scheduled-runs.md) are kept | in-memory |
32
33
  | `ANBARIC_AUDITOR_TYPE` | the audit sink (`cloud` → platform) | console |
33
34
  | `ANBARIC_JSON_STORE_TYPE` | the JSON document store | in-memory |
34
35
  | `ANBARIC_SECRET_STORE_TYPE` | the secret store | in-memory (encrypted) |
@@ -265,6 +265,7 @@ have a definition, or writes to it are rejected.
265
265
  ```ts
266
266
  id : string
267
267
  required : boolean = false
268
+ example? : any // a realistic value, for tooling
268
269
  validation : (value : any) => boolean // default: () => true
269
270
 
270
271
  constructor(id : string)
@@ -275,11 +276,65 @@ Configure by mutation:
275
276
  ```ts
276
277
  const email = new PropertyDefinition("email");
277
278
  email.required = true;
279
+ email.example = "someone@example.com";
278
280
  email.validation = (value) => typeof value === "string" && value.includes("@");
279
281
  ```
280
282
 
281
283
  Pass the definitions as the `StateMachine`'s fourth argument.
282
284
 
285
+ `example` is carried into the machine's published definition, so tools that
286
+ start a job — the admin console's **Start** dialog, generated documentation —
287
+ can offer a realistic value instead of an empty box. It is never validated and
288
+ never becomes a default; it is purely descriptive.
289
+
290
+ ---
291
+
292
+ ## `Schedule`
293
+
294
+ Which days a machine runs on, and at what times on those days. See
295
+ [Scheduled runs](../features/scheduled-runs.md).
296
+
297
+ ```ts
298
+ constructor(dayTest : (date : Date) => boolean, times : Array<{ hours : number, minutes : number }>)
299
+
300
+ getRuns(from : Date, to : Date) : Array<Date> // exclusive of `from`, inclusive of `to`
301
+
302
+ static everyDay() : (date : Date) => boolean
303
+ static daysOfWeek(days : Array<number>) // 0 = Sunday … 6 = Saturday
304
+ static daysOfMonth(days : Array<number>) // calendar dates
305
+ ```
306
+
307
+ The day test is an ordinary predicate, so any rule you can write in code — the
308
+ last working day of a quarter, every other Tuesday — is a schedule.
309
+
310
+ ---
311
+
312
+ ## `JobRunScheduler`
313
+
314
+ Starts jobs on a timetable. Runs are planned ahead and stored, so the plan
315
+ survives a restart and missed runs are caught up rather than skipped.
316
+
317
+ ```ts
318
+ static instance() : JobRunScheduler
319
+
320
+ schedule(machine : StateMachine, at : Schedule,
321
+ lookaheadMs : number = 86_400_000,
322
+ randomRunOffsetMs : [number, number] = [0, 120_000]) : void
323
+
324
+ tick(now? : Date) : Promise<void> // plan and start everything owed; mostly for tests
325
+ cleanUp() : Promise<void>
326
+ ```
327
+
328
+ `lookaheadMs` is how far ahead runs are planned; `randomRunOffsetMs` spreads
329
+ machines that would otherwise all start on the same second, and is applied when
330
+ the run is planned so the stored time is the time it runs. Pass `[0, 0]` to
331
+ start exactly on the minute. Each scheduled job carries its run in the
332
+ `scheduledFor` property.
333
+
334
+ Storage comes from `JobRunSchedulePersistenceFactory` — in memory locally, the
335
+ platform database when deployed, where claiming a due run is atomic so several
336
+ instances can schedule the same machines safely.
337
+
283
338
  ---
284
339
 
285
340
  ## `Actor`
@@ -0,0 +1,106 @@
1
+ # Scheduled runs
2
+
3
+ Some work isn't started by a person or an event — it just needs to happen at a
4
+ certain time. A nightly reconciliation, a weekly digest, an invoice run on the
5
+ 1st of the month. The **job run scheduler** starts jobs on a timetable, so a
6
+ machine that should run at 09:00 gets a job at 09:00 without anything asking it
7
+ to.
8
+
9
+ ## Scheduling a machine
10
+
11
+ Give the scheduler a machine and a `Schedule`:
12
+
13
+ ```ts
14
+ import {JobRunScheduler, Schedule, StateMachine} from "anbaric";
15
+
16
+ const reconciliation = new StateMachine("reconciliation", [/* … */]);
17
+
18
+ JobRunScheduler.instance().schedule(
19
+ reconciliation,
20
+ new Schedule(Schedule.everyDay(), [{ hours: 2, minutes: 0 }]),
21
+ );
22
+ ```
23
+
24
+ That's it. From then on a job is started on `reconciliation` at 02:00 every day,
25
+ in its start state, and progresses like any other job.
26
+
27
+ ## Describing a timetable
28
+
29
+ A `Schedule` is **which days** and **what times on those days**, kept separate so
30
+ one shape covers everything:
31
+
32
+ ```ts
33
+ new Schedule(Schedule.everyDay(), [{ hours: 9, minutes: 0 }]); // 09:00 daily
34
+ new Schedule(Schedule.daysOfWeek([1, 2, 3, 4, 5]), [{ hours: 9, minutes: 0 },
35
+ { hours: 17, minutes: 30 }]); // weekdays, twice
36
+ new Schedule(Schedule.daysOfMonth([1]), [{ hours: 0, minutes: 0 }]); // the 1st, midnight
37
+ ```
38
+
39
+ `daysOfWeek` takes 0 (Sunday) to 6 (Saturday); `daysOfMonth` takes calendar
40
+ dates. The day test is just a predicate, so anything you can express in code —
41
+ last working day of the quarter, every other Tuesday — is a schedule:
42
+
43
+ ```ts
44
+ const quarterEnd = (date : Date) => [2, 5, 8, 11].includes(date.getMonth()) && date.getDate() === 28;
45
+ new Schedule(quarterEnd, [{ hours: 23, minutes: 0 }]);
46
+ ```
47
+
48
+ ## Runs are planned before they happen
49
+
50
+ The scheduler doesn't wake up and ask "is anything due?". It **plans ahead** —
51
+ by default a day at a time — writing each future run to storage, then starts
52
+ runs as they come due. Two things follow from that, both deliberate:
53
+
54
+ - **A restart doesn't lose the timetable.** The plan is already stored, so the
55
+ scheduler picks up where it left off.
56
+ - **Downtime doesn't silently skip runs.** A scheduler that was down for two
57
+ days starts the runs it missed as soon as it comes back, rather than
58
+ pretending they never existed. If you don't want that catch-up for a
59
+ particular machine, make its action check `scheduledFor` and return early.
60
+
61
+ Every scheduled job carries the run it belongs to:
62
+
63
+ ```ts
64
+ processRun.run = async (job) => {
65
+ const scheduledFor = new Date(job.properties.get("scheduledFor"));
66
+ // …reconcile everything up to scheduledFor
67
+ };
68
+ ```
69
+
70
+ ## Spreading the load
71
+
72
+ Machines scheduled at the same time would otherwise all start on the same
73
+ second. Each machine gets a small random offset — up to two minutes by default —
74
+ applied **when the run is planned**, so the stored time is the time it really
75
+ runs. Control it per machine:
76
+
77
+ ```ts
78
+ JobRunScheduler.instance().schedule(
79
+ digest,
80
+ new Schedule(Schedule.everyDay(), [{ hours: 6, minutes: 0 }]),
81
+ 1000 * 60 * 60 * 24, // plan a day ahead
82
+ [0, 1000 * 60 * 15], // start somewhere in the 15 minutes after 06:00
83
+ );
84
+ ```
85
+
86
+ Pass `[0, 0]` for a machine that must start exactly on the minute.
87
+
88
+ ## Local and deployed
89
+
90
+ Like every other service, the scheduler resolves its storage through a factory.
91
+ Locally the plan is held in memory, so scheduling works on your laptop with no
92
+ setup — though a plan in memory dies with the process. Deployed, the platform
93
+ sets `ANBARIC_JOB_RUN_SCHEDULE_PERSISTENCE_TYPE=cloud` and the plan lives in the
94
+ platform database, surviving restarts and redeploys. **Your code is identical
95
+ either way** — see [Environment and factories](../api/environment.md).
96
+
97
+ Because the plan is shared, more than one instance of your app can run the
98
+ scheduler safely: claiming a due run is atomic, so each run is started exactly
99
+ once no matter how many instances are up.
100
+
101
+ ## Scheduling and queueing are different things
102
+
103
+ A scheduled run is a **timetable entry**: "this machine should get a job at
104
+ 09:00". `Queue.schedule` delays an **existing job**: "look at this job again in
105
+ ten minutes". Reach for the scheduler when the trigger is the clock, and for a
106
+ delayed enqueue when a job needs to wait before continuing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anbaric",
3
- "version": "1.23.2",
3
+ "version": "1.24.0",
4
4
  "description": "Everything needed to write an Anbaric app: state machines, jobs, document and secret stores, local in-memory implementations and the Anbaric Cloud clients",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,9 +24,9 @@
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "dependencies": {
27
- "anbaric-impl-cloud": "^1.23.2",
28
- "anbaric-data-store": "^1.23.2",
29
- "anbaric-state-machine": "^1.23.2",
30
- "anbaric-tsapi": "^1.23.2"
27
+ "anbaric-impl-cloud": "^1.24.0",
28
+ "anbaric-data-store": "^1.24.0",
29
+ "anbaric-state-machine": "^1.24.0",
30
+ "anbaric-tsapi": "^1.24.0"
31
31
  }
32
32
  }