relq 1.0.65 → 1.0.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.
@@ -83,6 +83,8 @@ class Relq {
83
83
  initialized = false;
84
84
  initPromise;
85
85
  isClosed = false;
86
+ clientConnected = false;
87
+ connectPromise;
86
88
  environment;
87
89
  poolErrorHandler;
88
90
  poolConnectHandler;
@@ -487,8 +489,14 @@ class Relq {
487
489
  connectionString: this.config.connectionString,
488
490
  ssl
489
491
  });
490
- this.client.on('end', () => this.emitter.emit('end'));
491
- this.client.on('error', (err) => this.emitter.emit('error', err));
492
+ this.client.on('end', () => {
493
+ this.clientConnected = false;
494
+ this.emitter.emit('end');
495
+ });
496
+ this.client.on('error', (err) => {
497
+ this.clientConnected = false;
498
+ this.emitter.emit('error', err);
499
+ });
492
500
  this.client.on('notice', (msg) => this.emitter.emit('notice', msg));
493
501
  this.client.on('notification', (msg) => this.emitter.emit('notification', msg));
494
502
  }
@@ -527,9 +535,85 @@ class Relq {
527
535
  await this.initialize();
528
536
  if (!this.usePooling && this.client) {
529
537
  await this.client.connect();
538
+ this.clientConnected = true;
530
539
  this.emitter.emit('connect', this.client);
531
540
  }
532
541
  }
542
+ async ensureConnection() {
543
+ if (this.usePooling || !this.client)
544
+ return;
545
+ if (this.clientConnected)
546
+ return;
547
+ if (this.connectPromise) {
548
+ return this.connectPromise;
549
+ }
550
+ this.connectPromise = (async () => {
551
+ try {
552
+ if (this.client && !this.clientConnected) {
553
+ try {
554
+ await this.client.connect();
555
+ this.clientConnected = true;
556
+ this.emitter.emit('connect', this.client);
557
+ }
558
+ catch (err) {
559
+ if (err.message?.includes('already been connected') ||
560
+ err.code === 'ECONNRESET' ||
561
+ err.code === 'ETIMEDOUT' ||
562
+ err.code === 'EPIPE' ||
563
+ err.message?.includes('Connection terminated')) {
564
+ this.client.removeAllListeners();
565
+ try {
566
+ await this.client.end();
567
+ }
568
+ catch { }
569
+ await this.recreateClient();
570
+ }
571
+ else {
572
+ throw err;
573
+ }
574
+ }
575
+ }
576
+ }
577
+ finally {
578
+ this.connectPromise = undefined;
579
+ }
580
+ })();
581
+ return this.connectPromise;
582
+ }
583
+ async recreateClient() {
584
+ const { Client: PgClientClass } = await loadPg();
585
+ let resolvedPassword = this.config.password;
586
+ const isAws = (0, config_types_1.isAwsDsqlConfig)(this.config);
587
+ if (isAws) {
588
+ resolvedPassword = await (0, aws_dsql_1.getAwsDsqlToken)(this.config.aws);
589
+ }
590
+ const host = isAws ? this.config.aws.hostname : (this.config.host || 'localhost');
591
+ const port = this.config.aws?.port ?? this.config.port ?? 5432;
592
+ const user = this.config.aws?.user ?? this.config.user ?? (isAws ? 'admin' : undefined);
593
+ const ssl = isAws ? (this.config.aws.ssl ?? true) : this.config.ssl;
594
+ this.client = new PgClientClass({
595
+ host,
596
+ port,
597
+ database: this.config.database,
598
+ user,
599
+ password: resolvedPassword,
600
+ connectionString: this.config.connectionString,
601
+ ssl
602
+ });
603
+ this.client.on('end', () => {
604
+ this.clientConnected = false;
605
+ this.emitter.emit('end');
606
+ });
607
+ this.client.on('error', (err) => {
608
+ this.clientConnected = false;
609
+ this.emitter.emit('error', err);
610
+ });
611
+ this.client.on('notice', (msg) => this.emitter.emit('notice', msg));
612
+ this.client.on('notification', (msg) => this.emitter.emit('notification', msg));
613
+ await this.client.connect();
614
+ this.clientConnected = true;
615
+ this.emitter.emit('connect', this.client);
616
+ }
533
617
  async subscribe(channel, callback) {
534
618
  if (!this.listener) {
535
619
  this.listener = new listener_connection_1.ListenerConnection({
@@ -576,6 +660,7 @@ class Relq {
576
660
  else if (this.client) {
577
661
  this.client.removeAllListeners();
578
662
  await this.client.end();
663
+ this.clientConnected = false;
579
664
  }
580
665
  this.isClosed = true;
581
666
  activeRelqInstances.delete(this);
@@ -601,26 +686,52 @@ class Relq {
601
686
  return this;
602
687
  }
603
688
  async _executeQuery(sql) {
604
- try {
605
- if (!sql || typeof sql !== 'string' || sql.trim() === '') {
606
- throw new relq_errors_1.RelqConfigError(`Invalid SQL query: ${sql === null ? 'null' : sql === undefined ? 'undefined' : 'empty string'}`);
607
- }
608
- await this.initialize();
609
- const startTime = performance.now();
610
- let result;
689
+ if (!sql || typeof sql !== 'string' || sql.trim() === '') {
690
+ throw new relq_errors_1.RelqConfigError(`Invalid SQL query: ${sql === null ? 'null' : sql === undefined ? 'undefined' : 'empty string'}`);
691
+ }
692
+ await this.initialize();
693
+ const executeQuery = async () => {
611
694
  if (this.pool) {
612
- result = await this.pool.query(sql);
695
+ return await this.pool.query(sql);
613
696
  }
614
697
  else if (this.client) {
615
- result = await this.client.query(sql);
698
+ await this.ensureConnection();
699
+ return await this.client.query(sql);
616
700
  }
617
701
  else {
618
702
  throw new relq_errors_1.RelqConfigError('No database connection available');
619
703
  }
704
+ };
705
+ try {
706
+ const startTime = performance.now();
707
+ const result = await executeQuery();
620
708
  const duration = performance.now() - startTime;
621
709
  return { result, duration };
622
710
  }
623
711
  catch (error) {
712
+ const isConnectionError = error.code === 'ECONNRESET' ||
713
+ error.code === 'ETIMEDOUT' ||
714
+ error.code === 'EPIPE' ||
715
+ error.code === 'ENOTCONN' ||
716
+ error.code === '57P01' ||
717
+ error.code === '57P02' ||
718
+ error.code === '57P03' ||
719
+ error.message?.includes('Connection terminated') ||
720
+ error.message?.includes('connection is closed') ||
721
+ error.message?.includes('Client has encountered a connection error');
722
+ if (isConnectionError && !this.usePooling && this.client) {
723
+ this.clientConnected = false;
724
+ try {
725
+ await this.ensureConnection();
726
+ const startTime = performance.now();
727
+ const result = await this.client.query(sql);
728
+ const duration = performance.now() - startTime;
729
+ return { result, duration };
730
+ }
731
+ catch (retryError) {
732
+ throw (0, relq_errors_1.parsePostgresError)(error, sql);
733
+ }
734
+ }
624
735
  throw (0, relq_errors_1.parsePostgresError)(error, sql);
625
736
  }
626
737
  }
@@ -47,6 +47,8 @@ export class Relq {
47
47
  initialized = false;
48
48
  initPromise;
49
49
  isClosed = false;
50
+ clientConnected = false;
51
+ connectPromise;
50
52
  environment;
51
53
  poolErrorHandler;
52
54
  poolConnectHandler;
@@ -451,8 +453,14 @@ export class Relq {
451
453
  connectionString: this.config.connectionString,
452
454
  ssl
453
455
  });
454
- this.client.on('end', () => this.emitter.emit('end'));
455
- this.client.on('error', (err) => this.emitter.emit('error', err));
456
+ this.client.on('end', () => {
457
+ this.clientConnected = false;
458
+ this.emitter.emit('end');
459
+ });
460
+ this.client.on('error', (err) => {
461
+ this.clientConnected = false;
462
+ this.emitter.emit('error', err);
463
+ });
456
464
  this.client.on('notice', (msg) => this.emitter.emit('notice', msg));
457
465
  this.client.on('notification', (msg) => this.emitter.emit('notification', msg));
458
466
  }
@@ -491,9 +499,85 @@ export class Relq {
491
499
  await this.initialize();
492
500
  if (!this.usePooling && this.client) {
493
501
  await this.client.connect();
502
+ this.clientConnected = true;
494
503
  this.emitter.emit('connect', this.client);
495
504
  }
496
505
  }
506
+ async ensureConnection() {
507
+ if (this.usePooling || !this.client)
508
+ return;
509
+ if (this.clientConnected)
510
+ return;
511
+ if (this.connectPromise) {
512
+ return this.connectPromise;
513
+ }
514
+ this.connectPromise = (async () => {
515
+ try {
516
+ if (this.client && !this.clientConnected) {
517
+ try {
518
+ await this.client.connect();
519
+ this.clientConnected = true;
520
+ this.emitter.emit('connect', this.client);
521
+ }
522
+ catch (err) {
523
+ if (err.message?.includes('already been connected') ||
524
+ err.code === 'ECONNRESET' ||
525
+ err.code === 'ETIMEDOUT' ||
526
+ err.code === 'EPIPE' ||
527
+ err.message?.includes('Connection terminated')) {
528
+ this.client.removeAllListeners();
529
+ try {
530
+ await this.client.end();
531
+ }
532
+ catch { }
533
+ await this.recreateClient();
534
+ }
535
+ else {
536
+ throw err;
537
+ }
538
+ }
539
+ }
540
+ }
541
+ finally {
542
+ this.connectPromise = undefined;
543
+ }
544
+ })();
545
+ return this.connectPromise;
546
+ }
547
+ async recreateClient() {
548
+ const { Client: PgClientClass } = await loadPg();
549
+ let resolvedPassword = this.config.password;
550
+ const isAws = isAwsDsqlConfig(this.config);
551
+ if (isAws) {
552
+ resolvedPassword = await getAwsDsqlToken(this.config.aws);
553
+ }
554
+ const host = isAws ? this.config.aws.hostname : (this.config.host || 'localhost');
555
+ const port = this.config.aws?.port ?? this.config.port ?? 5432;
556
+ const user = this.config.aws?.user ?? this.config.user ?? (isAws ? 'admin' : undefined);
557
+ const ssl = isAws ? (this.config.aws.ssl ?? true) : this.config.ssl;
558
+ this.client = new PgClientClass({
559
+ host,
560
+ port,
561
+ database: this.config.database,
562
+ user,
563
+ password: resolvedPassword,
564
+ connectionString: this.config.connectionString,
565
+ ssl
566
+ });
567
+ this.client.on('end', () => {
568
+ this.clientConnected = false;
569
+ this.emitter.emit('end');
570
+ });
571
+ this.client.on('error', (err) => {
572
+ this.clientConnected = false;
573
+ this.emitter.emit('error', err);
574
+ });
575
+ this.client.on('notice', (msg) => this.emitter.emit('notice', msg));
576
+ this.client.on('notification', (msg) => this.emitter.emit('notification', msg));
577
+ await this.client.connect();
578
+ this.clientConnected = true;
579
+ this.emitter.emit('connect', this.client);
580
+ }
497
581
  async subscribe(channel, callback) {
498
582
  if (!this.listener) {
499
583
  this.listener = new ListenerConnection({
@@ -540,6 +624,7 @@ export class Relq {
540
624
  else if (this.client) {
541
625
  this.client.removeAllListeners();
542
626
  await this.client.end();
627
+ this.clientConnected = false;
543
628
  }
544
629
  this.isClosed = true;
545
630
  activeRelqInstances.delete(this);
@@ -565,26 +650,52 @@ export class Relq {
565
650
  return this;
566
651
  }
567
652
  async _executeQuery(sql) {
568
- try {
569
- if (!sql || typeof sql !== 'string' || sql.trim() === '') {
570
- throw new RelqConfigError(`Invalid SQL query: ${sql === null ? 'null' : sql === undefined ? 'undefined' : 'empty string'}`);
571
- }
572
- await this.initialize();
573
- const startTime = performance.now();
574
- let result;
653
+ if (!sql || typeof sql !== 'string' || sql.trim() === '') {
654
+ throw new RelqConfigError(`Invalid SQL query: ${sql === null ? 'null' : sql === undefined ? 'undefined' : 'empty string'}`);
655
+ }
656
+ await this.initialize();
657
+ const executeQuery = async () => {
575
658
  if (this.pool) {
576
- result = await this.pool.query(sql);
659
+ return await this.pool.query(sql);
577
660
  }
578
661
  else if (this.client) {
579
- result = await this.client.query(sql);
662
+ await this.ensureConnection();
663
+ return await this.client.query(sql);
580
664
  }
581
665
  else {
582
666
  throw new RelqConfigError('No database connection available');
583
667
  }
668
+ };
669
+ try {
670
+ const startTime = performance.now();
671
+ const result = await executeQuery();
584
672
  const duration = performance.now() - startTime;
585
673
  return { result, duration };
586
674
  }
587
675
  catch (error) {
676
+ const isConnectionError = error.code === 'ECONNRESET' ||
677
+ error.code === 'ETIMEDOUT' ||
678
+ error.code === 'EPIPE' ||
679
+ error.code === 'ENOTCONN' ||
680
+ error.code === '57P01' ||
681
+ error.code === '57P02' ||
682
+ error.code === '57P03' ||
683
+ error.message?.includes('Connection terminated') ||
684
+ error.message?.includes('connection is closed') ||
685
+ error.message?.includes('Client has encountered a connection error');
686
+ if (isConnectionError && !this.usePooling && this.client) {
687
+ this.clientConnected = false;
688
+ try {
689
+ await this.ensureConnection();
690
+ const startTime = performance.now();
691
+ const result = await this.client.query(sql);
692
+ const duration = performance.now() - startTime;
693
+ return { result, duration };
694
+ }
695
+ catch (retryError) {
696
+ throw parsePostgresError(error, sql);
697
+ }
698
+ }
588
699
  throw parsePostgresError(error, sql);
589
700
  }
590
701
  }
package/dist/index.d.ts CHANGED
@@ -8347,6 +8347,8 @@ export declare class Relq<TSchema = any> {
8347
8347
  private initialized;
8348
8348
  private initPromise?;
8349
8349
  private isClosed;
8350
+ private clientConnected;
8351
+ private connectPromise?;
8350
8352
  private environment;
8351
8353
  private poolErrorHandler?;
8352
8354
  private poolConnectHandler?;
@@ -8494,6 +8496,17 @@ export declare class Relq<TSchema = any> {
8494
8496
  * @internal
8495
8497
  */
8496
8498
  private connect;
8499
+ /**
8500
+ * Ensure single client is connected, with auto-reconnect on failure
8501
+ * Handles concurrent calls by reusing the same connect promise
8502
+ * @internal
8503
+ */
8504
+ private ensureConnection;
8505
+ /**
8506
+ * Recreate the client after connection loss
8507
+ * @internal
8508
+ */
8509
+ private recreateClient;
8497
8510
  /**
8498
8511
  * Subscribe to a PostgreSQL NOTIFY channel
8499
8512
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relq",
3
- "version": "1.0.65",
3
+ "version": "1.0.66",
4
4
  "description": "The Fully-Typed PostgreSQL ORM for TypeScript",
5
5
  "author": "Olajide Mathew O. <olajide.mathew@yuniq.solutions>",
6
6
  "license": "MIT",