enqueue method
Queues payload under idempotencyKey — replaces any existing item
with the same key rather than duplicating it. Bounded at capacity:
once full, the lowest-priority item is evicted to make room.
Implementation
SdkResult<void> enqueue({
required String idempotencyKey,
required Map<String, Object?> payload,
int priority = 0,
int? expiresAtMs,
}) {
if (idempotencyKey.isEmpty) {
return const SdkFailure(
kind: SdkErrorKind.validation,
message: 'idempotencyKey must not be empty',
);
}
final next = [...items]
..removeWhere((i) => i.idempotencyKey == idempotencyKey);
if (next.length >= capacity) {
// BUG-44: eviction candidates exclude any item already parked in
// manualReview (never auto-discard something only a human decision
// can resolve) and must have STRICTLY LOWER priority than the
// incoming item (a low/equal-priority newcomer can't bump an
// existing higher-or-equal-priority item). If nothing qualifies,
// the outbox is genuinely full for this item — reject it loudly
// instead of silently evicting something it has no right to.
final evictable = [
for (var i = 0; i < next.length; i++)
if (!next[i].manualReview && next[i].priority < priority) i,
];
if (evictable.isEmpty) {
return const SdkFailure(
kind: SdkErrorKind.validation,
message:
'Outbox is full and no lower-priority, non-manualReview item '
'can be evicted for this one',
);
}
var lowestIndex = evictable.first;
for (final i in evictable) {
if (next[i].priority < next[lowestIndex].priority) lowestIndex = i;
}
next.removeAt(lowestIndex);
}
next.add(
OutboxItem(
idempotencyKey: idempotencyKey,
payload: payload,
priority: priority,
expiresAtMs: expiresAtMs,
),
);
items.assignAll(next);
// BUG-89: a drain() pass already in flight took its `ordered` snapshot
// before this item existed, so it won't see it — flag it so drain()'s
// own loop runs one more pass after this one finishes instead of
// leaving this item stuck until some later, not-guaranteed-to-happen
// drain() call.
if (_draining) _dirtyDuringDrain = true;
unawaited(_persist());
// Auto-drain only when a ConnectivityCoordinator confirms we're online
// right now — without one, the caller drives drain() explicitly (e.g.
// this SDK's own tests, or an app not wiring FEAT-62 in). Auto-draining
// unconditionally here would race a caller enqueuing several items
// back-to-back against each one's own fire-and-forget drain attempt,
// breaking the priority ordering a single drain() call guarantees.
if (connectivity?.state == ConnectivityState.online) {
unawaited(drain());
}
return const SdkSuccess(null);
}