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# autoswitch_state.json (contract 1.7): the last switch, the account it left,
12# and the quarantine of accounts whose refresh token is dead. Read lock-free,
13# written read-modify-write under <root>/.autoswitch_state.lock, unknown keys
14# kept. `--dry-run` never writes it.
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# The state object; {} when the file is missing, torn or not an object.
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# Hold the state lock around `act`.
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# ---- the last switch --------------------------------------------------------------------------
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# What `_perform` records after a switch. `left_headroom` is centi-percent,
91# `left_recovery` unix seconds (None for the reference's inf).
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# ---- quarantine ------------------------------------------------------------------------------
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# lastSwitchAt as seconds, None when absent or not a number.
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# `now - lastSwitchAt < cooldown`
161def in_cooldown(+st: Json.Json, +now: U32, +cooldown: U32) -> Bool:
162  cool2(last_switch_at(st), now, cooldown)