daemon/status.bend

cswap-bend-cli@0.1.0.0 · 100 lines · 18 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/fs.bend as Fs
4import cswap-bend-cli@0.1.0.0/fs/write.bend as FsW
5import cswap-bend-cli@0.1.0.0/act.bend as Act
6import cswap-bend-cli@0.1.0.0/daemon/config.bend as Cfg
7
8# <state>/daemon-status.json, written atomically (0600) at the end of each
9# tick so `cswap daemon status` can say what the loop is doing without
10# touching the network. Percentages are centi-percent U32 (printed 12.5);
11# times are ISO UTC stamps. `config` is the effective autoswitch config of the
12# running loop (settings.json overridden by the command line), so `cswap tui`
13# shows what the daemon uses and not the file default.
14
15type Entry is Data:
16  Entry{slot: String, pct5: Maybe<&2, U32>, pct7: Maybe<&2, U32>, fetched_at: Maybe<&2, String>, backoff_until: Maybe<&2, String>, last_error: Maybe<&2, String>}
17
18type Status is Data:
19  Status{pid: U32, version: String, started_at: String, last_poll_at: Maybe<&2, String>, next_poll_at: Maybe<&2, String>, active: Maybe<&2, String>, entries: List<&2, Entry>, cfg: Cfg.Cfg}
20
21def status_path(+state_dir: String) -> String:
22  state_dir ++ "/daemon-status.json"
23
24def pick_s(+c: Bool, +x: String, +y: String) -> String:
25  match c:
26    case True{}:
27      x
28    case False{}:
29      y
30
31def two(+n: U32) -> String:
32  pick_s(U32.is_lt(n, 10), "0" ++ U32.show(n), U32.show(n))
33
34def frac(+c: U32) -> String:
35  pick_s(U32.is_eq((c % 10 : U32), 0), U32.show((c / 10 : U32)), two(c))
36
37def pct(+c: U32) -> Json.Json:
38  Json.JNum{U32.show((c / 100 : U32)) ++ "." ++ frac((c % 100 : U32))}
39
40def opt_pct(+m: Maybe<&2, U32>) -> Json.Json:
41  match m:
42    case Some{c}:
43      pct(c)
44    case None{}:
45      Json.JNull{}
46
47def opt_str(+m: Maybe<&2, String>) -> Json.Json:
48  match m:
49    case Some{s}:
50      Json.JStr{s}
51    case None{}:
52      Json.JNull{}
53
54def f(+k: String, +v: Json.Json, +rest: Json.Json) -> Json.Json:
55  Json.JField{k, v, rest}
56
57def entry_json(e: Entry) -> Json.Json:
58  match e:
59    case Entry{s, p5, p7, fa, bu, le}:
60      Json.JObj{f("pct_5h", opt_pct(p5), f("pct_7d", opt_pct(p7), f("fetched_at", opt_str(fa), f("backoff_until", opt_str(bu), f("last_error", opt_str(le), Json.JNil{})))))}
61
62def entry_slot(e: Entry) -> String:
63  match e:
64    case Entry{s, p5, p7, fa, bu, le}:
65      s
66
67def entries_chain(+es: List<&2, Entry>) -> Json.Json:
68  match es:
69    case Nil{}:
70      Json.JNil{}
71    case Con{e, t}:
72      f(entry_slot(e), entry_json(e), entries_chain(t))
73
74def opt_num(+m: Maybe<&2, String>) -> Json.Json:
75  match m:
76    case Some{s}:
77      Json.JNum{s}
78    case None{}:
79      Json.JNull{}
80
81def str_arr(+xs: List<&2, String>) -> List<&2, Json.Json>:
82  match xs:
83    case Nil{}:
84      Nil{}
85    case Con{h, t}:
86      Con{Json.JStr{h}, str_arr(t)}
87
88def cfg_json(+c: Cfg.Cfg) -> Json.Json:
89  Json.JObj{f("threshold", pct(Cfg.cfg_threshold(c)), f("interval_seconds", Json.JNum{U32.show(Cfg.cfg_interval(c))}, f("cooldown_seconds", Json.JNum{U32.show(Cfg.cfg_cooldown(c))},
90    f("strategy", Json.JStr{Cfg.cfg_strategy(c)}, f("models", Json.arr(str_arr(Cfg.cfg_models(c))), f("include_api_keys", Json.JBool{Cfg.cfg_include_api(c)}, Json.JNil{}))))))}
91
92def to_json(s: Status) -> Json.Json:
93  match s:
94    case Status{pid, version, started, last_poll, next_poll, active, es, cfg}:
95      Json.JObj{f("schemaVersion", Json.JNum{"1"}, f("pid", Json.JNum{U32.show(pid)}, f("version", Json.JStr{version}, f("started_at", Json.JStr{started},
96        f("last_poll_at", opt_str(last_poll), f("next_poll_at", opt_str(next_poll), f("active", opt_num(active), f("config", cfg_json(cfg), f("accounts", Json.JObj{entries_chain(es)}, Json.JNil{})))))))))}
97
98# Replace the status file (temp file + rename, 0600).
99def write(+state_dir: String, s: Status) -> Act.of(Unit):
100  FsW.write_atomic(status_path(state_dir), Json.emit_pretty(to_json(s)), "600")