d421487bcd
MqttSubscriptionManager is the P1 plain-MQTT ingest path: it holds the authored RawPath → MqttTagDefinition table behind the shared EquipmentTagRefResolver, establishes one SUBSCRIBE per distinct authored topic, and turns each inbound message into a LastValueCache update plus an OnDataChange notification. Load-bearing choices, each pinned by a falsified test: - The published reference IS the RawPath (def.Name), never the topic and never the TagConfig blob — DriverHostActor's dual-namespace fan-out is RawPath-keyed, so any other key passes every unit test and delivers nothing in production. - One message fans out to EVERY tag on its topic; several tags routinely share a topic with different jsonPaths, and stopping at the first match silently starves the rest. - An unauthored topic raises nothing: MQTT ingest never auto-provisions. - Wildcard-authored topics (accepted by the parser, warned at deploy) match via MQTTnet's own MqttTopicFilterComparer, so '+'/'#' behave as a broker would. Concrete topics stay a single lookup; the wildcard scan runs only when one exists. - Retained seeds are honoured natively on MQTT 5.0 (retain handling) AND filtered client-side on the retain flag, because 3.1.1 brokers always replay. - Nothing throws on MQTTnet's dispatcher thread: a malformed payload degrades that tag (BadDecodingError / BadTypeMismatch), and a throwing OnDataChange subscriber is contained without starving the tags behind it. - The manager issues the FIRST subscribe itself — Reconnected deliberately does not fire after the initial ConnectAsync. - Reconnect re-subscribe: total failure THROWS (MqttConnection then tears the session down and retries under backoff, rather than serving a connected-but-deaf driver); a partial SUBACK rejection degrades only the denied refs, because retrying forever over one ACL-denied topic would take every tag dark. MqttConnection gains a MessageReceived observer (fired on the dispatcher, throwing observers contained) and a bounded SubscribeAsync under its own linked-CTS deadline — deliberately not MqttClientOptions.Timeout, which already governs every MQTTnet op. A refused filter is an outcome, not an exception; only the SUBSCRIBE itself failing throws. Classify() is parameterised by operation name (messages unchanged for connect). JSONPath is a documented subset ($, $.a.b, $['a'], $.a[0]); filters/slices/ recursive descent report a miss rather than pulling in a JSONPath package for an expression form that selects a set where a tag needs one scalar. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW