createChangeController
createChangeController<
TData>(options):ChangeController<TData>
Defined in: core/src/events/controller.ts:476
Creates the change controller over a store, an emitter and a hook registry. The store stays synchronous and hook-free; the controller is the only place where edits wait for hooks.
commit(changeSet) — one user action or API change:
- The change set is re-validated against the store’s current model (
revalidateChangeSet): aligned to the granularity, unknown and locked tasks dropped, conflicts recomputed. - The
beforeTaskChangehooks run one after another in registration order (Chain of Responsibility), each with{ changeSet, reason, conflicts }of the current change set:falsecancels; aChangeSetreplaces the current one (one without areasongets the current one’s), which is re-validated first, so the next hook sees it aligned and with its own conflicts;trueor nothing passes. A hook that throws, rejects or answers anything else cancels, and the error is emitted onerror(source: 'hook'). Before a hook’s promise is awaited, the change set being confirmed is shown as the preview. - Cancelled: the preview is removed and the result is
{ status: 'cancelled' }. Nothing left to change (empty from the start or after re-validation): the preview is removed,'unchanged'. - Otherwise the change set is re-validated once more if the store changed while a hook was
awaited, and applied with the store’s
applyChangeSet: uncontrolled data is patched, controlled data stays and the change set goes to the committed layer of the draft until the owner’s nextsetTasks. Then the observers (observe) and the events:issues(when re-validation reported any, also on cancel), exactly onetasksChange, then per changetaskDatesChange(whenstartorendExclusivechanged; with theprecisionofafterwhen the change sets one) andtaskProgressChange(whenprogresschanged).
When no hook returns a promise, the whole commit happens synchronously inside the call; the returned promise only reports the result.
One operation at a time. While a commit or a link creation awaits a hook,
ChangeController.pending is true: commit, createLink and deleteLink resolve
{ status: 'busy' } at once and preview is refused (the interaction layer does not start a
drag while pending). The lock is released before the after-events, so their listeners may
start the next operation.
Waiting for the owner (controlled data). While the store shows a committed change
the owner has not applied yet (state.draft?.committed), every operation that changes data
(commit, apply, createLink, applyLinkCreate, deleteLink, applyLinkDelete) resolves
'busy': it would be computed on the owner’s old data, and the change would be lost. Previews
still show over it. The owner’s setTasks (through update) ends the wait.
Event order. An operation started while another one reports (by one of its observers or
event listeners) is applied at once, and its observers are called right after its store
change, but its events wait in an outbox: the running operation’s events all go out first,
then those of the nested operations, in the order they were applied. Each action still emits
exactly one tasksChange. The nested call returns its result at once; what its observers and
listeners throw is rethrown by the outermost operation. The events of the history and the edit
session (historyChange, dirtyChange, editRollback) go through report and keep this
order too: an operation’s own historyChange and dirtyChange come right before its events,
also for a nested one, and a session step started by a listener reports after the running
operation’s events.
createLink(link): the link is checked as the model normalizes it
(normalizeLink): one the model would drop (a malformed field, an invalid lag or one with
months, a misaligned lag with alignInput: 'reject') is 'rejected' as 'invalid' with the
issues; a link to or from an unknown task, or a duplicate (the same source, target and type,
defaults applied, whatever the id; or an explicit link.id already in use), is 'rejected' at
once too. An id-less link whose default id `${source}:${type}:${target}` another link
took (a link with other ends that has it as its explicit id) gets the first free id of
`${defaultId}#2`, #3, …, written as its explicit id into a copy of the link, which is
what the hooks see, what is appended and what linkCreate and the result carry: the model
would drop the link otherwise. Then the beforeLinkCreate hooks run in order with
{ link, id, wouldCreateCycle } (from the link graph): false cancels, a throw or rejection
cancels with an error event. A link that would close a cycle (or a self-link) is then always
'rejected' with 'cycle', whatever the hooks answered: they may only veto more. The checks
are repeated if the store changed while a hook was awaited. Then uncontrolled links get the
link appended (setLinks), controlled ones stay for the owner to apply, and linkCreate is
emitted.
deleteLink(id): an unknown id is 'rejected'. Uncontrolled data loses the
link: removed from links, or, for a link from the Task.dependencies shorthand, the matching
entries are removed from its target task (setTasks). Controlled data stays for the owner.
Then linkDelete is emitted. Permissions (linkable, editable) are the interaction layer’s
job; API calls are never refused for them.
Without hooks. apply, applyLinkCreate and applyLinkDelete do the same synchronously
and skip the before-hooks (undo, redo); they are 'busy' like the others.
update(command) forwards the owner’s setTasks / setLinks / setOptions / setData
to the store and reports it to the observers, which is how edit sessions and the history learn
about data replaced from outside.
Errors. A before-hook that throws, rejects or answers something invalid cancels the
operation ('cancelled', reason: 'error', with the error) and the error is emitted on
error. An exception of an after-event listener reaches the error listeners; when there
are none it rejects the returned promise (or is thrown by the synchronous methods), after the
change was applied (it is never swallowed). So does what a store listener throws: the
observers and the events still run. Several errors are one AggregateError naming the
operation ('2 event listeners or observers threw during commit()').
Type Parameters
Section titled “Type Parameters”TData = unknown
Parameters
Section titled “Parameters”options
Section titled “options”ChangeControllerOptions<TData>
The store, the emitter, the hook registry and initial handlers.
Returns
Section titled “Returns”ChangeController<TData>
The controller.
Example
Section titled “Example”const store = createStore({ tasks, links });const emitter = createEmitter<CoreEvents>();const controller = createChangeController({ store, emitter, handlers: { onBeforeTaskChange: ({ conflicts }) => conflicts.length === 0 || confirm() },});emitter.on('tasksChange', (changeSet) => save(changeSet));controller.preview(dragPreview); // while draggingconst result = await controller.commit(drop); // on pointer upif (result.status === 'cancelled') showWhy(result.reason);