masterrecord 0.3.65 → 0.3.66

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.
@@ -6,6 +6,12 @@ class schema{
6
6
  this._dbEnsured = false;
7
7
  }
8
8
 
9
+ _poolKey(type, cfg) {
10
+ const host = cfg.host || 'localhost';
11
+ const port = cfg.port || (type === 'mysql' ? 3306 : 5432);
12
+ return `${type}:${cfg.user}@${host}:${port}/${cfg.database}`;
13
+ }
14
+
9
15
  /**
10
16
  * Wait for async database initialization (MySQL/PostgreSQL) to complete.
11
17
  * The context constructor fires off an async pool init that may not have
@@ -84,6 +90,26 @@ class schema{
84
90
  if(!config){ throw new Error('No MySQL config available for retry'); }
85
91
  const MySQLEngine = require('masterrecord/mySQLEngine');
86
92
  const MySQLAsyncClient = require('masterrecord/mySQLConnect');
93
+
94
+ // Check global pool cache first -- another context may have already retried
95
+ const _pools = global.__MR_POOLS__;
96
+ const key = _pools ? this._poolKey('mysql', config) : null;
97
+
98
+ if (_pools && key && _pools.has(key)) {
99
+ const cached = _pools.get(key);
100
+ cached.refCount++;
101
+ if (cached.promise) {
102
+ const result = await cached.promise;
103
+ this.context._SQLEngine = result.engine;
104
+ this.context.db = result.client;
105
+ } else {
106
+ this.context._SQLEngine = cached.engine;
107
+ this.context.db = cached.client;
108
+ }
109
+ console.log('[MySQL] Reusing existing pool after database creation');
110
+ return;
111
+ }
112
+
87
113
  console.log('[MySQL] Retrying connection after database creation...');
88
114
  const client = new MySQLAsyncClient(config);
89
115
  await client.connect();
@@ -92,6 +118,11 @@ class schema{
92
118
  this.context._SQLEngine.setDB(pool);
93
119
  this.context._SQLEngine.__name = 'mysql2';
94
120
  this.context.db = client;
121
+
122
+ // Register in global pool cache so other contexts can reuse
123
+ if (_pools && key) {
124
+ _pools.set(key, { client, engine: this.context._SQLEngine, refCount: 1, dbType: 'mysql' });
125
+ }
95
126
  console.log('[MySQL] Connection pool ready');
96
127
  }
97
128
 
@@ -140,12 +171,40 @@ class schema{
140
171
  const config = this.context._dbConfig;
141
172
  if(!config){ throw new Error('No PostgreSQL config available for retry'); }
142
173
  const PostgresClient = require('masterrecord/postgresSyncConnect');
174
+
175
+ // Check global pool cache first -- another context may have already retried
176
+ const _pools = global.__MR_POOLS__;
177
+ const key = _pools ? this._poolKey('postgres', config) : null;
178
+
179
+ if (_pools && key && _pools.has(key)) {
180
+ const cached = _pools.get(key);
181
+ cached.refCount++;
182
+ if (cached.promise) {
183
+ const result = await cached.promise;
184
+ this.context._SQLEngine = result.engine;
185
+ this.context._SQLEngine.__name = 'pg';
186
+ this.context.db = result.pool;
187
+ } else {
188
+ this.context._SQLEngine = cached.engine;
189
+ this.context._SQLEngine.__name = 'pg';
190
+ this.context.db = cached.pool;
191
+ }
192
+ console.log('[PostgreSQL] Reusing existing pool after database creation');
193
+ return;
194
+ }
195
+
143
196
  console.log('[PostgreSQL] Retrying connection after database creation...');
144
197
  const connection = new PostgresClient();
145
198
  await connection.connect(config);
146
199
  this.context._SQLEngine = connection.getEngine();
147
200
  this.context._SQLEngine.__name = 'pg';
148
- this.context.db = connection.getPool();
201
+ const pool = connection.getPool();
202
+ this.context.db = pool;
203
+
204
+ // Register in global pool cache so other contexts can reuse
205
+ if (_pools && key) {
206
+ _pools.set(key, { pool, engine: this.context._SQLEngine, client: connection, refCount: 1, dbType: 'postgres' });
207
+ }
149
208
  console.log('[PostgreSQL] Connection pool ready');
150
209
  }
151
210
 
package/context.js CHANGED
@@ -376,6 +376,15 @@ class context {
376
376
  if (_pools.has(key)) {
377
377
  const cached = _pools.get(key);
378
378
  cached.refCount++;
379
+ if (cached.promise) {
380
+ // Another caller is initializing -- await the same promise
381
+ const result = await cached.promise;
382
+ this._SQLEngine = result.engine;
383
+ this.isMySQL = true;
384
+ console.log(`[MySQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
385
+ return result.client;
386
+ }
387
+ // Already resolved
379
388
  this._SQLEngine = cached.engine;
380
389
  this.isMySQL = true;
381
390
  console.log(`[MySQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
@@ -383,18 +392,38 @@ class context {
383
392
  }
384
393
 
385
394
  console.log('[MySQL] Initializing async connection pool...');
386
- const client = new MySQLAsyncClient(env);
387
- await client.connect();
388
395
 
389
- const pool = client.getPool();
390
- this._SQLEngine = new MySQLEngine();
391
- this._SQLEngine.setDB(pool);
392
- this._SQLEngine.__name = sqlName;
393
- this.isMySQL = true;
396
+ // Store promise IMMEDIATELY to prevent race condition with concurrent callers
397
+ const initPromise = (async () => {
398
+ const client = new MySQLAsyncClient(env);
399
+ await client.connect();
400
+ const pool = client.getPool();
401
+ const engine = new MySQLEngine();
402
+ engine.setDB(pool);
403
+ engine.__name = sqlName;
404
+ return { client, engine };
405
+ })();
406
+
407
+ _pools.set(key, { promise: initPromise, refCount: 1, dbType: 'mysql' });
394
408
 
395
- _pools.set(key, { client, engine: this._SQLEngine, refCount: 1, dbType: 'mysql' });
409
+ let result;
410
+ try {
411
+ result = await initPromise;
412
+ } catch (err) {
413
+ // Remove failed entry so future callers can retry
414
+ _pools.delete(key);
415
+ throw err;
416
+ }
417
+
418
+ // Replace pending entry with resolved entry, preserving refCount from concurrent joiners
419
+ const pending = _pools.get(key);
420
+ const currentRefCount = pending ? pending.refCount : 1;
421
+ _pools.set(key, { client: result.client, engine: result.engine, refCount: currentRefCount, dbType: 'mysql' });
422
+
423
+ this._SQLEngine = result.engine;
424
+ this.isMySQL = true;
396
425
  console.log('[MySQL] Connection pool ready');
397
- return client;
426
+ return result.client;
398
427
  } catch (error) {
399
428
  // Preserve original error if it's already a ContextError
400
429
  if (error instanceof ContextError) {
@@ -459,20 +488,49 @@ class context {
459
488
  if (_pools.has(key)) {
460
489
  const cached = _pools.get(key);
461
490
  cached.refCount++;
491
+ if (cached.promise) {
492
+ // Another caller is initializing -- await the same promise
493
+ const result = await cached.promise;
494
+ this._SQLEngine = result.engine;
495
+ this._SQLEngine.__name = sqlName;
496
+ console.log(`[PostgreSQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
497
+ return result.pool;
498
+ }
499
+ // Already resolved
462
500
  this._SQLEngine = cached.engine;
463
501
  this._SQLEngine.__name = sqlName;
464
502
  console.log(`[PostgreSQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
465
503
  return cached.pool;
466
504
  }
467
505
 
468
- const connection = new PostgresClient();
469
- await connection.connect(env);
470
- this._SQLEngine = connection.getEngine();
471
- this._SQLEngine.__name = sqlName;
506
+ // Store promise IMMEDIATELY to prevent race condition with concurrent callers
507
+ const initPromise = (async () => {
508
+ const connection = new PostgresClient();
509
+ await connection.connect(env);
510
+ const engine = connection.getEngine();
511
+ engine.__name = sqlName;
512
+ const pool = connection.getPool();
513
+ return { pool, engine, client: connection };
514
+ })();
515
+
516
+ _pools.set(key, { promise: initPromise, refCount: 1, dbType: 'postgres' });
472
517
 
473
- const pool = connection.getPool();
474
- _pools.set(key, { pool, engine: this._SQLEngine, client: connection, refCount: 1, dbType: 'postgres' });
475
- return pool;
518
+ let result;
519
+ try {
520
+ result = await initPromise;
521
+ } catch (err) {
522
+ // Remove failed entry so future callers can retry
523
+ _pools.delete(key);
524
+ throw err;
525
+ }
526
+
527
+ // Replace pending entry with resolved entry, preserving refCount from concurrent joiners
528
+ const pending = _pools.get(key);
529
+ const currentRefCount = pending ? pending.refCount : 1;
530
+ _pools.set(key, { pool: result.pool, engine: result.engine, client: result.client, refCount: currentRefCount, dbType: 'postgres' });
531
+
532
+ this._SQLEngine = result.engine;
533
+ return result.pool;
476
534
  } catch (error) {
477
535
  // Preserve original error if it's already a ContextError
478
536
  if (error instanceof ContextError) {
@@ -2058,6 +2116,8 @@ class context {
2058
2116
  async close() {
2059
2117
  // Find this instance's pool in the registry and decrement
2060
2118
  for (const [key, entry] of _pools) {
2119
+ // Skip pending entries -- they have no engine yet
2120
+ if (entry.promise) continue;
2061
2121
  if (entry.engine === this._SQLEngine) {
2062
2122
  entry.refCount--;
2063
2123
  if (entry.refCount <= 0) {
@@ -2096,7 +2156,17 @@ class context {
2096
2156
  static async closeAll() {
2097
2157
  for (const [key, entry] of _pools) {
2098
2158
  try {
2099
- if (entry.engine && typeof entry.engine.close === 'function') {
2159
+ if (entry.promise) {
2160
+ // Wait for pending init to complete, then close it
2161
+ try {
2162
+ const result = await entry.promise;
2163
+ if (result.engine && typeof result.engine.close === 'function') {
2164
+ await result.engine.close();
2165
+ }
2166
+ } catch (_initErr) {
2167
+ // Init failed -- nothing to close
2168
+ }
2169
+ } else if (entry.engine && typeof entry.engine.close === 'function') {
2100
2170
  await entry.engine.close();
2101
2171
  }
2102
2172
  } catch (err) {
@@ -2118,7 +2188,8 @@ class context {
2118
2188
  */
2119
2189
  static getPoolStats() {
2120
2190
  return Array.from(_pools.entries()).map(([key, entry]) => ({
2121
- key, dbType: entry.dbType, refCount: entry.refCount
2191
+ key, dbType: entry.dbType, refCount: entry.refCount,
2192
+ status: entry.promise ? 'pending' : 'ready'
2122
2193
  }));
2123
2194
  }
2124
2195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.65",
3
+ "version": "0.3.66",
4
4
  "description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
5
5
  "main": "MasterRecord.js",
6
6
  "bin": {