invalidation_coordinator 0.1.0 copy "invalidation_coordinator: ^0.1.0" to clipboard
invalidation_coordinator: ^0.1.0 copied to clipboard

Coalesces keyed invalidations into serialized reconciliation batches with trailing execution and graceful shutdown.

invalidation_coordinator #

A small, pure Dart coordinator for keyed refresh and reconciliation work. It collapses duplicate invalidations, runs one batch at a time, preserves work that arrives during execution, and shuts down gracefully.

The problem #

Several independent producers can discover that the same resource is stale:

Profile screen ───────┐
Push notification ────┼──→ refresh profile
Lifecycle event ──────┘

Starting a refresh for every signal wastes work and can introduce races. InvalidationCoordinator<K> treats equal pending keys as one logical need to reconcile current state:

profile, profile, inbox, profile → {profile, inbox}

This is not an event bus. It does not broadcast every signal. It coordinates the minimum keyed work required to make current state fresh again.

Quick start #

import 'package:invalidation_coordinator/invalidation_coordinator.dart';

enum Resource { profile, inbox, transactions }

final coordinator = InvalidationCoordinator<Resource>(
  handler: (resources) async {
    if (resources.contains(Resource.profile)) {
      await refreshProfile();
    }
    if (resources.contains(Resource.inbox)) {
      await refreshInbox();
    }
    if (resources.contains(Resource.transactions)) {
      await refreshTransactions();
    }
  },
  onError: (resources, error, stackTrace) {
    reportRefreshFailure(resources, error, stackTrace);
  },
);

coordinator.invalidate(Resource.profile);
coordinator.invalidateAll([
  Resource.profile,
  Resource.inbox,
]);

await coordinator.idle;
await coordinator.close();

The full runnable example demonstrates signals arriving during an active refresh in the package example.

Trailing invalidations #

The active snapshot never changes. If more invalidations arrive while its handler is running, they form one trailing pending batch:

running:  {profile, inbox}
arrives:  profile, transactions, profile
trailing: {profile, transactions}

After the active handler settles, the coordinator yields to the event queue and then runs the trailing snapshot. More keys accepted during that yield can join the same batch. There is never more than one handler invocation running at once.

The yield prevents a self-invalidation chain from monopolizing Dart's microtask queue. Exact timing, iteration order, and batch boundaries are intentionally not guaranteed.

Waiting for idle #

isIdle is false whenever work is pending, scheduled, running, or being reported synchronously through onError.

coordinator.invalidate(Resource.profile);
await coordinator.idle;

idle completes at the next quiescent point, including all trailing work accepted before that point. Handler failures do not fail this Future; they are reported through onError.

An already completed idle Future remains completed if later work arrives. Read idle again to observe that later work. Do not await coordinator.idle from its own handler, because the handler must finish before idle can complete.

Graceful close #

close() synchronously stops admission and asynchronously drains everything already accepted:

final closed = coordinator.close();

// Throws StateError, even for invalidateAll([]).
coordinator.invalidate(Resource.inbox);

await closed;

Repeated calls are safe and await the same shutdown. Active work is not cancelled and no timeout is imposed. Failures are reported while draining, and remaining accepted work continues. Do not await close() from the handler.

Error handling #

onError is required and synchronous. It receives the exact immutable snapshot passed to the failed handler, plus the original error and stack trace:

onError: (keys, error, stackTrace) {
  logger.error('Refresh failed for $keys', error, stackTrace);
},

Failed work is never retried automatically. If another equivalent invalidation was accepted while the failed attempt ran, that independent trailing work still runs. Retry limits, cooldowns, and reporting policy belong to the application.

The handler and onError execute in the Zone where the coordinator was constructed. Handler-returned Futures must be able to deliver their completion in that error zone. See handler Futures and error zones if your application deliberately moves Futures across distinct error zones.

If onError itself throws, the secondary error goes to the construction Zone's uncaught-error channel. Coordinator cleanup is preserved if that Zone allows the application to continue.

Keys and snapshots #

  • Equal pending keys collapse according to == and hashCode.
  • Keys must have stable, non-throwing equality and hash codes.
  • Every handler receives a non-empty, detached, unmodifiable Set<K>.
  • Key objects themselves are not frozen.
  • Completed keys are not retained for historical deduplication.
  • Include account, session, or tenant scope in the key when isolation matters.

When not to use this package #

Do not use invalidation_coordinator when:

  • every event, count, or ordering position must be preserved;
  • callers need a result for each individual signal;
  • work must survive process or isolate termination;
  • you need automatic retries, backoff, cancellation, throttling, or rate limits;
  • different keys should execute concurrently;
  • you need an event bus, message broker, persistent queue, or state-management framework;
  • a single-flight cache of one request result is the real requirement.

The package is in-memory, isolate-local, framework-neutral, and has no runtime dependencies. It works in Dart and Flutter applications on all Dart-supported platforms.

License #

MIT

2
likes
160
points
98
downloads

Documentation

API reference

Publisher

verified publisherthodz.com

Weekly Downloads

Coalesces keyed invalidations into serialized reconciliation batches with trailing execution and graceful shutdown.

Repository (GitHub)
View/report issues

Topics

#async #coalescing #concurrency #invalidation #refresh

License

MIT (license)

More

Packages that depend on invalidation_coordinator