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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//! The two frame kinds a legacy stream carries, and the port that fills them.
//!
//! # Why the ring holds a type rather than a `Value`
//!
//! Because one of the two frames is not JSON. The `endpoint` event's data is
//! a bare URI, and `Value::String("/messages?sessionId=x")` would put quotes
//! in it — `data: "/messages?sessionId=x"` — which is not a URI and is not
//! what the specification says to send. A client doing the specified thing
//! with the event hands that to a URL parser and fails.
//!
//! So the payload is an enum and the *variant* picks the name, and it is
//! impossible to send a `message` event carrying a URI or an `endpoint` event
//! carrying JSON. That is the reason this is an enum rather than a
//! `MessageEndpoint` plus a `Value` in a struct: the struct would let a
//! caller pair the wrong one with the wrong name, and the mismatch would be
//! found by a peer in production.
//!
//! # Why the `endpoint` frame rides the ring
//!
//! Because the specification requires it to be the **first** event, and
//! ordering enforced by a struct field is a convention while ordering enforced
//! by the ring's insertion order is a guarantee. The router pushes the
//! `endpoint` frame before it opens the stream and before the session can
//! receive anything, so a `message` frame physically cannot precede it. A
//! router that prepended the frame to the response body instead would have
//! two things to keep in order — the frame and the stream — and one place to
//! get it wrong.
//!
//! The cost is that the frame is a ring entry, so it is replayed by a client
//! that reconnects. That is harmless and in fact useful: a reconnecting
//! client needs the endpoint again. The legacy transport has no `Last-Event-ID`
//! and no resume, so the ring is used here for ordering, not for replay.
use Value;
use crate;
use crateAuthContext;
use crateSessionId;
use crate;
/// One frame on a legacy stream.
/// A legacy session's server→client tail.
pub type LegacyTail = ;
/// One legacy session: the stream the server writes to, and whatever the
/// dispatcher keeps for it.
///
/// The store holds these, so a session that expires or is deleted takes its
/// dispatcher state with it. A dispatcher that kept its own table keyed by
/// session id would have no way to learn the session had gone.
/// What the protocol layer has to be able to do for the legacy `POST`.
///
/// A port, and it belongs here because the router is its consumer. One method,
/// because the router asks one question: *what should this client frame cause
/// the server to send back?*
///
/// It is not [`RequestHandler`](crate::adapters::primary::transport::http::RequestHandler),
/// and the difference is not cosmetic. That trait returns an HTTP `Response`,
/// which is right when a request's answer travels in the response. Here it
/// does not: the answer goes onto the SSE stream, and the `POST`'s own response
/// carries only a status. Handing the router a `Response` it has to take apart
/// to find the body, and push that body somewhere else, is a shape that invites
/// the mistake of forwarding the `POST`'s response body directly to the client
/// — which is exactly the bug the 2025-03-26 client fallback has to avoid.
///
/// Returns a `Vec` because one frame can produce a response *and* notifications
/// the server chose to send, and a single-output signature would force the
/// caller to pick one.
///
/// # Examples
///
/// ```
/// use protocol_droid::adapters::primary::transport::http::Refusal;
/// use protocol_droid::adapters::primary::transport::legacy_sse::{LegacyDispatcher, LegacyTail};
/// use protocol_droid::domain::McpSessionId;
/// use protocol_droid::domain::session::SessionId;
/// use protocol_droid::domain::registry::AuthContext;
///
/// /// Answers every request with the frame it was sent, which is all this test needs.
/// struct Echo;
///
/// impl LegacyDispatcher for Echo {
/// type Session = ();
///
/// fn open(&self, _id: SessionId, _tail: &LegacyTail) {}
///
/// async fn dispatch(
/// &self,
/// _auth: &AuthContext,
/// _id: &McpSessionId,
/// _session: &(),
/// frame: &serde_json::Value,
/// ) -> Result<Vec<serde_json::Value>, Refusal> {
/// Ok(vec![frame.clone()])
/// }
/// }
/// # let _ = Echo;
/// ```