[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintignore",
    "content": "/coverage/**/*\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"extends\": \"@voxpelli\",\n  \"root\": true\n}\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n  schedule:\n    - cron: '15 0 * * 5'\n\npermissions:\n  actions: read\n  contents: read\n  security-events: write\n\njobs:\n  analyze:\n    uses: voxpelli/ghatemplates/.github/workflows/codeql-analysis.yml@main\n"
  },
  {
    "path": ".github/workflows/dependency-review.yml",
    "content": "name: 'Dependency Review'\n\non: [pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  dependency-review:\n    uses: voxpelli/ghatemplates/.github/workflows/dependency-review.yml@main\n\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Linting\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - '*'\n  pull_request:\n    branches:\n      - main\n\npermissions:\n  contents: read\n\njobs:\n  lint:\n    uses: voxpelli/ghatemplates/.github/workflows/lint.yml@main\n"
  },
  {
    "path": ".github/workflows/nodejs.yml",
    "content": "name: Node CI\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - '*'\n  pull_request:\n    branches:\n      - main\n\npermissions:\n  contents: read\n\njobs:\n  test:\n    uses: voxpelli/ghatemplates/.github/workflows/test-pg.yml@main\n    with:\n      node-versions: '16,18,20'\n      os: 'ubuntu-latest'\n      pg-versions: '9.4,12,13'\n"
  },
  {
    "path": ".github/workflows/types.yml",
    "content": "name: Type Checks\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - '*'\n  pull_request:\n    branches:\n      - main\n  schedule:\n    - cron: '14 5 * * 1,3,5'\n\npermissions:\n  contents: read\n\njobs:\n  type-check:\n    uses: voxpelli/ghatemplates/.github/workflows/type-check.yml@main\n    with:\n      ts-versions: ${{ github.event.schedule && 'next' || '5.0,next' }}\n      ts-libs: 'es2020;esnext'\n"
  },
  {
    "path": ".gitignore",
    "content": "# 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.json\n/yarn.lock\n\n# Generated types\n*.d.ts\n*.d.ts.map\n\n# Library specific ones\n.env\n"
  },
  {
    "path": ".husky/pre-push",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpm test\n"
  },
  {
    "path": ".npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 Pelle Wessman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# PG PubSub\n\nA Publish/Subscribe implementation on top of [PostgreSQL NOTIFY/LISTEN](https://www.postgresql.org/docs/current/sql-notify.html)\n\n[![npm version](https://img.shields.io/npm/v/pg-pubsub.svg?style=flat)](https://www.npmjs.com/package/pg-pubsub)\n[![npm downloads](https://img.shields.io/npm/dm/pg-pubsub.svg?style=flat)](https://www.npmjs.com/package/pg-pubsub)\n[![Module type: CJS](https://img.shields.io/badge/module%20type-cjs-brightgreen)](https://github.com/voxpelli/badges-cjs-esm)\n[![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)](https://github.com/voxpelli/types-in-js)\n[![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg)](https://github.com/voxpelli/eslint-config)\n[![Follow @voxpelli@mastodon.social](https://img.shields.io/mastodon/follow/109247025527949675?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@voxpelli)\n\n## Installation\n\n```bash\nnpm install pg-pubsub --save\n```\n\n## Requirements\n\n* Postgres >= 9.4\n\n## Usage\n\n```js\nconst PGPubsub = require('pg-pubsub');\nconst pubsubInstance = new PGPubsub(uri[, options]);\n```\n\n### Options\n\n```js\n{\n  [log]: Function // default: silent when NODE_ENV=production, otherwise defaults to console.log(...)\n}\n```\n\n### Methods\n\n* **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.\n* **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.\n* **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.\n* **close(): Promise<void>** – closes down the database connection and removes all listeners. Useful for graceful shutdowns.\n* All [EventEmitter methods](http://nodejs.org/api/events.html#events_class_events_eventemitter) are inherited from `EventEmitter`\n\n### Examples\n\n#### Simple\n\n```javascript\nconst pubsubInstance = new PGPubsub('postgres://username@localhost/database');\n\nawait pubsubInstance.addChannel('channelName', function (channelPayload) {\n  // Process the payload – if it was JSON that JSON has been parsed into an object for you\n});\n\nawait pubsubInstance.publish('channelName', { hello: \"world\" });\n```\n\nThe above sends `NOTIFY channelName, '{\"hello\":\"world\"}'` to PostgreSQL, which will trigger the above listener with the parsed JSON in `channelPayload`.\n\n#### Advanced\n\n```javascript\nconst pubsubInstance = new PGPubsub('postgres://username@localhost/database');\n\nawait pubsubInstance.addChannel('channelName');\n\n// pubsubInstance is a full EventEmitter object that sends events on channel names\npubsubInstance.once('channelName', channelPayload => {\n  // Process the payload\n});\n```\n\n## Description\n\nCreating 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.\n\n## Lint / Test\n\n- setup a postgres database to run the integration tests\n  - the easist way to do this is via docker, `docker run -it -p 5432:5432 -e POSTGRES_DB=pgpubsub_test postgres`\n- `npm test`\n\nFor an all-in-one command, try:\n```sh\n# fire up a new DB container, run tests against it, and clean it up!\ndocker rm -f pgpubsub_test || true && \\\ndocker run -itd -p 5432:5432 -e POSTGRES_DB=pgpubsub_test --name pgpubsub_test postgres && \\\nnpm test && \\\ndocker rm -f pgpubsub_test\n```\n\n"
  },
  {
    "path": "declaration.tsconfig.json",
    "content": "\n{\n  \"extends\": \"./tsconfig\",\n  \"exclude\": [\n    \"test/**/*.js\"\n  ],\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": false,\n    \"emitDeclarationOnly\": true\n  }\n}\n"
  },
  {
    "path": "index.js",
    "content": "/* eslint-disable promise/prefer-await-to-then */\n// @ts-check\n/// <reference types=\"node\" />\n/// <reference types=\"pg\" />\n\n'use strict';\n\nconst EventEmitter = require('node:events');\n\nconst pgFormat = require('pg-format');\nconst { ErrorWithCause } = require('pony-cause');\n\nconst { pgClientRetry } = require('./lib/client');\n\n// TODO: Move to an async generator approach rather than EventEmitter\n\n/** @typedef {(payload: any) => void} PGPubsubCallback */\n\nclass PGPubsub extends EventEmitter {\n  /** @type {string[]} */\n  #channels = [];\n\n  /** @type {import('promised-retry')} */\n  #retry;\n\n  /**\n   * @param {string | import('pg').ClientConfig} [conString]\n   * @param {{ log?: typeof console.log, retryLimit?: number }} options\n   */\n  // eslint-disable-next-line n/no-process-env\n  constructor (conString = process.env['DATABASE_URL'], { log, retryLimit } = {}) {\n    super();\n\n    this.setMaxListeners(0);\n\n    this.#retry = pgClientRetry({\n      clientOptions: typeof conString === 'object' ? conString : { connectionString: conString },\n      retryLimit,\n      log,\n      shouldReconnect: () => this.#channels.length !== 0,\n      successCallback: client => {\n        client.on('notification', msg => this.#processNotification(msg));\n\n        Promise.all(this.#channels.map(channel => client.query('LISTEN \"' + channel + '\"')))\n          .catch(/** @param {unknown} err */err => {\n            this.emit(\n              'error',\n              new ErrorWithCause('Failed to set up channels on new connection', { cause: err })\n            );\n          });\n\n        return client;\n      },\n    });\n  }\n\n  /**\n   * @protected\n   * @param {boolean} [noNewConnections]\n   * @returns {Promise<import('pg').Client>}\n   */\n  async _getDB (noNewConnections) {\n    return this.#retry.try(!noNewConnections)\n      .catch(/** @param {unknown} err */err => {\n        throw new ErrorWithCause('Failed to establish database connection', { cause: err });\n      });\n  }\n\n  /**\n   * @param {import('pg').Notification} msg\n   * @returns {void}\n   */\n  #processNotification (msg) {\n    let payload = msg.payload || '';\n\n    // If the payload is valid JSON, then replace it with such\n    try { payload = JSON.parse(payload); } catch {}\n\n    this.emit(msg.channel, payload);\n  }\n\n  /**\n   * @param {string} channel\n   * @param {PGPubsubCallback} [callback]\n   * @returns {Promise<void>}\n   */\n  async addChannel (channel, callback) {\n    if (!this.#channels.includes(channel)) {\n      this.#channels.push(channel);\n\n      // TODO: Can't this possibly result in both the try() method and this method adding a LISTEN for it?\n      try {\n        const db = await this._getDB();\n        await db.query('LISTEN \"' + channel + '\"');\n      } catch (err) {\n        throw new ErrorWithCause('Failed to listen to channel', { cause: err });\n      }\n    }\n\n    if (callback) {\n      this.on(channel, callback);\n    }\n  }\n\n  /**\n   * @param {string} channel\n   * @param {PGPubsubCallback} [callback]\n   * @returns {this}\n   */\n  removeChannel (channel, callback) {\n    const pos = this.#channels.indexOf(channel);\n\n    if (pos === -1) {\n      return this;\n    }\n\n    if (callback) {\n      this.removeListener(channel, callback);\n    } else {\n      this.removeAllListeners(channel);\n    }\n\n    if (this.listeners(channel).length === 0) {\n      this.#channels.splice(pos, 1);\n      this._getDB(true)\n        .then(db => db.query('UNLISTEN \"' + channel + '\"'))\n        .catch(/** @param {unknown} err */err => {\n          this.emit(\n            'error',\n            new ErrorWithCause('Failed to stop listening to channel', { cause: err })\n          );\n        });\n    }\n\n    return this;\n  }\n\n  /**\n   * @param {string} channel\n   * @param {any} [data]\n   * @returns {Promise<void>}\n   */\n  async publish (channel, data) {\n    const payload = data ? ', ' + pgFormat.literal(JSON.stringify(data)) : '';\n\n    try {\n      const db = await this._getDB();\n      await db.query(`NOTIFY \"${channel}\"${payload}`);\n    } catch (err) {\n      throw new ErrorWithCause('Failed to publish to channel', { cause: err });\n    }\n  }\n\n  /** @returns {Promise<void>} */\n  async close () {\n    this.removeAllListeners();\n    this.#channels = [];\n    return this.#retry.end();\n  }\n\n  reset () {\n    return this.#retry.reset();\n  }\n}\n\nmodule.exports = PGPubsub;\n"
  },
  {
    "path": "lib/client.js",
    "content": "// @ts-check\n/// <reference types=\"node\" />\n/// <reference types=\"pg\" />\n\n'use strict';\n\nconst Retry = require('promised-retry');\n\nconst { Client } = require('pg');\n\n/** @returns {typeof console.log} */\nconst getDefaultLog = () =>\n  // eslint-disable-next-line n/no-process-env\n  process.env['NODE_ENV'] === 'production'\n    ? () => {}\n    // eslint-disable-next-line no-console\n    : console.log.bind(console);\n\n/**\n * @typedef PgClientRetryOptions\n * @property {string|import('pg').ClientConfig} clientOptions\n * @property {(typeof console.log)|undefined} [log]\n * @property {number|undefined} [retryLimit]\n * @property {(client: import('pg').Client) => Promise<import('pg').Client>|import('pg').Client} [successCallback]\n * @property {() => boolean} [shouldReconnect]\n */\n\n/**\n * @param {PgClientRetryOptions} options\n * @returns {import('promised-retry')}\n */\nconst pgClientRetry = (options) => {\n  const {\n    clientOptions,\n    log = getDefaultLog(),\n    retryLimit,\n    shouldReconnect,\n    successCallback,\n  } = options;\n\n  const retry = new Retry({\n    // TODO: Improve types for this in promised-retry\n    'try': async () => {\n      const client = new Client(clientOptions);\n\n      // TODO: Add client.on('end') ?\n      // If the connection fail after we have established it, then we need to reset the state of our retry mechanism and restart from scratch.\n      client.on('error', () => {\n        retry.reset();\n        if (shouldReconnect && shouldReconnect()) retry.try();\n        client.end(err => {\n          log('Received error when disconnecting from database in error callback: ' + (err && err.message));\n        });\n      });\n\n      // Do the connect\n      await client.connect();\n\n      return client;\n    },\n    // TODO: Improve types for this in promised-retry, what should actually be returned?\n    success:\n      /**\n       * @param {import('pg').Client} client\n       * @returns {Promise<import('pg').Client>}\n       */\n      async client => successCallback ? successCallback(client) : client,\n    // TODO: Improve types for this in promised-retry\n    end:\n      /**\n       * @param {import('pg').Client} [client]\n       * @returns {Promise<void>}\n       */\n      async client => client ? client.end() : undefined,\n    name: 'pgClientRetry',\n    retryLimit,\n    log,\n  });\n\n  return retry;\n};\n\nmodule.exports = {\n  pgClientRetry,\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"pg-pubsub\",\n  \"version\": \"0.8.1\",\n  \"description\": \"A Publish/Subscribe implementation on top of PostgreSQL NOTIFY/LISTEN\",\n  \"homepage\": \"http://github.com/voxpelli/node-pg-pubsub\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git://github.com/voxpelli/node-pg-pubsub.git\"\n  },\n  \"author\": {\n    \"name\": \"Pelle Wessman\",\n    \"email\": \"pelle@kodfabrik.se\",\n    \"url\": \"http://kodfabrik.se/\"\n  },\n  \"license\": \"MIT\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"main\": \"index.js\",\n  \"types\": \"index.d.ts\",\n  \"files\": [\n    \"index.js\",\n    \"index.d.ts\",\n    \"index.d.ts.map\",\n    \"lib/*.js\",\n    \"lib/*.d.ts\",\n    \"lib/*.d.ts.map\"\n  ],\n  \"scripts\": {\n    \"build:0\": \"run-s clean\",\n    \"build:1-declaration\": \"tsc -p declaration.tsconfig.json\",\n    \"build:2-add-ignores\": \"ts-ignore-import '**/*.d.ts'\",\n    \"build\": \"run-s build:*\",\n    \"check:dependency-check\": \"dependency-check *.js 'test/**/*.js' --no-dev\",\n    \"check:installed-check\": \"installed-check -i eslint-plugin-jsdoc\",\n    \"check:lint\": \"eslint --report-unused-disable-directives .\",\n    \"check:tsc\": \"tsc\",\n    \"check:type-coverage\": \"type-coverage --detail --strict --at-least 98 --ignore-files 'test/**/*'\",\n    \"check\": \"run-s clean && run-p check:*\",\n    \"clean:declarations\": \"rm -rf $(find . -maxdepth 2 -type f -name '*.d.ts*')\",\n    \"clean\": \"run-p clean:*\",\n    \"prepublishOnly\": \"run-s build\",\n    \"test:mocha\": \"cross-env NODE_ENV=test c8 --reporter=lcov --reporter text mocha test/**/*.spec.js\",\n    \"test-ci\": \"run-s test:*\",\n    \"test\": \"run-s check test:*\",\n    \"prepare\": \"husky install\"\n  },\n  \"dependencies\": {\n    \"pg\": \"^8.7.3\",\n    \"pg-format\": \"^1.0.2\",\n    \"pony-cause\": \"^2.1.10\",\n    \"promised-retry\": \"^0.5.0\"\n  },\n  \"devDependencies\": {\n    \"@hdsydsvenskan/ts-ignore-import\": \"^2.0.0\",\n    \"@types/chai\": \"^4.3.5\",\n    \"@types/chai-as-promised\": \"^7.1.5\",\n    \"@types/mocha\": \"^10.0.1\",\n    \"@types/node\": \"^16.18.28\",\n    \"@types/pg\": \"^8.6.6\",\n    \"@types/pg-format\": \"^1.0.2\",\n    \"@voxpelli/eslint-config\": \"^16.0.7\",\n    \"@voxpelli/tsconfig\": \"^7.0.0\",\n    \"c8\": \"^7.13.0\",\n    \"chai\": \"^4.3.7\",\n    \"chai-as-promised\": \"^7.1.1\",\n    \"cross-env\": \"^7.0.3\",\n    \"dependency-check\": \"^5.0.0-7\",\n    \"dotenv\": \"^16.0.3\",\n    \"eslint\": \"^8.40.0\",\n    \"eslint-config-standard\": \"^17.0.0\",\n    \"eslint-plugin-es\": \"^4.1.0\",\n    \"eslint-plugin-import\": \"^2.27.5\",\n    \"eslint-plugin-jsdoc\": \"^44.2.2\",\n    \"eslint-plugin-mocha\": \"^10.1.0\",\n    \"eslint-plugin-n\": \"^15.7.0\",\n    \"eslint-plugin-promise\": \"^6.1.1\",\n    \"eslint-plugin-security\": \"^1.7.1\",\n    \"eslint-plugin-sort-destructure-keys\": \"^1.5.0\",\n    \"eslint-plugin-unicorn\": \"^47.0.0\",\n    \"husky\": \"^8.0.3\",\n    \"installed-check\": \"^7.0.0\",\n    \"mocha\": \"^10.2.0\",\n    \"npm-run-all2\": \"^6.0.5\",\n    \"type-coverage\": \"^2.25.3\",\n    \"typescript\": \"~5.0.4\"\n  }\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"extends\": [\n    \"github>voxpelli/renovate-config\"\n  ]\n}\n"
  },
  {
    "path": "sample.env",
    "content": "DATABASE_TEST_URL=\"postgres://postgres@localhost/pgpubsub_test\"\nDATABASE_TEST_URL_INVALID_USER=\"postgres://foobar:pass@localhost/pgpubsub_test\"\nDATABASE_TEST_URL_INVALID_PASSWORD=\"postgres://postgres:invalidpass@localhost/pgpubsub_test\"\n"
  },
  {
    "path": "test/.eslintrc",
    "content": "{\n  \"env\": {\n    \"mocha\": true\n  },\n  \"rules\": {\n    \"no-unused-expressions\": 0,\n    \"node/no-unpublished-require\": 0,\n    \"promise/prefer-await-to-then\": 0\n  }\n}\n"
  },
  {
    "path": "test/db-utils.js",
    "content": "/* eslint-disable n/no-process-env */\n\n// @ts-check\n/// <reference types=\"node\" />\n\n'use strict';\n\nconst pathModule = require('node:path');\n\nconst dotEnvFile = process.env['DOTENV_FILE'] || pathModule.resolve(__dirname, './.env');\n\nrequire('dotenv').config({ path: dotEnvFile });\n\nconst connectionDetails = process.env['DATABASE_TEST_URL'] || {\n  database: process.env['PGDATABASE'] || 'pgpubsub_test',\n};\n\nmodule.exports = { connectionDetails };\n"
  },
  {
    "path": "test/integration/main.spec.js",
    "content": "// @ts-check\n/// <reference types=\"node\" />\n/// <reference types=\"mocha\" />\n/// <reference types=\"chai\" />\n/// <reference types=\"chai-as-promised\" />\n\n'use strict';\n\nconst chai = require('chai');\nconst chaiAsPromised = require('chai-as-promised');\n\nconst { connectionDetails } = require('../db-utils');\nconst PGPubsub = require('../../');\n\nchai.use(chaiAsPromised);\nchai.should();\n\n// @ts-ignore\n// eslint-disable-next-line no-console\nprocess.on('unhandledRejection', err => { console.log('Unhandled Rejection:', err.stack); });\n\n// eslint-disable-next-line n/no-process-env\nconst conStringInvalidUser = process.env['DATABASE_TEST_URL_INVALID_USER'] || 'postgres://invalidUsername@localhost/pgpubsub_test';\n// eslint-disable-next-line n/no-process-env\nconst conStringInvalidPassword = process.env['DATABASE_TEST_URL_INVALID_PASSWORD'] || 'postgres://postgres:invalid@localhost/pgpubsub_test';\n\n/**\n * @template T\n * @returns {[Promise<T>, (value?: T | PromiseLike<T>) => void, (err: Error) => void]}\n */\nconst resolveablePromise = () => {\n  /** @type {(value?: T | PromiseLike<T>) => void} */\n  let resolver;\n  /** @type {(err: Error) => void} */\n  let rejecter;\n\n  const resolveable = new Promise((resolve, reject) => {\n    resolver = resolve;\n    rejecter = reject;\n  });\n\n  // @ts-ignore\n  return [resolveable, resolver, rejecter];\n};\n\ndescribe('Pubsub', () => {\n  /** @type {import('../../index')} */\n  let pubsubInstance;\n  /** @type {import('pg').Client} */\n  let db;\n\n  beforeEach(async () => {\n    pubsubInstance = new PGPubsub(connectionDetails, {\n      log: function (...params) {\n        if (typeof arguments[0] !== 'string' || !arguments[0].startsWith('Success')) {\n          // eslint-disable-next-line no-console\n          console.log.call(this, ...params);\n        }\n      },\n    });\n\n    // @ts-ignore\n    db = await pubsubInstance._getDB();\n  });\n\n  afterEach(() => pubsubInstance.close());\n\n  describe('init', function () {\n    this.timeout(2000);\n\n    it('should handle errenous database user', async () => {\n      pubsubInstance.close();\n      pubsubInstance = new PGPubsub(conStringInvalidUser, {\n        log: () => {},\n        retryLimit: 1,\n      });\n      // @ts-ignore\n      return pubsubInstance._getDB()\n        .should.be.rejectedWith(/Failed to establish database connection/);\n    });\n\n    // TODO: Fix, doesn't work on Travis right now\n    it.skip('should handle errenous database password', async () => {\n      pubsubInstance.close();\n      pubsubInstance = new PGPubsub(conStringInvalidPassword, {\n        log: () => {},\n        retryLimit: 1,\n      });\n      // @ts-ignore\n      return pubsubInstance._getDB()\n        .should.be.rejectedWith(/Failed to establish database connection/);\n    });\n  });\n\n  describe('receive', function () {\n    it('should receive a notification', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      await pubsubInstance.addChannel('foobar', (channelPayload) => {\n        channelPayload.should.deep.equal({ abc: 123 });\n        resolve();\n      });\n\n      await db.query('NOTIFY foobar, \\'{\"abc\":123}\\'');\n\n      return result;\n    });\n\n    it('should handle non-JSON notifications', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      await pubsubInstance.addChannel('foobar', channelPayload => {\n        channelPayload.should.equal('barfoo');\n        resolve();\n      });\n      await db.query('NOTIFY foobar, \\'barfoo\\'');\n\n      return result;\n    });\n\n    it('should only receive notifications from correct channel', async () => {\n      const [result1, resolve1] = resolveablePromise();\n      const [result2, resolve2] = resolveablePromise();\n\n      await pubsubInstance.addChannel('foo', channelPayload => {\n        channelPayload.should.deep.equal({ abc: 123 });\n        resolve1();\n      });\n\n      await pubsubInstance.addChannel('bar', channelPayload => {\n        channelPayload.should.deep.equal({ xyz: 789 });\n        resolve2();\n      });\n\n      await Promise.all([\n        db.query('NOTIFY def, \\'{\"ghi\":456}\\''),\n        db.query('NOTIFY foo, \\'{\"abc\":123}\\''),\n        db.query('NOTIFY bar, \\'{\"xyz\":789}\\''),\n      ]);\n\n      await Promise.all([\n        result1,\n        result2,\n      ]);\n    });\n\n    it('should handle non-alphanumeric channel names', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      await pubsubInstance.addChannel('97a38cd1-d332-4240-93e4-1ff436a7da2a', function (channelPayload) {\n        channelPayload.should.deep.equal({ 'non-alpha': true });\n        resolve();\n      });\n\n      await db.query('NOTIFY \"97a38cd1-d332-4240-93e4-1ff436a7da2a\", \\'{\"non-alpha\":true}\\'');\n\n      return result;\n    });\n\n    it('should stop listening when channel is removed', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      await pubsubInstance.addChannel('foo', function () {\n        throw new Error('This channel should have been removed and should not receive any items');\n      });\n\n      await pubsubInstance.addChannel('foo', function () {\n        throw new Error('This channel should have been removed and should not receive any items');\n      });\n\n      await pubsubInstance.addChannel('bar', function () {\n        resolve();\n      });\n\n      pubsubInstance.removeChannel('foo');\n\n      await db.query('NOTIFY foo, \\'{\"abc\":123}\\'');\n      await db.query('NOTIFY bar, \\'{\"xyz\":789}\\'');\n\n      return result;\n    });\n\n    it('should allow multiple listener for same channel', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      let first = false;\n\n      await pubsubInstance.addChannel('foobar', function () {\n        first = true;\n      });\n      await pubsubInstance.addChannel('foobar', function () {\n        first.should.be.ok;\n        resolve();\n      });\n\n      await db.query('NOTIFY foobar, \\'{\"abc\":123}\\'');\n\n      return result;\n    });\n\n    it('should be able to remove specific listener', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      let second = false;\n\n      // eslint-disable-next-line unicorn/consistent-function-scoping\n      const listener = function () {\n        throw new Error('This channel should have been removed and should not receive any items');\n      };\n\n      await pubsubInstance.addChannel('foobar', listener);\n\n      await pubsubInstance.addChannel('foobar', function () {\n        if (second) {\n          resolve();\n        } else {\n          second = true;\n        }\n      });\n\n      pubsubInstance.removeChannel('foobar', listener);\n\n      await db.query('NOTIFY foobar, \\'{\"abc\":123}\\'');\n      await db.query('NOTIFY foobar, \\'{\"abc\":123}\\'');\n\n      return result;\n    });\n\n    it('should support EventEmitter methods for listening', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      await pubsubInstance.addChannel('foobar');\n\n      pubsubInstance.on('foobar', function () {\n        resolve();\n      });\n\n      await db.query('NOTIFY foobar, \\'{\"abc\":123}\\'');\n\n      return result;\n    });\n\n    it('should support recovery after reconnect', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      await pubsubInstance.addChannel('foobar', function () {\n        resolve();\n      });\n\n      setImmediate(() => {\n        db.end();\n        pubsubInstance.reset();\n\n        // @ts-ignore\n        // eslint-disable-next-line promise/always-return, promise/catch-or-return\n        pubsubInstance._getDB().then(async db => {\n          await db.query('NOTIFY foobar, \\'{\"abc\":123}\\'');\n        });\n      });\n\n      return result;\n    });\n  });\n\n  describe('publish', function () {\n    it('should publish a notification', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      const data = { abc: 123 };\n\n      await pubsubInstance.addChannel('foobar', function (channelPayload) {\n        channelPayload.should.deep.equal(data);\n        resolve();\n      });\n\n      await pubsubInstance.publish('foobar', data);\n\n      return result;\n    });\n\n    it('should not be vulnerable to SQL injection', async () => {\n      const [result, resolve] = resolveablePromise();\n\n      const data = { abc: '\\'\"; AND DO SOMETHING BAD' };\n\n      await pubsubInstance.addChannel('foobar', function (channelPayload) {\n        channelPayload.should.deep.equal(data);\n        resolve();\n      });\n\n      await pubsubInstance.publish('foobar', data);\n\n      return result;\n    });\n\n    it('should gracefully handle too large payloads', async () => {\n      const data = Array.from({ length: 10000 });\n      data.fill('a');\n      return pubsubInstance.publish('foobar', data).should.be.rejectedWith(Error);\n    });\n  });\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@voxpelli/tsconfig/node16.json\",\n  \"files\": [\n    \"index.js\",\n  ],\n  \"include\": [\n    \"lib/**/*.js\",\n    \"test/**/*.js\",\n  ]\n}\n"
  }
]