Skip to content

Saving Server Data

Use createCommandExecution for writes whose server outcome must survive a lost response. It is exported from @revolist/revogrid-pro; no separate engine package installation is needed. Loading and Caching Server Data covers the read configuration for pagination, infinite loading and grouping.

The executor captures an operation ID, resource ID, expected revision and command payload once. Repeated execute() calls share a pending attempt or return its stored receipt. There are no automatic write retries. Read retry/polling settings and QueryClient mutation defaults do not cause command submission.

The backend must authenticate the current tenant and permission scope, validate the expected revision, and return a receipt correlated by operationId:

StatusRequired fields beyond operationIdMeaning
acceptedchanges, revisionThe server committed canonical changes
rejectedmessageThe server definitively declined this command
conflictmessage, authoritative, revisionThe expected revision was superseded
unknownOptional messageThe outcome still needs recovery

A thrown transport error, aborted request, malformed receipt or wrong operation ID produces unknown, never a fabricated rejection or optimistic rollback. Use validateReceipt to check product-specific revision/completeness rules in addition to the built-in shape and correlation checks.

For recovery and retry, the backend must store receipts durably by operation ID within the authenticated resource scope. An idempotency guarantee requires the write and its receipt to be committed atomically, and reuse of an ID with a different payload to be rejected. An HTTP request ID alone is insufficient. Client handles do not deduplicate independent application instances or browser tabs.

import {
createCommandExecution,
type CommandReceipt,
} from '@revolist/revogrid-pro';
type UpdateOrder = { id: string; title: string };
type CanonicalOrder = { id: string; title: string };
const execution = createCommandExecution<UpdateOrder, CanonicalOrder>({
operationId: crypto.randomUUID(),
resourceId: 'orders',
expectedRevision: 'revision-17',
command: { id: 'order-1', title: 'Updated order' },
}, {
submit: async (envelope, { signal }) => {
const response = await fetch('/api/orders/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() as Promise<CommandReceipt<CanonicalOrder>>;
},
recover: async (envelope, { signal }) => {
const response = await fetch(
`/api/orders/receipts/${encodeURIComponent(envelope.operationId)}`, { signal },
);
if (!response.ok) throw new Error('Receipt unavailable');
return response.json() as Promise<CommandReceipt<CanonicalOrder>>;
},
applyAccepted: canonical => {
// Atomically update your domain store, preserve/rebase newer proposals,
// and record accepted history here.
console.log(canonical);
},
onStatus: state => console.log(state.status, state.error),
});
const receipt = await execution.execute();
if (receipt.status === 'unknown') {
await execution.recover(); // Receipt lookup; no additional write.
}

Use the same handle for duplicate UI actions. The engine owns a detached snapshot: later caller edits, provider mutations and changes to execution.envelope do not change the captured ID, revision or payload used by recovery/retry. Transport data must be structured-cloneable and cannot contain shared writable buffers.

The domain retains ownership of pending edits, conflict resolution and history. After a conflict, use the authoritative data to rebase the proposal and create a new execution with a new operation ID and expected revision. A definitive conflict/rejection is never retried under its old operation ID.

Public types are CommandEnvelope<Command, Revision>, CommandReceipt<Changes, Revision, Conflict>, CommandProvider<Command, Changes, Revision, Conflict>, CommandOptions<Command, Changes, Revision, Conflict>, CommandState<Changes, Revision, Conflict> and CommandExecution<Command, Changes, Revision, Conflict>. Revision defaults to string; authoritative conflict data defaults to the canonical changes type. Opaque revisions are validated and interpreted by the provider, not numerically ordered.

CommandOptions fieldBehavior / default
submitRequired provider receiving the captured envelope and attempt-specific signal
recoverOptional receipt lookup provider; missing recovery rejects explicitly
idempotentfalse; only explicit true enables retry() of unknown writes
resumefalse; restore an already-submitted envelope as unknown without submitting
validateReceiptOptional product validation before accepting any receipt
applyAcceptedApply canonical changes atomically; may return a promise
sourceOptional shared query source; accepted() runs after canonical application
onStatusOptional lifecycle callback; not called after disposal
Handle memberBehavior
envelopeDetached captured envelope; persist it if application-owned recovery must survive reload
getState()Detached receipt plus status, local submission attempt count and optional error
execute()Submit once; share a pending attempt or return the stored receipt
recover()Look up an existing submitted command receipt; never resubmit
retry()Explicit retry of an unknown write with the identical envelope; requires idempotency
reconcile()Retry accepted canonical application/read synchronization without another write
cancel()Abort the current transport; an unconfirmed submission remains unknown
dispose()Abort transport, settle logical callers and stop late domain/status delivery

Status is idle, submitting, recovering, a receipt status, or disposed. Application/refetch errors remain attached to an accepted receipt. They do not turn it into a failed write or permit resubmission. Call reconcile() to retry local synchronization; successful domain application is not repeated if only query synchronization failed. Domain application must be atomic if it can throw.

Cancellation cannot undo a server write. An abort-ignoring physical submission must settle before another retry/recovery attempt starts. A receipt confirmed before cancellation/disposal retains its definitive outcome. Asynchronous domain callbacks already entered by the application must guard their own lifetime.

Pass the same source used by the grid’s query.source. The executor applies canonical changes first, then calls source.accepted() to advance the acceptance fence, cancel obsolete reads and refetch active views. A pre-acceptance response cannot hydrate either shared grid. The source fence does not replace backend revision checks or replica read-after-write guarantees.

Default standalone loader contexts are private; use an explicit source to connect external writes to shared reads. Snapshot queries still require a new provider snapshot after membership changes. With query: false, invalidate the application-owned cache and use the loader’s normal refresh path; a shared Query source cannot refetch bypassed loaders.

Persist the original envelope in your domain’s recovery store before submission. Recreate an execution with that envelope and resume: true, then call recover(). In resume mode execute() returns unknown and does not write. Apply the recovered receipt to the appropriate domain/source scope. The executor provides no durable browser storage, cross-tab coordination, automatic retry or generic proposal ledger.

See Scheduler integration for product configuration, acceptance and recovery.

See Kanban integration for product configuration, acceptance and recovery.

See Gantt for product configuration, acceptance and recovery.