feat(events): schedule, recurrence and the calendar (Phase 4)
The four closed recurrence shapes computed in the definition's own IANA zone, a fourteen-day materialisation horizon with projections beyond it, series as a managed thing, and the admin calendar that replaces the plugin this feature exists to replace. An event now happens on its own. No schema change: Phase 1 built every column this needed. - events/recurrence.js is the ONE place an occurrence is computed, so the runner's expansion and the calendar's forecast cannot disagree. No date library added — Node ships the tzdata one would vendor, behind Intl. - The runner's materialise leg is now two halves: expand, then sweep. The window starts at `now - grace`, so an occurrence nobody could have seen is never invented retroactively; the horizon is what makes the missed sweep mean anything for a recurrence. - Publishing is the schedule switch and archiving turns it off, and publishing re-pins every occurrence that has not started. - A projection is never drawn over an instant a run occupies, so a cancelled occurrence does not reappear as a forecast. 54 new tests, incl. the DST fixture set the plan asked for and three new statements proved against a real MariaDB. Suite 1768/1711/56 skipped/1 fail (pre-existing CRLF). Walked end to end on the local review stack. Docs: RunicGateway/docs#PENDING Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ const runsDb = require('../src/model/events/eventRuns.db')
|
||||
const stepsDb = require('../src/model/events/eventRunSteps.db')
|
||||
const logDb = require('../src/model/events/eventRunLog.db')
|
||||
const versionsDb = require('../src/model/events/eventVersions.db')
|
||||
const definitionsDb = require('../src/model/events/eventDefinitions.db')
|
||||
const db = require('../src/utils/db')
|
||||
|
||||
after(() => db.close())
|
||||
@@ -47,7 +48,7 @@ const later = (ms) => new Date(T0.getTime() + ms)
|
||||
let store
|
||||
const originals = {}
|
||||
|
||||
for (const [name, mod] of [['runsDb', runsDb], ['stepsDb', stepsDb], ['logDb', logDb], ['versionsDb', versionsDb]]) {
|
||||
for (const [name, mod] of [['runsDb', runsDb], ['stepsDb', stepsDb], ['logDb', logDb], ['versionsDb', versionsDb], ['definitionsDb', definitionsDb]]) {
|
||||
originals[name] = { mod, fns: { ...mod } }
|
||||
}
|
||||
|
||||
@@ -68,6 +69,14 @@ function installStubs() {
|
||||
nextStepId: 1,
|
||||
}
|
||||
|
||||
// Phase 4 put a schedule-expansion leg in front of the tick. This file is
|
||||
// about what the runner does with runs that ALREADY exist, so it has nothing
|
||||
// to expand — but the leg is a real query, and left unstubbed every `tick()`
|
||||
// here would reach for the dead-port pool and wait on it. Answering with an
|
||||
// empty list is what keeps this file measuring the runner rather than a
|
||||
// connection timeout.
|
||||
Object.assign(definitionsDb, { findSchedulable: async () => [] })
|
||||
|
||||
// Snapshots, not live references. A SQL SELECT hands back a copy, and the
|
||||
// runner reads `step.attempts` as the value BEFORE its own claim incremented
|
||||
// it — returning references here would make the retry budget off by one in the
|
||||
|
||||
Reference in New Issue
Block a user