Skip to main content

rivetkit_inspector_protocol/
versioned.rs

1use anyhow::{Result, bail};
2use serde_bare::Uint;
3use vbare::OwnedVersionedData;
4
5use crate::generated::{v1, v2, v3, v4, v5, v6};
6
7const WORKFLOW_HISTORY_DROPPED_ERROR: &str = "inspector.workflow_history_dropped";
8const QUEUE_DROPPED_ERROR: &str = "inspector.queue_dropped";
9const TRACE_DROPPED_ERROR: &str = "inspector.trace_dropped";
10const DATABASE_DROPPED_ERROR: &str = "inspector.database_dropped";
11const SCHEDULES_DROPPED_ERROR: &str = "inspector.schedules_dropped";
12
13pub enum ToServer {
14	V1(v1::ToServer),
15	V2(v2::ToServer),
16	V3(v3::ToServer),
17	V4(v4::ToServer),
18	V5(v5::ToServer),
19	V6(v6::ToServer),
20}
21
22impl OwnedVersionedData for ToServer {
23	type Latest = v6::ToServer;
24
25	fn wrap_latest(latest: Self::Latest) -> Self {
26		Self::V6(latest)
27	}
28
29	fn unwrap_latest(self) -> Result<Self::Latest> {
30		match self {
31			Self::V6(data) => Ok(data),
32			_ => bail!("version not latest"),
33		}
34	}
35
36	fn deserialize_version(payload: &[u8], version: u16) -> Result<Self> {
37		match version {
38			1 => Ok(Self::V1(serde_bare::from_slice(payload)?)),
39			2 => Ok(Self::V2(serde_bare::from_slice(payload)?)),
40			3 => Ok(Self::V3(serde_bare::from_slice(payload)?)),
41			4 => Ok(Self::V4(serde_bare::from_slice(payload)?)),
42			5 => Ok(Self::V5(serde_bare::from_slice(payload)?)),
43			6 => Ok(Self::V6(serde_bare::from_slice(payload)?)),
44			_ => bail!("invalid inspector protocol version for ToServer: {version}"),
45		}
46	}
47
48	fn serialize_version(self, version: u16) -> Result<Vec<u8>> {
49		match (self, version) {
50			(Self::V1(data), 1) => serde_bare::to_vec(&data).map_err(Into::into),
51			(Self::V2(data), 2) => serde_bare::to_vec(&data).map_err(Into::into),
52			(Self::V3(data), 3) => serde_bare::to_vec(&data).map_err(Into::into),
53			(Self::V4(data), 4) => serde_bare::to_vec(&data).map_err(Into::into),
54			(Self::V5(data), 5) => serde_bare::to_vec(&data).map_err(Into::into),
55			(Self::V6(data), 6) => serde_bare::to_vec(&data).map_err(Into::into),
56			(_, version) => bail!("unexpected inspector protocol version for ToServer: {version}"),
57		}
58	}
59
60	fn deserialize_converters() -> Vec<impl Fn(Self) -> Result<Self>> {
61		vec![
62			Self::v1_to_v2,
63			Self::v2_to_v3,
64			Self::v3_to_v4,
65			Self::v4_to_v5,
66			Self::v5_to_v6,
67		]
68	}
69
70	fn serialize_converters() -> Vec<impl Fn(Self) -> Result<Self>> {
71		vec![
72			Self::v6_to_v5,
73			Self::v5_to_v4,
74			Self::v4_to_v3,
75			Self::v3_to_v2,
76			Self::v2_to_v1,
77		]
78	}
79}
80
81impl ToServer {
82	fn v1_to_v2(self) -> Result<Self> {
83		let Self::V1(data) = self else {
84			bail!("expected inspector protocol v1 ToServer")
85		};
86
87		let body = match data.body {
88			v1::ToServerBody::PatchStateRequest(req) => {
89				v2::ToServerBody::PatchStateRequest(req.into())
90			}
91			v1::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()),
92			v1::ToServerBody::ConnectionsRequest(req) => {
93				v2::ToServerBody::ConnectionsRequest(req.into())
94			}
95			v1::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()),
96			v1::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()),
97			v1::ToServerBody::EventsRequest(_) | v1::ToServerBody::ClearEventsRequest(_) => {
98				bail!("cannot convert inspector v1 events requests to v2")
99			}
100		};
101
102		Ok(Self::V2(v2::ToServer { body }))
103	}
104
105	fn v2_to_v3(self) -> Result<Self> {
106		let Self::V2(data) = self else {
107			bail!("expected inspector protocol v2 ToServer")
108		};
109		Ok(Self::V3(data.into()))
110	}
111
112	fn v3_to_v4(self) -> Result<Self> {
113		let Self::V3(data) = self else {
114			bail!("expected inspector protocol v3 ToServer")
115		};
116
117		let body = match data.body {
118			v3::ToServerBody::PatchStateRequest(req) => {
119				v4::ToServerBody::PatchStateRequest(req.into())
120			}
121			v3::ToServerBody::StateRequest(req) => v4::ToServerBody::StateRequest(req.into()),
122			v3::ToServerBody::ConnectionsRequest(req) => {
123				v4::ToServerBody::ConnectionsRequest(req.into())
124			}
125			v3::ToServerBody::ActionRequest(req) => v4::ToServerBody::ActionRequest(req.into()),
126			v3::ToServerBody::RpcsListRequest(req) => v4::ToServerBody::RpcsListRequest(req.into()),
127			v3::ToServerBody::TraceQueryRequest(req) => {
128				v4::ToServerBody::TraceQueryRequest(req.into())
129			}
130			v3::ToServerBody::QueueRequest(req) => v4::ToServerBody::QueueRequest(req.into()),
131			v3::ToServerBody::WorkflowHistoryRequest(req) => {
132				v4::ToServerBody::WorkflowHistoryRequest(req.into())
133			}
134			v3::ToServerBody::DatabaseSchemaRequest(req) => {
135				v4::ToServerBody::DatabaseSchemaRequest(req.into())
136			}
137			v3::ToServerBody::DatabaseTableRowsRequest(req) => {
138				v4::ToServerBody::DatabaseTableRowsRequest(req.into())
139			}
140		};
141
142		Ok(Self::V4(v4::ToServer { body }))
143	}
144
145	fn v4_to_v5(self) -> Result<Self> {
146		let Self::V4(data) = self else {
147			bail!("expected inspector protocol v4 ToServer")
148		};
149		Ok(Self::V5(data.into()))
150	}
151
152	fn v5_to_v6(self) -> Result<Self> {
153		let Self::V5(data) = self else {
154			bail!("expected inspector protocol v5 ToServer")
155		};
156		Ok(Self::V6(data.into()))
157	}
158
159	fn v6_to_v5(self) -> Result<Self> {
160		let Self::V6(data) = self else {
161			bail!("expected inspector protocol v6 ToServer")
162		};
163
164		let body = match data.body {
165			v6::ToServerBody::PatchStateRequest(req) => {
166				v5::ToServerBody::PatchStateRequest(req.into())
167			}
168			v6::ToServerBody::StateRequest(req) => v5::ToServerBody::StateRequest(req.into()),
169			v6::ToServerBody::ConnectionsRequest(req) => {
170				v5::ToServerBody::ConnectionsRequest(req.into())
171			}
172			v6::ToServerBody::ActionRequest(req) => v5::ToServerBody::ActionRequest(req.into()),
173			v6::ToServerBody::RpcsListRequest(req) => v5::ToServerBody::RpcsListRequest(req.into()),
174			v6::ToServerBody::TraceQueryRequest(req) => {
175				v5::ToServerBody::TraceQueryRequest(req.into())
176			}
177			v6::ToServerBody::QueueRequest(req) => v5::ToServerBody::QueueRequest(req.into()),
178			v6::ToServerBody::WorkflowHistoryRequest(req) => {
179				v5::ToServerBody::WorkflowHistoryRequest(req.into())
180			}
181			v6::ToServerBody::WorkflowReplayRequest(req) => {
182				v5::ToServerBody::WorkflowReplayRequest(req.into())
183			}
184			v6::ToServerBody::DatabaseSchemaRequest(req) => {
185				v5::ToServerBody::DatabaseSchemaRequest(req.into())
186			}
187			v6::ToServerBody::DatabaseTableRowsRequest(req) => {
188				v5::ToServerBody::DatabaseTableRowsRequest(req.into())
189			}
190			v6::ToServerBody::SchedulesRequest(_)
191			| v6::ToServerBody::ScheduleHistoryRequest(_)
192			| v6::ToServerBody::ScheduleDeleteRequest(_) => {
193				bail!("cannot convert inspector v6 schedule requests to v5")
194			}
195		};
196
197		Ok(Self::V5(v5::ToServer { body }))
198	}
199
200	fn v5_to_v4(self) -> Result<Self> {
201		let Self::V5(data) = self else {
202			bail!("expected inspector protocol v5 ToServer")
203		};
204		Ok(Self::V4(data.into()))
205	}
206
207	fn v4_to_v3(self) -> Result<Self> {
208		let Self::V4(data) = self else {
209			bail!("expected inspector protocol v4 ToServer")
210		};
211
212		let body = match data.body {
213			v4::ToServerBody::PatchStateRequest(req) => {
214				v3::ToServerBody::PatchStateRequest(req.into())
215			}
216			v4::ToServerBody::StateRequest(req) => v3::ToServerBody::StateRequest(req.into()),
217			v4::ToServerBody::ConnectionsRequest(req) => {
218				v3::ToServerBody::ConnectionsRequest(req.into())
219			}
220			v4::ToServerBody::ActionRequest(req) => v3::ToServerBody::ActionRequest(req.into()),
221			v4::ToServerBody::RpcsListRequest(req) => v3::ToServerBody::RpcsListRequest(req.into()),
222			v4::ToServerBody::TraceQueryRequest(req) => {
223				v3::ToServerBody::TraceQueryRequest(req.into())
224			}
225			v4::ToServerBody::QueueRequest(req) => v3::ToServerBody::QueueRequest(req.into()),
226			v4::ToServerBody::WorkflowHistoryRequest(req) => {
227				v3::ToServerBody::WorkflowHistoryRequest(req.into())
228			}
229			v4::ToServerBody::WorkflowReplayRequest(_) => {
230				bail!("cannot convert inspector v4 workflow replay requests to v3")
231			}
232			v4::ToServerBody::DatabaseSchemaRequest(req) => {
233				v3::ToServerBody::DatabaseSchemaRequest(req.into())
234			}
235			v4::ToServerBody::DatabaseTableRowsRequest(req) => {
236				v3::ToServerBody::DatabaseTableRowsRequest(req.into())
237			}
238		};
239
240		Ok(Self::V3(v3::ToServer { body }))
241	}
242
243	fn v3_to_v2(self) -> Result<Self> {
244		let Self::V3(data) = self else {
245			bail!("expected inspector protocol v3 ToServer")
246		};
247
248		let body = match data.body {
249			v3::ToServerBody::PatchStateRequest(req) => {
250				v2::ToServerBody::PatchStateRequest(req.into())
251			}
252			v3::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()),
253			v3::ToServerBody::ConnectionsRequest(req) => {
254				v2::ToServerBody::ConnectionsRequest(req.into())
255			}
256			v3::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()),
257			v3::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()),
258			v3::ToServerBody::TraceQueryRequest(req) => {
259				v2::ToServerBody::TraceQueryRequest(req.into())
260			}
261			v3::ToServerBody::QueueRequest(req) => v2::ToServerBody::QueueRequest(req.into()),
262			v3::ToServerBody::WorkflowHistoryRequest(req) => {
263				v2::ToServerBody::WorkflowHistoryRequest(req.into())
264			}
265			v3::ToServerBody::DatabaseSchemaRequest(_)
266			| v3::ToServerBody::DatabaseTableRowsRequest(_) => {
267				bail!("cannot convert inspector v3 database requests to v2")
268			}
269		};
270
271		Ok(Self::V2(v2::ToServer { body }))
272	}
273
274	fn v2_to_v1(self) -> Result<Self> {
275		let Self::V2(data) = self else {
276			bail!("expected inspector protocol v2 ToServer")
277		};
278
279		let body = match data.body {
280			v2::ToServerBody::PatchStateRequest(req) => {
281				v1::ToServerBody::PatchStateRequest(req.into())
282			}
283			v2::ToServerBody::StateRequest(req) => v1::ToServerBody::StateRequest(req.into()),
284			v2::ToServerBody::ConnectionsRequest(req) => {
285				v1::ToServerBody::ConnectionsRequest(req.into())
286			}
287			v2::ToServerBody::ActionRequest(req) => v1::ToServerBody::ActionRequest(req.into()),
288			v2::ToServerBody::RpcsListRequest(req) => v1::ToServerBody::RpcsListRequest(req.into()),
289			v2::ToServerBody::TraceQueryRequest(_)
290			| v2::ToServerBody::QueueRequest(_)
291			| v2::ToServerBody::WorkflowHistoryRequest(_) => {
292				bail!("cannot convert inspector v2 queue/trace/workflow requests to v1")
293			}
294		};
295
296		Ok(Self::V1(v1::ToServer { body }))
297	}
298}
299
300pub enum ToClient {
301	V1(v1::ToClient),
302	V2(v2::ToClient),
303	V3(v3::ToClient),
304	V4(v4::ToClient),
305	V5(v5::ToClient),
306	V6(v6::ToClient),
307}
308
309impl OwnedVersionedData for ToClient {
310	type Latest = v6::ToClient;
311
312	fn wrap_latest(latest: Self::Latest) -> Self {
313		Self::V6(latest)
314	}
315
316	fn unwrap_latest(self) -> Result<Self::Latest> {
317		match self {
318			Self::V6(data) => Ok(data),
319			_ => bail!("version not latest"),
320		}
321	}
322
323	fn deserialize_version(payload: &[u8], version: u16) -> Result<Self> {
324		match version {
325			1 => Ok(Self::V1(serde_bare::from_slice(payload)?)),
326			2 => Ok(Self::V2(serde_bare::from_slice(payload)?)),
327			3 => Ok(Self::V3(serde_bare::from_slice(payload)?)),
328			4 => Ok(Self::V4(serde_bare::from_slice(payload)?)),
329			5 => Ok(Self::V5(serde_bare::from_slice(payload)?)),
330			6 => Ok(Self::V6(serde_bare::from_slice(payload)?)),
331			_ => bail!("invalid inspector protocol version for ToClient: {version}"),
332		}
333	}
334
335	fn serialize_version(self, version: u16) -> Result<Vec<u8>> {
336		match (self, version) {
337			(Self::V1(data), 1) => serde_bare::to_vec(&data).map_err(Into::into),
338			(Self::V2(data), 2) => serde_bare::to_vec(&data).map_err(Into::into),
339			(Self::V3(data), 3) => serde_bare::to_vec(&data).map_err(Into::into),
340			(Self::V4(data), 4) => serde_bare::to_vec(&data).map_err(Into::into),
341			(Self::V5(data), 5) => serde_bare::to_vec(&data).map_err(Into::into),
342			(Self::V6(data), 6) => serde_bare::to_vec(&data).map_err(Into::into),
343			(_, version) => bail!("unexpected inspector protocol version for ToClient: {version}"),
344		}
345	}
346
347	fn deserialize_converters() -> Vec<impl Fn(Self) -> Result<Self>> {
348		vec![
349			Self::v1_to_v2,
350			Self::v2_to_v3,
351			Self::v3_to_v4,
352			Self::v4_to_v5,
353			Self::v5_to_v6,
354		]
355	}
356
357	fn serialize_converters() -> Vec<impl Fn(Self) -> Result<Self>> {
358		vec![
359			Self::v6_to_v5,
360			Self::v5_to_v4,
361			Self::v4_to_v3,
362			Self::v3_to_v2,
363			Self::v2_to_v1,
364		]
365	}
366}
367
368impl ToClient {
369	fn v1_to_v2(self) -> Result<Self> {
370		let Self::V1(data) = self else {
371			bail!("expected inspector protocol v1 ToClient")
372		};
373
374		let body = match data.body {
375			v1::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()),
376			v1::ToClientBody::ConnectionsResponse(resp) => {
377				v2::ToClientBody::ConnectionsResponse(resp.into())
378			}
379			v1::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()),
380			v1::ToClientBody::RpcsListResponse(resp) => {
381				v2::ToClientBody::RpcsListResponse(resp.into())
382			}
383			v1::ToClientBody::ConnectionsUpdated(update) => {
384				v2::ToClientBody::ConnectionsUpdated(update.into())
385			}
386			v1::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()),
387			v1::ToClientBody::Error(error) => v2::ToClientBody::Error(error.into()),
388			v1::ToClientBody::Init(init) => v2::ToClientBody::Init(v2::Init {
389				connections: convert_vec(init.connections),
390				state: init.state,
391				is_state_enabled: init.is_state_enabled,
392				rpcs: init.rpcs,
393				is_database_enabled: init.is_database_enabled,
394				queue_size: Uint(0),
395				workflow_history: None,
396				is_workflow_enabled: false,
397			}),
398			v1::ToClientBody::EventsResponse(_) | v1::ToClientBody::EventsUpdated(_) => {
399				bail!("cannot convert inspector v1 events responses to v2")
400			}
401		};
402
403		Ok(Self::V2(v2::ToClient { body }))
404	}
405
406	fn v2_to_v3(self) -> Result<Self> {
407		let Self::V2(data) = self else {
408			bail!("expected inspector protocol v2 ToClient")
409		};
410		Ok(Self::V3(data.into()))
411	}
412
413	fn v3_to_v4(self) -> Result<Self> {
414		let Self::V3(data) = self else {
415			bail!("expected inspector protocol v3 ToClient")
416		};
417
418		let body = match data.body {
419			v3::ToClientBody::StateResponse(resp) => v4::ToClientBody::StateResponse(resp.into()),
420			v3::ToClientBody::ConnectionsResponse(resp) => {
421				v4::ToClientBody::ConnectionsResponse(resp.into())
422			}
423			v3::ToClientBody::ActionResponse(resp) => v4::ToClientBody::ActionResponse(resp.into()),
424			v3::ToClientBody::ConnectionsUpdated(update) => {
425				v4::ToClientBody::ConnectionsUpdated(update.into())
426			}
427			v3::ToClientBody::QueueUpdated(update) => v4::ToClientBody::QueueUpdated(update.into()),
428			v3::ToClientBody::StateUpdated(update) => v4::ToClientBody::StateUpdated(update.into()),
429			v3::ToClientBody::WorkflowHistoryUpdated(update) => {
430				v4::ToClientBody::WorkflowHistoryUpdated(update.into())
431			}
432			v3::ToClientBody::RpcsListResponse(resp) => {
433				v4::ToClientBody::RpcsListResponse(resp.into())
434			}
435			v3::ToClientBody::TraceQueryResponse(resp) => {
436				v4::ToClientBody::TraceQueryResponse(resp.into())
437			}
438			v3::ToClientBody::QueueResponse(resp) => v4::ToClientBody::QueueResponse(resp.into()),
439			v3::ToClientBody::WorkflowHistoryResponse(resp) => {
440				v4::ToClientBody::WorkflowHistoryResponse(resp.into())
441			}
442			v3::ToClientBody::Error(error) => v4::ToClientBody::Error(error.into()),
443			v3::ToClientBody::Init(init) => v4::ToClientBody::Init(init.into()),
444			v3::ToClientBody::DatabaseSchemaResponse(resp) => {
445				v4::ToClientBody::DatabaseSchemaResponse(resp.into())
446			}
447			v3::ToClientBody::DatabaseTableRowsResponse(resp) => {
448				v4::ToClientBody::DatabaseTableRowsResponse(resp.into())
449			}
450		};
451
452		Ok(Self::V4(v4::ToClient { body }))
453	}
454
455	fn v4_to_v5(self) -> Result<Self> {
456		let Self::V4(data) = self else {
457			bail!("expected inspector protocol v4 ToClient")
458		};
459
460		let body = match data.body {
461			v4::ToClientBody::StateResponse(resp) => v5::ToClientBody::StateResponse(resp.into()),
462			v4::ToClientBody::ConnectionsResponse(resp) => {
463				v5::ToClientBody::ConnectionsResponse(resp.into())
464			}
465			v4::ToClientBody::ActionResponse(resp) => v5::ToClientBody::ActionResponse(resp.into()),
466			v4::ToClientBody::ConnectionsUpdated(update) => {
467				v5::ToClientBody::ConnectionsUpdated(update.into())
468			}
469			v4::ToClientBody::QueueUpdated(update) => v5::ToClientBody::QueueUpdated(update.into()),
470			v4::ToClientBody::StateUpdated(update) => v5::ToClientBody::StateUpdated(update.into()),
471			v4::ToClientBody::WorkflowHistoryUpdated(update) => {
472				v5::ToClientBody::WorkflowHistoryUpdated(update.into())
473			}
474			v4::ToClientBody::RpcsListResponse(resp) => {
475				v5::ToClientBody::RpcsListResponse(resp.into())
476			}
477			v4::ToClientBody::TraceQueryResponse(resp) => {
478				v5::ToClientBody::TraceQueryResponse(resp.into())
479			}
480			v4::ToClientBody::QueueResponse(resp) => v5::ToClientBody::QueueResponse(resp.into()),
481			v4::ToClientBody::WorkflowHistoryResponse(resp) => {
482				v5::ToClientBody::WorkflowHistoryResponse(resp.into())
483			}
484			v4::ToClientBody::WorkflowReplayResponse(resp) => {
485				v5::ToClientBody::WorkflowReplayResponse(resp.into())
486			}
487			v4::ToClientBody::Error(error) => v5::ToClientBody::Error(error.into()),
488			// An older server that only spoke v4 has no tab config to report,
489			// so upgrade to an empty list. Clients treating this as "no custom
490			// tabs" is correct for a pre-tab-config-in-init runtime.
491			v4::ToClientBody::Init(init) => v5::ToClientBody::Init(v5::Init {
492				connections: convert_vec(init.connections),
493				state: init.state,
494				is_state_enabled: init.is_state_enabled,
495				rpcs: init.rpcs,
496				is_database_enabled: init.is_database_enabled,
497				queue_size: init.queue_size,
498				workflow_history: init.workflow_history,
499				is_workflow_enabled: init.is_workflow_enabled,
500				tab_config: Vec::new(),
501			}),
502			v4::ToClientBody::DatabaseSchemaResponse(resp) => {
503				v5::ToClientBody::DatabaseSchemaResponse(resp.into())
504			}
505			v4::ToClientBody::DatabaseTableRowsResponse(resp) => {
506				v5::ToClientBody::DatabaseTableRowsResponse(resp.into())
507			}
508		};
509
510		Ok(Self::V5(v5::ToClient { body }))
511	}
512
513	fn v5_to_v6(self) -> Result<Self> {
514		let Self::V5(data) = self else {
515			bail!("expected inspector protocol v5 ToClient")
516		};
517
518		let body = match data.body {
519			v5::ToClientBody::StateResponse(resp) => v6::ToClientBody::StateResponse(resp.into()),
520			v5::ToClientBody::ConnectionsResponse(resp) => {
521				v6::ToClientBody::ConnectionsResponse(resp.into())
522			}
523			v5::ToClientBody::ActionResponse(resp) => v6::ToClientBody::ActionResponse(resp.into()),
524			v5::ToClientBody::ConnectionsUpdated(update) => {
525				v6::ToClientBody::ConnectionsUpdated(update.into())
526			}
527			v5::ToClientBody::QueueUpdated(update) => v6::ToClientBody::QueueUpdated(update.into()),
528			v5::ToClientBody::StateUpdated(update) => v6::ToClientBody::StateUpdated(update.into()),
529			v5::ToClientBody::WorkflowHistoryUpdated(update) => {
530				v6::ToClientBody::WorkflowHistoryUpdated(update.into())
531			}
532			v5::ToClientBody::RpcsListResponse(resp) => {
533				v6::ToClientBody::RpcsListResponse(resp.into())
534			}
535			v5::ToClientBody::TraceQueryResponse(resp) => {
536				v6::ToClientBody::TraceQueryResponse(resp.into())
537			}
538			v5::ToClientBody::QueueResponse(resp) => v6::ToClientBody::QueueResponse(resp.into()),
539			v5::ToClientBody::WorkflowHistoryResponse(resp) => {
540				v6::ToClientBody::WorkflowHistoryResponse(resp.into())
541			}
542			v5::ToClientBody::WorkflowReplayResponse(resp) => {
543				v6::ToClientBody::WorkflowReplayResponse(resp.into())
544			}
545			v5::ToClientBody::Error(error) => v6::ToClientBody::Error(error.into()),
546			v5::ToClientBody::Init(init) => v6::ToClientBody::Init(v6::Init {
547				connections: convert_vec(init.connections),
548				state: init.state,
549				is_state_enabled: init.is_state_enabled,
550				rpcs: init.rpcs,
551				is_database_enabled: init.is_database_enabled,
552				queue_size: init.queue_size,
553				workflow_history: init.workflow_history,
554				is_workflow_enabled: init.is_workflow_enabled,
555				tab_config: convert_vec(init.tab_config),
556				schedules: Vec::new(),
557			}),
558			v5::ToClientBody::DatabaseSchemaResponse(resp) => {
559				v6::ToClientBody::DatabaseSchemaResponse(resp.into())
560			}
561			v5::ToClientBody::DatabaseTableRowsResponse(resp) => {
562				v6::ToClientBody::DatabaseTableRowsResponse(resp.into())
563			}
564		};
565
566		Ok(Self::V6(v6::ToClient { body }))
567	}
568
569	fn v6_to_v5(self) -> Result<Self> {
570		let Self::V6(data) = self else {
571			bail!("expected inspector protocol v6 ToClient")
572		};
573
574		let body = match data.body {
575			v6::ToClientBody::StateResponse(resp) => v5::ToClientBody::StateResponse(resp.into()),
576			v6::ToClientBody::ConnectionsResponse(resp) => {
577				v5::ToClientBody::ConnectionsResponse(resp.into())
578			}
579			v6::ToClientBody::ActionResponse(resp) => v5::ToClientBody::ActionResponse(resp.into()),
580			v6::ToClientBody::ConnectionsUpdated(update) => {
581				v5::ToClientBody::ConnectionsUpdated(update.into())
582			}
583			v6::ToClientBody::QueueUpdated(update) => v5::ToClientBody::QueueUpdated(update.into()),
584			v6::ToClientBody::StateUpdated(update) => v5::ToClientBody::StateUpdated(update.into()),
585			v6::ToClientBody::WorkflowHistoryUpdated(update) => {
586				v5::ToClientBody::WorkflowHistoryUpdated(update.into())
587			}
588			v6::ToClientBody::RpcsListResponse(resp) => {
589				v5::ToClientBody::RpcsListResponse(resp.into())
590			}
591			v6::ToClientBody::TraceQueryResponse(resp) => {
592				v5::ToClientBody::TraceQueryResponse(resp.into())
593			}
594			v6::ToClientBody::QueueResponse(resp) => v5::ToClientBody::QueueResponse(resp.into()),
595			v6::ToClientBody::WorkflowHistoryResponse(resp) => {
596				v5::ToClientBody::WorkflowHistoryResponse(resp.into())
597			}
598			v6::ToClientBody::WorkflowReplayResponse(resp) => {
599				v5::ToClientBody::WorkflowReplayResponse(resp.into())
600			}
601			v6::ToClientBody::Error(error) => v5::ToClientBody::Error(error.into()),
602			v6::ToClientBody::Init(init) => v5::ToClientBody::Init(v5::Init {
603				connections: convert_vec(init.connections),
604				state: init.state,
605				is_state_enabled: init.is_state_enabled,
606				rpcs: init.rpcs,
607				is_database_enabled: init.is_database_enabled,
608				queue_size: init.queue_size,
609				workflow_history: init.workflow_history,
610				is_workflow_enabled: init.is_workflow_enabled,
611				tab_config: convert_vec(init.tab_config),
612			}),
613			v6::ToClientBody::DatabaseSchemaResponse(resp) => {
614				v5::ToClientBody::DatabaseSchemaResponse(resp.into())
615			}
616			v6::ToClientBody::DatabaseTableRowsResponse(resp) => {
617				v5::ToClientBody::DatabaseTableRowsResponse(resp.into())
618			}
619			v6::ToClientBody::SchedulesUpdated(_)
620			| v6::ToClientBody::SchedulesResponse(_)
621			| v6::ToClientBody::ScheduleHistoryResponse(_)
622			| v6::ToClientBody::ScheduleDeleteResponse(_) => v5::ToClientBody::Error(v5::Error {
623				message: SCHEDULES_DROPPED_ERROR.to_owned(),
624			}),
625		};
626
627		Ok(Self::V5(v5::ToClient { body }))
628	}
629
630	fn v5_to_v4(self) -> Result<Self> {
631		let Self::V5(data) = self else {
632			bail!("expected inspector protocol v5 ToClient")
633		};
634
635		let body = match data.body {
636			v5::ToClientBody::StateResponse(resp) => v4::ToClientBody::StateResponse(resp.into()),
637			v5::ToClientBody::ConnectionsResponse(resp) => {
638				v4::ToClientBody::ConnectionsResponse(resp.into())
639			}
640			v5::ToClientBody::ActionResponse(resp) => v4::ToClientBody::ActionResponse(resp.into()),
641			v5::ToClientBody::ConnectionsUpdated(update) => {
642				v4::ToClientBody::ConnectionsUpdated(update.into())
643			}
644			v5::ToClientBody::QueueUpdated(update) => v4::ToClientBody::QueueUpdated(update.into()),
645			v5::ToClientBody::StateUpdated(update) => v4::ToClientBody::StateUpdated(update.into()),
646			v5::ToClientBody::WorkflowHistoryUpdated(update) => {
647				v4::ToClientBody::WorkflowHistoryUpdated(update.into())
648			}
649			v5::ToClientBody::RpcsListResponse(resp) => {
650				v4::ToClientBody::RpcsListResponse(resp.into())
651			}
652			v5::ToClientBody::TraceQueryResponse(resp) => {
653				v4::ToClientBody::TraceQueryResponse(resp.into())
654			}
655			v5::ToClientBody::QueueResponse(resp) => v4::ToClientBody::QueueResponse(resp.into()),
656			v5::ToClientBody::WorkflowHistoryResponse(resp) => {
657				v4::ToClientBody::WorkflowHistoryResponse(resp.into())
658			}
659			v5::ToClientBody::WorkflowReplayResponse(resp) => {
660				v4::ToClientBody::WorkflowReplayResponse(resp.into())
661			}
662			v5::ToClientBody::Error(error) => v4::ToClientBody::Error(error.into()),
663			// v4 has no tab-config field; drop it. A v4 client falls back to
664			// the `/inspector/tab-config` HTTP fetch for the same data.
665			v5::ToClientBody::Init(init) => v4::ToClientBody::Init(v4::Init {
666				connections: convert_vec(init.connections),
667				state: init.state,
668				is_state_enabled: init.is_state_enabled,
669				rpcs: init.rpcs,
670				is_database_enabled: init.is_database_enabled,
671				queue_size: init.queue_size,
672				workflow_history: init.workflow_history,
673				is_workflow_enabled: init.is_workflow_enabled,
674			}),
675			v5::ToClientBody::DatabaseSchemaResponse(resp) => {
676				v4::ToClientBody::DatabaseSchemaResponse(resp.into())
677			}
678			v5::ToClientBody::DatabaseTableRowsResponse(resp) => {
679				v4::ToClientBody::DatabaseTableRowsResponse(resp.into())
680			}
681		};
682
683		Ok(Self::V4(v4::ToClient { body }))
684	}
685
686	fn v4_to_v3(self) -> Result<Self> {
687		let Self::V4(data) = self else {
688			bail!("expected inspector protocol v4 ToClient")
689		};
690
691		let body = match data.body {
692			v4::ToClientBody::StateResponse(resp) => v3::ToClientBody::StateResponse(resp.into()),
693			v4::ToClientBody::ConnectionsResponse(resp) => {
694				v3::ToClientBody::ConnectionsResponse(resp.into())
695			}
696			v4::ToClientBody::ActionResponse(resp) => v3::ToClientBody::ActionResponse(resp.into()),
697			v4::ToClientBody::ConnectionsUpdated(update) => {
698				v3::ToClientBody::ConnectionsUpdated(update.into())
699			}
700			v4::ToClientBody::QueueUpdated(update) => v3::ToClientBody::QueueUpdated(update.into()),
701			v4::ToClientBody::StateUpdated(update) => v3::ToClientBody::StateUpdated(update.into()),
702			v4::ToClientBody::WorkflowHistoryUpdated(update) => {
703				v3::ToClientBody::WorkflowHistoryUpdated(update.into())
704			}
705			v4::ToClientBody::RpcsListResponse(resp) => {
706				v3::ToClientBody::RpcsListResponse(resp.into())
707			}
708			v4::ToClientBody::TraceQueryResponse(resp) => {
709				v3::ToClientBody::TraceQueryResponse(resp.into())
710			}
711			v4::ToClientBody::QueueResponse(resp) => v3::ToClientBody::QueueResponse(resp.into()),
712			v4::ToClientBody::WorkflowHistoryResponse(resp) => {
713				v3::ToClientBody::WorkflowHistoryResponse(resp.into())
714			}
715			v4::ToClientBody::WorkflowReplayResponse(_) => {
716				v3::ToClientBody::Error(dropped_error(WORKFLOW_HISTORY_DROPPED_ERROR).into())
717			}
718			v4::ToClientBody::Error(error) => v3::ToClientBody::Error(error.into()),
719			v4::ToClientBody::Init(init) => v3::ToClientBody::Init(init.into()),
720			v4::ToClientBody::DatabaseSchemaResponse(resp) => {
721				v3::ToClientBody::DatabaseSchemaResponse(resp.into())
722			}
723			v4::ToClientBody::DatabaseTableRowsResponse(resp) => {
724				v3::ToClientBody::DatabaseTableRowsResponse(resp.into())
725			}
726		};
727
728		Ok(Self::V3(v3::ToClient { body }))
729	}
730
731	fn v3_to_v2(self) -> Result<Self> {
732		let Self::V3(data) = self else {
733			bail!("expected inspector protocol v3 ToClient")
734		};
735
736		let body = match data.body {
737			v3::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()),
738			v3::ToClientBody::ConnectionsResponse(resp) => {
739				v2::ToClientBody::ConnectionsResponse(resp.into())
740			}
741			v3::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()),
742			v3::ToClientBody::ConnectionsUpdated(update) => {
743				v2::ToClientBody::ConnectionsUpdated(update.into())
744			}
745			v3::ToClientBody::QueueUpdated(update) => v2::ToClientBody::QueueUpdated(update.into()),
746			v3::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()),
747			v3::ToClientBody::WorkflowHistoryUpdated(update) => {
748				v2::ToClientBody::WorkflowHistoryUpdated(update.into())
749			}
750			v3::ToClientBody::RpcsListResponse(resp) => {
751				v2::ToClientBody::RpcsListResponse(resp.into())
752			}
753			v3::ToClientBody::TraceQueryResponse(resp) => {
754				v2::ToClientBody::TraceQueryResponse(resp.into())
755			}
756			v3::ToClientBody::QueueResponse(resp) => v2::ToClientBody::QueueResponse(resp.into()),
757			v3::ToClientBody::WorkflowHistoryResponse(resp) => {
758				v2::ToClientBody::WorkflowHistoryResponse(resp.into())
759			}
760			v3::ToClientBody::Error(error) => v2::ToClientBody::Error(error.into()),
761			v3::ToClientBody::Init(init) => v2::ToClientBody::Init(init.into()),
762			v3::ToClientBody::DatabaseSchemaResponse(_)
763			| v3::ToClientBody::DatabaseTableRowsResponse(_) => {
764				v2::ToClientBody::Error(dropped_error(DATABASE_DROPPED_ERROR))
765			}
766		};
767
768		Ok(Self::V2(v2::ToClient { body }))
769	}
770
771	fn v2_to_v1(self) -> Result<Self> {
772		let Self::V2(data) = self else {
773			bail!("expected inspector protocol v2 ToClient")
774		};
775
776		let body = match data.body {
777			v2::ToClientBody::StateResponse(resp) => v1::ToClientBody::StateResponse(resp.into()),
778			v2::ToClientBody::ConnectionsResponse(resp) => {
779				v1::ToClientBody::ConnectionsResponse(resp.into())
780			}
781			v2::ToClientBody::ActionResponse(resp) => v1::ToClientBody::ActionResponse(resp.into()),
782			v2::ToClientBody::ConnectionsUpdated(update) => {
783				v1::ToClientBody::ConnectionsUpdated(update.into())
784			}
785			v2::ToClientBody::StateUpdated(update) => v1::ToClientBody::StateUpdated(update.into()),
786			v2::ToClientBody::RpcsListResponse(resp) => {
787				v1::ToClientBody::RpcsListResponse(resp.into())
788			}
789			v2::ToClientBody::Error(error) => v1::ToClientBody::Error(error.into()),
790			v2::ToClientBody::Init(init) => v1::ToClientBody::Init(v1::Init {
791				connections: init.connections.into_iter().map(Into::into).collect(),
792				events: Vec::new(),
793				state: init.state,
794				is_state_enabled: init.is_state_enabled,
795				rpcs: init.rpcs,
796				is_database_enabled: init.is_database_enabled,
797			}),
798			v2::ToClientBody::QueueUpdated(_) | v2::ToClientBody::QueueResponse(_) => {
799				v1::ToClientBody::Error(dropped_error(QUEUE_DROPPED_ERROR).into())
800			}
801			v2::ToClientBody::WorkflowHistoryUpdated(_)
802			| v2::ToClientBody::WorkflowHistoryResponse(_) => {
803				v1::ToClientBody::Error(dropped_error(WORKFLOW_HISTORY_DROPPED_ERROR).into())
804			}
805			v2::ToClientBody::TraceQueryResponse(_) => {
806				v1::ToClientBody::Error(dropped_error(TRACE_DROPPED_ERROR).into())
807			}
808		};
809
810		Ok(Self::V1(v1::ToClient { body }))
811	}
812}
813
814fn convert_vec<From, To>(values: Vec<From>) -> Vec<To>
815where
816	From: Into<To>,
817{
818	values.into_iter().map(Into::into).collect()
819}
820
821macro_rules! impl_same_fields_pair {
822	($left:ident, $right:ident, $ty:ident { $($field:ident),+ $(,)? }) => {
823		impl From<$left::$ty> for $right::$ty {
824			fn from(value: $left::$ty) -> Self {
825				Self {
826					$($field: value.$field),+
827				}
828			}
829		}
830
831		impl From<$right::$ty> for $left::$ty {
832			fn from(value: $right::$ty) -> Self {
833				Self {
834					$($field: value.$field),+
835				}
836			}
837		}
838	};
839}
840
841macro_rules! impl_connection_list_pair {
842	($left:ident, $right:ident, $ty:ident) => {
843		impl From<$left::$ty> for $right::$ty {
844			fn from(value: $left::$ty) -> Self {
845				Self {
846					connections: convert_vec(value.connections),
847				}
848			}
849		}
850
851		impl From<$right::$ty> for $left::$ty {
852			fn from(value: $right::$ty) -> Self {
853				Self {
854					connections: convert_vec(value.connections),
855				}
856			}
857		}
858	};
859}
860
861macro_rules! impl_connections_response_pair {
862	($left:ident, $right:ident) => {
863		impl From<$left::ConnectionsResponse> for $right::ConnectionsResponse {
864			fn from(value: $left::ConnectionsResponse) -> Self {
865				Self {
866					rid: value.rid,
867					connections: convert_vec(value.connections),
868				}
869			}
870		}
871
872		impl From<$right::ConnectionsResponse> for $left::ConnectionsResponse {
873			fn from(value: $right::ConnectionsResponse) -> Self {
874				Self {
875					rid: value.rid,
876					connections: convert_vec(value.connections),
877				}
878			}
879		}
880	};
881}
882
883macro_rules! impl_queue_status_pair {
884	($left:ident, $right:ident) => {
885		impl From<$left::QueueStatus> for $right::QueueStatus {
886			fn from(value: $left::QueueStatus) -> Self {
887				Self {
888					size: value.size,
889					max_size: value.max_size,
890					messages: convert_vec(value.messages),
891					truncated: value.truncated,
892				}
893			}
894		}
895
896		impl From<$right::QueueStatus> for $left::QueueStatus {
897			fn from(value: $right::QueueStatus) -> Self {
898				Self {
899					size: value.size,
900					max_size: value.max_size,
901					messages: convert_vec(value.messages),
902					truncated: value.truncated,
903				}
904			}
905		}
906	};
907}
908
909macro_rules! impl_queue_response_pair {
910	($left:ident, $right:ident) => {
911		impl From<$left::QueueResponse> for $right::QueueResponse {
912			fn from(value: $left::QueueResponse) -> Self {
913				Self {
914					rid: value.rid,
915					status: value.status.into(),
916				}
917			}
918		}
919
920		impl From<$right::QueueResponse> for $left::QueueResponse {
921			fn from(value: $right::QueueResponse) -> Self {
922				Self {
923					rid: value.rid,
924					status: value.status.into(),
925				}
926			}
927		}
928	};
929}
930
931macro_rules! impl_init_pair {
932	($left:ident, $right:ident) => {
933		impl From<$left::Init> for $right::Init {
934			fn from(value: $left::Init) -> Self {
935				Self {
936					connections: convert_vec(value.connections),
937					state: value.state,
938					is_state_enabled: value.is_state_enabled,
939					rpcs: value.rpcs,
940					is_database_enabled: value.is_database_enabled,
941					queue_size: value.queue_size,
942					workflow_history: value.workflow_history,
943					is_workflow_enabled: value.is_workflow_enabled,
944				}
945			}
946		}
947
948		impl From<$right::Init> for $left::Init {
949			fn from(value: $right::Init) -> Self {
950				Self {
951					connections: convert_vec(value.connections),
952					state: value.state,
953					is_state_enabled: value.is_state_enabled,
954					rpcs: value.rpcs,
955					is_database_enabled: value.is_database_enabled,
956					queue_size: value.queue_size,
957					workflow_history: value.workflow_history,
958					is_workflow_enabled: value.is_workflow_enabled,
959				}
960			}
961		}
962	};
963}
964
965macro_rules! impl_common_actor_pair {
966	($left:ident, $right:ident) => {
967		impl_same_fields_pair!($left, $right, PatchStateRequest { state });
968		impl_same_fields_pair!($left, $right, ActionRequest { id, name, args });
969		impl_same_fields_pair!($left, $right, StateRequest { id });
970		impl_same_fields_pair!($left, $right, ConnectionsRequest { id });
971		impl_same_fields_pair!($left, $right, RpcsListRequest { id });
972		impl_same_fields_pair!($left, $right, Connection { id, details });
973		impl_connections_response_pair!($left, $right);
974		impl_connection_list_pair!($left, $right, ConnectionsUpdated);
975		impl_same_fields_pair!(
976			$left,
977			$right,
978			StateResponse {
979				rid,
980				state,
981				is_state_enabled,
982			}
983		);
984		impl_same_fields_pair!($left, $right, ActionResponse { rid, output });
985		impl_same_fields_pair!($left, $right, StateUpdated { state });
986		impl_same_fields_pair!($left, $right, RpcsListResponse { rid, rpcs });
987		impl_same_fields_pair!($left, $right, Error { message });
988	};
989}
990
991macro_rules! impl_queue_workflow_pair {
992	($left:ident, $right:ident) => {
993		impl_same_fields_pair!(
994			$left,
995			$right,
996			TraceQueryRequest {
997				id,
998				start_ms,
999				end_ms,
1000				limit,
1001			}
1002		);
1003		impl_same_fields_pair!($left, $right, TraceQueryResponse { rid, payload });
1004		impl_same_fields_pair!($left, $right, QueueRequest { id, limit });
1005		impl_same_fields_pair!(
1006			$left,
1007			$right,
1008			QueueMessageSummary {
1009				id,
1010				name,
1011				created_at_ms,
1012			}
1013		);
1014		impl_queue_status_pair!($left, $right);
1015		impl_queue_response_pair!($left, $right);
1016		impl_same_fields_pair!($left, $right, QueueUpdated { queue_size });
1017		impl_same_fields_pair!($left, $right, WorkflowHistoryRequest { id });
1018		impl_same_fields_pair!(
1019			$left,
1020			$right,
1021			WorkflowHistoryResponse {
1022				rid,
1023				history,
1024				is_workflow_enabled,
1025			}
1026		);
1027		impl_same_fields_pair!($left, $right, WorkflowHistoryUpdated { history });
1028		impl_init_pair!($left, $right);
1029	};
1030}
1031
1032macro_rules! impl_database_pair {
1033	($left:ident, $right:ident) => {
1034		impl_same_fields_pair!($left, $right, DatabaseSchemaRequest { id });
1035		impl_same_fields_pair!($left, $right, DatabaseSchemaResponse { rid, schema });
1036		impl_same_fields_pair!(
1037			$left,
1038			$right,
1039			DatabaseTableRowsRequest {
1040				id,
1041				table,
1042				limit,
1043				offset,
1044			}
1045		);
1046		impl_same_fields_pair!($left, $right, DatabaseTableRowsResponse { rid, result });
1047	};
1048}
1049
1050impl_common_actor_pair!(v1, v2);
1051impl_common_actor_pair!(v2, v3);
1052impl_common_actor_pair!(v3, v4);
1053impl_common_actor_pair!(v4, v5);
1054impl_common_actor_pair!(v5, v6);
1055impl_queue_workflow_pair!(v2, v3);
1056impl_queue_workflow_pair!(v3, v4);
1057impl_database_pair!(v3, v4);
1058impl_database_pair!(v4, v5);
1059impl_database_pair!(v5, v6);
1060
1061// v5 <-> v6 differ in `Init` and v6 adds schedule messages. Shared types are
1062// converted field-by-field; schedule-only messages are handled by the v6
1063// converter above so older clients receive a structured dropped-feature error.
1064impl_same_fields_pair!(
1065	v5,
1066	v6,
1067	TabConfigEntry {
1068		id,
1069		label,
1070		icon,
1071		hidden
1072	}
1073);
1074impl_same_fields_pair!(
1075	v5,
1076	v6,
1077	TraceQueryRequest {
1078		id,
1079		start_ms,
1080		end_ms,
1081		limit,
1082	}
1083);
1084impl_same_fields_pair!(v5, v6, TraceQueryResponse { rid, payload });
1085impl_same_fields_pair!(v5, v6, QueueRequest { id, limit });
1086impl_same_fields_pair!(
1087	v5,
1088	v6,
1089	QueueMessageSummary {
1090		id,
1091		name,
1092		created_at_ms,
1093	}
1094);
1095impl_queue_status_pair!(v5, v6);
1096impl_queue_response_pair!(v5, v6);
1097impl_same_fields_pair!(v5, v6, QueueUpdated { queue_size });
1098impl_same_fields_pair!(v5, v6, WorkflowHistoryRequest { id });
1099impl_same_fields_pair!(
1100	v5,
1101	v6,
1102	WorkflowHistoryResponse {
1103		rid,
1104		history,
1105		is_workflow_enabled,
1106	}
1107);
1108impl_same_fields_pair!(v5, v6, WorkflowHistoryUpdated { history });
1109impl_same_fields_pair!(v5, v6, WorkflowReplayRequest { id, entry_id });
1110impl_same_fields_pair!(
1111	v5,
1112	v6,
1113	WorkflowReplayResponse {
1114		rid,
1115		history,
1116		is_workflow_enabled,
1117	}
1118);
1119
1120impl From<v5::ToServerBody> for v6::ToServerBody {
1121	fn from(value: v5::ToServerBody) -> Self {
1122		match value {
1123			v5::ToServerBody::PatchStateRequest(req) => Self::PatchStateRequest(req.into()),
1124			v5::ToServerBody::StateRequest(req) => Self::StateRequest(req.into()),
1125			v5::ToServerBody::ConnectionsRequest(req) => Self::ConnectionsRequest(req.into()),
1126			v5::ToServerBody::ActionRequest(req) => Self::ActionRequest(req.into()),
1127			v5::ToServerBody::RpcsListRequest(req) => Self::RpcsListRequest(req.into()),
1128			v5::ToServerBody::TraceQueryRequest(req) => Self::TraceQueryRequest(req.into()),
1129			v5::ToServerBody::QueueRequest(req) => Self::QueueRequest(req.into()),
1130			v5::ToServerBody::WorkflowHistoryRequest(req) => {
1131				Self::WorkflowHistoryRequest(req.into())
1132			}
1133			v5::ToServerBody::WorkflowReplayRequest(req) => Self::WorkflowReplayRequest(req.into()),
1134			v5::ToServerBody::DatabaseSchemaRequest(req) => Self::DatabaseSchemaRequest(req.into()),
1135			v5::ToServerBody::DatabaseTableRowsRequest(req) => {
1136				Self::DatabaseTableRowsRequest(req.into())
1137			}
1138		}
1139	}
1140}
1141
1142impl From<v5::ToServer> for v6::ToServer {
1143	fn from(value: v5::ToServer) -> Self {
1144		Self {
1145			body: value.body.into(),
1146		}
1147	}
1148}
1149
1150// v4 <-> v5 differ only in `Init` (v5 adds `tab_config`), which is converted
1151// inline in the ToClient converters. Every other type is field-identical, so
1152// generate the plain field-copy `From` impls here. `Init` is deliberately
1153// excluded (no `impl_init_pair!`) because its shapes differ.
1154impl_same_fields_pair!(
1155	v4,
1156	v5,
1157	TraceQueryRequest {
1158		id,
1159		start_ms,
1160		end_ms,
1161		limit,
1162	}
1163);
1164impl_same_fields_pair!(v4, v5, TraceQueryResponse { rid, payload });
1165impl_same_fields_pair!(v4, v5, QueueRequest { id, limit });
1166impl_same_fields_pair!(
1167	v4,
1168	v5,
1169	QueueMessageSummary {
1170		id,
1171		name,
1172		created_at_ms,
1173	}
1174);
1175impl_queue_status_pair!(v4, v5);
1176impl_queue_response_pair!(v4, v5);
1177impl_same_fields_pair!(v4, v5, QueueUpdated { queue_size });
1178impl_same_fields_pair!(v4, v5, WorkflowHistoryRequest { id });
1179impl_same_fields_pair!(
1180	v4,
1181	v5,
1182	WorkflowHistoryResponse {
1183		rid,
1184		history,
1185		is_workflow_enabled,
1186	}
1187);
1188impl_same_fields_pair!(v4, v5, WorkflowHistoryUpdated { history });
1189impl_same_fields_pair!(v4, v5, WorkflowReplayRequest { id, entry_id });
1190impl_same_fields_pair!(
1191	v4,
1192	v5,
1193	WorkflowReplayResponse {
1194		rid,
1195		history,
1196		is_workflow_enabled,
1197	}
1198);
1199
1200impl From<v4::ToServerBody> for v5::ToServerBody {
1201	fn from(value: v4::ToServerBody) -> Self {
1202		match value {
1203			v4::ToServerBody::PatchStateRequest(req) => Self::PatchStateRequest(req.into()),
1204			v4::ToServerBody::StateRequest(req) => Self::StateRequest(req.into()),
1205			v4::ToServerBody::ConnectionsRequest(req) => Self::ConnectionsRequest(req.into()),
1206			v4::ToServerBody::ActionRequest(req) => Self::ActionRequest(req.into()),
1207			v4::ToServerBody::RpcsListRequest(req) => Self::RpcsListRequest(req.into()),
1208			v4::ToServerBody::TraceQueryRequest(req) => Self::TraceQueryRequest(req.into()),
1209			v4::ToServerBody::QueueRequest(req) => Self::QueueRequest(req.into()),
1210			v4::ToServerBody::WorkflowHistoryRequest(req) => {
1211				Self::WorkflowHistoryRequest(req.into())
1212			}
1213			v4::ToServerBody::WorkflowReplayRequest(req) => Self::WorkflowReplayRequest(req.into()),
1214			v4::ToServerBody::DatabaseSchemaRequest(req) => Self::DatabaseSchemaRequest(req.into()),
1215			v4::ToServerBody::DatabaseTableRowsRequest(req) => {
1216				Self::DatabaseTableRowsRequest(req.into())
1217			}
1218		}
1219	}
1220}
1221
1222impl From<v5::ToServerBody> for v4::ToServerBody {
1223	fn from(value: v5::ToServerBody) -> Self {
1224		match value {
1225			v5::ToServerBody::PatchStateRequest(req) => Self::PatchStateRequest(req.into()),
1226			v5::ToServerBody::StateRequest(req) => Self::StateRequest(req.into()),
1227			v5::ToServerBody::ConnectionsRequest(req) => Self::ConnectionsRequest(req.into()),
1228			v5::ToServerBody::ActionRequest(req) => Self::ActionRequest(req.into()),
1229			v5::ToServerBody::RpcsListRequest(req) => Self::RpcsListRequest(req.into()),
1230			v5::ToServerBody::TraceQueryRequest(req) => Self::TraceQueryRequest(req.into()),
1231			v5::ToServerBody::QueueRequest(req) => Self::QueueRequest(req.into()),
1232			v5::ToServerBody::WorkflowHistoryRequest(req) => {
1233				Self::WorkflowHistoryRequest(req.into())
1234			}
1235			v5::ToServerBody::WorkflowReplayRequest(req) => Self::WorkflowReplayRequest(req.into()),
1236			v5::ToServerBody::DatabaseSchemaRequest(req) => Self::DatabaseSchemaRequest(req.into()),
1237			v5::ToServerBody::DatabaseTableRowsRequest(req) => {
1238				Self::DatabaseTableRowsRequest(req.into())
1239			}
1240		}
1241	}
1242}
1243
1244impl From<v4::ToServer> for v5::ToServer {
1245	fn from(value: v4::ToServer) -> Self {
1246		Self {
1247			body: value.body.into(),
1248		}
1249	}
1250}
1251
1252impl From<v5::ToServer> for v4::ToServer {
1253	fn from(value: v5::ToServer) -> Self {
1254		Self {
1255			body: value.body.into(),
1256		}
1257	}
1258}
1259
1260impl From<v2::ToServerBody> for v3::ToServerBody {
1261	fn from(value: v2::ToServerBody) -> Self {
1262		match value {
1263			v2::ToServerBody::PatchStateRequest(req) => Self::PatchStateRequest(req.into()),
1264			v2::ToServerBody::StateRequest(req) => Self::StateRequest(req.into()),
1265			v2::ToServerBody::ConnectionsRequest(req) => Self::ConnectionsRequest(req.into()),
1266			v2::ToServerBody::ActionRequest(req) => Self::ActionRequest(req.into()),
1267			v2::ToServerBody::RpcsListRequest(req) => Self::RpcsListRequest(req.into()),
1268			v2::ToServerBody::TraceQueryRequest(req) => Self::TraceQueryRequest(req.into()),
1269			v2::ToServerBody::QueueRequest(req) => Self::QueueRequest(req.into()),
1270			v2::ToServerBody::WorkflowHistoryRequest(req) => {
1271				Self::WorkflowHistoryRequest(req.into())
1272			}
1273		}
1274	}
1275}
1276
1277impl From<v2::ToServer> for v3::ToServer {
1278	fn from(value: v2::ToServer) -> Self {
1279		Self {
1280			body: value.body.into(),
1281		}
1282	}
1283}
1284
1285impl From<v2::ToClientBody> for v3::ToClientBody {
1286	fn from(value: v2::ToClientBody) -> Self {
1287		match value {
1288			v2::ToClientBody::StateResponse(resp) => Self::StateResponse(resp.into()),
1289			v2::ToClientBody::ConnectionsResponse(resp) => Self::ConnectionsResponse(resp.into()),
1290			v2::ToClientBody::ActionResponse(resp) => Self::ActionResponse(resp.into()),
1291			v2::ToClientBody::ConnectionsUpdated(update) => Self::ConnectionsUpdated(update.into()),
1292			v2::ToClientBody::QueueUpdated(update) => Self::QueueUpdated(update.into()),
1293			v2::ToClientBody::StateUpdated(update) => Self::StateUpdated(update.into()),
1294			v2::ToClientBody::WorkflowHistoryUpdated(update) => {
1295				Self::WorkflowHistoryUpdated(update.into())
1296			}
1297			v2::ToClientBody::RpcsListResponse(resp) => Self::RpcsListResponse(resp.into()),
1298			v2::ToClientBody::TraceQueryResponse(resp) => Self::TraceQueryResponse(resp.into()),
1299			v2::ToClientBody::QueueResponse(resp) => Self::QueueResponse(resp.into()),
1300			v2::ToClientBody::WorkflowHistoryResponse(resp) => {
1301				Self::WorkflowHistoryResponse(resp.into())
1302			}
1303			v2::ToClientBody::Error(error) => Self::Error(error.into()),
1304			v2::ToClientBody::Init(init) => Self::Init(init.into()),
1305		}
1306	}
1307}
1308
1309impl From<v2::ToClient> for v3::ToClient {
1310	fn from(value: v2::ToClient) -> Self {
1311		Self {
1312			body: value.body.into(),
1313		}
1314	}
1315}
1316
1317fn dropped_error(message: &str) -> v2::Error {
1318	v2::Error {
1319		message: message.to_owned(),
1320	}
1321}
1322
1323#[cfg(test)]
1324mod tests {
1325	use super::*;
1326
1327	#[test]
1328	fn v3_database_schema_request_keeps_meaning_when_upgrading_to_v4() {
1329		let request = ToServer::V3(v3::ToServer {
1330			body: v3::ToServerBody::DatabaseSchemaRequest(v3::DatabaseSchemaRequest {
1331				id: Uint(7),
1332			}),
1333		});
1334
1335		let ToServer::V4(upgraded) = ToServer::v3_to_v4(request).unwrap() else {
1336			panic!("expected v4 request")
1337		};
1338
1339		assert!(matches!(
1340			upgraded.body,
1341			v4::ToServerBody::DatabaseSchemaRequest(v4::DatabaseSchemaRequest { id }) if id == Uint(7)
1342		));
1343	}
1344
1345	fn v5_init_with_tabs() -> v5::Init {
1346		v5::Init {
1347			connections: Vec::new(),
1348			state: None,
1349			is_state_enabled: false,
1350			rpcs: Vec::new(),
1351			is_database_enabled: false,
1352			queue_size: Uint(0),
1353			workflow_history: None,
1354			is_workflow_enabled: false,
1355			tab_config: vec![v5::TabConfigEntry {
1356				id: "custom".to_owned(),
1357				label: Some("Custom".to_owned()),
1358				icon: Some("star".to_owned()),
1359				hidden: false,
1360			}],
1361		}
1362	}
1363
1364	#[test]
1365	fn v5_init_tab_config_drops_when_downgrading_to_v4() {
1366		let response = ToClient::V5(v5::ToClient {
1367			body: v5::ToClientBody::Init(v5_init_with_tabs()),
1368		});
1369
1370		let ToClient::V4(downgraded) = ToClient::v5_to_v4(response).unwrap() else {
1371			panic!("expected v4 response")
1372		};
1373
1374		// v4 has no tab_config field, so the tabs are dropped but the rest of
1375		// the Init survives the downgrade.
1376		assert!(matches!(
1377			downgraded.body,
1378			v4::ToClientBody::Init(v4::Init {
1379				is_workflow_enabled: false,
1380				..
1381			})
1382		));
1383	}
1384
1385	#[test]
1386	fn v4_init_upgrades_to_v5_with_empty_tab_config() {
1387		let response = ToClient::V4(v4::ToClient {
1388			body: v4::ToClientBody::Init(v4::Init {
1389				connections: Vec::new(),
1390				state: None,
1391				is_state_enabled: false,
1392				rpcs: Vec::new(),
1393				is_database_enabled: false,
1394				queue_size: Uint(0),
1395				workflow_history: None,
1396				is_workflow_enabled: false,
1397			}),
1398		});
1399
1400		let ToClient::V5(upgraded) = ToClient::v4_to_v5(response).unwrap() else {
1401			panic!("expected v5 response")
1402		};
1403
1404		let v5::ToClientBody::Init(init) = upgraded.body else {
1405			panic!("expected Init body")
1406		};
1407		assert!(init.tab_config.is_empty());
1408	}
1409
1410	#[test]
1411	fn v5_init_tab_config_round_trips_at_version_5() {
1412		let original = v5_init_with_tabs();
1413		let latest = ToClient::v5_to_v6(ToClient::V5(v5::ToClient {
1414			body: v5::ToClientBody::Init(original.clone()),
1415		}))
1416		.unwrap();
1417		let encoded = latest.serialize_with_embedded_version(5).unwrap();
1418
1419		// `deserialize_with_embedded_version` upgrades to and returns the latest
1420		// wire type (v6::ToClient), not the versioned enum.
1421		let decoded =
1422			<ToClient as OwnedVersionedData>::deserialize_with_embedded_version(&encoded).unwrap();
1423		let v6::ToClientBody::Init(init) = decoded.body else {
1424			panic!("expected Init body")
1425		};
1426		assert_eq!(init.tab_config.len(), original.tab_config.len());
1427		assert_eq!(init.tab_config[0].id, original.tab_config[0].id);
1428		assert!(init.schedules.is_empty());
1429	}
1430
1431	#[test]
1432	fn v6_schedule_messages_downgrade_to_structured_error() {
1433		let response = ToClient::V6(v6::ToClient {
1434			body: v6::ToClientBody::SchedulesUpdated(v6::SchedulesUpdated {
1435				schedules: Vec::new(),
1436			}),
1437		});
1438
1439		let ToClient::V5(downgraded) = ToClient::v6_to_v5(response).unwrap() else {
1440			panic!("expected v5 response")
1441		};
1442		assert_eq!(
1443			downgraded.body,
1444			v5::ToClientBody::Error(v5::Error {
1445				message: SCHEDULES_DROPPED_ERROR.to_owned(),
1446			})
1447		);
1448	}
1449
1450	#[test]
1451	fn v5_init_upgrades_to_v6_with_empty_schedules() {
1452		let response = ToClient::V5(v5::ToClient {
1453			body: v5::ToClientBody::Init(v5_init_with_tabs()),
1454		});
1455		let ToClient::V6(upgraded) = ToClient::v5_to_v6(response).unwrap() else {
1456			panic!("expected v6 response")
1457		};
1458		let v6::ToClientBody::Init(init) = upgraded.body else {
1459			panic!("expected Init body")
1460		};
1461		assert!(init.schedules.is_empty());
1462		assert_eq!(init.tab_config.len(), 1);
1463	}
1464
1465	#[test]
1466	fn v4_workflow_replay_response_downgrades_to_v3_error() {
1467		let response = ToClient::V4(v4::ToClient {
1468			body: v4::ToClientBody::WorkflowReplayResponse(v4::WorkflowReplayResponse {
1469				rid: Uint(11),
1470				history: Some(b"workflow".to_vec()),
1471				is_workflow_enabled: true,
1472			}),
1473		});
1474
1475		let ToClient::V3(downgraded) = ToClient::v4_to_v3(response).unwrap() else {
1476			panic!("expected v3 response")
1477		};
1478
1479		assert_eq!(
1480			downgraded.body,
1481			v3::ToClientBody::Error(v3::Error {
1482				message: WORKFLOW_HISTORY_DROPPED_ERROR.to_owned(),
1483			})
1484		);
1485	}
1486}