All files / web/src/db index.ts

57.33% Statements 86/150
75% Branches 3/4
66.66% Functions 2/3
57.33% Lines 86/150

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 1511x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                                                                                                 1x 1x 1x 1x 1x 1x 1x 1x 6234x 6234x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 6234x 6234x 1x 1x 1x 1x 1x 1x 1x 6234x 6234x 1x 1x 1x  
import http from 'node:http'
import { createClient } from '@libsql/client'
import { drizzle } from 'drizzle-orm/libsql'
import * as schema from './schema'
 
/**
 * Database connection and client
 *
 * Creates a singleton libSQL connection with Drizzle ORM.
 *
 * Connection URL formats:
 * - Dev: file:./data/sqlite.db (local SQLite file, no server needed)
 * - Prod: http://libsql.abaci.svc.cluster.local:8080 (libSQL server in k8s)
 *
 * IMPORTANT: The database connection is lazy-loaded to avoid accessing
 * the database at module import time, which would cause build failures
 * when the database doesn't exist (e.g., in CI/CD environments).
 */
 
const databaseUrl = process.env.DATABASE_URL || 'file:./data/sqlite.db'
const authToken = process.env.DATABASE_AUTH_TOKEN
 
/**
 * Pool-limited HTTP agent for libsql connections.
 *
 * Node.js's built-in fetch (undici) creates an unbounded connection pool:
 * every parallel query opens a new TCP connection that stays ESTABLISHED
 * forever. With 3 replicas doing 7+ parallel queries per page load, this
 * quickly exhausts sqld's max_concurrent_connections.
 *
 * By passing a custom fetch backed by http.Agent, we get:
 * - maxSockets: 10 — at most 10 TCP connections to libsql per pod
 * - keepAlive: true — connections are reused across requests
 * - keepAliveMsecs: 10s — idle connections are probed to stay alive
 * - Excess requests queue instead of opening new connections
 */
const libsqlAgent = new http.Agent({
  keepAlive: true,
  keepAliveMsecs: 10_000,
  maxSockets: 10,
  maxFreeSockets: 4,
})
 
/**
 * Custom fetch for @libsql/client that uses our pool-limited HTTP agent.
 * Only applies to HTTP URLs (production libsql); file:// URLs (dev) skip this.
 */
async function libsqlFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
  // Extract URL, method, headers, and body — @libsql/client passes a single
  // Request object (no init), so we must read everything from it.
  const isRequest = typeof input === 'object' && !(input instanceof URL) && 'method' in input
  const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
  // Only intercept HTTP requests to the libsql server
  if (!url.startsWith('http://')) {
    return fetch(input, init)
  }

  const method = init?.method || (isRequest ? (input as Request).method : 'GET')
  const rawHeaders = init?.headers || (isRequest ? (input as Request).headers : undefined)
  const headerObj: Record<string, string> = {}
  if (rawHeaders instanceof Headers) {
    rawHeaders.forEach((v, k) => {
      headerObj[k] = v
    })
  } else if (rawHeaders && typeof rawHeaders === 'object') {
    Object.assign(headerObj, rawHeaders)
  }

  // Read body: prefer init.body, fall back to Request.text()
  let bodyStr: string | undefined
  if (init?.body) {
    bodyStr = String(init.body)
  } else if (isRequest) {
    bodyStr = await (input as Request).text()
  }
  if (bodyStr) {
    headerObj['Content-Length'] = Buffer.byteLength(bodyStr).toString()
  }

  return new Promise<Response>((resolve, reject) => {
    const parsedUrl = new URL(url)
    const req = http.request(
      {
        hostname: parsedUrl.hostname,
        port: parsedUrl.port || 80,
        path: parsedUrl.pathname + parsedUrl.search,
        method,
        headers: headerObj,
        agent: libsqlAgent,
      },
      (res) => {
        const chunks: Buffer[] = []
        res.on('data', (chunk: Buffer) => chunks.push(chunk))
        res.on('end', () => {
          const body = Buffer.concat(chunks)
          resolve(
            new Response(body, {
              status: res.statusCode || 500,
              statusText: res.statusMessage || '',
              headers: new Headers(res.headers as Record<string, string>),
            })
          )
        })
        res.on('error', reject)
      }
    )
    req.on('error', reject)
    if (bodyStr) req.write(bodyStr)
    req.end()
  })
}
 
let _client: ReturnType<typeof createClient> | null = null
let _db: ReturnType<typeof drizzle<typeof schema>> | null = null
 
/**
 * Get the database connection (lazy-loaded singleton)
 * Only creates the connection when first accessed at runtime
 */
function getDb() {
  if (!_db) {
    const isHttp = databaseUrl.startsWith('http')
    _client = createClient({
      url: databaseUrl,
      authToken: authToken,
      // Limit concurrent HTTP requests to the libsql server per pod.
      // With 3 replicas, this caps total connections at ~60, well within
      // sqld's SQLD_MAX_CONCURRENT_CONNECTIONS=512.
      concurrency: 20,
      // Use pool-limited fetch for HTTP connections (production).
      // Prevents unbounded TCP connection growth from undici's default pool.
      ...(isHttp ? { fetch: libsqlFetch as unknown as typeof globalThis.fetch } : {}),
    })
 
    _db = drizzle(_client, { schema })
  }
  return _db
}
 
/**
 * Database client instance
 * Uses a Proxy to lazy-load the connection on first access
 */
export const db = new Proxy({} as ReturnType<typeof drizzle<typeof schema>>, {
  get(_target, prop) {
    return getDb()[prop as keyof ReturnType<typeof drizzle<typeof schema>>]
  },
})
 
export { schema }