service status on atproto

refactor: move relevant code into store.ts and routes and automatically generate routes map

ptr.pet 356aadf7 bf0bc92e

verified
+222 -115
+53
proxy/bun.lock
··· 14 14 }, 15 15 "devDependencies": { 16 16 "@types/bun": "latest", 17 + "concurrently": "^9.2.0", 17 18 }, 18 19 "peerDependencies": { 19 20 "typescript": "^5", ··· 49 50 50 51 "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], 51 52 53 + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 54 + 55 + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 56 + 52 57 "barometer-lexicon": ["barometer-lexicon@file:../lib", { "dependencies": { "@atcute/lexicons": "^1.1.0" }, "devDependencies": { "@atcute/lex-cli": "^2.1.1", "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5" } }], 53 58 54 59 "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], 55 60 61 + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 62 + 63 + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], 64 + 65 + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 66 + 67 + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 68 + 69 + "concurrently": ["concurrently@9.2.0", "", { "dependencies": { "chalk": "^4.1.2", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { "concurrently": "dist/bin/concurrently.js", "conc": "dist/bin/concurrently.js" } }, "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ=="], 70 + 56 71 "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 57 72 73 + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 74 + 75 + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 76 + 58 77 "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], 78 + 79 + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], 80 + 81 + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 82 + 83 + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 84 + 85 + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 59 86 60 87 "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 61 88 62 89 "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], 63 90 91 + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], 92 + 93 + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], 94 + 95 + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], 96 + 97 + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 98 + 99 + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 100 + 101 + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], 102 + 103 + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], 104 + 105 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 106 + 64 107 "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 65 108 66 109 "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], 110 + 111 + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 112 + 113 + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 114 + 115 + "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=="], 116 + 117 + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], 118 + 119 + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 67 120 } 68 121 }
+39
proxy/gen-routes.ts
··· 1 + #!/usr/bin/env bun 2 + // build-routes.ts - Run this at build time 3 + 4 + import { Glob } from "bun"; 5 + import { writeFileSync } from "fs"; 6 + 7 + const routeImports: string[] = []; 8 + const routeMap: string[] = []; 9 + 10 + // Use Bun.Glob to find all route files 11 + const glob = new Glob("*.ts"); 12 + 13 + for await (const file of glob.scan("./src/routes")) { 14 + // Skip index.ts 15 + if (file === "index.ts") continue; 16 + 17 + const routeName = file.replace(".ts", ""); 18 + const routePath = `/${routeName}`; 19 + 20 + // Generate import statement 21 + routeImports.push(`import ${routeName}Route from "./${routeName}";`); 22 + 23 + // Generate route map entry 24 + routeMap.push(` "${routePath}": ${routeName}Route,`); 25 + } 26 + 27 + // Generate the complete index.ts content 28 + const indexContent = `// Auto-generated route index 29 + ${routeImports.join("\n")} 30 + 31 + export const routes = { 32 + ${routeMap.join(",\n")} 33 + }; 34 + 35 + export default routes; 36 + `; 37 + 38 + // Write the generated index.ts file 39 + writeFileSync("./src/routes/index.ts", indexContent);
+4 -2
proxy/package.json
··· 4 4 "type": "module", 5 5 "private": true, 6 6 "scripts": { 7 - "dev": "bun --watch src/index.ts" 7 + "build:routes": "bun run gen-routes.ts", 8 + "dev": "bun run build:routes && concurrently 'bun --watch gen-routes.ts' 'bun --watch src/index.ts'" 8 9 }, 9 10 "devDependencies": { 10 - "@types/bun": "latest" 11 + "@types/bun": "latest", 12 + "concurrently": "^9.2.0" 11 13 }, 12 14 "peerDependencies": { 13 15 "typescript": "^5"
+6 -113
proxy/src/index.ts
··· 7 7 PlcDidDocumentResolver, 8 8 WebDidDocumentResolver, 9 9 } from "@atcute/identity-resolver"; 10 - import { 11 - parseCanonicalResourceUri, 12 - type RecordKey, 13 - } from "@atcute/lexicons/syntax"; 14 10 import { config } from "./config"; 15 11 import type {} from "@atcute/atproto"; 16 - import { is, safeParse } from "@atcute/lexicons"; 17 - import { 18 - SystemsGazeBarometerState, 19 - SystemsGazeBarometerHost, 20 - SystemsGazeBarometerService, 21 - SystemsGazeBarometerCheck, 22 - } from "barometer-lexicon"; 23 - import { err, expect, getRecord, ok, putRecord, type Result } from "./utils"; 24 - 25 - interface Check { 26 - record: SystemsGazeBarometerCheck.Main; 27 - } 28 - interface Service { 29 - checks: Map<RecordKey, Check>; 30 - record: SystemsGazeBarometerService.Main; 31 - } 32 - const services = new Map<RecordKey, Service>(); 33 - let host: SystemsGazeBarometerHost.Main | null = null; 12 + import { getRecord, ok, putRecord, type Result } from "./utils"; 13 + import store from "./store"; 14 + import routes from "./routes"; 34 15 35 16 const docResolver = new CompositeDidDocumentResolver({ 36 17 methods: { ··· 58 39 os.hostname(), 59 40 ); 60 41 if (maybeRecord.ok) { 61 - host = maybeRecord.value; 42 + store.host = maybeRecord.value; 62 43 } 63 44 64 45 // if it doesnt exist we make a new one 65 - if (host === null) { 46 + if (store.host === null) { 66 47 const hostname = os.hostname(); 67 48 await putRecord( 68 49 { ··· 75 56 ); 76 57 } 77 58 78 - interface PushRequest { 79 - serviceName?: string; // service manager service name 80 - state: SystemsGazeBarometerState.Main; 81 - } 82 - 83 - const parsePushRequest = (json: unknown): Result<PushRequest, string> => { 84 - if (typeof json !== "object" || json === null) { 85 - return err("invalid request"); 86 - } 87 - if ("serviceName" in json && typeof json.serviceName !== "string") { 88 - return err("serviceName is not a string"); 89 - } 90 - if ("state" in json) { 91 - const parsed = safeParse(SystemsGazeBarometerState.mainSchema, json.state); 92 - if (!parsed.ok) { 93 - return err(`state is invalid: ${parsed.message}`); 94 - } 95 - } else { 96 - return err("state not found"); 97 - } 98 - return ok(json as PushRequest); 99 - }; 100 - 101 - const badRequest = <Error extends { msg: string }>(error: Error) => { 102 - return new Response(JSON.stringify(error), { status: 400 }); 103 - }; 104 - const server = Bun.serve({ 105 - routes: { 106 - "/push": { 107 - POST: async (req) => { 108 - const maybeData = parsePushRequest(await req.json()); 109 - if (!maybeData.ok) { 110 - return badRequest({ 111 - msg: `invalid request: ${maybeData.error}`, 112 - }); 113 - } 114 - const data = maybeData.value; 115 - 116 - const serviceAtUri = expect( 117 - parseCanonicalResourceUri(data.state.forService), 118 - ); 119 - let service = services.get(serviceAtUri.rkey); 120 - if (!service) { 121 - const serviceRecord = await getRecord( 122 - "systems.gaze.barometer.service", 123 - serviceAtUri.rkey, 124 - ); 125 - if (!serviceRecord.ok) { 126 - return badRequest({ 127 - msg: `service was not found or is invalid: ${serviceRecord.error}`, 128 - }); 129 - } 130 - service = { 131 - record: serviceRecord.value, 132 - checks: new Map(), 133 - }; 134 - services.set(serviceAtUri.rkey, service); 135 - } 136 - 137 - if (data.state.generatedBy) { 138 - const checkAtUri = expect( 139 - parseCanonicalResourceUri(data.state.generatedBy), 140 - ); 141 - let check = service.checks.get(checkAtUri.rkey); 142 - if (!check) { 143 - let checkRecord = await getRecord( 144 - "systems.gaze.barometer.check", 145 - checkAtUri.rkey, 146 - ); 147 - if (!checkRecord.ok) { 148 - return badRequest({ 149 - msg: `check record not found or is invalid: ${checkRecord.error}`, 150 - }); 151 - } 152 - check = { 153 - record: checkRecord.value, 154 - }; 155 - service.checks.set(checkAtUri.rkey, check); 156 - } 157 - } 158 - 159 - const result = await putRecord(data.state); 160 - return new Response( 161 - JSON.stringify({ cid: result.cid, uri: result.uri }), 162 - ); 163 - }, 164 - }, 165 - }, 166 - }); 59 + const server = Bun.serve({ routes }); 167 60 168 61 console.log(`server running on http://localhost:${server.port}`);
+8
proxy/src/routes/index.ts
··· 1 + // Auto-generated route index 2 + import pushRoute from "./push"; 3 + 4 + export const routes = { 5 + "/push": pushRoute, 6 + }; 7 + 8 + export default routes;
+85
proxy/src/routes/push.ts
··· 1 + import { SystemsGazeBarometerState } from "barometer-lexicon"; 2 + import { err, expect, getRecord, ok, putRecord, type Result } from "../utils"; 3 + import { parseCanonicalResourceUri, safeParse } from "@atcute/lexicons"; 4 + import store from "../store"; 5 + 6 + interface PushRequest { 7 + serviceName?: string; // service manager service name 8 + state: SystemsGazeBarometerState.Main; 9 + } 10 + 11 + const parsePushRequest = (json: unknown): Result<PushRequest, string> => { 12 + if (typeof json !== "object" || json === null) { 13 + return err("invalid request"); 14 + } 15 + if ("serviceName" in json && typeof json.serviceName !== "string") { 16 + return err("serviceName is not a string"); 17 + } 18 + if ("state" in json) { 19 + const parsed = safeParse(SystemsGazeBarometerState.mainSchema, json.state); 20 + if (!parsed.ok) { 21 + return err(`state is invalid: ${parsed.message}`); 22 + } 23 + } else { 24 + return err("state not found"); 25 + } 26 + return ok(json as PushRequest); 27 + }; 28 + 29 + const badRequest = <Error extends { msg: string }>(error: Error) => { 30 + return new Response(JSON.stringify(error), { status: 400 }); 31 + }; 32 + const POST = async (req: Bun.BunRequest) => { 33 + const maybeData = parsePushRequest(await req.json()); 34 + if (!maybeData.ok) { 35 + return badRequest({ 36 + msg: `invalid request: ${maybeData.error}`, 37 + }); 38 + } 39 + const data = maybeData.value; 40 + 41 + const serviceAtUri = expect(parseCanonicalResourceUri(data.state.forService)); 42 + let service = store.services.get(serviceAtUri.rkey); 43 + if (!service) { 44 + const serviceRecord = await getRecord( 45 + "systems.gaze.barometer.service", 46 + serviceAtUri.rkey, 47 + ); 48 + if (!serviceRecord.ok) { 49 + return badRequest({ 50 + msg: `service was not found or is invalid: ${serviceRecord.error}`, 51 + }); 52 + } 53 + service = { 54 + record: serviceRecord.value, 55 + checks: new Map(), 56 + }; 57 + store.services.set(serviceAtUri.rkey, service); 58 + } 59 + 60 + if (data.state.generatedBy) { 61 + const checkAtUri = expect( 62 + parseCanonicalResourceUri(data.state.generatedBy), 63 + ); 64 + let check = service.checks.get(checkAtUri.rkey); 65 + if (!check) { 66 + let checkRecord = await getRecord( 67 + "systems.gaze.barometer.check", 68 + checkAtUri.rkey, 69 + ); 70 + if (!checkRecord.ok) { 71 + return badRequest({ 72 + msg: `check record not found or is invalid: ${checkRecord.error}`, 73 + }); 74 + } 75 + check = { 76 + record: checkRecord.value, 77 + }; 78 + service.checks.set(checkAtUri.rkey, check); 79 + } 80 + } 81 + 82 + const result = await putRecord(data.state); 83 + return new Response(JSON.stringify({ cid: result.cid, uri: result.uri })); 84 + }; 85 + export default { POST };
+27
proxy/src/store.ts
··· 1 + import type { RecordKey } from "@atcute/lexicons"; 2 + import type { 3 + SystemsGazeBarometerCheck, 4 + SystemsGazeBarometerHost, 5 + SystemsGazeBarometerService, 6 + } from "barometer-lexicon"; 7 + 8 + interface Check { 9 + record: SystemsGazeBarometerCheck.Main; 10 + } 11 + interface Service { 12 + checks: Map<RecordKey, Check>; 13 + record: SystemsGazeBarometerService.Main; 14 + } 15 + 16 + class Store { 17 + services; 18 + host: SystemsGazeBarometerHost.Main | null; 19 + 20 + constructor() { 21 + this.services = new Map<RecordKey, Service>(); 22 + this.host = null; 23 + } 24 + } 25 + 26 + const store = new Store(); 27 + export default store;