Full Code of voxpelli/node-pg-pubsub for AI

main 1a8187477fcb cached
23 files
25.9 KB
7.4k tokens
9 symbols
1 requests
Download .txt
Repository: voxpelli/node-pg-pubsub
Branch: main
Commit: 1a8187477fcb
Files: 23
Total size: 25.9 KB

Directory structure:
gitextract_5do_q0e6/

├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github/
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── dependency-review.yml
│       ├── lint.yml
│       ├── nodejs.yml
│       └── types.yml
├── .gitignore
├── .husky/
│   └── pre-push
├── .npmrc
├── LICENSE
├── README.md
├── declaration.tsconfig.json
├── index.js
├── lib/
│   └── client.js
├── package.json
├── renovate.json
├── sample.env
├── test/
│   ├── .eslintrc
│   ├── db-utils.js
│   └── integration/
│       └── main.spec.js
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true


================================================
FILE: .eslintignore
================================================
/coverage/**/*


================================================
FILE: .eslintrc
================================================
{
  "extends": "@voxpelli",
  "root": true
}


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
name: "CodeQL"

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '15 0 * * 5'

permissions:
  actions: read
  contents: read
  security-events: write

jobs:
  analyze:
    uses: voxpelli/ghatemplates/.github/workflows/codeql-analysis.yml@main


================================================
FILE: .github/workflows/dependency-review.yml
================================================
name: 'Dependency Review'

on: [pull_request]

permissions:
  contents: read

jobs:
  dependency-review:
    uses: voxpelli/ghatemplates/.github/workflows/dependency-review.yml@main



================================================
FILE: .github/workflows/lint.yml
================================================
name: Linting

on:
  push:
    branches:
      - main
    tags:
      - '*'
  pull_request:
    branches:
      - main

permissions:
  contents: read

jobs:
  lint:
    uses: voxpelli/ghatemplates/.github/workflows/lint.yml@main


================================================
FILE: .github/workflows/nodejs.yml
================================================
name: Node CI

on:
  push:
    branches:
      - main
    tags:
      - '*'
  pull_request:
    branches:
      - main

permissions:
  contents: read

jobs:
  test:
    uses: voxpelli/ghatemplates/.github/workflows/test-pg.yml@main
    with:
      node-versions: '16,18,20'
      os: 'ubuntu-latest'
      pg-versions: '9.4,12,13'


================================================
FILE: .github/workflows/types.yml
================================================
name: Type Checks

on:
  push:
    branches:
      - main
    tags:
      - '*'
  pull_request:
    branches:
      - main
  schedule:
    - cron: '14 5 * * 1,3,5'

permissions:
  contents: read

jobs:
  type-check:
    uses: voxpelli/ghatemplates/.github/workflows/type-check.yml@main
    with:
      ts-versions: ${{ github.event.schedule && 'next' || '5.0,next' }}
      ts-libs: 'es2020;esnext'


================================================
FILE: .gitignore
================================================
# Basic ones
/coverage
/docs
/node_modules
/.env
/.nyc_output

# We're a library, so please, no lock files
/package-lock.json
/yarn.lock

# Generated types
*.d.ts
*.d.ts.map

# Library specific ones
.env


================================================
FILE: .husky/pre-push
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm test


================================================
FILE: .npmrc
================================================
package-lock=false


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2017 Pelle Wessman

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# PG PubSub

