service status on atproto
at main 5.2 kB view raw
1import { 2 err, 3 expect, 4 getRecord, 5 getUri, 6 ok, 7 putRecord, 8 type CheckUri, 9 type CollectionUri, 10 type Result, 11 type ServiceUri, 12 type StateUri, 13} from "../utils"; 14import { 15 parseCanonicalResourceUri, 16 safeParse, 17 type CanonicalResourceUri, 18 type ParsedCanonicalResourceUri, 19 type ResourceUri, 20} from "@atcute/lexicons"; 21import store, { type Check, type Service, type State } from "../store"; 22import { systemctlShow } from "../systemd"; 23import { config } from "../config"; 24import { now as generateTid } from "@atcute/tid"; 25import * as v from "@atcute/lexicons/validations"; 26import type { SystemsGazeBarometerService } from "barometer-lexicon"; 27import type { SystemsGazeBarometerState } from "barometer-lexicon"; 28 29// this is hacky but we want to make forService be optional so its okay 30const StateSchemaSubset = v.record( 31 v.tidString(), 32 v.object({ 33 changedAt: v.optional(v.datetimeString()), 34 forService: v.optional(v.resourceUriString()), 35 generatedBy: v.optional(v.resourceUriString()), 36 reason: v.optional(v.string()), 37 state: v.literalEnum([ 38 "systems.gaze.barometer.status.degraded", 39 "systems.gaze.barometer.status.healthy", 40 ]), 41 }), 42); 43 44interface PushRequest { 45 serviceName?: string; // service manager service name 46 state: v.InferOutput<typeof StateSchemaSubset>; 47} 48 49const parsePushRequest = (json: unknown): Result<PushRequest, string> => { 50 if (typeof json !== "object" || json === null) { 51 return err("invalid request"); 52 } 53 if ("serviceName" in json && typeof json.serviceName !== "string") { 54 return err("serviceName is not a string"); 55 } 56 if ("state" in json) { 57 const parsed = safeParse(StateSchemaSubset, json.state); 58 if (!parsed.ok) { 59 return err(`state is invalid: ${parsed.message}`); 60 } 61 } else { 62 return err("state not found"); 63 } 64 return ok(json as PushRequest); 65}; 66 67const error = <Error extends { msg: string }>( 68 error: Error, 69 status: number = 400, 70) => { 71 return new Response(JSON.stringify(error), { status }); 72}; 73export const POST = async (req: Bun.BunRequest) => { 74 const maybeData = parsePushRequest(await req.json()); 75 if (!maybeData.ok) { 76 return error({ 77 msg: `invalid request: ${maybeData.error}`, 78 }); 79 } 80 const data = maybeData.value; 81 82 let service: Service | undefined = undefined; 83 let serviceAtUri: ServiceUri | undefined; 84 if (data.state.forService) { 85 serviceAtUri = data.state.forService as ServiceUri; 86 const maybeService = await store.getOrFetch(serviceAtUri); 87 if (!maybeService.ok) 88 return error({ 89 msg: `could not fetch service: ${maybeService.error}`, 90 }); 91 service = maybeService.value; 92 } else if (data.serviceName) { 93 const maybeService = await store.getServiceFromSystemd(data.serviceName); 94 if (!maybeService.ok) 95 return error({ 96 msg: `could not fetch service from systemd: ${maybeService.error}`, 97 }); 98 const [uri, srv] = maybeService.value; 99 serviceAtUri = uri; 100 service = srv; 101 } else { 102 return error({ 103 msg: `either 'state.forService' or 'serviceName' must be provided`, 104 }); 105 } 106 107 let check: Check | undefined = undefined; 108 if (data.state.generatedBy) { 109 const maybeCheck = await store.getOrFetch( 110 data.state.generatedBy as CheckUri, 111 ); 112 if (!maybeCheck.ok) return error({ msg: maybeCheck.error }); 113 check = maybeCheck.value; 114 if (check.record.forService !== serviceAtUri) 115 return error({ 116 msg: `check record does not point to the same service as the state record service`, 117 }); 118 // update services with check 119 service.checks.add(check.rkey); 120 store.services.set(serviceAtUri, service); 121 } 122 123 // get current state uri 124 const currentStateUri = 125 check && check.record.currentState 126 ? check.record.currentState 127 : service.record.currentState; 128 129 if (currentStateUri) { 130 // fetch current state 131 const record = await store.getOrFetch(currentStateUri as StateUri); 132 if (!record.ok) return error({ msg: record.error }); 133 const currentState = record.value; 134 135 // check if the state has changed 136 if (currentState.record.state === data.state.state) 137 return error( 138 { 139 msg: `state can't be the same as the latest state`, 140 }, 141 208, 142 ); 143 } 144 145 const stateRecord: SystemsGazeBarometerState.Main = { 146 $type: "systems.gaze.barometer.state", 147 ...data.state, 148 forService: serviceAtUri, 149 changedAt: data.state.changedAt ?? new Date().toISOString(), 150 previous: currentStateUri, 151 }; 152 const rkey = generateTid(); 153 const result = await putRecord(stateRecord, rkey); 154 155 // store committed state in "cache" 156 store.states.set(result.uri as StateUri, { record: stateRecord, rkey }); 157 158 // update check with new state url 159 if (check) { 160 check.record.currentState = result.uri; 161 store.checks.set(getUri("systems.gaze.barometer.check", check.rkey), check); 162 await putRecord(check.record, check.rkey); 163 } else { 164 // update service with new state url 165 service.record.currentState = result.uri; 166 store.services.set(serviceAtUri, service); 167 await putRecord(service.record, service.rkey); 168 } 169 170 return new Response(JSON.stringify({ cid: result.cid, uri: result.uri })); 171};