@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/config/environment.dev.js +3 -3
- package/lib/chemicals.js +1087 -1061
- package/lib/connection.js +87 -1
- package/lib/db/schema.js +27 -27
- package/package.json +1 -1
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
|
-
|
|
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