bopodev-api 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/server.ts +16 -3
- package/src/worker/scheduler.ts +63 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bopodev-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"nanoid": "^5.1.5",
|
|
16
16
|
"ws": "^8.19.0",
|
|
17
17
|
"zod": "^4.1.5",
|
|
18
|
-
"bopodev-agent-sdk": "0.1.
|
|
19
|
-
"bopodev-
|
|
20
|
-
"bopodev-
|
|
18
|
+
"bopodev-agent-sdk": "0.1.8",
|
|
19
|
+
"bopodev-contracts": "0.1.8",
|
|
20
|
+
"bopodev-db": "0.1.8"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/cors": "^2.8.19",
|
package/src/server.ts
CHANGED
|
@@ -59,10 +59,12 @@ async function main() {
|
|
|
59
59
|
console.log(`BopoHQ API running on http://localhost:${port}`);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
62
|
+
const configuredSchedulerCompanyIds = parseSchedulerCompanyIds(process.env.BOPO_SCHEDULER_COMPANY_IDS);
|
|
63
|
+
if (process.env.BOPO_DEFAULT_COMPANY_ID && !process.env.BOPO_SCHEDULER_COMPANY_IDS) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.warn("[startup] BOPO_DEFAULT_COMPANY_ID no longer scopes heartbeat sweeps; scheduler now sweeps all companies.");
|
|
65
66
|
}
|
|
67
|
+
createHeartbeatScheduler(db, realtimeHub, { companyIds: configuredSchedulerCompanyIds });
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
void main();
|
|
@@ -90,3 +92,14 @@ function emitCodexPreflightWarning(health: RuntimeCommandHealth) {
|
|
|
90
92
|
process.stderr.write(` Details: ${JSON.stringify(health)}\n`);
|
|
91
93
|
}
|
|
92
94
|
}
|
|
95
|
+
|
|
96
|
+
function parseSchedulerCompanyIds(rawValue?: string) {
|
|
97
|
+
if (!rawValue || rawValue.trim().length === 0) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const ids = rawValue
|
|
101
|
+
.split(",")
|
|
102
|
+
.map((value) => value.trim())
|
|
103
|
+
.filter((value) => value.length > 0);
|
|
104
|
+
return ids.length > 0 ? ids : undefined;
|
|
105
|
+
}
|
package/src/worker/scheduler.ts
CHANGED
|
@@ -1,23 +1,80 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
1
2
|
import type { BopoDb } from "bopodev-db";
|
|
2
3
|
import type { RealtimeHub } from "../realtime/hub";
|
|
3
4
|
import { runHeartbeatSweep } from "../services/heartbeat-service";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
interface HeartbeatSchedulerOptions {
|
|
7
|
+
companyIds?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createHeartbeatScheduler(db: BopoDb, realtimeHub?: RealtimeHub, options?: HeartbeatSchedulerOptions) {
|
|
6
11
|
const intervalMs = Number(process.env.BOPO_HEARTBEAT_SWEEP_MS ?? 60_000);
|
|
12
|
+
const warnedMissingConfiguredCompanyIds = new Set<string>();
|
|
7
13
|
let running = false;
|
|
8
14
|
const timer = setInterval(() => {
|
|
9
15
|
if (running) {
|
|
10
16
|
return;
|
|
11
17
|
}
|
|
12
18
|
running = true;
|
|
13
|
-
void
|
|
14
|
-
.catch((error) => {
|
|
15
|
-
// eslint-disable-next-line no-console
|
|
16
|
-
console.error("[scheduler] heartbeat sweep failed", error);
|
|
17
|
-
})
|
|
19
|
+
void sweepScheduledCompanies(db, realtimeHub, options, warnedMissingConfiguredCompanyIds)
|
|
18
20
|
.finally(() => {
|
|
19
21
|
running = false;
|
|
20
22
|
});
|
|
21
23
|
}, intervalMs);
|
|
22
24
|
return () => clearInterval(timer);
|
|
23
25
|
}
|
|
26
|
+
|
|
27
|
+
async function sweepScheduledCompanies(
|
|
28
|
+
db: BopoDb,
|
|
29
|
+
realtimeHub?: RealtimeHub,
|
|
30
|
+
options?: HeartbeatSchedulerOptions,
|
|
31
|
+
warnedMissingConfiguredCompanyIds?: Set<string>
|
|
32
|
+
) {
|
|
33
|
+
const companyIds = options?.companyIds?.length
|
|
34
|
+
? await resolveConfiguredCompanyIds(db, options.companyIds, warnedMissingConfiguredCompanyIds ?? new Set<string>())
|
|
35
|
+
: await listCompanyIds(db);
|
|
36
|
+
for (const companyId of companyIds) {
|
|
37
|
+
try {
|
|
38
|
+
await runHeartbeatSweep(db, companyId, { realtimeHub });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// eslint-disable-next-line no-console
|
|
41
|
+
console.error(`[scheduler] heartbeat sweep failed for company '${companyId}': ${toErrorMessage(error)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function listCompanyIds(db: BopoDb) {
|
|
47
|
+
const result = await db.execute(sql`
|
|
48
|
+
SELECT id
|
|
49
|
+
FROM companies
|
|
50
|
+
ORDER BY created_at ASC
|
|
51
|
+
`);
|
|
52
|
+
return (result.rows ?? [])
|
|
53
|
+
.map((row) => row.id)
|
|
54
|
+
.filter((id): id is string => typeof id === "string" && id.length > 0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function resolveConfiguredCompanyIds(
|
|
58
|
+
db: BopoDb,
|
|
59
|
+
configuredCompanyIds: string[],
|
|
60
|
+
warnedMissingConfiguredCompanyIds: Set<string>
|
|
61
|
+
) {
|
|
62
|
+
const existingCompanyIds = new Set(await listCompanyIds(db));
|
|
63
|
+
const activeCompanyIds: string[] = [];
|
|
64
|
+
for (const companyId of configuredCompanyIds) {
|
|
65
|
+
if (existingCompanyIds.has(companyId)) {
|
|
66
|
+
activeCompanyIds.push(companyId);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!warnedMissingConfiguredCompanyIds.has(companyId)) {
|
|
70
|
+
warnedMissingConfiguredCompanyIds.add(companyId);
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.warn(`[scheduler] skipping unknown company id '${companyId}' from BOPO_SCHEDULER_COMPANY_IDS`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return activeCompanyIds;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toErrorMessage(error: unknown) {
|
|
79
|
+
return error instanceof Error ? error.message : String(error);
|
|
80
|
+
}
|