@open-mercato/shared 0.6.7-develop.6870.1.b69802067a → 0.6.8-develop.6874.1.982d6097d8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/db/mikro.js +12 -3
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +3 -3
- package/src/lib/db/__tests__/mikro.test.ts +25 -0
- package/src/lib/db/mikro.ts +38 -13
package/dist/lib/db/mikro.js
CHANGED
|
@@ -49,6 +49,16 @@ function resolvePoolConfig(env = process.env) {
|
|
|
49
49
|
lockTimeoutMs: parsePositiveIntEnv(env.DB_LOCK_TIMEOUT_MS)
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
+
function attachPoolErrorHandlers(pool) {
|
|
53
|
+
pool.on("error", (err) => {
|
|
54
|
+
logger.warn("Idle pg pool client error (connection reaped/terminated)", { err });
|
|
55
|
+
});
|
|
56
|
+
pool.on("connect", (client) => {
|
|
57
|
+
client.on("error", (err) => {
|
|
58
|
+
logger.warn("pg client error (connection reaped/terminated)", { err });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
52
62
|
async function getOrm() {
|
|
53
63
|
if (ormInstance) {
|
|
54
64
|
return ormInstance;
|
|
@@ -113,9 +123,7 @@ async function getOrm() {
|
|
|
113
123
|
options: connectionOptions,
|
|
114
124
|
ssl: sslConfig,
|
|
115
125
|
onPoolCreated: (pool) => {
|
|
116
|
-
pool
|
|
117
|
-
logger.warn("Idle pg pool client error (connection reaped/terminated)", { err });
|
|
118
|
-
});
|
|
126
|
+
attachPoolErrorHandlers(pool);
|
|
119
127
|
if (process.env.OM_DB_POOL_DEBUG === "1" || process.env.OM_INTEGRATION_TEST === "true") {
|
|
120
128
|
logger.info("pg pool created with options", {
|
|
121
129
|
max: pool.options?.max,
|
|
@@ -139,6 +147,7 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
139
147
|
void closeOrmIfLoaded();
|
|
140
148
|
}
|
|
141
149
|
export {
|
|
150
|
+
attachPoolErrorHandlers,
|
|
142
151
|
getOrm,
|
|
143
152
|
getOrmEntities,
|
|
144
153
|
registerOrmEntities,
|
package/dist/lib/db/mikro.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/db/mikro.ts"],
|
|
4
|
-
"sourcesContent": ["import 'dotenv/config'\nimport 'reflect-metadata'\nimport { MikroORM } from '@mikro-orm/core'\nimport { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy'\nimport { PostgreSqlDriver, type EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport { getSslConfig } from './ssl'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'orm' })\n\nexport type AppMikroORM = MikroORM<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>\n\nlet ormInstance: AppMikroORM | null = null\n\n// Use globalThis so standalone apps survive duplicated shared package module instances.\nconst GLOBAL_ENTITIES_KEY = '__openMercatoOrmEntities__'\n\nfunction getRegisteredEntities(): any[] | null {\n return (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] as any[] | null ?? null\n}\n\nfunction setRegisteredEntities(entities: any[]): void {\n (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] = entities\n}\n\nexport function registerOrmEntities(entities: any[]) {\n if (getRegisteredEntities() !== null && process.env.NODE_ENV === 'development') {\n logger.debug('ORM entities re-registered (this may occur during HMR)')\n }\n setRegisteredEntities(entities)\n}\n\nexport function getOrmEntities(): any[] {\n const entities = getRegisteredEntities()\n if (!entities) {\n throw new Error('[Bootstrap] ORM entities not registered. Call registerOrmEntities() at bootstrap.')\n }\n return entities\n}\n\nexport type ResolvedPoolConfig = {\n poolMin: number\n poolMax: number\n poolIdleTimeout: number\n poolAcquireTimeout: number\n idleSessionTimeoutMs: number | undefined\n idleInTransactionTimeoutMs: number | undefined\n statementTimeoutMs: number | undefined\n lockTimeoutMs: number | undefined\n}\n\n// Parse an optional positive-millisecond env var. Returns undefined when unset,\n// non-numeric, or non-positive so callers treat \"no value\" as \"no timeout\".\nfunction parsePositiveIntEnv(raw: string | undefined): number | undefined {\n const parsed = parseInt(raw || '')\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined\n}\n\nexport function resolvePoolConfig(env: NodeJS.ProcessEnv = process.env): ResolvedPoolConfig {\n const idleSessionTimeoutEnv = parseInt(env.DB_IDLE_SESSION_TIMEOUT_MS || '')\n const idleInTxTimeoutEnv = parseInt(env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || '')\n return {\n poolMin: parseInt(env.DB_POOL_MIN || '2'),\n poolMax: parseInt(env.DB_POOL_MAX || '20'),\n poolIdleTimeout: parseInt(env.DB_POOL_IDLE_TIMEOUT || '3000'),\n poolAcquireTimeout: parseInt(env.DB_POOL_ACQUIRE_TIMEOUT || '6000'),\n idleSessionTimeoutMs: Number.isFinite(idleSessionTimeoutEnv)\n ? idleSessionTimeoutEnv\n : env.NODE_ENV === 'production'\n ? undefined\n : 600_000,\n // Finite default in every environment (including production) so a leaked or idle\n // open transaction cannot pin a pool connection indefinitely and exhaust the pool.\n // Mirrors the long-standing dev value; override (incl. 0 to disable) via env.\n idleInTransactionTimeoutMs: Number.isFinite(idleInTxTimeoutEnv) ? idleInTxTimeoutEnv : 120_000,\n // Opt-in guards against runaway statements and lock waits. No timeout when unset.\n statementTimeoutMs: parsePositiveIntEnv(env.DB_STATEMENT_TIMEOUT_MS),\n lockTimeoutMs: parsePositiveIntEnv(env.DB_LOCK_TIMEOUT_MS),\n }\n}\n\nexport async function getOrm() {\n if (ormInstance) {\n return ormInstance\n }\n\n const entities = getOrmEntities()\n const clientUrl = process.env.DATABASE_URL\n if (!clientUrl) {\n throw new Error('DATABASE_URL is not set')\n }\n\n // Parse connection pool settings from environment\n const {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n } = resolvePoolConfig()\n const connectionOptions =\n idleSessionTimeoutMs && idleSessionTimeoutMs > 0\n ? `-c idle_session_timeout=${idleSessionTimeoutMs}`\n : undefined\n\n const sslConfig = getSslConfig()\n\n if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {\n logger.info('Pool config', {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n nodeEnv: process.env.NODE_ENV,\n })\n }\n\n ormInstance = await MikroORM.init<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>({\n driver: PostgreSqlDriver,\n clientUrl,\n entities,\n debug: false,\n // v7 no longer defaults to ReflectMetadataProvider. Entities in this repo use\n // `@mikro-orm/decorators/legacy`, which relies on TypeScript `emitDecoratorMetadata`\n // + reflect-metadata for type inference (nullability, column types). Without this,\n // inferred types are silently wrong at runtime.\n metadataProvider: ReflectMetadataProvider,\n // MikroORM v7 pool shape (min/max/idleTimeoutMillis). Knex-era `acquireTimeoutMillis` /\n // `destroyTimeoutMillis` were removed; acquire wait maps to pg `connectionTimeoutMillis`\n // below under `driverOptions`. Mirror `connectionTimeoutMillis` here too \u2014 older Mikro\n // versions read it from `pool`; v7 reads from `driverOptions` but accepting both\n // costs nothing and protects us from upstream config-merge regressions.\n pool: {\n min: poolMin,\n max: poolMax,\n idleTimeoutMillis: poolIdleTimeout,\n acquireTimeoutMillis: poolAcquireTimeout,\n } as any,\n // Driver options are merged into pg.PoolConfig (ClientConfig + pg-pool).\n driverOptions: {\n connectionTimeoutMillis: poolAcquireTimeout,\n idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,\n statement_timeout: statementTimeoutMs,\n lock_timeout: lockTimeoutMs,\n options: connectionOptions,\n ssl: sslConfig,\n onPoolCreated: (pool:
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import 'dotenv/config'\nimport 'reflect-metadata'\nimport { MikroORM } from '@mikro-orm/core'\nimport { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy'\nimport { PostgreSqlDriver, type EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport { getSslConfig } from './ssl'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'orm' })\n\nexport type AppMikroORM = MikroORM<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>\n\nlet ormInstance: AppMikroORM | null = null\n\n// Use globalThis so standalone apps survive duplicated shared package module instances.\nconst GLOBAL_ENTITIES_KEY = '__openMercatoOrmEntities__'\n\nfunction getRegisteredEntities(): any[] | null {\n return (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] as any[] | null ?? null\n}\n\nfunction setRegisteredEntities(entities: any[]): void {\n (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] = entities\n}\n\nexport function registerOrmEntities(entities: any[]) {\n if (getRegisteredEntities() !== null && process.env.NODE_ENV === 'development') {\n logger.debug('ORM entities re-registered (this may occur during HMR)')\n }\n setRegisteredEntities(entities)\n}\n\nexport function getOrmEntities(): any[] {\n const entities = getRegisteredEntities()\n if (!entities) {\n throw new Error('[Bootstrap] ORM entities not registered. Call registerOrmEntities() at bootstrap.')\n }\n return entities\n}\n\nexport type ResolvedPoolConfig = {\n poolMin: number\n poolMax: number\n poolIdleTimeout: number\n poolAcquireTimeout: number\n idleSessionTimeoutMs: number | undefined\n idleInTransactionTimeoutMs: number | undefined\n statementTimeoutMs: number | undefined\n lockTimeoutMs: number | undefined\n}\n\n// Parse an optional positive-millisecond env var. Returns undefined when unset,\n// non-numeric, or non-positive so callers treat \"no value\" as \"no timeout\".\nfunction parsePositiveIntEnv(raw: string | undefined): number | undefined {\n const parsed = parseInt(raw || '')\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined\n}\n\nexport function resolvePoolConfig(env: NodeJS.ProcessEnv = process.env): ResolvedPoolConfig {\n const idleSessionTimeoutEnv = parseInt(env.DB_IDLE_SESSION_TIMEOUT_MS || '')\n const idleInTxTimeoutEnv = parseInt(env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || '')\n return {\n poolMin: parseInt(env.DB_POOL_MIN || '2'),\n poolMax: parseInt(env.DB_POOL_MAX || '20'),\n poolIdleTimeout: parseInt(env.DB_POOL_IDLE_TIMEOUT || '3000'),\n poolAcquireTimeout: parseInt(env.DB_POOL_ACQUIRE_TIMEOUT || '6000'),\n idleSessionTimeoutMs: Number.isFinite(idleSessionTimeoutEnv)\n ? idleSessionTimeoutEnv\n : env.NODE_ENV === 'production'\n ? undefined\n : 600_000,\n // Finite default in every environment (including production) so a leaked or idle\n // open transaction cannot pin a pool connection indefinitely and exhaust the pool.\n // Mirrors the long-standing dev value; override (incl. 0 to disable) via env.\n idleInTransactionTimeoutMs: Number.isFinite(idleInTxTimeoutEnv) ? idleInTxTimeoutEnv : 120_000,\n // Opt-in guards against runaway statements and lock waits. No timeout when unset.\n statementTimeoutMs: parsePositiveIntEnv(env.DB_STATEMENT_TIMEOUT_MS),\n lockTimeoutMs: parsePositiveIntEnv(env.DB_LOCK_TIMEOUT_MS),\n }\n}\n\ntype PoolLike = {\n on(event: 'error', listener: (err: unknown) => void): unknown\n on(event: 'connect', listener: (client: { on(event: 'error', listener: (err: unknown) => void): unknown }) => void): unknown\n options?: Record<string, unknown>\n}\n\n// Postgres can terminate a connection at any moment (admin termination, network\n// drop, and \u2014 most relevantly for long-running daemons \u2014 the\n// `idle_in_transaction_session_timeout` configured above, FATAL 25P03). Where\n// node-postgres surfaces that depends on the client's state:\n// - IDLE (checked into the pool): pg-pool re-emits on the pool's 'error' event.\n// - CHECKED OUT (e.g. a connection pinned by an open transaction while the app\n// awaits non-DB work): pg-pool removes its idle listener, so the FATAL emits\n// on the Client itself.\n// Either way an unlistened 'error' event crashes the whole process (\"Scheduler\n// polling engine exited unexpectedly with exit code 1\"). Swallow both: the pool\n// discards the dead client, and any in-flight transaction still fails normally\n// on its next query/commit against the dead connection.\n// The per-client listener is deliberately attached once on 'connect' and never\n// removed: it is a last-resort sink whose only job is to guarantee the 'error'\n// event always has a listener, in every client state. It is not error handling\n// and must not be \"cleaned up\" \u2014 removing it reintroduces the process crash.\n// A reaped IDLE client therefore logs twice (once here, once via the pool-level\n// handler that pg-pool's own idle listener re-emits); the pool-level line is the\n// one that identifies the client as idle.\nexport function attachPoolErrorHandlers(pool: PoolLike): void {\n pool.on('error', (err: unknown) => {\n logger.warn('Idle pg pool client error (connection reaped/terminated)', { err })\n })\n pool.on('connect', (client) => {\n client.on('error', (err: unknown) => {\n logger.warn('pg client error (connection reaped/terminated)', { err })\n })\n })\n}\n\nexport async function getOrm() {\n if (ormInstance) {\n return ormInstance\n }\n\n const entities = getOrmEntities()\n const clientUrl = process.env.DATABASE_URL\n if (!clientUrl) {\n throw new Error('DATABASE_URL is not set')\n }\n\n // Parse connection pool settings from environment\n const {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n } = resolvePoolConfig()\n const connectionOptions =\n idleSessionTimeoutMs && idleSessionTimeoutMs > 0\n ? `-c idle_session_timeout=${idleSessionTimeoutMs}`\n : undefined\n\n const sslConfig = getSslConfig()\n\n if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {\n logger.info('Pool config', {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n nodeEnv: process.env.NODE_ENV,\n })\n }\n\n ormInstance = await MikroORM.init<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>({\n driver: PostgreSqlDriver,\n clientUrl,\n entities,\n debug: false,\n // v7 no longer defaults to ReflectMetadataProvider. Entities in this repo use\n // `@mikro-orm/decorators/legacy`, which relies on TypeScript `emitDecoratorMetadata`\n // + reflect-metadata for type inference (nullability, column types). Without this,\n // inferred types are silently wrong at runtime.\n metadataProvider: ReflectMetadataProvider,\n // MikroORM v7 pool shape (min/max/idleTimeoutMillis). Knex-era `acquireTimeoutMillis` /\n // `destroyTimeoutMillis` were removed; acquire wait maps to pg `connectionTimeoutMillis`\n // below under `driverOptions`. Mirror `connectionTimeoutMillis` here too \u2014 older Mikro\n // versions read it from `pool`; v7 reads from `driverOptions` but accepting both\n // costs nothing and protects us from upstream config-merge regressions.\n pool: {\n min: poolMin,\n max: poolMax,\n idleTimeoutMillis: poolIdleTimeout,\n acquireTimeoutMillis: poolAcquireTimeout,\n } as any,\n // Driver options are merged into pg.PoolConfig (ClientConfig + pg-pool).\n driverOptions: {\n connectionTimeoutMillis: poolAcquireTimeout,\n idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,\n statement_timeout: statementTimeoutMs,\n lock_timeout: lockTimeoutMs,\n options: connectionOptions,\n ssl: sslConfig,\n onPoolCreated: (pool: PoolLike) => {\n attachPoolErrorHandlers(pool)\n if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {\n logger.info('pg pool created with options', {\n max: pool.options?.max,\n min: pool.options?.min,\n idleTimeoutMillis: pool.options?.idleTimeoutMillis,\n connectionTimeoutMillis: pool.options?.connectionTimeoutMillis,\n })\n }\n },\n },\n })\n\n return ormInstance\n}\n\n\nasync function closeOrmIfLoaded(): Promise<void> {\n if (ormInstance) {\n await ormInstance.close(true)\n ormInstance = null\n }\n}\n\n// In dev mode, handle reloads cleanly without leaving dangling connections.\nif (process.env.NODE_ENV !== 'production') {\n void closeOrmIfLoaded()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.6874.1.982d6097d8';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8-develop.6874.1.982d6097d8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"@mikro-orm/core": "^7.1.8",
|
|
106
106
|
"@mikro-orm/decorators": "^7.1.8",
|
|
107
107
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
108
|
-
"@open-mercato/cache": "0.6.
|
|
108
|
+
"@open-mercato/cache": "0.6.8-develop.6874.1.982d6097d8",
|
|
109
109
|
"@types/sanitize-html": "^2.16.1",
|
|
110
110
|
"dotenv": "^17.4.2",
|
|
111
111
|
"pino": "^10.3.1",
|
|
@@ -130,5 +130,5 @@
|
|
|
130
130
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
131
131
|
"directory": "packages/shared"
|
|
132
132
|
},
|
|
133
|
-
"stableVersion": "0.6.
|
|
133
|
+
"stableVersion": "0.6.7"
|
|
134
134
|
}
|
|
@@ -24,6 +24,31 @@ describe('ORM entity registry', () => {
|
|
|
24
24
|
})
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
+
describe('attachPoolErrorHandlers', () => {
|
|
28
|
+
it('swallows errors from idle pooled clients (pool-level emit)', async () => {
|
|
29
|
+
const { EventEmitter } = await import('node:events')
|
|
30
|
+
const { attachPoolErrorHandlers } = await import('../mikro')
|
|
31
|
+
const pool = new EventEmitter()
|
|
32
|
+
|
|
33
|
+
attachPoolErrorHandlers(pool as any)
|
|
34
|
+
|
|
35
|
+
expect(() => pool.emit('error', new Error('terminating connection due to idle-in-transaction timeout'))).not.toThrow()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('swallows errors from checked-out clients (client-level emit)', async () => {
|
|
39
|
+
const { EventEmitter } = await import('node:events')
|
|
40
|
+
const { attachPoolErrorHandlers } = await import('../mikro')
|
|
41
|
+
const pool = new EventEmitter()
|
|
42
|
+
const client = new EventEmitter()
|
|
43
|
+
|
|
44
|
+
attachPoolErrorHandlers(pool as any)
|
|
45
|
+
pool.emit('connect', client)
|
|
46
|
+
|
|
47
|
+
expect(client.listenerCount('error')).toBe(1)
|
|
48
|
+
expect(() => client.emit('error', new Error('terminating connection due to idle-in-transaction timeout'))).not.toThrow()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
27
52
|
describe('resolvePoolConfig', () => {
|
|
28
53
|
const baseEnv = (extra: Record<string, string | undefined> = {}): NodeJS.ProcessEnv =>
|
|
29
54
|
({ ...extra }) as NodeJS.ProcessEnv
|
package/src/lib/db/mikro.ts
CHANGED
|
@@ -79,6 +79,42 @@ export function resolvePoolConfig(env: NodeJS.ProcessEnv = process.env): Resolve
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
type PoolLike = {
|
|
83
|
+
on(event: 'error', listener: (err: unknown) => void): unknown
|
|
84
|
+
on(event: 'connect', listener: (client: { on(event: 'error', listener: (err: unknown) => void): unknown }) => void): unknown
|
|
85
|
+
options?: Record<string, unknown>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Postgres can terminate a connection at any moment (admin termination, network
|
|
89
|
+
// drop, and — most relevantly for long-running daemons — the
|
|
90
|
+
// `idle_in_transaction_session_timeout` configured above, FATAL 25P03). Where
|
|
91
|
+
// node-postgres surfaces that depends on the client's state:
|
|
92
|
+
// - IDLE (checked into the pool): pg-pool re-emits on the pool's 'error' event.
|
|
93
|
+
// - CHECKED OUT (e.g. a connection pinned by an open transaction while the app
|
|
94
|
+
// awaits non-DB work): pg-pool removes its idle listener, so the FATAL emits
|
|
95
|
+
// on the Client itself.
|
|
96
|
+
// Either way an unlistened 'error' event crashes the whole process ("Scheduler
|
|
97
|
+
// polling engine exited unexpectedly with exit code 1"). Swallow both: the pool
|
|
98
|
+
// discards the dead client, and any in-flight transaction still fails normally
|
|
99
|
+
// on its next query/commit against the dead connection.
|
|
100
|
+
// The per-client listener is deliberately attached once on 'connect' and never
|
|
101
|
+
// removed: it is a last-resort sink whose only job is to guarantee the 'error'
|
|
102
|
+
// event always has a listener, in every client state. It is not error handling
|
|
103
|
+
// and must not be "cleaned up" — removing it reintroduces the process crash.
|
|
104
|
+
// A reaped IDLE client therefore logs twice (once here, once via the pool-level
|
|
105
|
+
// handler that pg-pool's own idle listener re-emits); the pool-level line is the
|
|
106
|
+
// one that identifies the client as idle.
|
|
107
|
+
export function attachPoolErrorHandlers(pool: PoolLike): void {
|
|
108
|
+
pool.on('error', (err: unknown) => {
|
|
109
|
+
logger.warn('Idle pg pool client error (connection reaped/terminated)', { err })
|
|
110
|
+
})
|
|
111
|
+
pool.on('connect', (client) => {
|
|
112
|
+
client.on('error', (err: unknown) => {
|
|
113
|
+
logger.warn('pg client error (connection reaped/terminated)', { err })
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
82
118
|
export async function getOrm() {
|
|
83
119
|
if (ormInstance) {
|
|
84
120
|
return ormInstance
|
|
@@ -151,19 +187,8 @@ export async function getOrm() {
|
|
|
151
187
|
lock_timeout: lockTimeoutMs,
|
|
152
188
|
options: connectionOptions,
|
|
153
189
|
ssl: sslConfig,
|
|
154
|
-
onPoolCreated: (pool:
|
|
155
|
-
|
|
156
|
-
// 'error' event. Postgres terminates idle sessions on its own (admin
|
|
157
|
-
// termination, network drop, and — most relevantly for long-running
|
|
158
|
-
// daemons like the scheduler — `idle_in_transaction_session_timeout`,
|
|
159
|
-
// which defaults to 120s above). Without a listener here, such a
|
|
160
|
-
// termination surfaces as an unhandled 'error' event and crashes the
|
|
161
|
-
// whole process (e.g. "Scheduler polling engine exited unexpectedly
|
|
162
|
-
// with exit code 1"). Swallow it: the pool discards the dead client and
|
|
163
|
-
// opens a fresh connection on the next acquire.
|
|
164
|
-
pool.on('error', (err: unknown) => {
|
|
165
|
-
logger.warn('Idle pg pool client error (connection reaped/terminated)', { err })
|
|
166
|
-
})
|
|
190
|
+
onPoolCreated: (pool: PoolLike) => {
|
|
191
|
+
attachPoolErrorHandlers(pool)
|
|
167
192
|
if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {
|
|
168
193
|
logger.info('pg pool created with options', {
|
|
169
194
|
max: pool.options?.max,
|