Skip to content

Mutations and App-Owned Saving

Rename one booking to save it. Select Reject next save to see the change roll back.

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.

FlowApplication ownsScheduler owns
App-owned savingListen to final event changes or gridedit, send your API requests, and manage save failures, retries and reconciliation.Local editing, validation, projection and history.
remote.commitMutationImplement one save callback returning acceptance or rejection, optionally with canonical event data.Pending state, optimistic/staged projection, rejection rollback and accepted history.
remote.commandsImplement 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.

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.

These options belong to remote, alongside saving. They are not fields of remote.query.

OptionDefaultMeaning
optimistictrueShow the proposed edit while saving. With false, keep the accepted schedule visible until acceptance. Applies when mutation hooks are configured.
rollbackOnErrortrueRestore 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'.
queryBuilt-in read engineControls 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.

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.