@smthrs/triggers 0.0.0-stage → 1.0.0-rc.4
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 +219 -0
- package/LICENSE +21 -0
- package/README.md +109 -2
- package/dist/cjs/CatchUp.d.ts +25 -0
- package/dist/cjs/CatchUp.d.ts.map +1 -0
- package/dist/cjs/CatchUp.js +72 -0
- package/dist/cjs/CatchUp.js.map +7 -0
- package/dist/cjs/Channel.d.ts +108 -0
- package/dist/cjs/Channel.d.ts.map +1 -0
- package/dist/cjs/Channel.js +25 -0
- package/dist/cjs/Channel.js.map +7 -0
- package/dist/cjs/ClaimDecision.d.ts +183 -0
- package/dist/cjs/ClaimDecision.d.ts.map +1 -0
- package/dist/cjs/ClaimDecision.js +144 -0
- package/dist/cjs/ClaimDecision.js.map +7 -0
- package/dist/cjs/Cron.d.ts +91 -0
- package/dist/cjs/Cron.d.ts.map +1 -0
- package/dist/cjs/Cron.js +108 -0
- package/dist/cjs/Cron.js.map +7 -0
- package/dist/cjs/DispatchReader.d.ts +77 -0
- package/dist/cjs/DispatchReader.d.ts.map +1 -0
- package/dist/cjs/DispatchReader.js +121 -0
- package/dist/cjs/DispatchReader.js.map +7 -0
- package/dist/cjs/Overlap.d.ts +40 -0
- package/dist/cjs/Overlap.d.ts.map +1 -0
- package/dist/cjs/Overlap.js +37 -0
- package/dist/cjs/Overlap.js.map +7 -0
- package/dist/cjs/Schedule.d.ts +96 -0
- package/dist/cjs/Schedule.d.ts.map +1 -0
- package/dist/cjs/Schedule.js +61 -0
- package/dist/cjs/Schedule.js.map +7 -0
- package/dist/cjs/Scheduler.d.ts +248 -0
- package/dist/cjs/Scheduler.d.ts.map +1 -0
- package/dist/cjs/Scheduler.js +647 -0
- package/dist/cjs/Scheduler.js.map +7 -0
- package/dist/cjs/SqlTriggerStore.d.ts +29 -0
- package/dist/cjs/SqlTriggerStore.d.ts.map +1 -0
- package/dist/cjs/SqlTriggerStore.js +483 -0
- package/dist/cjs/SqlTriggerStore.js.map +7 -0
- package/dist/cjs/Trigger.d.ts +81 -0
- package/dist/cjs/Trigger.d.ts.map +1 -0
- package/dist/cjs/Trigger.js +54 -0
- package/dist/cjs/Trigger.js.map +7 -0
- package/dist/cjs/TriggerError.d.ts +58 -0
- package/dist/cjs/TriggerError.d.ts.map +1 -0
- package/dist/cjs/TriggerError.js +83 -0
- package/dist/cjs/TriggerError.js.map +7 -0
- package/dist/cjs/TriggerStore.d.ts +411 -0
- package/dist/cjs/TriggerStore.d.ts.map +1 -0
- package/dist/cjs/TriggerStore.js +127 -0
- package/dist/cjs/TriggerStore.js.map +7 -0
- package/dist/cjs/Webhook.d.ts +118 -0
- package/dist/cjs/Webhook.d.ts.map +1 -0
- package/dist/cjs/Webhook.js +153 -0
- package/dist/cjs/Webhook.js.map +7 -0
- package/dist/cjs/index.d.ts +30 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +57 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/ActiveRuns.d.ts +51 -0
- package/dist/cjs/internal/ActiveRuns.d.ts.map +1 -0
- package/dist/cjs/internal/ActiveRuns.js +56 -0
- package/dist/cjs/internal/ActiveRuns.js.map +7 -0
- package/dist/cjs/internal/DueOccurrences.d.ts +32 -0
- package/dist/cjs/internal/DueOccurrences.d.ts.map +1 -0
- package/dist/cjs/internal/DueOccurrences.js +80 -0
- package/dist/cjs/internal/DueOccurrences.js.map +7 -0
- package/dist/cjs/internal/WallClock.d.ts +59 -0
- package/dist/cjs/internal/WallClock.d.ts.map +1 -0
- package/dist/cjs/internal/WallClock.js +132 -0
- package/dist/cjs/internal/WallClock.js.map +7 -0
- package/dist/cjs/migrations/0001_triggers.d.ts +15 -0
- package/dist/cjs/migrations/0001_triggers.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_triggers.js +64 -0
- package/dist/cjs/migrations/0001_triggers.js.map +7 -0
- package/dist/cjs/migrations/0002_reservation_lease.d.ts +15 -0
- package/dist/cjs/migrations/0002_reservation_lease.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_reservation_lease.js +41 -0
- package/dist/cjs/migrations/0002_reservation_lease.js.map +7 -0
- package/dist/cjs/migrations/0003_heartbeat.d.ts +16 -0
- package/dist/cjs/migrations/0003_heartbeat.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_heartbeat.js +44 -0
- package/dist/cjs/migrations/0003_heartbeat.js.map +7 -0
- package/dist/cjs/migrations/0004_fire_run_index.d.ts +17 -0
- package/dist/cjs/migrations/0004_fire_run_index.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_fire_run_index.js +40 -0
- package/dist/cjs/migrations/0004_fire_run_index.js.map +7 -0
- package/dist/cjs/migrations/index.d.ts +33 -0
- package/dist/cjs/migrations/index.d.ts.map +1 -0
- package/dist/cjs/migrations/index.js +50 -0
- package/dist/cjs/migrations/index.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestTriggers.d.ts +12 -0
- package/dist/cjs/test/TestTriggers.d.ts.map +1 -0
- package/dist/cjs/test/TestTriggers.js +421 -0
- package/dist/cjs/test/TestTriggers.js.map +7 -0
- package/dist/esm/CatchUp.d.ts +25 -0
- package/dist/esm/CatchUp.d.ts.map +1 -0
- package/dist/esm/CatchUp.js +52 -0
- package/dist/esm/CatchUp.js.map +1 -0
- package/dist/esm/Channel.d.ts +108 -0
- package/dist/esm/Channel.d.ts.map +1 -0
- package/dist/esm/Channel.js +19 -0
- package/dist/esm/Channel.js.map +1 -0
- package/dist/esm/ClaimDecision.d.ts +183 -0
- package/dist/esm/ClaimDecision.d.ts.map +1 -0
- package/dist/esm/ClaimDecision.js +172 -0
- package/dist/esm/ClaimDecision.js.map +1 -0
- package/dist/esm/Cron.d.ts +91 -0
- package/dist/esm/Cron.d.ts.map +1 -0
- package/dist/esm/Cron.js +132 -0
- package/dist/esm/Cron.js.map +1 -0
- package/dist/esm/DispatchReader.d.ts +77 -0
- package/dist/esm/DispatchReader.d.ts.map +1 -0
- package/dist/esm/DispatchReader.js +152 -0
- package/dist/esm/DispatchReader.js.map +1 -0
- package/dist/esm/Overlap.d.ts +40 -0
- package/dist/esm/Overlap.d.ts.map +1 -0
- package/dist/esm/Overlap.js +31 -0
- package/dist/esm/Overlap.js.map +1 -0
- package/dist/esm/Schedule.d.ts +96 -0
- package/dist/esm/Schedule.d.ts.map +1 -0
- package/dist/esm/Schedule.js +73 -0
- package/dist/esm/Schedule.js.map +1 -0
- package/dist/esm/Scheduler.d.ts +248 -0
- package/dist/esm/Scheduler.d.ts.map +1 -0
- package/dist/esm/Scheduler.js +734 -0
- package/dist/esm/Scheduler.js.map +1 -0
- package/dist/esm/SqlTriggerStore.d.ts +29 -0
- package/dist/esm/SqlTriggerStore.d.ts.map +1 -0
- package/dist/esm/SqlTriggerStore.js +483 -0
- package/dist/esm/SqlTriggerStore.js.map +1 -0
- package/dist/esm/Trigger.d.ts +81 -0
- package/dist/esm/Trigger.d.ts.map +1 -0
- package/dist/esm/Trigger.js +56 -0
- package/dist/esm/Trigger.js.map +1 -0
- package/dist/esm/TriggerError.d.ts +58 -0
- package/dist/esm/TriggerError.d.ts.map +1 -0
- package/dist/esm/TriggerError.js +110 -0
- package/dist/esm/TriggerError.js.map +1 -0
- package/dist/esm/TriggerStore.d.ts +411 -0
- package/dist/esm/TriggerStore.d.ts.map +1 -0
- package/dist/esm/TriggerStore.js +209 -0
- package/dist/esm/TriggerStore.js.map +1 -0
- package/dist/esm/Webhook.d.ts +118 -0
- package/dist/esm/Webhook.d.ts.map +1 -0
- package/dist/esm/Webhook.js +186 -0
- package/dist/esm/Webhook.js.map +1 -0
- package/dist/esm/index.d.ts +30 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +30 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ActiveRuns.d.ts +51 -0
- package/dist/esm/internal/ActiveRuns.d.ts.map +1 -0
- package/dist/esm/internal/ActiveRuns.js +34 -0
- package/dist/esm/internal/ActiveRuns.js.map +1 -0
- package/dist/esm/internal/DueOccurrences.d.ts +32 -0
- package/dist/esm/internal/DueOccurrences.d.ts.map +1 -0
- package/dist/esm/internal/DueOccurrences.js +55 -0
- package/dist/esm/internal/DueOccurrences.js.map +1 -0
- package/dist/esm/internal/WallClock.d.ts +59 -0
- package/dist/esm/internal/WallClock.d.ts.map +1 -0
- package/dist/esm/internal/WallClock.js +155 -0
- package/dist/esm/internal/WallClock.js.map +1 -0
- package/dist/esm/migrations/0001_triggers.d.ts +15 -0
- package/dist/esm/migrations/0001_triggers.d.ts.map +1 -0
- package/dist/esm/migrations/0001_triggers.js +42 -0
- package/dist/esm/migrations/0001_triggers.js.map +1 -0
- package/dist/esm/migrations/0002_reservation_lease.d.ts +15 -0
- package/dist/esm/migrations/0002_reservation_lease.d.ts.map +1 -0
- package/dist/esm/migrations/0002_reservation_lease.js +19 -0
- package/dist/esm/migrations/0002_reservation_lease.js.map +1 -0
- package/dist/esm/migrations/0003_heartbeat.d.ts +16 -0
- package/dist/esm/migrations/0003_heartbeat.d.ts.map +1 -0
- package/dist/esm/migrations/0003_heartbeat.js +23 -0
- package/dist/esm/migrations/0003_heartbeat.js.map +1 -0
- package/dist/esm/migrations/0004_fire_run_index.d.ts +17 -0
- package/dist/esm/migrations/0004_fire_run_index.d.ts.map +1 -0
- package/dist/esm/migrations/0004_fire_run_index.js +20 -0
- package/dist/esm/migrations/0004_fire_run_index.js.map +1 -0
- package/dist/esm/migrations/index.d.ts +33 -0
- package/dist/esm/migrations/index.d.ts.map +1 -0
- package/dist/esm/migrations/index.js +37 -0
- package/dist/esm/migrations/index.js.map +1 -0
- package/dist/esm/test/TestTriggers.d.ts +12 -0
- package/dist/esm/test/TestTriggers.d.ts.map +1 -0
- package/dist/esm/test/TestTriggers.js +439 -0
- package/dist/esm/test/TestTriggers.js.map +1 -0
- package/docs/README.md +140 -0
- package/docs/api.md +1186 -0
- package/docs/concepts/channels.md +103 -0
- package/docs/concepts/claim-protocol.md +229 -0
- package/docs/concepts/policies.md +124 -0
- package/docs/concepts/schedules.md +134 -0
- package/docs/guides/choose-a-policy.md +113 -0
- package/docs/guides/declare-a-trigger.md +136 -0
- package/docs/guides/ingest-a-webhook.md +164 -0
- package/docs/guides/run-the-scheduler.md +173 -0
- package/docs/guides/testing.md +157 -0
- package/docs/installation.md +81 -0
- package/docs/quickstart.md +163 -0
- package/docs/troubleshooting.md +281 -0
- package/package.json +241 -3
- package/src/CatchUp.ts +73 -0
- package/src/Channel.ts +121 -0
- package/src/ClaimDecision.ts +303 -0
- package/src/Cron.ts +173 -0
- package/src/DispatchReader.ts +179 -0
- package/src/Overlap.ts +53 -0
- package/src/Schedule.ts +112 -0
- package/src/Scheduler.ts +1154 -0
- package/src/SqlTriggerStore.ts +638 -0
- package/src/Trigger.ts +84 -0
- package/src/TriggerError.ts +129 -0
- package/src/TriggerStore.ts +514 -0
- package/src/Webhook.ts +270 -0
- package/src/index.ts +30 -0
- package/src/internal/ActiveRuns.ts +83 -0
- package/src/internal/DueOccurrences.ts +101 -0
- package/src/internal/WallClock.ts +172 -0
- package/src/migrations/0001_triggers.ts +43 -0
- package/src/migrations/0002_reservation_lease.ts +20 -0
- package/src/migrations/0003_heartbeat.ts +24 -0
- package/src/migrations/0004_fire_run_index.ts +21 -0
- package/src/migrations/index.ts +40 -0
- package/src/test/TestTriggers.ts +563 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# @smthrs/triggers
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `TriggerStore.history` reads the fire ledger newest first, filtered by
|
|
8
|
+
trigger, run, or outcome and paged by cursor and limit, so a listing can show
|
|
9
|
+
what a trigger did without joining the runtime's run table.
|
|
10
|
+
- `TriggerStore.inspect` reads the run or reservation and the buffered
|
|
11
|
+
occurrence one trigger holds without expiring anything.
|
|
12
|
+
- `TriggerStore.heartbeat` and `lastHeartbeat`, backed by the
|
|
13
|
+
`0003_heartbeat` migration, record which scheduler host last polled the
|
|
14
|
+
store. `Scheduler.Options.host` names the host; `Scheduler.defaultHost` is
|
|
15
|
+
`"local"`. A heartbeat the store cannot record is logged and the tick goes on.
|
|
16
|
+
- `DispatchReader`, the `@smthrs/control` read port served from a
|
|
17
|
+
`TriggerStore`, so `Control.list` can answer `triggers` and `fires` from the
|
|
18
|
+
store the scheduler writes: last fire, buffered occurrence, active run, the
|
|
19
|
+
next five occurrences, and the newest heartbeat.
|
|
20
|
+
- `TriggerStore.pruneFires({ olderThan })` deletes settled ledger rows older
|
|
21
|
+
than a cutoff and answers how many it removed, keeping the buffered
|
|
22
|
+
occurrence and the row naming the active run. Nothing else in the store
|
|
23
|
+
deletes from `flows_trigger_fires`, so a host that never prunes keeps one row
|
|
24
|
+
per occurrence for as long as the database lives.
|
|
25
|
+
- `Scheduler.Options.fireRetention`, defaulting to thirty days
|
|
26
|
+
(`Scheduler.defaultFireRetention`): a tick prunes settled fires older than
|
|
27
|
+
that at most once an hour. The `0004_fire_run_index` migration indexes the
|
|
28
|
+
ledger by run id.
|
|
29
|
+
- Daylight-saving tests cover `America/Los_Angeles` and `America/New_York`
|
|
30
|
+
across both 2026 transitions: weekly Friday series (including one that
|
|
31
|
+
crosses UTC midnight), a daily time in the spring-forward gap and in the
|
|
32
|
+
fall-back repeat, an hourly series, and host-zone independence, each through
|
|
33
|
+
`Cron` and through the scheduler over the SQL store.
|
|
34
|
+
- The scheduler traces `Scheduler.runOnce`, `Scheduler.processTrigger`, and
|
|
35
|
+
`Scheduler.launch`, and records `smithers.triggers.fires` by outcome plus
|
|
36
|
+
tick and launch duration histograms (`Scheduler.durationBoundaries`).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- `RunnerService.isActive` is replaced by `inspect`, which answers a
|
|
41
|
+
`RunState`. A run that failed, was cancelled, or is unknown to the runner is
|
|
42
|
+
recorded as a `failed` fire naming that state; it used to be recorded as
|
|
43
|
+
`completed`.
|
|
44
|
+
- A tick no longer waits for a launch. The claim is durable and the launch runs
|
|
45
|
+
on its own fiber, so a plan parked awaiting approval no longer holds other
|
|
46
|
+
triggers, and a boundary another trigger crosses meanwhile is claimed.
|
|
47
|
+
- The first poll after a restart dispatches the current occurrence even when
|
|
48
|
+
the backlog exceeds `maxCatchUp` or the policy is `catchUp: "none"`.
|
|
49
|
+
- `DispatchReader.fires` reads only the ledger rows the requested page needs.
|
|
50
|
+
|
|
51
|
+
- `TriggerStore.list` answers `Listed` rows: the decode result for each row and
|
|
52
|
+
the state that row holds. A row whose stored input cannot be decoded is
|
|
53
|
+
isolated there, with its trigger id in the failure, rather than failing the
|
|
54
|
+
whole listing, so one corrupt declaration no longer stops every healthy
|
|
55
|
+
schedule in the store on every tick.
|
|
56
|
+
- A scheduler tick reads the listed row before it asks the store again. A
|
|
57
|
+
trigger holding neither a run nor a buffered occurrence answers `activeRun`
|
|
58
|
+
and `claimPending` by itself, which cost two write transactions per idle
|
|
59
|
+
trigger per tick. `DispatchReader.list` takes each summary's held state from
|
|
60
|
+
the same listing instead of inspecting per trigger.
|
|
61
|
+
- The guides, the claim-protocol concept, and the `Service` documentation say
|
|
62
|
+
that a tick polls `list` rather than `listEnabled`, because a trigger
|
|
63
|
+
disabled while a run was active still has to recover that occurrence, and
|
|
64
|
+
that only claim, result, pending-state, and active-run methods fail with
|
|
65
|
+
`unknown_trigger`: `get` answers `None` and `register` creates the row.
|
|
66
|
+
|
|
67
|
+
- Both trigger stores now apply one claim decision. The revision and enabled
|
|
68
|
+
fences, the expired-reservation reclaim with its supersede predecessor
|
|
69
|
+
lookup, the three resumable predicates, the cursor advance, and the lease
|
|
70
|
+
recovery behind `activeRun` were written out once in SQL and again over maps;
|
|
71
|
+
each store now gathers its snapshot, calls the shared decision, and applies
|
|
72
|
+
the writes it answers.
|
|
73
|
+
- `test/TestTriggers` registration validates the declaration and serializes its
|
|
74
|
+
input at the call boundary, the way SQL registration already did, so a
|
|
75
|
+
declaration mutated after `register` returns cannot change what was stored
|
|
76
|
+
and an unsatisfiable schedule is refused rather than accepted. Its `list` and
|
|
77
|
+
`listEnabled` order by id, and every reading is an independent value that
|
|
78
|
+
cannot be mutated back into stored state.
|
|
79
|
+
- The testing guide, the reference, the claim-protocol concept, and the README
|
|
80
|
+
state what the in-memory store shares with the SQL store and where it stops:
|
|
81
|
+
it holds one process's state, reports no `store` write failures, shows no
|
|
82
|
+
contention between connections, and applies no migrations.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- Daylight saving (#1930). `Cron` now searches wall-clock times and resolves
|
|
87
|
+
each one in the trigger's zone. A time that spring forward skips fires once,
|
|
88
|
+
at the instant it would have had on the old offset (a daily 02:30 fires at
|
|
89
|
+
03:30 daylight time); `previousAtOrBefore` used to fail `unsatisfiable_cron`
|
|
90
|
+
there, so the scheduler never launched the gap day. A time that fall back
|
|
91
|
+
repeats fires once, at its first instant; the repeated instant used to match
|
|
92
|
+
too and launch the day again under a second key. An expression whose hour
|
|
93
|
+
field is every hour still fires in both passes. Milliseconds are zeroed in
|
|
94
|
+
UTC; on a host whose own zone was repeating an hour, a matched occurrence
|
|
95
|
+
used to come back an hour early.
|
|
96
|
+
|
|
97
|
+
### Removed
|
|
98
|
+
|
|
99
|
+
- `TriggerStore.takePending`. Nothing dispatched through it: the scheduler
|
|
100
|
+
takes a buffered occurrence with `claimPending`, which applies the claim
|
|
101
|
+
fences in the same transaction, and reads one without consuming it through
|
|
102
|
+
`inspect`. A destructive read beside those was one more method every store
|
|
103
|
+
had to implement and one more way to drop a buffered occurrence with no
|
|
104
|
+
claim decision behind it.
|
|
105
|
+
|
|
106
|
+
## [1.0.0-rc.0] - 2026-09-01
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- Added Effect Cron-backed declarations, overlap and bounded catch-up
|
|
111
|
+
policies, durable CAS fire claims, a scoped scheduler, authority-free
|
|
112
|
+
channels, and verified raw-byte webhooks.
|
|
113
|
+
|
|
114
|
+
### Changed
|
|
115
|
+
|
|
116
|
+
- A fire claim now carries `expectedRevision` and reads the overlap policy,
|
|
117
|
+
`enabled`, and `revision` inside the claim transaction, so a caller holding a
|
|
118
|
+
declaration snapshot from before an edit is refused with `revision_mismatch`
|
|
119
|
+
or `trigger_disabled` instead of firing it.
|
|
120
|
+
- `TriggerStore.due(now)` is now `TriggerStore.listEnabled()`. The parameter was
|
|
121
|
+
accepted and discarded; due-ness is a cron computation the scheduler performs
|
|
122
|
+
against its own watermark.
|
|
123
|
+
- A webhook `credential` is required and is forwarded to `Channel.Verify` and to
|
|
124
|
+
`Webhook.SignatureConfig.expected` on every request, so a signing secret is
|
|
125
|
+
resolved through the host resolver rather than captured in a closure at
|
|
126
|
+
declaration time. `expected` returns an Effect, so a resolution or HMAC
|
|
127
|
+
failure is a typed `verification_failed` rather than a defect.
|
|
128
|
+
- `Webhook.ingest` copies `body`, `headers`, and `idempotencyKey` before
|
|
129
|
+
verification, so verification, delivery fingerprinting, and decoding all read
|
|
130
|
+
one private snapshot. It no longer registers the channel on every request;
|
|
131
|
+
registration is `Webhook.register`.
|
|
132
|
+
- `Webhook.constantTimeEqual` iterates over the expected signature's length
|
|
133
|
+
rather than the longer of the two inputs, so its work is fixed by the secret
|
|
134
|
+
side of the comparison.
|
|
135
|
+
- Error codes gained `runner`, `invalid_options`, `trigger_disabled`, and
|
|
136
|
+
`revision_mismatch`, and `TriggerError` gained `path`. A declaration failure
|
|
137
|
+
names its offending field and summarizes the schema issue tree instead of
|
|
138
|
+
carrying a five-kilobyte AST as its cause.
|
|
139
|
+
- `last_fired_at_ms` only ever moves forward, so an older run settling after a
|
|
140
|
+
newer occurrence was skipped no longer replays settled work.
|
|
141
|
+
- Scope closure detaches run monitors instead of cancelling them: scheduled runs
|
|
142
|
+
are durable and the next incarnation re-attaches to them.
|
|
143
|
+
- `Cron.occurrencesBetween` caps an unstated limit at `Cron.maxOccurrences` and
|
|
144
|
+
refuses a limit that is not a non-negative safe integer.
|
|
145
|
+
- `Schedule.maxCatchUp` defaults to `0` and is bounded above by
|
|
146
|
+
`Schedule.maxCatchUpLimit`. Every catch-up policy answers to it, `one`
|
|
147
|
+
included.
|
|
148
|
+
- `TriggerStore.Claim` is a discriminated union: a claim that hands the caller
|
|
149
|
+
work to launch carries a required `reservationId`, and a claim that only
|
|
150
|
+
records a decision carries none.
|
|
151
|
+
- `TriggerStore.Service` gained `activeOccurrence(triggerId, runId)`, and
|
|
152
|
+
`TriggerStore.reservationOccurrence` reads the occurrence out of a reservation
|
|
153
|
+
id. A recovered run is settled against the occurrence it actually launched,
|
|
154
|
+
which `lastFiredAt` cannot name once a later occurrence has been skipped or
|
|
155
|
+
buffered past it.
|
|
156
|
+
- A supersede claim records the run it is displacing on the occurrence that
|
|
157
|
+
displaced it, so a claimant that dies before it can cancel leaves the
|
|
158
|
+
predecessor's run id behind for the next incarnation to find.
|
|
159
|
+
- The package documentation is colocated and hand-maintained.
|
|
160
|
+
`packages/smithers/agent/triggers/README.md`, its `docs/` directory, and the
|
|
161
|
+
JSDoc in `src/` own the contract. The `docs` and `docsFiles` targets in
|
|
162
|
+
`PACKAGE.ts` expose them, and `apps/site/scripts/sync-api-docs.mjs` owns the
|
|
163
|
+
site copy.
|
|
164
|
+
|
|
165
|
+
### Fixed
|
|
166
|
+
|
|
167
|
+
- A run held at Control's `accepted` status is no longer recorded as completed
|
|
168
|
+
on the next poll, which had released the overlap guard for a run that had not
|
|
169
|
+
started.
|
|
170
|
+
- A newly registered trigger no longer fires an arbitrarily stale past
|
|
171
|
+
occurrence under `catchUp: "none"`; its first sight establishes the watermark.
|
|
172
|
+
- A launch reservation recovered from the store is no longer cleared on the
|
|
173
|
+
following tick before its lease expires.
|
|
174
|
+
- The occurrence watermark advances only past occurrences this process finished
|
|
175
|
+
dispatching, and a buffered occurrence taken from the row is re-armed when the
|
|
176
|
+
claim or dispatch that followed it failed.
|
|
177
|
+
- `TriggerStore` failures no longer collapse into `store`: a typed refusal the
|
|
178
|
+
store raised travels out unchanged, and every missing-row path in both the SQL
|
|
179
|
+
store and the in-memory test layer fails with `unknown_trigger`.
|
|
180
|
+
- A parked plan is re-offered a bounded number of times rather than once a
|
|
181
|
+
second for the life of the scope, and a scheduler poll interval must be finite
|
|
182
|
+
and positive.
|
|
183
|
+
- A schema the migrator cannot apply arrives as a typed `store` failure instead
|
|
184
|
+
of a defect that escapes a constructor whose signature promises
|
|
185
|
+
`TriggerError`.
|
|
186
|
+
- A launch no longer resolves the wrong `started` handle. An inner binding
|
|
187
|
+
shadowed the `Deferred` the launch awaits, so `Deferred.succeed` was handed the
|
|
188
|
+
run id string: every scheduler tick that launched a run waited forever.
|
|
189
|
+
- A recovered launch reservation is re-read from the store on every tick rather
|
|
190
|
+
than cached as live, so its lease can expire and re-arm the occurrence it
|
|
191
|
+
holds instead of pinning the trigger until the process restarts.
|
|
192
|
+
- An expired reservation over an occurrence that was claimed but never launched
|
|
193
|
+
re-arms that occurrence instead of leaving it with no outcome, no active run,
|
|
194
|
+
and nothing pointing at it.
|
|
195
|
+
- `claimPending` clears the buffer only when the claim it made is not itself a
|
|
196
|
+
buffer, so an occurrence the overlap policy re-buffers is no longer discarded.
|
|
197
|
+
- A supersede whose cancellation fails restores the run it displaced and queues
|
|
198
|
+
the replacement occurrence, and it records the terminal result before it
|
|
199
|
+
detaches the only monitor that could have written one.
|
|
200
|
+
- A launch whose `launched` result cannot be persisted is left to a later poll
|
|
201
|
+
rather than recorded failed while its run is still executing.
|
|
202
|
+
- A terminal result carrying no run id no longer clears a newer run's active
|
|
203
|
+
pointer.
|
|
204
|
+
- Coverage thresholds are 100 on all four metrics. The ratchet that stood at
|
|
205
|
+
68/67/81/80 is what let every defect above ship untested.
|
|
206
|
+
- The CommonJS build no longer hands the migrator `{ default }` objects instead
|
|
207
|
+
of migration Effects. esbuild compiles a default import of a sibling ESM
|
|
208
|
+
module to `__toESM(require(...), 1).default`, which under Node interop is the
|
|
209
|
+
whole exports object, so `require("@smthrs/triggers/SqlTriggerStore")` failed
|
|
210
|
+
at the first migration. The migration modules export `triggers` and
|
|
211
|
+
`reservationLease` by name, `migrations/index.ts` imports them by name, and
|
|
212
|
+
the record is exported as `migrations` so a test can assert each entry has
|
|
213
|
+
`pipe`.
|
|
214
|
+
|
|
215
|
+
### Removed
|
|
216
|
+
|
|
217
|
+
- `@smthrs/triggers/migrations/*` is no longer a public subpath. The intended
|
|
218
|
+
entry point is `SqlTriggerStore.layer`, which applies both migrations in
|
|
219
|
+
order.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Cory and the Smithers Flows contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @smthrs/triggers
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Release candidate scope, host requirements and compatibility review are defined in the [library support policy](https://github.com/smithersai/smithers/blob/main/RELEASE_SUPPORT.md).
|
|
4
|
+
|
|
5
|
+
**Documentation:** https://triggers.smithers.sh
|
|
6
|
+
|
|
7
|
+
Durable cron triggers and verified inbound channels for flows. A trigger is a
|
|
8
|
+
durable row: a flow id, a JSON input, and a cron expression. A channel is a
|
|
9
|
+
verified door: it authenticates raw request bytes and maps them onto a start or
|
|
10
|
+
a signal.
|
|
11
|
+
|
|
12
|
+
Nothing here executes a flow. A launch is handed to
|
|
13
|
+
[`@smthrs/control`](https://control.smithers.sh), so a scheduled or webhook
|
|
14
|
+
started run gets exactly the authority a person starting it would get.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
`@smthrs/triggers` is not on npm at 1.0.0-rc.0. It ships as a member of the
|
|
19
|
+
[smithers repository](https://github.com/smithersai/smithers) workspace, so
|
|
20
|
+
using it today means working from a checkout:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/smithersai/smithers.git
|
|
24
|
+
cd smithers
|
|
25
|
+
pnpm install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It needs Node.js 26.4.0 or later and `effect` 4.0.0-rc.115. The full
|
|
29
|
+
requirements, the import forms, and the layers a running host adds are on
|
|
30
|
+
the [installation page](https://triggers.smithers.sh/installation/).
|
|
31
|
+
|
|
32
|
+
## The smallest declaration
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import * as Trigger from "@smthrs/triggers/Trigger"
|
|
36
|
+
|
|
37
|
+
const nightly = Trigger.make({
|
|
38
|
+
id: "nightly-report",
|
|
39
|
+
flowId: "reports/nightly",
|
|
40
|
+
input: { channel: "#ops" },
|
|
41
|
+
cron: "0 3 * * *",
|
|
42
|
+
timezone: "UTC",
|
|
43
|
+
overlap: "skip",
|
|
44
|
+
catchUp: "one",
|
|
45
|
+
maxCatchUp: 1,
|
|
46
|
+
enabled: true
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`nightly` is an `Effect<Trigger, TriggerError>`. `Trigger.make` fills the policy
|
|
51
|
+
defaults and refuses a cron expression the calendar never satisfies. Registering
|
|
52
|
+
it in a `TriggerStore` makes it durable, and a running `Scheduler` fires it from
|
|
53
|
+
there. The [quickstart](https://triggers.smithers.sh/quickstart/) walks that
|
|
54
|
+
whole path, launch included, on a test clock.
|
|
55
|
+
|
|
56
|
+
## What the package does that a cron loop does not
|
|
57
|
+
|
|
58
|
+
- Coordinates claims when two hosts share a database. Lease recovery can retry
|
|
59
|
+
the same occurrence, so preventing duplicate runs requires runner deduplication.
|
|
60
|
+
- Decides what a boundary means while the previous run is still going: skip it,
|
|
61
|
+
remember the newest one, or cancel the run in flight and replace it.
|
|
62
|
+
- Bounds what a trigger owes after downtime, by a number the declaration states.
|
|
63
|
+
- Accepts an inbound request without handing it authority. A verified payload
|
|
64
|
+
names a flow; it cannot widen what that flow may do.
|
|
65
|
+
|
|
66
|
+
Scheduled dispatch makes at-least-once launch attempts. `RunnerService.start`
|
|
67
|
+
must durably deduplicate by `idempotencyKey` and return the same run identity on
|
|
68
|
+
replay, including across host restarts.
|
|
69
|
+
|
|
70
|
+
## Public API
|
|
71
|
+
|
|
72
|
+
The root entry point exports these namespaces, and each is also importable from
|
|
73
|
+
`@smthrs/triggers/<Module>`. Every export, with its signature and its
|
|
74
|
+
guarantees, is on the
|
|
75
|
+
[API reference](https://triggers.smithers.sh/reference/api/).
|
|
76
|
+
|
|
77
|
+
| Namespace | What it is |
|
|
78
|
+
| ----------------- | -------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `Trigger` | The trigger declaration: a flow id, a JSON input, a schedule, and an enabled flag. |
|
|
80
|
+
| `Schedule` | The reusable schedule half of a declaration: cron, timezone, and the two policies. |
|
|
81
|
+
| `Cron` | Typed wrappers around Effect's cron: parse, next, previous, and a bounded occurrence search. |
|
|
82
|
+
| `Overlap` | The pure decision for an occurrence that arrives while a run is in flight. |
|
|
83
|
+
| `CatchUp` | The pure computation of what a trigger owes after downtime, bounded by its declaration. |
|
|
84
|
+
| `TriggerStore` | The durable state contract: registration, listing, the claim protocol, and results. |
|
|
85
|
+
| `SqlTriggerStore` | The SQLite and PostgreSQL implementation of that contract, with its own migrations. |
|
|
86
|
+
| `Scheduler` | The Clock-driven poller, and the `Runner` port it launches through. |
|
|
87
|
+
| `DispatchReader` | The `@smthrs/control` read port served from a store: trigger summaries and the fire ledger. |
|
|
88
|
+
| `Channel` | The authority-free inbound channel declaration: verify, then map to a start or a signal. |
|
|
89
|
+
| `Webhook` | A verified webhook door built on `Channel`, dispatching only through Control. |
|
|
90
|
+
| `TriggerError` | The one failure type, carrying a stable code and an optional field path. |
|
|
91
|
+
|
|
92
|
+
`@smthrs/triggers/test/TestTriggers` is an in-memory `TriggerStore` for tests.
|
|
93
|
+
It applies the same claim decision as the SQL store, with the same refusal
|
|
94
|
+
codes, lease timing, watermark rules, registration validation, and id-ordered
|
|
95
|
+
listings. It keeps no rows and applies no migrations, so a test about a schema
|
|
96
|
+
or a column shape needs `SqlTriggerStore`.
|
|
97
|
+
|
|
98
|
+
## Documentation
|
|
99
|
+
|
|
100
|
+
- [Overview](https://triggers.smithers.sh)
|
|
101
|
+
- [Quickstart](https://triggers.smithers.sh/quickstart/)
|
|
102
|
+
- [The claim protocol](https://triggers.smithers.sh/concepts/claim-protocol/)
|
|
103
|
+
- [Choose an overlap and catch-up policy](https://triggers.smithers.sh/guides/choose-a-policy/)
|
|
104
|
+
- [Ingest a verified webhook](https://triggers.smithers.sh/guides/ingest-a-webhook/)
|
|
105
|
+
- [Troubleshooting](https://triggers.smithers.sh/troubleshooting/), which lists
|
|
106
|
+
every failure code, what causes it, and what to change.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure catch-up policy decisions.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.1.0
|
|
5
|
+
*/
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import type { Cron } from "./Cron.ts";
|
|
8
|
+
import type { CatchUp as Policy } from "./Schedule.ts";
|
|
9
|
+
import { TriggerError } from "./TriggerError.ts";
|
|
10
|
+
/**
|
|
11
|
+
* The occurrences a trigger owes since it last fired, bounded by
|
|
12
|
+
* `maxCatchUp` and by the policy named in `Schedule.CatchUp`: `none` owes
|
|
13
|
+
* nothing, `one` owes only the most recent, and `all` owes every missed tick.
|
|
14
|
+
*
|
|
15
|
+
* Every policy answers to `maxCatchUp`, `one` included: a bound of zero says
|
|
16
|
+
* no occurrence may be caught up, so a missed occurrence under `one` is
|
|
17
|
+
* `catch_up_bound_exceeded` exactly as three missed occurrences under `all`
|
|
18
|
+
* are. The bound is validated before any policy branch, so an unusable bound
|
|
19
|
+
* is refused even where the policy owes nothing.
|
|
20
|
+
*
|
|
21
|
+
* @category computation
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
export declare const occurrences: (policy: Policy, maxCatchUp: number, lastFiredAt: Date | undefined, now: Date, cron: Cron) => Effect.Effect<ReadonlyArray<Date>, TriggerError>;
|
|
25
|
+
//# sourceMappingURL=CatchUp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CatchUp.d.ts","sourceRoot":"","sources":["../../src/CatchUp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC,OAAO,KAAK,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,MAAM,EACd,YAAY,MAAM,EAClB,aAAa,IAAI,GAAG,SAAS,EAC7B,KAAK,IAAI,EACT,MAAM,IAAI,KACT,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,YAAY,CAwCjD,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var CatchUp_exports = {};
|
|
30
|
+
__export(CatchUp_exports, {
|
|
31
|
+
occurrences: () => occurrences
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(CatchUp_exports);
|
|
34
|
+
var Effect = __toESM(require("effect/Effect"), 1);
|
|
35
|
+
var CronSchedule = __toESM(require("./Cron.js"), 1);
|
|
36
|
+
var import_TriggerError = require("./TriggerError.js");
|
|
37
|
+
const occurrences = (policy, maxCatchUp, lastFiredAt, now, cron) => {
|
|
38
|
+
if (!Number.isSafeInteger(maxCatchUp) || maxCatchUp < 0) {
|
|
39
|
+
return Effect.fail(
|
|
40
|
+
new import_TriggerError.TriggerError({
|
|
41
|
+
code: "catch_up_bound_exceeded",
|
|
42
|
+
message: `maxCatchUp must be a non-negative safe integer, received ${maxCatchUp}`,
|
|
43
|
+
path: "maxCatchUp"
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (lastFiredAt === void 0) return Effect.succeed([]);
|
|
48
|
+
if (policy === "none") return Effect.succeed([]);
|
|
49
|
+
if (policy === "one") {
|
|
50
|
+
return CronSchedule.previousAtOrBefore(cron, now).pipe(
|
|
51
|
+
Effect.flatMap(
|
|
52
|
+
(latest) => latest.getTime() <= lastFiredAt.getTime() ? Effect.succeed([]) : maxCatchUp < 1 ? Effect.fail(
|
|
53
|
+
new import_TriggerError.TriggerError({
|
|
54
|
+
code: "catch_up_bound_exceeded",
|
|
55
|
+
message: `missed 1 occurrence; maxCatchUp is ${maxCatchUp}`
|
|
56
|
+
})
|
|
57
|
+
) : Effect.succeed([latest])
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return CronSchedule.occurrencesBetween(cron, lastFiredAt, now, maxCatchUp + 1).pipe(
|
|
62
|
+
Effect.flatMap(
|
|
63
|
+
(missed) => missed.length > maxCatchUp ? Effect.fail(
|
|
64
|
+
new import_TriggerError.TriggerError({
|
|
65
|
+
code: "catch_up_bound_exceeded",
|
|
66
|
+
message: `missed ${missed.length} occurrences; maxCatchUp is ${maxCatchUp}`
|
|
67
|
+
})
|
|
68
|
+
) : Effect.succeed(missed)
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=CatchUp.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/CatchUp.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Pure catch-up policy decisions.\n *\n * @since 0.1.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport type { Cron } from \"./Cron.ts\"\nimport * as CronSchedule from \"./Cron.ts\"\nimport type { CatchUp as Policy } from \"./Schedule.ts\"\nimport { TriggerError } from \"./TriggerError.ts\"\n\n/**\n * The occurrences a trigger owes since it last fired, bounded by\n * `maxCatchUp` and by the policy named in `Schedule.CatchUp`: `none` owes\n * nothing, `one` owes only the most recent, and `all` owes every missed tick.\n *\n * Every policy answers to `maxCatchUp`, `one` included: a bound of zero says\n * no occurrence may be caught up, so a missed occurrence under `one` is\n * `catch_up_bound_exceeded` exactly as three missed occurrences under `all`\n * are. The bound is validated before any policy branch, so an unusable bound\n * is refused even where the policy owes nothing.\n *\n * @category computation\n * @since 0.1.0\n */\nexport const occurrences = (\n policy: Policy,\n maxCatchUp: number,\n lastFiredAt: Date | undefined,\n now: Date,\n cron: Cron\n): Effect.Effect<ReadonlyArray<Date>, TriggerError> => {\n if (!Number.isSafeInteger(maxCatchUp) || maxCatchUp < 0) {\n return Effect.fail(\n new TriggerError({\n code: \"catch_up_bound_exceeded\",\n message: `maxCatchUp must be a non-negative safe integer, received ${maxCatchUp}`,\n path: \"maxCatchUp\"\n })\n )\n }\n if (lastFiredAt === undefined) return Effect.succeed([])\n if (policy === \"none\") return Effect.succeed([])\n if (policy === \"one\") {\n return CronSchedule.previousAtOrBefore(cron, now).pipe(\n Effect.flatMap((latest) =>\n latest.getTime() <= lastFiredAt.getTime()\n ? Effect.succeed<ReadonlyArray<Date>>([])\n : maxCatchUp < 1\n ? Effect.fail(\n new TriggerError({\n code: \"catch_up_bound_exceeded\",\n message: `missed 1 occurrence; maxCatchUp is ${maxCatchUp}`\n })\n )\n : Effect.succeed<ReadonlyArray<Date>>([latest])\n )\n )\n }\n return CronSchedule.occurrencesBetween(cron, lastFiredAt, now, maxCatchUp + 1).pipe(\n Effect.flatMap((missed) =>\n missed.length > maxCatchUp\n ? Effect.fail(\n new TriggerError({\n code: \"catch_up_bound_exceeded\",\n message: `missed ${missed.length} occurrences; maxCatchUp is ${maxCatchUp}`\n })\n )\n : Effect.succeed(missed)\n )\n )\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,aAAwB;AAExB,mBAA8B;AAE9B,0BAA6B;AAgBtB,MAAM,cAAc,CACzB,QACA,YACA,aACA,KACA,SACqD;AACrD,MAAI,CAAC,OAAO,cAAc,UAAU,KAAK,aAAa,GAAG;AACvD,WAAO,OAAO;AAAA,MACZ,IAAI,iCAAa;AAAA,QACf,MAAM;AAAA,QACN,SAAS,4DAA4D,UAAU;AAAA,QAC/E,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AACA,MAAI,gBAAgB,OAAW,QAAO,OAAO,QAAQ,CAAC,CAAC;AACvD,MAAI,WAAW,OAAQ,QAAO,OAAO,QAAQ,CAAC,CAAC;AAC/C,MAAI,WAAW,OAAO;AACpB,WAAO,aAAa,mBAAmB,MAAM,GAAG,EAAE;AAAA,MAChD,OAAO;AAAA,QAAQ,CAAC,WACd,OAAO,QAAQ,KAAK,YAAY,QAAQ,IACpC,OAAO,QAA6B,CAAC,CAAC,IACtC,aAAa,IACb,OAAO;AAAA,UACP,IAAI,iCAAa;AAAA,YACf,MAAM;AAAA,YACN,SAAS,sCAAsC,UAAU;AAAA,UAC3D,CAAC;AAAA,QACH,IACE,OAAO,QAA6B,CAAC,MAAM,CAAC;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AACA,SAAO,aAAa,mBAAmB,MAAM,aAAa,KAAK,aAAa,CAAC,EAAE;AAAA,IAC7E,OAAO;AAAA,MAAQ,CAAC,WACd,OAAO,SAAS,aACZ,OAAO;AAAA,QACP,IAAI,iCAAa;AAAA,UACf,MAAM;AAAA,UACN,SAAS,UAAU,OAAO,MAAM,+BAA+B,UAAU;AAAA,QAC3E,CAAC;AAAA,MACH,IACE,OAAO,QAAQ,MAAM;AAAA,IAC3B;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authority-free channel declarations.
|
|
3
|
+
*
|
|
4
|
+
* A channel authenticates opaque transport input, maps verified payloads to
|
|
5
|
+
* control-plane starts or signals, and may project run state outbound. It
|
|
6
|
+
* carries no execution authority: the target flow's envelope and permission
|
|
7
|
+
* checks apply unchanged.
|
|
8
|
+
*
|
|
9
|
+
* @see packages/smithers/agent/triggers/docs/api.md
|
|
10
|
+
* @since 0.1.0
|
|
11
|
+
*/
|
|
12
|
+
import type { IdempotencyKey } from "@smthrs/control/ControlSchema";
|
|
13
|
+
import type { CredentialRef } from "@smthrs/control/Credential";
|
|
14
|
+
import type * as Effect from "effect/Effect";
|
|
15
|
+
import type * as Redacted from "effect/Redacted";
|
|
16
|
+
import type * as Schema from "effect/Schema";
|
|
17
|
+
import type { TriggerError } from "./TriggerError.ts";
|
|
18
|
+
/**
|
|
19
|
+
* Opaque inbound transport data. Verification must inspect this value before
|
|
20
|
+
* any payload decoding occurs.
|
|
21
|
+
*
|
|
22
|
+
* @category models
|
|
23
|
+
* @since 0.1.0
|
|
24
|
+
*/
|
|
25
|
+
export interface RawInbound {
|
|
26
|
+
readonly body: Uint8Array;
|
|
27
|
+
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
28
|
+
readonly idempotencyKey: IdempotencyKey;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A verified request that starts a flow.
|
|
32
|
+
*
|
|
33
|
+
* @category models
|
|
34
|
+
* @since 0.1.0
|
|
35
|
+
*/
|
|
36
|
+
export interface Start {
|
|
37
|
+
readonly start: {
|
|
38
|
+
readonly flowId: string;
|
|
39
|
+
readonly input: unknown;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A verified request that resolves a waiting flow step.
|
|
44
|
+
*
|
|
45
|
+
* @category models
|
|
46
|
+
* @since 0.1.0
|
|
47
|
+
*/
|
|
48
|
+
export interface Signal {
|
|
49
|
+
readonly signal: {
|
|
50
|
+
readonly runId: string;
|
|
51
|
+
readonly stepId: string;
|
|
52
|
+
readonly value: unknown;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The only operations an inbound channel may request.
|
|
57
|
+
*
|
|
58
|
+
* @category models
|
|
59
|
+
* @since 0.1.0
|
|
60
|
+
*/
|
|
61
|
+
export type Inbound = Start | Signal;
|
|
62
|
+
/**
|
|
63
|
+
* Authenticates raw request bytes and headers against a credential reference.
|
|
64
|
+
*
|
|
65
|
+
* The credential arrives as a redacted reference rather than a secret, and it
|
|
66
|
+
* arrives per request rather than at declaration time: a verifier resolves it
|
|
67
|
+
* through the host's resolver when it needs the bytes. Dropping this parameter
|
|
68
|
+
* left a verifier no way to reach a secret except by closing over it in plain
|
|
69
|
+
* memory at declaration time, which is the shape the reference exists to
|
|
70
|
+
* prevent. `packages/smithers/control/src/WebhookChannel.ts` states the same contract
|
|
71
|
+
* for the control-plane side of the boundary.
|
|
72
|
+
*
|
|
73
|
+
* @category models
|
|
74
|
+
* @since 0.1.0
|
|
75
|
+
*/
|
|
76
|
+
export type Verify = (raw: RawInbound, credential: Redacted.Redacted<CredentialRef>) => Effect.Effect<void, TriggerError>;
|
|
77
|
+
/**
|
|
78
|
+
* An authority-free bidirectional channel declaration.
|
|
79
|
+
*
|
|
80
|
+
* `inbound` only describes a control-plane start or signal. It cannot supply
|
|
81
|
+
* capabilities, grants, or an alternate execution envelope.
|
|
82
|
+
*
|
|
83
|
+
* @category declarations
|
|
84
|
+
* @since 0.1.0
|
|
85
|
+
*/
|
|
86
|
+
export interface Channel<Payload, Run = never, Outbound = never> {
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly verify: Verify;
|
|
89
|
+
readonly inbound: (payload: Payload) => Inbound;
|
|
90
|
+
readonly outbound?: ((run: Run) => Outbound) | undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Configuration for a schema-declared channel.
|
|
94
|
+
*
|
|
95
|
+
* @category models
|
|
96
|
+
* @since 0.1.0
|
|
97
|
+
*/
|
|
98
|
+
export interface Config<Payload, Run = never, Outbound = never> extends Channel<Payload, Run, Outbound> {
|
|
99
|
+
readonly schema: Schema.Schema<Payload>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Declares a channel without adding authority or execution behavior.
|
|
103
|
+
*
|
|
104
|
+
* @category constructors
|
|
105
|
+
* @since 0.1.0
|
|
106
|
+
*/
|
|
107
|
+
export declare const make: <Payload, Run = never, Outbound = never>(config: Channel<Payload, Run, Outbound>) => Channel<Payload, Run, Outbound>;
|
|
108
|
+
//# sourceMappingURL=Channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Channel.d.ts","sourceRoot":"","sources":["../../src/Channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAC/D,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC9D,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;CACxC;AAED;;;;;GAKG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KACxB,CAAA;CACF;AAED;;;;;GAKG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KACxB,CAAA;CACF;AAED;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;AAEpC;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,MAAM,GAAG,CACnB,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,KACzC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;AAEtC;;;;;;;;GAQG;AACH,MAAM,WAAW,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,KAAK,EAAE,QAAQ,GAAG,KAAK;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,QAAQ,CAAC,GAAG,SAAS,CAAA;CACzD;AAED;;;;;GAKG;AACH,MAAM,WAAW,MAAM,CAAC,OAAO,EAAE,GAAG,GAAG,KAAK,EAAE,QAAQ,GAAG,KAAK,CAAE,SAAQ,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC;IACrG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;CACxC;AAED;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,EAAE,GAAG,GAAG,KAAK,EAAE,QAAQ,GAAG,KAAK,EACzD,QAAQ,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,KACtC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAW,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var Channel_exports = {};
|
|
20
|
+
__export(Channel_exports, {
|
|
21
|
+
make: () => make
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(Channel_exports);
|
|
24
|
+
const make = (config) => config;
|
|
25
|
+
//# sourceMappingURL=Channel.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/Channel.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Authority-free channel declarations.\n *\n * A channel authenticates opaque transport input, maps verified payloads to\n * control-plane starts or signals, and may project run state outbound. It\n * carries no execution authority: the target flow's envelope and permission\n * checks apply unchanged.\n *\n * @see packages/smithers/agent/triggers/docs/api.md\n * @since 0.1.0\n */\n\nimport type { IdempotencyKey } from \"@smthrs/control/ControlSchema\"\nimport type { CredentialRef } from \"@smthrs/control/Credential\"\nimport type * as Effect from \"effect/Effect\"\nimport type * as Redacted from \"effect/Redacted\"\nimport type * as Schema from \"effect/Schema\"\nimport type { TriggerError } from \"./TriggerError.ts\"\n\n/**\n * Opaque inbound transport data. Verification must inspect this value before\n * any payload decoding occurs.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface RawInbound {\n readonly body: Uint8Array\n readonly headers: Readonly<Record<string, string | undefined>>\n readonly idempotencyKey: IdempotencyKey\n}\n\n/**\n * A verified request that starts a flow.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Start {\n readonly start: {\n readonly flowId: string\n readonly input: unknown\n }\n}\n\n/**\n * A verified request that resolves a waiting flow step.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Signal {\n readonly signal: {\n readonly runId: string\n readonly stepId: string\n readonly value: unknown\n }\n}\n\n/**\n * The only operations an inbound channel may request.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Inbound = Start | Signal\n\n/**\n * Authenticates raw request bytes and headers against a credential reference.\n *\n * The credential arrives as a redacted reference rather than a secret, and it\n * arrives per request rather than at declaration time: a verifier resolves it\n * through the host's resolver when it needs the bytes. Dropping this parameter\n * left a verifier no way to reach a secret except by closing over it in plain\n * memory at declaration time, which is the shape the reference exists to\n * prevent. `packages/smithers/control/src/WebhookChannel.ts` states the same contract\n * for the control-plane side of the boundary.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Verify = (\n raw: RawInbound,\n credential: Redacted.Redacted<CredentialRef>\n) => Effect.Effect<void, TriggerError>\n\n/**\n * An authority-free bidirectional channel declaration.\n *\n * `inbound` only describes a control-plane start or signal. It cannot supply\n * capabilities, grants, or an alternate execution envelope.\n *\n * @category declarations\n * @since 0.1.0\n */\nexport interface Channel<Payload, Run = never, Outbound = never> {\n readonly name: string\n readonly verify: Verify\n readonly inbound: (payload: Payload) => Inbound\n readonly outbound?: ((run: Run) => Outbound) | undefined\n}\n\n/**\n * Configuration for a schema-declared channel.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Config<Payload, Run = never, Outbound = never> extends Channel<Payload, Run, Outbound> {\n readonly schema: Schema.Schema<Payload>\n}\n\n/**\n * Declares a channel without adding authority or execution behavior.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <Payload, Run = never, Outbound = never>(\n config: Channel<Payload, Run, Outbound>\n): Channel<Payload, Run, Outbound> => config\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAsHO,MAAM,OAAO,CAClB,WACoC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|