Repository navigation
[scheduler] Dependencies: data model #22854
Description
Part of #22853 (event-scheduling umbrella) — FS behavior.
Define the data structure for event dependencies. The shape only ships what is implemented: the type union starts with 'FinishToStart', and the other PDM types plus lag/lagUnit are added together with their implementations — widening a union and adding optional fields are additive changes, so nothing needs to be reserved up front.
A dependency is not a field on the event — it's a separate relationship (a graph edge) between two events, referencing them by id. One event can take part in many dependencies.
type SchedulerDependencyId = string | number; // same shape as SchedulerEventId
interface SchedulerDependency {
id: SchedulerDependencyId;
source: SchedulerEventId; // predecessor event id (Bryntum: `from`)
target: SchedulerEventId; // successor event id (Bryntum: `to`)
type: 'FinishToStart'; // union widened with 'StartToStart' | 'FinishToFinish' | 'StartToFinish' when implemented
}Lag (design decided now, fields added when implemented): the fields are deliberately NOT reserved in the type — an unimplemented field in the public shape is a silent no-op promise (lag: 2 doing nothing invites bug reports), and adding optional fields later is non-breaking anyway. What IS decided now is the design for when it lands: lag alone would under-specify it — every engine pairs the amount with a unit (Bryntum: lag + lagUnit, default 'day'; Syncfusion: "+3d"/"+3h"/"+3m"; MS Project: working-time days vs elapsed 6ed). Since the scheduler has no working-calendar concept, lag will mean elapsed (calendar) time, applied through the date adapter (DST-safe), with the unit made explicit via lagUnit.
Where everything lives (three levels)
| What | Where |
|---|---|
| Data — the dependencies collection | reactive store state → EventTimelinePremiumState |
| Hook / delegation on event create/update/delete | SchedulerStore (interface, recurring-events pattern) |
| Logic / engine — cascade, mutations | the scheduling plugin (premium-internals) |
Data — in the reactive store state
Dependencies are timeline-premium-only, and components render the arrows through selectors that subscribe to the reactive state — so the collection must live in state, not as a private plugin field. Store it in EventTimelinePremiumState (which extends SchedulerState), following the events naming convention (eventModelList / eventModelLookup) but with only the two fields dependencies actually need:
interface EventTimelinePremiumState extends SchedulerState {
// ...
dependencyModelList: readonly SchedulerDependency[]; // prop mirror / iteration
dependencyModelLookup: Map<SchedulerDependencyId, SchedulerDependency>; // lookup by id
}No separate id list (unlike events): dependencies have no semantic order and no model/processed split, and a Map preserves insertion order, so lookup.keys() already yields ordered ids if a selector ever needs them.
The graph indexes the engine needs — adjacency by source (cascade) and by target (cascade-delete, cycle guard) — are derivable, so they live in memoized selectors over dependencyModelList, not in state.
Controlled prop, like events. Events are a controlled prop: state mirrors parameters.events, and mutations don't write state — they emit the new array via onEventsChange. Dependencies follow the same pattern: a dependencies parameter synced into the slice above + an onDependenciesChange callback emitted by the mutation paths.
Hook — in SchedulerStore (community base)
A SchedulerSchedulingPluginInterface + an optional injected schedulingPlugin ref, with delegation hooks in the event create/update/delete path (the cascade must trigger when an event moves, and event updates flow through the base store — same as recurringEventsPlugin). Premium-agnostic.
Note the seam: the delegation must run inside updateEvents, before onEventsChange, so cascaded changes merge into the same atomic batch. The eventsUpdated published event (the lazy-loading seam) fires in a queueMicrotask after onEventsChange and is not suitable for this (details in #22857).
Logic — in the plugin (premium-internals)
The actual implementation: reads/mutates the dependencies state slice via store.set(...), plus the cascade engine (and later constraint reconciliation). Holds only transient data privately, exactly like SchedulerLazyLoadingPlugin keeps its cache/queue private while writing canonical data into store state. Injected only for premium → no premium code in the community package. This shared home is what lets constraints (used by both premium products, which don't share a premium store base) reuse it next quarter. See Architecture in #22853.
Referential integrity
Dependencies reference events by id, so the model must define what happens when a reference breaks:
- Deleting an event also removes every dependency it takes part in (as
sourceortarget), in the same atomic update — no dangling edges. - Data from props referencing an unknown event id: the dependency is kept in state but ignored by rendering and the engine. Dev-mode warning only when there's no
dataSource— with lazy loading a missing event is expected (it may simply not be fetched yet), so a warning there would be noise. - Lazy loading: the engine never fetches missing events — auto-scheduling only operates on loaded events. Documented limitation, not something the data model works around (behavior details in [scheduler] Dependencies: auto-scheduling engine (FS) #22857).
Recurring events — dependencies are disallowed in v1
Dependencies-by-event-id is unsound for recurring series:
- All occurrences of a series share the base event
id; occurrences are computed (keyed${id}::${dateKey}), never materialized, so a specific occurrence isn't addressable. - Scoped edits mint new ids ("only this" detaches a standalone event with a fresh id; "this and following" creates
${id}::${dateKey}) — a dependency pointing at the original would silently break or point at the wrong thing. - FS against a series has no defined semantics (which occurrence's end?).
No competitor solves this. Bryntum Scheduler Pro has both features but never combines them — their official recurrence demo explicitly sets features: { dependencies: false }, and there is no documented behavior for a dependency on a recurring event. Bryntum Gantt rejects recurrence outright ("one task equals one row"). DHTMLX and Syncfusion keep the two features in separate products (Scheduler = recurrence, Gantt = dependencies). The only products combining them (MS Project, Wrike) escape the problem by materializing each occurrence as a first-class task with its own id — and even there, linking "the series" is degenerate (MS Project resolves it to the last occurrence and doesn't draw the line).
v1 policy: addDependency rejects events with an rrule (with a reason, like the cycle guard), and dependencies from props referencing a recurring event are ignored + dev-mode warning.
Open question: should the timeline expose a prop to disable recurrence entirely, making the "Gantt mode" trade-off explicit? Prior art: Bryntum's recurrence is opt-in (enableRecurringEvents, default false); Bryntum Gantt simply doesn't support recurrence.
Exposure — internal-only until the slice is complete
The sub-issue PRs merge incrementally while the scheduler is in beta, so they land with no public API surface — a data model without rendering or interaction would show up in the API docs as dead API, and beta users adopting it mid-slice would hit churn for nothing. No re-exports from @mui/x-scheduler-premium, no buildPublicAPI additions, and the dependencies / onDependenciesChange parameters live on an internal store-parameters type instead of EventTimelinePremiumParameters (which feeds the public component props, propTypes and API docs). A final PR flips the feature public deliberately once FS works end-to-end: move the two props to EventTimelinePremiumParameters, extend the parameters extractor, re-export the types, regenerate proptypes/docs. addDependency/deleteDependency stay internal store methods consumed by the interaction UI — the event CRUD methods (createEvent, deleteEvent, …) are not exposed through apiRef either, and exposing imperative CRUD would be a separate decision made for events and dependencies together.
Scope
- Define the
SchedulerDependencyshape referencing events by id; thetypeunion starts withFinishToStartonly (see the lag note above for why nothing is reserved). - Add the dependencies slice (model + lookup) to
EventTimelinePremiumState, fed by a controlleddependenciesparameter +onDependenciesChange; adjacency indexes as memoized selectors. - Add the
SchedulerSchedulingPluginInterface+ injection hook inSchedulerStore. - Manage the slice from the scheduling plugin; expose read/write as internal store methods (
addDependency/deleteDependency) for the interaction UI, mirroring the event CRUD methods — see the exposure note above. - Referential integrity: cascade-delete on event deletion; ignore + warn on unknown ids.
- Reject dependencies on recurring events (API + props ingestion).
- Keep the shape additive so SS/FF/SF (union widening) and
lag/lagUnit(optional fields) can be added later with their implementations.
Out of scope
- Rendering, interaction, and the auto-scheduling engine logic (separate sub-issues).
Activity
github-actions commentedon Jul 17, 2026
This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue.
Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.