@toxplanet/pegasus-sdk 1.1.17 → 1.1.18

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/lib/connection.js CHANGED
@@ -24,6 +24,7 @@ class PegasusConnection {
24
24
  this.secretsClient = null;
25
25
  this.cachedSecret = null;
26
26
  this.isConnected = false;
27
+ this.lastActivityAt = null;
27
28
  }
28
29
 
29
30
  async getSecret() {
@@ -63,7 +64,8 @@ class PegasusConnection {
63
64
  min: this.config.postgres.minConnections,
64
65
  idleTimeoutMillis: this.config.postgres.idleTimeoutMillis,
65
66
  connectionTimeoutMillis: this.config.postgres.connectionTimeoutMillis,
66
- allowExitOnIdle: true,
67
+ keepAlive: true,
68
+ keepAliveInitialDelayMillis: 10000,
67
69
  ssl: this.config.postgres.ssl,
68
70
  statement_timeout: this.config.postgres.statementTimeout,
69
71
  query_timeout: this.config.postgres.queryTimeout
@@ -76,6 +78,7 @@ class PegasusConnection {
76
78
  });
77
79
 
78
80
  this.pgPool.on('connect', () => {
81
+ this.lastActivityAt = Date.now();
79
82
  logInfo('pegasus-sdk', 'PostgreSQL client connected');
80
83
  });
81
84
 
@@ -83,6 +86,16 @@ class PegasusConnection {
83
86
  logInfo('pegasus-sdk', 'PostgreSQL client removed from pool');
84
87
  });
85
88
 
