daemon/view.bend
cswap-bend-cli@0.1.0.0 · 285 lines · 48 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/store.bend as Store
4import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
5import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
6import cswap-bend-cli@0.1.0.0/usage.bend as Usage
7import cswap-bend-cli@0.1.0.0/usage/cred.bend as UCred
8import cswap-bend-cli@0.1.0.0/policy/types.bend as T
9import cswap-bend-cli@0.1.0.0/policy/usage.bend as PU
10import cswap-bend-cli@0.1.0.0/policy/rank.bend as PR
11import cswap-bend-cli@0.1.0.0/daemon/rows.bend as Rows
12import cswap-bend-cli@0.1.0.0/daemon/events.bend as Ev
13import cswap-bend-cli@0.1.0.0/daemon/config.bend as Cfg
14import cswap-bend-cli@0.1.0.0/daemon/gather.bend as G
15import cswap-bend-cli@0.1.0.0/daemon/state.bend as St
16
17
18
19
20def nat(+n: U32) -> Nat:
21 U32.to_nat(n)
22
23def strategy2(+cf: Bool) -> T.Strategy:
24 match cf:
25 case True{}:
26 T.ConsumeFirst{}
27 case False{}:
28 T.Best{}
29
30def strategy_of(+s: String) -> T.Strategy:
31 strategy2(String.eq(s, "consume-first"))
32
33
34def pol_cfg(+c: Cfg.Cfg) -> T.Config:
35 match c:
36 case Cfg.Cfg{th, iv, cd, hy, st, ia, ut, ms}:
37 T.Config{nat(th), ms, strategy_of(st), nat(iv), nat(cd), nat(hy), 180n, nat(ut), ia}
38
39
40
41def sel_mt(+c: Bool, +x: Maybe<&2, T.Trigger>, +y: Maybe<&2, T.Trigger>) -> Maybe<&2, T.Trigger>:
42 match c:
43 case True{}:
44 x
45 case False{}:
46 y
47
48def trigger_of(+s: String) -> Maybe<&2, T.Trigger>:
49 sel_mt(String.eq(s, "proactive"), Some{T.Proactive{}},
50 sel_mt(String.eq(s, "at-limit"), Some{T.AtLimit{}},
51 sel_mt(String.eq(s, "failover"), Some{T.Failover{}},
52 sel_mt(String.eq(s, "consume-first"), Some{T.ConsumeFirstT{}}, None{}))))
53
54def num_nat(+m: Maybe<&2, Json.Json>) -> Maybe<&2, Nat>:
55 Rows.nat_opt(Rows.bind_s(Rows.num_text(m), t => Rows.epoch_text(t)))
56
57def centi_nat(+m: Maybe<&2, Json.Json>) -> Maybe<&2, Nat>:
58 Rows.nat_opt(Rows.bind_s(Rows.num_text(m), t => Cfg.centi(t)))
59
60def slot_nat(+m: Maybe<&2, Json.Json>) -> Maybe<&2, Nat>:
61 Rows.nat_opt(Rows.bind_s(Rows.str_text(m), t => Cfg.read_u32(t)))
62
63def snap2(+tr: Maybe<&2, Json.Json>, +st: Json.Json) -> T.Snap:
64 match tr:
65 case Some{Json.JStr{s}}:
66 T.Snap{centi_nat(Json.get("leftHeadroom", st)), num_nat(Json.get("leftRecoveryAt", st)), trigger_of(s)}
67 case _:
68 T.NoSnap{}
69
70def snap_of(+st: Json.Json) -> T.Snap:
71 snap2(Json.get("leftTrigger", st), st)
72
73def state_of(+st: Json.Json) -> T.State:
74 T.State{Rows.nat_opt(St.last_switch_at(st)), slot_nat(Json.get("lastSwitchTo", st)), num_nat(Json.get("lastSwitchFrom", st)), snap_of(st)}
75
76
77
78def accounts_of(+es: List<&2, G.Ent>) -> List<&2, T.Account>:
79 match es:
80 case Nil{}:
81 []
82 case Con{G.Ent{s, e, o, c, a}, t}:
83 a <> accounts_of(t)
84
85def oauth_of(+creds: String) -> Json.Json:
86 SSeq.get_or("claudeAiOauth", Json.unwrap(Json.parse(creds)), Json.JNull{})
87
88def sel_sn(+c: Bool, +x: T.Sentinel, +y: T.Sentinel) -> T.Sentinel:
89 match c:
90 case True{}:
91 x
92 case False{}:
93 y
94
95def sent2(+is_active: Bool, +has: Bool, +api: Bool, +creds: String, +now: U32) -> T.Sentinel:
96 match has api:
97 case False{} _:
98 T.OtherSentinel{}
99 case True{} True{}:
100 T.OtherSentinel{}
101 case True{} False{}:
102 sel_sn(is_active && UCred.expired(oauth_of(creds), now), T.TokenExpired{}, T.NoSentinel{})
103
104
105
106def sentinel_of(+is_active: Bool, +e: G.Ent, +now: U32) -> T.Sentinel:
107 match e:
108 case G.Ent{slot, email, org, +creds, acct}:
109 sent2(is_active, SBase.nonempty(creds), SBase.looks_like_api_key(creds), creds, now)
110
111def row_guard(+row: Json.Json, +email: String, +org: String) -> Json.Json:
112 Store.sel.j(String.eq(SSeq.field_str(row, "email"), email) && String.eq(SSeq.field_str(row, "organizationUuid"), org), row, Json.JNull{})
113
114def row_of(+rows: Json.Json, +e: G.Ent) -> Json.Json:
115 match e:
116 case G.Ent{slot, email, org, creds, acct}:
117 row_guard(SSeq.get_or(slot, rows, Json.JNull{}), email, org)
118
119def is_slot(+active: Maybe<&2, String>, +slot: String) -> Bool:
120 match active:
121 case Some{a}:
122 String.eq(a, slot)
123 case None{}:
124 False{}
125
126def usage_one(+rows: Json.Json, +e: G.Ent, +active: Maybe<&2, String>, +now: U32) -> T.Usage:
127 match e:
128 case G.Ent{+slot, +email, +org, +creds, +acct}:
129 Rows.usage_of(nat(SSeq.str_u32(slot)), row_of(rows, e), sentinel_of(is_slot(active, slot), e, now))
130
131def usages_of(+rows: Json.Json, +es: List<&2, G.Ent>, +active: Maybe<&2, String>, +now: U32) -> List<&2, T.Usage>:
132 match es:
133 case Nil{}:
134 []
135 case Con{e, t}:
136 usage_one(rows, e, active, now) <> usages_of(rows, t, active, now)
137
138def sel_mu(+c: Bool, +x: Maybe<&2, T.Usage>, +y: Maybe<&2, T.Usage>) -> Maybe<&2, T.Usage>:
139 match c:
140 case True{}:
141 x
142 case False{}:
143 y
144
145def usage_pick(+u: T.Usage, +slot: Nat, +rest: Maybe<&2, T.Usage>) -> Maybe<&2, T.Usage>:
146 match u:
147 case T.Usage{+s, five, fr, seven, sr, ss, fa, bo, er, fl, np, pi, ad, hu, cu, se}:
148 sel_mu(Nat.is_eq(s, slot), Some{u}, rest)
149
150def usage_for(+us: List<&2, T.Usage>, +slot: Nat) -> Maybe<&2, T.Usage>:
151 match us:
152 case Nil{}:
153 None{}
154 case Con{u, t}:
155 usage_pick(u, slot, usage_for(t, slot))
156
157def sel_me(+c: Bool, +x: Maybe<&2, G.Ent>, +y: Maybe<&2, G.Ent>) -> Maybe<&2, G.Ent>:
158 match c:
159 case True{}:
160 x
161 case False{}:
162 y
163
164def ent_pick(+e: G.Ent, +slot: String, +rest: Maybe<&2, G.Ent>) -> Maybe<&2, G.Ent>:
165 match e:
166 case G.Ent{+s, em, o, c, a}:
167 sel_me(String.eq(s, slot), Some{e}, rest)
168
169def ent_for(+es: List<&2, G.Ent>, +slot: String) -> Maybe<&2, G.Ent>:
170 match es:
171 case Nil{}:
172 None{}
173 case Con{e, t}:
174 ent_pick(e, slot, ent_for(t, slot))
175
176def email2(+m: Maybe<&2, G.Ent>) -> String:
177 match m:
178 case Some{G.Ent{s, em, o, c, a}}:
179 em
180 case None{}:
181 ""
182
183
184
185def email_of(+es: List<&2, G.Ent>, +slot: String) -> String:
186 email2(ent_for(es, slot))
187
188def scoped_keep(+hit: Bool, +w: Ev.Win, +rest: List<&2, Ev.Win>) -> List<&2, Ev.Win>:
189 match hit:
190 case True{}:
191 w <> rest
192 case False{}:
193 rest
194
195def scoped_wins(+ss: List<&2, T.Scoped>, +ms: List<&2, String>, +now: Nat) -> List<&2, Ev.Win>:
196 match ss:
197 case Nil{}:
198 []
199 case Con{T.Scoped{n, p, r}, t}:
200 scoped_keep(PU.model.wanted(String.to_lower(n), ms), Ev.Win{n, U32.from_nat(p)}, scoped_wins(t, ms, now))
201
202def opt_win(+name: String, +m: Maybe<&2, Nat>, +rest: List<&2, Ev.Win>) -> List<&2, Ev.Win>:
203 match m:
204 case Some{p}:
205 Ev.Win{name, U32.from_nat(p)} <> rest
206 case None{}:
207 rest
208
209def win_rows(+u: T.Usage, +ms: List<&2, String>, +now: Nat) -> List<&2, Ev.Win>:
210 match u:
211 case T.Usage{s, five, fr, seven, sr, ss, fa, bo, er, fl, np, pi, ad, hu, cu, se}:
212 opt_win("5h", five, opt_win("7d", seven, scoped_wins(ss, ms, now)))
213
214def head_u32(+m: Maybe<&2, Nat>) -> Maybe<&2, U32>:
215 match m:
216 case Some{n}:
217 Some{U32.from_nat(n)}
218 case None{}:
219 None{}
220
221def err_text(+row: Json.Json) -> Maybe<&2, String>:
222 Rows.str_text(Json.get("lastError", row))
223
224def sel_wl(+c: Bool, +x: List<&2, Ev.Win>, +y: List<&2, Ev.Win>) -> List<&2, Ev.Win>:
225 match c:
226 case True{}:
227 x
228 case False{}:
229 y
230
231def poll_row2(+slot: String, +trusted: Bool, +u: T.Usage, +err: Maybe<&2, String>, +ms: List<&2, String>, +now: Nat) -> Ev.Row:
232 Ev.Row{slot, head_u32(PU.headroom(u, now, ms)), sel_wl(trusted, win_rows(u, ms, now), []), err}
233
234
235
236def poll_row(+e: G.Ent, +u: T.Usage, +rows: Json.Json, +ms: List<&2, String>, +now: Nat) -> Ev.Row:
237 match e:
238 case G.Ent{+slot, email, org, creds, acct}:
239 poll_row2(slot, PU.trusted(u, now, ms), u, err_text(row_of(rows, e)), ms, now)
240
241def row_opt(+e: G.Ent, +u: Maybe<&2, T.Usage>, +rows: Json.Json, +ms: List<&2, String>, +now: Nat) -> Maybe<&2, Ev.Row>:
242 match u:
243 case Some{x}:
244 Some{poll_row(e, x, rows, ms, now)}
245 case None{}:
246 None{}
247
248def push_row(+m: Maybe<&2, Ev.Row>, +rest: List<&2, Ev.Row>) -> List<&2, Ev.Row>:
249 match m:
250 case Some{r}:
251 r <> rest
252 case None{}:
253 rest
254
255def ent_slot(+e: G.Ent) -> String:
256 match e:
257 case G.Ent{s, em, o, c, a}:
258 s
259
260def poll_rows(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +rows: Json.Json, +ms: List<&2, String>, +now: Nat) -> List<&2, Ev.Row>:
261 match es:
262 case Nil{}:
263 []
264 case Con{e, t}:
265 push_row(row_opt(e, usage_for(us, nat(SSeq.str_u32(ent_slot(e)))), rows, ms, now), poll_rows(t, us, rows, ms, now))
266
267def slot_nat_of(+s: String) -> Nat:
268 nat(SSeq.str_u32(s))
269
270
271def ref_json(+slot: String, +es: List<&2, G.Ent>) -> Json.Json:
272 Ev.ref(slot, email_of(es, slot))
273
274def opt_json(+m: Maybe<&2, String>) -> Json.Json:
275 match m:
276 case Some{x}:
277 Json.JStr{x}
278 case None{}:
279 Json.JNull{}
280
281def snap_accounts(+s: G.Snap) -> List<&2, T.Account>:
282 accounts_of(G.snap_ents(s))
283
284def snap_state(+s: G.Snap) -> T.State:
285 state_of(G.snap_state(s))