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
//! Exhaustive route classifications for the existing wire surfaces.
use super::HotAction;
use liminal::protocol::FrameType;
use liminal_protocol::wire::{ClientDiscriminant, PARTICIPANT_FRAME_TYPE};
use serde::{Deserialize, Serialize};
/// Where the exact placed subject must be resolved before deciding.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SubjectSource {
/// Subject carried by this frame and resolved through admitted mapping.
FrameSubject,
/// Subject retained by this connection's subscription.
SubscriptionBinding,
/// Subject retained by this connection's conversation.
ConversationBinding,
/// Every explicitly served subject in the admitted worker registration.
WorkerBinding,
/// Subject retained by the recipient-bound push correlation.
PushBinding,
/// Conversation-to-subject mapping, never a guessed numeric subject.
ParticipantBinding,
/// The link the frame offers, under the network, with the end it names.
FrameLink,
/// The link open on this connection's stream, and the frame's subject.
LinkBinding,
}
/// Permission policy; control and ignored inbound frames create no data effect.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "class", rename_all = "snake_case")]
pub enum RouteClassification {
/// One offline decision before the operation's effect.
Hot {
/// Required subject permission.
action: HotAction,
/// Existing authoritative binding used to resolve the subject.
source: SubjectSource,
},
/// Release an already-owned resource without creating new reach.
Cleanup {
/// Permission associated with the released resource.
action: HotAction,
/// Binding whose ownership must still be checked.
source: SubjectSource,
},
/// One live decision before the operation's effect, never the hot seam:
/// the frame's offered end names `link_export` or `link_import` on the
/// link (ACCESS-002 R1).
Deliberate {
/// Binding used to resolve the link and the end.
source: SubjectSource,
},
/// A message crossing a link: `link_export` held by the exporting
/// machine and `link_import` by the importing one on the opened link
/// resource, both decided offline from the two passes (ACCESS-002 R1).
Crossing {
/// Binding used to resolve the link and the subject.
source: SubjectSource,
},
/// Credential admission; no subject operation is authorized by this result.
Admission,
/// Liveness or connection teardown with no new subject effect.
Control,
/// Inbound server-originated frame, ignored without dispatch.
ServerOnly,
/// Decode the participant subrecord and classify it independently.
ParticipantEnvelope,
/// Unknown inbound frame; no data operation can dispatch.
Unknown,
}
const fn hot(action: HotAction, source: SubjectSource) -> RouteClassification {
RouteClassification::Hot { action, source }
}
const fn cleanup(action: HotAction, source: SubjectSource) -> RouteClassification {
RouteClassification::Cleanup { action, source }
}
/// Classifies every core frame category, including the participant extension.
///
/// This match has no catch-all over known variants; a new wire route requires
/// an explicit policy before the classifier can compile.
#[must_use]
pub const fn classify_wire_route(frame: FrameType) -> RouteClassification {
use SubjectSource as Source;
match frame {
FrameType::Connect => RouteClassification::Admission,
FrameType::Disconnect | FrameType::Ping => RouteClassification::Control,
FrameType::Publish => hot(HotAction::Publish, Source::FrameSubject),
FrameType::Subscribe => hot(HotAction::Subscribe, Source::FrameSubject),
FrameType::Unsubscribe => cleanup(HotAction::Subscribe, Source::SubscriptionBinding),
FrameType::ConversationOpen => hot(HotAction::Call, Source::FrameSubject),
FrameType::ConversationMessage => hot(HotAction::Call, Source::ConversationBinding),
FrameType::ConversationClose => cleanup(HotAction::Call, Source::ConversationBinding),
FrameType::Accept | FrameType::Defer | FrameType::Reject => {
hot(HotAction::Subscribe, Source::SubscriptionBinding)
}
FrameType::PushReply => hot(HotAction::Serve, Source::PushBinding),
FrameType::WorkerRegister => hot(HotAction::Serve, Source::WorkerBinding),
FrameType::LinkOpen => RouteClassification::Deliberate {
source: Source::FrameLink,
},
FrameType::LinkMessage => RouteClassification::Crossing {
source: Source::LinkBinding,
},
FrameType::ConnectAck
| FrameType::ConnectError
| FrameType::SubscribeAck
| FrameType::SubscribeError
| FrameType::PublishAck
| FrameType::PublishError
| FrameType::ConversationError
| FrameType::Pong
| FrameType::Push
| FrameType::WorkerRegisterAck
| FrameType::LinkOpenAck
| FrameType::DisconnectReason
| FrameType::Deliver => RouteClassification::ServerOnly,
FrameType::Unknown(tag) => {
if tag == PARTICIPANT_FRAME_TYPE {
RouteClassification::ParticipantEnvelope
} else {
RouteClassification::Unknown
}
}
}
}
/// Classifies every decoded client participant subrecord.
///
/// A recovery batch must decide every mapped conversation before any effect.
/// Cleanup still requires the existing participant/connection ownership proof.
#[must_use]
pub const fn classify_participant_route(route: ClientDiscriminant) -> RouteClassification {
use SubjectSource::ParticipantBinding as Source;
match route {
ClientDiscriminant::EnrollmentRequest
| ClientDiscriminant::CredentialAttachRequest
| ClientDiscriminant::ParticipantAck
| ClientDiscriminant::MarkerAck
| ClientDiscriminant::ObserverRecoveryHandshake
| ClientDiscriminant::SettlementQuery => hot(HotAction::Subscribe, Source),
ClientDiscriminant::RecordAdmission => hot(HotAction::Publish, Source),
ClientDiscriminant::DetachRequest | ClientDiscriminant::LeaveRequest => {
cleanup(HotAction::Subscribe, Source)
}
}
}