89
+ // Eagerly verify the connection is actually reachable before declaring
90
+ // isConnected = true. Without this, new Pool() never opens a socket and
91
+ // isConnected would be a lie — the first real query would pay the full
92
+ // TCP+SSL+auth cost while racing the connectionTimeoutMillis.
93
+ const verifyClient = await this.pgPool.connect();
94
+ await verifyClient.query('SELECT 1');
95
+ verifyClient.release();
96
+ this.lastActivityAt = Date.now();
97
+ logInfo('pegasus-sdk', 'PostgreSQL connection verified and ready');
98
+
86
99
  if (this.openSearchEndpoint) {
87
100
  this.osClient = new Client({
88
101
  ...AwsSigv4Signer({
@@ -100,6 +113,79 @@ class PegasusConnection {
100
113
  this.isConnected = true;
101
114
  }
102
115
 
116
+ async reconnect() {
117
+ logInfo('pegasus-sdk', 'Reconnecting PostgreSQL pool (stale connection detected)');
118
+ if (this.pgPool) {
119
+ try {
120
+ await this.pgPool.end();
121
+ } catch (err) {
122
+ logError('pegasus-sdk', 'PegasusConnection', 'reconnect.end', err);
123
+ }
124
+ this.pgPool = null;
125
+ }
126
+ this.isConnected = false;
127
+ this.lastActivityAt = null;
128
+ await this.connect();
129
+ }
130
+
131
+ /**
132
+ * Call this after any successful database operation so ensureConnected()
133
+ * knows the connection was recently healthy and can skip the liveness check.
134
+ */
135
+ recordActivity() {
136
+ this.lastActivityAt = Date.now();
137
+ }
138
+
139
+ /**
140
+ * Proactively verify the connection is alive before executing a batch of
141
+ * queries. If the pool has been idle long enough that connections may have
142
+ * gone stale, a lightweight SELECT 1 is issued first. If that fails, the
143
+ * pool is rebuilt before the caller's real query fires — avoiding the full
144
+ * connectionTimeoutMillis wait on the real query.
145
+ *
146
+ * @returns {Promise<boolean>} true if a reconnect happened (caller should
147
+ * reset any cached Drizzle db instance), false if connection is healthy.
148
+ */
149
+ async ensureConnected() {
150
+ if (!this.pgPool || !this.isConnected) {
151
+ await this.connect();
152
+ return true;
153
+ }
154
+
155
+ // If we used this connection recently, trust it's still alive.
156
+ const FRESH_THRESHOLD_MS = 60_000;
157
+ const idleSince = Date.now() - (this.lastActivityAt || 0);
158
+ if (idleSince < FRESH_THRESHOLD_MS) {
159
+ return false;
160
+ }
161
+
162
+ logInfo('pegasus-sdk', `[ensureConnected] Pool inactive for ${Math.round(idleSince / 1000)}s — running liveness check before query`);
163
+
164
+ let client;
165
+ try {
166
+ // Use a short race timeout so a dead connection fails fast rather than
167
+ // waiting the full connectionTimeoutMillis before we know to reconnect.
168
+ client = await Promise.race([
169
+ this.pgPool.connect(),
170
+ new Promise((_, reject) =>
171
+ setTimeout(() => reject(new Error('liveness check timeout')), 3000)
172
+ )
173
+ ]);
174
+ await client.query('SELECT 1');
175
+ client.release();
176
+ this.lastActivityAt = Date.now();
177
+ logInfo('pegasus-sdk', '[ensureConnected] Liveness check passed');
178
+ return false;
179
+ } catch (err) {
180
+ if (client) {
181
+ try { client.release(true); } catch (_) {}
182
+ }
183
+ logInfo('pegasus-sdk', `[ensureConnected] Liveness check failed (${err.message}) — reconnecting pool proactively`);
184
+ await this.reconnect();
185
+ return true;
186
+ }
187
+ }
188
+
103
189
  async disconnect() {
104
190
  if (!this.isConnected) {
105
191
  return;
package/lib/db/schema.js CHANGED
@@ -1,27 +1,27 @@
1
- const { pgTable, uuid, text, jsonb, timestamp, index } = require('drizzle-orm/pg-core');
2
- const { sql } = require('drizzle-orm');
3
-
4
- const chemicals = pgTable('chemicals', {
5
- chemicalId: uuid('chemical_id').defaultRandom().primaryKey(),
6
- sourceId: text('source_id').notNull().unique(),
7
- chemicalName: text('chemical_name').notNull(),
8
- chemicalMeta: jsonb('chemical_meta'),
9
- chemicalIdentifiers: jsonb('chemical_identifiers'),
10
- chemicalSynonyms: text('chemical_synonyms').array(),
11
- chemicalCategories: text('chemical_categories').array(),
12
- createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
13
- updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
14
- importedAt: timestamp('imported_at', { withTimezone: true }).defaultNow()
15
- }, (table) => {
16
- return {
17
- nameIdx: index('idx_chemicals_name').on(table.chemicalName),
18
- createdAtIdx: index('idx_chemicals_created_at').on(table.createdAt),
19
- updatedAtIdx: index('idx_chemicals_updated_at').on(table.updatedAt),
20
- identifiersGinIdx: index('idx_chemicals_identifiers_gin').on(table.chemicalIdentifiers),
21
- synonymsGinIdx: index('idx_chemicals_synonyms_gin').on(table.chemicalSynonyms)
22
- };
23
- });
24
-
25
- module.exports = {
26
- chemicals
27
- };
1
+ const { pgTable, uuid, text, jsonb, timestamp, index } = require('drizzle-orm/pg-core');
2
+ const { sql } = require('drizzle-orm');
3
+
4
+ const chemicals = pgTable('chemicals', {
5
+ chemicalId: uuid('chemical_id').defaultRandom().primaryKey(),
6
+ sourceId: text('source_id').notNull().unique(),
7
+ chemicalName: text('chemical_name').notNull(),
8
+ chemicalMeta: jsonb('chemical_meta'),
9
+ chemicalIdentifiers: jsonb('chemical_identifiers'),
10
+ chemicalSynonyms: text('chemical_synonyms').array(),
11
+ chemicalCategories: text('chemical_categories').array(),
12
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
13
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
14
+ importedAt: timestamp('imported_at', { withTimezone: true }).defaultNow()
15
+ }, (table) => {
16
+ return {
17
+ nameIdx: index('idx_chemicals_name').on(table.chemicalName),
18
+ createdAtIdx: index('idx_chemicals_created_at').on(table.createdAt),
19
+ updatedAtIdx: index('idx_chemicals_updated_at').on(table.updatedAt),
20
+ identifiersGinIdx: index('idx_chemicals_identifiers_gin').on(table.chemicalIdentifiers),
21
+ synonymsGinIdx: index('idx_chemicals_synonyms_gin').on(table.chemicalSynonyms)
22
+ };
23
+ });
24
+
25
+ module.exports = {
26
+ chemicals
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toxplanet/pegasus-sdk",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
4
4
  "description": "SDK for migrating chemical data to Pegasus PostgreSQL + OpenSearch architecture with Elasticsearch client compatibility",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",