1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! **Exactly-once resolution: the contract, not the storage** (A3, #1837).
//!
//! The abstract shape lives here because it is a set of function
//! signatures over types this crate already owns, which costs nothing
//! against the dependency guard. The only implementation that needs a
//! database lives in `newt-core`, which is the layer that has one.
//!
//! **This contract does not mandate a SQL shape.** The store in
//! `newt-core` resolves through a rowcount CAS inside an `Immediate`
//! transaction, but that is a property of *that* implementation — the
//! serialization it needs comes from SQLite's own locking across
//! independent connections, not from anything stated here. An in-memory
//! implementation may legitimately collapse the whole thing to one
//! compare-and-swap. What every implementation must preserve is the
//! observable contract below.
//!
//! **Idempotency keys are a promise, and a violated promise is an
//! error.** A key says "this is the same submission I already sent". If
//! two DIFFERENT responses arrive under one key, honouring the first and
//! silently discarding the second would let a retry substitute a
//! different answer — an authorization-relevant substitution disguised as
//! a network retry. So that case is
//! [`ResolutionError::IdempotencyConflict`], not a quiet first-wins. A
//! genuine retry — the same response bytes, hence the same
//! [`ResponseId`] — collapses to [`Resolution::Replayed`].
use Error;
use crate;
/// What a caller asks a store to persist: this response, resolving this
/// offer, under this key.
///
/// Ids only. The store is a resolver, not a second copy of the record.
/// The outcome of an attempt to resolve an offer.
/// The facts of an idempotency-key collision.
///
/// Boxed into [`ResolutionError`] rather than inlined: two content ids
/// and a key would make every `Result` in this module carry the conflict
/// case's width on the success path too.
/// Why a resolution attempt could not be carried out.
///
/// Generic over the implementation's own error so the contract does not
/// drag a storage error type into this crate.
/// Persisting the exactly-once decision.
///
/// One offer resolves at most once. Every implementation must satisfy:
///
/// - the FIRST valid response for an offer returns [`Resolution::Won`];
/// - any later, different response returns [`Resolution::Lost`] naming
/// the winner — never a second `Won`, and never a silent success;
/// - the same response under the same key returns
/// [`Resolution::Replayed`], leaving the winner unchanged;
/// - a different response under an already-used key is
/// [`ResolutionError::IdempotencyConflict`];
/// - concurrent attempts from independent connections settle on ONE
/// winner, and every loser observes that same winner.