Skip to content

Commit 46a59ed

Browse files
committed
feat(pg-cache): pluggable pool-factory seam (default = pg.Pool)
1 parent 18f0e15 commit 46a59ed

4 files changed

Lines changed: 169 additions & 7 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Locks the pool-factory seam: getPgPool must use a registered factory when
2+
// present, and fall back to the default pg.Pool builder otherwise. This is what
3+
// lets an alternate backend (e.g. PGlite) plug in without any change to pgpm /
4+
// pgsql-* — and guarantees the default path is untouched when nothing registers.
5+
6+
import { randomUUID } from 'crypto';
7+
import pg from 'pg';
8+
9+
import {
10+
defaultPgPoolFactory,
11+
getActivePgPoolFactory,
12+
getPgPool,
13+
hasPgPoolFactory,
14+
PgPoolFactory,
15+
registerPgPoolFactory
16+
} from '../index';
17+
import { pgCache } from '../lru';
18+
19+
const createMockPool = (): pg.Pool =>
20+
({ query: jest.fn(), connect: jest.fn(), end: jest.fn(async () => {}) } as unknown as pg.Pool);
21+
22+
const freshConfig = () => {
23+
const database = `seam_${randomUUID()}`;
24+
return { database, host: 'localhost', port: 5432, user: 'postgres', password: 'x' };
25+
};
26+
27+
describe('pg-cache pool-factory seam', () => {
28+
afterEach(() => {
29+
registerPgPoolFactory(undefined);
30+
});
31+
32+
it('defaults to no registered factory', () => {
33+
expect(hasPgPoolFactory()).toBe(false);
34+
expect(getActivePgPoolFactory()).toBeUndefined();
35+
});
36+
37+
it('register/reset toggles the active factory', () => {
38+
const factory: PgPoolFactory = () => createMockPool();
39+
registerPgPoolFactory(factory);
40+
expect(hasPgPoolFactory()).toBe(true);
41+
expect(getActivePgPoolFactory()).toBe(factory);
42+
43+
registerPgPoolFactory(undefined);
44+
expect(hasPgPoolFactory()).toBe(false);
45+
});
46+
47+
it('getPgPool builds via the registered factory (no real pg connection)', () => {
48+
const cfg = freshConfig();
49+
const mock = createMockPool();
50+
const factory = jest.fn<pg.Pool, [any]>(() => mock);
51+
registerPgPoolFactory(factory);
52+
53+
const pool = getPgPool(cfg);
54+
55+
expect(factory).toHaveBeenCalledTimes(1);
56+
expect(pool).toBe(mock);
57+
58+
pgCache.delete(cfg.database);
59+
});
60+
61+
it('caches by database: a second call reuses the pool and does not re-invoke the factory', () => {
62+
const cfg = freshConfig();
63+
const factory = jest.fn<pg.Pool, [any]>(() => createMockPool());
64+
registerPgPoolFactory(factory);
65+
66+
const first = getPgPool(cfg);
67+
const second = getPgPool(cfg);
68+
69+
expect(first).toBe(second);
70+
expect(factory).toHaveBeenCalledTimes(1);
71+
72+
pgCache.delete(cfg.database);
73+
});
74+
75+
it('falls back to defaultPgPoolFactory when nothing is registered', () => {
76+
const cfg = freshConfig();
77+
// defaultPgPoolFactory builds a real pg.Pool but does NOT connect until a
78+
// query runs, so this is safe without a live server.
79+
const pool = getPgPool(cfg);
80+
expect(pool).toBeInstanceOf(pg.Pool);
81+
pgCache.delete(cfg.database);
82+
});
83+
84+
it('defaultPgPoolFactory returns a pg.Pool', () => {
85+
const pool = defaultPgPoolFactory(freshConfig());
86+
expect(pool).toBeInstanceOf(pg.Pool);
87+
return pool.end();
88+
});
89+
});

