masterrecord 0.3.65 → 0.3.67

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.
@@ -64,7 +64,9 @@
64
64
  "Bash(masterrecord add-migration:*)",
65
65
  "Bash(masterrecord:*)",
66
66
  "Bash(git -C /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training log --oneline --all -- *MASTERRECORD_ISSUE*)",
67
- "Bash(npm ls -g masterrecord 2>&1 | head -5)"
67
+ "Bash(npm ls -g masterrecord 2>&1 | head -5)",
68
+ "Bash(gh pr:*)",
69
+ "Read(//tmp/**)"
68
70
  ],
69
71
  "deny": [],
70
72
  "ask": []
@@ -1,4 +1,6 @@
1
1
  // version 0.0.6
2
+ const { _poolKey } = require('masterrecord/context');
3
+
2
4
  class schema{
3
5
 
4
6
  constructor(context){
@@ -84,6 +86,26 @@ class schema{
84
86
  if(!config){ throw new Error('No MySQL config available for retry'); }
85
87
  const MySQLEngine = require('masterrecord/mySQLEngine');
86
88
  const MySQLAsyncClient = require('masterrecord/mySQLConnect');
89
+
90
+ // Check global pool cache first -- another context may have already retried
91
+ const _pools = global.__MR_POOLS__;
92
+ const key = _poolKey('mysql', config);
93
+
94
+ if (_pools.has(key)) {
95
+ const cached = _pools.get(key);
96
+ cached.refCount++;
97
+ if (cached.promise) {
98
+ const result = await cached.promise;
99
+ this.context._SQLEngine = result.engine;
100
+ this.context.db = result.client;
101
+ } else {
102
+ this.context._SQLEngine = cached.engine;
103
+ this.context.db = cached.client;
104
+ }
105
+ console.log('[MySQL] Reusing existing pool after database creation');
106
+ return;
107
+ }
108
+
87
109
  console.log('[MySQL] Retrying connection after database creation...');
88
110
  const client = new MySQLAsyncClient(config);
89
111
  await client.connect();
@@ -92,6 +114,9 @@ class schema{
92
114
  this.context._SQLEngine.setDB(pool);
93
115
  this.context._SQLEngine.__name = 'mysql2';
94
116
  this.context.db = client;
117
+
118
+ // Register in global pool cache so other contexts can reuse
119
+ _pools.set(key, { client, engine: this.context._SQLEngine, refCount: 1, dbType: 'mysql' });
95
120
  console.log('[MySQL] Connection pool ready');
96
121
  }
97
122
 
@@ -140,12 +165,38 @@ class schema{
140
165
  const config = this.context._dbConfig;
141
166
  if(!config){ throw new Error('No PostgreSQL config available for retry'); }
142
167
  const PostgresClient = require('masterrecord/postgresSyncConnect');
168
+
169
+ // Check global pool cache first -- another context may have already retried
170
+ const _pools = global.__MR_POOLS__;
171
+ const key = _poolKey('postgres', config);
172
+
173
+ if (_pools.has(key)) {
174
+ const cached = _pools.get(key);
175
+ cached.refCount++;
176
+ if (cached.promise) {
177
+ const result = await cached.promise;
178
+ this.context._SQLEngine = result.engine;
179
+ this.context._SQLEngine.__name = 'pg';
180
+ this.context.db = result.pool;
181
+ } else {
182
+ this.context._SQLEngine = cached.engine;
183
+ this.context._SQLEngine.__name = 'pg';
184
+ this.context.db = cached.pool;
185
+ }
186
+ console.log('[PostgreSQL] Reusing existing pool after database creation');
187
+ return;
188
+ }
189
+
143
190
  console.log('[PostgreSQL] Retrying connection after database creation...');
144
191
  const connection = new PostgresClient();
145
192
  await connection.connect(config);
146
193
  this.context._SQLEngine = connection.getEngine();
147
194
  this.context._SQLEngine.__name = 'pg';
148
- this.context.db = connection.getPool();
195
+ const pool = connection.getPool();
196
+ this.context.db = pool;
197
+
198
+ // Register in global pool cache so other contexts can reuse
199
+ _pools.set(key, { pool, engine: this.context._SQLEngine, client: connection, refCount: 1, dbType: 'postgres' });
149
200
  console.log('[PostgreSQL] Connection pool ready');
150
201
  }
151
202
 
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' });
408
+
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' });
394
422
 
395
- _pools.set(key, { client, engine: this._SQLEngine, refCount: 1, dbType: 'mysql' });
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
+ })();
472
515
 
473
- const pool = connection.getPool();
474
- _pools.set(key, { pool, engine: this._SQLEngine, client: connection, refCount: 1, dbType: 'postgres' });
475
- return pool;
516
+ _pools.set(key, { promise: initPromise, refCount: 1, dbType: 'postgres' });
517
+
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
 
@@ -2331,4 +2402,7 @@ module.exports = context;
2331
2402
  module.exports.ContextError = ContextError;
2332
2403
  module.exports.ConfigurationError = ConfigurationError;
2333
2404
  module.exports.DatabaseConnectionError = DatabaseConnectionError;
2334
- module.exports.EntityValidationError = EntityValidationError;
2405
+ module.exports.EntityValidationError = EntityValidationError;
2406
+
2407
+ // Export pool key generator for use by schema.js (single source of truth)
2408
+ module.exports._poolKey = _poolKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.65",
3
+ "version": "0.3.67",
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": {