All files / api/src/connectors index.ts

94.44% Statements 34/36
100% Branches 18/18
100% Functions 6/6
94.44% Lines 34/36

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 891x             1x 1x             1x                                       1x   1x 4x   4x 2x 2x   1x 2x   1x 2x                         1x   1x 14x 8x 8x   7x 4x 4x 3x 3x 3x 4x 4x     4x 4x   5x 5x     1x 7x 7x  
import { getLibsqlUrl } from '@api/constants';
import type {
  CacheStorageConnector,
  LogsStorageConnector,
  UserDataStorageConnector,
} from '@api/types/connector';
import type { AppContext } from '@api/types/hono';
import { info } from '@shared/console-logging';
import {
  getLibsqlClient,
  libsqlCacheStorageConnector,
  libsqlLogsStorageConnector,
  libsqlUserDataStorageConnector,
  migrateLibsql,
} from './libsql';
import {
  supabaseCacheStorageConnector,
  supabaseLogsStorageConnector,
  supabaseUserDataStorageConnector,
} from './supabase';
 
/**
 * Storage backend selection.
 *
 * Setting `LIBSQL_URL` chooses libSQL; leaving it unset keeps Supabase, which
 * is what every existing deployment does. One variable covers both shapes
 * because the URL scheme carries the rest of the decision: `file:` is an
 * embedded database for the single-container deployment, `libsql://` or
 * `https://` is a remote one for Workers or any multi-instance deployment.
 *
 * The choice is made per request rather than at module load because on
 * Cloudflare Workers there is no environment until a request arrives — `c.env`
 * is populated from the request context, and `server.ts` merges `process.env`
 * into it so Node resolves the same way.
 */
export const usesLibsql = (c: AppContext): boolean => Boolean(getLibsqlUrl(c));
 
export const resolveUserDataConnector = (
  c: AppContext,
): UserDataStorageConnector =>
  usesLibsql(c)
    ? libsqlUserDataStorageConnector
    : supabaseUserDataStorageConnector;
 
export const resolveLogsConnector = (c: AppContext): LogsStorageConnector =>
  usesLibsql(c) ? libsqlLogsStorageConnector : supabaseLogsStorageConnector;
 
export const resolveCacheConnector = (c: AppContext): CacheStorageConnector =>
  usesLibsql(c) ? libsqlCacheStorageConnector : supabaseCacheStorageConnector;
 
/**
 * Migration state for the life of the process (or the Worker isolate).
 *
 * The promise is cached rather than a boolean so that requests arriving while
 * the first migration is still running wait for it instead of starting their
 * own. A failure clears the cache so the next request retries rather than
 * inheriting a rejected promise forever.
 *
 * Postgres is migrated by the `migrations` service in docker-compose, so this
 * only ever applies to libSQL.
 */
let migration: Promise<void> | null = null;
 
export const ensureStorageReady = async (c: AppContext): Promise<void> => {
  if (!usesLibsql(c)) {
    return;
  }
 
  if (!migration) {
    migration = migrateLibsql(getLibsqlClient(c))
      .then((applied) => {
        if (applied.length > 0) {
          info(`[libsql] applied ${applied.length} migration(s)`);
        }
      })
      .catch((e: unknown) => {
        migration = null;
        throw e;
      });
  }
 
  await migration;
};
 
/** Test helper: forget that migrations have run. */
export const resetStorageReady = (): void => {
  migration = null;
};