Mutations and App-Owned Saving
Live server example
Section titled “Live server example”Rename one booking to save it. Select Reject next save to see the change roll back.
Loading Scheduler example…
Loading Scheduler example…
A mutation is a proposed change to schedule data: creating, moving, resizing, editing or deleting an event, including bulk changes. Reading a new viewport is a query; saving an edited event is a mutation. Scheduler computes the proposed event state and applies local permission/conflict checks. Your application decides how to persist it.
Choose who coordinates saving
Section titled “Choose who coordinates saving”| Flow | Application owns | Scheduler owns |
|---|---|---|
| App-owned saving | Listen to final event changes or gridedit, send your API requests, and manage save failures, retries and reconciliation. | Local editing, validation, projection and history. |
remote.commitMutation | Implement one save callback returning acceptance or rejection, optionally with canonical event data. | Pending state, optimistic/staged projection, rejection rollback and accepted history. |
remote.commands | Implement a backend with operation IDs, durable receipts and revision checks; supply submit/recover callbacks. | Ordered submission, unknown-outcome tracking, explicit recovery and accepted history. |
Remote loaders do not require a mutation callback. For app-owned full or incremental saves, use Data ownership and save flows. Persist each edit through one chosen flow: adding a second save listener to a configured remote callback would submit the edit twice. Final events are observational and cannot cancel an already committed edit.
validateMutation at the top level is synchronous local validation. remote.validateMutation is an optional asynchronous application hook before the save callback. Do not add a separate validation HTTP request unless the backend needs it; the save endpoint can validate and commit atomically.
Minimal save callback
Section titled “Minimal save callback”import type { EventSchedulerConfig } from '@revolist/scheduler';
const remote: EventSchedulerConfig['remote'] = { enabled: true, optimistic: true, rollbackOnError: true, commitMutation: async ({ action, eventId, event, changes, signal }) => { const response = await fetch('/api/scheduler/events', { method: 'POST', signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, eventId, event, changes }), }); if (!response.ok) throw new Error('Save failed'); // Endpoint returns { accepted, event?, events?, message? }. return response.json(); },};The request supplies the proposed event, previousEvent, complete projected events, previousEvents, changes, date range and attempt signal. Choose the fields your endpoint needs; do not serialize the AbortSignal. Return { accepted: true, event: canonicalEvent } for a canonical single-event response, or { accepted: true, events: canonicalEvents } for a complete event collection. A partial events array is not an incremental patch. Return { accepted: false, message } to reject.
Optimistic projection and rollback
Section titled “Optimistic projection and rollback”These options belong to remote, alongside saving. They are not fields of remote.query.
| Option | Default | Meaning |
|---|---|---|
optimistic | true | Show the proposed edit while saving. With false, keep the accepted schedule visible until acceptance. Applies when mutation hooks are configured. |
rollbackOnError | true | Restore rejected optimistic changes while preserving later edits. With false, keep the rejected proposal visible for application reconciliation; it is not accepted history. |
refreshPolicy | 'none' | After acceptance, optionally reload 'visible-range' or 'changed-range'. |
query | Built-in read engine | Controls read caching, freshness, retries, polling and reconnect. It does not retry writes or choose rollback behavior. |
Legacy commitMutation treats thrown errors as rejection. A lost HTTP response does not prove the backend rejected a write; use receipts below when this distinction matters. A command outcome of unknown keeps its proposal pending, even with rollback enabled, until recovery establishes acceptance or rejection.
Observe event-scheduler-remote-mutation-pending, -committed, -rejected and -unknown (each with the full event-scheduler-remote-mutation- prefix). Inspect scheduler.getRemoteState() for pending/failed event IDs and unknown mutation request IDs. Rejected writes do not enter undo history. Read refreshes preserve pending proposals.
Command receipts
Section titled “Command receipts”Scheduler supports an additive remote.commands configuration. Existing
validateMutation / commitMutation callbacks retain their legacy behavior,
including legacy thrown-error rejection. Configure commands instead of
commitMutation to opt into explicit receipts and unknown-outcome handling.
import type { EventSchedulerRemoteCommands } from '@revolist/scheduler';
const commands: EventSchedulerRemoteCommands = { resourceId: 'schedule-42', getExpectedRevision: () => 'schedule-revision-17', submit: async (envelope, { signal }) => { const response = await fetch('/api/schedules/commands', { method: 'POST', signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(envelope), }); if (!response.ok) throw new Error('Write response unavailable'); return response.json(); }, recover: async (envelope, { signal }) => { const response = await fetch(`/api/schedules/receipts/${encodeURIComponent(envelope.operationId)}`, { signal }); if (!response.ok) throw new Error('Receipt unavailable'); return response.json(); },};
// Add to the existing Scheduler configuration:const remote = { enabled: true, optimistic: false, commands };EventSchedulerRemoteCommands supplies resourceId, optional
getExpectedRevision(request), and the executor’s submit, recover,
idempotent, validateReceipt and source settings. Scheduler owns canonical
application and status; applyAccepted, onStatus and resume are not Scheduler
configuration fields. The command payload preserves the existing mutation request
shape, with its signal supplied separately per attempt. Accepted changes may
contain canonical event / events, assignments, message and conflict metadata.
getExpectedRevision should return the current acknowledged backend revision.
Update that domain value from accepted receipts; resolve conflicts explicitly
before constructing a rebased command.
Scheduler serializes receipt commands in initiation order. Unknown outcomes hold
subsequent receipt commands, preserve optimistic/staged edits and do not enter
undo history. getRemoteState().unknownMutationIds exposes request IDs needing
recovery. Use plugin.recoverRemoteMutation(requestId) for receipt lookup, or
plugin.retryRemoteMutation(requestId) only when idempotent: true is backed by
the server contract. Acceptance records canonical history once; rejected/conflict
receipts follow existing rejection/rollback policy, with the full receipt supplied
on mutation event details for authoritative conflict handling.
Observe event-scheduler-remote-mutation-unknown alongside the existing pending,
committed and rejected events. Changing command resource/provider/source scope or
destroying the Scheduler disposes its old executions; late receipts cannot update
the new view. Recover outstanding backend writes using an application-owned store
when a view has been destroyed. Read loaders retain their existing contracts.
The executor does not synthesize compensating undo/redo writes. Applications
retain responsibility for persisting those commands through their domain policy.