+15
proxy/bun.lock
+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
+1
proxy/package.json
+3
proxy/src/index.ts
+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
+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
+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
+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
}