The shared transactional command wrapper — one CommandMiddleware every feature reuses via
contribution.use(...) (sheet, movable, …) instead of re-implementing (or copying) it. Each
wrapped callback stays pure; the middleware adds, uniformly:
batch — runs the work in transaction.doBatch(..., label) so it's one undo entry;
busy — notifier.showBusy(label) for the duration;
error — a thrown error is reported via notifier.showError (a falsy throw = user cancel ⇒ swallowed);
repeatable — after success, sets manager.setRepeatableOperation so F4 / redo-when-empty
re-dispatches the same command + args (command.execute(args)), which reads live selection,
so the action re-applies to the current target. Repeat-after-repeat works (re-executing re-arms it).
serialized — wrapped commands run one at a time (chained on tail). transaction.doBatch
opens a transaction synchronously but commits on the callback's promise, so two overlapping async
commands interleave on the store's shared LIFO stack and corrupt it ("Transaction stack out of sync"
→ "Transaction already closed") — e.g. holding a key that repeats. Chaining makes each command fully
commit before the next opens.
A command opts out of the whole wrapper with the tag 'noTransaction' (e.g. dialogs / navigation that
shouldn't batch), or out of just repeat with 'noRepeat'. A noTransaction command is not queued —
it doesn't open our transaction, and serializing a transient op (zoom / selection) behind a slow
mutation would just add lag.
The shared transactional command wrapper — one
CommandMiddlewareevery feature reuses viacontribution.use(...)(sheet, movable, …) instead of re-implementing (or copying) it. Each wrapped callback stays pure; the middleware adds, uniformly:transaction.doBatch(..., label)so it's one undo entry;notifier.showBusy(label)for the duration;notifier.showError(a falsy throw = user cancel ⇒ swallowed);manager.setRepeatableOperationso F4 / redo-when-empty re-dispatches the same command + args (command.execute(args)), which reads live selection, so the action re-applies to the current target. Repeat-after-repeat works (re-executing re-arms it).tail).transaction.doBatchopens a transaction synchronously but commits on the callback's promise, so two overlapping async commands interleave on the store's shared LIFO stack and corrupt it ("Transaction stack out of sync" → "Transaction already closed") — e.g. holding a key that repeats. Chaining makes each command fully commit before the next opens.A command opts out of the whole wrapper with the tag
'noTransaction'(e.g. dialogs / navigation that shouldn't batch), or out of just repeat with'noRepeat'. AnoTransactioncommand is not queued — it doesn't open our transaction, and serializing a transient op (zoom / selection) behind a slow mutation would just add lag.