Files
doormile_milderapp/lib/data/mutation_guard.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 05:40:35 +05:30

71 lines
3.3 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// ONE PRESS, ONE REQUEST
///
/// Every business-critical action in this app is a POST that the backend is not
/// idempotent about: `accept`, `reject`, `reached`, `parcel`, `payment`,
/// `pickup-complete`, `deliver`, `skip`, `duty/start`, `duty/end`,
/// `breaks/start`, `breaks/end`, `availability`.
///
/// A rider presses those on a moving bike, with gloves, on a screen he cannot
/// look at for long — so a double tap is not an edge case, it is Tuesday. And
/// the failure is not cosmetic: two `duty/start` calls are an error the server
/// rejects, two `payment` calls are two payments, and two `accept` calls race
/// each other to decide what the list shows next.
///
/// The screens each solved this, or did not, with their own `_busy` flag —
/// which works until the widget rebuilds, the callback is captured twice, or the
/// action is reachable from two places (a card and a sheet) that do not share a
/// flag. This is one place that solves it once, keyed by the thing being
/// mutated rather than by the widget that happens to be showing it.
///
/// ── What this is not ──
///
/// Not a queue and not a retry. A second press while the first is in flight is
/// **dropped**, not deferred: the rider meant one thing, and running it twice a
/// second later is the bug, not the fix. Not a cache either — once the call
/// completes the key is free, so a genuine second accept of the same stop after
/// a failure still goes through.
/// ─────────────────────────────────────────────────────────────────────────
class MutationGuard {
MutationGuard._();
/// Keys with a request currently in flight.
static final Set<String> _inFlight = <String>{};
/// True while [key] has a mutation running.
///
/// Read by buttons so they can show the disabled/loading state that makes the
/// dropped second press *visible* rather than merely harmless.
static bool isBusy(String key) => _inFlight.contains(key);
/// Runs [action] unless [key] is already running, in which case it returns
/// null and does nothing.
///
/// The key names the **resource and the verb**, not the screen:
/// `accept:1042`, `deliver:77`, `duty`. Two widgets showing the same stop
/// therefore share one guard, which is the whole point.
static Future<T?> run<T>(String key, Future<T> Function() action) async {
if (_inFlight.contains(key)) {
debugPrint('[GUARD] dropped duplicate "$key" — one is already running');
return null;
}
_inFlight.add(key);
try {
return await action();
} finally {
// `finally`, so a thrown request frees its key. A guard that leaks on
// failure is worse than no guard: the rider's retry would be silently
// dropped for the rest of the session.
_inFlight.remove(key);
}
}
/// Clears every key. For tests, and for sign-out — a new session must not
/// inherit the last one's in-flight set.
static void reset() => _inFlight.clear();
}