service status on atproto

feat!: include current state for service / check in lexicon, let state include uri to previous state for easier traversal

ptr.pet bcc1fe2f 7ad815b1

verified
Changed files
+443 -338
lexicon
lib
src
lexicons
types
systems
gaze
proxy
+26 -22
lexicon/check.json
···
{
-
"lexicon": 1,
-
"id": "systems.gaze.barometer.check",
-
"defs": {
-
"main": {
-
"type": "record",
-
"key": "tid",
-
"record": {
-
"type": "object",
-
"required": ["name", "forService"],
-
"properties": {
-
"name": {
-
"type": "string"
-
},
-
"description": {
-
"type": "string"
-
},
-
"forService": {
-
"type": "string",
-
"format": "at-uri"
-
}
+
"lexicon": 1,
+
"id": "systems.gaze.barometer.check",
+
"defs": {
+
"main": {
+
"type": "record",
+
"key": "any",
+
"record": {
+
"type": "object",
+
"required": ["name", "forService"],
+
"properties": {
+
"name": {
+
"type": "string"
+
},
+
"description": {
+
"type": "string"
+
},
+
"forService": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"currentState": {
+
"type": "string",
+
"format": "at-uri"
+
}
+
}
+
}
}
-
}
}
-
}
}
+21 -21
lexicon/host.json
···
{
-
"lexicon": 1,
-
"id": "systems.gaze.barometer.host",
-
"defs": {
-
"main": {
-
"type": "record",
-
"key": "any",
-
"record": {
-
"type": "object",
-
"required": ["name", "os"],
-
"properties": {
-
"name": {
-
"type": "string"
-
},
-
"description": {
-
"type": "string"
-
},
-
"os": {
-
"type": "string"
-
}
+
"lexicon": 1,
+
"id": "systems.gaze.barometer.host",
+
"defs": {
+
"main": {
+
"type": "record",
+
"key": "any",
+
"record": {
+
"type": "object",
+
"required": ["name", "os"],
+
"properties": {
+
"name": {
+
"type": "string"
+
},
+
"description": {
+
"type": "string"
+
},
+
"os": {
+
"type": "string"
+
}
+
}
+
}
}
-
}
}
-
}
}
+30 -26
lexicon/service.json
···
{
-
"lexicon": 1,
-
"id": "systems.gaze.barometer.service",
-
"defs": {
-
"main": {
-
"type": "record",
-
"key": "tid",
-
"record": {
-
"type": "object",
-
"required": ["name"],
-
"properties": {
-
"name": {
-
"type": "string"
-
},
-
"description": {
-
"type": "string"
-
},
-
"hostedBy": {
-
"type": "string",
-
"format": "at-uri"
-
},
-
"appUri": {
-
"type": "string",
-
"format": "uri"
-
}
+
"lexicon": 1,
+
"id": "systems.gaze.barometer.service",
+
"defs": {
+
"main": {
+
"type": "record",
+
"key": "any",
+
"record": {
+
"type": "object",
+
"required": ["name"],
+
"properties": {
+
"name": {
+
"type": "string"
+
},
+
"description": {
+
"type": "string"
+
},
+
"appUri": {
+
"type": "string",
+
"format": "uri"
+
},
+
"hostedBy": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"currentState": {
+
"type": "string",
+
"format": "at-uri"
+
}
+
}
+
}
}
-
}
}
-
}
}
+38 -42
lexicon/state.json
···
{
-
"lexicon": 1,
-
"id": "systems.gaze.barometer.state",
-
"defs": {
-
"main": {
-
"type": "record",
-
"key": "tid",
-
"record": {
-
"type": "object",
-
"required": ["from", "to", "changedAt", "forService"],
-
"properties": {
-
"from": {
-
"type": "string",
-
"enum": [
-
"systems.gaze.barometer.status.healthy",
-
"systems.gaze.barometer.status.degraded",
-
"systems.gaze.barometer.status.unknown"
-
]
-
},
-
"to": {
-
"type": "string",
-
"enum": [
-
"systems.gaze.barometer.status.healthy",
-
"systems.gaze.barometer.status.degraded"
-
]
-
},
-
"reason": {
-
"type": "string"
-
},
-
"changedAt": {
-
"type": "string",
-
"format": "datetime"
-
},
-
"forService": {
-
"type": "string",
-
"format": "at-uri"
-
},
-
"generatedBy": {
-
"type": "string",
-
"format": "at-uri"
-
}
+
"lexicon": 1,
+
"id": "systems.gaze.barometer.state",
+
"defs": {
+
"main": {
+
"type": "record",
+
"key": "tid",
+
"record": {
+
"type": "object",
+
"required": ["state", "changedAt", "forService"],
+
"properties": {
+
"state": {
+
"type": "string",
+
"enum": [
+
"systems.gaze.barometer.status.healthy",
+
"systems.gaze.barometer.status.degraded"
+
]
+
},
+
"reason": {
+
"type": "string"
+
},
+
"changedAt": {
+
"type": "string",
+
"format": "datetime"
+
},
+
"forService": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"generatedBy": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"previous": {
+
"type": "string",
+
"format": "at-uri"
+
}
+
}
+
}
}
-
}
}
-
}
}
+7 -7
lexicon/status/degraded.json
···
{
-
"lexicon": 1,
-
"id": "systems.gaze.barometer.status.degraded",
-
"defs": {
-
"main": {
-
"type": "token",
-
"description": "represents that a service / check is not working as it should"
+
"lexicon": 1,
+
"id": "systems.gaze.barometer.status.degraded",
+
"defs": {
+
"main": {
+
"type": "token",
+
"description": "represents that a service / check is not working as it should"
+
}
}
-
}
}
+7 -7
lexicon/status/healthy.json
···
{
-
"lexicon": 1,
-
"id": "systems.gaze.barometer.status.healthy",
-
"defs": {
-
"main": {
-
"type": "token",
-
"description": "represents that a service / check is working properly"
+
"lexicon": 1,
+
"id": "systems.gaze.barometer.status.healthy",
+
"defs": {
+
"main": {
+
"type": "token",
+
"description": "represents that a service / check is working properly"
+
}
}
-
}
}
-10
lexicon/status/unknown.json
···
-
{
-
"lexicon": 1,
-
"id": "systems.gaze.barometer.status.unknown",
-
"defs": {
-
"main": {
-
"type": "token",
-
"description": "represents that the state of the service / check is unknown (only used for first from field in a state record)"
-
}
-
}
-
}
-1
lib/src/lexicons/index.ts
···
export * as SystemsGazeBarometerState from "./types/systems/gaze/barometer/state.js";
export * as SystemsGazeBarometerStatusDegraded from "./types/systems/gaze/barometer/status/degraded.js";
export * as SystemsGazeBarometerStatusHealthy from "./types/systems/gaze/barometer/status/healthy.js";
-
export * as SystemsGazeBarometerStatusUnknown from "./types/systems/gaze/barometer/status/unknown.js";
+2 -1
lib/src/lexicons/types/systems/gaze/barometer/check.ts
···
import type {} from "@atcute/lexicons/ambient";
const _mainSchema = /*#__PURE__*/ v.record(
-
/*#__PURE__*/ v.tidString(),
+
/*#__PURE__*/ v.string(),
/*#__PURE__*/ v.object({
$type: /*#__PURE__*/ v.literal("systems.gaze.barometer.check"),
+
currentState: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()),
description: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
forService: /*#__PURE__*/ v.resourceUriString(),
name: /*#__PURE__*/ v.string(),
+2 -1
lib/src/lexicons/types/systems/gaze/barometer/service.ts
···
import type {} from "@atcute/lexicons/ambient";
const _mainSchema = /*#__PURE__*/ v.record(
-
/*#__PURE__*/ v.tidString(),
+
/*#__PURE__*/ v.string(),
/*#__PURE__*/ v.object({
$type: /*#__PURE__*/ v.literal("systems.gaze.barometer.service"),
appUri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()),
+
currentState: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()),
description: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
hostedBy: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()),
name: /*#__PURE__*/ v.string(),
+2 -6
lib/src/lexicons/types/systems/gaze/barometer/state.ts
···
$type: /*#__PURE__*/ v.literal("systems.gaze.barometer.state"),
changedAt: /*#__PURE__*/ v.datetimeString(),
forService: /*#__PURE__*/ v.resourceUriString(),
-
from: /*#__PURE__*/ v.literalEnum([
-
"systems.gaze.barometer.status.degraded",
-
"systems.gaze.barometer.status.healthy",
-
"systems.gaze.barometer.status.unknown",
-
]),
generatedBy: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()),
+
previous: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()),
reason: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
-
to: /*#__PURE__*/ v.literalEnum([
+
state: /*#__PURE__*/ v.literalEnum([
"systems.gaze.barometer.status.degraded",
"systems.gaze.barometer.status.healthy",
]),
-14
lib/src/lexicons/types/systems/gaze/barometer/status/unknown.ts
···
-
import type {} from "@atcute/lexicons";
-
import * as v from "@atcute/lexicons/validations";
-
-
const _mainSchema = /*#__PURE__*/ v.literal(
-
"systems.gaze.barometer.status.unknown",
-
);
-
-
type main$schematype = typeof _mainSchema;
-
-
export interface mainSchema extends main$schematype {}
-
-
export const mainSchema = _mainSchema as mainSchema;
-
-
export type Main = v.InferInput<typeof mainSchema>;
+3
proxy/bun.lock
···
"@atcute/lexicons": "^1.1.0",
"@atcute/tid": "^1.0.2",
"barometer-lexicon": "file:../lib",
+
"nanoid": "^5.1.5",
"parsimmon": "^1.18.1",
},
"devDependencies": {
···
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
+
+
"nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
"parsimmon": ["parsimmon@1.18.1", "", {}, "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="],
+1
proxy/package.json
···
"@atcute/lexicons": "^1.1.0",
"@atcute/tid": "^1.0.2",
"barometer-lexicon": "file:../lib",
+
"nanoid": "^5.1.5",
"parsimmon": "^1.18.1"
}
}
+1 -3
proxy/src/config.ts
···
check: (value: unknown) => boolean = () => true,
): Value => {
const value = env[`${prefix}${name}`];
-
if (check(value)) {
-
return value as Value;
-
}
+
if (check(value)) return value as Value;
throw `config key ${name} is invalid`;
};
return {
+79 -74
proxy/src/jetstream.ts
···
import { JetstreamSubscription } from "@atcute/jetstream";
-
import { is, parse, parseCanonicalResourceUri } from "@atcute/lexicons";
+
import {
+
is,
+
parse,
+
parseCanonicalResourceUri,
+
type RecordKey,
+
} from "@atcute/lexicons";
import {
SystemsGazeBarometerService,
SystemsGazeBarometerCheck,
-
SystemsGazeBarometerState,
} from "barometer-lexicon";
import { config } from "./config";
import store, { type Service } from "./store";
-
import { expect, getRecord, log } from "./utils";
+
import { expect, getRecord, getUri, log, type ServiceUri } from "./utils";
const subscription = new JetstreamSubscription({
url: "wss://jetstream2.us-east.bsky.network",
wantedCollections: [
"systems.gaze.barometer.service",
"systems.gaze.barometer.check",
+
"systems.gaze.barometer.state",
],
wantedDids: [config.repoDid],
});
+
const handleService = async (
+
record: Record<string, unknown>,
+
rkey: RecordKey,
+
) => {
+
const collection = "systems.gaze.barometer.service";
+
const serviceRecord = parse(SystemsGazeBarometerService.mainSchema, record);
+
// we dont care if its a dangling service
+
if (!serviceRecord.hostedBy) return true;
+
const hostAtUri = expect(parseCanonicalResourceUri(serviceRecord.hostedBy));
+
// not our host
+
if (hostAtUri.rkey !== store.hostname) return true;
+
const serviceUri = getUri(collection, rkey);
+
const service: Service = store.services.get(serviceUri) ?? {
+
record: serviceRecord,
+
checks: new Set(),
+
rkey,
+
};
+
store.services.set(serviceUri, {
+
...service,
+
record: serviceRecord,
+
});
+
return false;
+
};
+
+
const handleCheck = async (
+
record: Record<string, unknown>,
+
rkey: RecordKey,
+
) => {
+
const collection = "systems.gaze.barometer.check";
+
const checkRecord = parse(SystemsGazeBarometerCheck.mainSchema, record);
+
const checkUri = getUri(collection, rkey);
+
const serviceUri = checkRecord.forService as ServiceUri;
+
const maybeService = await store.getOrFetch(serviceUri);
+
if (!maybeService.ok) {
+
log.error(
+
`can't fetch service record (${serviceUri}) for check record (${checkUri})`,
+
);
+
return true;
+
}
+
const service = maybeService.value;
+
service.checks.add(rkey);
+
store.checks.set(checkUri, {
+
record: checkRecord,
+
rkey,
+
});
+
store.services.set(serviceUri, service);
+
return false;
+
};
+
export const handleEvents = async () => {
for await (const event of subscription) {
-
if (event.kind !== "commit") {
-
continue;
-
}
+
if (event.kind !== "commit") continue;
const { operation, collection, rkey } = event.commit;
// log.info(`${operation} at://${event.did}/${collection}/${rkey}`);
if (operation === "create" || operation === "update") {
const record = event.commit.record;
switch (collection) {
case "systems.gaze.barometer.service": {
-
const serviceRecord = parse(
-
SystemsGazeBarometerService.mainSchema,
-
record,
-
);
-
// we dont care if its a dangling service
-
if (!serviceRecord.hostedBy) {
-
continue;
-
}
-
const hostAtUri = expect(
-
parseCanonicalResourceUri(serviceRecord.hostedBy),
-
);
-
// not our host
-
if (hostAtUri.rkey !== store.hostname) {
-
continue;
-
}
-
const service: Service = store.services.get(rkey) ?? {
-
record: serviceRecord,
-
checks: new Set(),
-
};
-
store.services.set(rkey, {
-
...service,
-
record: serviceRecord,
-
});
+
if (await handleService(record, rkey)) continue;
break;
}
case "systems.gaze.barometer.check": {
-
const checkRecord = parse(
-
SystemsGazeBarometerCheck.mainSchema,
-
record,
-
);
-
const parsedServiceAtUri = expect(
-
parseCanonicalResourceUri(checkRecord.forService),
-
);
-
let service = store.services.get(parsedServiceAtUri.rkey);
-
if (!service) {
-
const serviceRecord = await getRecord(
-
"systems.gaze.barometer.service",
-
parsedServiceAtUri.rkey,
-
);
-
if (!serviceRecord.ok) {
-
// cant get service record
-
log.error(
-
`can't fetch service record (${checkRecord.forService}) for check record (at://${event.did}/${collection}/${rkey})`,
-
);
-
continue;
-
}
-
service = {
-
record: serviceRecord.value,
-
checks: new Set(),
-
};
-
}
-
service.checks.add(rkey);
-
store.checks.set(rkey, { record: checkRecord });
-
store.services.set(parsedServiceAtUri.rkey, service);
+
if (await handleCheck(record, rkey)) continue;
break;
}
}
} else {
switch (collection) {
case "systems.gaze.barometer.service": {
-
const service = store.services.get(rkey);
-
if (!service) {
-
continue;
+
const serviceUri = getUri(collection, rkey);
+
const service = store.services.get(serviceUri);
+
if (!service) continue;
+
for (const checkRkey of service.checks) {
+
store.checks.delete(
+
getUri("systems.gaze.barometer.check", checkRkey),
+
);
}
-
for (const checkKey of service.checks) {
-
store.checks.delete(checkKey);
-
}
-
store.services.delete(rkey);
+
store.services.delete(serviceUri);
break;
}
case "systems.gaze.barometer.check": {
-
const check = store.checks.get(rkey);
-
if (!check) {
-
continue;
-
}
-
const parsedServiceAtUri = expect(
-
parseCanonicalResourceUri(check.record.forService),
-
);
-
const service = store.services.get(parsedServiceAtUri.rkey);
+
const checkUri = getUri(collection, rkey);
+
const check = store.checks.get(checkUri);
+
if (!check) continue;
+
const serviceUri = check.record.forService as ServiceUri;
+
const service = store.services.get(serviceUri);
if (service) {
service.checks.delete(rkey);
-
store.services.set(parsedServiceAtUri.rkey, service);
+
store.services.set(serviceUri, service);
}
-
store.checks.delete(rkey);
+
store.checks.delete(checkUri);
+
break;
+
}
+
case "systems.gaze.barometer.state": {
+
store.states.delete(getUri(collection, rkey));
break;
}
}
+95 -86
proxy/src/routes/push.ts
···
-
import { err, expect, getRecord, ok, putRecord, type Result } from "../utils";
+
import {
+
err,
+
expect,
+
getRecord,
+
getUri,
+
ok,
+
putRecord,
+
type CheckUri,
+
type CollectionUri,
+
type Result,
+
type ServiceUri,
+
type StateUri,
+
} from "../utils";
import {
parseCanonicalResourceUri,
safeParse,
···
type ParsedCanonicalResourceUri,
type ResourceUri,
} from "@atcute/lexicons";
-
import store, { type Service } from "../store";
+
import store, { type Check, type Service, type State } from "../store";
import { systemctlShow } from "../systemd";
import { config } from "../config";
import { now as generateTid } from "@atcute/tid";
import * as v from "@atcute/lexicons/validations";
import type { SystemsGazeBarometerService } from "barometer-lexicon";
+
import type { SystemsGazeBarometerState } from "barometer-lexicon";
// this is hacky but we want to make forService be optional so its okay
const StateSchemaSubset = v.record(
v.tidString(),
v.object({
-
$type: v.literal("systems.gaze.barometer.state"),
-
changedAt: v.datetimeString(),
+
changedAt: v.optional(v.datetimeString()),
forService: v.optional(v.resourceUriString()),
generatedBy: v.optional(v.resourceUriString()),
reason: v.optional(v.string()),
-
from: v.literalEnum([
-
"systems.gaze.barometer.status.degraded",
-
"systems.gaze.barometer.status.healthy",
-
"systems.gaze.barometer.status.unknown",
-
]),
-
to: v.literalEnum([
+
state: v.literalEnum([
"systems.gaze.barometer.status.degraded",
"systems.gaze.barometer.status.healthy",
]),
···
return ok(json as PushRequest);
};
-
const badRequest = <Error extends { msg: string }>(error: Error) => {
-
return new Response(JSON.stringify(error), { status: 400 });
+
const error = <Error extends { msg: string }>(
+
error: Error,
+
status: number = 400,
+
) => {
+
return new Response(JSON.stringify(error), { status });
};
export const POST = async (req: Bun.BunRequest) => {
const maybeData = parsePushRequest(await req.json());
if (!maybeData.ok) {
-
return badRequest({
+
return error({
msg: `invalid request: ${maybeData.error}`,
});
}
const data = maybeData.value;
let service: Service | undefined = undefined;
-
let serviceAtUri: ResourceUri;
-
let parsedServiceAtUri: ParsedCanonicalResourceUri;
+
let serviceAtUri: ServiceUri | undefined;
if (data.state.forService) {
-
parsedServiceAtUri = expect(
-
parseCanonicalResourceUri(data.state.forService),
-
);
-
service = store.services.get(parsedServiceAtUri.rkey);
-
if (!service) {
-
let serviceRecord = await getRecord(
-
"systems.gaze.barometer.service",
-
parsedServiceAtUri.rkey,
-
);
-
if (!serviceRecord.ok) {
-
return badRequest({
-
msg: `service was not found or is invalid: ${serviceRecord.error}`,
-
});
-
}
-
service = {
-
record: serviceRecord.value,
-
checks: new Set(),
-
};
-
store.services.set(parsedServiceAtUri.rkey, service);
-
}
-
serviceAtUri = data.state.forService;
+
serviceAtUri = data.state.forService as ServiceUri;
+
const maybeService = await store.getOrFetch(serviceAtUri);
+
if (!maybeService.ok)
+
return error({
+
msg: `could not fetch service: ${maybeService.error}`,
+
});
+
service = maybeService.value;
} else if (data.serviceName) {
-
const serviceInfo = await systemctlShow(data.serviceName);
-
if (serviceInfo.ok) {
-
const record: SystemsGazeBarometerService.Main = {
-
$type: "systems.gaze.barometer.service",
-
name: data.serviceName,
-
description: serviceInfo.value.description,
-
hostedBy: `at://${config.repoDid}/systems.gaze.barometer.host/${store.hostname}`,
-
};
-
const rkey = generateTid();
-
const putAt = await putRecord(record, rkey);
-
data.state.forService = putAt.uri;
-
service = {
-
record,
-
checks: new Set(),
-
};
-
store.services.set(rkey, service);
-
serviceAtUri = putAt.uri;
-
parsedServiceAtUri = expect(parseCanonicalResourceUri(putAt.uri));
-
} else {
-
return badRequest({
-
msg: `could not fetch service from systemd: ${serviceInfo.error}`,
+
const maybeService = await store.getServiceFromSystemd(data.serviceName);
+
if (!maybeService.ok)
+
return error({
+
msg: `could not fetch service from systemd: ${maybeService.error}`,
});
-
}
+
const [uri, srv] = maybeService.value;
+
serviceAtUri = uri;
+
service = srv;
} else {
-
return badRequest({
+
return error({
msg: `either 'state.forService' or 'serviceName' must be provided`,
});
}
+
let check: Check | undefined = undefined;
if (data.state.generatedBy) {
-
const checkAtUri = expect(
-
parseCanonicalResourceUri(data.state.generatedBy),
+
const maybeCheck = await store.getOrFetch(
+
data.state.generatedBy as CheckUri,
);
-
let check = store.checks.get(checkAtUri.rkey);
-
if (!check) {
-
let checkRecord = await getRecord(
-
"systems.gaze.barometer.check",
-
checkAtUri.rkey,
-
);
-
if (!checkRecord.ok) {
-
return badRequest({
-
msg: `check record not found or is invalid: ${checkRecord.error}`,
-
});
-
}
-
check = {
-
record: checkRecord.value,
-
};
-
store.checks.set(checkAtUri.rkey, check);
-
}
-
if (check.record.forService !== serviceAtUri) {
-
return badRequest({
+
if (!maybeCheck.ok) return error({ msg: maybeCheck.error });
+
check = maybeCheck.value;
+
if (check.record.forService !== serviceAtUri)
+
return error({
msg: `check record does not point to the same service as the state record service`,
});
-
}
// update services with check
-
service.checks.add(checkAtUri.rkey);
-
store.services.set(parsedServiceAtUri.rkey, service);
+
service.checks.add(check.rkey);
+
store.services.set(serviceAtUri, service);
}
-
const result = await putRecord(
-
{ ...data.state, forService: data.state.forService! },
-
generateTid(),
-
);
+
// get current state uri
+
const currentStateUri =
+
check && check.record.currentState
+
? check.record.currentState
+
: service.record.currentState;
+
+
if (currentStateUri) {
+
// fetch current state
+
const record = await store.getOrFetch(currentStateUri as StateUri);
+
if (!record.ok) return error({ msg: record.error });
+
const currentState = record.value;
+
+
// check if the state has changed
+
if (currentState.record.state === data.state.state)
+
return error(
+
{
+
msg: `state can't be the same as the latest state`,
+
},
+
208,
+
);
+
}
+
+
const stateRecord: SystemsGazeBarometerState.Main = {
+
$type: "systems.gaze.barometer.state",
+
...data.state,
+
forService: serviceAtUri,
+
changedAt: data.state.changedAt ?? new Date().toISOString(),
+
previous: currentStateUri,
+
};
+
const rkey = generateTid();
+
const result = await putRecord(stateRecord, rkey);
+
+
// store committed state in "cache"
+
store.states.set(result.uri as StateUri, { record: stateRecord, rkey });
+
+
// update check with new state url
+
if (check) {
+
check.record.currentState = result.uri;
+
store.checks.set(getUri("systems.gaze.barometer.check", check.rkey), check);
+
await putRecord(check.record, check.rkey);
+
} else {
+
// update service with new state url
+
service.record.currentState = result.uri;
+
store.services.set(serviceAtUri, service);
+
await putRecord(service.record, service.rkey);
+
}
+
return new Response(JSON.stringify({ cid: result.cid, uri: result.uri }));
};
+103 -5
proxy/src/store.ts
···
import os from "os";
-
import type { RecordKey } from "@atcute/lexicons";
+
import { parseCanonicalResourceUri, type RecordKey } from "@atcute/lexicons";
import type {
SystemsGazeBarometerCheck,
SystemsGazeBarometerHost,
SystemsGazeBarometerService,
+
SystemsGazeBarometerState,
} from "barometer-lexicon";
+
import {
+
err,
+
expect,
+
getRecord,
+
ok,
+
putRecord,
+
type CheckUri,
+
type CollectionUri,
+
type Result,
+
type ServiceUri,
+
type StateUri,
+
} from "./utils";
+
import { systemctlShow } from "./systemd";
+
import { config } from "./config";
+
import { now as generateTid } from "@atcute/tid";
+
export interface State {
+
rkey: RecordKey;
+
record: SystemsGazeBarometerState.Main;
+
}
export interface Check {
+
rkey: RecordKey;
record: SystemsGazeBarometerCheck.Main;
}
export interface Service {
checks: Set<RecordKey>;
+
rkey: RecordKey;
record: SystemsGazeBarometerService.Main;
}
class Store {
-
services;
-
checks;
+
services: Map<ServiceUri, Service>;
+
checks: Map<CheckUri, Check>;
+
states: Map<StateUri, State>;
host: SystemsGazeBarometerHost.Main | null;
hostname: string;
constructor() {
-
this.services = new Map<RecordKey, Service>();
-
this.checks = new Map<RecordKey, Check>();
+
this.services = new Map();
+
this.checks = new Map();
+
this.states = new Map();
this.host = null;
this.hostname = os.hostname();
}
+
+
getOrFetch = async <
+
Nsid extends
+
| "systems.gaze.barometer.state"
+
| "systems.gaze.barometer.check"
+
| "systems.gaze.barometer.service",
+
Uri extends CollectionUri<Nsid>,
+
>(
+
uri: Uri,
+
): Promise<
+
Result<
+
Uri extends StateUri
+
? State
+
: Uri extends CheckUri
+
? Check
+
: Uri extends ServiceUri
+
? Service
+
: never,
+
string
+
>
+
> => {
+
const parsedUri = expect(parseCanonicalResourceUri(uri));
+
const nsid = parsedUri.collection;
+
const record = await getRecord(nsid as Nsid, parsedUri.rkey);
+
if (!record.ok)
+
return err(`record not found or is invalid: ${record.error}`);
+
const data = {
+
record: record.value,
+
rkey: parsedUri.rkey,
+
};
+
+
switch (nsid) {
+
case "systems.gaze.barometer.state":
+
this.states.set(uri as StateUri, data as State);
+
return ok(data as any);
+
case "systems.gaze.barometer.check":
+
this.checks.set(uri as CheckUri, data as Check);
+
return ok(data as any);
+
case "systems.gaze.barometer.service":
+
this.services.set(
+
uri as ServiceUri,
+
{ checks: new Set(), ...data } as Service,
+
);
+
return ok(data as any);
+
default:
+
throw new Error(`unsupported namespace: ${nsid}`);
+
}
+
};
+
+
getServiceFromSystemd = async (
+
serviceName: string,
+
): Promise<Result<[ServiceUri, Service], string>> => {
+
const serviceInfo = await systemctlShow(serviceName);
+
if (serviceInfo.ok) {
+
const record: SystemsGazeBarometerService.Main = {
+
$type: "systems.gaze.barometer.service",
+
name: serviceName,
+
description: serviceInfo.value.description,
+
hostedBy: `at://${config.repoDid}/systems.gaze.barometer.host/${store.hostname}`,
+
};
+
const rkey = generateTid();
+
const putAt = await putRecord(record, rkey);
+
const serviceUri = putAt.uri as ServiceUri;
+
const service: Service = {
+
record,
+
checks: new Set(),
+
rkey,
+
};
+
store.services.set(serviceUri, service);
+
return ok([serviceUri, service]);
+
} else {
+
return err(`could not fetch service from systemd: ${serviceInfo.error}`);
+
}
+
};
}
const store = new Store();
+26 -12
proxy/src/utils.ts
···
import { config } from "./config";
import { ok as clientOk } from "@atcute/client";
import { atpClient } from ".";
+
import { now as generateTid } from "@atcute/tid";
+
import type { AtprotoDid } from "@atcute/lexicons/syntax";
+
+
export type CollectionUri<Nsid extends string> =
+
`at://${AtprotoDid}/${Nsid}/${RecordKey}`;
+
export type StateUri = CollectionUri<"systems.gaze.barometer.state">;
+
export type CheckUri = CollectionUri<"systems.gaze.barometer.check">;
+
export type ServiceUri = CollectionUri<"systems.gaze.barometer.service">;
+
+
export const getUri = <
+
Collection extends
+
| "systems.gaze.barometer.state"
+
| "systems.gaze.barometer.check"
+
| "systems.gaze.barometer.service",
+
>(
+
collection: Collection,
+
rkey: RecordKey,
+
): CollectionUri<Collection> => {
+
return `at://${config.repoDid}/${collection}/${rkey}`;
+
};
export type Result<T, E> =
| {
···
rkey,
},
});
-
if (!maybeRecord.ok) {
+
if (!maybeRecord.ok)
return err(maybeRecord.data.message ?? maybeRecord.data.error);
-
}
const maybeTyped = safeParse(
BarometerSchemas[collection],
maybeRecord.data.value,
);
-
if (!maybeTyped.ok) {
-
return err(maybeTyped.message);
-
}
+
if (!maybeTyped.ok) return err(maybeTyped.message);
return maybeTyped;
};
···
Collection extends keyof typeof BarometerSchemas,
>(
record: InferOutput<(typeof BarometerSchemas)[Collection]>,
-
rkey: RecordKey,
+
rkey: RecordKey | null = null,
) => {
return await clientOk(
atpClient.post("com.atproto.repo.putRecord", {
···
collection: record["$type"],
repo: config.repoDid,
record,
-
rkey,
+
rkey: rkey ?? generateTid(),
},
}),
);
···
async (req, srv) => {
for (const fn of fns) {
const result = await fn(req);
-
if (result instanceof Response) {
-
return result;
-
} else {
-
req = result;
-
}
+
if (result instanceof Response) return result;
+
else req = result;
}
return route(req, srv);
};