Skip to content

[scheduler] Dependencies: data model #22854

Closed

Description

@rita-codes

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 source or target), 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 SchedulerDependency shape referencing events by id; the type union starts with FinishToStart only (see the lag note above for why nothing is reserved).
  • Add the dependencies slice (model + lookup) to EventTimelinePremiumState, fed by a controlled dependencies parameter + onDependenciesChange; adjacency indexes as memoized selectors.
  • Add the SchedulerSchedulingPluginInterface + injection hook in SchedulerStore.
  • 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

  1. added
    type: new featureExpand the scope of the product to solve a new problem.
    plan: PremiumImpact at least one Premium user.
    scope: schedulerChanges related to the scheduler.
    on Jun 17, 2026
  2. self-assigned this
    on Jun 23, 2026
  3. github-actions commented on Jul 17, 2026

    @github-actions
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

on holdThere is a blocker, we need to wait.plan: PremiumImpact at least one Premium user.scope: schedulerChanges related to the scheduler.type: new featureExpand the scope of the product to solve a new problem.

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions