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.
- package/.claude/settings.local.json +3 -1
- package/Migrations/schema.js +52 -1
- package/context.js +93 -19
- package/package.json +1 -1
|
@@ -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": []
|
package/Migrations/schema.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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.
|
|
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.
|
|
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": {
|