Repository navigation
Expand file tree
/
Copy pathprobes.ts
More file actions
112 lines (106 loc) · 3.46 KB
/
Copy pathprobes.ts
File metadata and controls
112 lines (106 loc) · 3.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { readFileSync } from 'node:fs'
import net from 'node:net'
import path from 'node:path'
import tls from 'node:tls'
import { getErrorMessage } from '@sim/utils/errors'
import { sleep } from '@sim/utils/helpers'
import postgres from 'postgres'
import { ROOT } from './env-files.ts'
export interface PgProbeResult {
ok: boolean
error?: string
pgvectorAvailable?: boolean
migrations?: { applied: number | null; journal: number }
}
function journalMigrationCount(): number {
const journalPath = path.join(ROOT, 'packages/db/migrations/meta/_journal.json')
const journal = JSON.parse(readFileSync(journalPath, 'utf8')) as { entries: unknown[] }
return journal.entries.length
}
export async function pgProbe(dsn: string): Promise<PgProbeResult> {
const sql = postgres(dsn, { max: 1, connect_timeout: 5, onnotice: () => {} })
try {
await sql`select 1`
const vector = await sql`select 1 from pg_available_extensions where name = 'vector'`
let applied: number | null = null
try {
const rows = await sql`select count(*)::int as n from drizzle.__drizzle_migrations`
applied = rows[0].n as number
} catch {
applied = null
}
return {
ok: true,
pgvectorAvailable: vector.length > 0,
migrations: { applied, journal: journalMigrationCount() },
}
} catch (error) {
return { ok: false, error: getErrorMessage(error, 'connection failed') }
} finally {
await sql.end({ timeout: 1 })
}
}
export function redisPing(url: string, timeoutMs = 2000): Promise<{ ok: boolean; error?: string }> {
return new Promise((resolve) => {
let parsed: URL
try {
parsed = new URL(url)
} catch {
resolve({ ok: false, error: 'invalid REDIS_URL' })
return
}
const port = Number(parsed.port || 6379)
const secure = parsed.protocol === 'rediss:'
const socket = secure
? tls.connect({
host: parsed.hostname,
port,
servername: process.env.REDIS_TLS_SERVERNAME || parsed.hostname,
})
: net.connect({ host: parsed.hostname, port })
let buffer = ''
const done = (result: { ok: boolean; error?: string }) => {
socket.destroy()
resolve(result)
}
socket.setTimeout(timeoutMs, () => done({ ok: false, error: 'timeout' }))
socket.once('error', (error) => done({ ok: false, error: getErrorMessage(error) }))
socket.once(secure ? 'secureConnect' : 'connect', () => {
const auth = parsed.password
? `AUTH ${parsed.username || ''} ${parsed.password}\r\n`.replace('AUTH ', 'AUTH ')
: ''
socket.write(`${auth}PING\r\n`)
})
socket.on('data', (chunk) => {
buffer += chunk.toString()
if (buffer.includes('+PONG')) done({ ok: true })
else if (
buffer.includes('-ERR') ||
buffer.includes('-NOAUTH') ||
buffer.includes('-WRONGPASS')
)
done({ ok: false, error: buffer.split('\r\n')[0] })
})
})
}
export async function httpHealth(url: string, timeoutMs = 3000): Promise<boolean> {
try {
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) })
return res.ok
} catch {
return false
}
}
/** Polls a probe until it succeeds or the window elapses. */
export async function waitFor(
probe: () => Promise<boolean>,
totalMs: number,
intervalMs = 2000
): Promise<boolean> {
const deadline = Date.now() + totalMs
while (Date.now() < deadline) {
if (await probe()) return true
await sleep(intervalMs)
}
return probe()
}