1use std::collections::HashMap;
12use std::io::{Read, Write};
13use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
14use std::sync::{Arc, Mutex, OnceLock};
15use std::time::{Duration, Instant};
16
17use packset_client::{Error, Hit};
18use serde::de::DeserializeOwned;
19use serde_json::Value;
20use url::Url;
21
22pub struct PacksetClient {
24 base: String,
25 origin: Url,
26 addr: Option<SocketAddr>,
27 workspace: Option<String>,
28 token: Mutex<Option<String>>,
29}
30
31impl Clone for PacksetClient {
32 fn clone(&self) -> Self {
33 Self {
34 base: self.base.clone(),
35 origin: self.origin.clone(),
36 addr: self.addr,
37 workspace: self.workspace.clone(),
38 token: Mutex::new(self.token.lock().unwrap_or_else(|p| p.into_inner()).clone()),
39 }
40 }
41}
42
43#[derive(Debug)]
46pub(crate) struct Answer {
47 pub status: u16,
48 pub body: String,
49 pub trusted: bool,
50}
51
52struct Slot {
53 stream: Option<TcpStream>,
54 proven: bool,
56 token_tag: u64,
57 carried: bool,
59}
60
61fn slots() -> &'static Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>> {
62 static SLOTS: OnceLock<Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>>> = OnceLock::new();
63 SLOTS.get_or_init(|| Mutex::new(HashMap::new()))
64}
65
66fn slot_for(addr: SocketAddr) -> Arc<Mutex<Slot>> {
67 let mut map = slots().lock().unwrap_or_else(|p| p.into_inner());
68 map.entry(addr)
69 .or_insert_with(|| {
70 Arc::new(Mutex::new(Slot {
71 stream: None,
72 proven: false,
73 token_tag: 0,
74 carried: false,
75 }))
76 })
77 .clone()
78}
79
80fn load_token() -> Option<String> {
81 packset_client::token()
82}
83
84fn token_tag(token: Option<&str>) -> u64 {
85 let Some(token) = token else {
86 return 0;
87 };
88 let mut hash = 0xcbf29ce484222325u64;
89 for byte in token.as_bytes() {
90 hash ^= u64::from(*byte);
91 hash = hash.wrapping_mul(0x100000001b3);
92 }
93 hash
94}
95
96fn nonce() -> Option<String> {
97 use std::io::Read as _;
98 let mut raw = [0u8; 32];
99 std::fs::File::open("/dev/urandom")
100 .and_then(|mut file| file.read_exact(&mut raw))
101 .ok()
102 .map(|()| hex::encode(raw))
103}
104
105fn pin(url: &Url) -> Option<SocketAddr> {
106 let host = url.host_str()?;
107 let port = url.port_or_known_default()?;
108 (host, port).to_socket_addrs().ok()?.next()
109}
110
111fn host_header(url: &Url) -> String {
112 match (url.host_str(), url.port()) {
113 (Some(host), Some(port)) if host.contains(':') => format!("[{host}]:{port}"),
114 (Some(host), Some(port)) => format!("{host}:{port}"),
115 (Some(host), None) => host.to_string(),
116 _ => "localhost".to_string(),
117 }
118}
119
120fn budget() -> Duration {
121 std::env::var("PACKSET_TIMEOUT_MS")
122 .ok()
123 .and_then(|v| v.trim().parse::<u64>().ok())
124 .filter(|ms| *ms > 0)
125 .map_or(Duration::from_secs(30), Duration::from_millis)
126}
127
128fn foreign(url: &str) -> bool {
131 #[cfg(target_os = "linux")]
132 {
133 use std::os::unix::fs::MetadataExt;
134 let Some(port) = loopback_port(url) else {
135 return false;
136 };
137 let Ok(me) = std::fs::metadata("/proc/self").map(|m| m.uid()) else {
138 return false;
139 };
140 let owners: Vec<u32> = ["/proc/net/tcp", "/proc/net/tcp6"]
141 .iter()
142 .filter_map(|table| std::fs::read_to_string(table).ok())
143 .flat_map(|text| listen_uids(&text, port))
144 .collect();
145 !owners.is_empty() && owners.iter().all(|uid| *uid != me)
146 }
147 #[cfg(not(target_os = "linux"))]
148 {
149 let _ = url;
150 false
151 }
152}
153
154fn loopback_port(url: &str) -> Option<u16> {
155 let rest = url
156 .strip_prefix("http://")
157 .or_else(|| url.strip_prefix("https://"))?;
158 let authority = rest.split(['/', '?', '#']).next()?;
159 let (host, port) = match authority.rsplit_once(':') {
160 Some((host, port)) if !port.contains(']') => (host, port.parse().ok()?),
161 _ => (authority, 80),
162 };
163 matches!(host, "127.0.0.1" | "localhost" | "[::1]").then_some(port)
164}
165
166fn listen_uids(table: &str, port: u16) -> Vec<u32> {
167 table
168 .lines()
169 .skip(1)
170 .filter_map(|line| {
171 let fields: Vec<&str> = line.split_whitespace().collect();
172 if fields.get(3) != Some(&"0A") {
173 return None;
174 }
175 let hex = fields.get(1)?.rsplit(':').next()?;
176 if u16::from_str_radix(hex, 16).ok()? != port {
177 return None;
178 }
179 fields.get(7)?.parse().ok()
180 })
181 .collect()
182}
183
184fn target(path: &str, query: &[(&str, &str)]) -> String {
185 if query.is_empty() {
186 return path.to_string();
187 }
188 let mut ser = url::form_urlencoded::Serializer::new(String::new());
189 for (key, value) in query {
190 ser.append_pair(key, value);
191 }
192 format!("{path}?{}", ser.finish())
193}
194
195struct Raw {
196 status: u16,
197 body: String,
198 keep: bool,
199}
200
201fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
202 if side_budget_spent() {
203 return Err(budget_spent());
204 }
205 let started = Instant::now();
206 let result = connect_once(addr, timeout);
207 if result.is_err() {
210 super::charge_side_wait(started.elapsed());
211 }
212 result
213}
214
215fn side_budget_spent() -> bool {
219 super::side_remaining_ms() == Some(0)
220}
221
222fn budget_spent() -> std::io::Error {
223 std::io::Error::new(
224 std::io::ErrorKind::TimedOut,
225 "the side event's pack budget is spent",
226 )
227}
228
229fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
230 let wait = capped_wait(timeout);
231 let stream = TcpStream::connect_timeout(&addr, wait)?;
232 stream.set_read_timeout(Some(wait))?;
233 stream.set_write_timeout(Some(wait))?;
234 let _ = stream.set_nodelay(true);
235 Ok(stream)
236}
237
238fn capped_wait(timeout: Duration) -> Duration {
241 let wait = super::cap_pack_wait(timeout);
242 if wait.is_zero() {
243 Duration::from_millis(1)
244 } else {
245 wait
246 }
247}
248
249fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
250 let mut sent = 0;
251 while sent < bytes.len() {
252 match stream.write(&bytes[sent..]) {
253 Ok(0) => {
254 return Err(std::io::Error::new(
255 std::io::ErrorKind::WriteZero,
256 "the pack closed the socket",
257 ))
258 }
259 Ok(n) => sent += n,
260 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
261 Err(err) => return Err(err),
262 }
263 }
264 stream.flush()
265}
266
267fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
268 let mut buf = Vec::new();
269 let mut tmp = [0u8; 4096];
270 let head_end = loop {
271 if buf.len() > 64 * 1024 {
272 return Err(std::io::Error::new(
273 std::io::ErrorKind::InvalidData,
274 "the pack's headers are too long",
275 ));
276 }
277 arm_read(stream, deadline)?;
278 let n = read_some(stream, &mut tmp)?;
279 buf.extend_from_slice(&tmp[..n]);
280 if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
281 break at;
282 }
283 };
284 let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
285 let status = header
286 .lines()
287 .next()
288 .and_then(|line| line.split_whitespace().nth(1))
289 .and_then(|code| code.parse().ok())
290 .ok_or_else(|| {
291 std::io::Error::new(
292 std::io::ErrorKind::InvalidData,
293 "the pack's status line is not HTTP",
294 )
295 })?;
296 let mut length = None;
297 let mut close = header.starts_with("HTTP/1.0");
298 for line in header.lines().skip(1) {
299 let Some((name, value)) = line.split_once(':') else {
300 continue;
301 };
302 if name.eq_ignore_ascii_case("content-length") {
303 length = value.trim().parse().ok();
304 } else if name.eq_ignore_ascii_case("connection")
305 && value.trim().eq_ignore_ascii_case("close")
306 {
307 close = true;
308 }
309 }
310 let mut body = buf.split_off(head_end + 4);
311 if let Some(length) = length {
312 while body.len() < length {
313 arm_read(stream, deadline)?;
314 let n = read_some(stream, &mut tmp)?;
315 body.extend_from_slice(&tmp[..n]);
316 if body.len() > length.saturating_add(64 * 1024) {
317 break;
318 }
319 }
320 body.truncate(length);
321 } else {
322 loop {
323 if deadline.saturating_duration_since(Instant::now()).is_zero() {
324 break;
325 }
326 let _ = arm_read(stream, deadline);
327 match stream.read(&mut tmp) {
328 Ok(0) => break,
329 Ok(n) => body.extend_from_slice(&tmp[..n]),
330 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
331 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
332 Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
333 Err(_) => break,
334 }
335 }
336 close = true;
337 }
338 Ok(Raw {
340 status,
341 body: String::from_utf8_lossy(&body).into_owned(),
342 keep: !close && !(300..400).contains(&status),
343 })
344}
345
346fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
347 let remain = deadline.saturating_duration_since(Instant::now());
348 if remain.is_zero() {
349 return Err(std::io::Error::new(
350 std::io::ErrorKind::TimedOut,
351 "the pack did not answer",
352 ));
353 }
354 stream.set_read_timeout(Some(remain))?;
355 Ok(())
356}
357
358fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
359 loop {
360 match stream.read(tmp) {
361 Ok(0) => {
362 return Err(std::io::Error::new(
363 std::io::ErrorKind::UnexpectedEof,
364 "the pack closed the socket",
365 ))
366 }
367 Ok(n) => return Ok(n),
368 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
369 Err(err) => return Err(err),
370 }
371 }
372}
373
374fn render(
375 method: &str,
376 host: &str,
377 path: &str,
378 token: Option<&str>,
379 body: Option<&[u8]>,
380) -> Vec<u8> {
381 let mut head =
382 format!("{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\n");
383 if let Some(token) = token {
384 head.push_str("Authorization: Bearer ");
385 head.push_str(token);
386 head.push_str("\r\n");
387 }
388 let mut out = Vec::new();
389 if let Some(body) = body {
390 head.push_str("Content-Type: application/json\r\n");
391 head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
392 out.extend(head.into_bytes());
393 out.extend_from_slice(body);
394 } else {
395 head.push_str("\r\n");
396 out.extend(head.into_bytes());
397 }
398 out
399}
400
401struct Wire<'a> {
402 method: &'a str,
403 path: &'a str,
404 query: &'a [(&'a str, &'a str)],
405 body: Option<&'a [u8]>,
406 token: Option<&'a str>,
407}
408
409enum Gate {
410 Send,
412 Bare,
414 Busy(Raw),
416}
417
418impl PacksetClient {
419 pub fn new(base: impl Into<String>) -> Self {
420 let mut base = base.into();
421 while base.ends_with('/') {
422 base.pop();
423 }
424 let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
425 let addr = Url::parse(&base).ok().as_ref().and_then(pin);
426 Self {
427 base,
428 origin,
429 addr,
430 workspace: None,
431 token: Mutex::new(load_token()),
432 }
433 }
434
435 pub fn from_env() -> Result<Self, Error> {
437 packset_client::load_seat_env();
438 let url = std::env::var("PACKSET_URL")
439 .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
440 .ok()
441 .filter(|url| !url.is_empty());
442 match url {
443 Some(url) if url == "off" => Err(Error::NoUrl),
444 Some(url) => Ok(Self::new(url)),
445 None => Ok(Self::new(format!(
446 "http://127.0.0.1:{}",
447 packset_client::default_port()
448 ))),
449 }
450 }
451
452 #[must_use]
453 pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
454 let workspace = workspace.into();
455 self.workspace = (!workspace.is_empty()).then_some(workspace);
456 self
457 }
458
459 pub fn base(&self) -> &str {
460 &self.base
461 }
462
463 #[must_use]
465 pub(crate) fn has_token(&self) -> bool {
466 load_token().is_some()
467 }
468
469 #[must_use]
472 pub(crate) fn may_carry(&self) -> bool {
473 load_token().is_some() && !foreign(&self.base)
474 }
475
476 #[must_use]
479 pub(crate) fn carried(&self) -> bool {
480 let Some(addr) = self.addr else {
481 return false;
482 };
483 let map = slots().lock().unwrap_or_else(|p| p.into_inner());
484 map.get(&addr)
485 .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
486 }
487
488 pub fn workspace(&self) -> String {
489 if let Some(workspace) = &self.workspace {
490 return workspace.clone();
491 }
492 if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
493 if !workspace.is_empty() {
494 return workspace;
495 }
496 }
497 let cwd = std::env::var("GROKOS_WORKSPACE")
498 .ok()
499 .map(|s| s.trim().to_string())
500 .filter(|s| !s.is_empty())
501 .map(std::path::PathBuf::from)
502 .or_else(|| std::env::current_dir().ok())
503 .unwrap_or_else(|| std::path::PathBuf::from("."));
504 let abs = cwd.canonicalize().unwrap_or(cwd);
505 let shown = abs.to_string_lossy().into_owned();
506 if let Ok(answer) = self.roundtrip(
507 "GET",
508 "/v1/identity",
509 &[("cwd", shown.as_str())],
510 None,
511 true,
512 budget(),
513 ) {
514 if answer.status == 200 {
515 if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
516 if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
517 if !ws.is_empty() {
518 return ws.to_string();
519 }
520 }
521 }
522 }
523 }
524 format!("dir:{}", abs.display())
525 }
526
527 pub(crate) fn roundtrip(
532 &self,
533 method: &str,
534 path: &str,
535 query: &[(&str, &str)],
536 body: Option<&[u8]>,
537 bearer: bool,
538 timeout: Duration,
539 ) -> Result<Answer, Error> {
540 let Some(addr) = self.addr else {
541 return Err(Error::Bad(format!(
542 "{} is not a socket this seat can pin",
543 self.base
544 )));
545 };
546 let token = load_token();
547 *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
548 let allow = bearer && token.is_some() && !foreign(&self.base);
549 self.exchange(
550 addr,
551 &Wire {
552 method,
553 path,
554 query,
555 body,
556 token: token.as_deref(),
557 },
558 allow,
559 timeout,
560 )
561 }
562
563 fn exchange(
564 &self,
565 addr: SocketAddr,
566 call: &Wire<'_>,
567 allow: bool,
568 timeout: Duration,
569 ) -> Result<Answer, Error> {
570 let slot = slot_for(addr);
571 let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
572 let tag = token_tag(call.token);
573 if live.token_tag != tag {
574 live.stream = None;
575 live.proven = false;
576 live.token_tag = tag;
577 }
578 let carried = if allow {
579 match self.gate(&mut live, addr, timeout)? {
580 Gate::Bare => None,
581 Gate::Send => call.token,
582 Gate::Busy(raw) => {
583 live.carried = false;
584 live.proven = false;
585 return Ok(Answer {
586 status: raw.status,
587 body: raw.body,
588 trusted: false,
589 });
590 }
591 }
592 } else {
593 None
594 };
595 if live.stream.is_none() {
596 match connect(addr, timeout) {
597 Ok(stream) => live.stream = Some(stream),
598 Err(err) => {
599 live.proven = false;
600 live.carried = false;
601 return Err(Error::Io(err));
602 }
603 }
604 live.proven = false;
608 }
609 let raw = match send(
610 &mut live,
611 &self.origin,
612 &Wire {
613 token: carried,
614 ..*call
615 },
616 timeout,
617 ) {
618 Ok(raw) => raw,
619 Err(err) => {
620 live.stream = None;
621 live.proven = false;
622 live.carried = false;
623 return Err(Error::Io(err));
624 }
625 };
626 let trusted = carried.is_some();
627 live.carried = trusted;
628 if trusted && live.stream.is_some() {
629 live.proven = true;
630 } else if live.stream.is_none() {
631 live.proven = false;
632 }
633 Ok(Answer {
634 status: raw.status,
635 body: raw.body,
636 trusted,
637 })
638 }
639
640 fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
643 if live.proven && live.stream.is_some() {
644 return Ok(Gate::Send);
645 }
646 live.proven = false;
647 let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
648 return Err(Error::Io(std::io::Error::other(
649 "no randomness for the pack proof",
650 )));
651 };
652 let workspace = self.proof_workspace();
653 let path = format!("/v1/atoms/{}", &id[..32]);
654 let query = [("workspace", workspace.as_str())];
655 let mut stage = 0u8;
656 let mut restarts = 0u8;
657 loop {
658 if live.stream.is_none() {
659 live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
660 live.proven = false;
661 }
662 let shown = if stage == 0 {
663 None
664 } else {
665 Some(bogus.as_str())
666 };
667 let raw = match send(
668 live,
669 &self.origin,
670 &Wire {
671 method: "GET",
672 path: &path,
673 query: &query,
674 body: None,
675 token: shown,
676 },
677 timeout,
678 ) {
679 Ok(raw) => raw,
680 Err(err) => {
681 live.stream = None;
682 live.proven = false;
683 restarts += 1;
684 if restarts > 2 {
685 return Err(Error::Io(err));
686 }
687 stage = 0;
688 continue;
689 }
690 };
691 if raw.status == 503 {
692 return Ok(Gate::Busy(raw));
693 }
694 if raw.status != 401 {
695 return Ok(Gate::Bare);
696 }
697 if stage == 0 {
698 stage = 1;
699 continue;
700 }
701 return Ok(Gate::Send);
702 }
703 }
704
705 fn proof_workspace(&self) -> String {
706 if let Some(workspace) = &self.workspace {
707 return workspace.clone();
708 }
709 std::env::var("PACKSET_WORKSPACE")
710 .ok()
711 .filter(|w| !w.is_empty())
712 .unwrap_or_else(|| "seat".to_string())
713 }
714
715 fn tried(
717 &self,
718 method: &str,
719 path: &str,
720 query: &[(&str, &str)],
721 body: Option<&[u8]>,
722 timeout: Duration,
723 ) -> Result<Answer, Error> {
724 let started = Instant::now();
725 let mut wait = Duration::from_millis(50);
726 for n in 0..4 {
727 let answer = self.roundtrip(method, path, query, body, true, timeout)?;
728 if answer.status == 503 && n < 3 && started.elapsed() + wait < timeout {
729 std::thread::sleep(wait);
730 wait *= 3;
731 continue;
732 }
733 return Ok(answer);
734 }
735 Err(Error::Bad(format!(
736 "{}{path}: 503: packsetd is busy",
737 self.base
738 )))
739 }
740
741 fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
742 if !(200..300).contains(&answer.status) {
743 return Err(refused(
744 &format!("{}{path}", self.base),
745 answer.status,
746 &answer.body,
747 ));
748 }
749 serde_json::from_str(&answer.body).map_err(Error::from)
750 }
751
752 pub fn health(&self) -> Result<String, Error> {
753 let answer = self.tried("GET", "/health", &[], None, budget())?;
754 if !(200..300).contains(&answer.status) {
755 return Err(refused(
756 &format!("{}/health", self.base),
757 answer.status,
758 &answer.body,
759 ));
760 }
761 Ok(answer.body)
762 }
763
764 pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
765 let query: Vec<(&str, &str)> = workspace
766 .map(|workspace| vec![("workspace", workspace)])
767 .unwrap_or_default();
768 let answer = self.tried("GET", "/v1/status", &query, None, budget())?;
769 self.json_ok("/v1/status", answer)
770 }
771
772 pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
773 let bytes = serde_json::to_vec(atom)?;
774 let answer = self.tried("POST", "/v1/atoms", &[], Some(&bytes), budget())?;
775 self.json_ok("/v1/atoms", answer)
776 }
777
778 pub fn delete_atom(
779 &self,
780 workspace: &str,
781 id: &str,
782 why: Option<&str>,
783 ) -> Result<Value, Error> {
784 let mut body = serde_json::json!({"workspace": workspace, "id": id});
785 if let Some(accession) = why {
786 body["why"] = Value::String(accession.to_string());
787 }
788 let bytes = serde_json::to_vec(&body)?;
789 let answer = self.tried("POST", "/v1/atoms/delete", &[], Some(&bytes), budget())?;
790 if answer.status == 404 {
791 return Err(Error::Bad(format!("no atom {id}")));
792 }
793 self.json_ok("/v1/atoms/delete", answer)
794 }
795
796 pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
797 let answer = self.tried(
798 "GET",
799 "/v1/atoms",
800 &[("workspace", workspace), ("kind", kind)],
801 None,
802 budget(),
803 )?;
804 let body: Value = self.json_ok("/v1/atoms", answer)?;
805 Ok(body
806 .get("atoms")
807 .and_then(Value::as_array)
808 .cloned()
809 .unwrap_or_default())
810 }
811
812 pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
813 let answer = self.tried(
814 "GET",
815 "/v1/pack",
816 &[("workspace", workspace), ("set", set)],
817 None,
818 budget(),
819 )?;
820 let body: Value = self.json_ok("/v1/pack", answer)?;
821 Ok(body
822 .get("atoms")
823 .and_then(Value::as_array)
824 .cloned()
825 .unwrap_or_default())
826 }
827
828 pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
829 let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
830 let answer = self.tried("POST", "/v1/sweep", &[], Some(&bytes), budget())?;
831 self.json_ok("/v1/sweep", answer)
832 }
833
834 pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
835 let bytes = serde_json::to_vec(&serde_json::json!({
836 "workspace": workspace,
837 "id": id,
838 "recalled": recalled,
839 }))?;
840 let answer = self.tried("POST", "/v1/grade", &[], Some(&bytes), budget())?;
841 self.json_ok("/v1/grade", answer)
842 }
843
844 pub fn activate_as(
845 &self,
846 workspace: &str,
847 q: &str,
848 limit: u32,
849 fire: bool,
850 lens: Option<&str>,
851 ) -> Result<Value, Error> {
852 let limit = limit.to_string();
853 let fire = if fire { "1" } else { "0" };
854 let mut query = vec![
855 ("workspace", workspace),
856 ("q", q),
857 ("limit", limit.as_str()),
858 ("fire", fire),
859 ];
860 if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
861 query.push(("as", name));
862 }
863 let answer = self.tried("GET", "/v1/activate", &query, None, budget())?;
864 self.json_ok("/v1/activate", answer)
865 }
866
867 pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
868 let limit = limit.to_string();
869 let answer = self.tried(
870 "GET",
871 "/v1/hubs",
872 &[("workspace", workspace), ("limit", limit.as_str())],
873 None,
874 budget(),
875 )?;
876 self.json_ok("/v1/hubs", answer)
877 }
878
879 pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
880 let bytes =
881 serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
882 let answer = self.tried("POST", "/v1/consolidate", &[], Some(&bytes), budget())?;
883 self.json_ok("/v1/consolidate", answer)
884 }
885
886 pub fn search_opts(
887 &self,
888 workspace: &str,
889 q: &str,
890 limit: u32,
891 as_of: Option<&str>,
892 rerank: bool,
893 ) -> Result<Vec<Hit>, Error> {
894 Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
895 }
896
897 pub(crate) fn search_fetched(
898 &self,
899 workspace: &str,
900 q: &str,
901 limit: u32,
902 as_of: Option<&str>,
903 rerank: bool,
904 ) -> Result<(Vec<Hit>, bool), Error> {
905 let limit_s = limit.to_string();
906 let mut query = vec![
907 ("workspace", workspace),
908 ("q", q),
909 ("limit", limit_s.as_str()),
910 ];
911 if let Some(at) = as_of {
912 query.push(("as_of", at));
913 }
914 if rerank {
915 query.push(("rerank", "1"));
916 }
917 let timeout = if rerank {
918 Duration::from_secs(60).max(budget())
919 } else {
920 budget()
921 };
922 let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
923 let trusted = answer.trusted;
924 let body: Value = self.json_ok("/v1/search", answer)?;
925 let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
926 Ok((serde_json::from_value(hits)?, trusted))
927 }
928
929 pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
930 let bytes = serde_json::to_vec(&serde_json::json!({
931 "workspace": workspace,
932 "ids": ids,
933 "as": "",
934 }))?;
935 let answer = self.tried("POST", "/v1/fire", &[], Some(&bytes), budget())?;
936 self.json_ok("/v1/fire", answer)
937 }
938}
939
940fn refused(url: &str, code: u16, body: &str) -> Error {
941 let reason = serde_json::from_str::<Value>(body)
942 .ok()
943 .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
944 .unwrap_or_else(|| body.to_string());
945 let reason = reason.trim();
946 if code == 401 {
947 return Error::Bad(format!(
948 "{url}: 401: {reason}; this seat's token does not match the writer's, \
949 so it is another user's writer or PACKSET_HOME names another pack"
950 ));
951 }
952 if reason.is_empty() {
953 Error::Bad(format!("{url}: status code {code}"))
954 } else {
955 Error::Bad(format!("{url}: {code}: {reason}"))
956 }
957}
958
959fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
960 if side_budget_spent() {
961 return Err(budget_spent());
962 }
963 let started = Instant::now();
964 let result = send_once(live, origin, call, timeout);
965 if result.is_err() {
968 super::charge_side_wait(started.elapsed());
969 }
970 result
971}
972
973fn send_once(
974 live: &mut Slot,
975 origin: &Url,
976 call: &Wire<'_>,
977 timeout: Duration,
978) -> std::io::Result<Raw> {
979 let stream = live
980 .stream
981 .as_mut()
982 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
983 let wait = capped_wait(timeout);
984 let deadline = Instant::now() + wait;
985 stream.set_read_timeout(Some(wait))?;
986 stream.set_write_timeout(Some(wait))?;
987 let bytes = render(
988 call.method,
989 &host_header(origin),
990 &target(call.path, call.query),
991 call.token,
992 call.body,
993 );
994 if let Err(err) = write_all(stream, &bytes) {
995 live.stream = None;
996 live.proven = false;
997 return Err(err);
998 }
999 match read_response(stream, deadline) {
1000 Ok(raw) => {
1001 if !raw.keep {
1002 live.stream = None;
1003 live.proven = false;
1004 }
1005 Ok(raw)
1006 }
1007 Err(err) => {
1008 live.stream = None;
1009 live.proven = false;
1010 Err(err)
1011 }
1012 }
1013}