submit method

Future<MissionOutcome> submit(
  1. Mission mission, {
  2. void onEvent(
    1. MissionEvent event
    )?,
})

Submits a mission. If an active coalescing group exists for the mission's key, the caller subscribes to that group's event stream and receives the same events. Otherwise, a new group is created and the mission executes once (FR-001, FR-002).

If idempotency is enabled and a cached outcome exists for the key, it is returned immediately without re-execution (FR-007).

Implementation

Future<MissionOutcome> submit(
  Mission mission, {
  void Function(MissionEvent event)? onEvent,
}) async {
  final cached = _cache.lookup(mission.key);
  if (cached != null) {
    mission.outcome = cached;
    mission.status = MissionStatus.completed;
    onEvent?.call(MissionEventCompleted(mission.id, cached));
    return cached;
  }

  final canonical = mission.key.canonical;
  final existing = _groups[canonical];
  if (existing != null) {
    existing.addSubscriber(mission.callerId);
    _missionIdToKey[mission.id] = canonical;
    final sub = existing.events.listen(onEvent ?? (_) {});
    try {
      return await existing.done;
    } finally {
      await sub.cancel();
    }
  }

  // New coalescing group — execute once.
  final group = CoalescingGroup(mission);
  group.addSubscriber(mission.callerId);
  _groups[canonical] = group;
  _missionIdToKey[mission.id] = canonical;

  final sub = group.events.listen(onEvent ?? (_) {});
  final cancelToken = CancelToken(
    gracePeriod: _config.cancellationGracePeriod,
  );
  group.cancelToken = cancelToken;

  try {
    final execOutcome = await _executor(mission, group, cancelToken);
    // If cancel() ran first, the group is already completed with the
    // salvaged outcome — use that. Otherwise, complete with the
    // executor's outcome.
    if (group.isCompleted) {
      // Cancellation completed the group; return the salvaged outcome.
      final salvaged = group.mission.outcome!;
      _cache.store(mission.key, salvaged);
      return salvaged;
    }
    group.complete(execOutcome);
    _cache.store(mission.key, execOutcome);
    return execOutcome;
  } catch (e, st) {
    final outcome = OutcomeFailed(e, st);
    group.complete(outcome);
    _cache.store(mission.key, outcome);
    return outcome;
  } finally {
    await sub.cancel();
    await group.close();
    _groups.remove(canonical);
    _missionIdToKey.remove(mission.id);
  }
}