works-calendar-engine
v1.0.0
Published
Framework-agnostic scheduling state machine with rule-based conflict detection.
Maintainers
Readme
works-calendar-engine
Framework-agnostic scheduling state machine with rule-based conflict detection.
import { CalendarEngine, evaluateConflicts } from 'works-calendar-engine';
const result = evaluateConflicts({
proposed: { id: 'shift-1', start: '2026-06-01T08:00', end: '2026-06-01T16:00', resource: 'alice' },
events: existingShifts,
rules: [{ id: 'no-overlap', type: 'resource-overlap', severity: 'hard' }],
});
if (!result.allowed) {
console.log(result.violations); // → [{ rule, severity, message, ... }]
}Pure TypeScript. Runs in Node, browsers, workers, edge. Only runtime dep is date-fns.
Why this exists
Most calendar libraries draw pixels. None of them validate. FullCalendar renders. React Big Calendar renders. Schedule-X renders. When you need to enforce "no double-booking" or "minimum 11 hours of rest between shifts" or "facility capacity = 6 docks," you write it yourself.
This is that piece, extracted.
What it does
- Conflict detection — 8 built-in rule types (resource overlap, category mutex, min rest, capacity overflow, outside business hours, availability violation, hold conflict, policy violation). Add your own.
- Schedule kinds — domain model for shift, on-call, open shift, covering. Normalizes messy real-world category strings.
- Recurrence — RFC 5545 RRULE expansion (FREQ, INTERVAL, COUNT, UNTIL, BYDAY, BYMONTHDAY, BYMONTH, EXDATE).
- State machine — typed mutations, begin/commit/rollback transactions, pub/sub subscriptions, undo/redo with full snapshots.
- Approvals — pure-function state-machine reducer for the requested → approved → finalized → denied lifecycle, with a hash-chained audit log (optionally HMAC-keyed for server-verified, tamper-resistant chains).
- Resource pools — query DSL for "all resources where role=driver and within 50mi of pickup."
What it isn't
- Not a renderer. Bring your own UI (React, Vue, Svelte, vanilla — works with any).
- Not a backend. It's framework-agnostic logic; persist however you want.
- Not a workflow engine. Approval reducer is in scope; multi-step DAG automation isn't (separate concern).
Install
npm install works-calendar-engine date-fnsdate-fns is a peer dep (engine accepts ^3.6.0 || ^4.0.0).
Examples
examples/cdn-embed— single HTML file, no build step. Loads the engine fromesm.shvia an import map and demonstratesevaluateConflictsagainst a small shift list. This is the literal "embed on a webpage" path.examples/react-basic— month grid + conflict badges, ~50 lines of consumer code.examples/node-server— Express POST/bookingsvalidates against the engine before writing to the DB.examples/fullcalendar-bridge— drop the engine underneath FullCalendar. IntercepteventChange, run the diff throughevaluateConflicts, reject on violation. You don't have to replace your calendar — just add intelligence underneath it.
API surface
The full public surface lives in src/index.ts. Highlights:
| Symbol | Purpose |
|---|---|
| CalendarEngine | State container with typed mutations + transactions |
| EventBus | Lifecycle pub/sub (booking.requested, .approved, ...) |
| UndoRedoManager | Full-snapshot undo/redo |
| buildOperation.* | Operation factories (fromDragMove, fromFormSave, ...) |
| evaluateConflicts | Run a candidate event through a rule set |
| evaluateAvailability | Check against availability windows |
| evaluateRequirements | "This shift needs 1 driver + 1 co-driver" |
| resolvePool | Expand a virtual pool to concrete resource IDs |
| findBlockingHold | Pre-booking hold checks |
| expandOccurrences / expandRRule | RRULE → concrete dates |
| transitionApproval / appendAuditEntry | Approval state machine + audit |
| normalizeEvent | Loose WorksCalendarEvent → strict EngineEvent |
Date input shape
start and end accept either a Date instance or an ISO-8601 string
('2026-06-01T08:00', with or without a timezone offset). Strings without
an offset are interpreted as local wall-clock time, then normalized to a
Date internally. If you want predictable cross-timezone behavior, pass
either a UTC Date or an ISO string with an explicit offset.
Security & embeddability
This package is designed to run inside third-party pages, sandboxed iframes, web workers, and edge runtimes. It commits to the following:
- No
eval, nonew Function(), no dynamic code generation. Safe under a strict Content Security Policy that omitsunsafe-evalandunsafe-inline. - No DOM access. The engine doesn't touch
window,document,localStorage, or any browser-only global. It runs identically in Node, browser main thread, web workers, service workers, Cloudflare Workers, and Vercel Edge. - No network calls. No
fetch, noXMLHttpRequest, no telemetry, no remote configuration. The only I/O is whatever you wire into theonErrorhook onEventBus/CalendarEngine. - No prototype pollution surface. Inputs are validated against
schemas before mutation; the engine never assigns into
Object.prototypeor accepts__proto__as a key. - Crypto uses Web Crypto (
globalThis.crypto) only.createIdpreferscrypto.randomUUID()and falls back togetRandomValues(); it throws rather than silently usingMath.random()if neither is available. The audit-chainsha256Hex/hmacSha256Hexare pure synchronous JS implementations — no Nodecryptoimport. The approval audit log defaults to an unkeyed (tamper-evident) SHA-256 chain; pass a server-heldkeytoappendAuditEntry/verifyAuditChainfor an HMAC-SHA256 chain that a client cannot forge. - No runtime dependencies except
date-fns(peer dep,^3.6.0 || ^4.0.0).date-fnsitself has no transitive runtime deps. sideEffects: false— bundlers tree-shake the engine down to only the symbols you actually import.- Ships ESM + CJS + sourcemaps. Works under Vite, Webpack 5,
Rollup, esbuild, Parcel, and bare Node (
importorrequire).
If you're embedding via <script type="module"> directly from a CDN,
esm.sh/works-calendar-engine is the supported path:
<script type="module">
import { evaluateConflicts } from 'https://esm.sh/works-calendar-engine';
// ...
</script>Status
0.1.x — public API surface (191 exports) is stable enough to build
against. Minor versions may add exports; breaking changes to existing
exports will bump to 0.2.0 and be listed in the CHANGELOG. The path
to 1.0.0 is feature completeness on the recurrence subset (full
RFC 5545 BYHOUR/BYMINUTE/BYSETPOS) and a frozen public surface.
See CHANGELOG.md.
License
MIT
