make kestrel a tak server, so that it can send and receive pois as cots data
Some checks failed
ci/woodpecker/pr/pr Pipeline failed
Some checks failed
ci/woodpecker/pr/pr Pipeline failed
This commit is contained in:
@@ -3,11 +3,13 @@ import { mkdirSync, existsSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { promisify } from 'node:util'
|
||||
import { bootstrapAdmin } from './bootstrap.js'
|
||||
import { registerCleanup } from './shutdown.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const sqlite3 = require('sqlite3')
|
||||
// Resolve from project root so bundled server (e.g. .output) finds node_modules/sqlite3
|
||||
const requireFromRoot = createRequire(join(process.cwd(), 'package.json'))
|
||||
const sqlite3 = requireFromRoot('sqlite3')
|
||||
|
||||
const SCHEMA_VERSION = 3
|
||||
const SCHEMA_VERSION = 4
|
||||
const DB_BUSY_TIMEOUT_MS = 5000
|
||||
|
||||
let dbInstance = null
|
||||
@@ -111,6 +113,12 @@ const migrateToV3 = async (run, all) => {
|
||||
await run('ALTER TABLE users ADD COLUMN avatar_path TEXT')
|
||||
}
|
||||
|
||||
const migrateToV4 = async (run, all) => {
|
||||
const info = await all('PRAGMA table_info(users)')
|
||||
if (info.some(c => c.name === 'cot_password_hash')) return
|
||||
await run('ALTER TABLE users ADD COLUMN cot_password_hash TEXT')
|
||||
}
|
||||
|
||||
const runMigrations = async (run, all, get) => {
|
||||
const version = await getSchemaVersion(get)
|
||||
if (version >= SCHEMA_VERSION) return
|
||||
@@ -122,6 +130,10 @@ const runMigrations = async (run, all, get) => {
|
||||
await migrateToV3(run, all)
|
||||
await setSchemaVersion(run, 3)
|
||||
}
|
||||
if (version < 4) {
|
||||
await migrateToV4(run, all)
|
||||
await setSchemaVersion(run, 4)
|
||||
}
|
||||
}
|
||||
|
||||
const initDb = async (db, run, all, get) => {
|
||||
@@ -167,9 +179,91 @@ export async function getDb() {
|
||||
}
|
||||
|
||||
dbInstance = { db, run, all, get }
|
||||
|
||||
registerCleanup(async () => {
|
||||
if (dbInstance) {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
dbInstance.db.close((err) => {
|
||||
if (err) reject(err)
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[db] Error closing database during shutdown:', error?.message)
|
||||
}
|
||||
dbInstance = null
|
||||
}
|
||||
})
|
||||
|
||||
return dbInstance
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check for database connection.
|
||||
* @returns {Promise<{ healthy: boolean, error?: string }>} Health status
|
||||
*/
|
||||
export async function healthCheck() {
|
||||
try {
|
||||
const db = await getDb()
|
||||
await db.get('SELECT 1')
|
||||
return { healthy: true }
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error?.message || String(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database connection model documentation:
|
||||
*
|
||||
* KestrelOS uses SQLite with WAL (Write-Ahead Logging) mode for concurrent access.
|
||||
* - Single connection instance shared across all requests (singleton pattern)
|
||||
* - WAL mode allows multiple readers and one writer concurrently
|
||||
* - Connection is initialized on first getDb() call and reused thereafter
|
||||
* - Busy timeout is set to 5000ms to handle concurrent access gracefully
|
||||
* - Transactions are supported via withTransaction() helper
|
||||
*
|
||||
* Concurrency considerations:
|
||||
* - SQLite with WAL handles concurrent reads efficiently
|
||||
* - Writes are serialized (one at a time)
|
||||
* - For high write loads, consider migrating to PostgreSQL
|
||||
* - Current model is suitable for moderate traffic (< 100 req/sec)
|
||||
*
|
||||
* Connection lifecycle:
|
||||
* - Created on first getDb() call
|
||||
* - Persists for application lifetime
|
||||
* - Closed during graceful shutdown
|
||||
* - Test path can be set via setDbPathForTest() for testing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Execute a callback within a database transaction.
|
||||
* Automatically commits on success or rolls back on error.
|
||||
* @param {object} db - Database instance from getDb()
|
||||
* @param {Function} callback - Async function receiving { run, all, get }
|
||||
* @returns {Promise<any>} Result of callback
|
||||
*/
|
||||
export async function withTransaction(db, callback) {
|
||||
const { run } = db
|
||||
await run('BEGIN TRANSACTION')
|
||||
try {
|
||||
const result = await callback(db)
|
||||
await run('COMMIT')
|
||||
return result
|
||||
}
|
||||
catch (error) {
|
||||
await run('ROLLBACK').catch(() => {
|
||||
// Ignore rollback errors
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function closeDb() {
|
||||
if (!dbInstance) return
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user