A Publish/Subscribe implementation on top of [PostgreSQL NOTIFY/LISTEN](https://www.postgresql.org/docs/current/sql-notify.html)

[![npm version](https://img.shields.io/npm/v/pg-pubsub.svg?style=flat)](https://www.npmjs.com/package/pg-pubsub)
[![npm downloads](https://img.shields.io/npm/dm/pg-pubsub.svg?style=flat)](https://www.npmjs.com/package/pg-pubsub)
[![Module type: CJS](https://img.shields.io/badge/module%20type-cjs-brightgreen)](https://github.com/voxpelli/badges-cjs-esm)
[![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)](https://github.com/voxpelli/types-in-js)
[![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg)](https://github.com/voxpelli/eslint-config)
[![Follow @voxpelli@mastodon.social](https://img.shields.io/mastodon/follow/109247025527949675?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@voxpelli)

## Installation

```bash
npm install pg-pubsub --save
```

## Requirements

* Postgres >= 9.4

## Usage

```js
const PGPubsub = require('pg-pubsub');
const pubsubInstance = new PGPubsub(uri[, options]);
```

### Options

```js
{
  [log]: Function // default: silent when NODE_ENV=production, otherwise defaults to console.log(...)
}
```

### Methods

* **addChannel(channelName[, eventListener])** – starts listening on a channel and optionally adds an event listener for that event. As `PGPubsub` inherits from `EventEmitter` one can also add it oneself. Returns a `Promise` that resolves when the listening has started.
* **removeChannel(channelName[, eventListener])** – either removes all event listeners and stops listeneing on the channel or removes the specified event listener and stops listening on the channel if that was the last listener attached.
* **publish(channelName, data)** – publishes the specified data JSON-encoded to the specified channel. It may be better to do this by sending the `NOTIFY channelName, '{"hello":"world"}'` query yourself using your ordinary Postgres pool, rather than relying on the single connection of this module. Returns a `Promise` that will become rejected or resolved depending on the success of the Postgres call.
* **close(): Promise<void>** – closes down the database connection and removes all listeners. Useful for graceful shutdowns.
* All [EventEmitter methods](http://nodejs.org/api/events.html#events_class_events_eventemitter) are inherited from `EventEmitter`

### Examples

#### Simple

```javascript
const pubsubInstance = new PGPubsub('postgres://username@localhost/database');

await pubsubInstance.addChannel('channelName', function (channelPayload) {
  // Process the payload – if it was JSON that JSON has been parsed into an object for you
});

await pubsubInstance.publish('channelName', { hello: "world" });
```

The above sends `NOTIFY channelName, '{"hello":"world"}'` to PostgreSQL, which will trigger the above listener with the parsed JSON in `channelPayload`.

#### Advanced

```javascript
const pubsubInstance = new PGPubsub('postgres://username@localhost/database');

await pubsubInstance.addChannel('channelName');

// pubsubInstance is a full EventEmitter object that sends events on channel names
pubsubInstance.once('channelName', channelPayload => {
  // Process the payload
});
```

## Description

Creating a `PGPubsub` instance will not do much up front. It will prepare itself to start a Postgres connection once the first channel is added and then it will keep a connection open until its shut down, reconnecting it if it gets lost, so that it can constantly listen for new notifications.

## Lint / Test

- setup a postgres database to run the integration tests
  - the easist way to do this is via docker, `docker run -it -p 5432:5432 -e POSTGRES_DB=pgpubsub_test postgres`
- `npm test`

For an all-in-one command, try:
```sh
# fire up a new DB container, run tests against it, and clean it up!
docker rm -f pgpubsub_test || true && \
docker run -itd -p 5432:5432 -e POSTGRES_DB=pgpubsub_test --name pgpubsub_test postgres && \
npm test && \
docker rm -f pgpubsub_test
```



================================================
FILE: declaration.tsconfig.json
================================================

{
  "extends": "./tsconfig",
  "exclude": [
    "test/**/*.js"
  ],
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "noEmit": false,
    "emitDeclarationOnly": true
  }
}


================================================
FILE: index.js
================================================
/* eslint-disable promise/prefer-await-to-then */
// @ts-check
/// <reference types="node" />
/// <reference types="pg" />

'use strict';

const EventEmitter = require('node:events');

const pgFormat = require('pg-format');
const { ErrorWithCause } = require('pony-cause');

const { pgClientRetry } = require('./lib/client');

// TODO: Move to an async generator approach rather than EventEmitter

/** @typedef {(payload: any) => void} PGPubsubCallback */

class PGPubsub extends EventEmitter {
  /** @type {string[]} */
  #channels = [];

  /** @type {import('promised-retry')} */
  #retry;

  /**
   * @param {string | import('pg').ClientConfig} [conString]
   * @param {{ log?: typeof console.log, retryLimit?: number }} options
   */
  // eslint-disable-next-line n/no-process-env
  constructor (conString = process.env['DATABASE_URL'], { log, retryLimit } = {}) {
    super();

    this.setMaxListeners(0);

    this.#retry = pgClientRetry({
      clientOptions: typeof conString === 'object' ? conString : { connectionString: conString },
      retryLimit,
      log,
      shouldReconnect: () => this.#channels.length !== 0,
      successCallback: client => {
        client.on('notification', msg => this.#processNotification(msg));

        Promise.all(this.#channels.map(channel => client.query('LISTEN "' + channel + '"')))
          .catch(/** @param {unknown} err */err => {
            this.emit(
              'error',
              new ErrorWithCause('Failed to set up channels on new connection', { cause: err })
            );
          });

        return client;
      },
    });
  }

  /**
   * @protected
   * @param {boolean} [noNewConnections]
   * @returns {Promise<import('pg').Client>}
   */
  async _getDB (noNewConnections) {
    return this.#retry.try(!noNewConnections)
      .catch(/** @param {unknown} err */err => {
        throw new ErrorWithCause('Failed to establish database connection', { cause: err });
      });
  }

  /**
   * @param {import('pg').Notification} msg
   * @returns {void}
   */
  #processNotification (msg) {
    let payload = msg.payload || '';

    // If the payload is valid JSON, then replace it with such
    try { payload = JSON.parse(payload); } catch {}

    this.emit(msg.channel, payload);
  }

  /**
   * @param {string} channel
   * @param {PGPubsubCallback} [callback]
   * @returns {Promise<void>}
   */
  async addChannel (channel, callback) {
    if (!this.#channels.includes(channel)) {
      this.#channels.push(channel);

      // TODO: Can't this possibly result in both the try() method and this method adding a LISTEN for it?
      try {
        const db = await this._getDB();
        await db.query('LISTEN "' + channel + '"');
      } catch (err) {
        throw new ErrorWithCause('Failed to listen to channel', { cause: err });
      }
    }

    if (callback) {
      this.on(channel, callback);
    }
  }

  /**
   * @param {string} channel
   * @param {PGPubsubCallback} [callback]
   * @returns {this}
   */
  removeChannel (channel, callback) {
    const pos = this.#channels.indexOf(channel);

    if (pos === -1) {
      return this;
    }

    if (callback) {
      this.removeListener(channel, callback);
    } else {
      this.removeAllListeners(channel);
    }

    if (this.listeners(channel).length === 0) {
      this.#channels.splice(pos, 1);
      this._getDB(true)
        .then(db => db.query('UNLISTEN "' + channel + '"'))
        .catch(/** @param {unknown} err */err => {
          this.emit(
            'error',
            new ErrorWithCause('Failed to stop listening to channel', { cause: err })
          );
        });
    }

    return this;
  }

  /**
   * @param {string} channel
   * @param {any} [data]
   * @returns {Promise<void>}
   */
  async publish (channel, data) {
    const payload = data ? ', ' + pgFormat.literal(JSON.stringify(data)) : '';

    try {
      const db = await this._getDB();
      await db.query(`NOTIFY "${channel}"${payload}`);
    } catch (err) {
      throw new ErrorWithCause('Failed to publish to channel', { cause: err });
    }
  }

  /** @returns {Promise<void>} */
  async close () {
    this.removeAllListeners();
    this.#channels = [];
    return this.#retry.end();
  }

  reset () {
    return this.#retry.reset();
  }
}

module.exports = PGPubsub;


================================================
FILE: lib/client.js
================================================
// @ts-check
/// <reference types="node" />
/// <reference types="pg" />

'use strict';

const Retry = require('promised-retry');

const { Client } = require('pg');

/** @returns {typeof console.log} */
const getDefaultLog = () =>
  // eslint-disable-next-line n/no-process-env
  process.env['NODE_ENV'] === 'production'
    ? () => {}
    // eslint-disable-next-line no-console
    : console.log.bind(console);

/**
 * @typedef PgClientRetryOptions
 * @property {string|import('pg').ClientConfig} clientOptions
 * @property {(typeof console.log)|undefined} [log]
 * @property {number|undefined} [retryLimit]
 * @property {(client: import('pg').Client) => Promise<import('pg').Client>|import('pg').Client} [successCallback]
 * @property {() => boolean} [shouldReconnect]
 */

/**
 * @param {PgClientRetryOptions} options
 * @returns {import('promised-retry')}
 */
const pgClientRetry = (options) => {
  const {
    clientOptions,
    log = getDefaultLog(),
    retryLimit,
    shouldReconnect,
    successCallback,
  } = options;

  const retry = new Retry({
    // TODO: Improve types for this in promised-retry
    'try': async () => {
      const client = new Client(clientOptions);

      // TODO: Add client.on('end') ?
      // If the connection fail after we have established it, then we need to reset the state of our retry mechanism and restart from scratch.
      client.on('error', () => {
        retry.reset();
        if (shouldReconnect && shouldReconnect()) retry.try();
        client.end(err => {
          log('Received error when disconnecting from database in error callback: ' + (err && err.message));
        });
      });

      // Do the connect
      await client.connect();

      return client;
    },
    // TODO: Improve types for this in promised-retry, what should actually be returned?
    success:
      /**
       * @param {import('pg').Client} client
       * @returns {Promise<import('pg').Client>}
       */
      async client => successCallback ? successCallback(client) : client,
    // TODO: Improve types for this in promised-retry
    end:
      /**
       * @param {import('pg').Client} [client]
       * @returns {Promise<void>}
       */
      async client => client ? client.end() : undefined,
    name: 'pgClientRetry',
    retryLimit,
    log,
  });

  return retry;
};

module.exports = {
  pgClientRetry,
};


================================================
FILE: package.json
================================================
{
  "name": "pg-pubsub",
  "version": "0.8.1",
  "description": "A Publish/Subscribe implementation on top of PostgreSQL NOTIFY/LISTEN",
  "homepage": "http://github.com/voxpelli/node-pg-pubsub",
  "repository": {
    "type": "git",
    "url": "git://github.com/voxpelli/node-pg-pubsub.git"
  },
  "author": {
    "name": "Pelle Wessman",
    "email": "pelle@kodfabrik.se",
    "url": "http://kodfabrik.se/"
  },
  "license": "MIT",
  "engines": {
    "node": ">=16.0.0"
  },
  "main": "index.js",
  "types": "index.d.ts",
  "files": [
    "index.js",
    "index.d.ts",
    "index.d.ts.map",
    "lib/*.js",
    "lib/*.d.ts",
    "lib/*.d.ts.map"
  ],
  "scripts": {
    "build:0": "run-s clean",
    "build:1-declaration": "tsc -p declaration.tsconfig.json",
    "build:2-add-ignores": "ts-ignore-import '**/*.d.ts'",
    "build": "run-s build:*",
    "check:dependency-check": "dependency-check *.js 'test/**/*.js' --no-dev",
    "check:installed-check": "installed-check -i eslint-plugin-jsdoc",
    "check:lint": "eslint --report-unused-disable-directives .",
    "check:tsc": "tsc",
    "check:type-coverage": "type-coverage --detail --strict --at-least 98 --ignore-files 'test/**/*'",
    "check": "run-s clean && run-p check:*",
    "clean:declarations": "rm -rf $(find . -maxdepth 2 -type f -name '*.d.ts*')",
    "clean": "run-p clean:*",
    "prepublishOnly": "run-s build",
    "test:mocha": "cross-env NODE_ENV=test c8 --reporter=lcov --reporter text mocha test/**/*.spec.js",
    "test-ci": "run-s test:*",
    "test": "run-s check test:*",
    "prepare": "husky install"
  },
  "dependencies": {
    "pg": "^8.7.3",
    "pg-format": "^1.0.2",
    "pony-cause": "^2.1.10",
    "promised-retry": "^0.5.0"
  },
  "devDependencies": {
    "@hdsydsvenskan/ts-ignore-import": "^2.0.0",
    "@types/chai": "^4.3.5",
    "@types/chai-as-promised": "^7.1.5",
    "@types/mocha": "^10.0.1",
    "@types/node": "^16.18.28",
    "@types/pg": "^8.6.6",
    "@types/pg-format": "^1.0.2",
    "@voxpelli/eslint-config": "^16.0.7",
    "@voxpelli/tsconfig": "^7.0.0",
    "c8": "^7.13.0",
    "chai": "^4.3.7",
    "chai-as-promised": "^7.1.1",
    "cross-env": "^7.0.3",
    "dependency-check": "^5.0.0-7",
    "dotenv": "^16.0.3",
    "eslint": "^8.40.0",
    "eslint-config-standard": "^17.0.0",
    "eslint-plugin-es": "^4.1.0",
    "eslint-plugin-import": "^2.27.5",
    "eslint-plugin-jsdoc": "^44.2.2",
    "eslint-plugin-mocha": "^10.1.0",
    "eslint-plugin-n": "^15.7.0",
    "eslint-plugin-promise": "^6.1.1",
    "eslint-plugin-security": "^1.7.1",
    "eslint-plugin-sort-destructure-keys": "^1.5.0",
    "eslint-plugin-unicorn": "^47.0.0",
    "husky": "^8.0.3",
    "installed-check": "^7.0.0",
    "mocha": "^10.2.0",
    "npm-run-all2": "^6.0.5",
    "type-coverage": "^2.25.3",
    "typescript": "~5.0.4"
  }
}


================================================
FILE: renovate.json
================================================
{
  "extends": [
    "github>voxpelli/renovate-config"
  ]
}


================================================
FILE: sample.env
================================================
DATABASE_TEST_URL="postgres://postgres@localhost/pgpubsub_test"
DATABASE_TEST_URL_INVALID_USER="postgres://foobar:pass@localhost/pgpubsub_test"
DATABASE_TEST_URL_INVALID_PASSWORD="postgres://postgres:invalidpass@localhost/pgpubsub_test"


================================================
FILE: test/.eslintrc
================================================
{
  "env": {
    "mocha": true
  },
  "rules": {
    "no-unused-expressions": 0,
    "node/no-unpublished-require": 0,
    "promise/prefer-await-to-then": 0
  }
}


================================================
FILE: test/db-utils.js
================================================
/* eslint-disable n/no-process-env */

// @ts-check
/// <reference types="node" />

'use strict';

const pathModule = require('node:path');

const dotEnvFile = process.env['DOTENV_FILE'] || pathModule.resolve(__dirname, './.env');

require('dotenv').config({ path: dotEnvFile });

const connectionDetails = process.env['DATABASE_TEST_URL'] || {
  database: process.env['PGDATABASE'] || 'pgpubsub_test',
};

module.exports = { connectionDetails };


================================================
FILE: test/integration/main.spec.js
================================================
// @ts-check
/// <reference types="node" />
/// <reference types="mocha" />
/// <reference types="chai" />
/// <reference types="chai-as-promised" />

'use strict';

const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');

const { connectionDetails } = require('../db-utils');
const PGPubsub = require('../../');

chai.use(chaiAsPromised);
chai.should();

// @ts-ignore
// eslint-disable-next-line no-console
process.on('unhandledRejection', err => { console.log('Unhandled Rejection:', err.stack); });

// eslint-disable-next-line n/no-process-env
const conStringInvalidUser = process.env['DATABASE_TEST_URL_INVALID_USER'] || 'postgres://invalidUsername@localhost/pgpubsub_test';
// eslint-disable-next-line n/no-process-env
const conStringInvalidPassword = process.env['DATABASE_TEST_URL_INVALID_PASSWORD'] || 'postgres://postgres:invalid@localhost/pgpubsub_test';

/**
 * @template T
 * @returns {[Promise<T>, (value?: T | PromiseLike<T>) => void, (err: Error) => void]}
 */
const resolveablePromise = () => {
  /** @type {(value?: T | PromiseLike<T>) => void} */
  let resolver;
  /** @type {(err: Error) => void} */
  let rejecter;

  const resolveable = new Promise((resolve, reject) => {
    resolver = resolve;
    rejecter = reject;
  });

  // @ts-ignore
  return [resolveable, resolver, rejecter];
};

describe('Pubsub', () => {
  /** @type {import('../../index')} */
  let pubsubInstance;
  /** @type {import('pg').Client} */
  let db;

  beforeEach(async () => {
    pubsubInstance = new PGPubsub(connectionDetails, {
      log: function (...params) {
        if (typeof arguments[0] !== 'string' || !arguments[0].startsWith('Success')) {
          // eslint-disable-next-line no-console
          console.log.call(this, ...params);
        }
      },
    });

    // @ts-ignore
    db = await pubsubInstance._getDB();
  });

  afterEach(() => pubsubInstance.close());

  describe('init', function () {
    this.timeout(2000);

    it('should handle errenous database user', async () => {
      pubsubInstance.close();
      pubsubInstance = new PGPubsub(conStringInvalidUser, {
        log: () => {},
        retryLimit: 1,
      });
      // @ts-ignore
      return pubsubInstance._getDB()
        .should.be.rejectedWith(/Failed to establish database connection/);
    });

    // TODO: Fix, doesn't work on Travis right now
    it.skip('should handle errenous database password', async () => {
      pubsubInstance.close();
      pubsubInstance = new PGPubsub(conStringInvalidPassword, {
        log: () => {},
        retryLimit: 1,
      });
      // @ts-ignore
      return pubsubInstance._getDB()
        .should.be.rejectedWith(/Failed to establish database connection/);
    });
  });

  describe('receive', function () {
    it('should receive a notification', async () => {
      const [result, resolve] = resolveablePromise();

      await pubsubInstance.addChannel('foobar', (channelPayload) => {
        channelPayload.should.deep.equal({ abc: 123 });
        resolve();
      });

      await db.query('NOTIFY foobar, \'{"abc":123}\'');

      return result;
    });

    it('should handle non-JSON notifications', async () => {
      const [result, resolve] = resolveablePromise();

      await pubsubInstance.addChannel('foobar', channelPayload => {
        channelPayload.should.equal('barfoo');
        resolve();
      });
      await db.query('NOTIFY foobar, \'barfoo\'');

      return result;
    });

    it('should only receive notifications from correct channel', async () => {
      const [result1, resolve1] = resolveablePromise();
      const [result2, resolve2] = resolveablePromise();

      await pubsubInstance.addChannel('foo', channelPayload => {
        channelPayload.should.deep.equal({ abc: 123 });
        resolve1();
      });

      await pubsubInstance.addChannel('bar', channelPayload => {
        channelPayload.should.deep.equal({ xyz: 789 });
        resolve2();
      });

      await Promise.all([
        db.query('NOTIFY def, \'{"ghi":456}\''),
        db.query('NOTIFY foo, \'{"abc":123}\''),
        db.query('NOTIFY bar, \'{"xyz":789}\''),
      ]);

      await Promise.all([
        result1,
        result2,
      ]);
    });

    it('should handle non-alphanumeric channel names', async () => {
      const [result, resolve] = resolveablePromise();

      await pubsubInstance.addChannel('97a38cd1-d332-4240-93e4-1ff436a7da2a', function (channelPayload) {
        channelPayload.should.deep.equal({ 'non-alpha': true });
        resolve();
      });

      await db.query('NOTIFY "97a38cd1-d332-4240-93e4-1ff436a7da2a", \'{"non-alpha":true}\'');

      return result;
    });

    it('should stop listening when channel is removed', async () => {
      const [result, resolve] = resolveablePromise();

      await pubsubInstance.addChannel('foo', function () {
        throw new Error('This channel should have been removed and should not receive any items');
      });

      await pubsubInstance.addChannel('foo', function () {
        throw new Error('This channel should have been removed and should not receive any items');
      });

      await pubsubInstance.addChannel('bar', function () {
        resolve();
      });

      pubsubInstance.removeChannel('foo');

      await db.query('NOTIFY foo, \'{"abc":123}\'');
      await db.query('NOTIFY bar, \'{"xyz":789}\'');

      return result;
    });

    it('should allow multiple listener for same channel', async () => {
      const [result, resolve] = resolveablePromise();

      let first = false;

      await pubsubInstance.addChannel('foobar', function () {
        first = true;
      });
      await pubsubInstance.addChannel('foobar', function () {
        first.should.be.ok;
        resolve();
      });

      await db.query('NOTIFY foobar, \'{"abc":123}\'');

      return result;
    });

    it('should be able to remove specific listener', async () => {
      const [result, resolve] = resolveablePromise();

      let second = false;

      // eslint-disable-next-line unicorn/consistent-function-scoping
      const listener = function () {
        throw new Error('This channel should have been removed and should not receive any items');
      };

      await pubsubInstance.addChannel('foobar', listener);

      await pubsubInstance.addChannel('foobar', function () {
        if (second) {
          resolve();
        } else {
          second = true;
        }
      });

      pubsubInstance.removeChannel('foobar', listener);

      await db.query('NOTIFY foobar, \'{"abc":123}\'');
      await db.query('NOTIFY foobar, \'{"abc":123}\'');

      return result;
    });

    it('should support EventEmitter methods for listening', async () => {
      const [result, resolve] = resolveablePromise();

      await pubsubInstance.addChannel('foobar');

      pubsubInstance.on('foobar', function () {
        resolve();
      });

      await db.query('NOTIFY foobar, \'{"abc":123}\'');

      return result;
    });

    it('should support recovery after reconnect', async () => {
      const [result, resolve] = resolveablePromise();

      await pubsubInstance.addChannel('foobar', function () {
        resolve();
      });

      setImmediate(() => {
        db.end();
        pubsubInstance.reset();

        // @ts-ignore
        // eslint-disable-next-line promise/always-return, promise/catch-or-return
        pubsubInstance._getDB().then(async db => {
          await db.query('NOTIFY foobar, \'{"abc":123}\'');
        });
      });

      return result;
    });
  });

  describe('publish', function () {
    it('should publish a notification', async () => {
      const [result, resolve] = resolveablePromise();

      const data = { abc: 123 };

      await pubsubInstance.addChannel('foobar', function (channelPayload) {
        channelPayload.should.deep.equal(data);
        resolve();
      });

      await pubsubInstance.publish('foobar', data);

      return result;
    });

    it('should not be vulnerable to SQL injection', async () => {
      const [result, resolve] = resolveablePromise();

      const data = { abc: '\'"; AND DO SOMETHING BAD' };

      await pubsubInstance.addChannel('foobar', function (channelPayload) {
        channelPayload.should.deep.equal(data);
        resolve();
      });

      await pubsubInstance.publish('foobar', data);

      return result;
    });

    it('should gracefully handle too large payloads', async () => {
      const data = Array.from({ length: 10000 });
      data.fill('a');
      return pubsubInstance.publish('foobar', data).should.be.rejectedWith(Error);
    });
  });
});


================================================
FILE: tsconfig.json
================================================
{
  "extends": "@voxpelli/tsconfig/node16.json",
  "files": [
    "index.js",
  ],
  "include": [
    "lib/**/*.js",
    "test/**/*.js",
  ]
}
Download .txt
gitextract_5do_q0e6/

├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github/
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── dependency-review.yml
│       ├── lint.yml
│       ├── nodejs.yml
│       └── types.yml
├── .gitignore
├── .husky/
│   └── pre-push
├── .npmrc
├── LICENSE
├── README.md
├── declaration.tsconfig.json
├── index.js
├── lib/
│   └── client.js
├── package.json
├── renovate.json
├── sample.env
├── test/
│   ├── .eslintrc
│   ├── db-utils.js
│   └── integration/
│       └── main.spec.js
└── tsconfig.json
Download .txt
SYMBOL INDEX (9 symbols across 1 files)

FILE: index.js
  class PGPubsub (line 19) | class PGPubsub extends EventEmitter {
    method constructor (line 31) | constructor (conString = process.env['DATABASE_URL'], { log, retryLimi...
    method _getDB (line 62) | async _getDB (noNewConnections) {
    method #processNotification (line 73) | #processNotification (msg) {
    method addChannel (line 87) | async addChannel (channel, callback) {
    method removeChannel (line 110) | removeChannel (channel, callback) {
    method publish (line 143) | async publish (channel, data) {
    method close (line 155) | async close () {
    method reset (line 161) | reset () {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (29K chars).
[
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_"
  },
  {
    "path": ".eslintignore",
    "chars": 15,
    "preview": "/coverage/**/*\n"
  },
  {
    "path": ".eslintrc",
    "chars": 45,
    "preview": "{\n  \"extends\": \"@voxpelli\",\n  \"root\": true\n}\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 292,
    "preview": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n  schedule:\n    - cron: '15 0 "
  },
  {
    "path": ".github/workflows/dependency-review.yml",
    "chars": 183,
    "preview": "name: 'Dependency Review'\n\non: [pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  dependency-review:\n    uses: voxpe"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 229,
    "preview": "name: Linting\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - '*'\n  pull_request:\n    branches:\n      - main\n\n"
  },
  {
    "path": ".github/workflows/nodejs.yml",
    "chars": 331,
    "preview": "name: Node CI\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - '*'\n  pull_request:\n    branches:\n      - main\n\n"
  },
  {
    "path": ".github/workflows/types.yml",
    "chars": 399,
    "preview": "name: Type Checks\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - '*'\n  pull_request:\n    branches:\n      - ma"
  },
  {
    "path": ".gitignore",
    "chars": 204,
    "preview": "# Basic ones\n/coverage\n/docs\n/node_modules\n/.env\n/.nyc_output\n\n# We're a library, so please, no lock files\n/package-lock"
  },
  {
    "path": ".husky/pre-push",
    "chars": 51,
    "preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpm test\n"
  },
  {
    "path": ".npmrc",
    "chars": 19,
    "preview": "package-lock=false\n"
  },
  {
    "path": "LICENSE",
    "chars": 1080,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2017 Pelle Wessman\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "README.md",
    "chars": 4103,
    "preview": "# PG PubSub\n\nA Publish/Subscribe implementation on top of [PostgreSQL NOTIFY/LISTEN](https://www.postgresql.org/docs/cur"
  },
  {
    "path": "declaration.tsconfig.json",
    "chars": 204,
    "preview": "\n{\n  \"extends\": \"./tsconfig\",\n  \"exclude\": [\n    \"test/**/*.js\"\n  ],\n  \"compilerOptions\": {\n    \"declaration\": true,\n   "
  },
  {
    "path": "index.js",
    "chars": 4307,
    "preview": "/* eslint-disable promise/prefer-await-to-then */\n// @ts-check\n/// <reference types=\"node\" />\n/// <reference types=\"pg\" "
  },
  {
    "path": "lib/client.js",
    "chars": 2359,
    "preview": "// @ts-check\n/// <reference types=\"node\" />\n/// <reference types=\"pg\" />\n\n'use strict';\n\nconst Retry = require('promised"
  },
  {
    "path": "package.json",
    "chars": 2827,
    "preview": "{\n  \"name\": \"pg-pubsub\",\n  \"version\": \"0.8.1\",\n  \"description\": \"A Publish/Subscribe implementation on top of PostgreSQL"
  },
  {
    "path": "renovate.json",
    "chars": 61,
    "preview": "{\n  \"extends\": [\n    \"github>voxpelli/renovate-config\"\n  ]\n}\n"
  },
  {
    "path": "sample.env",
    "chars": 237,
    "preview": "DATABASE_TEST_URL=\"postgres://postgres@localhost/pgpubsub_test\"\nDATABASE_TEST_URL_INVALID_USER=\"postgres://foobar:pass@l"
  },
  {
    "path": "test/.eslintrc",
    "chars": 163,
    "preview": "{\n  \"env\": {\n    \"mocha\": true\n  },\n  \"rules\": {\n    \"no-unused-expressions\": 0,\n    \"node/no-unpublished-require\": 0,\n "
  },
  {
    "path": "test/db-utils.js",
    "chars": 447,
    "preview": "/* eslint-disable n/no-process-env */\n\n// @ts-check\n/// <reference types=\"node\" />\n\n'use strict';\n\nconst pathModule = re"
  },
  {
    "path": "test/integration/main.spec.js",
    "chars": 8636,
    "preview": "// @ts-check\n/// <reference types=\"node\" />\n/// <reference types=\"mocha\" />\n/// <reference types=\"chai\" />\n/// <referenc"
  },
  {
    "path": "tsconfig.json",
    "chars": 143,
    "preview": "{\n  \"extends\": \"@voxpelli/tsconfig/node16.json\",\n  \"files\": [\n    \"index.js\",\n  ],\n  \"include\": [\n    \"lib/**/*.js\",\n   "
  }
]

About this extraction

This page contains the full source code of the voxpelli/node-pg-pubsub GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (25.9 KB), approximately 7.4k tokens, and a symbol index with 9 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!