Skip to content

Cancelling live interactions - #766

Merged
lukasz-jazwa merged 20 commits into
mainfrom
cancel-interaction
Aug 5, 2026
Merged

Cancelling live interactions#766
lukasz-jazwa merged 20 commits into
mainfrom
cancel-interaction

Conversation

@Jacek-Synergy

Copy link
Copy Markdown
Collaborator

No description provided.

Jacek-Synergy and others added 19 commits August 3, 2026 06:55
A directive destroyed as a bystander (e.g. virtualization destroying node
or port components during a touch pan) no longer resets the shared
TouchEventsStateService.currentEvent, so gesture exclusivity survives.
Only the directive whose gesture set the marker clears it — on pointerup,
on cancelActiveInteraction, or when destroyed mid-gesture.
Covers every gesture (drag, linking, resize, rotate, pan): state rollback,
immediate listener teardown, the cancelled reason on the ended events, the
kept viewport after a cancelled pan, the programmatic no-op, and Escape
staying non-intrusive when nothing is cancellable.
- move the drag rollback snapshot (initialPositions) off the public
  DraggingActionState onto the handler-private gesture state, matching the
  documented rule that gesture bookkeeping stays out of the public action
  state; regenerate api-report and docs
- cancelActiveInteraction: derive the return value from hasActiveInteraction
  and collect cancel errors in a plain list
- panning handlers: no-op cancel() early when no pan is active
- pointer-move-selection cancel(): guard clause instead of one big if
- tests: reuse macrotask/mockEnvironment from test-utils, collapse the
  repeated router.emit literals behind an emitGesture helper, share the
  dragging-state fixture in flow-core tests
- e2e fixture: promote nodePosition/centerOf/beginDrag/nextFrame onto the
  Diagram page object and reuse them in the cancel-interaction spec
- a cancel arriving while the gesture's normal end phase is in flight is
  refused: the drag handler reads gesture.ended, resize/rotate track the
  finishing state identity, linking uses a _finishing marker shared by
  finishLinking and cancelLinking (also prevents duplicate edgeDrawEnded
  and double-cancel re-entry); handler cancel() now returns whether it
  tore anything down and cancelActiveInteraction reports that truthfully
- cancelActiveInteraction refuses to run while a transaction is active
  (the rollback would merge into it and could be discarded with it) and
  ignores concurrent re-entrant calls
- gesture directives and ManualLinkingService guard against a second
  start mid-gesture, which orphaned interaction-cleanup registrations
  and made hasActiveInteraction() stick to true
- minimap navigation joins the cancel machinery: drag registers an
  interaction cleanup, cancel tears down its listeners and pointer
  capture, destroy mid-drag clears the panning state it set
- tests: cancel-vs-in-flight-end at unit and integration level,
  transaction guard, concurrent double cancel, group-children rollback,
  virtualized panning cancel, cancelLinking finishing/identity guards,
  re-entry and unregister-on-pointerup specs, ManualLinkingService and
  minimap specs, e2e: selection intact after cancel, Escape free after a
  completed gesture, programmatic cancel of manual linking
- finishingState in the resize/rotate handlers is set-only: every gesture
  starts with a fresh state object, so a stale reference can never match a
  live gesture — the four conditional resets were pure bookkeeping
- integration test: hoist the shared startDrag helper instead of a second
  copy (startDragOn/dragStartEvent removed)
- minimap ngOnDestroy uses the existing setPanningState helper
- ManualLinkingService.startLinking calls the idempotent cleanup()
  unconditionally
- one canonical re-entry-guard comment across the five gesture directives
- FlowCore keeps one cancellableGestures registry (action-state probe
  paired with the handler's input-event name) that both
  hasActiveInteraction() and cancelActiveInteraction() derive from — a new
  cancellable gesture is one entry, and the dragging/pointerMoveSelection
  naming seam is an explicit pair instead of a surprise
- the teardown-claim concept moves into EventHandler as
  claimTeardown()/isTeardownClaimed(); resize and rotate drop their twin
  finishingState fields, and the base-class doc is the single place that
  explains why drag (gesture.ended) and linking (_finishing) differ
- every gesture participant names its teardown removeListeners() (rotate,
  linking, ManualLinkingService, minimap renamed)
- rotate.directive follows the sibling shape: validate targetData before
  committing any state, tear down before emitting the end phase,
  pointercancel delegates to pointerup
- pointer-move-selection and panning directives clear their core action
  state when destroyed mid-gesture, like resize/rotate/linking/minimap
  already did
A gesture started right after Escape could capture geometry from the model
before the cancel's rollback transaction committed: the second resize's
start phase read the pre-rollback size as its baseline, so the first pointer
move snapped the node back to the size from just before Escape. The same
window existed for rotate (initial angle), drag (position snapshot) and
linking (stale port geometry).

FlowCore exposes isCancellingInteraction() and every gesture handler drops
incoming input events while it is true — the window closes when the rollback
commit resolves, which the awaitable-emit contract guarantees. Reproduced
and pinned by an integration test that parks the cancelResize pass on a slow
middleware and races a second resize into the window.
The resize and rotate handles stopPropagation() on pointerdown, so the
keyboard directive's bubble-phase focus grab never fired for them. A
gesture started while focus was outside the diagram (e.g. right after
clicking a toolbar button) left every shortcut dead — Escape could not
cancel the resize. The focus grab now runs as a capture-phase listener,
which fires before any handler can stop propagation.

E2e reproduces the exact flow: focus a button outside the diagram, start
a resize on the handle, press Escape — the size must roll back.
onPointerDown stays a plain method (the capture listener wraps it), so the
api-report only gains the additive OnDestroy members. Adds the missing
CHANGELOG entry for the focus fix and regenerates the api docs.
FlowCore keeps the public facade (unchanged signatures and TSDoc, thin
delegations); the cleanup registry, the re-entrancy latch, the cancellable
gestures registry and the cancel orchestration move into a dedicated
collaborator, following the TransactionManager/MeasurementTracker pattern.
No caller changes.
Comment-quality pass over the branch's additions: constraints and
mechanisms stay, in-progress narration goes.

- the four directives' marker guard now states why it exists (the marker
  keeps concurrent gestures out) and what gestureActive marks (the
  marker's writer)
- EventHandler docs trimmed to contracts; handle() documents the
  un-awaited invocation interleaving contract the drag handler's comment
  already pointed at
- FlowCore facade docs shortened to match the service canon; the
  registerInteractionCleanup and isCancellingInteraction docs drop
  consumer narration
- cancel-linking/finish-linking lose the "mirror" narration; the
  cancelReason write in the drag handler explains it is not a dead write
- keyboard-inputs capture comment generalized beyond resize/rotate;
  minimap gains the canonical re-entry comment and the captureElement
  rationale
Covers what the FlowCore facade tests cannot see: cleanups run before
handler cancels, the cancelling latch is up only while a cancel is in
flight and is released on rejection, the transaction guard refuses
without touching cleanups or the router, and a re-entrant cancel is a
side-effect-free no-op.
@lukasz-jazwa
lukasz-jazwa merged commit f14c3a4 into main Aug 5, 2026
2 checks passed
@lukasz-jazwa
lukasz-jazwa deleted the cancel-interaction branch August 5, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants