Skip to main content

termwright_protocol/
client.rs

1//! Blocking unix-socket client for the semantic side-channel.
2//!
3//! **Dormant rule.** Without `TERMWRIGHT_ENDPOINT` and `TERMWRIGHT_TOKEN` in
4//! the environment [`Client::from_env`] returns `None`: the application opens
5//! no socket, writes no marker, and renders exactly the bytes it would have
6//! rendered anyway.
7//!
8//! The client is deliberately blocking and single-threaded. A TUI renders on
9//! one thread and the marker must follow that render's last byte, so
10//! [`Client::publish`] does its socket work inline and hands back the marker
11//! to write. Driver requests are picked up by [`Client::poll`], which never
12//! blocks.
13
14use std::collections::VecDeque;
15use std::io::{ErrorKind, Read, Write};
16use std::os::unix::net::UnixStream;
17use std::sync::Arc;
18use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
19
20use serde_json::value::RawValue;
21use serde_json::Value;
22
23use crate::debug::{describe_endpoint, error_label, join_capabilities, on_off, Category, DebugLog};
24use crate::diffing::build_delta;
25use crate::error::Error;
26use crate::framing::{encode_frame, FrameDecoder};
27use crate::limits::{Limits, DEFAULT_LIMITS};
28use crate::logs::{AttrValue, LogLevel, LogRecord, MAX_LOG_ATTRS};
29use crate::marker::encode_marker;
30use crate::messages::{
31    default_capabilities, parse_driver_message, GetTree, GetTreeResult, Hello, HelloAck,
32    LogMessage, ProbeInfo, ProtocolErrorMessage, RevisionCommit, SnapshotMessage,
33};
34use crate::roles::Capability;
35use crate::tree::Snapshot;
36use crate::validate::validate_snapshot;
37
38/// Environment variable naming the driver's socket.
39pub const ENV_ENDPOINT: &str = "TERMWRIGHT_ENDPOINT";
40/// Environment variable carrying the per-launch session token.
41pub const ENV_TOKEN: &str = "TERMWRIGHT_TOKEN";
42/// Environment variable pinning the protocol version.
43pub const ENV_PROTOCOL: &str = "TERMWRIGHT_PROTOCOL";
44
45/// Default handshake budget.
46pub const DIAL_TIMEOUT: Duration = Duration::from_secs(5);
47
48/// Default bound on a single frame write.
49///
50/// This client is blocking by design — a TUI renders on one thread and the
51/// marker must follow that render's last byte — so an unbounded `write_all`
52/// turns a driver that stopped reading into an application that stopped
53/// drawing. A driver that cannot take a frame in a quarter of a second is not
54/// keeping up, and the next frame carries newer state anyway.
55pub const WRITE_TIMEOUT: Duration = Duration::from_millis(250);
56
57/// How many recent revisions stay answerable by `get-tree`.
58const SNAPSHOT_HISTORY: usize = 8;
59
60/// How a client identifies itself and what it can provide.
61#[derive(Debug, Clone)]
62pub struct Options {
63    /// Adapter name sent in the handshake.
64    pub adapter_name: String,
65    /// Adapter version sent in the handshake.
66    pub adapter_version: String,
67    /// Capabilities announced to the driver.
68    pub capabilities: Vec<Capability>,
69    /// Limits in force until `hello-ack` replaces them.
70    pub limits: Limits,
71    /// Bound on a single frame write. `None` disables it, which is only sane
72    /// for a caller that publishes off the render path.
73    pub write_timeout: Option<Duration>,
74    /// What a probe says it can observe. `None` for a hand-written adapter,
75    /// which is what the driver assumes by default.
76    pub probe: Option<ProbeInfo>,
77    /// Adapter-side diagnostic log, or `None` for silence — which is what
78    /// [`Options::new`] leaves here unless `TERMWRIGHT_DEBUG_FILE` names a
79    /// file. Shared rather than owned so an adapter can log alongside the
80    /// client on the same file.
81    pub debug: Option<Arc<DebugLog>>,
82}
83
84impl Options {
85    /// Options for an adapter that also forwards application logs.
86    ///
87    /// Announcing `logs` is what makes the driver grant a budget; without it
88    /// the driver sends none and the adapter must stay silent.
89    pub fn with_logs(adapter_name: impl Into<String>, adapter_version: impl Into<String>) -> Self {
90        let mut options = Self::new(adapter_name, adapter_version);
91        options.capabilities.push(Capability::Logs);
92        options
93    }
94
95    /// Options for an adapter with the default capability set.
96    pub fn new(adapter_name: impl Into<String>, adapter_version: impl Into<String>) -> Self {
97        Self {
98            adapter_name: adapter_name.into(),
99            adapter_version: adapter_version.into(),
100            capabilities: default_capabilities(),
101            limits: DEFAULT_LIMITS,
102            write_timeout: Some(WRITE_TIMEOUT),
103            probe: None,
104            // Left silent on purpose: opening a file is a side effect, and a
105            // constructor is the wrong place for one. `Client::from_env` is
106            // where the environment is read, here and in the other clients.
107            debug: None,
108        }
109    }
110}
111
112/// Wall-clock milliseconds, the only clock both sides agree on without
113/// negotiating: an adapter cannot know when the driver opened the session.
114fn epoch_millis() -> i64 {
115    SystemTime::now()
116        .duration_since(UNIX_EPOCH)
117        .map(|since| since.as_millis() as i64)
118        .unwrap_or(0)
119}
120
121/// Rate limiter for the log channel: `burst` capacity on top of the sustained
122/// rate, refilled continuously.
123///
124/// The adapter enforces its own budget and drops locally, which is what keeps
125/// a log storm from eating the frame budget the semantic tree needs.
126#[derive(Debug)]
127struct TokenBucket {
128    per_second: f64,
129    capacity: f64,
130    tokens: f64,
131    updated: Instant,
132}
133
134impl TokenBucket {
135    fn new(per_second: i64, burst: i64, now: Instant) -> Self {
136        let rate = per_second.max(0) as f64;
137        let capacity = rate + burst.max(0) as f64;
138        Self {
139            per_second: rate,
140            capacity,
141            tokens: capacity,
142            updated: now,
143        }
144    }
145
146    /// Consume one token, refilling first. `false` means "over budget".
147    fn take(&mut self, now: Instant) -> bool {
148        if self.per_second <= 0.0 {
149            return false;
150        }
151        let elapsed = now.saturating_duration_since(self.updated).as_secs_f64();
152        self.updated = now;
153        self.tokens = (self.tokens + elapsed * self.per_second).min(self.capacity);
154        if self.tokens < 1.0 {
155            return false;
156        }
157        self.tokens -= 1.0;
158        true
159    }
160}
161
162/// One semantic session: handshake, snapshot publishing, render markers.
163///
164/// The client owns the revision counter; an adapter never picks its own.
165#[derive(Debug)]
166pub struct Client {
167    endpoint: String,
168    token: String,
169    options: Options,
170    protocol: String,
171
172    stream: Option<UnixStream>,
173    decoder: FrameDecoder,
174    limits: Limits,
175    session_id: Option<String>,
176    revision: i64,
177    marker_enabled: bool,
178    log_budget: Option<crate::messages::LogBudget>,
179    published: Option<Value>,
180    deltas_sent: u64,
181    snapshots_sent: u64,
182    log_seq: i64,
183    log_bucket: Option<TokenBucket>,
184    logs_dropped: u64,
185    subscribe: String,
186    history: VecDeque<(i64, Box<RawValue>)>,
187    force_full: bool,
188}
189
190impl Client {
191    /// Build a client for an explicit endpoint and token.
192    pub fn new(endpoint: impl Into<String>, token: impl Into<String>, options: Options) -> Self {
193        let limits = options.limits;
194        Self {
195            endpoint: endpoint.into(),
196            token: token.into(),
197            options,
198            protocol: crate::messages::PROTOCOL_ID.into(),
199            stream: None,
200            decoder: FrameDecoder::new(limits.max_frame_bytes, limits.max_depth),
201            limits,
202            session_id: None,
203            revision: 0,
204            marker_enabled: false,
205            log_budget: None,
206            published: None,
207            deltas_sent: 0,
208            snapshots_sent: 0,
209            log_seq: 0,
210            log_bucket: None,
211            logs_dropped: 0,
212            subscribe: "snapshots".to_owned(),
213            history: VecDeque::new(),
214            force_full: false,
215        }
216    }
217
218    /// Whether this client negotiated evidence-qualified protocol v2.
219    ///
220    /// Snapshot producers must know this before observing a frame because v1
221    /// forbids qualified geometry while v2 requires it on every node.
222    #[must_use]
223    pub fn qualified_observations(&self) -> bool {
224        self.protocol == crate::messages::PROTOCOL_V2_ID
225    }
226
227    /// Build a client from `TERMWRIGHT_*`, or `None` when not instrumented.
228    ///
229    /// This is the dormant rule in one function: no endpoint or no token means
230    /// no client, and the caller must then open nothing and emit nothing.
231    pub fn from_env(mut options: Options) -> Option<Self> {
232        if options.debug.is_none() {
233            options.debug = DebugLog::from_env(&options.adapter_name).map(Arc::new);
234        }
235        Self::from_values(
236            std::env::var(ENV_ENDPOINT).ok().as_deref(),
237            std::env::var(ENV_TOKEN).ok().as_deref(),
238            std::env::var(ENV_PROTOCOL).ok().as_deref(),
239            options,
240        )
241    }
242
243    /// Build a client from explicit endpoint, token and protocol values,
244    /// applying the same dormant rule as [`Client::from_env`].
245    ///
246    /// Use this when the process manages its own environment, or in tests.
247    /// A missing or empty endpoint or token, a protocol other than
248    /// `termwright/1`, or a Windows named pipe all yield `None`.
249    pub fn from_values(
250        endpoint: Option<&str>,
251        token: Option<&str>,
252        protocol: Option<&str>,
253        options: Options,
254    ) -> Option<Self> {
255        let endpoint = endpoint.filter(|value| !value.is_empty());
256        let token = token.filter(|value| !value.is_empty());
257        let (Some(endpoint), Some(token)) = (endpoint, token) else {
258            if let Some(log) = options.debug.as_ref() {
259                let mut missing = Vec::new();
260                if endpoint.is_none() {
261                    missing.push(ENV_ENDPOINT);
262                }
263                if token.is_none() {
264                    missing.push(ENV_TOKEN);
265                }
266                log.line(
267                    Category::Diag,
268                    &format!("dormant: {} not set", missing.join(" and ")),
269                );
270            }
271            return None;
272        };
273        if let Some(protocol) = protocol.filter(|value| !value.is_empty()) {
274            if protocol != crate::messages::PROTOCOL_ID
275                && protocol != crate::messages::PROTOCOL_V2_ID
276                && protocol != "1"
277                && protocol != "2"
278            {
279                if let Some(log) = options.debug.as_ref() {
280                    log.line(
281                        Category::Diag,
282                        &format!(
283                            "dormant: {ENV_PROTOCOL}={protocol:?} is not {:?}",
284                            crate::messages::PROTOCOL_ID
285                        ),
286                    );
287                }
288                return None;
289            }
290        }
291        if endpoint.starts_with(r"\\.\pipe\") || endpoint.starts_with(r"\\?\pipe\") {
292            // Named pipes need a Windows-only transport; stay dormant rather
293            // than half-working.
294            if let Some(log) = options.debug.as_ref() {
295                log.line(
296                    Category::Diag,
297                    &format!(
298                        "dormant: {} needs a Windows transport this client does not have",
299                        describe_endpoint(endpoint)
300                    ),
301                );
302            }
303            return None;
304        }
305        let v2 = matches!(protocol, Some(crate::messages::PROTOCOL_V2_ID) | Some("2"));
306        let mut client = Self::new(endpoint, token, options);
307        if v2 {
308            client.protocol = crate::messages::PROTOCOL_V2_ID.into();
309            if !client
310                .options
311                .capabilities
312                .contains(&Capability::QualifiedObservations)
313            {
314                client
315                    .options
316                    .capabilities
317                    .push(Capability::QualifiedObservations);
318            }
319        }
320        Some(client)
321    }
322
323    /// Connect, send `hello`, and wait for `hello-ack`.
324    ///
325    /// # Errors
326    /// Returns [`Error::Io`] when the endpoint is unreachable and
327    /// [`Error::HandshakeTimeout`] when the driver does not answer. A failed
328    /// side-channel must not take the application down: callers are expected
329    /// to carry on rendering.
330    pub fn connect(&mut self, timeout: Duration) -> Result<(), Error> {
331        self.debug_line(
332            Category::Sem,
333            &format!(
334                "dial {} timeout={}ms",
335                describe_endpoint(&self.endpoint),
336                timeout.as_millis()
337            ),
338        );
339        let stream = match UnixStream::connect(&self.endpoint) {
340            Ok(stream) => stream,
341            Err(error) => {
342                self.debug_line(
343                    Category::Diag,
344                    &format!("dial failed, staying dormant: {}", error_label(&error)),
345                );
346                return Err(error.into());
347            }
348        };
349        stream.set_read_timeout(Some(Duration::from_millis(50)))?;
350        stream.set_write_timeout(self.options.write_timeout)?;
351        self.stream = Some(stream);
352
353        let mut hello = Hello::new(
354            &self.token,
355            &self.options.adapter_name,
356            &self.options.adapter_version,
357            self.options.capabilities.clone(),
358        );
359        hello.protocol = self.protocol.clone();
360        if let Some(probe) = self.options.probe.clone() {
361            hello = hello.with_probe(probe);
362        }
363        self.send(&hello)?;
364        self.debug_line(
365            Category::Sem,
366            &format!(
367                "hello sent adapter={}/{} caps={}",
368                self.options.adapter_name,
369                self.options.adapter_version,
370                join_capabilities(&self.options.capabilities)
371            ),
372        );
373
374        let deadline = Instant::now() + timeout;
375        while self.session_id.is_none() {
376            if Instant::now() >= deadline {
377                self.debug_line(
378                    Category::Diag,
379                    &format!(
380                        "no hello-ack within {}ms, staying dormant",
381                        timeout.as_millis()
382                    ),
383                );
384                self.close();
385                return Err(Error::HandshakeTimeout);
386            }
387            self.poll()?;
388        }
389        Ok(())
390    }
391
392    /// Write one diagnostic line, when diagnostics are on.
393    ///
394    /// Named apart from [`Client::log`], which is the application's own log
395    /// channel to the driver: these two go to different places for different
396    /// readers, and confusing them would put application text in a CI artifact
397    /// or diagnostics on the wire.
398    fn debug_line(&self, category: Category, message: &str) {
399        if let Some(log) = self.options.debug.as_ref() {
400            log.line(category, message);
401        }
402    }
403
404    /// Whether the handshake completed and the link is still up.
405    pub fn connected(&self) -> bool {
406        self.session_id.is_some() && self.stream.is_some()
407    }
408
409    /// The id the driver assigned, or `None` before the handshake.
410    pub fn session_id(&self) -> Option<&str> {
411        self.session_id.as_deref()
412    }
413
414    /// The last revision this client published.
415    pub fn revision(&self) -> i64 {
416        self.revision
417    }
418
419    /// The log-channel allowance the driver granted, or `None` when logs are
420    /// disabled — which is the case unless the adapter announced `logs`.
421    pub fn log_budget(&self) -> Option<crate::messages::LogBudget> {
422        self.log_budget
423    }
424
425    /// The ceilings in force, as negotiated by `hello-ack`.
426    pub fn limits(&self) -> &Limits {
427        &self.limits
428    }
429
430    /// Make the next publish send a whole tree.
431    ///
432    /// The producer's obligation from D5: a probe that lost anything from its
433    /// own stream of facts — a dropped frame, a coalesced burst, a write that
434    /// failed — must not follow it with a patch. The driver would apply that
435    /// patch to a tree that never accounted for what was lost, and the
436    /// divergence would be silent.
437    pub fn require_full_snapshot(&mut self) {
438        self.force_full = true;
439    }
440
441    /// Whether the obligation is outstanding.
442    #[must_use]
443    pub fn full_snapshot_required(&self) -> bool {
444        self.force_full
445    }
446
447    /// Drop the session. The application keeps running.
448    pub fn close(&mut self) {
449        if let Some(stream) = self.stream.take() {
450            self.debug_line(
451                Category::Sem,
452                &format!(
453                    "close r{} snapshots={} deltas={} logs_dropped={}",
454                    self.revision, self.snapshots_sent, self.deltas_sent, self.logs_dropped
455                ),
456            );
457            let _ = stream.shutdown(std::net::Shutdown::Both);
458        }
459        self.session_id = None;
460    }
461
462    /// Publish a snapshot for the next revision and return its marker.
463    ///
464    /// Write the marker to stdout **after** the render's last byte: it commits
465    /// the bytes that precede it. `session_id` and `revision` on the snapshot
466    /// are overwritten with the session's own.
467    ///
468    /// Returns `Ok(None)` when there is no live session or the driver did not
469    /// ask for markers, so a dormant app takes no branch.
470    ///
471    /// # Errors
472    /// Returns [`Error::Validation`] if the snapshot is invalid — that is an
473    /// adapter bug, so it is loud rather than silent — or [`Error::Io`] if the
474    /// channel broke.
475    pub fn publish(&mut self, snapshot: &mut Snapshot) -> Result<Option<String>, Error> {
476        let result = self.publish_inner(snapshot);
477        if matches!(result, Err(Error::Protocol(_) | Error::Validation(_))) {
478            // Nothing reached the wire for a local encode/validation refusal.
479            // Keep the link usable, but make recovery explicit and independent
480            // of whether a framework wrapper remembers to report the gap.
481            self.require_full_snapshot();
482        }
483        result
484    }
485
486    fn publish_inner(&mut self, snapshot: &mut Snapshot) -> Result<Option<String>, Error> {
487        let Some(session_id) = self.session_id.clone() else {
488            return Ok(None);
489        };
490        if self.stream.is_none() {
491            return Ok(None);
492        }
493
494        let revision = self.revision + 1;
495        snapshot.v = if self.protocol == crate::messages::PROTOCOL_V2_ID {
496            2
497        } else {
498            1
499        };
500        snapshot.session_id = session_id.clone();
501        snapshot.revision = revision;
502
503        let body = serde_json::to_string(&snapshot).map_err(|_| {
504            Error::Protocol(crate::error::Violation::new(
505                "frame-malformed",
506                "snapshot is not JSON-serialisable",
507            ))
508        })?;
509        let parsed: Value = serde_json::from_str(&body).expect("just serialised");
510        validate_snapshot(&parsed, &self.limits)?;
511
512        let marker = if self.marker_enabled {
513            Some(encode_marker(&self.token, &session_id, revision)?)
514        } else {
515            None
516        };
517
518        // Encode every frame before writing the first byte. A local ceiling
519        // failure is recoverable; sending the tree and only then discovering
520        // that its commit cannot be encoded would leave the wire half-applied.
521        let mut sent_delta = false;
522        let tree_frame = if self.subscribe != "revisions" {
523            // A delta when the driver asked for one and it is worth sending;
524            // a whole tree on the first publish, under a snapshots
525            // subscription, or past roughly half the tree. The base advances
526            // only once a message has been built from it, so a skipped
527            // publish cannot leave the driver patching a tree it never got.
528            // An outstanding obligation overrides the choice: a gap in the
529            // producer's own facts means the next tree must be whole.
530            let forced = self.force_full;
531            if forced {
532                self.debug_line(
533                    Category::Io,
534                    &format!("r{revision} full snapshot: the producer reported a gap"),
535                );
536            }
537            let delta = if self.subscribe == "diffs" && !forced {
538                self.published
539                    .as_ref()
540                    .and_then(|base| build_delta(base, &parsed))
541            } else {
542                None
543            };
544            match delta {
545                Some(delta) => {
546                    sent_delta = true;
547                    Some(encode_frame(&delta, self.limits.max_frame_bytes)?)
548                }
549                None => Some(encode_frame(
550                    &SnapshotMessage::new(snapshot),
551                    self.limits.max_frame_bytes,
552                )?),
553            }
554        } else {
555            None
556        };
557        let commit_frame =
558            encode_frame(&RevisionCommit::new(revision), self.limits.max_frame_bytes)?;
559
560        if let Some(frame) = &tree_frame {
561            self.write_frame(frame)?;
562        }
563        self.write_frame(&commit_frame)?;
564
565        // Only bytes that are fully on the wire become a revision/base. This
566        // keeps a locally rejected frame from making the next delta refer to a
567        // tree the driver never received.
568        self.revision = revision;
569        self.remember(revision, RawValue::from_string(body).expect("valid JSON"));
570        if tree_frame.is_some() {
571            self.published = Some(parsed);
572            self.force_full = false;
573            if sent_delta {
574                self.deltas_sent += 1;
575            } else {
576                self.snapshots_sent += 1;
577            }
578        }
579
580        Ok(marker)
581    }
582
583    /// Patches this client has published.
584    pub fn deltas_sent(&self) -> u64 {
585        self.deltas_sent
586    }
587
588    /// Whole trees this client has published.
589    pub fn snapshots_sent(&self) -> u64 {
590        self.snapshots_sent
591    }
592
593    /// Records this adapter dropped locally, for being over budget or over a
594    /// limit. Each one left a gap in the sequence.
595    pub fn logs_dropped(&self) -> u64 {
596        self.logs_dropped
597    }
598
599    /// Forward one application log record, if the driver asked for logs.
600    ///
601    /// Returns whether the record went out. A record is dropped when the
602    /// session is not live, when the driver granted no budget, when this
603    /// adapter is over its rate, or when the record breaks a limit.
604    ///
605    /// Every attempt consumes a sequence number, dropped or not: the gap left
606    /// in `seq` is precisely how the driver learns records were lost here
607    /// rather than in transit.
608    ///
609    /// `seq` is assigned here whatever the caller set, because the adapter is
610    /// the only authority on it: the channel is open to several publishers,
611    /// and two of them can pick the same number in good faith. A caller's own
612    /// number is kept as the `origin.seq` attribute, which is a diagnostic
613    /// rather than a promise — it is dropped rather than allowed to push the
614    /// record over a limit.
615    pub fn log(&mut self, mut record: LogRecord) -> bool {
616        if self.session_id.is_none() || self.stream.is_none() || self.log_bucket.is_none() {
617            return false;
618        }
619
620        let origin = record.seq;
621        self.log_seq += 1;
622        record.seq = self.log_seq;
623        if record.ts == 0 {
624            record.ts = epoch_millis();
625        }
626        if record.revision.is_none() && self.revision > 0 {
627            record.revision = Some(self.revision);
628        }
629
630        let now = Instant::now();
631        let allowed = self
632            .log_bucket
633            .as_mut()
634            .is_some_and(|bucket| bucket.take(now));
635        if !allowed {
636            self.logs_dropped += 1;
637            return false;
638        }
639        if origin > 0 && record.attrs.len() < MAX_LOG_ATTRS {
640            // A hint is never worth turning a log line into a rejected frame,
641            // so it is backed out if it costs the record its validity.
642            record
643                .attrs
644                .insert("origin.seq".to_owned(), AttrValue::Int(origin));
645            if record.validate(&self.limits).is_err() {
646                record.attrs.remove("origin.seq");
647            }
648        }
649        if record.validate(&self.limits).is_err() {
650            // An oversized or malformed record is dropped locally rather than
651            // taking the channel down; the gap in seq reports it.
652            self.logs_dropped += 1;
653            return false;
654        }
655        self.send(&LogMessage::new(&record)).is_ok()
656    }
657
658    /// Convenience for the common call: a level and a message.
659    pub fn log_message(&mut self, level: LogLevel, message: impl Into<String>) -> bool {
660        self.log(LogRecord::new(level, message))
661    }
662
663    /// Read and answer whatever the driver has sent, without blocking.
664    ///
665    /// Call it on every render tick, or whenever convenient: `get-tree`
666    /// requests are answered from the retained snapshots.
667    ///
668    /// # Errors
669    /// Returns [`Error::Io`] if the channel broke, or [`Error::Parse`] if the
670    /// driver sent something the contract forbids.
671    pub fn poll(&mut self) -> Result<(), Error> {
672        let mut buffer = [0u8; 8192];
673        loop {
674            let read = match self.stream.as_mut() {
675                None => return Ok(()),
676                Some(stream) => stream.read(&mut buffer),
677            };
678            match read {
679                Ok(0) => {
680                    self.close();
681                    return Ok(());
682                }
683                Ok(count) => {
684                    let frames = self.decoder.push(&buffer[..count])?;
685                    for frame in frames {
686                        self.handle(&frame.value)?;
687                    }
688                }
689                Err(error)
690                    if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) =>
691                {
692                    return Ok(())
693                }
694                Err(error) if error.kind() == ErrorKind::Interrupted => continue,
695                Err(error) => {
696                    self.close();
697                    return Err(Error::Io(error));
698                }
699            }
700        }
701    }
702
703    fn handle(&mut self, value: &Value) -> Result<(), Error> {
704        if let Err(error) = parse_driver_message(value, &self.limits) {
705            self.debug_line(
706                Category::Diag,
707                &format!("rejected a driver message: {error}"),
708            );
709            let _ = self.send(&ProtocolErrorMessage::new("malformed", error.to_string()));
710            self.close();
711            return Err(Error::Parse(error));
712        }
713
714        match value.get("type").and_then(Value::as_str) {
715            Some("hello-ack") => {
716                let ack: HelloAck = serde_json::from_value(value.clone()).expect("validated above");
717                self.session_id = Some(ack.session_id);
718                self.limits = ack.limits;
719                self.marker_enabled = ack.marker.enabled;
720                self.log_budget = ack.logs;
721                self.log_bucket = match ack.logs {
722                    Some(budget) if budget.enabled => Some(TokenBucket::new(
723                        budget.max_records_per_second,
724                        budget.burst,
725                        Instant::now(),
726                    )),
727                    _ => None,
728                };
729                self.subscribe = ack.subscribe;
730                if let Some(log) = self.options.debug.as_ref() {
731                    let session = self.session_id.clone().unwrap_or_default();
732                    log.set_label(&session);
733                    log.line(
734                        Category::Sem,
735                        &format!(
736                            "hello-ack session={session} marker={} subscribe={} logs={}",
737                            on_off(self.marker_enabled),
738                            self.subscribe,
739                            on_off(self.log_bucket.is_some())
740                        ),
741                    );
742                }
743            }
744            Some("get-tree") => {
745                let request: GetTree =
746                    serde_json::from_value(value.clone()).expect("validated above");
747                let wanted = request.revision.unwrap_or(self.revision);
748                let held = self
749                    .history
750                    .iter()
751                    .find(|(revision, _)| *revision == wanted)
752                    .map(|(_, body)| body.clone());
753                let answer = match held {
754                    Some(body) => GetTreeResult::found(request.request_id, body),
755                    None => GetTreeResult::missing(
756                        request.request_id,
757                        format!("revision {wanted} is not retained"),
758                    ),
759                };
760                self.send(&answer)?;
761            }
762            Some("error") => {
763                self.debug_line(
764                    Category::Diag,
765                    &format!(
766                        "driver ended the session: {}",
767                        value.get("code").and_then(Value::as_str).unwrap_or("?")
768                    ),
769                );
770                self.close();
771            }
772            _ => {}
773        }
774        Ok(())
775    }
776
777    fn remember(&mut self, revision: i64, body: Box<RawValue>) {
778        self.history.push_back((revision, body));
779        while self.history.len() > SNAPSHOT_HISTORY {
780            self.history.pop_front();
781        }
782    }
783
784    fn send<T: serde::Serialize>(&mut self, message: &T) -> Result<(), Error> {
785        let frame = encode_frame(message, self.limits.max_frame_bytes)?;
786        self.write_frame(&frame)
787    }
788
789    fn write_frame(&mut self, frame: &[u8]) -> Result<(), Error> {
790        let Some(stream) = self.stream.as_mut() else {
791            return Ok(());
792        };
793        match stream.write_all(frame).and_then(|()| stream.flush()) {
794            Ok(()) => Ok(()),
795            Err(error) => {
796                let timed_out = matches!(
797                    error.kind(),
798                    ErrorKind::WouldBlock | ErrorKind::TimedOut | ErrorKind::Interrupted
799                );
800                self.close();
801                if timed_out {
802                    // `write_all` may have delivered part of a length-prefixed
803                    // frame, and there is no resynchronisation point in the
804                    // stream, so the session is unrecoverable rather than slow.
805                    self.debug_line(
806                        Category::Diag,
807                        "write deadline exceeded; session is unrecoverable",
808                    );
809                    return Err(Error::WriteTimeout);
810                }
811                Err(Error::Io(error))
812            }
813        }
814    }
815}