Skip to main content

operation_recovery 0.1.1 copy "operation_recovery: ^0.1.1" to clipboard
operation_recovery: ^0.1.1 copied to clipboard

Explicit submission and lookup-only recovery of operations with uncertain backend outcomes, with scoped storage and immutable terminal states.

operation_recovery #

A pure Dart package for operations whose backend outcome is uncertain. A lost response cannot establish whether a write succeeded. Recovery must ask the authoritative backend instead of automatically sending the write again.

Release and verification #

operation_recovery is published on pub.dev. The first release, 0.1.0, provides immutable domain records, atomic scoped storage, typed handlers, explicit submission, lookup-only reconciliation, and a separate SQLite reference adapter.

The initial independent Windows acceptance run passed 539 core, 90 storage, 58 codec, and 32 real process tests. The lost-response crash demo used exactly one HTTP submission, one backend effect, and one lookup, with SQLite 3.54.0, WAL/FULL. Memory tests alone do not establish crash recovery.

The maintainer confirmed green Quality and Reliability workflows for the 0.1.0 release, including Windows/Linux clean checkouts on Dart 3.10.0 and stable, native SQLite/process tests, publication archive inspection, and extracted-package/consumer verification. Workflow runs retain native evidence and packaging artifacts. See CONTRIBUTING.md for reproducible verification commands. Source: hachem-djefafla/operation_recovery.

Core has no runtime dependencies and no Flutter, HTTP, or database imports. The SQLite dependency belongs only to that separate example package.

Installation #

Install from pub.dev:

dart pub add operation_recovery

Or add a compatible dependency to your pubspec.yaml:

dependencies:
  operation_recovery: ^0.1.0

The package requires Dart >=3.10.0 <4.0.0. Windows and Linux are verified CI platforms. SQLite stays in the separate example package and does not become a core dependency.

Public domain API #

The API integration guide covers handlers, startup recovery, authentication, account switching, and shutdown. Run dart run example/public_api_smoke.dart for a minimal public-import workflow, or dart run example/main.dart for a real HTTP response-loss example.

Import package:operation_recovery/operation_recovery.dart.

Type Responsibility
OperationIdentity Validated immutable identity, value equality, conflict checks
OperationRecord Prepared construction and validated snapshot restoration
RecoveryMetadata Persisted lookup reservations, cumulative attempts, and next-check time
OperationState Five states with terminal/unresolved classification
BackendObservation Closed set of semantic backend observations
LookupFailureReason Authentication, transport, or service unavailability
OperationRecoveryException Base of the exported typed domain exceptions
final identity = OperationIdentity(
  id: 'op-123',
  scope: 'account-456',
  type: 'transfer',
  idempotencyKey: 'key-123',
  requestFingerprint: 'sha256:application-provided',
);
final snapshot = OperationRecord.prepared(
  identity: identity,
  at: DateTime.utc(2026, 10, 20),
);
assert(snapshot.state == OperationState.prepared);
assert(snapshot.state.isUnresolved);

This creates domain data only. It performs no persistence or submission. Run the HTTP lost-response example with dart run example/main.dart. It submits once, loses the response after the simulated backend applies the operation, and resolves the outcome by lookup. Its storage is memory-only.

Backend observations are OperationSucceeded, OperationRejected, OperationPending, OperationUnknown, and LookupUnavailable. The last can carry LookupFailureReason.authenticationRequired; expired credentials and HTTP 401 during reconciliation must map to that recoverable condition.

The state machine and storage coordinator live under lib/src/ and are not exported. All coordinator state changes use the state machine. The coordinator serializes each complete storage-handler-storage sequence per operation ID.

Public recovery API #

This workflow uses the real loopback backend from example/main.dart, which drops a successful response. Applications supply their own OperationHandler with authoritative status lookup and no HTTP write retries. Run this within example/, alongside its backend source:

import 'package:operation_recovery/operation_recovery.dart';
import 'simulated_backend.dart';

Future<void> main() async {
  final backend = await SimulatedBackend.start();
  final registration = OperationRegistration<String>(
    type: 'transfer',
    handler: DemoHttpHandler(backend.baseUri),
  );
  final recovery = OperationRecovery(
    scope: 'account-456',
    store: MemoryOperationStore(),
    handlers: [registration],
    policy: RecoveryPolicy(
      maxAttempts: 3,
      initialDelay: const Duration(seconds: 1),
      maxDelay: const Duration(seconds: 8),
    ),
  );
  final identity = OperationIdentity(
    id: 'op-123',
    scope: 'account-456',
    type: 'transfer',
    idempotencyKey: 'key-123',
    requestFingerprint: 'sha256:application-provided',
  );
  try {
    final uncertain = await recovery.start(
      registration,
      identity: identity,
      request: 'demo transfer',
    );
    print(uncertain.state); // awaitingOutcome after response loss
    final confirmed = await recovery.reconcile(identity);
    print(confirmed.state); // succeeded after authoritative lookup
  } finally {
    await recovery.close();
    await backend.close();
  }
}

Implement OperationHandler<TRequest> with separate submit and lookupStatus methods. Register it using an OperationRegistration<TRequest> token and pass that same token to start. Construct one OperationRecovery per scope with a store, the registrations, and an explicit RecoveryPolicy. Request data is transient; the application supplies its matching fingerprint and owns credentials.

API Behavior
start(registration, identity: ..., request: ...) Submit once only after this invocation acknowledges its uncertainty CAS
get(id) Read a scoped snapshot
reconcile(identity) Reserve and perform one due lookup; never submit
reconcilePending(concurrency: ...) Process due records with per-operation success/failure results
resume(identity) Reset an attention record's cycle budget without dispatching
events Asynchronous committed-snapshot notifications, including reservations
close() Stop new/queued work and drain already-dispatched handlers and saves

Failed or ambiguous uncertainty acknowledgements never permit submission, including after a successful read. Recovery uses persisted attempt reservations and bounded exponential backoff. Exhaustion pauses in needsAttention; explicit resume enables lookups only. Handlers own deadlines and must disable write retry middleware. Observer exceptions cannot change committed business state.

On startup, open the existing durable database, recreate handlers for the same scope, and call reconcilePending(). No background polling or write replay runs automatically. Restore expired credentials in the application, honor persisted due times, and use resume(identity) explicitly if recovery reached attention. Subscribe to events for live committed snapshots and use get for current state. On logout, await close() before releasing the old account's clients/credentials; create a new scoped facade for the next account and close stores separately.

One coordinator serializes matching IDs; independent IDs can progress concurrently. Multiple coordinators/processes have no shared handler-ownership guarantee. Shutdown waits for dispatched handlers to settle and does not own stores/clients. See the recovery contract for precise semantics and the runnable example for integration.

Public storage API #

OperationStore defines atomic create-if-absent, versioned compare-and-swap, scoped reads, and scoped unresolved listing. MemoryOperationStore is the non-durable reference implementation:

final store = MemoryOperationStore();
await store.createIfAbsent(snapshot);
final stored = await store.read(id: identity.id, scope: identity.scope);

Use this snippet inside an async function. Memory atomicity applies only to calls sharing one instance within one isolate's execution context. It does not provide sharing or atomicity across processes/isolates, or persistence across restarts.

Foreign-scope and missing reads both return null. Conflicting writes fail without modification. Lists contain only unresolved records in the requested scope.

OperationStoreWriteException distinguishes definitely uncommitted writes from unknown commit outcomes through StoreCommitStatus. Neither is an acknowledgement. The coordinator surfaces CAS conflicts with OperationStoreConflictException without retrying or refreshing the version. See the storage contract for details.

Contracts and verification #

  • Architecture and verification boundaries
  • State machine, identity, and validation
  • Storage, concurrency, and scope guarantees
  • Submission, reconciliation, events, and shutdown
  • SQLite persistence and serialization
  • Backend integration contract
  • Failure model and acceptance matrix
  • Architecture decisions
  • Case study: a successful write with a lost response
  • Contribution and CI instructions

From a repository checkout, resolve both packages before whole-tree analysis:

dart pub get
dart pub -C example/sqlite_recovery get
dart format --output=none --set-exit-if-changed .
dart analyze --fatal-infos
dart test

Licensed under MIT. Measured core coverage through the source-mode runner is 99.33% (442/445 executable lines). CI enforces a 90% minimum without excluding safety-critical implementation code to meet the target. Each new patch must pass the quality, reliability, and publication checks on its own revision.

1
likes
150
points
--
downloads

Documentation

API reference

Publisher

verified publisherthodz.com

Explicit submission and lookup-only recovery of operations with uncertain backend outcomes, with scoped storage and immutable terminal states.

Topics

#reliability #recovery #idempotency #persistence #state-machine

License

MIT (license)

More

Packages that depend on operation_recovery