daemon/state.bend
cswap-bend-cli@0.1.0.0 · 162 lines · 33 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/lock.bend as Lock
7import cswap-bend-cli@0.1.0.0/store.bend as Store
8import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
9import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
10
11
12
13
14
15
16def ActM(-A: Type) -> Type:
17 Act.of(A)
18
19def ActM.pure(-A: Type, x: A) -> Act.of(A):
20 Act.pure(A, x)
21
22def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
23 Act.bind(A, B, m, f)
24
25def state_path(+root: String) -> String:
26 root ++ "/autoswitch_state.json"
27
28def lock_path(+root: String) -> String:
29 root ++ "/.autoswitch_state.lock"
30
31def empty() -> Json.Json:
32 Json.JObj{Json.JNil{}}
33
34def obj_or_empty(+j: Json.Json) -> Json.Json:
35 match j:
36 case Json.JObj{f}:
37 j
38 case _:
39 empty()
40
41def parsed(r: Result<&2, &2, String, Json.Json>) -> Json.Json:
42 match r:
43 case Done{j}:
44 obj_or_empty(j)
45 case Fail{e}:
46 empty()
47
48def read2(r: Fs.Res(String)) -> Json.Json:
49 match r:
50 case Done{t}:
51 parsed(Json.parse(t))
52 case Fail{e}:
53 empty()
54
55
56def read_state(+root: String) -> IO(Json.Json):
57 IO.bind(Fs.Res(String), Json.Json, Fs.read_file(state_path(root)), r => IO.pure(Json.Json, read2(r)))
58
59def write_state(+root: String, +j: Json.Json) -> Act.of(Unit):
60 FsW.write_atomic(state_path(root), Json.emit_pretty(j), "600")
61
62
63def with_state_lock(-A: Type, +root: String, act: Act.of(A)) -> Act.of(A):
64 Lock.with_lock(A, lock_path(root), 10000, act)
65
66
67
68def fl(+t: U32) -> Json.Json:
69 Json.JNum{U32.show(t) ++ ".0"}
70
71def opt(+m: Maybe<&2, U32>, f: U32 -> Json.Json) -> Json.Json:
72 match m:
73 case None{}:
74 Json.JNull{}
75 case Some{v}:
76 f(v)
77
78def two(+n: U32) -> String:
79 SBase.sel.s(U32.is_lt(n, 10), "0" ++ U32.show(n), U32.show(n))
80
81def frac(+c: U32) -> String:
82 SBase.sel.s(U32.is_eq((c % 10 : U32), 0), U32.show((c / 10 : U32)), two(c))
83
84def cents_json(+c: U32) -> Json.Json:
85 Json.JNum{U32.show((c / 100 : U32)) ++ "." ++ frac((c % 100 : U32))}
86
87def int_json(+n: U32) -> Json.Json:
88 Json.JNum{U32.show(n)}
89
90
91
92def record_switch(+st: Json.Json, +now: U32, +to: String, +from: Maybe<&2, U32>, +left_headroom: Maybe<&2, U32>, +left_recovery: Maybe<&2, U32>, +trigger: String) -> Json.Json:
93 Json.set("leftTrigger", Json.JStr{trigger},
94 Json.set("leftRecoveryAt", opt(left_recovery, u => fl(u)),
95 Json.set("leftHeadroom", opt(left_headroom, h => cents_json(h)),
96 Json.set("lastSwitchFrom", opt(from, n => int_json(n)),
97 Json.set("lastSwitchTo", Json.JStr{to},
98 Json.set("lastSwitchAt", fl(now),
99 Json.set("schemaVersion", Json.JNum{"1"}, obj_or_empty(st))))))))
100
101
102
103def quarantine_of(+st: Json.Json) -> Json.Json:
104 obj_or_empty(SSeq.get_or("quarantine", st, empty()))
105
106def opt_text(+m: Maybe<&2, String>) -> Json.Json:
107 match m:
108 case None{}:
109 Json.JNull{}
110 case Some{s}:
111 Json.JStr{s}
112
113def entry(+email: String, +reason: String, +at: String, +fp: Maybe<&2, String>) -> Json.Json:
114 Json.JObj{Json.JField{"email", Json.JStr{email}, Json.JField{"reason", Json.JStr{reason}, Json.JField{"at", Json.JStr{at},
115 Json.JField{"refreshTokenFingerprint", opt_text(fp), Json.JNil{}}}}}}
116
117def quarantine(+st: Json.Json, +slot: String, +email: String, +reason: String, +at: String, +fp: Maybe<&2, String>) -> Json.Json:
118 Json.set("quarantine", Json.set(slot, entry(email, reason, at, fp), quarantine_of(st)), Json.set("schemaVersion", Json.JNum{"1"}, obj_or_empty(st)))
119
120def release(+st: Json.Json, +slot: String) -> Json.Json:
121 Json.set("quarantine", Json.del(slot, quarantine_of(st)), Json.set("schemaVersion", Json.JNum{"1"}, obj_or_empty(st)))
122
123def quarantined_slots(+st: Json.Json) -> List<&2, String>:
124 Json.keys(quarantine_of(st))
125
126def entry_email(+st: Json.Json, +slot: String) -> String:
127 SSeq.field_str(SSeq.get_or(slot, quarantine_of(st), Json.JNull{}), "email")
128
129def entry_fp2(+m: Maybe<&2, Json.Json>) -> Maybe<&2, String>:
130 match m:
131 case Some{Json.JStr{s}}:
132 Some{s}
133 case _:
134 None{}
135
136def entry_fp(+st: Json.Json, +slot: String) -> Maybe<&2, String>:
137 entry_fp2(Json.get("refreshTokenFingerprint", SSeq.get_or(slot, quarantine_of(st), Json.JNull{})))
138
139def first_part(+t: String) -> String:
140 Maybe.default(&2, String, List.head(&2, String, String.split(t, '.')), "")
141
142def last2(+m: Maybe<&2, Json.Json>) -> Maybe<&2, U32>:
143 match m:
144 case Some{Json.JNum{t}}:
145 U32.read(first_part(t))
146 case _:
147 None{}
148
149
150def last_switch_at(+st: Json.Json) -> Maybe<&2, U32>:
151 last2(Json.get("lastSwitchAt", st))
152
153def cool2(+m: Maybe<&2, U32>, +now: U32, +cooldown: U32) -> Bool:
154 match m:
155 case Some{l}:
156 U32.is_lt((now - l : U32), cooldown)
157 case None{}:
158 False{}
159
160
161def in_cooldown(+st: Json.Json, +now: U32, +cooldown: U32) -> Bool:
162 cool2(last_switch_at(st), now, cooldown)