T-HUB/packages/scheduling
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
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/.
2026-10-10 11:56:42 +00:00
..
src Tactical Media Broadcast Hub — initial release 2026-10-10 11:56:42 +00:00
package.json Tactical Media Broadcast Hub — initial release 2026-10-10 11:56:42 +00:00
README.md Tactical Media Broadcast Hub — initial release 2026-10-10 11:56:42 +00:00
tsconfig.json Tactical Media Broadcast Hub — initial release 2026-10-10 11:56:42 +00:00

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

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.