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>
71 lines
3.3 KiB
Dart
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();
|
|
}
|