Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
56 lines
2.5 KiB
Markdown
56 lines
2.5 KiB
Markdown
# Scheduling
|
|
|
|
Shared schedule time semantics for WP3 (publishing) and WP13 (multi-output
|
|
schedules). Pure functions and types only — no app, database or Temporal
|
|
imports, and no runtime dependency beyond Node's `Intl` tzdata.
|
|
|
|
A schedule is a five-field cron expression plus an explicit IANA timezone
|
|
(never server-local; `UTC` allowed). Occurrences are evaluated in that zone as
|
|
nominal local date-times, then materialized to UTC fire instants. The
|
|
occurrence key is the materialized UTC instant, so `UNIQUE (workspace_id,
|
|
schedule_id, fire_at_utc)` holds by construction.
|
|
|
|
## Policies
|
|
|
|
- **DST gap** (nonexistent local time): fires at the first valid instant after
|
|
the gap (shift forward). Works for 1-hour gaps and Pacific/Apia's whole-day
|
|
skip alike.
|
|
- **DST fold** (ambiguous local time): fires ONCE, at its first (earlier-offset)
|
|
instant.
|
|
- **Collapse**: several nominal local times materializing onto one post-gap
|
|
instant produce exactly one occurrence, labelled with the latest collapsed
|
|
slot — the conservative value for the `last_fired_nominal_local` gate.
|
|
- **Re-materialization** on tzdata or timezone change: deletes all unclaimed
|
|
future occurrences, then regenerates only occurrences strictly after
|
|
max(change instant, last claimed fire instant) whose nominal slot is strictly
|
|
later than `last_fired_nominal_local`. A slot already fired under the old
|
|
zone (daily 09:00 fired in New York, zone switched to Los Angeles) never
|
|
fires again. The tzdata version is reported for recording.
|
|
|
|
## API
|
|
|
|
```ts
|
|
import {
|
|
parseCron, // five-field cron -> sets (rejects 6-field/`?`/`L`/`W`/`#`/macros)
|
|
nextOccurrences, // (spec, from, count, {exclusive}) -> Occurrence[]
|
|
materializeOccurrence, // ("2024-03-10T02:30", zone) -> epoch ms
|
|
rematerialize, // (oldSpec, newSpec, storedRows, {changeInstant, ...})
|
|
canonicalTimezone, // validates + canonicalizes; rejects "local"/offsets
|
|
currentTzdataVersion, // process.versions.tz
|
|
} from "@tactical-media-broadcast-hub/scheduling";
|
|
```
|
|
|
|
Errors: `SchedulingError` base, with `InvalidCronError`, `ImpossibleScheduleError`,
|
|
`InvalidTimezoneError`, `InvalidNominalLocalError`, `InvalidInstantError`. Note
|
|
`toInstantMs` rejects offset-less ISO datetimes that would otherwise parse in
|
|
the server's local zone.
|
|
|
|
## Verification
|
|
|
|
```
|
|
pnpm exec tsc -p tsconfig.json --noEmit
|
|
pnpm exec vitest run
|
|
```
|
|
|
|
`pnpm install` is required once so the workspace links this package's
|
|
`vitest`/`typescript` devDependencies into `packages/scheduling/node_modules`.
|