postgres/pg-cache/src/driver.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type pg from 'pg';
2+
import type { PgConfig, PgPoolConfig } from 'pg-env';
3+
4+
/**
5+
* Minimal connection-factory seam for pg-cache.
6+
*
7+
* `getPgPool` funnels every pooled connection in the stack through a single
8+
* factory. By default that factory builds a real `pg.Pool` (see
9+
* `defaultPgPoolFactory`). Registering an alternate factory lets a different
10+
* backend (e.g. an in-process PGlite instance) supply the pool WITHOUT any
11+
* backend-specific dependency leaking into pg-cache or its consumers.
12+
*
13+
* The factory returns a `pg.Pool`; the only surface pgpm/pgsql-* actually rely
14+
* on is `query()`, `connect()` and `end()`, so an adapter may return an object
15+
* that implements that subset. `QueryablePool` documents that contract.
16+
*/
17+
export interface QueryableClient {
18+
query(text: string, values?: any[]): Promise<any>;
19+
release(...args: any[]): void;
20+
}
21+
22+
export interface QueryablePool {
23+
query(text: string, values?: any[]): Promise<any>;
24+
connect(): Promise<QueryableClient>;
25+
end(): Promise<void>;
26+
}
27+
28+
export type PgPoolFactory = (
29+
config: Partial<PgConfig> & { pool?: PgPoolConfig }
30+
) => pg.Pool;
31+
32+
let activeFactory: PgPoolFactory | undefined;
33+
34+
/**
35+
* Register the factory `getPgPool` uses to build new pools. Pass `undefined`
36+
* to restore the default (`pg.Pool`) behavior.
37+
*
38+
* Note: only affects pools created after registration; already-cached pools are
39+
* unchanged. Callers that need a clean slate should tear down existing pools
40+
* first (see `teardownPgPools`).
41+
*/
42+
export const registerPgPoolFactory = (factory: PgPoolFactory | undefined): void => {
43+
activeFactory = factory;
44+
};
45+
46+
/** The currently-registered factory, or `undefined` when using the default. */
47+
export const getActivePgPoolFactory = (): PgPoolFactory | undefined => activeFactory;
48+
49+
/** Whether a non-default pool factory is currently registered. */
50+
export const hasPgPoolFactory = (): boolean => activeFactory !== undefined;

postgres/pg-cache/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
// Main exports from pg-cache package
2+
export {
3+
getActivePgPoolFactory,
4+
hasPgPoolFactory,
5+
registerPgPoolFactory
6+
} from './driver';
27
export {
38
close,
49
getPgCacheConfig,
@@ -8,9 +13,11 @@ export {
813
} from './lru';
914
export {
1015
buildConnectionString,
16+
defaultPgPoolFactory,
1117
getPgPool,
1218
getPgPoolConfig
1319
} from './pg';
1420

1521
// Re-export types
22+
export type { PgPoolFactory, QueryableClient, QueryablePool } from './driver';
1623
export type { PgCacheConfig, PoolCleanupCallback } from './lru';

postgres/pg-cache/src/pg.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import pg from 'pg';
22
import { getPgEnvOptions, PgConfig, PgPoolConfig } from 'pg-env';
33
import { Logger } from '@pgpmjs/logger';
44

5+
import { getActivePgPoolFactory, PgPoolFactory } from './driver';
56
import { pgCache } from './lru';
67

78
const log = new Logger('pg-cache');
@@ -38,13 +39,13 @@ export function getPgPoolConfig(overrides?: PgPoolConfig): pg.PoolConfig {
3839
};
3940
}
4041

41-
export const getPgPool = (pgConfig: Partial<PgConfig> & { pool?: PgPoolConfig }): pg.Pool => {
42+
/**
43+
* Default pool factory: builds a real `pg.Pool` over TCP. This is the behavior
44+
* used whenever no alternate driver is registered (see `./driver`).
45+
*/
46+
export const defaultPgPoolFactory: PgPoolFactory = (pgConfig): pg.Pool => {
4247
const config = getPgEnvOptions(pgConfig);
43-
const { user, password, host, port, database, } = config;
44-
if (pgCache.has(database)) {
45-
const cached = pgCache.get(database);
46-
if (cached) return cached;
47-
}
48+
const { user, password, host, port, database } = config;
4849
const connectionString = buildConnectionString(user, password, host, port, database);
4950
const poolConfig = getPgPoolConfig(pgConfig.pool);
5051
const pgPool = new pg.Pool({ connectionString, ...poolConfig });
@@ -100,7 +101,22 @@ export const getPgPool = (pgConfig: Partial<PgConfig> & { pool?: PgPoolConfig })
100101
log.error(`Pool ${database} unexpected idle connection error [${err.code || 'unknown'}]: ${err.message}`);
101102
}
102103
});
103-
104+
105+
return pgPool;
106+
};
107+
108+
export const getPgPool = (pgConfig: Partial<PgConfig> & { pool?: PgPoolConfig }): pg.Pool => {
109+
const config = getPgEnvOptions(pgConfig);
110+
const { database } = config;
111+
if (pgCache.has(database)) {
112+
const cached = pgCache.get(database);
113+
if (cached) return cached;
114+
}
115+
116+
// Route through the registered driver (default = pg.Pool over TCP).
117+
const factory = getActivePgPoolFactory() ?? defaultPgPoolFactory;
118+
const pgPool = factory(pgConfig);
119+
104120
pgCache.set(database, pgPool);
105121
return pgPool;
106122
};

0 commit comments

Comments
 (0)