service status on atproto

feat: handle relevant events from the jetstream

ptr.pet 7ad815b1 7ed262bd

verified
Changed files
+171 -10
proxy
+15
proxy/bun.lock
··· 8 8 "@atcute/client": "^4.0.3", 9 9 "@atcute/identity": "^1.0.3", 10 10 "@atcute/identity-resolver": "^1.1.3", 11 + "@atcute/jetstream": "^1.0.2", 11 12 "@atcute/lexicons": "^1.1.0", 12 13 "@atcute/tid": "^1.0.2", 13 14 "barometer-lexicon": "file:../lib", ··· 32 33 33 34 "@atcute/identity-resolver": ["@atcute/identity-resolver@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.0.4", "@atcute/util-fetch": "^1.0.1", "@badrap/valita": "^0.4.4" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-KZgGgg99CWaV7Df3+h3X/WMrDzTPQVfsaoIVbTNLx2B56BvCL2EmaxPSVw/7BFUJMZHlVU4rtoEB4lyvNyMswA=="], 34 35 36 + "@atcute/jetstream": ["@atcute/jetstream@1.0.2", "", { "dependencies": { "@atcute/lexicons": "^1.0.2", "@badrap/valita": "^0.4.2", "@mary-ext/event-iterator": "^1.0.0", "@mary-ext/simple-event-emitter": "^1.0.0", "partysocket": "^1.1.4", "type-fest": "^4.41.0", "yocto-queue": "^1.2.1" } }, "sha512-ZtdNNxl4zq9cgUpXSL9F+AsXUZt0Zuyj0V7974D7LxdMxfTItPnMZ9dRG8GoFkkGz3+pszdsG888Ix8C0F2+mA=="], 37 + 35 38 "@atcute/lex-cli": ["@atcute/lex-cli@2.1.1", "", { "dependencies": { "@atcute/lexicon-doc": "^1.0.2", "@badrap/valita": "^0.4.5", "@externdefs/collider": "^0.3.0", "picocolors": "^1.1.1", "prettier": "^3.5.3" }, "bin": { "lex-cli": "cli.mjs" } }, "sha512-QaR0sOP8Z24opGHKsSfleDbP/ahUb6HECkVaOqSwG7ORZzbLK1w0265o1BRjCVr2dT6FxlsMUa2Ge85JMA9bxg=="], 36 39 37 40 "@atcute/lexicon-doc": ["@atcute/lexicon-doc@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.5" } }, "sha512-U7rinsTOwXGGcrF6/s7GzTXargcQpDr4BTrj5ci/XTK+POEK5jpcI+Ag1fF932pBX3k97em6y4TWwTSO8M/McQ=="], ··· 46 49 47 50 "@externdefs/collider": ["@externdefs/collider@0.3.0", "", { "peerDependencies": { "@badrap/valita": "^0.4.4" } }, "sha512-x5CpeZ4c8n+1wMFthUMWSQKqCGcQo52/Qbda5ES+JFRRg/D8Ep6/JOvUUq5HExFuv/wW+6UYG2U/mXzw0IAd8Q=="], 48 51 52 + "@mary-ext/event-iterator": ["@mary-ext/event-iterator@1.0.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ=="], 53 + 54 + "@mary-ext/simple-event-emitter": ["@mary-ext/simple-event-emitter@1.0.0", "", {}, "sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg=="], 55 + 49 56 "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], 50 57 51 58 "@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], ··· 80 87 81 88 "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], 82 89 90 + "event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="], 91 + 83 92 "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], 84 93 85 94 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], ··· 89 98 "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 90 99 91 100 "parsimmon": ["parsimmon@1.18.1", "", {}, "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="], 101 + 102 + "partysocket": ["partysocket@1.1.4", "", { "dependencies": { "event-target-polyfill": "^0.0.4" } }, "sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A=="], 92 103 93 104 "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 94 105 ··· 110 121 111 122 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 112 123 124 + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], 125 + 113 126 "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 114 127 115 128 "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], ··· 121 134 "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], 122 135 123 136 "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], 137 + 138 + "yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], 124 139 125 140 "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 126 141 }
+1
proxy/package.json
··· 20 20 "@atcute/client": "^4.0.3", 21 21 "@atcute/identity": "^1.0.3", 22 22 "@atcute/identity-resolver": "^1.1.3", 23 + "@atcute/jetstream": "^1.0.2", 23 24 "@atcute/lexicons": "^1.1.0", 24 25 "@atcute/tid": "^1.0.2", 25 26 "barometer-lexicon": "file:../lib",
+3
proxy/src/index.ts
··· 20 20 } from "./utils"; 21 21 import store from "./store"; 22 22 import routes from "./routes"; 23 + import { handleEvents } from "./jetstream"; 23 24 24 25 const docResolver = new CompositeDidDocumentResolver({ 25 26 methods: { ··· 73 74 }); 74 75 75 76 console.log(`server running on http://localhost:${server.port}`); 77 + 78 + await handleEvents();
+121
proxy/src/jetstream.ts
··· 1 + import { JetstreamSubscription } from "@atcute/jetstream"; 2 + import { is, parse, parseCanonicalResourceUri } from "@atcute/lexicons"; 3 + import { 4 + SystemsGazeBarometerService, 5 + SystemsGazeBarometerCheck, 6 + SystemsGazeBarometerState, 7 + } from "barometer-lexicon"; 8 + import { config } from "./config"; 9 + import store, { type Service } from "./store"; 10 + import { expect, getRecord, log } from "./utils"; 11 + 12 + const subscription = new JetstreamSubscription({ 13 + url: "wss://jetstream2.us-east.bsky.network", 14 + wantedCollections: [ 15 + "systems.gaze.barometer.service", 16 + "systems.gaze.barometer.check", 17 + ], 18 + wantedDids: [config.repoDid], 19 + }); 20 + 21 + export const handleEvents = async () => { 22 + for await (const event of subscription) { 23 + if (event.kind !== "commit") { 24 + continue; 25 + } 26 + const { operation, collection, rkey } = event.commit; 27 + // log.info(`${operation} at://${event.did}/${collection}/${rkey}`); 28 + if (operation === "create" || operation === "update") { 29 + const record = event.commit.record; 30 + switch (collection) { 31 + case "systems.gaze.barometer.service": { 32 + const serviceRecord = parse( 33 + SystemsGazeBarometerService.mainSchema, 34 + record, 35 + ); 36 + // we dont care if its a dangling service 37 + if (!serviceRecord.hostedBy) { 38 + continue; 39 + } 40 + const hostAtUri = expect( 41 + parseCanonicalResourceUri(serviceRecord.hostedBy), 42 + ); 43 + // not our host 44 + if (hostAtUri.rkey !== store.hostname) { 45 + continue; 46 + } 47 + const service: Service = store.services.get(rkey) ?? { 48 + record: serviceRecord, 49 + checks: new Set(), 50 + }; 51 + store.services.set(rkey, { 52 + ...service, 53 + record: serviceRecord, 54 + }); 55 + break; 56 + } 57 + case "systems.gaze.barometer.check": { 58 + const checkRecord = parse( 59 + SystemsGazeBarometerCheck.mainSchema, 60 + record, 61 + ); 62 + const parsedServiceAtUri = expect( 63 + parseCanonicalResourceUri(checkRecord.forService), 64 + ); 65 + let service = store.services.get(parsedServiceAtUri.rkey); 66 + if (!service) { 67 + const serviceRecord = await getRecord( 68 + "systems.gaze.barometer.service", 69 + parsedServiceAtUri.rkey, 70 + ); 71 + if (!serviceRecord.ok) { 72 + // cant get service record 73 + log.error( 74 + `can't fetch service record (${checkRecord.forService}) for check record (at://${event.did}/${collection}/${rkey})`, 75 + ); 76 + continue; 77 + } 78 + service = { 79 + record: serviceRecord.value, 80 + checks: new Set(), 81 + }; 82 + } 83 + service.checks.add(rkey); 84 + store.checks.set(rkey, { record: checkRecord }); 85 + store.services.set(parsedServiceAtUri.rkey, service); 86 + break; 87 + } 88 + } 89 + } else { 90 + switch (collection) { 91 + case "systems.gaze.barometer.service": { 92 + const service = store.services.get(rkey); 93 + if (!service) { 94 + continue; 95 + } 96 + for (const checkKey of service.checks) { 97 + store.checks.delete(checkKey); 98 + } 99 + store.services.delete(rkey); 100 + break; 101 + } 102 + case "systems.gaze.barometer.check": { 103 + const check = store.checks.get(rkey); 104 + if (!check) { 105 + continue; 106 + } 107 + const parsedServiceAtUri = expect( 108 + parseCanonicalResourceUri(check.record.forService), 109 + ); 110 + const service = store.services.get(parsedServiceAtUri.rkey); 111 + if (service) { 112 + service.checks.delete(rkey); 113 + store.services.set(parsedServiceAtUri.rkey, service); 114 + } 115 + store.checks.delete(rkey); 116 + break; 117 + } 118 + } 119 + } 120 + } 121 + };
+28 -9
proxy/src/routes/push.ts
··· 1 1 import { err, expect, getRecord, ok, putRecord, type Result } from "../utils"; 2 - import { parseCanonicalResourceUri, safeParse } from "@atcute/lexicons"; 2 + import { 3 + parseCanonicalResourceUri, 4 + safeParse, 5 + type CanonicalResourceUri, 6 + type ParsedCanonicalResourceUri, 7 + type ResourceUri, 8 + } from "@atcute/lexicons"; 3 9 import store, { type Service } from "../store"; 4 10 import { systemctlShow } from "../systemd"; 5 11 import { config } from "../config"; ··· 64 70 const data = maybeData.value; 65 71 66 72 let service: Service | undefined = undefined; 73 + let serviceAtUri: ResourceUri; 74 + let parsedServiceAtUri: ParsedCanonicalResourceUri; 67 75 if (data.state.forService) { 68 - const serviceAtUri = expect( 76 + parsedServiceAtUri = expect( 69 77 parseCanonicalResourceUri(data.state.forService), 70 78 ); 71 - service = store.services.get(serviceAtUri.rkey); 79 + service = store.services.get(parsedServiceAtUri.rkey); 72 80 if (!service) { 73 81 let serviceRecord = await getRecord( 74 82 "systems.gaze.barometer.service", 75 - serviceAtUri.rkey, 83 + parsedServiceAtUri.rkey, 76 84 ); 77 85 if (!serviceRecord.ok) { 78 86 return badRequest({ ··· 81 89 } 82 90 service = { 83 91 record: serviceRecord.value, 84 - checks: new Map(), 92 + checks: new Set(), 85 93 }; 86 - store.services.set(serviceAtUri.rkey, service); 94 + store.services.set(parsedServiceAtUri.rkey, service); 87 95 } 96 + serviceAtUri = data.state.forService; 88 97 } else if (data.serviceName) { 89 98 const serviceInfo = await systemctlShow(data.serviceName); 90 99 if (serviceInfo.ok) { ··· 99 108 data.state.forService = putAt.uri; 100 109 service = { 101 110 record, 102 - checks: new Map(), 111 + checks: new Set(), 103 112 }; 104 113 store.services.set(rkey, service); 114 + serviceAtUri = putAt.uri; 115 + parsedServiceAtUri = expect(parseCanonicalResourceUri(putAt.uri)); 105 116 } else { 106 117 return badRequest({ 107 118 msg: `could not fetch service from systemd: ${serviceInfo.error}`, ··· 117 128 const checkAtUri = expect( 118 129 parseCanonicalResourceUri(data.state.generatedBy), 119 130 ); 120 - let check = service.checks.get(checkAtUri.rkey); 131 + let check = store.checks.get(checkAtUri.rkey); 121 132 if (!check) { 122 133 let checkRecord = await getRecord( 123 134 "systems.gaze.barometer.check", ··· 131 142 check = { 132 143 record: checkRecord.value, 133 144 }; 134 - service.checks.set(checkAtUri.rkey, check); 145 + store.checks.set(checkAtUri.rkey, check); 135 146 } 147 + if (check.record.forService !== serviceAtUri) { 148 + return badRequest({ 149 + msg: `check record does not point to the same service as the state record service`, 150 + }); 151 + } 152 + // update services with check 153 + service.checks.add(checkAtUri.rkey); 154 + store.services.set(parsedServiceAtUri.rkey, service); 136 155 } 137 156 138 157 const result = await putRecord(
+3 -1
proxy/src/store.ts
··· 10 10 record: SystemsGazeBarometerCheck.Main; 11 11 } 12 12 export interface Service { 13 - checks: Map<RecordKey, Check>; 13 + checks: Set<RecordKey>; 14 14 record: SystemsGazeBarometerService.Main; 15 15 } 16 16 17 17 class Store { 18 18 services; 19 + checks; 19 20 host: SystemsGazeBarometerHost.Main | null; 20 21 hostname: string; 21 22 22 23 constructor() { 23 24 this.services = new Map<RecordKey, Service>(); 25 + this.checks = new Map<RecordKey, Check>(); 24 26 this.host = null; 25 27 this.hostname = os.hostname(); 26 28 }