[
  {
    "path": ".dockerignore",
    "content": "**/.git/\n**/.github/\n**/node_modules/\n\n**/Dockerfile\n**/Dockerfile.*\n\n**/.env.*.local\n**/.env.local"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Server version**\n\n**Protocol (binary or text)**\n\n**Websocket server (node-http or uws)**\n\n**Server OS**\n\n**Node version**\n\n**Server config**\nPlease share the config sections that might be relevant for this issue\n\n**Client (Js, Java, other) and client version**\n\n**Client config**\nPlease share the config sections that might be relevant for this issue\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/workflows/lint-test.yml",
    "content": "name: lint-and-test\n\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v3\n      with:\n        node-version: '22.x'\n    - run: npm install\n    - run: npm run lint\n    - run: npm run test:all:coverage\n    - run: npm run e2e:uws\n    - name: Coveralls\n      if: startsWith(matrix.node-version, '22.')\n      uses: coverallsapp/github-action@master\n      with:\n        github-token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  push:\n    # Sequence of patterns matched against refs/tags\n    tags:\n      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10\n\njobs:\n  create_release:\n    name: Create release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Create release\n        id: create_release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: Release ${{ github.ref }}\n\n  npm_publish:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n    # Setup .npmrc file to publish to npm\n    - uses: actions/setup-node@v3\n      with:\n        node-version: '22.x'\n        registry-url: 'https://registry.npmjs.org'\n    - run: npm install\n    - run: npm run lint\n    - run: npm run test\n    - run: npm run tsc\n    - run: npm publish --access public\n      env:\n        NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n  linux:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout reposistory\n      uses: actions/checkout@v3\n    - name: Checkout submodules\n      run: git submodule update --init --recursive\n    - name: Use Node.js\n      env:\n        DEFAULT_DELAY: 50\n      uses: actions/setup-node@v3\n      with:\n        node-version: '22.x'\n    - run: npm install\n    - run: npm run lint\n    - run: npm run test\n    - run: bash ./scripts/package.sh true true\n    - name: Upload Release Asset\n      uses: alexellis/upload-assets@0.2.2\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        asset_paths: '[\"build/*/*.tar.gz\"]'\n\n  windows:\n    runs-on: windows-latest\n    steps:\n    - name: Checkout reposistory\n      uses: actions/checkout@v3\n    - name: Checkout submodules\n      run: git submodule update --init --recursive\n    - name: Use Node.js\n      env:\n        DEFAULT_DELAY: 50\n      uses: actions/setup-node@v3\n      with:\n        node-version: '22.x'\n    - run: npm install\n    - run: npm run lint\n    - run: npm run test\n    - run: bash ./scripts/package.sh true true\n    - name: Upload Release Asset\n      uses: alexellis/upload-assets@0.2.2\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        asset_paths: '[\"build/*/*.zip\"]'\n\n  macos:\n    runs-on: macos-latest\n    steps:\n    - name: Checkout reposistory\n      uses: actions/checkout@v3\n    - name: Checkout submodules\n      run: git submodule update --init --recursive\n    - name: Use Node.js\n      env:\n        DEFAULT_DELAY: 50\n      uses: actions/setup-node@v3\n      with:\n        node-version: '22.x'\n    - run: npm install\n    - run: npm run lint\n    - run: npm run test\n    - run: bash ./scripts/package.sh true true\n    - name: Upload Release Asset\n      uses: alexellis/upload-assets@0.2.2\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        asset_paths: '[\"build/*/*.pkg\"]'\n\n  docker-amd:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n    - name: Log in to Docker Hub\n      uses: docker/login-action@v2\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n    - name: Extract metadata (tags, labels) for Docker\n      id: meta\n      uses: docker/metadata-action@v4\n      with:\n        images: deepstreamio/deepstream.io\n        flavor: |\n          latest=true\n        tags: |\n          type=semver,pattern={{version}}\n    - name: Build and push Docker image\n      uses: docker/build-push-action@v4\n      with:\n        context: .\n        file: Dockerfile\n        push: true\n        platforms: linux/amd64\n        tags: ${{ steps.meta.outputs.tags }}\n        labels: ${{ steps.meta.outputs.labels }}\n\n  docker-arm:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: docker/setup-qemu-action@v1\n    - uses: docker/setup-buildx-action@v1\n    - uses: actions/checkout@v3\n    - name: Log in to Docker Hub\n      uses: docker/login-action@v2\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n    - name: Extract metadata (tags, labels) for Docker\n      id: meta\n      uses: docker/metadata-action@v4\n      with:\n        images: deepstreamio/deepstream.io\n        flavor: |\n          latest=true\n        tags: |\n          type=semver,pattern={{version}}\n    - name: Build and push Docker image\n      uses: docker/build-push-action@v4\n      with:\n        context: .\n        file: Dockerfile\n        push: true\n        platforms: linux/arm64\n        tags: ${{ steps.meta.outputs.tags }}\n        labels: ${{ steps.meta.outputs.labels }}\n\n  docker-alpine:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: docker/setup-qemu-action@v1\n    - uses: docker/setup-buildx-action@v1\n    - uses: actions/checkout@v3\n    - name: Log in to Docker Hub\n      uses: docker/login-action@v2\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n    - name: Extract metadata (tags, labels) for Docker\n      id: meta\n      uses: docker/metadata-action@v4\n      with:\n        images: deepstreamio/deepstream.io\n        flavor: |\n          latest=true\n          suffix=-alpine,onlatest=true\n        tags: |\n          type=semver,pattern={{version}}\n    - name: Build and push Docker image\n      uses: docker/build-push-action@v4\n      with:\n        context: .\n        file: Dockerfile.alpine\n        push: true\n        platforms: linux/amd64,linux/arm64\n        tags: ${{ steps.meta.outputs.tags }}\n        labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".gitignore",
    "content": "heap-snapshots\n.nyc_combined_coverage\ndist\n.DS_Store\n.vscode/settings.json\n.nyc_output\ntemp-e2e-test\nlocal-storage\n\n# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# pkg\nbuild\nmeta.json\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\n# Commenting this out is preferred by some people, see\n# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-\nnode_modules\nnpm-shrinkwrap.json\nnpm-debug.*\n\n# Users Environment Variables\n.lock-wscript\n\n# IDE configuration\n.idea\n\nlicenses.json\n\n# Typescript\ndist\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"test-e2e/features\"]\n\tpath = test-e2e/features\n\turl = https://github.com/deepstreamIO/deepstream.io-e2e.git\n[submodule \"connectors/cache/redis\"]\n\tpath = connectors/cache/redis\n\turl = https://github.com/deepstreamIO/deepstream.io-cache-redis.git\n[submodule \"connectors/cache/memcached\"]\n\tpath = connectors/cache/memcached\n\turl = https://github.com/deepstreamIO/deepstream.io-cache-memcached.git\n[submodule \"connectors/cache/hazelcast\"]\n\tpath = connectors/cache/hazelcast\n\turl = https://github.com/deepstreamIO/deepstream.io-cache-hazelcast.git\n[submodule \"connectors/storage/postgres\"]\n\tpath = connectors/storage/postgres\n\turl = https://github.com/deepstreamIO/deepstream.io-storage-postgres.git\n[submodule \"connectors/storage/rethinkdb\"]\n\tpath = connectors/storage/rethinkdb\n\turl = https://github.com/deepstreamIO/deepstream.io-storage-rethinkdb.git\n[submodule \"connectors/storage/mongodb\"]\n\tpath = connectors/storage/mongodb\n\turl = https://github.com/deepstreamIO/deepstream.io-storage-mongodb.git\n[submodule \"connectors/storage/elasticsearch\"]\n\tpath = connectors/storage/elasticsearch\n\turl = https://github.com/deepstreamIO/deepstream.io-storage-elasticsearch.git\n[submodule \"client\"]\n\tpath = client\n\turl = https://github.com/deepstreamIO/deepstream.io-client-js.git\n[submodule \"connectors/clusterNode/redis\"]\n\tpath = connectors/clusterNode/redis\n\turl = https://github.com/deepstreamIO/deepstream.io-clusternode-redis.git\n[submodule \"connectors/logger/winston\"]\n\tpath = connectors/logger/winston\n\turl = https://github.com/deepstreamIO/deepstream.io-logger-winston.git\n[submodule \"plugins/aws\"]\n\tpath = plugins/aws\n\turl = https://github.com/deepstreamIO/deepstream.io-plugin-aws.git\n"
  },
  {
    "path": ".npmignore",
    "content": "# Typescript files\n/scripts\n/bin\n.tsconfig.json\n.tslint.json\n\n# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# pkg\nbuild\nmeta.json\n\n# Users Environment Variables\n.lock-wscript\n\n# IDE configuration\n.idea\n\n# CI\n.travis.yml\nappveyor.yml\n\n# testing\ntest\ntest-e2e\nbenchmarks\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## [10.0.0] - 2025.08.02\n\n### Fix - BREAKING CHANGE\n\nThe storage authentication service, when creating new users automatically, was encoding the password hashes in 'ascii' format which can create characters that are not allowed in a database text field. This has been fixed by encoding the password hashes in 'base64'. Since this is a crippling bug and no one noticed before, I'm going to assume the feature is not being used in production and therefore a new non-backwards compatible version will be released.\n\n### Chore\n\n## [9.1.3] - 2025.07.04\n\n### Fix\n\nHandle Erase messages as Delete messages in valve permissions\n\n## [9.1.2] - 2025.06.30\n\n### Fix\n\nLog metadata\n\n## [9.1.1] - 2025.06.18\n\n### Fix - lint\n\n## [9.1.0] - 2025.06.18\n\n### Fix - breaking change\n\nwhen using uws http server response.writeStatus must be called before any other method, otherwise all response status are 200.\nI decided not to make a major release because if somebody else was using it a bug report would have been made.\nAnd thanks to a LLM's for the quick refactoring!\n\n## [9.0.1] - 2025.06.17\n\n### Feature\n\nEnable pino logger options thus making it possible to use transports\n\n## [9.0.0] - 2025.06.13\n\n### Fix - Breaking change\n\n- The log level namig logic was not being implemented properly. logLevels as String where not converted to numbers, therefore those levels where not being properly enforced. The simplest path is just to use logLevels as numbers everywhere. This is a breaking change because config files with log levels as string will error at the config validation stage.\n\n### Chore\n\n- Update deps\n\n## [8.0.0] - 2025.04.26\n\n### Chore\n\n- Update nodejs to version 22.x\n- Update uWebsockets\n- Update deps\n\n## [7.0.10] - 2024.03.06\n\n### Task\n\n- Separate docker ubuntu amd and arm releases due to intermitent build failures in arm. See https://github.com/nodejs/docker-node/issues/1335\n\n## [7.0.9] - 2024.03.04\n\n### Fix\n\n- http auth issue #1138\n\n## [7.0.8] - 2023.10.23\n\n### Task\n\n- multi arch docker image builds. Thanks @daanh432\n\n### Chore\n\n- update deps. Thanks @hugojosefson\n\n## [7.0.7] - 2023.07.20\n\n### Fix\n\n- Cork uWebsocketjs http responses\n\n## [7.0.6] - 2023.07.19\n\n### Task\n\n- update uWebsocketjs\n\n## [7.0.5] - 2023.03.24\n\n### Fix\n\n- use cli/config file options for cluster\n\n## [7.0.4] - 2023.03.08\n\n### Feature\n\n- deepstream cluster CLI enabled in order to run a cluster of deepstream servers on each available processor core\n- combine monitoring: now deepstream accepts an array of monitoring plugins in order to have separate plugins when it comes to monitoring messages, server activity and other custom functionality that might be required. For example you can run the included log monitoring, and an audit plugin that could save to another system/server which users are writing to which records by listening to incoming messages. This allows to create inmediate database replication strategies and so forth.\n\n## [7.0.3] - 2023.03.02\n\n### Fix\n\n- set pkg entrypoint for daemon\n\n## [7.0.2] - 2023.02.27\n\n### Fix\n\n- log monitoring\n- daemon issues due to pkg bug\n\n## [7.0.1] - 2023.02.03\n\n### Fix\n\n- pkg issues\n\n## [7.0.0] - 2023.02.03\n\n### Chore\n\n- Update nodejs to version 18.x\n- Update uWebsockets\n- Update deps\n\n## [6.2.2] - 2022.12.22\n\n### Fix\n\n- Enforce http origins options\n- log a warning when a websocket message is not sent\n- uws server add maxBackpressure option, defaults to 1024*1024\n\n## [6.2.1] - 2022.06.20\n\n### Task\n\n- update deps\n\n## [6.2.0] - 2022.06.05\n\n### Task\n\n- eliminate externalUrl from config and cluster messages since it was not used\n\n- set `provideRequestorName` and `provideRequestorData` as false by default to avoid overhead since from now on that data can actually be sent on rpc messages and handled on @deepstream/client > 6.0.2.\n\n### Misc\n\n- update submodules and deps\n\n## [6.1.2] - 2022.04.22\n\n### Fix\n\n- Revert to lockfileVersion 1\n\n## [6.1.1] - 2022.04.22\n\n### Fix\n\n- Check maxMessageSize limit on POST requests when using uws http server\n\n### Chore\n\n- update @deepstream/client dev dependency and update e2e tests\n\n## [6.1.0] - 2022.03.17\n\n### Fix\n\n- Send write ack error messages when cache, storage or permission fails. For this to work it requires client version >= 6\n- Do not remove cluster nodes if cluster size is 1\n\n### Feature\n\n- Enable vertical cluster node option:\nNow we can use all available cores in a vertical cluster! By setting the cluster option to `clusterNode: { name: 'vertical' }` you can run as many deepstream instances you want in each of the server cores and it will be one syncronized cluster.\n\n## [6.0.1] - 2022.01.02\n\n### Task\n\n- Check that executable works in pre-push hook\n- Use pkg instead of nexe\n\n## [6.0.0] - 2021.12.12\n\n### Chore\n\n- Update nodejs to version 14.x\n- Update uWebsockets\n\n## [5.2.6] - 2021.11.27\n\n### Misc\n\n- Update dependencies\n\n## [5.2.5] - 2021.11.27\n\n### Misc\n\n- Update dependencies\n\n## [5.2.4] - 2021.04.05\n\n### Misc\n\n- Disable telemetry\n\n## [5.2.3] - 2021.03.09\n\n### Feature\n\n- enable combine authentication. Now when the auth config has more than one authentication strategy the server will query them in order untill one passes or all fail.\n\n## [5.2.2] - 2021.03.01\n\n### Feature\n\n- enable querying for specific user presence on http endpoint\n\n\n## [5.2.1] - 2021.02.24\n\n### Fixes\n\n- uws idleTimeout is in seconds! That's why it didn't closed the connection on time.\n- buffer ack messages\n- remove noDelay from default-options and use it on subscription registry as a param to enable/disable buffering\n\n\n## [5.2.0] - 2021.02.17\n\n### Task\n\n- Github Actions CI/CD\n\n## [5.1.8] - 2020.11.25\n\n### Misc\n\n- Updating uws to support apple silicon\n\n### Fix\n\n- Increasing CI heartbeat timeouts\n\n\n## [5.1.7] - 2020.11.25\n\n### Fix\n\n- Calling destroy on socketWrapper instead of close, since close is a reaction and destroy an action\n\n## [5.1.6] - 2020.11.24\n\n### Fix\n\n- Fix #1091 heartbeat not working with node-http/ws\n\nThis fix now adds a timestamps to every message frame recieved and sets up an interval\nper socket to check heartbeat exists. Didn't do any performance tests but I'm assuming having a single interval is cheaper than setting a timeout and canceling on each message.\n\nThis fix also exposes that we don't serialize STATE_TOPIC_REGISTRY/NOT_SUBSCRIBED message correctly which throws an error and crashes the server. That fix will require more elbow grease and hasn't been reported so will just keep an eye on issues.\n\n## [5.1.5] - 2020.10.29\n\n### Fix\n\n- Fix #1089 install service gives error\n\n### Misc\n\n- Updating dependencies\n\n## [5.1.4] - 2020.10.16\n\n### Fix\n\n- Fixed bug causing the server to crash with older sdks.\n\n## [5.1.3] - 2020.08.08\n\n### Fix\n\n- Use upgrade property for uWebSockets http server.\n\n### Misc\n\n- Do not run e2e:v3 test on the pre-push hook\n\n## [5.1.2] - 2020.07.05\n\n### Misc\n\n- Updating all dependencies\n\n## [5.1.1] - 2020.05.11\n\n### Fix\n\nDisabling telemetry for tests\n\nFixing critical bug with client sdk version tracking\n\n## [5.1.0] - 2020.05.11\n\n### Feat\n\nAdding telemetry. The server code is also in the rep (under telemetry-server).\n\nThis uses a random uuid as your deploymentId, which is pretty much the only way\nI can avoid having thousands of records in the database from one machine restarting / ci process.\n\nIf possible please use a different ID for production environments!\n\n```yaml\n# This disables specific feature in DS, which is a more performant way\n# than disabling via permissions and is also how telemetry figures out\n# what features are enabled\nenabledFeatures:\n  record: true\n  event: true\n  rpc: true\n  presence: true\n\ntelemetry:\n  type: deepstreamIO\n  options:\n    # Disable telemetry entirely\n    enabled: true\n    # Prints whatever will be sent to the telemetry endpoint,\n    # without actually sending it\n    debug: false\n    # An anonymous uuid that allows us to know its one unique\n    # deployment. Please don't generate these randomly, it really\n    # skews up analytics. This is in the config and user generated\n    # because we don't want to\n    deploymentId: <uuid goes here>\n```\n\n### Fix\n\nFixes by the awesome @jaime-ez around heartbeats and ping messages!\n\n## [5.0.16] - 2020.04.30\n\n### Feat\n\n- Add two new plugins:\n\n  - heap-snapshot\n    This allows deepstream to save its heap space for analysis by v8 tools\n\n    ```yaml\n    plugins:\n      heap-snapshot:\n        name: 'heap-snapshot'\n        options:\n          interval: 60000\n          outputDir: file(../heap-snapshots)\n    ```\n\n  - aws\n    This is a general plugin for all AWS services, currently allows us to sync\n    the heap-snapshot directory to S3 which is useful when running via docker.\n    The functionality however is generic, so you could have a plugin that outputs\n    other useful data.\n    You can also very simply add more services (or more of the same ones). This\n    just makes it easier for us to maintain the plugin.\n\n    ```yaml\n    plugins:\n      aws:\n        name: aws\n        options:\n          accessKeyId: ${AWS_ACCESS_KEY}\n          secretAccessKey: ${AWS_SECRET_ACCESS_KEY}\n          services:\n            - type: s3-sync\n              options:\n                syncInterval: 60000\n                syncDir: file(../heap-snapshots)\n                bucketName: ${SYNC_BUCKET_NAME}\n                bucketRegion: ${AWS_DEFAULT_REGION}\n    ```\n\n- SocketData is passed to monitoring service in order to allow for fined grain monitoring.\n\n### Fix\n\n- uws service accepts options requests and enforces CORS params\n\n## [5.0.15] - 2020.04.24\n\n### Fix\n\n- Allowing valve file to be passed in via nodeJS config (@jaime-ez)\n\n### Misc\n\n- Updating uws dependency\n\n## [5.0.14] - 2020.04.19\n\n### Misc\n\n- Adding a log line for MQTT incoming connections for clarity\n\n## [5.0.13] - 2020.04.16\n\n### Fix\n\n- Fixing issue where record updates via clusters didn't always get sent correctly to subscribers\n\n### Misc\n\n- Updating dependencies\n\n## [5.0.12] - 2020.03.06\n\n### Fix\n\n- Fixing issue where sending messages between multiple protocols can break. Verbose logging will be removed in the next release.\n\n## [5.0.11] - 2020.03.05\n\n### Fix\n\n- adding debug logs for listening, and allowing to subscribers to listen to self\n\n## [5.0.10] - 2020.03.05\n\n### Fix\n\n- Fixing log level output as debug logs are being ignored\n\n## [5.0.9] - 2020.02.22\n\n### Misc\n\n- Attempt to fix npm publishing issue due to travis bug\n\n## [5.0.8] - 2020.02.16\n\n### Fixes\n\n- Call onClientDisconnect with userId instead of socket\n\n## [5.0.7] - 2020.02.08\n\n### Fixes\n\n- Fixes in storage auth handler by @abird\n\n## [5.0.6] - 2020.02.08\n\n### Improvement\n\n- Updating all dependencies\n\n### Fixes\n\n- Call onClientDisconnect from combined auth handler\n\n## [5.0.5] - 2019.11.24\n\n\n### Feat\n\n- Adding winston to docker and package image\n\n### Fix\n\n- Fixed critical issue where invalid websocket frames kill server\n\n## [5.0.4] - 2019.11.05\n\n### Fix\n\n- Provide all HTTP headers to auth endpoint when using ws\n\n## [5.0.3] - 2019.11.05\n\n### Feat\n\n- Adding a log monitoring plugin, useful for kibana log forwarding\n- Adding header authentication for http monitoring\n\n## [5.0.2] - 2019.11.04\n\n### Fix\n\n- Production only dependency did not include ds-types\n- Close down MQTT server on stop\n\n## [5.0.1] - 2019.11.02\n\n### Features\n\n- Replaces ENV variables in config loaded using fileLoader [#1022](https://github.com/deepstreamIO/deepstream.io/issues/1022)\n\n### Fixes\n\n- Fixes odd types scripting issue breaking plugin interfaces when using `deepstream.getServices()`\n- Fixes hash generation via CLI [#1025](https://github.com/deepstreamIO/deepstream.io/issues/1025)\n\n## [5.0.0] - 2019.10.27\n\n### Features:\n\n- New License\n- Singular HTTP Service\n- SSL Support reintroduced\n- Better Config file validation\n- JSON Logger\n- NGINX Helper\n- Combined authentication handler\n- Embedded dependencies\n- Builtin HTTP Monitoring\n- Storage authentication endpoint\n- Guess whats back, official clustering support!\n\n### Backwards compatibility\n\n- Custom authentication plugins now have to use the async/await API\n- Custom permission handlers need to slightly tweak the function arguments\n- Deployment configuration has to be (simplified) to respect the single HTTP/Websocket port\n\n### Upgrade guide\n\nYou can see the upgrade guide for backwards compatibility [here](https://deepstream.io/tutorials/upgrade-guides/v5/server/)\n\n## [4.2.5] - 2019.10.01\n\n### Fix\n\nRemoving dom lib from typescript, which exposed variables that throw exceptions #1008\n\n## [4.2.4] - 2019.10.01\n\n### Fix\n\nCannot read property 'N' of undefined #920\n\n## [4.2.3] - 2019.10.01\n\n### Improvement\n\nHardening the config validator\n\n### Fix\n\nAllow empty password for mqtt endpoint when authentication is not enabled (@Aapkostka) #1003\n\nResolvePluginClass should look for lowercase plugins #1002\n\nRPC not routed to different deepstream node depending on startup order #1001\n\n### Misc\n\nUpdating dependencies\n\n## [4.2.2] - 2019.09.17\n\n### Fix\n\nAdding health-checks for all ws based endpoints.\n\n## [4.2.1] - 2019.09.17\n\n### Fix\n\nRemove conflicting port for those starting using node with an empty config object.\n\n### Improvement\n\nLimit the dead socket log to reduce insane spam.\n\n## [4.2.0] - 2019.09.09\n\n### Feat\n\nTwo new connection endpoints have been added. They are currently experimental and will be properly\nannounced with associated documentation.\n\nOne endpoint is mqtt! This allows us to support mqtt auth (using username and password), retain using records and QoS 1 using write acks. The only issue is since mqtt only supports one sort\nof concept (with flags distinguishing them) we bridge both events and records together. That means if you subscribe to 'temperature/london', you'll get the update from both a client doing `event.emit('temperature/london')` and `record.setData('temperature/london')`.\n\nThe second endpoint is `ws-json` which allows users to interact with deepstream by just passing through json serialized text blobs instead of protobuf. This is mainly to help a few people trying to write SDKs without the hassle of a protobuf layer.\n\nValue also injects a `name` variable which allows you to reference the name your currently in. Useful for cross referencing.\n\n### Fix\n\nSubscription registry seemed to have a massive leak when it came to dead sockets! This has now been fixed. The sockets seemed to have gotten the remove event deleted earlier in their lifecycle which prohibited it from doing a proper clean up later.\n\n## [4.1.0] - 2019.08.30\n\n### Feat\n\nBackwards compatibility with V3 clients / text protocol using a ws-text connection endpoint\n\nThis has a couple of small differences, like `has` is no longer supported and `snapshot` errors\nare exposed using the global `error` callback instead of via the response. Otherwise all the e2e\ntests work, and best of all you can run both at the same time if you want to run JS 4.0\nand Java 3.0 simultaneously!\n\nIt is worth keeping in mind there is a small CPU overhead between switching from V3 custom deepstream\nencoding to JSON (V4), so it is advised to monitor your CPU when possible!\n\n```\n- type: ws-text\n    options:\n        # port for the websocket server\n        port: 6021\n        # host for the websocket server\n        host: 0.0.0.0\n```\n\n## [4.0.6] - 2019.08.19\n\n### Feat\n\nAllow SUBSCRIBE and READ without CREATE actions, for clients that are in read only mode\n\n### Improvement\n\nAdding declaration types (thank you @Vortex375!)\n\n## [4.0.5] - 2019.08.09\n\n### Improvement\n\nAdding meta objects to logs and monitoring for easier tagging to monitoring solutions\n\n## [4.0.4] - 2019.08.05\n\n### Fix\n\n- Don't buffer error messages in relation to connections, otherwise the client will get the close event first\n- Ignore ping messages during the connecting and authenticating stages\n\n## [4.0.3] - 2019.08.04\n\n### Fix\n\n- Notify monitoring plugin of all messages sent out individually\n\n## [4.0.2] - 2019.08.03\n\n### Features\n\n- Alpine docker image\n\n### Fix\n\n- Override the http port and host correctly\n\n## [4.0.1] - 2019.07.31\n\n### Improvements\n\n- Exit immediately if HTTP server port is occupied\n- When using debug, log exact error to why a plugin could not be loaded\n\n## [4.0.0] - 2019.07.30\n\n### Features:\n\n- New protobuf protocol support (under the hood)\n- Bulk actions instead of individual subscribes (under the hood)\n- Official Plugin Support\n- Monitoring Support\n- Clustering Support (with small caveats)\n- Listening Discovery Simplification\n- V2 storage API\n- V2 cache API\n- Notify API\n\n### Improvements\n\n- Lazy data parsing\n- Improved deepstream lifecycle\n- Upgraded development tools\n- New deepstream.io website\n\n### Backwards compatibility\n\n- All V3 SDKs no longer compatible due to protobuf binary protocol\n\n### Upgrade guide\n\nYou can see the upgrade guide for backwards compatibility [here](https://deepstream.io/tutorials/upgrade-guides/v4/server/)\n\n### TLDR;\n\nYou can see the in depth side explanation of the changes [here](https://deepstream.io/releases/server/v4-0-0/)\n\n## [3.1.0] - 2017.09.25\n\n### Features\n\n- a new standardised logging API with `debug`, `info`, `warn` and `error` methods\n- the presence feature can now be used on a per user basis. The online status of individual users can be queried for as well as subscribed to. Check out the tutorial on our website [here](https://deepstreamhub.com/tutorials/guides/presence/)\n\n### Improvements\n\n- `perMessageDeflate` option can now be passed to uws, courtesy of [@daviderenger](@daviderenger) [#786](https://github.com/deepstreamIO/deepstream.io/pull/786)\n- various fixes and performance improvements to the subscription registry [#780](https://github.com/deepstreamIO/deepstream.io/pull/780), courtesy of [@ronag](@ronag).\n\n### Fixes\n\n- allow updating and writing to Lists via the HTTP API [#788](https://github.com/deepstreamIO/deepstream.io/pull/788) courtesy of [@rbarroetavena](@rbarroetavena)\n- no data when sending HTTP requests is now considered undefined, rather than null [#798](https://github.com/deepstreamIO/deepstream.io/pull/798).\n\n### Miscellaneous\n\n- internal refactor to pull e2e client operations into framework and abstract from Cucumber steps.\n\n## [3.0.1] - 2017.08.14\n\n### Features\n\n- Added a `restart` option to deepstream service CLI\n- deepstream will now write to a pid file at `/var/run/deepstream/deepstream.pid` while running as a service\n- Authentication and permission plugins can now be configured via config and will be resolved as normal plugins. Either a path or name will need to be provided at the top level, and any options specified will also be passed in.\n\n## [3.0.0] - 2017.07.26\n\n### Features\n\n#### [HTTP API](https://deepstreamhub.com/docs/http/v1/)\nEnabling clients to create, read, update and delete records, emit events, request RPCS\nand read presence using a JSON bulk request/response format via HTTP.\n- The HTTP API is enabled by default on PORT 8080 and can be configured in the\nconnectionEndpoints -> http section of deepstream's `config.yml`\n- To disable the HTTP API set the above config to null\n\n#### [PHP Client Support](https://deepstreamhub.com/docs/client-php/DeepstreamClient/)\nThe above HTTP API makes deepstream.io compatible with the deepstream PHP client\n\n#### Multi Endpoint Architecture\nThe deepstream 3.0 release lays the groundwork for multiple combinable endpoints/protocols,\ne.g. GraphQL or Binary to be used together. It also introduces a new endpoint type enabling\ndevelopers to write their own. Please note - at the moment it is not possible to run multiple subscription\nbased endpoints (e.g. websocket) simultaneously.\n\n#### Message Connector Discontinuation\nTo address the scalability issues associated with the message connector interface's coarse topics\ndeepstream will move to a build-in, high performance p2p/small world network based clustering approach, available\nas an enterprise plugin. The current message connector support is discontinued.\n\n### Miscellaneous\n- Moved end-to-end tests into this repository from `deepstream.io-client-js`.\n- Replaced `javascript-state-machine` dependency with custom state machine.\n\n### Fixes\n- Improved handling of invalid record names.\n\n## [2.4.0] - 2017.07.01\n\n## Features\n\n- Added new CLI command, including:\n\n  + deepstream daemon\n  This command forks deepstream and monitors it for crashes, allowing it to restart automatically to avoid downtime\n\n  + deepstream service add\n  This command allows you to create an init.d or systemd script automatically and add it to your system.\n  ```bash\n  sudo deepstream service --help\n  Usage: service [options] [add|remove|start|stop|status]\n  Add, remove, start or stop deepstream as a service to your operating system\n  ```\n\n- Added brew cask support\n\nYou can now install easily install deepstream on your mac using `brew cask install deepstream` driven by config files within `/user/local/etc/deepstream/conf`\n\n## Fixes\n\n- Fix issue where certain invalid paths would return 'Invalid Type' on the server.\n- Fix issue in request/response where selecting a remote server as not done uniformly.\n\n## [2.3.7] - 2017.06.20\n\n## Fixes\n\n- Fix issue where using both `.0.` and `[0]` within a json path resulted in inserting into an array. However, when using other SDKs such as Java they would be treated as an Object key or array index.\n- Fix issue where nested array access/manipulation didn't work via json paths.\n\n## Compatability Issue\n\nDue to the nature of this fix, it may result in compatability issues with applications that used json paths incorrectly ( using `.0.` intead of `[0]` ). Please ensure you change those before upgrading.\n\n## [2.3.6] - 2017.06.12\n\n## Fixes\n\n- Fix for issue [#703](https://github.com/deepstreamIO/deepstream.io/issues/703)\n  where record deletions were not being propogated correctly within a cluster.\n- Fixes config-loading issue present in the binary release of 2.3.5.\n\n## [2.3.5] - 2017.06.12\n\n## Fixes\n\n- Hardcode v3.0.0-rc1 dependency on javascript-state-machine, as v3.0.1 causes deepstream.io startup to fail\n\n## [2.3.4] - 2017.06.02\n\n## Fixes\n\n- Hot path needs to store values in the correct format\n\n## [2.3.3] - 2017.06.02\n\n### Fixes\n\n- Binary config files have the correct latest structure\n- Fix an issue where heavy concurrent writes on the same record fail\n\n## [2.3.2] - 2017.05.31\n\n### Fixes\n\n- Fixing a connection data regression where it wasn't formatted the same as pre 2.3.0\n\n## [2.3.1] - 2017.05.30\n\n### Fixes\n\n- Correctly merging config options from `config.yml` file with the default options\n\n## [2.3.0] - 2017.05.29\n\n### Features\n\n- Adds \"storageHotPathPatterns\" config option.\n- Adds support for `setData()`: upsert-style record updates without requiring that a client is\n  subscribed to the record. This uses a new 'CU' (Create and Update) message. The `setData()` API\n  is up to 10x faster than subscribing, setting, then discarding a record.\n- Support for connection endpoint plugins.\n\n### Enhancements\n\n- Significant performance improvements stemming from message batching.\n\n### Miscellaneous\n\n- Moved uws into a connection endpoint plugin.\n- Explicit state-machine that initializes and closes dependencies in a well-defined order.\n\n## [2.2.2] - 2017.05.03\n\n### Enhancements\n- Adds support for custom authentication and permissioning plugins.\n- Adds support for generic plugins.\n\n### Fixes\n- Added check to ensure subscriptions are not removed from distributed state registry prematurely.\n\n## [2.2.1] - 2017.04.24\n\n### Enhancements\n\n- Unsolicited RPCs now get a `INVALID_RPC_CORRELATION_ID` message\n\n### Fixes\n\n- RPC lifecycles have been improved and don't throw exceptions on response after a timeout by [ronag](ronag)\n- Correct options now being passed into the `RuleCache`, courtesy of [ralphtheninja](ralphtheninja)\n\n## [2.2.0] - 2017.04.08\n\n### Enhancements\n\n- Records now can be set with a version -1, which ignores version conflicts by [datasage](datasage)\n- Delete events are now propagated in the correct order by [datasage](datasage)\n- You can now request the HEAD of a record to retrieve just its version number by [datasage](datasage)\n- Providers for listeners are now by default selected randomly instead of in order of subscription\n- Ensure record updates are not scalar values before trying to save them in cache by [datasage](datasage)\n- Long lived RPC requests now use dynamic lookups for providers rather than building the Set upfront by [ronag]{ronag}\n- Huge optimization to subscription registry, where the time for registering a subscriber has been reduced from n^2 to O(n log n)\n\n### Miscellaneous\n\n- Deleting grunt since everything is script based\n\n\n## [2.1.6] - 2017.03.29\n\n### Miscellaneous\n\n- Due to uws releases being pulled from NPM, we're now using uws from a git repo\n- Created a separate repo [uws-dependency](https://github.com/deepstreamIO/uws-dependency) with binaries.\n\n## [2.1.4 - 2.1.5]\n\n- Due to problems with build resulting from uws unpublishing, these two npm packages\n  have been unpublished (noop)\n\n## [2.1.3] - 2017.02.25\n\n### Bug Fixes\n\n- Unsolicited message in Listening when all clients unsubscribe [#531]\n- Handle Non text based websocket frame [#538]\n- Aligning binary config with node [#488]\n- Event subscription data mishandled in Valve [#510]\n- Logging after logger is destroyed [#527]\n- Deepstream crash on empty users file [#512]\n- Logging error object instead of name in connection error [#420]\n\n### Enhancements\n\n- maxRuleIterations must be 1 or higher [#498]\n- Ignore sender in subscriptionRegistry if messagebus [#473]\n- Removing dead config options [#599]\n- getAlternativeProvider in RPC Handler deals with more edge cases [#566]\n- Update UWS build version to 0.12\n- Packages built against node 6.10\n\n## [2.1.2] - 2016.12.28\n\n### Bug fixes\n\n- Fixing write error where only initial value is written to storage [#517](https://github.com/deepstreamIO/deepstream.io/issues/517)\n\n## [2.1.1] - 2016.12.28\n\n### Bug fixes\n\n- Valve cross referencing in both a create and read results in a ack timeout [#514](https://github.com/deepstreamIO/deepstream.io/issues/514)\n\n## [2.1.0] - 2016.12.20\n\n### Features\n\n- Record write acknowledgement. Records are now able to be set with an optional callback which will be called with any errors from storing the record in cache/storage [#472](https://github.com/deepstreamIO/deepstream.io/pull/472)\n\n### Enhancements\n\n- Applying an ESLint rule set to the repo [#482](https://github.com/deepstreamIO/deepstream.io/pull/482)\n- Stricter valve permissioning language checks [#486](https://github.com/deepstreamIO/deepstream.io/pull/486) by [@Iiridayn](https://github.com/Iiridayn)\n- Update uWS version to [v0.12.0](https://github.com/uWebSockets/uWebSockets/releases/tag/v0.12.0)\n\n### Bug fixes\n\n- Better handling/parsing of authentication messages [#463](https://github.com/deepstreamIO/deepstream.io/issues/463)\n- Properly returning handshake data (headers) from SocketWrapper [#450](https://github.com/deepstreamIO/deepstream.io/issues/450)\n- Fix case where CLIENT_DISCONNECTED is not sent from SocketWrapper [#470](https://github.com/deepstreamIO/deepstream.io/issues/470)\n- Fixed issue where listen does not recover from server restart [#476](https://github.com/deepstreamIO/deepstream.io/issues/476)\n- Handling presence events properly. Now when a user logs in, subscribed clients are only notified the first time the user logs in, and the last time they log out [#499](https://github.com/deepstreamIO/deepstream.io/pull/499)\n\n## [2.0.1] - 2016.11.21\n\n### Bug Fixes\n\n- Fixed issue where connectionData was not available in auth requests\n  [#450](https://github.com/deepstreamIO/deepstream.io/issues/450)\n- Changelog of 2.0.0 mistakenly said that heartbeats were on port 80 instead of 6020\n\n## [2.0.0] - 2016.11.18\n\n### Features\n- User presence has been added, enabling querying and subscription to who is\n  online within a cluster\n- Introduces the configuration option `broadcastTimeout` to `config.yml` to allow coalescing of\n  broadcasts. This option can be used to improve broadcast message latency such\n  as events, data-sync and presence\n  For example, the performance of broadcasting 100 events to 1000 subscribers\n  was improved by a factor of 20\n- Adds client heartbeats, along with configuration option`heartbeatInterval` in `config.yml`.\n  If a connected client fails to send a heartbeat within this timeout, it will be\n  considered to have disconnected [#419](https://github.com/deepstreamIO/deepstream.io/issues/419)\n- Adds healthchecks – deepstream now responds to http GET requests to path\n  `/health-check` on port 6020 with code 200. This path can be configured with\n  the `healthCheckPath` option in `config.yml`\n\n### Enhancements\n- E2E tests refactored\n- uWS is now compiled into the deepstream binary, eliminating reliability\n  issues caused by dynamic linking\n\n### Breaking Changes\n\n- Clients prior to v2.0.0 are no longer compatible\n- Changed format of RPC request ACK messages to be more consistent with the\n  rest of the specs\n  [#408](https://github.com/deepstreamIO/deepstream.io/issues/408)\n- We removed support for TCP and engine.io, providing huge performance gains by\n  integrating tightly with native uWS\n- Support for webRTC has been removed\n- You can no longer set custom data transforms directly on deepstream\n\n## [1.1.2] - 2016-10-17\n\n### Bug Fixes\n\n- Sending an invalid connection message is not caught by server [#401](https://github.com/deepstreamIO/deepstream.io/issues/401)\n\n## [1.1.1] - 2016-09-30\n\n### Bug Fixes\n\n- Storage connector now logs errors with the correct namepspace [@Iiridayn](@Iiridayn)\n\n### Enhancements\n\n- RPC now uses distributed state and no longer depends on custom rpc discovery logic\n- Deepstream now uses connection challenges by default rather than automatically replying with an ack\n- Upgraded to uWS 0.9.0\n\n\n## [1.1.0] - 2016-09-08\n\n### Bug Fixes\n\n- Fix wrong validation of Valve Permissions when `data` is used as a property [#346](https://github.com/deepstreamIO/deepstream.io/pull/346)\n\n### Enhancements\n\n- Outgoing connections now have throttle options that allow you to configure maximum package sizes to find your personal sweet spot between latency and speed\n\n```yaml\n# the time (in milliseconds) to wait for a buffer to fill before sending it out\ntimeBetweenSendingQueuedPackages: 1\n# the amount of messages that should fit into a buffer before sending between the time to fill\nmaxMessagesPerPacket: 1000\n```\n\n### Features\n\n- Listening: Listeners have been drastically improved [https://github.com/deepstreamIO/deepstream.io/issues/211], and now:\n- works correctly across a cluster\n- can inform the user whenever the last subscription has been removed even if the listener itself is subscribed\n- only allows a single listener to provide a record\n- has a concept of provided, allowing records on the client side to be aware if the data is being actively updated by a backend component\n\nAs part of this story, we now have multiple significant improvements in the server itself, such as:\n- a `distributed state registry` which allows all clusters to keep their state in sync\n- a `unique state provider` allowing cluster wide locks\n- a `cluster-registry` that provides shares server presence and state across the cluster\n\nBecause of these we can now start working on some really cool features such as advanced failover, user presence and others!\n\n\n## [1.0.4] - 2016-08-16\n\n### Bug Fixes\n\n- Auth: File authentication sends server data to client on cleartext passwords [#322](https://github.com/deepstreamIO/deepstream.io/issues/322)\n\n- Auth: HTTP authentication missing logger during when attempting to log any errors occured on http server [#320](https://github.com/deepstreamIO/deepstream.io/issues/320)\n\n\n## [1.0.3] - 2016-07-28\n\n### Bug Fixes\n\n- CLI: installer for connectors sometimes fail to download (and extract) the archive [#305](https://github.com/deepstreamIO/deepstream.io/issues/305)\n- Auth: File authentication doesn't contain `serverData` and `clientData` [#304](https://github.com/deepstreamIO/deepstream.io/issues/304)\n\n###### Read data using `FileAuthentication` using clientData and serverData rather than data\n\n```yaml\nuserA:\n  password: tsA+yfWGoEk9uEU/GX1JokkzteayLj6YFTwmraQrO7k=75KQ2Mzm\n  serverData:\n    role: admin\n  clientData:\n    nickname: Dave\n```\n\n### Features\n\n###### Make connection timeout\n\nUsers can now provide a `unauthenticatedClientTimeout` config option that forces connections to close if they don't authenticate in time.\nThis helps reduce load on server by terminating idle connections.\n\n- `null`: Disable timeout\n- `number`: Time in milliseconds before connection is terminated\n\n## [1.0.2] - 2016-07-19\n\n### Bug Fixes\n\n- Fixed issue regarding last subscription to a deleted record not being cleared up\n\n## [1.0.1] - 2016-07-18\n\n### Bug Fixes\n\n- Fix issue when try to pass options to the default logger [#288](https://github.com/deepstreamIO/deepstream.io/pull/288) ([update docs](https://github.com/deepstreamIO/deepstream.io-website/pull/35/commits/838617d93cf00e66176cdf06d161fd8f86574aa1) as well)\n\n- Fix issue deleting a record does not unsubscribe it and all other connections, not allowing resubscriptions to occur #293\n\n#### Enhancements\n\n###### Throw better error if dependency doesn't implement Emitter or isReady\n\n## [1.0.0] - 2016-07-09\n\n### Features\n\n###### CLI\nYou can start deepstream via a command line interface. You find it in the _bin_ directory. It provides these subcommands:\n  - `start`\n  - `stop`\n  - `status`\n  - `install`\n  - `info`\n  - `hash`\n  append a `--help` to see the usage.\n\n###### File based configuration\nYou can now use a file based configuration instead of setting options via `ds.set(key, value)`.\ndeepstream is shipped with a _conf_ directory which contains three files:\n  - __config.yml__ this is the main config file, you can specify most of the deepstream options in that file\n  - __permissions.yml__ this file can be consumed by the PermissionHandler. It's not used by default, but you can enable it in the _config.yml_\n  - __users.yml__ this file can be consumed by the AuthenticationHandler. It's not used by default, but you can enable it in the _config.yml_\n\nFor all config types support these file types: __.yml__, __.json__ and __.js__\n\n###### Constructor API\nThere are different options what you can pass:\n  - not passing any arguments ( consistent with 0.x )\n  - passing `null` will result in loading the default configuration file in the directory _conf/config.yml_\n  - passing a string which is a path to a configuration file, supported formats: __.yml__, __.json__ and __.js__\n  - passing an object which defines several options, all other options will be merged from deepstream's default values\n\n###### Valve Permissions rules\nYou can write your permission into a structured file. This file supports a special syntax, which allows you to do advanced permission checks. This syntax is called __Valve__.\n\n#### Enhancements\n\n###### uws\ndeepstream now uses [uws](https://github.com/uWebSockets/uWebSockets), a native C++ websocket server\n\n###### no process.exit on plugin initialization error or timeout\ndeepstream will not longer stops your process via `process.exit()`. This happened before when a connector failed to initialize correctly [#243](https://github.com/deepstreamIO/deepstream.io/issues/243) instead it will throw an error now.\n\nCurrently the API provides no event or callback to handle this error\nother than subscribing to the global `uncaughtException` event.\n\n```javascript\nprocess.once('uncaughtException', err => {\n  // err.code will equal to of these constant values:\n  // C.EVENT.PLUGIN_INITIALIZATION_TIMEOUT\n  // or C.EVENT.PLUGIN_INITIALIZATION_ERROR\n})\n```\nKeep in mind that deepstream will be in an unpredictable state and you should consider to create a new instance.\n\n### Breaking Changes\n\n###### Permission Handler\nIn 0.x you can set a `permissionHandler` which needs to implement two functions:\n\n- `isValidUser(connectionData, authData, callback)`\n- `canPerformAction(username, message, callback)`\n\nIn deepstream 1.0 the `isValidUser` and `onClientDisconnect` methods are no longer part of the `permissionHandler` and are instead within the new `authenticationHandler`.\n\nYou can reuse the same 0.x permission handler except you will have to set it on both explicitly.\n\n```javascript\nconst permissionHandler = new CustomPermissionHandler()\nds.set( 'permissionHandler', permissionHandler )\nds.set( 'authenticationHandler', permissionHandler )\n```\n\n###### Plugin API\nAll connectors including, the `permissionHandler`, `authenticationHandler` and `logger` all need to implement the plugin interface which means exporting an object that:\n\n- has a constructor\n- has an `isReady` property which is true once the connector has been initialized. For example in the case a database connector this would only be `true` once the connection has been established. If the connector is synchronous you can set this to true within the constructor.\n- extends the EventEmitter, and emits a `ready` event once initialized and `error` on error.\n\n###### Logger and colors options\nThe color flag can't be set in the root level of the configuration anymore.\nThe default logger will print logs to the StdOut/StdErr in colors.\nYou can use the [deepstream.io-logger-winston](https://www.npmjs.com/package/deepstream.io-logger-winston) which can be configured in the config.yml file with several options.\n\n###### Connection redirects\ndeepstream clients now have a handshake protocol which allows them to be redirected to the most efficient node and expect an initial connection ack before logging in. As such In order to connect a client to deepstream server you need also to have a client with version 1.0 or higher.\n\nMore details in the [client changelog](https://github.com/deepstreamIO/deepstream.io-client-js/blob/master/CHANGELOG.md).\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:22 as builder\nWORKDIR /app\n\nCOPY package*.json ./\n\nRUN npm ci\nRUN npm install --omit=dev \\\n    @deepstream/cache-redis \\\n    # @deepstream/cache-memcached \\\n    # @deepstream/cache-hazelcast \\\n    @deepstream/clusternode-redis \\\n    @deepstream/storage-mongodb \\\n    @deepstream/storage-rethinkdb \\\n    @deepstream/storage-elasticsearch \\\n    @deepstream/storage-postgres \\\n    @deepstream/logger-winston \\\n    @deepstream/plugin-aws\n\nCOPY . .\n\nRUN npm run tsc\n\nFROM node:22\nWORKDIR /usr/local/deepstream\nCOPY --from=builder /app/node_modules/ ./node_modules\nCOPY --from=builder /app/dist/ .\n\nEXPOSE 6020\nEXPOSE 8080\nEXPOSE 9229\n\nCMD [\"node\", \"./bin/deepstream.js\", \"start\", \"--inspect=0.0.0.0:9229\"]\n"
  },
  {
    "path": "Dockerfile.alpine",
    "content": "FROM node:22-alpine as builder\nWORKDIR /app\n# RUN curl \"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\"\n# RUN unzip awscliv2.zip\n# RUN ./aws/install\n\nCOPY package*.json ./\n\nRUN npm ci\nRUN npm install --omit=dev \\\n    @deepstream/cache-redis \\\n    # @deepstream/cache-memcached \\\n    # @deepstream/cache-hazelcast \\\n    @deepstream/clusternode-redis \\\n    @deepstream/storage-mongodb \\\n    @deepstream/storage-rethinkdb \\\n    @deepstream/storage-elasticsearch \\\n    @deepstream/storage-postgres \\\n    @deepstream/logger-winston \\\n    @deepstream/plugin-aws\n\nRUN npm uninstall --save uWebSockets.js\n\nCOPY . .\n\nRUN npm run tsc\n\nFROM node:22-alpine\nWORKDIR /usr/local/deepstream\nCOPY --from=builder /app/node_modules/ ./node_modules\nCOPY --from=builder /app/dist/ .\n\nEXPOSE 6020\nEXPOSE 8080\nEXPOSE 9229\n\nCMD [\"node\", \"./bin/deepstream.js\", \"start\", \"--inspect=0.0.0.0:9229\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright 2019 deepstreamHub GmbH\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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."
  },
  {
    "path": "README.md",
    "content": "# deepstream - the open realtime server <a href='https://deepstreamio.github.io/'><img src='./elton-square.png' height='60' alt='deepstream'></a>\n\ndeepstream is an open source server inspired by concepts behind financial trading technology. It allows clients and backend services to sync data, send messages and make rpcs at very high speed and scale.\n\n[![npm version](https://badge.fury.io/js/%40deepstream%2Fserver.svg)](https://badge.fury.io/js/%40deepstream%2Fserver)[![Docker Stars](https://img.shields.io/docker/pulls/deepstreamio/deepstream.io.svg)](https://hub.docker.com/r/deepstreamio/deepstream.io/)\n\ndeepstream has three core concepts for enabling realtime application development\n\n- **records** ([realtime document sync](https://deepstreamio.github.io/docs/tutorials/core/datasync/records))\n\nrecords are schema-less, persistent documents that can be manipulated and observed. Any change is synchronized with all connected clients and backend processes in milliseconds. Records can reference each other and be arranged in lists to allow modelling of relational data\n\n- **events** ([publish subscribe messaging](https://deepstreamio.github.io/docs/tutorials/core/pubsub/events))\n\nevents allow for high performance, many-to-many messaging. deepstream provides topic based routing from sender to subscriber, data serialisation and subscription listening.\n\n- **rpcs** ([request response workflows](https://deepstreamio.github.io/docs/tutorials/core/request-response/rpc))\n\nremote procedure calls allow for secure and highly available request response communication. deepstream handles load-balancing, failover, data-transport and message routing.\n\n- **security** ([Authentication](https://deepstreamio.github.io/docs/tutorials/core/auth/auth-introduction) and [Permissions](https://deepstreamio.github.io/docs/tutorials/core/permission/valve-introduction))\n\ndeepstream offers a combination of different authentication mechanisms with a powerful permission-language called Valve that allows you to specify which user can perform which action with which data.\n\n### Getting Started:\n\n1. [Tutorials - What is deepstream](https://deepstreamio.github.io/docs/tutorials/concepts/what-is-deepstream)\n2. [Installing deepstream](https://deepstreamio.github.io/docs/tutorials/install/linux)\n3. [Quickstart](https://deepstreamio.github.io/docs/tutorials/getting-started/javascript)\n4. [Documentation](https://deepstreamio.github.io/docs/docs)\n\n### Community Links\n\n1. [Stack Overflow](https://stackoverflow.com/questions/tagged/deepstream.io)\n2. [Github Discussions](https://github.com/deepstreamIO/deepstreamIO.github.io/discussions)\n\n### Contributing\n\ndeepstream development is a great way to get into building performant Node.js applications, and contributions are always welcome with lots of ❤. Contributing to deepstream is as simple as having Node.js (10+) and TypeScript (3+) installed, cloning the repo and making some changes.\n\n```\n~ » git clone git@github.com:deepstreamIO/deepstream.io.git\n~ » cd deepstream.io\n~/deepstream.io » git submodule update --init\n~/deepstream.io » npm i\n~/deepstream.io » npm start\n      _                     _\n   __| | ___  ___ _ __  ___| |_ _ __ ___  __ _ _ __ ____\n  / _` |/ _ \\/ _ \\ '_ \\/ __| __| '__/ _ \\/ _` | '_ ` _  \\\n | (_| |  __/  __/ |_) \\__ \\ |_| | |  __/ (_| | | | | | |\n  \\__,_|\\___|\\___| .__/|___/\\__|_|  \\___|\\__,_|_| |_| |_|\n                 |_|\n =====================   starting   =====================\n```\n\nFrom here you can make your changes, and check the unit tests pass:\n\n```\n~/deepstream.io » npm t\n```\n\nIf your changes are substantial you can also run our extensive end-to-end testing framework:\n\n```\n~/deepstream.io » npm run e2e\n```\n\nFor power users who want to make sure the binary works, you can run `sh scripts/package.sh true`. You'll need to download the usual [node-gyp](https://github.com/nodejs/node-gyp) build environment for this to work and we only support the latest LTS version to compile. This step is usually not needed though unless your modifying resource files or changing dependencies.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nReport the vulnerability in the security [advisory section](https://github.com/deepstreamIO/deepstream.io/security) of the repository.\n\nIf your submission is valid, a GitHub advisory will be published and if present in NPM or other released packages, a CVE will be requested.\n"
  },
  {
    "path": "ascii-logo.txt",
    "content": "      _                     _ \n   __| | ___  ___ _ __  ___| |_ _ __ ___  __ _ _ __ ____ \n  / _` |/ _ \\/ _ \\ '_ \\/ __| __| '__/ _ \\/ _` | '_ ` _  \\\n | (_| |  __/  __/ |_) \\__ \\ |_| | |  __/ (_| | | | | | |\n  \\__,_|\\___|\\___| .__/|___/\\__|_|  \\___|\\__,_|_| |_| |_|\n                 |_|"
  },
  {
    "path": "bin/deepstream-cluster.ts",
    "content": "import { Command } from 'commander'\nimport * as cluster from 'cluster'\nconst numCPUs = require('os').cpus().length\nimport { EVENT } from '@deepstream/types'\n\nexport const verticalCluster = (program: Command) => {\n  program\n    .command('cluster')\n    .description('start a vertical cluster of deepstream servers')\n\n    .option('-c, --config [file]', 'configuration file, parent directory will be used as prefix for other config files')\n    .option('-l, --lib-dir [directory]', 'path where to lookup for plugins like connectors and logger')\n\n    .option('--cluster-size <amount>', 'the amount of nodes to run in the cluster. Defaults to all available cores')\n    .option('--host <host>', 'host for the http service')\n    .option('--port <port>', 'port for the http service')\n    .option('--disable-auth', 'Force deepstream to use \"none\" auth type')\n    .option('--disable-permissions', 'Force deepstream to use \"none\" permissions')\n    .option('--log-level <level>', 'Log messages with this level and above')\n    .action(action)\n}\n\nfunction action () {\n    // @ts-ignore\n    global.deepstreamCLI = this\n    const workers = new Set<any>()\n\n    if (!global.deepstreamCLI.clusterSize) {\n      global.deepstreamCLI.clusterSize = numCPUs\n    }\n\n    if (global.deepstreamCLI.clusterSize && global.deepstreamCLI.clusterSize > numCPUs) {\n      console.warn('Setting more nodes than available cores can decrease performance')\n    }\n\n    const setupWorkerProcesses = () => {\n        console.log('Master cluster setting up ' + global.deepstreamCLI.clusterSize + ' deepstream nodes')\n\n        for (let i = 0; i < global.deepstreamCLI.clusterSize; i++) {\n            workers.add(cluster.fork())\n        }\n\n        // process is clustered on a core and process id is assigned\n        cluster.on('online', (worker) => {\n            console.log(`Deepstream ${worker.process.pid} is listening`)\n        })\n\n        // if any of the worker process dies then start a new one by simply forking another one\n        cluster.on('exit', (worker, code, signal) => {\n            console.log(`Deepstream ${worker.process.pid} died with code: ${code}, and signal: ${signal}`)\n            console.log('Starting a new worker')\n            workers.delete(worker)\n            workers.add(cluster.fork())\n        })\n    }\n\n    // if it is a master process then call setting up worker process\n    // @ts-ignore\n    if (cluster.isPrimary) {\n        setupWorkerProcesses()\n    } else {\n        const { Deepstream } = require('../src/deepstream.io')\n        try {\n          const ds = new Deepstream(null)\n          ds.on(EVENT.FATAL_EXCEPTION, () => process.exit(1))\n          ds.start()\n          process\n            .removeAllListeners('SIGINT').on('SIGINT', () => {\n              ds.on('stopped', () => process.exit(0))\n              ds.stop()\n            })\n        } catch (err: any) {\n          console.error(err.toString())\n          process.exit(1)\n        }\n    }\n}\n"
  },
  {
    "path": "bin/deepstream-daemon.ts",
    "content": "// @ts-ignore\nimport * as dsDaemon from '../src/service/daemon'\nimport { Command } from 'commander'\n\nexport const daemon = (program: Command) => {\n  program\n    .command('daemon')\n    .description('start a daemon for deepstream server')\n\n    .option('-c, --config [file]', 'configuration file, parent directory will be used as prefix for other config files')\n    .option('-l, --lib-dir [directory]', 'path where to lookup for plugins like connectors and logger')\n\n    .option('--host <host>', 'host for the http service')\n    .option('--port <port>', 'port for the http service')\n    .option('--disable-auth', 'Force deepstream to use \"none\" auth type')\n    .option('--disable-permissions', 'Force deepstream to use \"none\" permissions')\n    .option('--log-level <level>', 'Log messages with this level and above')\n    .action(action)\n}\n\nfunction action () {\n  dsDaemon.start({ processExec: process.argv[1] })\n}\n"
  },
  {
    "path": "bin/deepstream-hash.ts",
    "content": "import * as jsYamlLoader from '../src/config/js-yaml-loader'\nimport { Command } from 'commander'\nimport { createHash } from '../src/utils/utils'\n\nexport const hash = (program: Command) => {\n  program\n    .command('hash [password]')\n    .description('Generate a hash from a plaintext password using file auth configuration settings')\n    .option('-c, --config [file]', 'configuration file containing file auth and hash settings')\n    .action(action)\n}\n\nasync function action (this: any, password: string) {\n  // @ts-ignore\n  global.deepstreamCLI = this\n  const config = (await jsYamlLoader.loadConfigWithoutInitialization()).config\n\n  const fileAuthHandlerConfig = config.auth.find((auth) => auth.type === 'file')\n\n  if (fileAuthHandlerConfig === undefined) {\n    console.error('Error: Can only use hash with file authentication as auth type')\n    return process.exit(1)\n  }\n\n  if (!fileAuthHandlerConfig.options.hash) {\n    console.error('Error: Can only use hash with file authentication')\n    return process.exit(1)\n  }\n\n  fileAuthHandlerConfig.options.path = ''\n\n  if (!password) {\n    console.error('Error: Must provide password to hash')\n    return process.exit(1)\n  }\n\n  const { iterations, keyLength, hash: algorithm } = fileAuthHandlerConfig.options\n  try {\n    const { hash: generatedHash, salt } = await createHash(password, { iterations, keyLength, algorithm })\n    console.log(`Password hash: ${generatedHash.toString('base64')}${salt}`)\n  } catch (e) {\n    console.error('Hash could not be created', e)\n    process.exit(1)\n  }\n}\n"
  },
  {
    "path": "bin/deepstream-info.ts",
    "content": "import * as jsYamlLoader from '../src/config/js-yaml-loader'\nimport { Command } from 'commander'\nimport { getDSInfo } from '../src/config/ds-info'\n\nexport const info = (program: Command) => {\n  program\n    .command('info')\n    .description('print meta information about build and runtime')\n    .option('-c, --config [file]', 'configuration file containing lib directory')\n    .option('-l, --lib-dir [directory]', 'directory of libraries')\n    .action(printMeta)\n}\n\nasync function printMeta (this: any) {\n  if (!this.libDir) {\n    try {\n      // @ts-ignore\n      global.deepstreamCLI = this\n      await jsYamlLoader.loadConfigWithoutInitialization()\n      // @ts-ignore\n      this.libDir = global.deepstreamLibDir\n    } catch (e) {\n      console.log(e)\n      console.error('Please provide a libDir or a configFile to provide the relevant install information')\n      process.exit(1)\n    }\n  }\n\n  const dsInfo = await getDSInfo(this.libDir)\n  console.log(JSON.stringify(dsInfo, null, 2))\n}\n"
  },
  {
    "path": "bin/deepstream-nginx.ts",
    "content": "// @ts-ignore\nimport * as dsService from '../src/service/service'\nimport { Command } from 'commander'\nimport { writeFileSync } from 'fs'\nimport * as jsYamlLoader from '../src/config/js-yaml-loader'\nimport * as fileUtil from '../src/config/file-utils'\n\nexport const nginx = (program: Command) => {\n  program\n    .command('nginx')\n    .description('Generate an nginx config file for deepstream')\n\n    .option('-c, --config [file]', 'The deepstream config file')\n    .option('-p, --port', 'The nginx port, defaults to 8080')\n    .option('-h, --host', 'The nginx host, defaults to localhost')\n    .option('--ssl', 'If ssl encryption should be added')\n    .option('--ssl-cert', 'The SSL Certificate')\n    .option('--ssl-key', 'The SSL Key')\n    .option('-o, --output [file]', 'The file to save the configuration to')\n    .action(execute)\n}\n\nasync function execute (this: any, action: string) {\n  // @ts-ignore\n  global.deepstreamCLI = this\n  const { config: dsConfig } = await jsYamlLoader.loadConfigWithoutInitialization()\n\n  if (this.ssl && (!this.sslCert || !this.sslKey)) {\n    console.error('Missing --ssl-cert or/key --ssl-key options')\n    process.exit(1)\n  }\n\n  const sslConfig = `\n        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;\n        ssl_ciphers         HIGH:!aNULL:!MD5;\n\n        ssl_certificate ${this.sslCert};\n        ssl_certificate_key ${this.sslKey};\n`\n\n  const websocketConfig = dsConfig.connectionEndpoints.reduce((result: string, endpoint: any) => {\n    if (endpoint.options.urlPath) {\n      return result + `\n      location ${endpoint.options.urlPath} {\n          proxy_pass http://deepstream;\n          proxy_http_version 1.1;\n          proxy_set_header Upgrade $http_upgrade;\n          proxy_set_header Connection \"Upgrade\";\n      }\n      `\n    }\n    return result\n  }, '')\n\n  let httpConfig = ''\n  const http = dsConfig.connectionEndpoints.find((endpoint: any) => endpoint.type === 'http')\n  if (http) {\n    const paths = new Set([http.options.getPath, http.options.postPath, http.options.authPath])\n    httpConfig = [...paths].reduce((result, path) => {\n      return result + `\n      location ${path} {\n        proxy_pass http://deepstream;\n        proxy_http_version 1.1;\n      }\n    `\n    }, '')\n  }\n\n  const config = `\nworker_processes  1;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    map $http_upgrade $connection_upgrade {\n      default upgrade;\n      '' close;\n    }\n\n    upstream deepstream {\n      server ${dsConfig.httpServer.options.host}:${dsConfig.httpServer.options.port};\n      # Insert more deepstream hosts / ports here for clustering to magically work\n    }\n\n    server {\n      listen ${this.port || 8080}${this.ssl ? ' ssl' : ''};\n      server_name ${this.serverName || 'localhost'};\n      ${this.ssl ? sslConfig : ''}\n      ${websocketConfig}\n      ${httpConfig}\n    }\n}`\n\n  if (this.output) {\n    writeFileSync(fileUtil.lookupConfRequirePath(this.output), config, 'utf8')\n    console.log(`Configuration written to ${fileUtil.lookupConfRequirePath(this.output)}`)\n  } else {\n    console.log(config)\n  }\n}\n"
  },
  {
    "path": "bin/deepstream-service.ts",
    "content": "// @ts-ignore\nimport * as dsService from '../src/service/service'\nimport { Command } from 'commander'\n\nexport const service = (program: Command) => {\n  program\n    .command('service [add|remove|start|stop|restart|status]')\n    .description('Add, remove, start or stop deepstream as a service to your operating system')\n\n    .option('-c, --config [file]', 'configuration file, parent directory will be used as prefix for other config files')\n\n    .option('-n, --service-name <name>', 'the name to register the service')\n    .option('-l, --log-dir <directory>', 'the directory for output logs')\n    .option('-p, --pid-directory <directory>', 'the directory for the pid file')\n    .option('--dry-run', 'outputs the service file to screen')\n    .action(execute)\n}\n\nfunction response (error: Error | string | null, result: string) {\n  if (error) {\n    console.log(error)\n  } else {\n    console.log(result)\n  }\n}\n\nfunction execute (this: any, action: string) {\n  const name = this.serviceName || 'deepstream'\n\n  if (action === 'add') {\n\n    if (!this.logDir || !this.config) {\n      console.error('Please provide the config and log directory when adding a service')\n      process.exit(1)\n    }\n\n    const options = {\n      exec: process.argv[1],\n      programArgs: [] as string[],\n      pidFile: this.pidFile || `/var/run/deepstream/${name}.pid`,\n      logDir: this.logDir,\n      dryRun: this.dryRun\n    }\n\n    if (this.config) {\n      options.programArgs.push('-c')\n      options.programArgs.push(this.config)\n    }\n\n    dsService.add (name, options, response)\n  } else if (action === 'remove') {\n    dsService.remove (name, response)\n  } else if (action === 'start' ) {\n    dsService.start (name, response)\n  } else if (action === 'stop' ) {\n    dsService.stop (name, response)\n  } else if (action === 'status') {\n    dsService.status(name, response)\n  } else if (action === 'restart') {\n    dsService.restart(name, response)\n  } else {\n    console.log('Unknown action for service, please \"add\", \"remove\", \"start\", \"stop\", \"restart\" or \"status\"')\n  }\n}\n"
  },
  {
    "path": "bin/deepstream-start.ts",
    "content": "import { Command } from 'commander'\nimport { EVENT } from '@deepstream/types'\n\nexport const start = (program: Command) => {\n  program\n    .command('start', { isDefault: true })\n    .description('start a deepstream server')\n\n    .option('-c, --config [file]', 'configuration file, parent directory will be used as prefix for other config files')\n    .option('-l, --lib-dir [directory]', 'path where to lookup for plugins like connectors and logger')\n\n    .option('--host <host>', 'host for the http service')\n    .option<number>('--port <port>', 'port for the http service', (value: string) => parseInteger('--port', value))\n    .option('--disable-auth', 'Force deepstream to use \"none\" auth type')\n    .option('--disable-permissions', 'Force deepstream to use \"none\" permissions')\n    .option<string>('--log-level <level>', 'Log messages with this level and above', parseLogLevel)\n    .option<boolean>('--colors [true|false]', 'Enable or disable logging with colors', (value: string) =>  parseBoolean('--colors', value))\n    .option('--inspect <url>', 'Enable node inspector')\n    .action(action)\n}\n\nfunction action () {\n  // @ts-ignore\n  global.deepstreamCLI = this\n  // @ts-ignore\n  const inspectUrl = global.deepstreamCLI.inspect\n  if (inspectUrl) {\n    const inspector = require('inspector')\n      // @ts-ignore\n    const [host, port] = global.deepstreamCLI.inspect.split(':')\n    if (!host || !port) {\n      throw new Error('Invalid inspect url, please provide host:port')\n    }\n    inspector.open(port, host)\n  }\n\n  const { Deepstream } = require('../src/deepstream.io')\n  try {\n    const ds = new Deepstream(null)\n    ds.on(EVENT.FATAL_EXCEPTION, () => process.exit(1))\n    ds.start()\n    process\n      .removeAllListeners('SIGINT').on('SIGINT', () => {\n        ds.on('stopped', () => process.exit(0))\n        ds.stop()\n      })\n  } catch (err) {\n    console.error(err?.toString())\n    process.exit(1)\n  }\n}\n\n/**\n* Used by commander to parse the log level and fails if invalid\n* value is passed in\n*/\nfunction parseLogLevel (logLevel: string) {\n  if (!/debug|info|warn|error|off/i.test(logLevel)) {\n    console.error('Log level must be one of the following (debug|info|warn|error|off)')\n    process.exit(1)\n  }\n  return logLevel.toUpperCase()\n}\n\n/**\n* Used by commander to parse numbers and fails if invalid\n* value is passed in\n*/\nfunction parseInteger (name: string, port: string) {\n  const portNumber = Number(port)\n  if (!portNumber) {\n    console.error(`Provided ${name} must be an integer`)\n    process.exit(1)\n  }\n  return portNumber\n}\n\n/**\n* Used by commander to parse boolean and fails if invalid\n* value is passed in\n*/\nfunction parseBoolean (name: string, enabled: string) {\n  let isEnabled\n  if (typeof enabled === 'undefined' || enabled === 'true') {\n    isEnabled = true\n  } else if (typeof enabled !== 'undefined' && enabled === 'false') {\n    isEnabled = false\n  } else {\n    console.error(`Invalid argument for ${name}, please provide true or false`)\n    process.exit(1)\n  }\n  return isEnabled\n}\n"
  },
  {
    "path": "bin/deepstream.ts",
    "content": "#!/usr/bin/env node\nimport * as pgk from '../package.json'\n\nimport { Command } from 'commander'\nimport { start } from './deepstream-start'\nimport { info } from './deepstream-info'\nimport { hash } from './deepstream-hash'\nimport { service } from './deepstream-service'\nimport { daemon } from './deepstream-daemon'\nimport { verticalCluster } from './deepstream-cluster'\nimport { nginx } from './deepstream-nginx'\n\nconst program = new Command('deepstream')\nprogram\n  .usage('[command]')\n  .version(pgk.version.toString())\n\nstart(program)\ninfo(program)\nhash(program)\nservice(program)\ndaemon(program)\nverticalCluster(program)\nnginx(program)\n\nprogram.parse(process.argv)\n"
  },
  {
    "path": "conf/config.yml",
    "content": "# General\n# Each server within a cluster needs a unique name. Set to UUID to have deepstream autogenerate a unique id\nserverName: UUID\n# Show the deepstream logo on startup\nshowLogo: true\n# Plugin startup timeout – deepstream init will fail if any plugins fail to emit a 'done' event within this timeout\ndependencyInitializationTimeout: 5000\n# Directory where all plugins reside\n#libDir: ../lib\n# Exit the process a fatal error occurs, like losing a cache connection\nexitOnFatalError: false\n# Log messages with this level and above. Valid levels are 0 (DEBUG), 1 (INFO), 2 (WARN), 3 (ERROR), 4 (FATAL), 100(OFF)\nlogLevel: 0\n\n# This disables specific feature in DS, which is a more performant way\n# than disabling via permissions and is also how telemetry figures out\n# what features are enabled\nenabledFeatures:\n  record: true\n  event: true\n  rpc: true\n  presence: true\n\ntelemetry:\n  type: deepstreamIO\n  options:\n    # Disable telemetry entirely\n    enabled: false\n    # Prints whatever will be sent to the telemetry endpoint,\n    # without actually sending it\n    debug: false\n    # An anonymous uuid that allows us to know its one unique\n    # deployment. Please don't generate these randomly if using\n    # node, it really skews up analytics.\n    # deploymentId: <uuid goes here>\n\nrpc:\n  # Timeout for client RPC acknowledgement\n  ackTimeout: 1000\n  # Timeout for actual RPC provider response\n  responseTimeout: 10000\n  # Don't send requestorName by default.\n  provideRequestorName: false\n  # Don't send requestorData by default.\n  provideRequestorData: false\n\nrecord:\n  # Maximum time permitted to fetch from cache\n  cacheRetrievalTimeout: 30000\n  # Maximum time permitted to fetch from storage\n  storageRetrievalTimeout: 30000\n  # A list of prefixes that, when a record starts with one of the prefixes the\n  # records data won't be stored in the db\n  # storageExclusionPrefixes:\n  #   - no-storage/\n  #   - temporary-data/\n  # A list of prefixes that, when a record is updated via setData and it matches one of the prefixes\n  # it will be permissioned and written directly to the cache and storage layers\n  # storageHotPathPrefixes:\n  #   - analytics/\n  #   - metrics/\n\n  # Invalid configuration: data should NOT have additional properties\nlisten:\n  # Try finding a provider randomly rather than by the order they subscribed to.\n  shuffleProviders: true\n  # The amount of time to wait for a provider to acknowledge or reject a listen request\n  responseTimeout: 500\n  # The amount of time before trying to reattempt finding matches for subscriptions. This\n  # is not a cheap operation so it's recommended to raise keep this at minutes rather then\n  # second intervals if you are experiencing heavy loads\n  rematchInterval: 60000\n  # The amount of time a server will refuse to retry finding a subscriber after a previously\n  # failed attempt. This is used to avoid servers constantly trying to find a match without a\n  # cooldown period\n  matchCooldown: 10000\n\nhttpServer:\n  type: default\n  options:\n    # url path for http health-checks, GET requests to this path will return 200 if deepstream is alive\n    healthCheckPath: /health-check\n    # -- CORS --\n    # if disabled, only requests with an 'Origin' header matching one specified under 'origins'\n    # below will be permitted and the 'Access-Control-Allow-Credentials' response header will be\n    # enabled\n    allowAllOrigins: true\n    # maximum allowed size of a POST request body, in bytes, defaults to 1 MB\n    maxMessageSize: 1048576\n    # a list of allowed origins\n    origins:\n      - 'https://example.com'\n    # Options required to create an ssl app\n    # ssl:\n    #   key: fileLoad(ssl/key.pem)\n    #   cert: fileLoad(ssl/cert.pem)\n    #   ca: ...\n\n  # type: uws\n  # options:\n  #   # url path for http health-checks, GET requests to this path will return 200 if deepstream is alive\n  #   healthCheckPath: /health-check\n  #   # -- CORS --\n  #   # if disabled, only requests with an 'Origin' header matching one specified under 'origins'\n  #   # below will be permitted and the 'Access-Control-Allow-Credentials' response header will be\n  #   # enabled\n  #   allowAllOrigins: true\n  #   # a list of allowed origins\n  #   origins:\n  #     - 'https://example.com'\n  #   # maximum allowed size of a POST request body, in bytes, defaults to 1 MB\n  #   maxMessageSize: 1048576\n  #   # Headers to copy over from websocket\n  #   headers:\n  #     - user-agent\n  #   # Options required to create an ssl app\n  #   ssl:\n  #     key: file(ssl/key.pem)\n  #     cert: file(ssl/cert.pem)\n  #   ##  dhParams: ...\n  #   ##  passphrase: ...\n\n# Connection Endpoint Configuration\n# to disable, replace configuration with null eg. `http: null`\nconnectionEndpoints:\n  - type: ws-binary\n    options:\n      # url path websocket connections connect to\n      urlPath: /deepstream\n      # the amount of milliseconds between each ping/heartbeat message\n      heartbeatInterval: 30000\n      # the amount of milliseconds that writes to sockets are buffered\n      outgoingBufferTimeout: 0\n      # the maximum amount of bytes to buffer before flushing, stops the client from large enough packages\n      # to block its responsiveness\n      maxBufferByteSize: 100000\n\n      # Security\n      # should the server log invalid auth data, defaults to false\n      logInvalidAuthData: false\n      # amount of time a connection can remain open while not being logged in\n      unauthenticatedClientTimeout: 180000\n      # invalid login attempts before the connection is cut\n      maxAuthAttempts: 3\n      # maximum allowed size of an individual message in bytes\n      maxMessageSize: 1048576\n\n  # - type: ws-text\n  #   options:\n  #     # url path websocket connections connect to\n  #     urlPath: /deepstream-v3\n  #     # the amount of milliseconds between each ping/heartbeat message\n  #     heartbeatInterval: 30000\n  #     # the amount of milliseconds that writes to sockets are buffered\n  #     outgoingBufferTimeout: 0\n  #     # the maximum amount of bytes to buffer before flushing, stops the client from large enough packages\n  #     # to block its responsiveness\n  #     maxBufferByteSize: 100000\n\n  #     # Security\n  #     # should the server log invalid auth data, defaults to false\n  #     logInvalidAuthData: false\n  #     # amount of time a connection can remain open while not being logged in\n  #     unauthenticatedClientTimeout: 180000\n  #     # invalid login attempts before the connection is cut\n  #     maxAuthAttempts: 3\n  #     # maximum allowed size of an individual message in bytes\n  #     maxMessageSize: 1048576\n\n  # - type: ws-json\n  #   options:\n  #     # url path websocket connections connect to\n  #     urlPath: /deepstream-json\n  #     # the amount of milliseconds between each ping/heartbeat message\n  #     heartbeatInterval: 30000\n  #     # the amount of milliseconds that writes to sockets are buffered\n  #     outgoingBufferTimeout: 0\n  #     # the maximum amount of bytes to buffer before flushing, stops the client from large enough packages\n  #     # to block its responsiveness\n  #     maxBufferByteSize: 100000\n\n  #     # Security\n  #     # should the server log invalid auth data, defaults to false\n  #     logInvalidAuthData: false\n  #     # amount of time a connection can remain open while not being logged in\n  #     unauthenticatedClientTimeout: 180000\n  #     # invalid login attempts before the connection is cut\n  #     maxAuthAttempts: 3\n  #     # maximum allowed size of an individual message in bytes\n  #     maxMessageSize: 1048576\n\n  - type: http\n    options:\n      # allow 'authData' parameter in POST requests, if disabled only token and OPEN auth is\n      # possible\n      allowAuthData: true\n      # path for POST requests\n      postPath: /api\n      # path for GET requests\n      getPath: /api\n      # should the server log invalid auth data, defaults to false\n      logInvalidAuthData: false\n      # http request timeout in milliseconds, defaults to 20000\n      requestTimeout: 20000\n\n  # - type: mqtt\n  #   options:\n  #       # port for the mqtt server\n  #       port: 1883\n  #       # host for the mqtt server\n  #       host: 0.0.0.0\n  #       # timeout for idle devices\n  #       idleTimeout: 60000\n\n# Logger Configuration\nlogger:\n  # use the default logger, this does not currently support meta objects\n  type: default\n  options:\n    colors: true\n\n  # log using json, this supports meta objects\n  # name: pino\n  # options:\n  # # this value will always overwrite value of logLevel (line 4)\n  #   logLevel: 0\n\n  # name: winston\n  # options:\n  #   transports:\n  #     # specify a list of transports (console, file, time)\n  #     -\n  #       type: console\n  #       options:\n  #         level: verbose\n  #         colorize: true\n  #     -\n  #       type: file\n  #       level: debug\n  #       options:\n  #         filename: 'logs.json'\n  #     -\n  #       type: time\n  #       level: warn\n  #       options:\n  #         filename: time-rotated-logfile\n  #         datePattern: .yyyy-MM-dd-HH-mm\n\n# cache:\n#   name: redis\n#   options:\n#     host: ${REDIS_HOST}\n#     port: ${REDIS_PORT}\n\n# storage:\n#   name: mongodb\n#   options:\n#     connectionString: ${MONGO_CONNECTION_STRING}\n#     db: default\n\n# Authentication\nauth:\n  - type: none\n\n  # # reading users and passwords from the storage layer\n  # - type: storage\n  #   options:\n  #     # the table users are stored in storage\n  #     table: Users\n  #     # the split character used for tables (defaults to /)\n  #     tableSplitChar: string\n  #     # automatically create users if they don't exist in the database\n  #     createUser: true\n  #     # the name of a HMAC digest algorithm\n  #     hash: 'md5'\n  #     # the number of times the algorithm should be applied\n  #     iterations: 100\n  #     # the length of the resulting key\n  #     # keyLength: 32\n\n  # - type: file\n  #   options:\n  #     # Path to the user file. Can be json, js or yml\n  #     users: fileLoad(users.yml)\n  #     # the name of a HMAC digest algorithm\n  #     hash: 'md5'\n  #     # the number of times the algorithm should be applied\n  #     iterations: 100\n  #     # the length of the resulting key\n  #     keyLength: 32\n\n  # # getting permissions from a http webhook\n  # - type: http\n  #   options:\n  #     # a post request will be send to this url on every incoming connection\n  #     endpointUrl: https://someurl.com/validateLogin\n  #     # any of these will be treated as access granted\n  #     permittedStatusCodes: [ 200 ]\n  #     # if the webhook didn't respond after this amount of milliseconds, the connection will be rejected\n  #     requestTimeout: 2000\n  #     # promote the following items from the login auth data into headers\n  #     promoteToHeader:\n  #       - token\n  #     # the codes which the auth handler should retry. This is useful for when the API you depend on is\n  #     # flaky or going through a not so blue/green deployment\n  #     retryStatusCodes: [ 404, 504 ]\n  #     # the maximum amount of retries before returning a false login\n  #     retryAttempts: 3\n  #     # the time in milliseconds between retries\n  #     retryInterval: 5000\n\n# Permissioning\npermission:\n  type: config\n  options:\n    # Permissions file\n    permissions: fileLoad(permissions.yml)\n    # Amount of times nested cross-references will be loaded. Avoids endless loops\n    maxRuleIterations: 3\n    # PermissionResults are cached to increase performance. Lower number means more loading\n    cacheEvacuationInterval: 60000\n\nmonitoring:\n  - type: none\n\n  # # Allows monitoring stats to be requested via HTTP, useful for polling agents\n  # # such as LogStash\n  # - type: http\n  #   options:\n  #     url: /monitoring\n  #     allowOpenPermissions: false\n  #     headerKey: deepstream-password2\n  #     headerValue: deepstream-secret\n\n  # # Logs monitoring stats, useful for kibana where you can visualize meta data\n  # - type: log\n  #   options:\n  #     logInterval: 30000\n  #     monitoringKey: DEEPSTREAM_MONITORING\n\n# clusterNode:\n#   type: default\n#   options:\n#     host: localhost\n#     port: 6379\n\n# Custom Plugins\n# plugins:\n#   custom:\n#     path: '...'\n\n#   heap-snapshot:\n#     name: 'heap-snapshot'\n#     options:\n#       interval: 60000\n#       outputDir: file(../heap-snapshots)\n\n#   aws:\n#     name: aws\n#     options:\n#       accessKeyId: ${AWS_ACCESS_KEY}\n#       secretAccessKey: ${AWS_SECRET_ACCESS_KEY}\n#       services:\n#         - type: s3-sync\n#           options:\n#             syncInterval: 60000\n#             syncDir: file(../heap-snapshots)\n#             bucketName: ${SYNC_BUCKET_NAME}\n#             bucketRegion: ${AWS_DEFAULT_REGION}"
  },
  {
    "path": "conf/permissions.yml",
    "content": "presence:\n  \"*\":\n    allow: true\nrecord:\n  \"*\":\n    create: true\n    write: true\n    read: true\n    delete: true\n    listen: true\n    notify: true\nevent:\n  \"*\":\n    publish: true\n    subscribe: true\n    listen: true\nrpc:\n  \"*\":\n    provide: true\n    request: true\n"
  },
  {
    "path": "conf/users.yml",
    "content": "# Username\nuserA:\n  # Password hash ( hash options passed within config.yml )\n  # This hash represents clear text \"password\" with default options\n  password: \"rCOSZxJrgze2AZdVQh12c6ErDMOG0M+Rx5Yu7S5d91c=GS4SbTQYmoaGwjm2shEobg==\"\n  # Server data is used within your permission handler\n  serverData:\n    someOptional: \"auth-data\"\n  # Client data is sent to the client on login\n  clientData:\n    favouriteColor: \"red\"\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@deepstream/server\",\n  \"version\": \"10.0.0\",\n  \"description\": \"a scalable server for realtime webapps\",\n  \"main\": \"./dist/src/deepstream.io.js\",\n  \"bin\": {\n    \"deepstream\": \"./dist/bin/deepstream\"\n  },\n  \"engines\": {\n    \"node\": \">=22.0.0\"\n  },\n  \"directories\": {\n    \"test\": \"test\"\n  },\n  \"pkg\": {\n    \"scripts\": \"./dist/src/config/*.js\",\n    \"assets\": \"./dist/ascii-logo.txt\"\n  },\n  \"mocha\": {\n    \"reporter\": \"dot\",\n    \"require\": [\n      \"ts-node/register/transpile-only\",\n      \"./src/test/common.ts\"\n    ],\n    \"exit\": true\n  },\n  \"scripts\": {\n    \"start:inspect\": \"npm run tsc && node --inspect dist/bin/deepstream\",\n    \"start\": \"ts-node --transpile-only --project tsconfig.json --files ./bin/deepstream.ts start\",\n    \"tsc\": \"sh scripts/tsc.sh\",\n    \"license\": \"mkdir -p build && node scripts/license-aggregator > build/LICENSE && cat scripts/resources/missing-licenses.txt >> build/LICENSE\",\n    \"lint\": \"tslint --project .\",\n    \"lint:fix\": \"npm run lint -- --fix\",\n    \"test\": \"mocha 'src/**/*.spec.ts'\",\n    \"test:coverage\": \"nyc mocha 'src/**/*.spec.ts' && npm run test:coverage:combine\",\n    \"test:http-server\": \"node test/test-helper/start-test-server.js\",\n    \"e2e\": \"ts-node --transpile-only --project tsconfig.json --files ./node_modules/.bin/cucumber-js test-e2e --require './test-e2e/steps/**/*.ts' --exit\",\n    \"e2e:v3\": \"V3=true npm run e2e -- --tags \\\"not @V4\\\"\",\n    \"e2e:uws\": \"uws=true npm run e2e\",\n    \"e2e:uws:v3\": \"uws=true V3=true npm run e2e -- --tags \\\"not @V4\\\"\",\n    \"e2e:rpc\": \"npm run e2e -- --tags \\\"@rpcs\\\"\",\n    \"e2e:event\": \"npm run e2e -- --tags \\\"@events\\\"\",\n    \"e2e:record\": \"npm run e2e -- --tags \\\"@records\\\"\",\n    \"e2e:login\": \"npm run e2e -- --tags \\\"@login\\\"\",\n    \"e2e:presence\": \"npm run e2e -- --tags \\\"@presence\\\"\",\n    \"e2e:http\": \"npm run e2e -- --tags \\\"@http\\\"\",\n    \"e2e:coverage\": \"nyc cucumber-js test-e2e --require './test-e2e/steps/**/*.ts' --exit && npm run test:coverage:combine\",\n    \"test:all:coverage\": \"rm -rf .nyc_combined_coverage .nyc_output && npm run test:coverage && npm run e2e:coverage && nyc report --reporter=lcov -t .nyc_combined_coverage\",\n    \"test:coverage:combine\": \"rm -rf .nyc_output/processinfo && mkdir -p .nyc_combined_coverage && mv -f .nyc_output/* .nyc_combined_coverage/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/deepstreamIO/deepstream.io.git\"\n  },\n  \"dependencies\": {\n    \"@deepstream/protobuf\": \"^1.0.8\",\n    \"@deepstream/types\": \"^2.3.2\",\n    \"ajv\": \"^8.17.1\",\n    \"ajv-formats\": \"^3.0.1\",\n    \"better-ajv-errors\": \"^1.2.0\",\n    \"body-parser\": \"^2.2.0\",\n    \"chalk\": \"^4.1.2\",\n    \"commander\": \"^11.1.0\",\n    \"content-type\": \"^1.0.5\",\n    \"glob\": \"^8.1.0\",\n    \"http-shutdown\": \"^1.2.2\",\n    \"http-status\": \"^1.7.0\",\n    \"js-yaml\": \"^4.1.0\",\n    \"mqtt-connection\": \"^4.1.0\",\n    \"needle\": \"^3.2.0\",\n    \"pino\": \"^9.6.0\",\n    \"source-map-support\": \"^0.5.21\",\n    \"uuid\": \"^8.3.2\",\n    \"uWebSockets.js\": \"github:uNetworking/uWebSockets.js#v20.51.0\",\n    \"ws\": \"^7.5.9\"\n  },\n  \"devDependencies\": {\n    \"@deepstream/client\": \"^7.0.4\",\n    \"@types/body-parser\": \"^1.19.3\",\n    \"@types/content-type\": \"^1.1.6\",\n    \"@types/cucumber\": \"^6.0.1\",\n    \"@types/glob\": \"^8.1.0\",\n    \"@types/js-yaml\": \"^4.0.7\",\n    \"@types/mkdirp\": \"^1.0.1\",\n    \"@types/mocha\": \"^8.0.4\",\n    \"@types/needle\": \"^3.2.1\",\n    \"@types/node\": \"^14.18.63\",\n    \"@types/sinon\": \"^10.0.19\",\n    \"@types/sinon-chai\": \"^3.2.10\",\n    \"@types/uuid\": \"^8.3.4\",\n    \"@types/ws\": \"^7.4.7\",\n    \"@yao-pkg/pkg\": \"^6.4.0\",\n    \"async\": \"^3.2.4\",\n    \"chai\": \"^4.3.10\",\n    \"coveralls\": \"^3.1.1\",\n    \"cucumber\": \"^6.0.7\",\n    \"deepstream.io-client-js\": \"^2.3.4\",\n    \"husky\": \"^4.3.8\",\n    \"istanbul\": \"^0.4.5\",\n    \"mocha\": \"^10.2.0\",\n    \"n0p3\": \"^1.0.2\",\n    \"nyc\": \"^15.1.0\",\n    \"proxyquire\": \"^2.1.3\",\n    \"sinon\": \"^16.1.0\",\n    \"sinon-chai\": \"^3.7.0\",\n    \"ts-essentials\": \"^10.0.4\",\n    \"ts-node\": \"^10.9.1\",\n    \"tslint\": \"^6.1.3\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"author\": \"deepstreamHub GmbH\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/deepstreamIO/deepstream.io/issues\"\n  },\n  \"homepage\": \"https://deepstreamio.github.io/\",\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"npm run lint && npm run tsc\",\n      \"pre-push\": \"npm run tsc && npm t && npm run e2e -- --fail-fast && npm run e2e:uws -- --fail-fast && bash scripts/package.sh true true && node scripts/node-test.js && node scripts/executable-test.js\",\n      \"pre-publish\": \"npm run tsc\"\n    }\n  },\n  \"nyc\": {\n    \"include\": [\n      \"src/**/*.ts\"\n    ],\n    \"exclude\": [\n      \"src/**/*.spec.ts\",\n      \"src/connection-endpoint/json/*\",\n      \"src/connection-endpoint/mqtt/*\",\n      \"src/connection-endpoint/text/*\"\n    ],\n    \"extension\": [\n      \".ts\"\n    ],\n    \"require\": [\n      \"ts-node/register/transpile-only\"\n    ],\n    \"reporter\": [\n      \"lcov\"\n    ],\n    \"sourceMap\": true,\n    \"instrument\": true\n  }\n}\n"
  },
  {
    "path": "scripts/connector/package-connector.sh",
    "content": "#!/bin/bash\nPACKAGED_NODE_VERSION=\"v10\"\nOS=$( node -e \"console.log(require('os').platform())\" )\nNODE_VERSION=$( node --version )\nCOMMIT=$( git log --pretty=format:%h -n 1 )\nPACKAGE_VERSION=$( cat package.json | grep version | awk '{ print $2 }' | sed s/\\\"//g | sed s/,//g )\nPACKAGE_NAME=$( cat package.json | grep name | awk '{ print $2 }' | sed s/\\\"//g | sed s/,//g )\nPACKAGE_NAME=$( node -e \"console.log(process.argv[1].replace('@deepstream/', 'deepstream.io-'))\" $PACKAGE_NAME )\n\n# These must happen before any exits otherwise deployment would fail\n# Clean the build directory\nrm -rf build\nmkdir -p build/$PACKAGE_VERSION\n\nif ! [[ $NODE_VERSION == $PACKAGED_NODE_VERSION* ]]; then\n\techo \"Packaging only done on $PACKAGED_NODE_VERSION.x\"\n\texit\nfi\n\nif [ $OS == \"darwin\" ]; then\n\tPLATFORM=\"mac\"\nelif  [ $OS == \"linux\" ]; then\n\tPLATFORM=\"linux\"\nelif  [ $OS == \"win32\" ]; then\n\tPLATFORM=\"windows\"\nelse\n\techo \"Operating system $OS not supported for packaging\"\n\texit\nfi\n\nFILE_NAME=$PACKAGE_NAME-$PLATFORM-$PACKAGE_VERSION-$COMMIT\n\n# Do a git archive and a production install\n# to have cleanest output\ngit archive --format=zip $COMMIT -o ./build/$PACKAGE_VERSION/temp.zip\ncd ./build/$PACKAGE_VERSION\nunzip temp.zip -d $PACKAGE_NAME\n\ncd $PACKAGE_NAME\nnpm install\nnpm run tsc # Generate dist\nrm -rf node_modules\n\nnpm install --omit=dev\necho 'Installed NPM Dependencies'\n\nif [ $PLATFORM == 'mac' ]; then\n\tFILE_NAME=\"$FILE_NAME.zip\"\n\tCLEAN_FILE_NAME=\"$PACKAGE_NAME-$PLATFORM.zip\"\n\tzip -r ../$FILE_NAME .\nelif [ $PLATFORM == 'windows' ]; then\n\tFILE_NAME=\"$FILE_NAME.zip\"\n\tCLEAN_FILE_NAME=\"$PACKAGE_NAME-$PLATFORM.zip\"\n\t7z a ../$FILE_NAME .\nelse\n\tFILE_NAME=\"$FILE_NAME.tar.gz\"\n\tCLEAN_FILE_NAME=\"$PACKAGE_NAME-$PLATFORM.tar.gz\"\n\ttar czf ../$FILE_NAME .\nfi\n\ncd ..\nrm -rf $PACKAGE_NAME temp.zip\n\ncp $FILE_NAME ./$CLEAN_FILE_NAME\necho 'Done'\n"
  },
  {
    "path": "scripts/connector/test-connector.sh",
    "content": "#!/bin/bash\nset -e\n\ncurl -o deepstream_package.json https://raw.githubusercontent.com/deepstreamIO/deepstream.io/master/package.json\nDEEPSTREAM_VERSION=\"$( cat deepstream_package.json | grep version | awk '{ print $2 }' | sed s/\\\"//g | sed s/,//g )\"\n\nNODE_VERSION=$( node --version )\nOS=$( node -e \"console.log(require('os').platform())\" )\nPACKAGE_VERSION=$( cat package.json | grep version | awk '{ print $2 }' | sed s/\\\"//g | sed s/,//g )\nPACKAGE_NAME=$( cat package.json | grep name | awk '{ print $2 }' | sed s/\\\"//g | sed s/,//g )\nTYPE=$( node -e \"console.log(process.argv[1].match('@deepstream/(.*)-(.*)')[1])\" $PACKAGE_NAME )\nCONNECTOR=$( node -e \"console.log(process.argv[1].match('@deepstream/(.*)-(.*)')[2])\" $PACKAGE_NAME )\n\nrm -rf build\nmkdir build\ncd build\n\nif [ -z $1 ]; then\n\tif [[ -z ${TRAVIS_TAG} ]] && [[ -z ${APPVEYOR_REPO_TAG} ]]; then\n\t\techo \"Only runs on tags\"\n\t\texit\n\telif [[ ${APPVEYOR_REPO_TAG} = false ]]; then\n\t\techo \"On appveyor, not a tag\"\n\t\texit\n\telse\n\t\techo \"Running on tag ${TRAVIS_TAG} ${APPVEYOR_REPO_TAG}\"\n\tfi\nelse\n\techo \"Build forced although not tag\"\nfi\n\necho \"Starting deepstream.io $TYPE $CONNECTOR $PACKAGE_VERSION test for $DEEPSTREAM_VERSION on $OS\"\n\necho \"Downloading deepstream $DEEPSTREAM_VERSION\"\nif [ $OS = \"win32\" ]; then\n\tDEEPSTREAM=deepstream.io-windows-${DEEPSTREAM_VERSION}\n\tcurl -o ${DEEPSTREAM}.zip -L https://github.com/deepstreamIO/deepstream.io/releases/download/v${DEEPSTREAM_VERSION}/${DEEPSTREAM}.zip\n\t7z x ${DEEPSTREAM}.zip -o${DEEPSTREAM}\nelif [ $OS == 'darwin' ]; then\n\tDEEPSTREAM=deepstream.io-mac-${DEEPSTREAM_VERSION}\n\tcurl -o ${DEEPSTREAM}.zip -L https://github.com/deepstreamIO/deepstream.io/releases/download/v${DEEPSTREAM_VERSION}/${DEEPSTREAM}.zip\n\tunzip ${DEEPSTREAM} -d ${DEEPSTREAM}\nelse\n\tDEEPSTREAM=deepstream.io-linux-${DEEPSTREAM_VERSION}\n\tmkdir -p ${DEEPSTREAM}\n\tcurl -o ${DEEPSTREAM}.tar.gz -L https://github.com/deepstreamIO/deepstream.io/releases/download/v${DEEPSTREAM_VERSION}/${DEEPSTREAM}.tar.gz\n\ttar -xzf ${DEEPSTREAM}.tar.gz -C ${DEEPSTREAM}\nfi\n\ncd ${DEEPSTREAM}\nchmod 555 deepstream\necho \"./deepstream --version\"\n./deepstream --version\necho \"./deepstream install $TYPE $CONNECTOR:$PACKAGE_VERSION\"\n./deepstream install $TYPE $CONNECTOR:$PACKAGE_VERSION --verbose\n./deepstream start -c ../../example-config.yml -l ./lib &\n\nPROC_ID=$!\n\nsleep 10\n\nif ! [ kill -0 \"$PROC_ID\" > /dev/null 2>&1 ]; then\n\techo \"Deepstream is not running after the first ten seconds\"\n\texit 1\nfi\n\n# Rest comes on beta.5\n"
  },
  {
    "path": "scripts/details.js",
    "content": "var exec = require( 'child_process' ).execSync;\nvar fs = require( 'fs' );\nvar path = require( 'path' );\nvar pkg = require( '../package' );\n\nif( process.argv[2] === 'VERSION' ) {\n\tconsole.log( pkg.version );\n} else if( process.argv[2] === 'UWS_VERSION' ) {\n\tconsole.log( pkg.dependencies[\"uWebSockets.js\"].replace('^','') );\n} else if( process.argv[2] === 'NAME' ) {\n\tconsole.log( pkg.name );\n} else if( process.argv[2] === 'OS' ) {\n\tconsole.log( require( 'os' ).platform() );\n} else if( process.argv[2] === 'COMMIT' ) {\n\tconsole.log( exec( 'git log --pretty=format:%h -n 1' ).toString() );\n} else if( process.argv[2] === 'META' ) {\n\twriteMetaFile();\n}\telse {\n\tconsole.log( 'ERROR: Pass in VERSION or NAME as env variable' );\n}\n\nfunction writeMetaFile() {\n\tvar meta = {\n\t\tdeepstreamVersion: pkg.version,\n\t\tgitRef: exec( 'git rev-parse HEAD' ).toString().trim(),\n\t\tbuildTime: new Date().toString()\n\t};\n\tfs.writeFileSync( path.join( __dirname, '..', 'dist', 'meta.json' ), JSON.stringify( meta, null, 2 ), 'utf8' );\n}\n"
  },
  {
    "path": "scripts/executable-test.js",
    "content": "const { exec } = require('child_process')\n\nexec('./build/deepstream info', (error, stdout, stderr) => {\n    if (error) {\n        console.log(`error: ${error.message}`)\n        process.exit(1)\n    }\n    if (stderr) {\n        console.log(`stderr: ${stderr}`)\n        process.exit(1)\n    }\n    process.exit(0)\n})"
  },
  {
    "path": "scripts/license-aggregator.js",
    "content": "#!/usr/bin/env node\nconst path = require('path')\nconst fs = require('fs')\nconst child_process = require('child_process')\nconst async = require('async')\n\nconst PRE_HEADER = fs.readFileSync('LICENSE', 'utf8')\nconst HEADER = `\nThis license applies to all parts of deepstream.io that are not externally\nmaintained libraries.\n\nThe externally maintained libraries used by deepstream.io are:\n`\nconst emptyState  = \"see MISSING LICENSES at the bottom of this file\"\n\n\nif (path.basename(process.cwd()) === 'scripts') {\n  console.error('Run this script from the project root!')\n  process.exit(0)\n}\n\nchild_process.execSync('npm list --omit=dev --json > licenses.json')\nconst mainModule = require('../licenses.json')\n\nconst moduleNames = []\ntraverseDependencies(mainModule)\n\nfunction traverseDependencies(module) {\n  for (let dependency in module.dependencies) {\n    moduleNames.push(dependency)\n    traverseDependencies(module.dependencies[dependency])\n  }\n}\n\n// This source code is taken from the 'license-spelunker' npm module, it was patched\n\n/*\nThe MIT License (MIT)\n\nCopyright (c) 2013 Mike Brevoort\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\nall copies 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\nTHE SOFTWARE.\n*/\nvar projPath = path.resolve(process.argv[2] || '.')\nconsole.error('Project Path', projPath)\nvar topPkg = require(path.join(projPath, 'package.json'))\n\nvar modules = []\nvar count = 0\n\ndoLevel(projPath)\n\n\nfunction doLevel(nodePath) {\n  var pkg = require(path.join(nodePath, 'package.json'))\n  if (topPkg.name !== pkg.name && moduleNames.indexOf(pkg.name) === -1) {\n    return\n  }\n  var nodeModulesPath = path.join(nodePath, 'node_modules')\n  count ++\n\n  //console.error('package.json license', pkg.license)\n\n  fs.exists(nodeModulesPath, function (dirExists) {\n    if (dirExists) {\n      fs.readdir(nodeModulesPath, function (err, files) {\n        if (err) throw err\n        files = files.map(function (f) { return path.join(nodeModulesPath, f) })\n        async.filter(files, isModuleDirectory, (err, directories) => {\n          directories.forEach(doLevel)\n        })\n      })\n    }\n  })\n\n  licenseText(nodePath, function (license) {\n    var licenceProperty = pkg.license || pkg.licenses\n    var licenceUrl = (pkg.license || {}).url\n    if ((licenceProperty || {}).type) {\n      licenceProperty = licenceProperty.type\n      licenceUrl = licenceProperty[0].url\n    }\n    if (((licenceProperty || {})[0] || []).type) {\n      licenceProperty = licenceProperty[0].type\n      licenceUrl = licenceProperty[0].url\n    }\n\n    if (pkg.name !== topPkg.name) {\n      modules.push({\n        name: pkg.name,\n        version: pkg.version,\n        url: 'http://npmjs.org/package/' + pkg.name,\n        localPath: path.relative(projPath,nodePath),\n        pkgLicense: licenceProperty,\n        licenceUrl: licenceUrl,\n        license: license\n      })\n    }\n    count--\n\n    if (count == 0) {\n      var noLicenseFile = modules.filter(function (m) { return m.license === emptyState })\n      var andNoPkgJsonLicense = noLicenseFile.filter(function (m) { return !m.pkgLicense })\n\n      // Status report\n      // Write to StdErr\n      console.error('LICENSE FILE REPORT FOR ', topPkg.name)\n      console.error(modules.length + ' nested dependencies')\n      console.error(noLicenseFile.length +  ' without identifiable license text')\n      console.error(andNoPkgJsonLicense.length +  ' without even a package.json license declaration', '\\n\\n')\n\n      // Write to StdOut\n      console.log(PRE_HEADER)\n      console.log('')\n      console.log(HEADER)\n      modules.forEach(function(m) {\n        console.log((modules.indexOf(m)+1) + ' ----------------------------------------------------------------------------')\n        console.log(m.name + '@' + m.version)\n        console.log(m.url)\n        console.log(m.localPath)\n        if (m.pkgLicense) console.log('From package.json license property:', JSON.stringify(m.pkgLicense))\n        if (m.licenceUrl) console.log('From package.json url property:', JSON.stringify(m.licenceUrl))\n        console.log('')\n        console.log(m.license)\n        console.log('')\n      })\n    }\n  })\n}\n\nfunction licenseText (nodePath, cb) {\n  var possibleLicensePaths = [\n    path.join(nodePath, 'LICENSE'),\n    path.join(nodePath, 'LICENCE'),\n    path.join(nodePath, 'LICENSE.md'),\n    path.join(nodePath, 'LICENSE.txt'),\n    path.join(nodePath, 'LICENSE-MIT'),\n    path.join(nodePath, 'LICENSE-BSD'),\n    path.join(nodePath, 'LICENSE.BSD'),\n    path.join(nodePath, 'MIT-LICENSE.txt'),\n    path.join(nodePath, 'Readme.md'),\n    path.join(nodePath, 'README.md'),\n    path.join(nodePath, 'README.markdown')\n  ]\n\n  async.reduceRight(possibleLicensePaths, emptyState, function (state, licensePath, reduceCb) {\n    var isAReadme = (licensePath.toLowerCase().indexOf('/readme') > 0)\n\n    // if we already found a licnese, don't bother looking at READMEs\n    if (state !== emptyState && isAReadme) return reduceCb (null, state)\n\n    fs.exists(licensePath, function (exists) {\n      if (!exists) return reduceCb(null, state)\n      fs.readFile(licensePath, { encoding: 'utf8' }, function (err, text) {\n        if (err) return logError(err, reduceCb)(err, state)\n\n        if (isAReadme) {\n          var match = text.match(/\\n[# ]*license[ \\t]*\\n/i)\n          if (match) {\n            //console.log(match.input.substring(match.index))\n            return reduceCb (null, 'FROM README:\\n' + match.input.substring(match.index))\n          }\n          else {\n            return reduceCb(null, state)\n          }\n        }\n        else {\n          return reduceCb (null, text)\n        }\n\n\n        return reduceCb (null, text)\n      })\n\n    })\n  }, function (err, license) {\n    if (err) return cb('ERROR FINDING LICENSE FILE ' + err )\n    cb (license)\n  })\n}\n\nfunction isModuleDirectory (dirPath, cb) {\n  var pkgPath = path.join(dirPath, 'package.json')\n  fs.stat(dirPath, function (err, stat) {\n    if (err) return logError(err, cb)(false)\n\n    var isdir = stat.isDirectory()\n    if (isdir) {\n      fs.access(pkgPath, (err) => {\n        cb(null, !err)\n      })\n    }\n    else {\n      cb(false)\n    }\n  })\n}\n\nfunction logError(err, cb) {\n  console.error('ERROR', err)\n  return cb\n}\n"
  },
  {
    "path": "scripts/linux-package.sh",
    "content": "#!/bin/bash\nset -e\n\nif [[ -z $1 ]]; then echo \"First param is distro ( centos | debian | ubuntu | ... )\"; exit 1; fi\nif [[ -z $2 ]]; then echo \"Second param is version ( bionic | 7 | ... )\"; exit 1; fi\n\nif [[ -z $3 ]]; then\n  echo \"No distribution version provided, so using the version from package.json\"\n  curl -O https://raw.githubusercontent.com/deepstreamIO/deepstream.io/master/package.json\n  VERSION=\"$( cat package.json | grep version | awk '{ print $2 }' | sed s/\\\"//g | sed s/,//g )\"\nelse\n  VERSION=\"$3\"\nfi\n\nDISTRO=$1\nDISTRO_NAME=$2\nGIT_TAG_NAME=v${VERSION}\n\n# RPM does not support dashes in versions\nRPM_PACKAGE_VERSION=$( sed \"s/-/_/\" <<< ${VERSION} )\n\nif [[ ${DISTRO} = \"ubuntu\" ]] || [[ ${DISTRO} = \"debian\" ]]; then\n  ENV=\"deb\"\nelif [[ ${DISTRO} = \"centos\" ]] || [[ ${DISTRO} = \"fedora\" ]]; then\n  ENV=\"rpm\"\nelse\n  echo \"Unsupported distro: $DISTRO\"\n  exit 1;\nfi\n\nmkdir -p build\ncd build\n\nif [[ ${DISTRO} = 'centos' ]]; then\n  cat >Dockerfile <<EOF\nFROM centos/devtoolset-7-toolchain-centos7\nUSER root\nRUN yum install -y centos-release-scl\nENV BUILD_NODE=true\nEOF\nelse\n  cat >Dockerfile <<EOF\nFROM ${DISTRO}:${DISTRO_NAME}\nEOF\nfi\n\nif [[ ${ENV} = 'deb' ]]; then\n  cat >>Dockerfile <<EOF\nRUN apt-get update\nRUN apt-get install -y curl build-essential git ruby ruby-dev rpm\nRUN curl -sL https://deb.nodesource.com/setup_18.x | bash -\nRUN apt-get install -y nodejs\nEOF\nelse\n  cat >>Dockerfile <<EOF\nRUN yum update -y\nRUN yum install -y git curl rpmbuild ruby ruby-devel rubygems rpm-build\nRUN yum -y install gcc gcc-c++ make openssl-devel\nRUN curl --silent --location https://rpm.nodesource.com/setup_18.x | bash -\nRUN yum -y install nodejs\nEOF\nfi\n\nif [[ ${DISTRO} = 'ubuntu' ]]; then\n  cat >>Dockerfile <<EOF\nRUN apt-get install -y python python2.7\nEOF\nfi\n\ncat >>Dockerfile <<EOF\nRUN gem install fpm  --conservative || echo \"fpm install failed\"\n\nRUN echo \"Start\"\n\nRUN git config --global http.sslverify false\nRUN git clone https://github.com/deepstreamio/deepstream.io.git\n\nWORKDIR deepstream.io\nRUN mkdir build\nRUN git checkout tags/${GIT_TAG_NAME}\nRUN npm install\n\nRUN chmod 555 scripts/package.sh\n\nRUN rm scripts/package.sh\nRUN curl -s -L https://raw.githubusercontent.com/deepstreamIO/deepstream.io/master/scripts/package.sh -o scripts/package.sh\nRUN chmod 555 scripts/package.sh\nRUN ./scripts/package.sh true\nEOF\n\nif [[ $ENV = 'deb' ]]; then\n    cat >>Dockerfile <<EOF\nRUN curl \\\n-T \"build/deepstream.io_${VERSION}_amd64.deb\" \\\n-H \"X-Bintray-Publish:1\" \\\n-H \"X-Bintray-Debian-Distribution:${DISTRO_NAME}\" \\\n-H \"X-Bintray-Debian-Component:main\" \\\n-H \"X-Bintray-Debian-Architecture:amd64\" \\\n-u yasserf:${BINTRAY_API_KEY} \\\n\"https://api.bintray.com/content/deepstreamio/deb/deepstream.io/${GIT_TAG_NAME}/deepstream.io_${DISTRO_NAME}_${GIT_TAG_NAME}_amd64.deb\"\nEOF\nfi\n\nif [[ ${ENV} = 'rpm' ]]; then\n    cat >>Dockerfile <<EOF\nRUN curl \\\n-T \"build/deepstream.io-${RPM_PACKAGE_VERSION}-1.x86_64.rpm\" \\\n-H \"X-Bintray-Publish:1\" \\\n-u \"yasserf:${BINTRAY_API_KEY}\" \\\n\"https://api.bintray.com/content/deepstreamio/rpm/deepstream.io/${GIT_TAG_NAME}/deepstream.io-${DISTRO_NAME}-${VERSION}-1.x86_64.rpm\"\nEOF\nfi\n\necho \"Using Dockerfile:\"\nsed -e 's@^@  @g' Dockerfile\n\n\nTAG=\"${GIT_TAG_NAME}\"\necho \"Building Docker image ${TAG}\"\ndocker build --no-cache --tag=${TAG} .\n\necho \"Removing Dockerfile\"\nrm -f Dockerfile\n\nCIDFILE=\"cidfile\"\nARGS=\"--cidfile=$CIDFILE\"\nrm -f ${CIDFILE} # Cannot exist\n\necho \"Running build\"\ndocker run ${ARGS} ${TAG}\n\necho \"Removing container\"\ndocker rm \"$(cat ${CIDFILE})\" >/dev/null\nrm -f \"${CIDFILE}\"\n\necho \"Build successful\"\n"
  },
  {
    "path": "scripts/linux-test.sh",
    "content": "#!/bin/bash\nset -e\n\nif [ -z $1 ]; then echo \"First param is distro ( centos | debian | ubuntu | ... )\"; exit 1; fi\nif [ -z $2 ]; then echo \"Second param is version ( wheezy | 7 | ... )\"; exit 1; fi\n\nif [ -z $3 ]; then\n  echo \"No distribution version provided, so using the version from package.json\"\n  curl -o package.json https://raw.githubusercontent.com/deepstreamIO/deepstream.io/master/package.json\n  VERSION=\"$( cat package.json | grep version | awk '{ print $2 }' | sed s/\\\"//g | sed s/,//g )\"\nelse\n  VERSION=\"$3\"\nfi\n\nDISTRO=$1\nDISTRO_NAME=$2\nGIT_TAG_NAME=v$VERSION\n\nif [ $DISTRO = \"ubuntu\" ] || [ $DISTRO = \"debian\" ]; then\n  ENV=\"deb\"\nelif [ $DISTRO = \"centos\" ]; then\n  ENV=\"rpm\"\nelse\n  echo \"Unsupported distro: $DISTRO\"\n  exit 1;\nfi\n\n  cat >Dockerfile <<EOF\nFROM $DISTRO:$DISTRO_NAME\nEOF\n\nif [ $ENV = 'deb' ]; then\n  cat >>Dockerfile <<EOF\nRUN echo \"deb http://dl.bintray.com/deepstreamio/deb $DISTRO_NAME main\" | tee -a /etc/apt/sources.list\nRUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 379CE192D401AB61\nRUN apt-get update\nRUN apt-get install -y deepstream.io\nEOF\nelse\n  cat >>Dockerfile <<EOF\nRUN yum install -y wget\nRUN wget https://bintray.com/deepstreamio/rpm/rpm -O /etc/yum.repos.d/bintray-deepstreamio-rpm.repo\nRUN yum install -y deepstream.io\nEOF\nfi\n\n  cat >>Dockerfile <<EOF\nRUN deepstream --version > version\nRUN cat version\nRUN grep -q ^$VERSION version\nRUN deepstream install connector publishingtest\nEOF\n\n\necho \"Using Dockerfile:\"\nsed -e 's@^@  @g' Dockerfile\n\nTAG=\"$DEBIAN_NAME_$GIT_TAG_NAME\"\necho \"Building Docker image ${TAG}\"\ndocker build --tag=${TAG} .\n\necho \"Removing Dockerfile\"\nrm -f Dockerfile\n\nCIDFILE=\"cidfile\"\nARGS=\"--cidfile=${CIDFILE}\"\nrm -f ${CIDFILE} # Cannot exist\n\necho \"Running build\"\ndocker run ${ARGS} ${TAG}\n\necho \"Removing container\"\ndocker rm \"$(cat ${CIDFILE})\" >/dev/null\nrm -f \"${CIDFILE}\"\n\necho \"Build successful\"\n"
  },
  {
    "path": "scripts/node-test.js",
    "content": "const { Deepstream } = require('../dist/src/deepstream.io')\n\nconst server = new Deepstream({})\nserver.start()\nserver.on('stopped', () => process.exit(0))\nsetTimeout(() => server.stop(), 2000)\n"
  },
  {
    "path": "scripts/package.sh",
    "content": "#!/bin/bash\nset -e\n\nLTS_VERSION=\"22\"\nNODE_VERSION=$( node --version )\nNODE_VERSION_WITHOUT_V=$( echo ${NODE_VERSION} | cut -c2-10 )\nCOMMIT=$( node scripts/details.js COMMIT )\nPACKAGE_VERSION=$( node scripts/details.js VERSION )\nPACKAGE_NAME=$( node scripts/details.js NAME )\nOS=$( node scripts/details.js OS )\nUWS_VERSION=$( node scripts/details.js UWS_VERSION )\nPACKAGE_DIR=build/${PACKAGE_VERSION}\nDEEPSTREAM_PACKAGE=${PACKAGE_DIR}/deepstream.io\nGIT_BRANCH=$( git rev-parse --abbrev-ref HEAD )\nCREATE_DISTROS=false\n\nEXECUTABLE_NAME=\"build/deepstream\"\n\n# Needed even for void builds for travis deploy to pass\nmkdir -p build\n\nif ! [[ ${NODE_VERSION_WITHOUT_V} == ${LTS_VERSION}* ]]; then\n    echo \"Packaging only done on $LTS_VERSION.x\"\n    exit\nfi\n\nif [[ -z $1  ]]; then\n    if ! [[ ${GIT_BRANCH} = 'master' ]]; then\n        echo \"Running on branch ${GIT_BRANCH}\"\n    else\n        echo \"Running on master\"\n    fi\nfi\n\nif [[ $2 ]]; then\n    echo 'Ignoring distros'\nelif [[ ${OS} = \"linux\" ]]; then\n    CREATE_DISTROS=true\n    echo \"Checking if FPM is installed\"\n    fpm --version\nfi\n\nfunction compile {\n    echo \"Starting deepstream.io packaging with Node.js $NODE_VERSION_WITHOUT_V\"\n    rm -rf build\n    mkdir build\n\n    echo \"Installing missing npm packages, just in case something changes\"\n    npm i\n\n    echo \"Transpiling\"\n    npm run tsc\n\n    echo \"Generate License File using unmodified npm packages\"\n    ./scripts/license-aggregator.js > build/DEPENDENCIES.LICENSE\n\n    echo \"Generating meta.json\"\n    node scripts/details.js META\n\n    # Creating package structure\n    rm -rf build/${PACKAGE_VERSION}\n    mkdir -p ${DEEPSTREAM_PACKAGE}\n    mkdir ${DEEPSTREAM_PACKAGE}/var\n    mkdir ${DEEPSTREAM_PACKAGE}/lib\n    mkdir ${DEEPSTREAM_PACKAGE}/doc\n\n    cd ${DEEPSTREAM_PACKAGE}/lib\n    echo '{ \"name\": \"TEMP\" }' > package.json\n\n    echo \"Adding uWebSockets.js to libs\"\n    npm install --omit=dev --install-strategy=shallow uWebSockets.js@${UWS_VERSION}\n\n    echo \"Adding cache plugins\"\n    npm install --omit=dev --install-strategy=shallow \\\n        @deepstream/cache-redis \\\n        @deepstream/cache-memcached \\\n        # @deepstream/cache-hazelcast\n\n    echo \"Adding cluster plugins\"\n    npm install --omit=dev --install-strategy=shallow \\\n        @deepstream/clusternode-redis\n\n    echo \"Adding storage plugins\"\n    npm install --omit=dev --install-strategy=shallow \\\n        @deepstream/storage-mongodb \\\n        @deepstream/storage-rethinkdb \\\n        @deepstream/storage-elasticsearch \\\n        @deepstream/storage-postgres\n\n    echo \"Adding logger plugins\"\n    npm install --omit=dev --install-strategy=shallow \\\n        @deepstream/logger-winston\n\n    mv node_modules/* .\n    rm -rf node_modules package.json\n    cd -\n\n    echo \"Creating '$EXECUTABLE_NAME', this will take a while...\"\n    LTS=${LTS_VERSION} OS=${OS} EXECUTABLE_NAME=${EXECUTABLE_NAME} node scripts/pkg.js\n\n    PROC_ID=$!\n    SECONDS=0;\n    while kill -0 \"$PROC_ID\" >/dev/null 2>&1; do\n        echo -ne \"\\rCompiling deepstream... ($SECONDS)\"\n        sleep 10\n    done\n\n    echo \"\"\n\n    if wait ${pid}; then\n        echo -e \"\\tPkg Build Succeeded\"\n    else\n        echo -e \"\\tPkg Build Failed\"\n        exit 1\n    fi\n\n    echo \"Adding docs\"\n    echo -e \"\\tAdding Readme\"\n    echo \"Documentation is available at https://deepstreamhub.com/open-source\n    \" > ${DEEPSTREAM_PACKAGE}/doc/README\n    echo -e \"\\tAdding Changelog\"\n    cp CHANGELOG.md ${DEEPSTREAM_PACKAGE}/doc/CHANGELOG.md\n    echo -e \"\\tAdding Licenses\"\n    curl -L https://raw.githubusercontent.com/nodejs/node/v22.x/LICENSE -o ${DEEPSTREAM_PACKAGE}/doc/NODE.LICENSE\n    mv build/DEPENDENCIES.LICENSE ${DEEPSTREAM_PACKAGE}/doc/LICENSE\n\n    echo \"Moving deepstream into package structure at $DEEPSTREAM_PACKAGE\"\n    cp -r conf ${DEEPSTREAM_PACKAGE}/\n    cp build/deepstream ${DEEPSTREAM_PACKAGE}/\n\n    echo \"Patching config file for zip lib and var directories\"\n    cp -f ./conf/config.yml ${DEEPSTREAM_PACKAGE}/conf/config.yml\n\n    if [[ ${OS} = \"darwin\" ]]; then\n        sed -i '' 's@#libDir: ../lib@libDir: ../lib@' ${DEEPSTREAM_PACKAGE}/conf/config.yml\n    else\n        sed -i 's@#libDir: ../lib@libDir: ../lib@' ${DEEPSTREAM_PACKAGE}/conf/config.yml\n    fi\n}\n\nfunction windows {\n    COMMIT_NAME=\"deepstream.io-windows-$PACKAGE_VERSION-$COMMIT.zip \"\n    CLEAN_NAME=\"deepstream.io-windows-$PACKAGE_VERSION.zip\"\n\n    echo \"OS is windows\"\n    echo -e \"\\tCreating zip deepstream.io-windows-$PACKAGE_VERSION.zip\"\n    cd ${DEEPSTREAM_PACKAGE}\n    7z a ../${COMMIT_NAME} . > /dev/null\n    cp ../${COMMIT_NAME} ../${CLEAN_NAME}\n    cd -\n}\n\nfunction mac {\n    COMMIT_NAME=\"deepstream.io-mac-${PACKAGE_VERSION}-${COMMIT}\"\n    CLEAN_NAME=\"deepstream.io-mac-${PACKAGE_VERSION}\"\n\n    echo \"OS is mac\"\n    echo -e \"\\tCreating ${CLEAN_NAME}\"\n\n    cd ${DEEPSTREAM_PACKAGE}\n    zip -r ../${COMMIT_NAME}.zip .\n    cp ../${COMMIT_NAME}.zip ../${CLEAN_NAME}.zip\n    cd -\n\n    rm -rf build/osxpkg\n    mkdir -p build/osxpkg/bin\n    mkdir -p build/osxpkg/etc/deepstream\n    mkdir -p build/osxpkg/lib/deepstream\n    mkdir -p build/osxpkg/share/doc/deepstream\n    mkdir -p build/osxpkg/var/log/deepstream\n\n    cp -r ${DEEPSTREAM_PACKAGE}/deepstream build/osxpkg/bin/deepstream\n    cp -r ${DEEPSTREAM_PACKAGE}/conf/* build/osxpkg/etc/deepstream\n    cp -r ${DEEPSTREAM_PACKAGE}/lib/* build/osxpkg/lib/deepstream\n    cp -r ${DEEPSTREAM_PACKAGE}/doc/* build/osxpkg/share/doc/deepstream\n\n    echo \"Patching config file for lib and var directories\"\n    sed -i '' 's@ ../lib@ /usr/local/lib/deepstream@' build/osxpkg/etc/deepstream/config.yml\n    sed -i '' 's@ ../var@ /usr/local/var/log/deepstream@' build/osxpkg/etc/deepstream/config.yml\n\n    cp build/osxpkg/etc/deepstream/config.yml build/osxpkg/etc/deepstream/config.defaults\n\n    chmod -R 777 build/osxpkg/bin\n    chmod -R 777 build/osxpkg/share\n    chmod -R 777 build/osxpkg/var\n    chmod -R 777 build/osxpkg/lib\n    chmod -R 777 build/osxpkg/etc\n\n    echo \"\\tCreating *.pkg\"\n    pkgbuild \\\n        --root build/osxpkg \\\n        --identifier deepstream.io \\\n        --version ${PACKAGE_VERSION} \\\n        --info scripts/resources/PackageInfo \\\n        --install-location /usr/local \\\n        ${DEEPSTREAM_PACKAGE}/../${COMMIT_NAME}.pkg\n\n    cp \\\n      ${DEEPSTREAM_PACKAGE}/../${COMMIT_NAME}.pkg \\\n      ${DEEPSTREAM_PACKAGE}/../${CLEAN_NAME}.pkg\n\n    rm -rf build/osxpkg\n}\n\nfunction linux {\n    echo \"OS is linux\"\n\n    echo -e \"\\tCreating tar.gz\"\n\n    COMMIT_NAME=\"deepstream.io-linux-${PACKAGE_VERSION}-${COMMIT}.tar.gz\"\n    CLEAN_NAME=\"deepstream.io-linux-${PACKAGE_VERSION}.tar.gz\"\n\n    cd ${DEEPSTREAM_PACKAGE}\n    tar czf ../${COMMIT_NAME} .\n    cp ../${COMMIT_NAME} ../${CLEAN_NAME}\n    cd -\n}\n\nfunction distros {\n    echo -e \"\\tPatching config file for linux distros...\"\n\n    if [[ ${OS} = \"darwin\" ]]; then\n        sed -i '' 's@ ../lib@ /var/lib/deepstream@' ${DEEPSTREAM_PACKAGE}/conf/config.yml\n        sed -i '' 's@ ../var@ /var/log/deepstream@' ${DEEPSTREAM_PACKAGE}/conf/config.yml\n    else\n        sed -i 's@ ../lib@ /var/lib/deepstream@' ${DEEPSTREAM_PACKAGE}/conf/config.yml\n        sed -i 's@ ../var@ /var/log/deepstream@' ${DEEPSTREAM_PACKAGE}/conf/config.yml\n    fi\n\n    echo -e \"\\t\\tCreating rpm\"\n\n    fpm \\\n        -s dir \\\n        -t rpm \\\n        --package ./build/ \\\n        --package-name-suffix ${COMMIT} \\\n        -n deepstream.io \\\n        -v ${PACKAGE_VERSION} \\\n        --license \"MIT\" \\\n        --vendor \"deepstreamHub GmbH\" \\\n        --description \"deepstream.io rpm package\" \\\n        --url https://deepstream.io/ \\\n        -m \"<info@deepstreamhub.com>\" \\\n        --after-install ./scripts/resources/daemon/after-install \\\n        --before-remove ./scripts/resources/daemon/before-remove \\\n        --before-upgrade ./scripts/resources/daemon/before-upgrade \\\n        --after-upgrade ./scripts/resources/daemon/after-upgrade \\\n        -f \\\n        ${DEEPSTREAM_PACKAGE}/doc/=/usr/share/doc/deepstream/ \\\n        ${DEEPSTREAM_PACKAGE}/conf/=/etc/deepstream/conf.d/ \\\n        ${DEEPSTREAM_PACKAGE}/lib/=/var/lib/deepstream/ \\\n        ./build/deepstream=/usr/bin/deepstream\n\n    echo -e \"\\t\\tCreating deb\"\n    fpm \\\n        -s dir \\\n        -t deb \\\n        --package ./build \\\n        --package-name-suffix ${COMMIT} \\\n        -n deepstream.io \\\n        -v ${PACKAGE_VERSION} \\\n        --license \"MIT\" \\\n        --vendor \"deepstreamHub GmbH\" \\\n        --description \"deepstream.io deb package\" \\\n        --url https://deepstream.io/ \\\n        -m \"<info@deepstreamhub.com>\" \\\n        --after-install ./scripts/resources/daemon/after-install \\\n        --before-remove ./scripts/resources/daemon/before-remove \\\n        --before-upgrade ./scripts/resources/daemon/before-upgrade \\\n        --after-upgrade ./scripts/resources/daemon/after-upgrade \\\n        -f \\\n        --deb-no-default-config-files \\\n        ${DEEPSTREAM_PACKAGE}/doc/=/usr/share/doc/deepstream/ \\\n        ${DEEPSTREAM_PACKAGE}/conf/=/etc/deepstream/conf.d/ \\\n        ${DEEPSTREAM_PACKAGE}/lib/=/var/lib/deepstream/ \\\n        ./build/deepstream=/usr/bin/deepstream\n}\n\nfunction clean {\n    rm -rf ${DEEPSTREAM_PACKAGE}\n}\n\ncompile\n\nif [[ $OS = \"win32\" ]]; then\n    windows\nelif [[ ${OS} = \"darwin\" ]]; then\n    mac\nelif [[ ${OS} = \"linux\" ]]; then\n    linux\n    if [[ ${CREATE_DISTROS} = true ]]; then\n        distros\n    fi\nfi\n\nclean\n\necho \"Files in build directory are $( ls build/ )\"\necho \"Done\"\n"
  },
  {
    "path": "scripts/pkg.js",
    "content": "const { exec } = require('@yao-pkg/pkg')\n\nconst LTS = process.env.LTS\nlet OS = process.env.OS\n\nif (OS === 'win32') {\n  OS = 'win'\n}\nif (OS === 'darwin') {\n  OS = 'macos'\n}\n\nconst target = 'node'+ LTS + '-' + OS\n\nexec(\n  [\n    'package.json',\n    '--targets',\n    target,\n    '--output',\n    process.env.EXECUTABLE_NAME,\n    '--options',\n    '--max-old-space-size=8192',\n    '--compress',\n    'GZip'\n  ]).then(() => {\n  console.log('success')\n})\n"
  },
  {
    "path": "scripts/release.sh",
    "content": "if [ -z $1 ]; then\n\techo \"Please provide a release version: patch, minor or major\"\n\texit\nfi\n\nif [ $( npm whoami ) != \"deepstreamio\" ]; then\n\techo \"Please verify you can log into npm as deepstreamio before trying to release\"\n\texit\nfi\n\necho 'Starting release'\n\nnpm version $1\necho \"Version now: $( node scripts/details.js VERSION )\"\n\necho 'Pushing to github'\ngit push --follow-tags\n\necho \"Now we wait for the CI to build and upload artifacts to release\"\n\n\n\n\n"
  },
  {
    "path": "scripts/resources/PackageInfo",
    "content": "<?xml version=\"1.0\"?>\n<pkg-info auth=\"none\">\n</pkg-info>\n"
  },
  {
    "path": "scripts/resources/daemon/after-install",
    "content": "cp /etc/deepstream/conf.d/* /etc/deepstream/\n\nmkdir -p /var/run/deepstream\nmkdir -p /var/log/deepstream\nmkdir -p /var/lib/deepstream\n\nchmod -R 777 /var/run/deepstream\nchmod -R 777 /var/log/deepstream\nchmod -R 777 /var/lib/deepstream\nchmod -R 777 /etc/deepstream"
  },
  {
    "path": "scripts/resources/daemon/after-upgrade",
    "content": ""
  },
  {
    "path": "scripts/resources/daemon/before-remove",
    "content": "deepstream stop"
  },
  {
    "path": "scripts/resources/daemon/before-upgrade",
    "content": "cd /var/lib/deepstream\nrm -rf deepstream.io-logger-winston*\nrm -rf /etc/deepstream/conf.d\n"
  },
  {
    "path": "scripts/resources/missing-licenses.txt",
    "content": "----------------------------------------------------------------------------\n\nMISSING LICENSES\n\n----------------------------------------------------------------------------\nadm-zip@0.4.7\nhttp://npmjs.org/package/adm-zip\nnode_modules/adm-zip\nFrom package.json license property: \"MIT\"\n\nCopyright (c) 2012 Another-D-Mention Software and other contributors,\nhttp://www.another-d-mention.ro/\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n----------------------------------------------------------------------------"
  },
  {
    "path": "scripts/resources/node.rc",
    "content": "#include \"winresrc.h\"\n#include \"node_version.h\"\n\n// Application icon\n1 ICON deepstream.ico\n\n// Version resource\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION NODE_MAJOR_VERSION,NODE_MINOR_VERSION,NODE_PATCH_VERSION,0\n PRODUCTVERSION NODE_MAJOR_VERSION,NODE_MINOR_VERSION,NODE_PATCH_VERSION,0\n FILEFLAGSMASK 0x3fL\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n#  ifdef NODE_VERSION_IS_RELEASE\n    FILEFLAGS 0x0L\n#  else\n    FILEFLAGS VS_FF_PRERELEASE\n#  endif\n#endif\n\n FILEOS VOS_NT_WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904b0\"\n        BEGIN\n            VALUE \"CompanyName\", \"deepstreamHub GmbH\"\n            VALUE \"ProductName\", \"deepstream.io\"\n            VALUE \"FileDescription\", \"A Scalable Server for Realtime Applications\"\n            VALUE \"FileVersion\", DEEPSTREAM_VERSION\n            VALUE \"ProductVersion\", DEEPSTREAM_VERSION\n            VALUE \"OriginalFilename\", \"deepstream.exe\"\n            VALUE \"InternalName\", \"deepstream\"\n            VALUE \"LegalCopyright\", \"Apache License 2.0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1200\n    END\nEND\n"
  },
  {
    "path": "scripts/sanity-test.sh",
    "content": "#!/bin/bash\nset -e\n\nif [[ $1 == \"deb\" ]]; then\n  source /etc/lsb-release && echo \"deb http://dl.bintray.com/deepstreamio/deb ${DISTRIB_CODENAME} main\" | sudo tee -a /etc/apt/sources.list\n  sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 379CE192D401AB61\n  sudo apt-get update\n  sudo apt-get install -y deepstream.io\nelif [[ $1 == \"rpm\" ]]; then\n  sudo yum install -y wget\n  sudo wget https://bintray.com/deepstreamio/rpm/rpm -O /etc/yum.repos.d/bintray-deepstreamio-rpm.repo\n  sudo yum install -y deepstream.io\nelif [[ $1 == \"tar\" ]]; then\n  if [[ -z $2 ]]; then\n    echo 'Missing version number when testing tar release'\n    exit 1\n  fi\n  curl -OL https://github.com/deepstreamIO/deepstream.io/releases/download/v$2/deepstream.io-linux-$2.tar.gz\n  tar xf deepstream.io-linux-$2.tar.gz\nelif [[ $1 == \"installed\" ]]; then\n  echo \"Assuming deepstream installed\"\nelse\n  echo \"use deb/rpm/tar/installed\"\n  exit 1\nfi\n\nsudo deepstream service add\nsudo deepstream service start\nsudo deepstream service status\n\nsleep 2\n\ncurl localhost:6020/health-check\n\nif [[ $? == 1 ]]; then\n  echo 'Deepstream service not running';\n  exit 1;\nfi\n\nsudo deepstream service stop\nsudo deepstream service remove\n"
  },
  {
    "path": "scripts/setup.sh",
    "content": "git clone https://github.com/deepstreamIO/deepstream.io.git\ncd deepstream.io\nsed -i 's/git@github.com:/https:\\/\\/github.com\\//' .gitmodules\ngit submodule update --init --recursive\nnpm i\nnpm run e2e:v3\n"
  },
  {
    "path": "scripts/trigger-build.sh",
    "content": "if [ -z $1 ]; then\n  echo \"Missing branch name as first arguments\"\n  exit 1\nfi\n\nbody=\"{\n  \\\"request\\\": {\n  \\\"branch\\\":\\\"$1\\\",\n  \\\"message\\\": \\\"Tag ${TRAVIS_TAG}\\\"\n}}\"\n\necho $body\ncurl -s -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json\" \\\n  -H \"Travis-API-Version: 3\" \\\n  -H \"Authorization: token ${TRAVIS_TOKEN}\" \\\n  -d \"$body\" \\\n  https://api.travis-ci.org/repo/deepstreamIO%2Fdeepstream.io/requests\n"
  },
  {
    "path": "scripts/tsc.sh",
    "content": "#!/usr/bin/env bash\nrm -rf dist\n./node_modules/.bin/tsc\ncp ./ascii-logo.txt ./dist/ascii-logo.txt\ncp ./package.json ./dist/package.json\ncp ./package-lock.json ./dist/package-lock.json\ncp -r conf ./dist/conf\ncp ./dist/bin/deepstream.js ./dist/bin/deepstream\ncp Dockerfile ./dist/Dockerfile\nchmod +x ./dist/bin/deepstream\n"
  },
  {
    "path": "src/config/config-initialiser.spec.ts",
    "content": "import { expect } from 'chai'\n\nimport * as path from 'path'\nimport * as defaultConfig from '../default-options'\nimport * as configInitialiser from './config-initialiser'\nimport { EventEmitter } from 'events'\nimport { LOG_LEVEL } from '@deepstream/types'\n\ndescribe('config-initializer', () => {\n  before(() => {\n    global.deepstreamConfDir = null\n    global.deepstreamLibDir = null\n    global.deepstreamCLI = null\n  })\n\n  describe('plugins are initialized as per configuration', () => {\n    it('loads plugins from a relative path', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.plugins = {\n        custom: {\n          path: './src/test/mock/plugin-mock',\n          options: { some: 'options' }\n        }\n      } as any\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      expect(result.services.plugins.custom.description).to.equal('mock-plugin')\n    })\n\n    it('loads plugins via module names', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.plugins = {\n        cache: {\n          path: 'n0p3',\n          options: {}\n        }\n      } as any\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      expect(result.services.toString()).to.equal('[object Object]')\n    })\n\n    it('loads plugins from a relative path and lib dir', () => {\n      global.deepstreamLibDir = './src/test/mock'\n\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.plugins = {\n        mock: {\n          path: './plugin-mock',\n          options: { some: 'options' }\n        }\n      } as any\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      expect(result.services.plugins.mock.description).to.equal('mock-plugin')\n    })\n  })\n\n  describe('translates shortcodes into paths', () => {\n    it('translates cache', () => {\n      global.deepstreamLibDir = '/foobar'\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.ERROR\n      let errored = false\n      config.plugins = {\n        cache: {\n          name: 'blablub'\n        }\n      } as any\n      try {\n        configInitialiser.initialize(new EventEmitter(), config)\n      } catch (e) {\n        errored = true\n      }\n\n      expect(errored).to.equal(true)\n    })\n  })\n\n  describe('creates the right authentication handler', () => {\n    before(() => {\n      global.deepstreamLibDir = './src/test/plugins'\n    })\n\n    it('works for authtype: none', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.auth = [{\n        type: 'none',\n        options: {}\n      }]\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      expect(result.services.authentication.description).to.equal('Open Authentication')\n    })\n\n    it('works for authtype: user', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.auth = [{\n        type: 'file',\n        options: {\n          users: {}\n        }\n      }]\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      expect(result.services.authentication.description).to.contain('File Authentication')\n    })\n\n    it('works for authtype: http', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.auth = [{\n        type: 'http',\n        options: {\n          endpointUrl: 'http://some-url.com',\n          permittedStatusCodes: [200],\n          requestTimeout: 2000,\n          retryAttempts: 2,\n          retryInterval: 50,\n          retryStatusCodes: [ 404 ]\n        }\n      }]\n\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      expect(result.services.authentication.description).to.equal('http webhook to http://some-url.com')\n    })\n\n    it('fails for missing auth sections', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      delete config.auth\n\n      expect(() => {\n        configInitialiser.initialize(new EventEmitter(), config)\n      }).to.throw('No authentication type specified')\n    })\n\n    it('allows passing a custom authentication handler', async () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.auth = [{\n        path: '../mock/authentication-handler-mock',\n        options: {\n          hello: 'there'\n        }\n      }]\n\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      await result.services.authentication.whenReady()\n    })\n\n    it('tries to find a custom authentication handler from name', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.auth = [{\n        name: 'my-custom-auth-handler',\n        options: {}\n      }]\n\n      expect(() => {\n        configInitialiser.initialize(new EventEmitter(), config)\n      }).to.throw()\n    })\n\n    it('overrides with type \"none\" when disableAuth is set', () => {\n      global.deepstreamCLI = { disableAuth: true }\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.auth = [{\n        type: 'http',\n        options: {}\n      }]\n\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      expect(result.services.authentication.description).to.equal('Open Authentication')\n      delete global.deepstreamCLI\n    })\n  })\n\n  describe('creates the permission service', () => {\n    it('creates the config permission service', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.permission = {\n        type: 'config',\n        options: {\n          path: './test-e2e/config/permissions-complex.json'\n        }\n      }\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      expect(result.services.permission.description).to.contain('Valve Permissions')\n    })\n\n    it('allows passing a custom permission handler', async () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.permission = {\n        path: '../mock/permission-handler-mock',\n        options: {\n          hello: 'there'\n        }\n      }\n\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      await result.services.permission.whenReady()\n    })\n\n    it('tries to find a custom authentication handler from name', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      config.auth = [{\n        name: 'my-custom-perm-handler',\n        options: {}\n      }]\n\n      expect(() => {\n        configInitialiser.initialize(new EventEmitter(), config)\n      }).to.throw()\n    })\n\n    it('fails for missing permission configs', () => {\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n      delete config.permission\n      expect(() => {\n        configInitialiser.initialize(new EventEmitter(), config)\n      }).to.throw('No permission type specified')\n    })\n\n    it('overrides with type \"none\" when disablePermissions is set', () => {\n      global.deepstreamCLI = { disablePermissions: true }\n      const config = defaultConfig.get()\n      config.logLevel = LOG_LEVEL.OFF\n\n      config.permission = {\n        type: 'config',\n        options: {}\n      }\n\n      const result = configInitialiser.initialize(new EventEmitter(), config)\n      expect(result.services.permission.description).to.equal('none')\n      delete global.deepstreamCLI\n    })\n  })\n})\n"
  },
  {
    "path": "src/config/config-initialiser.ts",
    "content": "import { readFileSync } from 'fs'\nimport { join } from 'path'\nimport { EOL } from 'os'\n\nimport * as utils from '../utils/utils'\nimport * as fileUtils from './file-utils'\nimport { DeepstreamConfig, DeepstreamServices, DeepstreamConnectionEndpoint, PluginConfig, DeepstreamLogger, DeepstreamAuthentication, DeepstreamPermission, LOG_LEVEL, EVENT, DeepstreamMonitoring, DeepstreamAuthenticationCombiner, DeepstreamHTTPService, DeepstreamClusterNode } from '@deepstream/types'\nimport { DistributedClusterRegistry } from '../services/cluster-registry/distributed-cluster-registry'\nimport { SingleClusterNode } from '../services/cluster-node/single-cluster-node'\nimport { VerticalClusterNode } from '../services/cluster-node/vertical-cluster-node'\nimport { DefaultSubscriptionRegistryFactory } from '../services/subscription-registry/default-subscription-registry-factory'\nimport { HTTPConnectionEndpoint } from '../connection-endpoint/http/connection-endpoint'\nimport { CombineAuthentication } from '../services/authentication/combine/combine-authentication'\nimport { OpenAuthentication } from '../services/authentication/open/open-authentication'\nimport { ConfigPermission } from '../services/permission/valve/config-permission'\nimport { OpenPermission } from '../services/permission/open/open-permission'\nimport { WSBinaryConnectionEndpoint } from '../connection-endpoint/websocket/binary/connection-endpoint'\nimport { WSTextConnectionEndpoint } from '../connection-endpoint/websocket/text/connection-endpoint'\nimport { WSJSONConnectionEndpoint } from '../connection-endpoint/websocket/json/connection-endpoint'\nimport { MQTTConnectionEndpoint } from '../connection-endpoint/mqtt/connection-endpoint'\nimport { FileBasedAuthentication } from '../services/authentication/file/file-based-authentication'\nimport { StorageBasedAuthentication } from '../services/authentication/storage/storage-based-authentication'\nimport { HttpAuthentication } from '../services/authentication/http/http-authentication'\nimport { NoopStorage } from '../services/storage/noop-storage'\nimport { LocalCache } from '../services/cache/local-cache'\nimport { StdOutLogger } from '../services/logger/std/std-out-logger'\nimport { PinoLogger } from '../services/logger/pino/pino-logger'\nimport { DeepstreamIOTelemetry } from '../services/telemetry/deepstreamio-telemetry'\n\nimport { NoopMonitoring } from '../services/monitoring/noop-monitoring'\nimport { CombineMonitoring } from '../services/monitoring/combine-monitoring'\nimport { DistributedLockRegistry } from '../services/lock/distributed-lock-registry'\nimport { DistributedStateRegistryFactory } from '../services/cluster-state/distributed-state-registry-factory'\nimport { get as getDefaultOptions } from '../default-options'\nimport Deepstream from '../deepstream.io'\nimport { NodeHTTP } from '../services/http/node/node-http'\nimport HTTPMonitoring from '../services/monitoring/http/monitoring-http'\nimport LogMonitoring from '../services/monitoring/log/monitoring-log'\nimport { InitialLogs } from './js-yaml-loader'\nimport * as configValidator from './config-validator'\nimport HeapSnapshot from '../plugins/heap-snapshot/heap-snapshot'\n\nlet commandLineArguments: any\n\nconst customPlugins = new Map()\n\nconst defaultPlugins = new Map<keyof DeepstreamServices, any>([\n  ['cache', LocalCache],\n  ['storage', NoopStorage],\n  ['logger', StdOutLogger],\n  ['locks', DistributedLockRegistry],\n  ['subscriptions', DefaultSubscriptionRegistryFactory],\n  ['clusterRegistry', DistributedClusterRegistry],\n  ['clusterStates', DistributedStateRegistryFactory],\n  ['clusterNode', SingleClusterNode],\n  ['httpService', NodeHTTP],\n])\n\nexport const mergeConnectionOptions = function (config: any) {\n  if (config && config.connectionEndpoints) {\n    const defaultConfig = getDefaultOptions()\n    for (const connectionEndpoint of config.connectionEndpoints) {\n      const defaultPlugin = defaultConfig.connectionEndpoints.find((defaultEndpoint) => defaultEndpoint.type === connectionEndpoint.type)\n      if (defaultPlugin) {\n        connectionEndpoint.options = utils.merge(defaultPlugin.options, connectionEndpoint.options)\n      }\n    }\n  }\n}\n\n/**\n * Registers plugins by name. Useful when wanting to include\n * custom plugins in a binary\n */\nexport const registerPlugin = function (name: string, construct: Function) {\n  customPlugins.set(name, construct)\n}\n\n/**\n * Takes a configuration object and instantiates functional properties.\n * CLI arguments will be considered.\n */\nexport const initialize = function (deepstream: Deepstream, config: DeepstreamConfig, initialLogs: InitialLogs = []): { config: DeepstreamConfig, services: DeepstreamServices } {\n  configValidator.validate(config)\n\n  if (config.showLogo === true) {\n    const logo = readFileSync(join(__dirname, '..', '..', '/ascii-logo.txt'), 'utf8')\n\n    process.stdout.write(logo)\n    process.stdout.write(`${EOL}=====================   starting   =====================${EOL}`)\n  }\n\n  // @ts-ignore\n  commandLineArguments = global.deepstreamCLI || {}\n  handleUUIDProperty(config)\n  mergeConnectionOptions(config)\n\n  const services = {} as DeepstreamServices\n  services.notifyFatalException = () => {\n    if (config.exitOnFatalError) {\n      process.exit(1)\n    } else {\n      deepstream.emit(EVENT.FATAL_EXCEPTION)\n    }\n  }\n  services.logger = handleLogger(config, services)\n  services.monitoring = handleMonitoringPlugins(config, services)\n\n  initialLogs.forEach((log) => {\n    switch (log.level) {\n      case LOG_LEVEL.DEBUG:\n        services.logger.debug(log.event, log.message, log.meta)\n        break\n      case LOG_LEVEL.ERROR:\n        services.logger.error(log.event, log.message, log.meta)\n        break\n      case LOG_LEVEL.INFO:\n        services.logger.info(log.event, log.message, log.meta)\n        break\n      case LOG_LEVEL.WARN:\n        services.logger.warn(log.event, log.message, log.meta)\n        break\n      case LOG_LEVEL.FATAL:\n        services.logger.fatal(log.event, log.message, log.meta)\n        break\n    }\n  })\n\n  services.subscriptions = new (resolvePluginClass(config.subscriptions, 'subscriptions', services.logger))(config.subscriptions.options, services, config)\n  services.storage = new (resolvePluginClass(config.storage, 'storage', services.logger))(config.storage.options, services, config)\n  services.cache = new (resolvePluginClass(config.cache, 'cache', services.logger))(config.cache.options, services, config)\n  services.authentication = handleAuthStrategies(config, services)\n  services.permission = handlePermissionStrategies(config, services)\n  services.connectionEndpoints = handleConnectionEndpoints(config, services)\n  services.locks = new (resolvePluginClass(config.locks, 'locks', services.logger))(config.locks.options, services, config)\n  services.clusterNode = handleClusterNode(config, services)\n  services.clusterRegistry = new (resolvePluginClass(config.clusterRegistry, 'clusterRegistry', services.logger))(config.clusterRegistry.options, services, config)\n  services.clusterStates = new (resolvePluginClass(config.clusterStates, 'clusterStates', services.logger))(config.clusterStates.options, services, config)\n  services.httpService = handleHTTPServer(config, services)\n  services.telemetry = handleTelemetry(config, services)\n  handleCustomPlugins(config, services)\n\n  return { config, services }\n}\n\n/**\n * Transform the UUID string config to a UUID in the config object.\n */\nfunction handleUUIDProperty (config: DeepstreamConfig): void {\n  if (config.serverName === 'UUID') {\n    config.serverName = utils.getUid()\n  }\n}\n\nfunction handleClusterNode  (config: DeepstreamConfig, services: any): DeepstreamClusterNode {\n  let ClusterNodeClass = defaultPlugins.get('clusterNode')\n\n  if (commandLineArguments.clusterSize) {\n    config.clusterNode.name = 'vertical'\n  }\n\n  if (config.clusterNode.name === 'vertical') {\n      ClusterNodeClass = VerticalClusterNode\n  } else if (config.clusterNode.name || config.clusterNode.path) {\n    ClusterNodeClass = resolvePluginClass(config.clusterNode, 'clusterNode', services.logger)\n    if (!ClusterNodeClass) {\n      throw new Error(`unable to resolve plugin ${config.clusterNode.name || config.clusterNode.path}`)\n    }\n  }\n\n  return new ClusterNodeClass(config.clusterNode.options, services, config)\n}\n\n/**\n * Initialize the logger and overwrite the root logLevel if it's set\n * CLI arguments will be considered.\n */\nfunction handleLogger (config: DeepstreamConfig, services: DeepstreamServices): DeepstreamLogger {\n  const configOptions = (config.logger || {}).options\n  if (commandLineArguments.colors !== undefined) {\n    configOptions.colors = commandLineArguments.colors\n  }\n  let LoggerClass = defaultPlugins.get('logger')\n  if (config.logger.name === 'pino') {\n    LoggerClass = PinoLogger\n  } else if (config.logger.name || config.logger.path) {\n    LoggerClass = resolvePluginClass(config.logger, 'logger', services.logger)\n    if (!LoggerClass) {\n      throw new Error(`unable to resolve plugin ${config.logger.name || config.logger.path}`)\n    }\n  }\n  const logger = new LoggerClass(configOptions, services, config)\n  if (logger.log) {\n    logger.debug = logger.debug || logger.log.bind(logger, LOG_LEVEL.DEBUG)\n    logger.info = logger.info || logger.log.bind(logger, LOG_LEVEL.INFO)\n    logger.warn = logger.warn || logger.log.bind(logger, LOG_LEVEL.WARN)\n    logger.error = logger.error || logger.log.bind(logger, LOG_LEVEL.ERROR)\n    logger.fatal = logger.fatal || logger.log.bind(logger, LOG_LEVEL.FATAL)\n  }\n\n  if (commandLineArguments.logLevel !== undefined) {\n    configOptions.logLevel = commandLineArguments.logLevel\n  }\n\n  if (LOG_LEVEL[configOptions.logLevel] !== undefined) {\n    if (typeof configOptions.logLevel === 'string') {\n      logger.setLogLevel(LOG_LEVEL[configOptions.logLevel])\n    } else {\n      logger.setLogLevel(configOptions.logLevel)\n    }\n  } else if (configOptions.logLevel) {\n    throw new Error (`Unknown logLevel ${LOG_LEVEL[configOptions.logLevel]}`)\n  }\n\n  return logger\n}\n\n/**\n * Handle the plugins property in the config object the connectors.\n * Plugins can be passed either as a __path__ property or as a __name__ property with\n * a naming convention: *{cache: {name: 'redis'}}* will be resolved to the\n * npm module *@deepstream/cache-redis*\n * Options to the constructor of the plugin can be passed as *options* object.\n *\n * CLI arguments will be considered.\n */\nfunction handleCustomPlugins (config: DeepstreamConfig, services: any): void {\n  services.plugins = {}\n\n  if (config.plugins == null) {\n    return\n  }\n  const plugins = { ...config.plugins }\n\n  for (const key in plugins) {\n    const plugin = plugins[key]\n    if (plugin.name === 'heap-snapshot') {\n      services.plugins[key] = new HeapSnapshot(plugin.options || {}, services)\n    } else {\n      const PluginConstructor = resolvePluginClass(plugin, 'plugin', services.logger)\n      services.plugins[key] = new PluginConstructor(plugin.options || {}, services, config)\n    }\n  }\n}\n\n/**\n * Handle connection endpoint plugin config.\n * The type is typically the protocol e.g. ws\n * Plugins can be passed either as a __path__ property or as a __name__ property with\n * a naming convetion: *{amqp: {name: 'my-plugin'}}* will be resolved to the\n * npm module *deepstream.io/connection-my-plugin*\n * Exception: the name *uws* will be resolved to deepstream.io's internal uWebSockets plugin\n * Options to the constructor of the plugin can be passed as *options* object.\n *\n * CLI arguments will be considered.\n */\nfunction handleConnectionEndpoints (config: DeepstreamConfig, services: any): DeepstreamConnectionEndpoint[] {\n  // delete any endpoints that have been set to `null`\n  for (const type in config.connectionEndpoints) {\n    if (config.connectionEndpoints[type] === null) {\n      delete config.connectionEndpoints[type]\n    }\n  }\n  if (!config.connectionEndpoints || Object.keys(config.connectionEndpoints).length === 0) {\n    throw new Error('No connection endpoints configured')\n  }\n\n  const connectionEndpoints: DeepstreamConnectionEndpoint[] = []\n  for (const plugin of config.connectionEndpoints) {\n    plugin.options = plugin.options || {}\n\n    let PluginConstructor\n    if (plugin.type === 'ws-binary') {\n      PluginConstructor = WSBinaryConnectionEndpoint\n    } else if (plugin.type === 'ws-text') {\n      PluginConstructor = WSTextConnectionEndpoint\n    } else if (plugin.type === 'ws-json') {\n      PluginConstructor = WSJSONConnectionEndpoint\n    } else if (plugin.type === 'mqtt') {\n      PluginConstructor = MQTTConnectionEndpoint\n    } else if (plugin.type === 'http') {\n      PluginConstructor = HTTPConnectionEndpoint\n    } else {\n      PluginConstructor = resolvePluginClass(plugin, 'connection', services.logger)\n    }\n\n    connectionEndpoints.push(new PluginConstructor(plugin.options, services, config))\n  }\n  return connectionEndpoints\n}\n\n/**\n * Instantiate the given plugin, which either needs a path property or a name\n * property which fits to the npm module name convention. Options will be passed\n * to the constructor.\n *\n * CLI arguments will be considered.\n */\nfunction resolvePluginClass (plugin: PluginConfig, type: string, logger: DeepstreamLogger): any {\n  if (customPlugins.has(plugin.name)) {\n    return customPlugins.get(plugin.name)\n  }\n\n  // Required for bundling via nexe\n  const req = require\n  let requirePath\n  let pluginConstructor\n  let es6Adaptor\n  if (plugin.path != null) {\n    try {\n      requirePath = fileUtils.lookupLibRequirePath(plugin.path)\n      es6Adaptor = req(requirePath)\n      pluginConstructor = es6Adaptor.default ? es6Adaptor.default : es6Adaptor\n    } catch (error) {\n      logger.fatal(EVENT.CONFIG_ERROR, `Error loading ${type} plugin via path ${requirePath}: ${error}`)\n      // Throw error due to how tests are written\n      throw new Error()\n    }\n  } else if (plugin.name != null && type) {\n    try {\n      requirePath = fileUtils.lookupLibRequirePath(`@deepstream/${type.toLowerCase()}-${plugin.name.toLowerCase()}`)\n      es6Adaptor = req(requirePath)\n    } catch (firstError) {\n      const firstPath = requirePath\n      try {\n        requirePath = fileUtils.lookupLibRequirePath(`deepstream.io-${type.toLowerCase()}-${plugin.name.toLowerCase()}`)\n        es6Adaptor = req(requirePath)\n      } catch (secondError) {\n        logger.debug(EVENT.CONFIG_ERROR, `Error loading module ${firstPath}: ${firstError}`)\n        logger.debug(EVENT.CONFIG_ERROR, `Error loading module ${requirePath}: ${secondError}`)\n        logger.fatal(EVENT.CONFIG_ERROR, 'Error loading module, exiting')\n        // Throw error due to how tests are written\n        throw new Error()\n      }\n    }\n    pluginConstructor = es6Adaptor.default ? es6Adaptor.default : es6Adaptor\n  } else if (plugin.name != null) {\n    try {\n      requirePath = fileUtils.lookupLibRequirePath(plugin.name)\n      es6Adaptor = req(requirePath)\n      pluginConstructor = es6Adaptor.default ? es6Adaptor.default : es6Adaptor\n    } catch (error) {\n      logger.fatal(EVENT.CONFIG_ERROR, `Error loading ${type} plugin via name ${plugin.name}`)\n      // Throw error due to how tests are written\n      throw new Error()\n    }\n  } else if (plugin.type === 'default' && defaultPlugins.has(type as any)) {\n    pluginConstructor = defaultPlugins.get(type as any)\n  } else {\n    // This error is used to bubble the event due to how tests are written\n    throw new Error(`Neither name nor path property found for ${type} plugin type: ${plugin.type}`)\n  }\n  return pluginConstructor\n}\n\n/**\n * Instantiates the authentication handlers registered for *config.auth.type*\n *\n * CLI arguments will be considered.\n */\nfunction handleAuthStrategies (config: DeepstreamConfig, services: DeepstreamServices): DeepstreamAuthenticationCombiner {\n  if (commandLineArguments.disableAuth) {\n    config.auth = [{\n      type: 'none',\n      options: {}\n    }]\n  }\n\n  if (!config.auth) {\n    throw new Error('No authentication type specified')\n  }\n\n  return new CombineAuthentication(config.auth.map((auth) => handleAuthStrategy(auth, config, services)))\n}\n\n/**\n * Instantiates the authentication handler registered for *config.auth.type*\n *\n * CLI arguments will be considered.\n */\nfunction handleAuthStrategy (auth: PluginConfig, config: DeepstreamConfig, services: DeepstreamServices): DeepstreamAuthentication {\n  let AuthenticationHandlerClass\n\n  const authStrategies = {\n    none: OpenAuthentication,\n    file: FileBasedAuthentication,\n    http: HttpAuthentication,\n    storage: StorageBasedAuthentication\n  }\n\n  if (auth.name || auth.path) {\n    AuthenticationHandlerClass = resolvePluginClass(auth, 'authentication', services.logger)\n    if (!AuthenticationHandlerClass) {\n      throw new Error(`unable to resolve authentication handler ${auth.name || auth.path}`)\n    }\n  } else if (auth.type && (authStrategies as any)[auth.type]) {\n    if (auth.options && auth.options.path) {\n      const req = require\n      auth.options.users = req(fileUtils.lookupConfRequirePath(auth.options.path))\n    }\n\n    AuthenticationHandlerClass = (authStrategies as any)[auth.type]\n  } else {\n    throw new Error(`Unknown authentication type ${auth.type}`)\n  }\n\n  if (config.auth.length > 1) {\n    if (!auth.options.reportInvalidParameters) auth.options.reportInvalidParameters = false\n  }\n  return new AuthenticationHandlerClass(auth.options, services, config)\n}\n\n/**\n * Instantiates the permission handler registered for *config.permission.type*\n *\n * CLI arguments will be considered.\n */\nfunction handlePermissionStrategies (config: DeepstreamConfig, services: DeepstreamServices): DeepstreamPermission {\n  const permission = config.permission\n\n  if (!config.permission) {\n    throw new Error('No permission type specified')\n  }\n\n  if (commandLineArguments.disablePermissions) {\n    config.permission.type = 'none'\n    config.permission.options = {}\n  }\n\n  let PermissionHandlerClass\n\n  const permissionStrategies = {\n    config: ConfigPermission,\n    none: OpenPermission\n  }\n\n  if (permission.name || permission.path) {\n    PermissionHandlerClass = resolvePluginClass(permission, 'permission', services.logger)\n    if (!PermissionHandlerClass) {\n      throw new Error(`unable to resolve plugin ${permission.name || permission.path}`)\n    }\n  } else if (permission.type && (permissionStrategies as any)[permission.type]) {\n    if (config.permission.options && config.permission.options.path) {\n      const req = require\n      config.permission.options.permissions = req(fileUtils.lookupConfRequirePath(config.permission.options.path))\n    }\n    PermissionHandlerClass = (permissionStrategies as any)[permission.type]\n  } else {\n    throw new Error(`Unknown permission type ${permission.type}`)\n  }\n\n  return new PermissionHandlerClass(permission.options, services, config)\n}\n\n/**\n * Instantiates the monitoring handlers registered for *config.monitoring.type*\n *\n */\nfunction handleMonitoringPlugins (config: DeepstreamConfig, services: DeepstreamServices): DeepstreamMonitoring {\n  if (!config.monitoring) {\n    config.monitoring = [{\n      type: 'none',\n      options: {}\n    }]\n  } else {\n    // this is in order to make it backwards compatible\n    if (!Array.isArray(config.monitoring)) {\n      config.monitoring = [config.monitoring]\n    }\n  }\n\n  return new CombineMonitoring(config.monitoring.map((monitoringConfig: PluginConfig) => handleMonitoring(monitoringConfig, config, services)))\n}\n\nfunction handleMonitoring (monitoringConfig: PluginConfig, config: DeepstreamConfig, services: DeepstreamServices): DeepstreamMonitoring {\n  let MonitoringClass\n\n  const monitoringPlugins = {\n    default: NoopMonitoring,\n    none: NoopMonitoring,\n    http: HTTPMonitoring,\n    log: LogMonitoring\n  }\n\n  if (monitoringConfig.name || monitoringConfig.path) {\n    return new (resolvePluginClass(monitoringConfig, 'monitoring', services.logger))(monitoringConfig.options, services, config)\n  } else if (monitoringConfig.type && (monitoringPlugins as any)[monitoringConfig.type]) {\n    MonitoringClass = (monitoringPlugins as any)[monitoringConfig.type]\n  } else {\n    throw new Error(`Unknown monitoring type ${monitoringConfig.type}`)\n  }\n\n  return new MonitoringClass(monitoringConfig.options, services, config)\n}\n\nfunction handleHTTPServer (config: DeepstreamConfig, services: DeepstreamServices): DeepstreamHTTPService {\n  let HttpServerClass\n\n  const httpPlugins = {\n    default: NodeHTTP\n  }\n\n  if (commandLineArguments.host) {\n    config.httpServer.options.host = commandLineArguments.host\n  }\n\n  if (commandLineArguments.port) {\n    config.httpServer.options.port = commandLineArguments.port\n  }\n\n  if (config.httpServer.name || config.httpServer.path) {\n    return new (resolvePluginClass(config.httpServer, 'httpServer', services.logger))(config.httpServer.options, services, config)\n  } else if (config.httpServer.type && (httpPlugins as any)[config.httpServer.type]) {\n    HttpServerClass = (httpPlugins as any)[config.httpServer.type]\n  } else if (config.httpServer.type === 'uws') {\n    try {\n      const { UWSHTTP } = require('../services/http/uws/uws-http')\n      HttpServerClass = UWSHTTP\n    } catch (e) {\n      throw new Error('Error loading uws http service, this is most likely due to uWebsocket.js not being supported on this platform')\n    }\n  } else {\n    throw new Error(`Unknown httpServer type ${config.httpServer.type}`)\n  }\n\n  return new HttpServerClass(config.httpServer.options, services, config)\n}\n\nfunction handleTelemetry (config: DeepstreamConfig, services: DeepstreamServices): DeepstreamMonitoring {\n  let TelemetryPlugin\n\n  const telemetryPlugins = {\n    deepstreamIO: DeepstreamIOTelemetry\n  }\n\n  if (config.telemetry.name || config.telemetry.path) {\n    return new (resolvePluginClass(config.telemetry, 'telemetry', services.logger))(config.telemetry.options, services, config)\n  } else if (config.telemetry.type && (telemetryPlugins as any)[config.telemetry.type]) {\n    TelemetryPlugin = (telemetryPlugins as any)[config.telemetry.type]\n  } else {\n    throw new Error(`Unknown telemetry type ${config.telemetry.type}`)\n  }\n\n  return new TelemetryPlugin(config.telemetry.options, services, config)\n}\n"
  },
  {
    "path": "src/config/config-validator.ts",
    "content": "import {Ajv} from 'ajv'\nimport addFormat from 'ajv-formats'\nimport betterAjvErrors from 'better-ajv-errors'\n\nimport { LOG_LEVEL } from '@deepstream/types'\n\nconst LogLevelValidation = {\n  type: ['integer'],\n  enum: [\n    LOG_LEVEL.DEBUG,\n    LOG_LEVEL.INFO,\n    LOG_LEVEL.WARN,\n    LOG_LEVEL.ERROR,\n    LOG_LEVEL.OFF\n  ]\n}\n\nfunction getPluginOptions (name: string, types: string[], properties: any) {\n  return {\n    [name]: {\n      type: 'object',\n      properties: {\n        type: { type: 'string', enum: types },\n        name: { type: 'string', minLength: 1 },\n        path: { type: 'string', minLength: 1 },\n        options: {\n          type: 'object',\n          properties\n        }\n      },\n      oneRequired: ['type', 'name', 'path']\n    }\n  }\n}\n\nconst generalOptions = {\n  libDir: { type: ['string', 'null'] },\n  serverName: { type: 'string', minLength: 1 },\n  showLogo: { type: 'boolean' },\n  exitOnFatalError: { type: 'boolean' },\n  dependencyInitializationTimeout: { type: 'number', minimum: 1000 },\n  logLevel: LogLevelValidation\n}\n\nconst enabledFeatures = {\n  enabledFeatures: {\n    record: { type: 'boolean' },\n    event: { type: 'boolean' },\n    rpc: { type: 'boolean' },\n    presence: { type: 'boolean' },\n    monitoring: { type: 'boolean' },\n  }\n}\n\nconst rpcOptions = {\n  rpc: {\n    type: ['object'],\n    ackTimeout: { type: 'integer', minimum: 1 },\n    responseTimeout: { type: 'integer', minimum: 1 }\n  }\n}\n\nconst recordOptions = {\n  record: {\n    type: ['object'],\n    cacheRetrievalTimeout: { type: 'integer', minimum: 50 },\n    storageRetrievalTimeout: { type: 'integer', minimum: 50 },\n    storageExclusionPrefixes: { type: ['null', 'array'], items: { type: 'string' } },\n    storageHotPathPrefixes: { type: ['null', 'array'], items: { type: 'string' } }\n  }\n}\n\nconst listenOptions = {\n  listen: {\n    type: ['object'],\n    shuffleProviders: { type: 'boolean' },\n    responseTimeout: { type: 'integer', minimum: 50 },\n    rematchInterval: { type: 'integer', minimum: 50 },\n    matchCooldown: { type: 'integer', minimum: 50 },\n  }\n}\n\nconst httpServer = getPluginOptions(\n  'httpServer',\n  ['default', 'uws'],\n  {\n      host: { type: 'string', minLength: 1 },\n      port: { type: 'integer', minimum: 1 },\n      allowAllOrigins: { type: 'boolean' },\n      origins: { type: 'array', items: { type: 'string', format: 'uri' } },\n  }\n)\n\nconst cacheOptions = getPluginOptions(\n  'cache',\n  ['default'],\n  {\n  }\n)\n\nconst storageOptions = getPluginOptions(\n  'storage',\n  ['default'],\n  {\n  }\n)\n\nconst telemetryOptions = getPluginOptions(\n  'telemetry',\n  ['deepstreamIO'],\n  {\n    enabled: { type: 'boolean' }\n  }\n)\n\nconst authenticationOptions = {\n  auth: {\n    type: 'array',\n    items: {\n      properties: {\n        type: { type: 'string', enum: ['none', 'file', 'http', 'storage'] },\n        name: { type: 'string', minLength: 1 },\n        path: { type: 'string', minLength: 1 },\n        options: {\n          type: 'object',\n          properties: {\n            hash: { type: 'string', minLength: 1 },\n            iterations: { type: 'integer', minimum: 1 },\n            keyLength: { type: 'integer', minimum: 1 },\n            createUser: { type: 'boolean' },\n            table: { type: 'string', minLength: 1 },\n            endpointUrl: { type: 'string', format: 'uri'},\n            permittedStatusCodes: { type: 'array', items: { type: 'integer' } },\n            requestTimeout: { type: 'integer', minimum: 1 },\n          }\n        }\n      }\n    }\n  }\n}\n\nconst permissionOptions = getPluginOptions(\n  'permission',\n  ['config', 'none'],\n  {\n    path: { type: 'string', minLength: 1 },\n    maxRuleIterations: { type: 'integer', minimum: 1 },\n    cacheEvacuationInterval: { type: 'integer', minimum: 1 }\n  }\n)\n\nconst connEndpointsOptions = {\n  connectionEndpoints: {\n    type: 'array',\n    items: {\n    properties: {\n          type: { type: 'string', enum: ['ws-text', 'ws-json', 'ws-binary', 'http', 'mqtt'] },\n          name: { type: 'string', minLength: 1 },\n          path: { type: 'string', minLength: 1 },\n          options: {\n            type: 'object',\n            properties: {\n              port: { type: 'integer', minimum: 1 },\n              host: { type: 'string', minLength: 1 },\n              healthCheckPath: { type: 'string', minLength: 1 },\n              maxMessageSize: { type: 'integer', minimum: 0 },\n\n              // WEBSOCKET\n              urlPath: { type: 'string', minLength: 1 },\n              heartbeatInterval: { type: 'integer', minimum: 1 },\n              outgoingBufferTimeout: { type: 'integer', minimum: 0 },\n\n              unauthenticatedClientTimeout: { type: ['integer', 'boolean'], minimum: 1 },\n              maxAuthAttempts: { type: 'integer', minimum: 1 },\n\n              // HTTP\n              allowAuthData: { type: 'boolean' },\n              enableAuthEndpoint: { type: 'boolean' },\n              authPath: { type: 'string', minLength: 1 },\n              postPath: { type: 'string', minLength: 1 },\n              getPath: { type: 'string', minLength: 1 },\n          }\n        }\n      }\n    }\n  }\n}\n\nconst loggerOptions = getPluginOptions(\n  'logger',\n  ['default', 'json'],\n  {\n    options: {\n      colors: { type: 'boolean' },\n      logLevel: LogLevelValidation\n     }\n  }\n)\n\nconst subscriptionsOptions = getPluginOptions(\n  'subscriptions',\n  ['default'],\n  {\n    subscriptionsSanityTimer: { type: 'integer', minimum: 50 },\n  }\n)\n\nconst monitoringOptions = {\n  monitoring: {\n    type: ['array', 'object'],\n    items: {\n      properties: {\n        type: { type: 'string', enum: ['none', 'log', 'http'] },\n        name: { type: 'string', minLength: 1 },\n        path: { type: 'string', minLength: 1 },\n      },\n      options: { type: 'object'}\n    }\n  }\n}\n\nconst locksOptions = getPluginOptions(\n  'locks',\n  ['default'],\n  {\n    holdTimeout: { type: 'integer', minimum: 50 },\n    requestTimeout: { type: 'integer', minimum: 50 },\n  }\n)\n\nconst clusterNodeOptions = getPluginOptions(\n  'clusterNode',\n  ['default', 'vertical'],\n  {\n  }\n)\n\nconst clusterRegistryOptions = getPluginOptions(\n  'clusterRegistry',\n  ['default'],\n  {\n    keepAliveInterval: { type: 'integer', minimum: 1 },\n    activeCheckInterval: { type: 'integer', minimum: 1 },\n    nodeInactiveTimeout: { type: 'integer', minimum: 1 },\n  }\n)\n\nconst clusterStatesOptions = getPluginOptions(\n  'clusterStates',\n  ['default'],\n  {\n    reconciliationTimeout: { type: 'integer', minimum: 1 },\n  }\n)\n\nconst customPluginsOptions = {\n  plugins: {\n    type: ['null', 'object'],\n    properties: {\n    }\n  }\n}\n\nconst schema = {\n  additionalProperties: false,\n  properties: {\n    ...generalOptions,\n    ...enabledFeatures,\n    ...rpcOptions,\n    ...recordOptions,\n    ...listenOptions,\n    ...httpServer,\n    ...connEndpointsOptions,\n    ...loggerOptions,\n    ...cacheOptions,\n    ...storageOptions,\n    ...authenticationOptions,\n    ...permissionOptions,\n    ...subscriptionsOptions,\n    ...monitoringOptions,\n    ...telemetryOptions,\n    ...locksOptions,\n    ...clusterNodeOptions,\n    ...clusterRegistryOptions,\n    ...clusterStatesOptions,\n    ...customPluginsOptions\n  }\n}\n\nexport const validate = function (config: Object): void {\n  const ajv = new Ajv({ allErrors: true, strict: false })\n  addFormat(ajv)\n  const validator = ajv.compile(schema)\n  const valid = validator(config)\n\n  if (!valid) {\n    const output = betterAjvErrors(schema, config, validator.errors ?? [], { format: 'js' })\n    console.error('There was an error validating your configuration:')\n    output.forEach((e, i) => console.error(`${i + 1})${e.error}${e.suggestion ? `. ${e.suggestion}` : ''}`))\n    process.exit(1)\n  }\n}\n"
  },
  {
    "path": "src/config/ds-info.ts",
    "content": "import * as fs from 'fs'\nimport * as path from 'path'\nimport * as os from 'os'\nimport * as glob from 'glob'\n\nexport const getDSInfo = (libDir?: string) => {\n    let meta\n    let pkg\n    try {\n        meta = require('../../meta.json')\n    } catch (err) {\n        // if deepstream is not installed as binary (source or npm)\n        pkg = require('../../package.json')\n        meta = {\n            deepstreamVersion: pkg.version,\n            ref: pkg.gitHead || pkg._resolved || 'N/A',\n            buildTime: 'N/A'\n        }\n    }\n    meta.platform = os.platform()\n    meta.arch = os.arch()\n    meta.nodeVersion = process.version\n    if (libDir) {\n        fetchLibs(libDir, meta)\n    }\n    return meta\n}\n\nconst fetchLibs = (libDir: string, meta: any) => {\n    const directory = libDir || 'lib'\n    const files = glob.sync(path.join(directory, '*', 'package.json'))\n    meta.libs = files.map((filePath: any) => {\n        const pkg = fs.readFileSync(filePath, 'utf8')\n        const object = JSON.parse(pkg)\n        return `${object.name}:${object.version}`\n    })\n}\n"
  },
  {
    "path": "src/config/file-utils.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as fileUtils from './file-utils'\nconst path = require('path')\n\ndescribe('fileUtils tests', () => {\n  it('check cases with no or a relative prefix', () => {\n        // node style path (no dot at the start and not absolute path)\n    expect(fileUtils.lookupRequirePath('foo-bar')).to.deep.equal('foo-bar')\n    expect(fileUtils.lookupRequirePath('dir/foo-bar')).to.deep.equal('dir/foo-bar')\n    expect(fileUtils.lookupRequirePath('foo-bar', 'pre')).to.deep.equal(path.resolve('pre', 'foo-bar'))\n    expect(fileUtils.lookupRequirePath('dir/foo-bar', 'pre')).to.deep.equal(path.resolve('pre', 'dir', 'foo-bar'))\n\n        // use an absolute path for the fileUtilsname\n    expect(fileUtils.lookupRequirePath('/usr/foo-bar')).to.deep.equal('/usr/foo-bar')\n    expect(fileUtils.lookupRequirePath('/usr/dir/foo-bar')).to.deep.equal('/usr/dir/foo-bar')\n    expect(fileUtils.lookupRequirePath('/usr/foo-bar', 'pre')).to.deep.equal('/usr/foo-bar')\n    expect(fileUtils.lookupRequirePath('/usr/dir/foo-bar', 'pre')).to.deep.equal('/usr/dir/foo-bar')\n\n        // use a relative path for the fileUtilsname\n    expect(fileUtils.lookupRequirePath('./foo-bar')).to.deep.equal(path.resolve('foo-bar'))\n    expect(fileUtils.lookupRequirePath('./dir/foo-bar')).to.deep.equal(path.resolve('dir', 'foo-bar'))\n    expect(fileUtils.lookupRequirePath('./foo-bar', 'pre')).to.deep.equal(path.resolve('pre', 'foo-bar'))\n    expect(fileUtils.lookupRequirePath('./dir/foo-bar', 'pre')).to.deep.equal(path.resolve('pre', 'dir', 'foo-bar'))\n  })\n\n  it('check cases with an absolute prefix', () => {\n        // node style path (no dot at the start and not absolute path)\n    expect(fileUtils.lookupRequirePath('foo-bar', '/pre')).to.deep.equal(path.resolve('/pre', 'foo-bar'))\n    expect(fileUtils.lookupRequirePath('dir/foo-bar', '/pre')).to.deep.equal(path.resolve('/pre', 'dir', 'foo-bar'))\n\n        // use an absolute path for the fileUtilsname\n    expect(fileUtils.lookupRequirePath('/usr/foo-bar', '/pre')).to.deep.equal('/usr/foo-bar')\n    expect(fileUtils.lookupRequirePath('/usr/dir/foo-bar', '/pre')).to.deep.equal('/usr/dir/foo-bar')\n\n        // use a relative path for the fileUtilsname\n    expect(fileUtils.lookupRequirePath('./foo-bar', '/pre')).to.deep.equal(path.resolve('/pre', 'foo-bar'))\n    expect(fileUtils.lookupRequirePath('./dir/foo-bar', '/pre')).to.deep.equal(path.resolve('/pre', 'dir', 'foo-bar'))\n  })\n})\n"
  },
  {
    "path": "src/config/file-utils.ts",
    "content": "import * as fs from 'fs'\nimport * as path from 'path'\n\n/**\n* Append the global library directory as the prefix to any path\n* used here\n*/\nexport const lookupLibRequirePath = function (filePath: string): string {\n  // @ts-ignore\n  return exports.lookupRequirePath(filePath, global.deepstreamLibDir)\n}\n\n/**\n* Append the global configuration directory as the prefix to any path\n* used here\n*/\nexport const lookupConfRequirePath = function (filePath: string): string {\n  // @ts-ignore\n  return exports.lookupRequirePath(filePath, global.deepstreamConfDir)\n}\n\n/**\n * Resolve a path which will be passed to *require*.\n *\n * If a prefix is not set the filePath will be returned\n * Otherwise it will either replace return a new path prepended with the prefix.\n * If the prefix is not an absolute path it will also prepend the CWD.\n *\n * file        || relative (starts with .) | absolute | else (npm module path)\n * -----------------------------------------------------------------------------\n * *prefix     || *CWD + prefix + file     | file     | *CWD + prefix + file\n * *no prefix  ||  CWD + file              | file     | file (resolved by nodes require)\n *\n * *CWD = ignore CWD if prefix is absolute\n */\nexport const lookupRequirePath = function (filePath: string, prefix?: string): string {\n  // filePath is absolute\n  if (path.parse(filePath).root !== '') {\n    return filePath\n  }\n\n  // filePath is not relative (and not absolute)\n  if (filePath[0] !== '.') {\n    if (prefix == null) {\n      return filePath\n    }\n    return resolvePrefixAndFile(filePath, prefix)\n  }\n\n  // filePath is relative, starts with .\n  if (prefix == null) {\n    return path.resolve(process.cwd(), filePath)\n  }\n  return resolvePrefixAndFile(filePath, prefix)\n}\n\n/**\n * Returns true if a file exists for a given path\n */\nexport const fileExistsSync = function (filePath: string): boolean {\n  try {\n    fs.lstatSync(filePath)\n    return true\n  } catch (e) {\n    return false\n  }\n}\n\n/**\n* Append the prefix to the current working directory,\n* or use it as an absolute path\n*/\nfunction resolvePrefixAndFile (nonAbsoluteFilePath: string, prefix: string): string {\n  // prefix is not absolute\n  if (path.parse(prefix).root === '') {\n    return path.resolve(process.cwd(), prefix, nonAbsoluteFilePath)\n  }\n\n  // prefix is absolute\n  return path.resolve(prefix, nonAbsoluteFilePath)\n}\n"
  },
  {
    "path": "src/config/js-yaml-loader.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport { spy } from 'sinon'\n\nimport * as path from 'path'\nconst proxyquire = require('proxyquire').noPreserveCache()\nconst utils = require('../utils/utils')\nconst jsYamlLoader = require('./js-yaml-loader')\n\nfunction setUpStub (fileExists?, fileContent?) {\n  const fileMock: any = {}\n  if (typeof fileExists !== 'undefined') {\n    fileMock.fileExistsSync = function () {\n      return !!fileExists\n    }\n  }\n  const fsMock: any = {}\n  if (typeof fileContent !== 'undefined') {\n    fsMock.readFileSync = function () {\n      return fileContent\n    }\n  }\n\n  const configLoader = proxyquire('./js-yaml-loader', {\n    './file-utils': fileMock,\n    'fs': fsMock\n  })\n  spy(fileMock, 'fileExistsSync')\n  spy(fsMock, 'readFileSync')\n  return {\n    configLoader,\n    fileMock\n  }\n}\n\ndescribe.skip('js-yaml-loader', () => {\n  afterEach(() => {\n    global.deepstreamConfDir = null\n    global.deepstreamLibDir = null\n    global.deepstreamCLI = null\n  })\n\n  describe('js-yaml-loader loads and parses json files', () => {\n    const jsonLoader = {\n      load: jsYamlLoader.readAndParseFile\n    }\n\n    it('initialises the loader', () => {\n      expect(typeof jsonLoader.load).to.equal('function')\n    })\n\n    it('errors if invoked with an invalid path', (done) => {\n      jsonLoader.load(null, (err, result) => {\n        expect(err.toString()).to.contain('path')\n        expect(result).to.equal(undefined)\n        done()\n      })\n    })\n\n    it('successfully loads and parses a valid JSON file', (done) => {\n      jsonLoader.load('./src/test/config/basic-valid-json.json', (err, result) => {\n        expect(err).to.equal(null)\n        expect(result).to.deep.equal({ pet: 'pug' })\n        done()\n      })\n    })\n\n    it('errors when trying to load non existant file', (done) => {\n      jsonLoader.load('./src/test/config/does-not-exist.json', (err, result) => {\n        expect(err.toString()).to.contain('no such file or directory')\n        expect(result).to.equal(undefined)\n        done()\n      })\n    })\n  })\n\n  describe('js-yaml-loader', () => {\n    it('loads the default yml file', () => {\n      const loader = jsYamlLoader\n      const result = loader.loadConfig()\n      let defaultYamlConfig = result.config\n\n      expect(result.file).to.deep.equal(path.join('conf', 'config.yml'))\n\n      // TODO\n      // expect(defaultYamlConfig.serverName).to.have.type('string')\n\n      defaultYamlConfig = utils.merge(defaultYamlConfig, {\n        permission: { type: 'none', options: null },\n        authentication: null,\n        plugins: null,\n        serverName: null,\n        logger: null\n      })\n      expect(defaultYamlConfig).not.to.equal(null)\n    })\n\n    it('tries to load yaml, js and json file and then default', () => {\n      const stub = setUpStub(false)\n\n      expect(() => {\n        stub.configLoader.loadConfig()\n      }).to.throw()\n\n      expect(stub.fileMock.fileExistsSync).to.have.callCount(28)\n\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith(path.join('conf', 'config.js'))\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith(path.join('conf', 'config.json'))\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith(path.join('conf', 'config.yml'))\n\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith('/etc/deepstream/config.js')\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith('/etc/deepstream/config.json')\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith('/etc/deepstream/config.yml')\n    })\n\n    it('load a custom yml file path', () => {\n      const stub = setUpStub()\n      const config = stub.configLoader.loadConfig('./src/test/config/config.yml').config\n      expect(stub.fileMock.fileExistsSync).to.have.callCount(1)\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith('./src/test/config/config.yml')\n      expect(config.serverName).not.to.equal(undefined)\n      expect(config.serverName).not.to.deep.equal('')\n      expect(config.serverName).not.to.deep.equal('UUID')\n      expect(config.port).to.deep.equal(1337)\n      expect(config.host).to.deep.equal('1.2.3.4')\n      expect(config.colors).to.deep.equal(false)\n      expect(config.showLogo).to.deep.equal(false)\n      // TODO\n      // expect(config.logLevel).to.deep.equal(C.LOG_LEVEL.ERROR)\n    })\n\n    it('loads a missing custom yml file path', () => {\n      const stub = setUpStub()\n      expect(() => {\n        stub.configLoader.loadConfig(null, { config: './src/test/config/does-not-exist.yml' })\n      }).to.throw('Configuration file not found at: ./src/test/config/does-not-exist.yml')\n    })\n\n    it('load a custom json file path', () => {\n      const stub = setUpStub(true, JSON.stringify({ port: 1001 }))\n      const config = stub.configLoader.loadConfig(null, { config: './foo.json' }).config\n      expect(stub.fileMock.fileExistsSync).to.have.callCount(1)\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith('./foo.json')\n      expect(config.port).to.deep.equal(1001)\n    })\n\n    it('load a custom js file path', () => {\n      const stub = setUpStub()\n\n      let config = stub.configLoader.loadConfig(null, { config: './src/test/config/config.js' }).config\n      expect(stub.fileMock.fileExistsSync).to.have.callCount(1)\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith('./src/test/config/config.js')\n      expect(config.port).to.deep.equal(1002)\n\n      config = stub.configLoader.loadConfig(null, { config: path.join(process.cwd(), 'src/test/config/config.js') }).config\n      expect(stub.fileMock.fileExistsSync).to.have.callCount(2)\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith(path.join(process.cwd(), 'test/test/config/config.js'))\n      expect(config.port).to.deep.equal(1002)\n    })\n\n    it('fails if the custom file format is not supported', () => {\n      const stub = setUpStub(true, 'content doesnt matter here')\n      expect(() => {\n        // tslint:disable-next-line:no-unused-expression\n        stub.configLoader.loadConfig(null, { config: './config.foo' }).config\n      }).to.throw('.foo is not supported as configuration file')\n    })\n\n    it('fails if the custom file was not found', () => {\n      const stub = setUpStub(false)\n      expect(() => {\n        // tslint:disable-next-line:no-unused-expression\n        stub.configLoader.loadConfig(null, { config: './not-existing-config' }).config\n      }).to.throw('Configuration file not found at: ./not-existing-config')\n      expect(stub.fileMock.fileExistsSync).to.have.callCount(1)\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith('./not-existing-config')\n    })\n\n    it('fails if the yaml file is invalid', () => {\n      const stub = setUpStub()\n      expect(() => {\n        // tslint:disable-next-line:no-unused-expression\n        stub.configLoader.loadConfig(null, { config: './src/test/config/config-broken.yml' }).config\n      }).to.throw(/asdsad: ooops/)\n      expect(stub.fileMock.fileExistsSync).to.have.callCount(1)\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith('./src/test/config/config-broken.yml')\n    })\n\n    it('fails if the js file is invalid', () => {\n      const stub = setUpStub()\n      expect(() => {\n        // tslint:disable-next-line:no-unused-expression\n        stub.configLoader.loadConfig(null, { config: './src/test/config/config-broken.js' }).config\n      }).to.throw(/foobarBreaksIt is not defined/)\n      expect(stub.fileMock.fileExistsSync).to.have.callCount(1)\n      expect(stub.fileMock.fileExistsSync).to.have.been.calledWith('./src/test/config/config-broken.js')\n    })\n  })\n\n  describe('supports environment variable substitution', () => {\n    let configLoader\n\n    beforeEach(() => {\n      process.env.ENVIRONMENT_VARIABLE_TEST_1 = 'an_environment_variable_value'\n      process.env.ENVIRONMENT_VARIABLE_TEST_2 = 'another_environment_variable_value'\n      process.env.EXAMPLE_HOST = 'host'\n      process.env.EXAMPLE_PORT = '1234'\n      configLoader = jsYamlLoader\n    })\n\n    it('does environment variable substitution for yaml', () => {\n      const config = configLoader.loadConfig(null, { config: './src/test/config/config.yml' }).config\n      expect(config.environmentvariable).to.equal('an_environment_variable_value')\n      expect(config.another.environmentvariable).to.equal('another_environment_variable_value')\n      // expect(config.thisenvironmentdoesntexist).to.equal('DOESNT_EXIST')\n      expect(config.multipleenvs).to.equal('host:1234')\n    })\n\n    it('does environment variable substitution for json', () => {\n      const config = configLoader.loadConfig(null, { config: './src/test/config/json-with-env-variables.json' }).config\n      expect(config.environmentvariable).to.equal('an_environment_variable_value')\n      expect(config.another.environmentvariable).to.equal('another_environment_variable_value')\n      // expect(config.thisenvironmentdoesntexist).to.equal('DOESNT_EXIST')\n      expect(config.multipleenvs).to.equal('host:1234')\n    })\n  })\n\n  describe('merges in deepstreamCLI options', () => {\n    let configLoader\n\n    beforeEach(() => {\n      global.deepstreamCLI = {\n        port: 5555\n      }\n      configLoader = jsYamlLoader\n    })\n\n    afterEach(() => {\n      delete process.env.deepstreamCLI\n    })\n\n    it('does cli substitution', () => {\n      const config = configLoader.loadConfig().config\n      expect(config.connectionEndpoints.websocket.options.port).to.deep.equal(5555)\n    })\n  })\n\n  describe('load plugins by relative path property', () => {\n    let services\n    beforeEach(() => {\n      const fileMock = {\n        fileExistsSync () {\n          return true\n        }\n      }\n      const fsMock = {\n        readFileSync (filePath) {\n          if (filePath === './config.json') {\n            return `{\n              \"plugins\": {\n                \"logger\": {\n                  \"path\": \"./logger\"\n                },\n                \"cache\": {\n                  \"path\": \"./cache\",\n                  \"options\": { \"foo\": 3, \"bar\": 4 }\n                }\n              }\n            }`\n          }\n          throw new Error(`should not require any other file: ${filePath}`)\n\n        }\n      }\n      const loggerModule = function (options) { return options }\n      loggerModule['@noCallThru'] = true\n      loggerModule['@global'] = true\n      class CacheModule {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      CacheModule['@noCallThru'] = true\n      CacheModule['@global'] = true\n      const configLoader = proxyquire('./js-yaml-loader', {\n        'fs': fsMock,\n        './file-utils': fileMock,\n        [path.resolve('./logger')]: loggerModule,\n        [path.resolve('./cache')]: CacheModule\n      })\n      services = configLoader.loadConfig(null, { config: './config.json' }).services\n    })\n\n    it('load plugins', () => {\n      expect(services.cache.options).to.deep.equal({ foo: 3, bar: 4 })\n    })\n  })\n\n  describe.skip('load plugins by path property (npm module style)', () => {\n    let services\n    beforeEach(() => {\n      const fileMock = {\n        fileExistsSync () {\n          return true\n        }\n      }\n      const fsMock = {\n        readFileSync (filePath) {\n          if (filePath === './config.json') {\n            return `{\n              \"plugins\": {\n                \"cache\": {\n                  \"path\": \"foo-bar-qox\",\n                  \"options\": { \"foo\": 3, \"bar\": 4 }\n                }\n              }\n            }`\n          }\n          throw new Error(`should not require any other file: ${filePath}`)\n        }\n      }\n      // tslint:disable-next-line:max-classes-per-file\n      class FooBar {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      FooBar['@noCallThru'] = true\n      FooBar['@global'] = true\n      const configLoader = proxyquire('./js-yaml-loader', {\n        'fs': fsMock,\n        './file-utils': fileMock,\n        'foo-bar-qox': FooBar\n      })\n      services = configLoader.loadConfig(null, { config: './config.json' }).services\n    })\n\n    it('load plugins', () => {\n      expect(services.cache.options).to.deep.equal({ foo: 3, bar: 4 })\n    })\n  })\n\n  describe('load plugins by name with a name convention', () => {\n    let services\n    beforeEach(() => {\n      const fileMock = {\n        fileExistsSync () {\n          return true\n        }\n      }\n      const fsMock = {\n        readFileSync (filePath) {\n          if (filePath === './config.json') {\n            return `{\n              \"plugins\": {\n                \"cache\": {\n                  \"name\": \"super-cache\",\n                  \"options\": { \"foo\": 5, \"bar\": 6 }\n                },\n                \"storage\": {\n                  \"name\": \"super-storage\",\n                  \"options\": { \"foo\": 7, \"bar\": 8 }\n                }\n              }\n            }`\n          }\n          throw new Error(`should not require any other file: ${filePath}`)\n\n        }\n      }\n      // tslint:disable-next-line:max-classes-per-file\n      class SuperCache {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      SuperCache['@noCallThru'] = true\n      SuperCache['@global'] = true\n      // tslint:disable-next-line:max-classes-per-file\n      class SuperStorage {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      SuperStorage['@noCallThru'] = true\n      SuperStorage['@global'] = true\n      const configLoader = proxyquire('./js-yaml-loader', {\n        'fs': fsMock,\n        './file-utils': fileMock,\n        'deepstream.io-cache-super-cache': SuperCache,\n        'deepstream.io-storage-super-storage': SuperStorage\n      })\n      services = configLoader.loadConfig(null, {\n        config: './config.json'\n      }).services\n    })\n\n    it('load plugins', () => {\n      expect(services.cache.options).to.deep.equal({ foo: 5, bar: 6 })\n      expect(services.storage.options).to.deep.equal({ foo: 7, bar: 8 })\n    })\n  })\n\n  describe('load plugins by name with a name convention with lib prefix', () => {\n    let services\n    beforeEach(() => {\n      const fileMock = {\n        fileExistsSync () {\n          return true\n        }\n      }\n      const fsMock = {\n        readFileSync (filePath) {\n          if (filePath === './config.json') {\n            return `{\n              \"plugins\": {\n                \"cache\": {\n                  \"name\": \"super-cache\",\n                  \"options\": { \"foo\": -1, \"bar\": -2 }\n                },\n                \"storage\": {\n                  \"name\": \"super-storage\",\n                  \"options\": { \"foo\": -3, \"bar\": -4 }\n                }\n              }\n            }`\n          }\n          throw new Error(`should not require any other file: ${filePath}`)\n\n        }\n      }\n      // tslint:disable-next-line:max-classes-per-file\n      class SuperCache {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      SuperCache['@noCallThru'] = true\n      SuperCache['@global'] = true\n      // tslint:disable-next-line:max-classes-per-file\n      class SuperStorage {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      SuperStorage['@noCallThru'] = true\n      SuperStorage['@global'] = true\n      // tslint:disable-next-line:max-classes-per-file\n      class HTTPMock {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      HTTPMock['@noCallThru'] = true\n      HTTPMock['@global'] = true\n      const configLoader = proxyquire('./js-yaml-loader', {\n        'fs': fsMock,\n        './file-utils': fileMock,\n        [path.resolve(process.cwd(), 'foobar', 'deepstream.io-cache-super-cache')]: SuperCache,\n        [path.resolve(process.cwd(), 'foobar', 'deepstream.io-storage-super-storage')]: SuperStorage,\n        [path.resolve(process.cwd(), 'foobar', 'deepstream.io-connection-http')]: HTTPMock\n      })\n      services = configLoader.loadConfig(null, {\n        config: './config.json',\n        libDir: 'foobar'\n      }).services\n    })\n\n    it('load plugins', () => {\n      expect(services.cache.options).to.deep.equal({ foo: -1, bar: -2 })\n      expect(services.storage.options).to.deep.equal({ foo: -3, bar: -4 })\n    })\n  })\n\n  describe('load plugins by name with a name convention with an absolute lib prefix', () => {\n    let services\n    beforeEach(() => {\n      const fileMock = {\n        fileExistsSync () {\n          return true\n        }\n      }\n      const fsMock = {\n        readFileSync (filePath) {\n          if (filePath === './config.json') {\n            return `{\n              \"plugins\": {\n                \"cache\": {\n                  \"name\": \"super-cache\",\n                  \"options\": { \"foo\": -1, \"bar\": -2 }\n                },\n                \"storage\": {\n                  \"name\": \"super-storage\",\n                  \"options\": { \"foo\": -3, \"bar\": -4 }\n                }\n              }\n            }`\n          }\n          throw new Error(`should not require any other file: ${filePath}`)\n\n        }\n      }\n      // tslint:disable-next-line:max-classes-per-file\n      class SuperCache {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      SuperCache['@noCallThru'] = true\n      SuperCache['@global'] = true\n      // tslint:disable-next-line:max-classes-per-file\n      class SuperStorage {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      SuperStorage['@noCallThru'] = true\n      SuperStorage['@global'] = true\n      // tslint:disable-next-line:max-classes-per-file\n      class HTTPMock {\n        public options: any\n        constructor (options) {\n          this.options = options\n        }\n      }\n      HTTPMock['@noCallThru'] = true\n      HTTPMock['@global'] = true\n      const configLoader = proxyquire('./js-yaml-loader', {\n        'fs': fsMock,\n        './file-utils': fileMock,\n        [path.resolve('/foobar', 'deepstream.io-cache-super-cache')]: SuperCache,\n        [path.resolve('/foobar', 'deepstream.io-storage-super-storage')]: SuperStorage,\n        [path.resolve('/foobar', 'deepstream.io-connection-http')]: HTTPMock\n      })\n      services = configLoader.loadConfig(null, {\n        config: './config.json',\n        libDir: '/foobar'\n      }).services\n    })\n\n    it('load plugins', () => {\n      expect(services.cache.options).to.deep.equal({ foo: -1, bar: -2 })\n      expect(services.storage.options).to.deep.equal({ foo: -3, bar: -4 })\n    })\n  })\n})\n"
  },
  {
    "path": "src/config/js-yaml-loader.ts",
    "content": "import * as fs from 'fs'\nimport * as yaml from 'js-yaml'\nimport * as path from 'path'\n\nimport { get as getDefaultOptions } from '../default-options'\nimport { merge } from '../utils/utils'\nimport { DeepstreamConfig, LOG_LEVEL, EVENT } from '@deepstream/types'\nimport Deepstream from '../deepstream.io'\nimport * as configInitializer from './config-initialiser'\nimport * as fileUtils from './file-utils'\n\nexport type InitialLogs = Array<{\n  level: LOG_LEVEL\n  message: string\n  event: any,\n  meta: any\n}>\n\nconst SUPPORTED_EXTENSIONS = ['.yml', '.yaml', '.json', '.js']\nconst DEFAULT_CONFIG_DIRS = [\n  '/etc/deepstream/conf',\n  path.join('.', 'conf', 'config'),\n  path.join('..', 'conf', 'config')\n]\n\nDEFAULT_CONFIG_DIRS.push(path.join(process.argv[1], '..', 'conf', 'config'))\nDEFAULT_CONFIG_DIRS.push(path.join(process.argv[1], '..', '..', 'conf', 'config'))\n\n/**\n * Reads and parse a general configuration file content.\n */\nexport const readAndParseFile = function (filePath: string, callback: Function): void {\n  try {\n    fs.readFile(filePath, 'utf8', (error, fileContent) => {\n      if (error) {\n        return callback(error)\n      }\n\n      try {\n        const config = parseFile(filePath, fileContent)\n        return callback(null, config)\n      } catch (parseError) {\n        return callback(parseError)\n      }\n    })\n  } catch (error) {\n    callback(error)\n  }\n}\n\n/**\n * Loads a config file without having to initialize it. Useful for one\n * off operations such as generating a hash via cli\n */\nexport const loadConfigWithoutInitialization = async function (filePath: string | null = null, initialLogs: InitialLogs = [], args?: object): Promise<{\n  config: DeepstreamConfig,\n  configPath: string\n}> {\n  // @ts-ignore\n  const argv = args || global.deepstreamCLI || {}\n  const configPath = setGlobalConfigDirectory(argv, filePath)\n\n  let configString = fs.readFileSync(configPath, { encoding: 'utf8' })\n  configString = configString.replace(/(^#)*#.*$/gm, '$1')\n  configString = configString.replace(/^\\s*\\n/gm, '')\n  configString = lookupConfigPaths(configString)\n  configString = await loadFiles(configString, initialLogs)\n\n  const rawConfig = parseFile(configPath, configString)\n  const config = extendConfig(rawConfig, argv)\n  setGlobalLibDirectory(argv, config)\n  return {\n    config,\n    configPath,\n  }\n}\n\n/**\n * Loads a file as deepstream config. CLI args have highest priority after the\n * configuration file. If some properties are not set they will be defaulted\n * to default values defined in the defaultOptions.js file.\n * Configuraiton file will be transformed to a deepstream object by evaluating\n * some properties like the plugins (logger and connectors).\n */\nexport const loadConfig = async function (deepstream: Deepstream, filePath: string | null, args?: object) {\n  const logs: InitialLogs = []\n  const config = await loadConfigWithoutInitialization(filePath, logs, args)\n  const result = configInitializer.initialize(deepstream, config.config, logs)\n  return {\n    config: result.config,\n    services: result.services,\n    file: config.configPath,\n  }\n}\n\n/**\n * Parse a general configuration file\n * These file extension ans formats are allowed:\n * .yml, .js, .json\n *\n * If no fileContent is passed the file is read synchronously\n */\nfunction parseFile<ConfigType = DeepstreamConfig> (filePath: string, fileContent: string): ConfigType {\n  const extension = path.extname(filePath)\n\n  if (extension === '.yml' || extension === '.yaml') {\n    return yaml.load(replaceEnvironmentVariables(fileContent)) as unknown as ConfigType\n  } else if (extension === '.js') {\n    return require(path.resolve(filePath))\n  } else if (extension === '.json') {\n    return JSON.parse(replaceEnvironmentVariables(fileContent))\n  } else {\n    throw new Error(`${extension} is not supported as configuration file`)\n  }\n}\n\n/**\n* Set the globalConfig prefix that will be used as the directory for ssl, permissions and auth\n* relative files within the config file\n*/\nfunction setGlobalConfigDirectory (argv: any, filePath?: string | null): string {\n  const customConfigPath =\n      argv.c ||\n      argv.config ||\n      filePath ||\n      process.env.DEEPSTREAM_CONFIG_DIRECTORY\n  const configPath = customConfigPath\n    ? verifyCustomConfigPath(customConfigPath)\n    : getDefaultConfigPath()\n\n  // @ts-ignore\n  global.deepstreamConfDir = path.dirname(configPath)\n  return configPath\n}\n\n/**\n* Set the globalLib prefix that will be used as the directory for the logger\n* and plugins within the config file\n*/\nfunction setGlobalLibDirectory (argv: any, config: DeepstreamConfig): void {\n  // @ts-ignore\n  const libDir =\n      argv.l ||\n      argv.libDir ||\n      (config.libDir && fileUtils.lookupConfRequirePath(config.libDir)) ||\n      process.env.DEEPSTREAM_LIBRARY_DIRECTORY\n  // @ts-ignore\n  global.deepstreamLibDir = libDir\n}\n\n/**\n * Augments the basic configuration with command line parameters\n * and normalizes paths within it\n */\nfunction extendConfig (config: any, argv: any): DeepstreamConfig {\n  const cliArgs = {}\n  let key\n\n  for (key in getDefaultOptions()) {\n    (cliArgs as any)[key] = argv[key]\n  }\n\n  return merge({ plugins: {} }, getDefaultOptions(), config, cliArgs) as DeepstreamConfig\n}\n\n/**\n * Checks if a config file is present at a given path\n */\nfunction verifyCustomConfigPath (configPath: string): string {\n  if (fileUtils.fileExistsSync(configPath)) {\n    return configPath\n  }\n\n  throw new Error(`Configuration file not found at: ${configPath}`)\n}\n\n/**\n * Fallback if no config path is specified. Will attempt to load the file from the default directory\n */\nfunction getDefaultConfigPath (): string {\n  let filePath\n  let i\n  let k\n\n  for (k = 0; k < DEFAULT_CONFIG_DIRS.length; k++) {\n    for (i = 0; i < SUPPORTED_EXTENSIONS.length; i++) {\n      filePath = DEFAULT_CONFIG_DIRS[k] + SUPPORTED_EXTENSIONS[i]\n      if (fileUtils.fileExistsSync(filePath)) {\n        return filePath\n      }\n    }\n  }\n\n  throw new Error('No config file found')\n}\n\n/**\n * Handle the introduction of global environment variables within\n * the yml file, allowing value substitution.\n *\n * For example:\n * ```\n * host: $HOST_NAME\n * port: $HOST_PORT\n * ```\n */\nfunction replaceEnvironmentVariables (fileContent: string): string {\n  const environmentVariable = new RegExp(/\\${([^}]+)}/g)\n  return fileContent.replace(environmentVariable, (a, b) => process.env[b] || '')\n}\n\nfunction lookupConfigPaths (fileContent: string): string {\n  const matches = fileContent.match(/file\\((.*)\\)/g)\n  if (matches) {\n    matches.forEach((match) => {\n      const [, filename] = match.match(/file\\((.*)\\)/) as any\n      fileContent = fileContent.replace(match, fileUtils.lookupConfRequirePath(filename))\n    })\n  }\n  return fileContent\n}\n\nasync function loadFiles (fileContent: string, initialLogs: InitialLogs): Promise<string> {\n  const matches = fileContent.match(/fileLoad\\((.*)\\)/g)\n  if (matches) {\n    const promises = matches.map(async (match) => {\n      const [, filename] = match.match(/fileLoad\\((.*)\\)/) as any\n      try {\n        let content: string = await new Promise((resolve, reject) =>\n          fs.readFile(fileUtils.lookupConfRequirePath(filename), { encoding: 'utf8' }, (err, data) => {\n            err ? reject(err) : resolve(data)\n          })\n        )\n        content = replaceEnvironmentVariables(content)\n        try {\n          if (['.yml', '.yaml', '.js', '.json'].includes(path.extname(filename))) {\n            content = parseFile(filename, content)\n          }\n          initialLogs.push({\n            level: LOG_LEVEL.INFO,\n            message: `Loaded content from ${fileUtils.lookupConfRequirePath(filename)} for ${match}`,\n            event: EVENT.CONFIG_TRANSFORM,\n            meta: undefined\n          })\n        } catch (e) {\n          initialLogs.push({\n            level: LOG_LEVEL.FATAL,\n            event: EVENT.CONFIG_ERROR,\n            message: `Error loading config file, invalid format in file ${fileUtils.lookupConfRequirePath(filename)} for ${match}`,\n            meta: undefined\n          })\n        }\n        fileContent = fileContent.replace(match, JSON.stringify(content))\n      } catch (e) {\n        initialLogs.push({\n          level: LOG_LEVEL.FATAL,\n          event: EVENT.CONFIG_ERROR,\n          message: `Error loading config file, missing file ${fileUtils.lookupConfRequirePath(filename)} for ${match}`,\n          meta: undefined\n        })\n      }\n    })\n    await Promise.all(promises)\n  }\n  return fileContent\n}\n"
  },
  {
    "path": "src/connection-endpoint/base/connection-endpoint.spec.ts",
    "content": "// import * as C from '../../src/constants'\n// const proxyquire = require('proxyquire').noPreserveCache()\n// import uwsMock from '../test-mocks/uws-mock'\n// import HttpMock from '../test-mocks/http-mock'\n// import LoggerMock from '../test-mocks/logger-mock'\n// import DependencyInitialiser from '../../src/utils/dependency-initialiser'\n// import PermissionHandlerMock from '../test-mocks/permission-handler-mock'\n// import AuthenticationHandlerMock from '../test-mocks/authentication-handler-mock'\n// import SocketMock from '../test-mocks/socket-mock'\n//\n// import { getTestMocks } from '../test-helper/test-mocks'\n//\n// const httpMock = new HttpMock()\n// const httpsMock = new HttpMock()\n// // since proxyquire.callThru is enabled, manually capture members from prototypes\n// httpMock.createServer = httpMock.createServer\n// httpsMock.createServer = httpsMock.createServer\n//\n// let client\n// let handshakeData\n//\n// const ConnectionEndpoint = proxyquire('../../src/message/uws/connection-endpoint', {\n//   'uws': uwsMock,\n//   'http': httpMock,\n//   'https': httpsMock,\n//   './socket-wrapper-factory': {\n//     createSocketWrapper: (options, data) => {\n//       handshakeData = data\n//       client = getTestMocks().getSocketWrapper('client')\n//       return client.socketWrapper\n//     }\n//   }\n// }).default\n//\n// let lastAuthenticatedMessage = null\n// let connectionEndpoint\n//\n// let authenticationHandlerMock\n// let config\n// let services\n//\n// describe.skip('connection endpoint', () => {\n//   beforeEach(done => {\n//     authenticationHandlerMock = new AuthenticationHandlerMock()\n//\n//     config = {\n//       unauthenticatedClientTimeout: null,\n//       maxAuthAttempts: 3,\n//       logInvalidAuthData: true,\n//       heartbeatInterval: 4000\n//     }\n//\n//     services = {\n//       authenticationHandler: authenticationHandlerMock,\n//       logger: new LoggerMock(),\n//       permission: new PermissionHandlerMock()\n//     }\n//\n//     connectionEndpoint = new ConnectionEndpoint(config, services)\n//     const depInit = new DependencyInitialiser({ config, services }, config, services, connectionEndpoint, 'connectionEndpoint')\n//     depInit.on('ready', () => {\n//       connectionEndpoint.unauthenticatedClientTimeout = 100\n//       connectionEndpoint.onMessages()\n//       connectionEndpoint.onMessages = function (socket, parsedMessages) {\n//         lastAuthenticatedMessage = parsedMessages[parsedMessages.length - 1]\n//       }\n//       connectionEndpoint.server._simulateUpgrade(new SocketMock())\n//       expect(uwsMock.lastUserData).not.to.equal(null)\n//       done()\n//     })\n//   })\n//\n//   afterEach(done => {\n//     connectionEndpoint.once('close', done)\n//     connectionEndpoint.close()\n//     client.socketWrapperMock.verify()\n//   })\n//\n//   it.skip('sets autopings on the websocket server', () => {\n//     expect(uwsMock.heartbeatInterval).to.equal(config.heartbeatInterval)\n//     expect(uwsMock.pingMessage).to.equal({\n//       topic: C.TOPIC.CONNECTION,\n//       action: CONNECTION_ACTION.PING\n//     })\n//   })\n//\n//   describe('the connection endpoint handles invalid connection messages', () => {\n//     it('handles invalid connection topic', () => {\n//       client.socketWrapperMock\n//         .expects('sendMessage')\n//         .once()\n//         .withExactArgs({\n//           topic: C.TOPIC.CONNECTION,\n//           action: CONNECTION_ACTION.INVALID_MESSAGE,\n//           originalTopic: C.TOPIC.AUTH,\n//           originalAction: AUTH_ACTION.AUTH_UNSUCCESSFUL,\n//           data: 'gibbeerish'\n//         })\n//\n//       client.socketWrapperMock\n//         .expects('destroy')\n//         .never()\n//       const message: C.Message = {\n//         topic: C.TOPIC.AUTH,\n//         action: AUTH_ACTION.AUTH_UNSUCCESSFUL,\n//         raw: 'gibbeerish'\n//       }\n//       uwsMock.messageHandler([message], client.socketWrapper)\n//     })\n//   })\n//\n//   it('the connection endpoint handles parser errors', () => {\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.PARSER,\n//         action: C.PARSER_ACTION.UNKNOWN_ACTION,\n//         data: Buffer.from('gibbeerish'),\n//         originalTopic: 5,\n//         originalAction: 177\n//       })\n//\n//     client.socketWrapperMock\n//       .expects('destroy')\n//       .withExactArgs()\n//\n//     const message: C.ParseError = {\n//       parseError: true,\n//       action: C.PARSER_ACTION.UNKNOWN_ACTION,\n//       parsedMessage: {\n//         topic: 5,\n//         action: 177\n//       },\n//       description: 'unknown RECORD action 177',\n//       raw: Buffer.from('gibbeerish')\n//     }\n//     uwsMock.messageHandler([message], client.socketWrapper)\n//   })\n//\n//   it('the connection endpoint handles invalid auth messages', () => {\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.AUTH,\n//         action: AUTH_ACTION.INVALID_MESSAGE,\n//         originalTopic: C.TOPIC.EVENT,\n//         originalAction: C.EVENT_ACTION.EMIT,\n//         data: 'gibbeerish'\n//       })\n//\n//     client.socketWrapperMock\n//       .expects('destroy')\n//       .never()\n//\n//     const message: C.Message = {\n//       topic: C.TOPIC.EVENT,\n//       action: C.EVENT_ACTION.EMIT,\n//       raw: 'gibbeerish'\n//     }\n//     uwsMock.messageHandler([message], client.socketWrapper)\n//   })\n//\n//   it('the connection endpoint handles auth null data', () => {\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.AUTH,\n//         action: AUTH_ACTION.INVALID_MESSAGE_DATA,\n//         originalAction: C.RPC_ACTION.REQUEST,\n//       })\n//\n//     client.socketWrapperMock\n//       .expects('destroy')\n//       .once()\n//       .withExactArgs()\n//\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: 'null' }], client.socketWrapper)\n//   })\n//\n//   it('the connection endpoint handles invalid auth json', () => {\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.AUTH,\n//         action: AUTH_ACTION.INVALID_MESSAGE_DATA,\n//         originalAction: C.RPC_ACTION.REQUEST,\n//       })\n//\n//     client.socketWrapperMock\n//       .expects('destroy')\n//       .once()\n//       .withExactArgs()\n//\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{ invalid }' }], client.socketWrapper)\n//   })\n//\n//   it('the connection endpoint does not route invalid auth messages to the permission', () => {\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.AUTH,\n//         parsedData: 'Invalid User',\n//         action: AUTH_ACTION.AUTH_UNSUCCESSFUL,\n//       })\n//\n//     expect(authenticationHandlerMock.lastUserValidationQueryArgs).to.equal(null)\n//     authenticationHandlerMock.nextUserValidationResult = false\n//\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"wolfram\"}' }], client.socketWrapper)\n//\n//     expect(authenticationHandlerMock.lastUserValidationQueryArgs.length).to.equal(3)\n//     expect(authenticationHandlerMock.lastUserValidationQueryArgs[1].user).to.equal('wolfram')\n//     expect(services.logger.lastLogMessage.indexOf('wolfram')).not.to.equal(-1)\n//   })\n//\n//   describe('the connection endpoint emits a client events for user with name', () => {\n//     beforeEach(() => {\n//       uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//     })\n//\n//     it('client has the correct connection data', () => {\n//       expect(handshakeData.remoteAddress).to.equal('127.0.0.1')\n//       expect(handshakeData.headers).to.not.equal(undefined)\n//     })\n//\n//     it('emits connected event for user with name', done => {\n//       connectionEndpoint.once('client-connected', socketWrapper => {\n//         expect(socketWrapper.user).to.equal('test-user')\n//         done()\n//       })\n//       uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//     })\n//\n//     it('emits disconnected event for user with name', done => {\n//       connectionEndpoint.once('client-disconnected', socketWrapper => {\n//         expect(socketWrapper.user).to.equal('test-user')\n//         done()\n//       })\n//\n//       uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//       client.socketWrapper.close()\n//     })\n//   })\n//\n//   describe('the connection endpoint doesn\\'t emit client events for user without a name', () => {\n//     beforeEach(() => {\n//       authenticationHandlerMock.nextUserIsAnonymous = true\n//       authenticationHandlerMock.nextUserValidationResult = true\n//       uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//     })\n//\n//     it('does not emit connected event', () => {\n//       const spy = spy()\n//       connectionEndpoint.once('client-connected', spy)\n//\n//       uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//\n//       expect(spy).to.have.callCount(0)\n//     })\n//\n//     it('does not emit disconnected event', () => {\n//       authenticationHandlerMock.nextUserIsAnonymous = true\n//       const spy = spy()\n//\n//       connectionEndpoint.once('client-disconnected', spy)\n//\n//       uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//       client.socketWrapper.close()\n//\n//       expect(spy).to.have.callCount(0)\n//     })\n//   })\n//\n//   it('disconnects if the number of invalid authentication attempts is exceeded', () => {\n//     authenticationHandlerMock.nextUserValidationResult = false\n//     config.maxAuthAttempts = 3\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .thrice()\n//       .withExactArgs({\n//         topic: C.TOPIC.AUTH,\n//         parsedData: 'Invalid User',\n//         action: AUTH_ACTION.AUTH_UNSUCCESSFUL,\n//       })\n//\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.AUTH,\n//         action: AUTH_ACTION.TOO_MANY_AUTH_ATTEMPTS,\n//       })\n//\n//     client.socketWrapperMock\n//       .expects('destroy')\n//       .once()\n//       .withExactArgs()\n//\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//   })\n//\n//   it('disconnects client if authentication timeout is exceeded', done => {\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.CONNECTION,\n//         action: CONNECTION_ACTION.AUTHENTICATION_TIMEOUT,\n//       })\n//\n//     client.socketWrapperMock\n//       .expects('destroy')\n//       .once()\n//       .withExactArgs()\n//\n//     setTimeout(done, 150)\n//   })\n//\n//   it.skip('authenticates valid sockets', () => {\n//     authenticationHandlerMock.nextUserValidationResult = true\n//\n//     client.socketWrapperMock\n//       .expects('destroy')\n//       .never()\n//\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.AUTH,\n//         action: AUTH_ACTION.AUTH_SUCCESSFUL,\n//         // parsedData: undefined\n//       })\n//\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//   })\n//\n//   it('notifies the permission when a client disconnects', () => {\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//\n//     client.socketWrapper.close()\n//\n//     expect(authenticationHandlerMock.onClientDisconnectCalledWith).to.equal('test-user')\n//   })\n//\n//   it('routes valid auth messages to the permission', () => {\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//     uwsMock.messageHandler([{ topic: C.TOPIC.EVENT, action: C.EVENT_ACTION.EMIT, data: 'test' }], client.socketWrapper)\n//\n//     const result = { topic: C.TOPIC.EVENT, action: C.EVENT_ACTION.EMIT, data: 'test' }\n//     expect(lastAuthenticatedMessage).to.deep.equal(result as any)\n//   })\n//\n//   it('forwards additional data for positive authentications', () => {\n//     authenticationHandlerMock.nextUserValidationResult = true\n//     authenticationHandlerMock.sendNextValidAuthWithData = true\n//\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.AUTH,\n//         action: AUTH_ACTION.AUTH_SUCCESSFUL,\n//         parsedData: 'test-data'\n//       })\n//\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//   })\n//\n//   it('connection endpoint doesn\\'t log credentials if logInvalidAuthData is set to false', () => {\n//     config.logInvalidAuthData = false\n//     authenticationHandlerMock.nextUserValidationResult = false\n//\n//     uwsMock.messageHandler([{ topic: C.TOPIC.CONNECTION, action: CONNECTION_ACTION.CHALLENGE, data: '' }], client.socketWrapper)\n//     uwsMock.messageHandler([{ topic: C.TOPIC.AUTH, action: C.RPC_ACTION.REQUEST, data: '{\"user\":\"test-user\"}' }], client.socketWrapper)\n//\n//     expect(services.logger.lastLogMessage.indexOf('wolfram')).to.equal(-1)\n//   })\n// })\n\n// const proxyquire = require('proxyquire').noPreserveCache()\n//\n// import * as uwsMock from '../test-mocks/uws-mock'\n// import HttpMock from '../test-mocks/http-mock'\n// import LoggerMock from '../test-mocks/logger-mock'\n// import PermissionHandlerMock from '../test-mocks/permission-handler-mock'\n//\n// const httpMock: any = new HttpMock()\n// const httpsMock: any = new HttpMock()\n// // since proxyquire.callThru is enabled, manually capture members from prototypes\n// httpMock.createServer = httpMock.createServer\n// httpsMock.createServer = httpsMock.createServer\n// const ConnectionEndpoint = proxyquire('../../src/message/uws/connection-endpoint', {\n//   uws: uwsMock,\n//   http: httpMock,\n//   https: httpsMock\n// }).default\n//\n// const config = {\n//   maxAuthAttempts: 3,\n//   logInvalidAuthData: true\n// }\n//\n// const services = {\n//   permission: PermissionHandlerMock,\n//   logger: new LoggerMock()\n// }\n//\n// const mockDs = { config, services }\n//\n// let connectionEndpoint\n//\n// const connectionEndpointInit = (endpointOptions, onReady) => {\n//   connectionEndpoint = new ConnectionEndpoint(endpointOptions)\n//   connectionEndpoint.setDeepstream(mockDs)\n//   connectionEndpoint.init()\n//   connectionEndpoint.on('ready', onReady)\n// }\n//\n// describe('validates HTTPS server conditions', () => {\n//   let error\n//   let sslOptions\n//   connectionEndpoint = null\n//\n//   before(() => {\n//     spyOn(httpMock, 'createServer').and.callThrough()\n//     spyOn(httpsMock, 'createServer').and.callThrough()\n//   })\n//\n//   beforeEach(() => {\n//     sslOptions = {\n//       permission: PermissionHandlerMock,\n//       logger: { log () {} }\n//     }\n//     error = { message: null }\n//   })\n//\n//   afterEach(done => {\n//     if (!connectionEndpoint || !connectionEndpoint.isReady) {\n//       done()\n//     } else {\n//       connectionEndpoint.once('close', done)\n//       connectionEndpoint.close()\n//     }\n//\n//     httpMock.createServer.resetHistory()\n//     httpsMock.createServer.resetHistory()\n//   })\n//\n//   it('creates a http connection when sslKey and sslCert are not provided', done => {\n//     connectionEndpointInit(sslOptions, () => {\n//       expect(httpMock.createServer).to.have.been.calledWith()\n//       expect(httpsMock.createServer).to.have.callCount(0)\n//       done()\n//     })\n//   })\n//\n//   it('creates a https connection when sslKey and sslCert are provided', done => {\n//     sslOptions.sslKey = 'sslPrivateKey'\n//     sslOptions.sslCert = 'sslCertificate'\n//     connectionEndpointInit(sslOptions, () => {\n//       expect(httpMock.createServer).to.have.callCount(0)\n//       expect(httpsMock.createServer).to.have.been.calledWith({ key: 'sslPrivateKey', cert: 'sslCertificate', ca: undefined })\n//       done()\n//     })\n//   })\n//\n//   it('creates a https connection when sslKey, sslCert and sslCa are provided', done => {\n//     sslOptions.sslKey = 'sslPrivateKey'\n//     sslOptions.sslCert = 'sslCertificate'\n//     sslOptions.sslCa = 'sslCertificateAuthority'\n//     connectionEndpointInit(sslOptions, () => {\n//       expect(httpMock.createServer).to.have.callCount(0)\n//       expect(httpsMock.createServer).to.have.been.calledWith({ key: 'sslPrivateKey', cert: 'sslCertificate', ca: 'sslCertificateAuthority' })\n//       done()\n//     })\n//   })\n//\n//   it('throws an exception when only sslCert is provided', () => {\n//     try {\n//       sslOptions.sslCert = 'sslCertificate'\n//       connectionEndpointInit(sslOptions, () => {})\n//     } catch (e) {\n//       error = e\n//     } finally {\n//       expect(error.message).to.equal('Must also include sslKey in order to use SSL')\n//     }\n//   })\n//\n//   it('throws an exception when only sslKey is provided', () => {\n//     try {\n//       sslOptions.sslKey = 'sslPrivateKey'\n//       connectionEndpointInit(sslOptions, () => {})\n//     } catch (e) {\n//       error = e\n//     } finally {\n//       expect(error.message).to.equal('Must also include sslCertFile in order to use SSL')\n//     }\n//   })\n//\n//   it('throws an exception when sslCert and sslCa is provided', () => {\n//     try {\n//       sslOptions.sslCert = 'sslCertificate'\n//       sslOptions.sslCa = 'sslCertificateAuthority'\n//       connectionEndpointInit(sslOptions, () => {})\n//     } catch (e) {\n//       error = e\n//     } finally {\n//       expect(error.message).to.equal('Must also include sslKey in order to use SSL')\n//     }\n//   })\n//\n//   it('throws an exception when sslKey and sslCa is provided', () => {\n//     try {\n//       sslOptions.sslKey = 'sslPrivateKey'\n//       sslOptions.sslCa = 'sslCertificateAuthority'\n//       connectionEndpointInit(sslOptions, () => {})\n//     } catch (e) {\n//       error = e\n//     } finally {\n//       expect(error.message).to.equal('Must also include sslCertFile in order to use SSL')\n//     }\n//   })\n// })\n\n// import * as C from '../../src/constants'\n// const proxyquire = require('proxyquire').noPreserveCache()\n// import HttpMock from '../test-mocks/http-mock'\n// import LoggerMock from '../test-mocks/logger-mock'\n//\n// const httpMock = new HttpMock()\n// const httpsMock = new HttpMock()\n// // since proxyquire.callThru is enabled, manually capture members from prototypes\n// httpMock.createServer = httpMock.createServer\n// httpsMock.createServer = httpsMock.createServer\n//\n// import { getTestMocks } from '../test-helper/test-mocks'\n//\n// let client\n//\n// const ConnectionEndpoint = proxyquire('../../src/message/uws/connection-endpoint', {\n//   './socket-wrapper-factory': {\n//     createSocketWrapper: () => {\n//       client = getTestMocks().getSocketWrapper('client')\n//       return client.socketWrapper\n//     }\n//   }\n// }).default\n// import DependencyInitialiser from '../../src/utils/dependency-initialiser'\n// import SocketMock from '../test-mocks/socket-mock'\n//\n// const permission = {\n//   isValidUser (connectionData, authData, callback) {\n//     callback(true, {\n//       username: 'someUser',\n//       clientData: { firstname: 'Wolfram' },\n//       serverData: { role: 'admin' }\n//     })\n//   },\n//   canPerformAction (username, message, callback) {\n//     callback(null, true)\n//   },\n//   onClientDisconnect () {}\n// }\n//\n// const config = {\n//   maxAuthAttempts: 3,\n//   logInvalidAuthData: true,\n//   unauthenticatedClientTimeout: 100\n// }\n//\n// const services = {\n//   permission,\n//   authenticationHandler: permission,\n//   logger: new LoggerMock()\n// }\n//\n// describe('permission passes additional user meta data', () => {\n//   let connectionEndpoint\n//\n//   beforeEach(done => {\n//     connectionEndpoint = new ConnectionEndpoint(config)\n//     const depInit = new DependencyInitialiser({ config, services }, config as any, services as any, connectionEndpoint, 'connectionEndpoint')\n//     depInit.on('ready', () => {\n//       connectionEndpoint.onMessages = function () {}\n//       connectionEndpoint.server._simulateUpgrade(new SocketMock())\n//\n//       uwsMock.messageHandler([{\n//         topic: C.TOPIC.CONNECTION,\n//         action: CONNECTION_ACTION.CHALLENGE,\n//         data: 'localhost:6021'\n//       }], client.socketWrapper)\n//\n//       done()\n//     })\n//   })\n//\n//   it('sends an authentication message', () => {\n//     spyOn(permission, 'isValidUser').and.callThrough()\n//\n//     client.socketWrapperMock\n//       .expects('sendMessage')\n//       .once()\n//       .withExactArgs({\n//         topic: C.TOPIC.AUTH,\n//         action: AUTH_ACTION.AUTH_SUCCESSFUL,\n//         parsedData: { firstname: 'Wolfram' }\n//       })\n//\n//     uwsMock.messageHandler([{\n//       topic: C.TOPIC.AUTH,\n//       action: AUTH_ACTION.REQUEST,\n//       data: '{ \"token\": 1234 }'\n//     }], client.socketWrapper)\n//\n//     expect(permission.isValidUser).to.have.callCount(1)\n//     expect((permission.isValidUser as any).calls.mostRecent().args[1]).to.deep.equal({ token: 1234 })\n//\n//     client.socketWrapperMock.verify()\n//   })\n//\n//   it('sends a record read message', () => {\n//     spyOn(connectionEndpoint, 'onMessages')\n//\n//     uwsMock.messageHandler([{\n//       topic: C.TOPIC.AUTH,\n//       action: AUTH_ACTION.REQUEST,\n//       data: '{ \"token\": 1234 }'\n//     }], client.socketWrapper)\n//\n//     uwsMock.messageHandler([{\n//       topic: C.TOPIC.RECORD,\n//       action: C.RECORD_ACTION.READ,\n//       name: 'recordA'\n//     }], client.socketWrapper)\n//\n//     expect(connectionEndpoint.onMessages).to.have.callCount(1)\n//     expect(connectionEndpoint.onMessages.calls.mostRecent().args[0].authData).to.deep.equal({ role: 'admin' })\n//   })\n// })\n"
  },
  {
    "path": "src/connection-endpoint/base/connection-endpoint.ts",
    "content": "\nimport { Message, ParseResult, PARSER_ACTION, TOPIC, CONNECTION_ACTION, ALL_ACTIONS, JSONObject, AUTH_ACTION } from '../../constants'\nimport { DeepstreamPlugin, SocketConnectionEndpoint, SocketWrapper, ConnectionListener, DeepstreamServices, DeepstreamConfig, EVENT, UnauthenticatedSocketWrapper } from '@deepstream/types'\n\nconst OPEN = 'OPEN'\n\nexport interface WebSocketServerConfig {\n  outgoingBufferTimeout: number,\n  maxBufferByteSize: number,\n  headers: string[],\n  healthCheckPath: string,\n  urlPath: string,\n  [index: string]: any,\n}\n\n/**\n * This is the frontmost class of deepstream's message pipeline. It receives\n * connections and authentication requests, authenticates sockets and\n * forwards messages it receives from authenticated sockets.\n */\nexport default class BaseWebsocketConnectionEndpoint extends DeepstreamPlugin implements SocketConnectionEndpoint {\n  public description: string = 'WebSocket Connection Endpoint'\n\n  private initialized: boolean = false\n  private flushTimeout: NodeJS.Timeout | null = null\n  private authenticatedSocketWrappers: Set<SocketWrapper> = new Set()\n  private scheduledSocketWrapperWrites: Set<UnauthenticatedSocketWrapper> = new Set()\n  private logInvalidAuthData: boolean = false\n  private maxAuthAttempts: number = 3\n  private unauthenticatedClientTimeout: number | boolean = false\n  private connectionListener!: ConnectionListener\n  private clientVersions: { [index: string]: Set<string> } = {}\n\n  constructor (private options: WebSocketServerConfig, protected services: DeepstreamServices, protected dsOptions: DeepstreamConfig) {\n    super()\n    this.flushSockets = this.flushSockets.bind(this)\n  }\n\n  public async whenReady (): Promise<void> {\n    await this.services.httpService.whenReady()\n  }\n\n  public createWebsocketServer () {\n  }\n\n  public closeWebsocketServer () {\n  }\n\n  public onSocketWrapperClosed (socketWrapper: UnauthenticatedSocketWrapper) {\n    socketWrapper.close()\n  }\n\n  public setConnectionListener (connectionListener: ConnectionListener) {\n    this.connectionListener = connectionListener\n  }\n\n  public getClientVersions () {\n    return this.clientVersions\n  }\n\n  /**\n   * Called for every message that's received\n   * from an authenticated socket\n   *\n   * This method will be overridden by an external class and is used instead\n   * of an event emitter to improve the performance of the messaging pipeline\n   */\n  public onMessages (socketWrapper: SocketWrapper, messages: Message[]) {\n  }\n\n  /**\n   * initialize and setup the http and WebSocket servers.\n   */\n  public init (): void {\n    if (this.initialized) {\n      throw new Error('init() must only be called once')\n    }\n    this.initialized = true\n\n    this.maxAuthAttempts = this.options.maxAuthAttempts\n    this.logInvalidAuthData = this.options.logInvalidAuthData\n    this.unauthenticatedClientTimeout = this.options.unauthenticatedClientTimeout\n\n    this.createWebsocketServer()\n  }\n\n  /**\n   * Called from a socketWrapper. This method tells the connection endpoint\n   * to flush the socket after a certain amount of time, used to low priority\n   * messages\n   */\n  public scheduleFlush (socketWrapper: SocketWrapper) {\n    this.scheduledSocketWrapperWrites.add(socketWrapper)\n    if (!this.flushTimeout) {\n      this.flushTimeout = setTimeout(this.flushSockets, this.options.outgoingBufferTimeout)\n    }\n  }\n\n  /**\n   * Called when the flushTimeout occurs in order to send  all pending socket acks\n   */\n  private flushSockets () {\n    for (const socketWrapper of this.scheduledSocketWrapperWrites) {\n      socketWrapper.flush()\n    }\n    this.scheduledSocketWrapperWrites.clear()\n    this.flushTimeout = null\n  }\n\n  protected getOption (option: string) {\n    return this.options[option]\n  }\n\n  public handleParseErrors (socketWrapper: SocketWrapper, parseResults: ParseResult[]): Message[] {\n    const messages: Message[] = []\n    for (const parseResult of parseResults) {\n      if (parseResult.parseError) {\n        this.services.logger!.warn(PARSER_ACTION[PARSER_ACTION.MESSAGE_PARSE_ERROR], 'error parsing connection message')\n\n        socketWrapper.sendMessage({\n          topic: TOPIC.PARSER,\n          action: parseResult.action,\n          data: parseResult.raw,\n          originalTopic: parseResult.parsedMessage.topic,\n          originalAction: parseResult.parsedMessage.action\n        }, false)\n        socketWrapper.destroy()\n        continue\n      }\n      const message = parseResult as Message\n      if (\n          message.topic === TOPIC.CONNECTION &&\n          message.action === CONNECTION_ACTION.PONG\n      ) {\n        continue\n      }\n      messages.push(message)\n    }\n    return messages\n  }\n\n  /**\n   * Receives a connected socket, wraps it in a SocketWrapper, sends a connection ack to the user\n   * and subscribes to authentication messages.\n   */\n  public onConnection (socketWrapper: UnauthenticatedSocketWrapper) {\n    const handshakeData = socketWrapper.getHandshakeData()\n    this.services.logger!.info(\n        EVENT.INCOMING_CONNECTION,\n        `from ${handshakeData.referer} (${handshakeData.remoteAddress})`\n    )\n\n    let disconnectTimer\n\n    if (this.unauthenticatedClientTimeout !== null && this.unauthenticatedClientTimeout !== false) {\n      const timeout = this.unauthenticatedClientTimeout as any\n      disconnectTimer = setTimeout(this.processConnectionTimeout.bind(this, socketWrapper), timeout)\n      socketWrapper.onClose(clearTimeout.bind(null, disconnectTimer))\n    }\n\n    socketWrapper.authCallback = this.authenticateConnection.bind(\n      this,\n      socketWrapper,\n      disconnectTimer\n    )\n    socketWrapper.onMessage = this.processConnectionMessage.bind(this, socketWrapper)\n  }\n\n  /**\n   * Always challenges the client that connects. This will be opened up later to allow users\n   * to put in their own challenge authentication.\n   */\n  public processConnectionMessage (socketWrapper: UnauthenticatedSocketWrapper, parsedMessages: Message[]) {\n    const msg = parsedMessages[0]\n\n    if (msg.topic !== TOPIC.CONNECTION) {\n      this.services.logger!.warn(CONNECTION_ACTION[CONNECTION_ACTION.INVALID_MESSAGE], 'invalid connection message')\n      socketWrapper.sendMessage({\n        topic: TOPIC.CONNECTION,\n        action: CONNECTION_ACTION.INVALID_MESSAGE,\n        originalTopic: msg.topic,\n        originalAction: msg.action\n      }, false)\n      return\n    }\n\n    if (msg.action === CONNECTION_ACTION.PING) {\n      return\n    }\n\n    if (msg.action === CONNECTION_ACTION.CHALLENGE) {\n      if (msg.sdkType && msg.sdkVersion) {\n        if (!this.clientVersions[msg.sdkType]) {\n          this.clientVersions[msg.sdkType] = new Set()\n        }\n        this.clientVersions[msg.sdkType].add(msg.sdkVersion)\n      }\n      socketWrapper.onMessage = socketWrapper.authCallback!\n      socketWrapper.sendMessage({\n        topic: TOPIC.CONNECTION,\n        action: CONNECTION_ACTION.ACCEPT\n      }, false)\n      return\n    }\n\n    this.services.logger!.error(PARSER_ACTION[PARSER_ACTION.UNKNOWN_ACTION], '', { message: msg })\n  }\n\n  /**\n   * Callback for the first message that's received from the socket.\n   * This is expected to be an auth-message. This method makes sure that's\n   * the case and - if so - forwards it to the permission handler for authentication\n   */\n  private authenticateConnection (socketWrapper: UnauthenticatedSocketWrapper, disconnectTimeout: NodeJS.Timeout | undefined, parsedMessages: Message[]): void {\n    const msg = parsedMessages[0]\n\n    let errorMsg\n\n    if (msg.topic === TOPIC.CONNECTION && msg.action === CONNECTION_ACTION.PING) {\n      return\n    }\n\n    if (msg.topic !== TOPIC.AUTH) {\n      this.services.logger!.warn(AUTH_ACTION[AUTH_ACTION.INVALID_MESSAGE], `invalid auth message: ${JSON.stringify(msg)}`)\n      socketWrapper.sendMessage({\n        topic: TOPIC.AUTH,\n        action: AUTH_ACTION.INVALID_MESSAGE,\n        originalTopic: msg.topic,\n        originalAction: msg.action\n      }, false)\n      return\n    }\n\n    /**\n     * Log the authentication attempt\n     */\n    const logMsg = socketWrapper.getHandshakeData().remoteAddress\n    this.services.logger!.debug(AUTH_ACTION[AUTH_ACTION.REQUEST], logMsg)\n\n    /**\n     * Ensure the message is a valid authentication message\n     */\n    if (msg.action !== AUTH_ACTION.REQUEST) {\n      errorMsg = this.logInvalidAuthData === true ? JSON.stringify(msg.parsedData) : ''\n      this.sendInvalidAuthMsg(socketWrapper, errorMsg, msg.action)\n      return\n    }\n\n    /**\n     * Ensure the authentication data is valid JSON\n     */\n    const result = socketWrapper.parseData(msg)\n    if (result instanceof Error || !msg.parsedData || typeof msg.parsedData !== 'object') {\n      errorMsg = 'Error parsing auth message'\n\n      if (this.logInvalidAuthData === true) {\n        errorMsg += ` \"${msg.data}\": ${result.toString()}`\n      }\n\n      this.sendInvalidAuthMsg(socketWrapper, errorMsg, msg.action)\n      return\n    }\n\n    /**\n     * Forward for authentication\n     */\n    this.services.authentication.isValidUser(\n      socketWrapper.getHandshakeData(),\n      msg.parsedData,\n      this.processAuthResult.bind(this, msg.parsedData, socketWrapper, disconnectTimeout)\n    )\n  }\n\n  /**\n   * Will be called for syntactically incorrect auth messages. Logs\n   * the message, sends an error to the client and closes the socket\n   */\n  private sendInvalidAuthMsg (socketWrapper: UnauthenticatedSocketWrapper, msg: string, originalAction: ALL_ACTIONS): void {\n    this.services.logger!.warn(AUTH_ACTION[AUTH_ACTION.INVALID_MESSAGE_DATA], this.logInvalidAuthData ? msg : '')\n    socketWrapper.sendMessage({\n      topic: TOPIC.AUTH,\n      action: AUTH_ACTION.INVALID_MESSAGE_DATA,\n      originalAction\n    }, false)\n    socketWrapper.destroy()\n  }\n\n  /**\n   * Callback for succesfully validated sockets. Removes\n   * all authentication specific logic and registeres the\n   * socket with the authenticated sockets\n   */\n  private registerAuthenticatedSocket (unauthenticatedSocketWrapper: UnauthenticatedSocketWrapper, userData: any): void {\n    const socketWrapper = this.appendDataToSocketWrapper(unauthenticatedSocketWrapper, userData)\n\n    unauthenticatedSocketWrapper.authCallback = null\n    unauthenticatedSocketWrapper.onMessage = (parsedMessages: Message[]) => {\n      this.onMessages(socketWrapper, parsedMessages)\n    }\n\n    this.authenticatedSocketWrappers.add(socketWrapper)\n\n    socketWrapper.sendMessage({\n      topic: TOPIC.AUTH,\n      action: AUTH_ACTION.AUTH_SUCCESSFUL,\n      parsedData: userData.clientData\n    })\n\n    this.connectionListener.onClientConnected(socketWrapper)\n    this.services.logger!.info(AUTH_ACTION[AUTH_ACTION.AUTH_SUCCESSFUL], socketWrapper.userId!)\n  }\n\n  /**\n   * Append connection data to the socket wrapper\n   */\n  private appendDataToSocketWrapper (socketWrapper: UnauthenticatedSocketWrapper, userData: any): SocketWrapper {\n    const authenticatedSocketWrapper = socketWrapper as SocketWrapper\n    authenticatedSocketWrapper.userId = userData.id || OPEN\n    authenticatedSocketWrapper.serverData = userData.serverData || null\n    authenticatedSocketWrapper.clientData = userData.clientData || null\n    return authenticatedSocketWrapper\n  }\n\n  /**\n   * Callback for invalid credentials. Will notify the client\n   * of the invalid auth attempt. If the number of invalid attempts\n   * exceed the threshold specified in options.maxAuthAttempts\n   * the client will be notified and the socket destroyed.\n   */\n  private processInvalidAuth (clientData: JSONObject, authData: JSONObject, socketWrapper: UnauthenticatedSocketWrapper): void {\n    let logMsg = 'invalid authentication data'\n\n    if (this.logInvalidAuthData === true) {\n      logMsg += `: ${JSON.stringify(authData)}`\n    }\n\n    this.services.logger!.info(AUTH_ACTION[AUTH_ACTION.AUTH_UNSUCCESSFUL], logMsg)\n    socketWrapper.sendMessage({\n      topic: TOPIC.AUTH,\n      action: AUTH_ACTION.AUTH_UNSUCCESSFUL,\n      parsedData: clientData\n    }, false)\n    socketWrapper.authAttempts++\n\n    if (socketWrapper.authAttempts >= this.maxAuthAttempts) {\n      this.services.logger!.info(AUTH_ACTION[AUTH_ACTION.TOO_MANY_AUTH_ATTEMPTS], 'too many authentication attempts')\n      socketWrapper.sendMessage({\n        topic: TOPIC.AUTH,\n        action: AUTH_ACTION.TOO_MANY_AUTH_ATTEMPTS\n      }, false)\n      setTimeout(() => socketWrapper.destroy(), 10)\n    }\n  }\n\n  /**\n   * Callback for connections that have not authenticated succesfully within\n   * the expected timeframe\n   */\n  private processConnectionTimeout (socketWrapper: UnauthenticatedSocketWrapper): void {\n    const log = 'connection has not authenticated successfully in the expected time'\n    this.services.logger!.info(CONNECTION_ACTION[CONNECTION_ACTION.AUTHENTICATION_TIMEOUT], log)\n    socketWrapper.sendMessage({\n      topic: TOPIC.CONNECTION,\n      action: CONNECTION_ACTION.AUTHENTICATION_TIMEOUT\n    }, false)\n    socketWrapper.destroy()\n  }\n\n  /**\n   * Callback for the results returned by the permission service\n   */\n  private processAuthResult (authData: any, socketWrapper: UnauthenticatedSocketWrapper, disconnectTimeout: NodeJS.Timeout | undefined, isAllowed: boolean, userData: any): void {\n    this.services.monitoring.onLogin(isAllowed, 'websocket')\n\n    userData = userData || {}\n    if (disconnectTimeout) {\n      clearTimeout(disconnectTimeout)\n    }\n\n    if (isAllowed === true) {\n      this.registerAuthenticatedSocket(socketWrapper, userData)\n    } else {\n      this.processInvalidAuth(userData.clientData, authData, socketWrapper)\n    }\n  }\n\n  /**\n   * Notifies the (optional) onClientDisconnect method of the permission\n   * that the specified client has disconnected\n   */\n  public onSocketClose (socketWrapper: UnauthenticatedSocketWrapper): void {\n    this.scheduledSocketWrapperWrites.delete(socketWrapper)\n    this.onSocketWrapperClosed(socketWrapper)\n\n    if (this.authenticatedSocketWrappers.delete(socketWrapper as SocketWrapper)) {\n      const authenticatedSocketWrapper = socketWrapper as SocketWrapper\n      if (this.services.authentication.onClientDisconnect) {\n        this.services.authentication.onClientDisconnect(authenticatedSocketWrapper.userId)\n      }\n      this.connectionListener.onClientDisconnected(authenticatedSocketWrapper)\n    }\n  }\n\n  /**\n   * Closes the ws server connection. The ConnectionEndpoint\n   * will emit a close event once succesfully shut down\n   */\n  public async close () {\n    await this.closeWebsocketServer()\n  }\n}\n"
  },
  {
    "path": "src/connection-endpoint/base/socket-wrapper.ts",
    "content": "import { TOPIC, CONNECTION_ACTION, ParseResult, Message } from '../../constants'\nimport { WebSocketServerConfig } from './connection-endpoint'\nimport { SocketConnectionEndpoint, StatefulSocketWrapper, DeepstreamServices, UnauthenticatedSocketWrapper, SocketWrapper, EVENT } from '@deepstream/types'\n\nexport abstract class WSSocketWrapper<SerializedType extends { length: number }> implements UnauthenticatedSocketWrapper {\n  public abstract socketType: string\n  public isRemote: false = false\n  public isClosed: boolean = false\n  public uuid: number = Math.random()\n  public authCallback: Function | null = null\n  public authAttempts: number = 0\n  public lastMessageRecievedAt: number = 0\n\n  private bufferedWrites: SerializedType[] = []\n  private closeCallbacks: Set<Function> = new Set()\n\n  public userId: string | null = null\n  public serverData: object | null = null\n  public clientData: object | null = null\n\n  private bufferedWritesTotalByteSize: number = 0\n\n  constructor (\n    private socket: any,\n    private handshakeData: any,\n    private services: DeepstreamServices,\n    private config: WebSocketServerConfig,\n    private connectionEndpoint: SocketConnectionEndpoint,\n    private isBinary: boolean\n   ) {\n  }\n\n  get isOpen () {\n    return this.isClosed !== true\n  }\n\n  protected invalidTypeReceived () {\n    this.services.logger.error(EVENT.ERROR, `Received an invalid message type on ${this.uuid}`)\n    this.destroy()\n  }\n\n  /**\n   * Called by the connection endpoint to flush all buffered writes.\n   * A buffered write is a write that is not a high priority, such as an ack\n   * and can wait to be bundled into another message if necessary\n   */\n  public flush () {\n    if (this.bufferedWritesTotalByteSize !== 0) {\n      this.bufferedWrites.forEach((bw) => this.writeMessage(this.socket, bw))\n      this.bufferedWritesTotalByteSize = 0\n      this.bufferedWrites = []\n    }\n  }\n\n  /**\n   * Sends a message based on the provided action and topic\n   */\n  public sendMessage (message: { topic: TOPIC, action: CONNECTION_ACTION } | Message, allowBuffering: boolean = true): void {\n    this.services.monitoring.onMessageSend(message)\n    this.sendBuiltMessage(this.getMessage(message), allowBuffering)\n  }\n\n  /**\n   * Sends a message based on the provided action and topic\n   */\n  public sendAckMessage (message: Message, allowBuffering: boolean = true): void {\n    this.services.monitoring.onMessageSend(message)\n    this.sendBuiltMessage(this.getAckMessage(message), allowBuffering)\n  }\n\n  public abstract getMessage (message: Message): SerializedType\n  public abstract getAckMessage (message: Message): SerializedType\n  public abstract parseMessage (message: SerializedType): ParseResult[]\n  public abstract parseData (message: Message): true | Error\n\n  public onMessage (messages: Message[]): void {\n  }\n\n  /**\n   * Destroys the socket. Removes all deepstream specific\n   * logic and closes the connection\n   */\n  public destroy (): void {\n    try {\n        this.socket.close()\n    } catch (e) {\n        this.socket.end()\n    }\n  }\n\n  public close (): void {\n    this.isClosed = true\n    this.authCallback = null\n\n    this.closeCallbacks.forEach((cb) => cb(this))\n    this.services.logger.info(EVENT.CLIENT_DISCONNECTED, this.userId!)\n  }\n\n  /**\n   * Returns a map of parameters that were collected\n   * during the initial http request that established the\n   * connection\n   */\n  public getHandshakeData (): any {\n    return this.handshakeData\n  }\n\n  public onClose (callback: (socketWrapper: StatefulSocketWrapper) => void): void {\n    this.closeCallbacks.add(callback)\n  }\n\n  public removeOnClose (callback: (socketWrapper: StatefulSocketWrapper) => void): void {\n    this.closeCallbacks.delete(callback)\n  }\n\n  public sendBuiltMessage (message: SerializedType, buffer?: boolean): void {\n    if (this.isOpen) {\n      if (this.config.outgoingBufferTimeout === 0) {\n        this.writeMessage(this.socket, message)\n      } else if (!buffer) {\n        this.flush()\n        this.writeMessage(this.socket, message)\n      } else {\n        this.bufferedWritesTotalByteSize += message.length\n        this.bufferedWrites.push(message)\n        if (this.bufferedWritesTotalByteSize > this.config.maxBufferByteSize) {\n          this.flush()\n        } else {\n          this.connectionEndpoint.scheduleFlush(this as SocketWrapper)\n        }\n      }\n    }\n  }\n\n  protected writeMessage (socket: any, message: SerializedType) {\n    this.services.httpService.sendWebsocketMessage(socket, message, this.isBinary)\n  }\n}\n"
  },
  {
    "path": "src/connection-endpoint/http/connection-endpoint.spec.ts",
    "content": "import { expect } from 'chai'\nimport * as needle from 'needle'\n\nimport LoggerMock from '../../test/mock/logger-mock'\nimport { DeepstreamServices, DeepstreamConfig } from '@deepstream/types';\nimport { OpenAuthentication } from '../../services/authentication/open/open-authentication';\nimport { OpenPermission } from '../../services/permission/open/open-permission';\nimport { NodeHTTP } from '../../services/http/node/node-http'\nimport { HTTPConnectionEndpoint } from './connection-endpoint';\n\nconst conf = {\n  authPath: '/api/v1/auth',\n  postPath: '/api/v1',\n  getPath: '/api/v1',\n  enableAuthEndpoint: true,\n  requestTimeout: 30,\n  allowAuthData: true\n}\n\nconst services: any = {\n  logger: new LoggerMock(),\n  authentication: new OpenAuthentication(),\n  permission: new OpenPermission(),\n  messageDistributor: { distribute () {} }\n}\nservices.httpService = new NodeHTTP({\n  port: 9898,\n  host: '127.0.0.1',\n  allowAllOrigins: true,\n  healthCheckPath: '/health-check',\n  maxMessageSize: 100000,\n  hostUrl: '',\n  headers: []\n}, services as DeepstreamServices, {} as DeepstreamConfig)\n\ndescribe.skip('http plugin', () => {\n  let httpConnectionEndpoint\n  const postUrl = 'http://127.0.0.1:9898/api/v1/'\n\n  before(async () => {\n    httpConnectionEndpoint = new HTTPConnectionEndpoint(conf, services as never as DeepstreamServices, {} as never as DeepstreamConfig)\n    httpConnectionEndpoint.init()\n    await httpConnectionEndpoint.whenReady()\n  })\n\n  after(async () => {\n    await services.httpService.close()\n  })\n\n  const message = Object.freeze({\n    token: 'fiwueeb-3942jjh3jh23i4h23i4h2',\n    body: [\n      {\n        topic: 'record',\n        action: 'write',\n        recordName: 'car/bmw',\n        data: { tyres : 2 }\n      },\n      {\n        topic: 'record',\n        action: 'write',\n        recordName: 'car/bmw',\n        path: 'tyres',\n        data: 3\n      },\n      {\n        topic: 'rpc',\n        action: 'make',\n        rpcName: 'add-two',\n        data: { numA: 6, numB: 3 }\n      },\n      {\n        topic: 'event',\n        action: 'emit',\n        eventName: 'time',\n        data: 1494343585338\n      }\n    ]\n  })\n\n  describe('POST endpoint', () => {\n    it('should reject a request with an empty path', (done) => {\n      needle.post('127.0.0.1:9898', message, { json: true }, (err, response) => {\n        expect(err).to.equal(null)\n        expect(response.statusCode).to.be.within(400, 499)\n        expect(response.headers['content-type']).to.match(/^text\\/plain/)\n        expect(response.body).to.match(/not found/i)\n        done()\n      })\n    })\n\n    it('should reject a request with a url-encoded payload', (done) => {\n      needle.post(postUrl, message, { json: false }, (err, response) => {\n        expect(err).to.equal(null)\n        expect(response.statusCode).to.be.within(400, 499)\n        expect(response.headers['content-type']).to.match(/^text\\/plain/)\n        expect(response.body).to.match(/media type/i)\n        done()\n      })\n    })\n\n    it('should reject a request with a non-object payload', () => Promise.all([\n      '123',\n      ['a', '2', '3.5'],\n      'foo',\n      null,\n      ''\n    ].map((payload) => needle('post', postUrl, payload, { json: true })\n      .then((response) => {\n        expect(response.statusCode).to.be.within(400, 499)\n        expect(response.headers['content-type']).to.match(/^text\\/plain/)\n        expect(response.body).to.match(/(fail|invalid)/i, JSON.stringify(payload))\n      })\n    )))\n\n    it('should accept a request without an auth token', (done) => {\n      const noToken = Object.assign({}, message, { token: undefined })\n      needle.post(postUrl, noToken, { json: true }, (err, response) => {\n        expect(err).to.equal(null)\n        expect(response.statusCode).to.equal(200)\n        done()\n      })\n    })\n\n    it('should return an unsuccessful result for an empty list of messages', (done) => {\n      needle.post(postUrl, { token: 'foo', body: [] }, { json: true }, (err, response) => {\n        expect(err).to.equal(null)\n        expect(response.statusCode).to.be.within(400, 499)\n        expect(response.body).to.match(/body.*must be a non-empty array/)\n        done()\n      })\n    })\n\n    it('should not return an error for a list of valid messages', (done) => {\n      needle.post(postUrl, message, { json: true }, (err) => {\n        expect(err).to.equal(null)\n        done()\n      })\n    })\n\n    it('should reject a request that has a mix of valid and invalid messages', (done) => {\n      const someValid = message.body.slice(0, 2)\n      const req = {\n        token: 'foo',\n        body: someValid.concat([{ pas: 'valide' } as any])\n      }\n      needle.post(postUrl, req, { json: true }, (err, response) => {\n        expect(err).to.equal(null)\n        expect(response.statusCode).to.be.within(400, 499)\n        expect(response.body).to.match(/failed to parse .* index 2/i)\n        done()\n      })\n    })\n\n    describe.skip('authentication', () => {\n      it('should reject a request that times out', async () => {\n        const response = await needle('post', postUrl, message, { json: true })\n        const resp = response.body\n        expect(resp.result).to.equal('FAILURE')\n        expect(resp.body[0].success).to.equal(false)\n        expect(resp.body[0].errorTopic).to.equal('connection')\n        expect(resp.body[0].errorEvent).to.equal('TIME')\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "src/connection-endpoint/http/connection-endpoint.ts",
    "content": "import JIFHandler from '../../jif/jif-handler'\nimport HTTPSocketWrapper from './socket-wrapper'\nimport * as HTTPStatus from 'http-status'\nimport { PARSER_ACTION, AUTH_ACTION, EVENT_ACTION, RECORD_ACTION, Message, ALL_ACTIONS, JSONObject } from '../../constants'\nimport { DeepstreamConnectionEndpoint, DeepstreamServices, SimpleSocketWrapper, SocketWrapper, JifResult, UnauthenticatedSocketWrapper, DeepstreamPlugin, DeepstreamConfig, EVENT, DeepstreamHTTPResponse, DeepstreamHTTPMeta, DeepstreamAuthenticationResult } from '@deepstream/types'\nexport interface HTTPEvents {\n  onAuthMessage: Function\n  onPostMessage: Function\n  onGetMessage: Function\n}\n\ninterface HTTPConnectionEndpointOptionsInterface {\n  enableAuthEndpoint: boolean,\n  authPath: string,\n  postPath: string,\n  getPath: string,\n  allowAuthData: boolean,\n  logInvalidAuthData: boolean,\n  requestTimeout: number\n}\n\nfunction checkConfigOption (config: any, option: string, expectedType?: string): void {\n  if ((expectedType && typeof config[option] !== expectedType) || config[option] === undefined) {\n    throw new Error(`The HTTP plugin requires that the \"${option}\" config option is set`)\n  }\n}\nexport class HTTPConnectionEndpoint extends DeepstreamPlugin implements DeepstreamConnectionEndpoint {\n  public description: string = 'HTTP connection endpoint'\n\n  private initialized: boolean = false\n  private jifHandler!: JIFHandler\n  private onSocketMessageBound: Function\n  private onSocketErrorBound: Function\n  private logInvalidAuthData: boolean = false\n  private requestTimeout!: number\n\n  constructor (private pluginOptions: HTTPConnectionEndpointOptionsInterface, private services: DeepstreamServices, public dsOptions: DeepstreamConfig) {\n    super()\n\n    checkConfigOption(pluginOptions, 'enableAuthEndpoint', 'boolean')\n    checkConfigOption(pluginOptions, 'authPath', 'string')\n    checkConfigOption(pluginOptions, 'postPath', 'string')\n    checkConfigOption(pluginOptions, 'getPath', 'string')\n\n    this.onSocketMessageBound = this.onSocketMessage.bind(this)\n    this.onSocketErrorBound = this.onSocketError.bind(this)\n    this.onPermissionResponse = this.onPermissionResponse.bind(this)\n\n    this.jifHandler = new JIFHandler(this.services)\n  }\n\n  public async whenReady (): Promise<void> {\n    await this.services.httpService.whenReady()\n  }\n\n  public async close () {\n  }\n\n  public getClientVersions () {\n    return {}\n  }\n\n  /**\n   * Initialize the http server.\n   */\n  public init (): void {\n    if (this.initialized) {\n      throw new Error('init() must only be called once')\n    }\n    this.initialized = true\n\n    if (this.pluginOptions.enableAuthEndpoint) {\n      this.services.httpService.registerPostPathPrefix(this.pluginOptions.authPath, this.onAuthMessage.bind(this))\n    }\n    this.services.httpService.registerPostPathPrefix(this.pluginOptions.postPath, this.onPostMessage.bind(this))\n    this.services.httpService.registerGetPathPrefix(this.pluginOptions.getPath, this.onGetMessage.bind(this))\n\n    this.logInvalidAuthData = this.pluginOptions.logInvalidAuthData\n    this.requestTimeout = this.pluginOptions.requestTimeout\n    if (this.requestTimeout === undefined) {\n      this.requestTimeout = 20000\n    }\n  }\n\n  /**\n   * Called for every message that's received\n   * from an authenticated socket\n   *\n   * This method will be overridden by an external class and is used instead\n   * of an event emitter to improve the performance of the messaging pipeline\n   */\n  public onMessages (socketWrapper: SimpleSocketWrapper, messages: Message[]): void {\n  }\n\n  private onGetMessage (meta: DeepstreamHTTPMeta, responseCallback: any) {\n    const message = 'Reading records via HTTP GET is not yet implemented, please use a post request instead.'\n    this.services.logger.warn(RECORD_ACTION[RECORD_ACTION.READ], message)\n    responseCallback({ statusCode: 400, message })\n    // TODO: implement a GET endpoint that reads the current state of a record\n  }\n\n  /**\n   * Handle a message to the authentication endpoint (for token generation).\n   *\n   * Passes the entire message to the configured authentication handler.\n   */\n  private onAuthMessage (authData: JSONObject, metadata: DeepstreamHTTPMeta, responseCallback: DeepstreamHTTPResponse): void {\n    this.services.authentication.isValidUser(\n      metadata,\n      authData,\n      (isAllowed, data) => {\n        this.services.monitoring.onLogin(isAllowed, 'http')\n\n        if (isAllowed === true) {\n          responseCallback(null, {\n            token: data!.token,\n            clientData: data!.clientData\n          })\n          return\n        }\n\n        let error = typeof data === 'string' ? data : 'Invalid authentication data.'\n\n        responseCallback({\n          statusCode: HTTPStatus.UNAUTHORIZED,\n          message: error\n        })\n\n        if (this.logInvalidAuthData === true) {\n          error += `: ${JSON.stringify(authData)}`\n        }\n\n        this.services.logger.debug(AUTH_ACTION[AUTH_ACTION.AUTH_UNSUCCESSFUL], error)\n      }\n    )\n  }\n\n  /**\n   * Handle a message to the POST endpoint\n   *\n   * Authenticates the message using authData, a token, or OPEN auth if enabled/provided.\n   */\n  private onPostMessage (\n    messageData: { token?: string, authData?: object, body: object[] },\n    metadata: DeepstreamHTTPMeta,\n    responseCallback: DeepstreamHTTPResponse\n  ): void {\n    if (!Array.isArray(messageData.body) || messageData.body.length < 1) {\n      const message = `Invalid message: the \"body\" parameter must ${\n        messageData.body ? 'be a non-empty array of Objects.' : 'exist.'\n      }`\n      responseCallback({ statusCode: HTTPStatus.BAD_REQUEST, message })\n      this.services.logger.warn(\n        PARSER_ACTION[PARSER_ACTION.INVALID_MESSAGE],\n        JSON.stringify(messageData.body)\n      )\n      return\n    }\n\n    let authData = {}\n    if (messageData.authData !== undefined) {\n      if (this.pluginOptions.allowAuthData !== true) {\n        const message = 'Authentication using authData is disabled. Try using a token instead.'\n        responseCallback({ statusCode: HTTPStatus.BAD_REQUEST, message })\n        this.services.logger.debug(\n          AUTH_ACTION[AUTH_ACTION.INVALID_MESSAGE_DATA],\n          'Auth rejected because allowAuthData was disabled'\n        )\n        return\n      }\n      if (messageData.authData === null || typeof messageData.authData !== 'object') {\n        const message = 'Invalid message: the \"authData\" parameter must be an object'\n        responseCallback({ statusCode: HTTPStatus.BAD_REQUEST, message })\n        this.services.logger.debug(\n          AUTH_ACTION[AUTH_ACTION.INVALID_MESSAGE_DATA],\n          `authData was not an object: ${\n            this.logInvalidAuthData === true ? JSON.stringify(messageData.authData) : '-'\n          }`\n        )\n        return\n      }\n      authData = messageData.authData\n    } else if (messageData.token !== undefined) {\n      if (typeof messageData.token !== 'string' || messageData.token.length === 0) {\n        const message = 'Invalid message: the \"token\" parameter must be a non-empty string'\n        responseCallback({ statusCode: HTTPStatus.BAD_REQUEST, message })\n        this.services.logger.debug(\n          AUTH_ACTION[AUTH_ACTION.INVALID_MESSAGE_DATA],\n          `auth token was not a string: ${\n            this.logInvalidAuthData === true ? messageData.token : '-'\n          }`\n        )\n        return\n      }\n      authData = { ...authData, token: messageData.token }\n    }\n\n    this.services.authentication.isValidUser(\n      metadata,\n      authData,\n      this.onMessageAuthResponse.bind(this, responseCallback, messageData)\n    )\n  }\n\n  /**\n   * Create and initialize a new SocketWrapper\n   */\n  private createSocketWrapper (\n    authResponseData: DeepstreamAuthenticationResult,\n    messageIndex: number,\n    messageResults: any,\n    responseCallback: Function,\n    requestTimeoutId: NodeJS.Timeout\n  ): UnauthenticatedSocketWrapper {\n    const socketWrapper = new HTTPSocketWrapper(\n      this.services, this.onSocketMessageBound, this.onSocketErrorBound\n    )\n\n    socketWrapper.init(\n      authResponseData, messageIndex, messageResults, responseCallback, requestTimeoutId\n    )\n\n    return socketWrapper\n  }\n\n  /**\n   * Handle response from authentication handler relating to a POST request.\n   *\n   * Parses, permissions and distributes the individual messages\n   */\n  private onMessageAuthResponse (\n    responseCallback: Function,\n    messageData: { body: object[] },\n    success: boolean,\n    authResponseData?: DeepstreamAuthenticationResult\n  ): void {\n    if (success !== true) {\n      const error = typeof authResponseData === 'string' ? authResponseData : 'Unsuccessful authentication attempt.'\n      responseCallback({\n        statusCode: HTTPStatus.UNAUTHORIZED,\n        message: error\n      })\n      return\n    }\n    const messageCount = messageData.body.length\n    const messageResults = new Array(messageCount).fill(null)\n\n    const parseResults = new Array(messageCount)\n    for (let messageIndex = 0; messageIndex < messageCount; messageIndex++) {\n      const parseResult = this.jifHandler.fromJIF(messageData.body[messageIndex])\n      parseResults[messageIndex] = parseResult\n      if (!parseResult.success) {\n        const message = `Failed to parse JIF object at index ${messageIndex}.`\n        responseCallback({\n          statusCode: HTTPStatus.BAD_REQUEST,\n          message: parseResult.error ? `${message} Reason: ${parseResult.error}` : message\n        })\n        this.services.logger.debug(PARSER_ACTION[PARSER_ACTION.MESSAGE_PARSE_ERROR], parseResult.error)\n        return\n      }\n    }\n\n    const requestTimeoutId = setTimeout(\n      () => this.onRequestTimeout(responseCallback, messageResults),\n      this.requestTimeout\n    )\n\n    // @ts-ignore\n    const dummySocketWrapper = this.createSocketWrapper(authResponseData, null, null, null, null) as SocketWrapper\n\n    for (let messageIndex = 0; messageIndex < messageCount; messageIndex++) {\n      const parseResult = parseResults[messageIndex]\n      if (parseResult.done) {\n        // Messages such as event emits do not need to wait for a response. However, we need to\n        // check that the message was successfully permissioned, so bypass the message-processor.\n        this.permissionEventEmit(\n          dummySocketWrapper, parseResult.message, messageResults, messageIndex\n        )\n        // check if a response can be sent immediately\n        if (messageIndex === messageCount - 1) {\n          HTTPConnectionEndpoint.checkComplete(messageResults, responseCallback, requestTimeoutId)\n        }\n      } else {\n        const socketWrapper = this.createSocketWrapper(\n          authResponseData!, messageIndex, messageResults, responseCallback, requestTimeoutId\n        )\n\n        /*\n         * TODO: work out a way to safely enable socket wrapper pooling\n         * if (this.socketWrapperPool.length === 0) {\n         *   socketWrapper = new HTTPSocketWrapper(\n         *     this.onSocketMessageBound,\n         *     this.onSocketErrorBound\n         *   )\n         * } else {\n         *   socketWrapper = this.socketWrapperPool.pop()\n         * }\n         */\n\n        // emit the message\n        this.onMessages(socketWrapper, [parseResult.message])\n      }\n    }\n  }\n\n  /**\n   * Handle messages from deepstream socketWrappers and inserts message responses into the HTTP\n   * response where possible.\n   */\n  private onSocketMessage (\n    messageResults: JifResult[], index: number, message: Message, responseCallback: Function, requestTimeoutId: NodeJS.Timer\n  ): void {\n    const parseResult = this.jifHandler.toJIF(message)\n    if (!parseResult) {\n      const errorMessage = `${message.topic} ${message.action} ${JSON.stringify(message.data)}`\n      this.services.logger.error(PARSER_ACTION[PARSER_ACTION.MESSAGE_PARSE_ERROR], errorMessage)\n      return\n    }\n    if (parseResult.done !== true) {\n      return\n    }\n    if (messageResults[index] === null) {\n      messageResults[index] = parseResult.message\n      HTTPConnectionEndpoint.checkComplete(messageResults, responseCallback, requestTimeoutId)\n    }\n  }\n\n  /**\n   * Handle errors from deepstream socketWrappers and inserts message rejections into the HTTP\n   * response where necessary.\n   */\n  private onSocketError (\n    messageResults: JifResult[],\n    index: number,\n    message: Message,\n    event: string,\n    errorMessage: string,\n    responseCallback: Function,\n    requestTimeoutId: NodeJS.Timer\n  ): void {\n    const parseResult = this.jifHandler.errorToJIF(message, event)\n    if (parseResult.done && messageResults[index] === null) {\n      messageResults[index] = parseResult.message\n      HTTPConnectionEndpoint.checkComplete(messageResults, responseCallback, requestTimeoutId)\n    }\n  }\n\n  /**\n   * Check whether any more responses are outstanding and finalize http response if not.\n   */\n  private static checkComplete (messageResults: JifResult[], responseCallback: Function, requestTimeoutId: NodeJS.Timer): void {\n    const messageResult = HTTPConnectionEndpoint.calculateMessageResult(messageResults)\n    if (messageResult === null) {\n      // insufficient responses received\n      return\n    }\n\n    clearTimeout(requestTimeoutId)\n\n    responseCallback(null, {\n      result: messageResult,\n      body: messageResults\n    })\n  }\n\n  /**\n   * Handle request timeout, sending any responses that have already resolved.\n   */\n  private onRequestTimeout (responseCallback: Function, messageResults: JifResult[]): void {\n    let numTimeouts = 0\n    for (let i = 0; i < messageResults.length; i++) {\n      if (messageResults[i] === null) {\n        messageResults[i] = {\n          success: false,\n          error: 'Request exceeded timeout before a response was received.',\n          errorTopic: 'connection',\n          errorEvent: EVENT.HTTP_REQUEST_TIMEOUT\n        }\n        numTimeouts++\n      }\n    }\n    if (numTimeouts === 0) {\n      return\n    }\n\n    this.services.logger.warn(EVENT.HTTP_REQUEST_TIMEOUT, 'HTTP Request timeout')\n\n    const result = HTTPConnectionEndpoint.calculateMessageResult(messageResults)\n\n    responseCallback(null, {\n      result,\n      body: messageResults\n    })\n  }\n\n  /**\n   * Calculate the 'result' field in a response depending on how many responses resolved\n   * successfully. Can be one of 'SUCCESS', 'FAILURE' or 'PARTIAL SUCCSS'\n   */\n  private static calculateMessageResult (messageResults: JifResult[]): string | null {\n    let numSucceeded = 0\n    for (let i = 0; i < messageResults.length; i++) {\n      if (!messageResults[i]) {\n        // todo: when does this happen\n        return null\n      }\n      if (messageResults[i].success) {\n        numSucceeded++\n      }\n    }\n\n    if (numSucceeded === messageResults.length) {\n      return 'SUCCESS'\n    }\n    if (numSucceeded === 0) {\n      return 'FAILURE'\n    }\n    return 'PARTIAL_SUCCESS'\n  }\n\n  /**\n   * Permission an event emit and capture the response directly\n   */\n  private permissionEventEmit (\n    socketWrapper: SocketWrapper,\n    parsedMessage: Message,\n    messageResults: JifResult[],\n    messageIndex: number\n  ): void {\n    this.services.permission.canPerformAction(\n      socketWrapper,\n      parsedMessage,\n      this.onPermissionResponse,\n      { messageResults, messageIndex }\n    )\n  }\n\n  /**\n   * Handle an event emit permission response\n   */\n  private onPermissionResponse (\n    socketWrapper: SocketWrapper,\n    message: Message,\n    { messageResults, messageIndex }: { messageResults: JifResult[], messageIndex: number },\n    error: string | Error | ALL_ACTIONS | null,\n    permissioned: boolean\n  ): void {\n    if (error !== null) {\n      this.services.logger.warn(EVENT_ACTION[EVENT_ACTION.MESSAGE_PERMISSION_ERROR], error.toString())\n    }\n    if (permissioned !== true) {\n      messageResults[messageIndex] = {\n        success: false,\n        error: 'Message denied. Action \\'emit\\' is not permitted.',\n        errorEvent: EVENT_ACTION[EVENT_ACTION.MESSAGE_DENIED],\n        errorAction: 'emit',\n        errorTopic: 'event'\n      }\n      return\n    }\n    messageResults[messageIndex] = { success: true }\n    this.services.messageDistributor.distribute(socketWrapper, message)\n  }\n}\n"
  },
  {
    "path": "src/connection-endpoint/http/socket-wrapper.ts",
    "content": "import { parseData } from '@deepstream/protobuf/dist/src/message-parser'\nimport { EventEmitter } from 'events'\nimport { DeepstreamServices, UnauthenticatedSocketWrapper, EVENT, DeepstreamAuthenticationResult } from '@deepstream/types'\nimport { Message, ParseResult } from '../../constants'\n\nexport default class HTTPSocketWrapper extends EventEmitter implements UnauthenticatedSocketWrapper {\n  public socketType = 'http'\n  public userId: string | null = null\n  public serverData: object | null = null\n  public clientData: object | null = null\n\n  public uuid: number = Math.random()\n\n  private correlationIndex: number = -1\n  private messageResults: any[] = []\n  private responseCallback: Function | null = null\n  private requestTimeout: NodeJS.Timeout | null = null\n\n  public authCallback: Function | null = null\n  public isRemote: boolean = false\n  public isClosed: boolean = false\n  // TODO: This isn't used here but is part of a stateful socketWrapper\n  public authAttempts = 0\n\n  constructor (private services: DeepstreamServices, private onMessageCallback: Function, private onErrorCallback: Function) {\n    super()\n  }\n\n  public init (\n    authResponseData: DeepstreamAuthenticationResult,\n    messageIndex: number,\n    messageResults: any[],\n    responseCallback: Function,\n    requestTimeoutId: NodeJS.Timeout\n   ) {\n    this.userId = authResponseData.id || 'OPEN'\n    this.clientData = authResponseData.clientData || null\n    this.serverData = authResponseData.serverData || null\n\n    this.correlationIndex = messageIndex\n    this.messageResults = messageResults\n    this.responseCallback = responseCallback\n    this.requestTimeout = requestTimeoutId\n  }\n\n  public close () {\n    this.isClosed = true\n  }\n\n  public flush () {\n  }\n\n  public onMessage () {\n  }\n\n  public getMessage () {\n  }\n\n  /**\n   * Returns a map of parameters that were collected\n   * during the initial http request that established the\n   * connection\n   */\n  public getHandshakeData () {\n    return {}\n  }\n\n  /**\n   * Sends an error on the specified topic. The\n   * action will automatically be set to C.ACTION.ERROR\n   */\n  public sendError (message: Message, event: EVENT, errorMessage: string) {\n    if (this.isClosed === false) {\n      parseData(message)\n      this.onErrorCallback(\n        this.messageResults,\n        this.correlationIndex,\n        message,\n        event,\n        errorMessage,\n        this.responseCallback,\n        this.requestTimeout\n      )\n    }\n  }\n\n  /**\n   * Sends a message based on the provided action and topic\n   */\n  public sendMessage (message: Message) {\n    if (message.action >= 100) {\n      message.isError = true\n    }\n    if (this.isClosed === false) {\n      this.services.monitoring.onMessageSend(message)\n\n      parseData(message)\n      this.onMessageCallback(\n        this.messageResults,\n        this.correlationIndex,\n        message,\n        this.responseCallback,\n        this.requestTimeout\n      )\n    }\n  }\n\n  public sendAckMessage (message: Message) {\n    message.isAck = true\n    this.sendMessage(message)\n  }\n\n  public parseData (message: Message) {\n    return parseData(message)\n  }\n\n  public parseMessage (serializedMessage: any): ParseResult[] {\n    throw new Error('Method not implemented.')\n  }\n\n  /**\n   * Destroys the socket. Removes all deepstream specific\n   * logic and closes the connection\n   *\n   * @public\n   * @returns {void}\n   */\n  public destroy () {\n  }\n\n  public onClose () {\n  }\n\n  public removeOnClose () {\n  }\n}\n"
  },
  {
    "path": "src/connection-endpoint/mqtt/connection-endpoint.ts",
    "content": "import { createMQTTSocketWrapper} from './socket-wrapper-factory'\nimport { DeepstreamServices, SocketWrapper, DeepstreamConfig, UnauthenticatedSocketWrapper, EVENT } from '@deepstream/types'\nimport ConnectionEndpoint, { WebSocketServerConfig } from '../base/connection-endpoint'\n\nimport { createServer as createTCPServer, Server as TCPServer } from 'net'\nimport { createServer as createTLSServer, Server as TLSServer } from 'tls'\n// @ts-ignore\nimport * as mqttCon from 'mqtt-connection'\nimport { TOPIC, CONNECTION_ACTION, AUTH_ACTION } from '../../constants'\nimport { Message } from '@deepstream/client/dist/src/constants'\nimport { EventEmitter } from 'events'\n\nexport interface MQTTConnectionEndpointConfig extends WebSocketServerConfig {\n  port: number,\n  host: string,\n  idleTimeout: number,\n  ssl?: {\n    key: string,\n    cert: string\n  }\n}\n\ntype MQTTPacket = any\ntype MQTTConnection = any\n\n/**\n * This is the frontmost class of deepstream's message pipeline. It receives\n * connections and authentication requests, authenticates sockets and\n * forwards messages it receives from authenticated sockets.\n */\nexport class MQTTConnectionEndpoint extends ConnectionEndpoint {\n  private server!: TCPServer | TLSServer\n  private connections = new Map<MQTTConnection, UnauthenticatedSocketWrapper>()\n  private logger = this.services.logger.getNameSpace('MQTT')\n\n  private isReady: boolean = false\n  private emitter = new EventEmitter()\n\n  constructor (private mqttOptions: MQTTConnectionEndpointConfig, services: DeepstreamServices, config: DeepstreamConfig) {\n    super(mqttOptions, services, config)\n    this.description = 'MQTT Protocol Connection Endpoint'\n    this.onMessages = this.onMessages.bind(this)\n  }\n\n  public async whenReady (): Promise<void> {\n    if (!this.isReady) {\n      return new Promise((resolve) => this.emitter.once('ready', resolve))\n    }\n  }\n\n  public async close (): Promise<void> {\n    return new Promise((resolve) => this.server.close(() => resolve()))\n  }\n\n  /**\n   * Initialize the ws endpoint, setup callbacks etc.\n   */\n  public createWebsocketServer () {\n    if (this.mqttOptions.ssl) {\n      this.server = createTLSServer({\n        key: this.mqttOptions.ssl.key,\n        cert: this.mqttOptions.ssl.cert\n      })\n    } else {\n      this.server = createTCPServer()\n    }\n\n    this.server.on(this.mqttOptions.ssl ? 'secureConnection' : 'connection', (stream) => {\n      const client: MQTTConnection = mqttCon(stream)\n      const socketWrapper = createMQTTSocketWrapper(client, {}, this.services, this.logger)\n      this.connections.set(client, socketWrapper)\n      this.onConnection(socketWrapper)\n\n      socketWrapper.onMessage([{\n        topic: TOPIC.CONNECTION,\n        action: CONNECTION_ACTION.CHALLENGE\n      }])\n\n      const logger = this.services.logger\n      // client connected\n      client.on('connect', function (packet: MQTTPacket) {\n        logger.debug(EVENT.INCOMING_CONNECTION, `MQTT Connection with username ${packet.username}`, { username: packet.username })\n        socketWrapper.onMessage([{\n          topic: TOPIC.AUTH,\n          action: AUTH_ACTION.REQUEST,\n          parsedData: {\n            username: packet.username,\n            password: packet.password && packet.password.toString()\n          }\n        }])\n      })\n\n      const closeClient = () => {\n        if (!this.connections.has(client)) {\n          return\n        }\n        this.onSocketClose(socketWrapper)\n        this.connections.delete(client)\n\n        socketWrapper.destroy()\n      }\n\n      // client disconnect\n      client.on('disconnect', closeClient)\n\n      // connection error handling\n      client.on('close', closeClient)\n\n      client.on('error', (e: any) => {\n        this.logger.error('CLIENT ERROR', e.toString())\n        closeClient()\n      })\n\n      // timeout idle streams after 5 minutes\n      stream.setTimeout(this.mqttOptions.idleTimeout)\n\n      // stream timeout\n      stream.on('timeout', function () { client.destroy() })\n\n      // client published\n      client.on('publish', (packet: MQTTPacket) => {\n        this.onMessages(socketWrapper as any, socketWrapper.parseMessage(packet) as Message[])\n      })\n\n      // // client pinged\n      client.on('pingreq', function () {\n        client.pingresp()\n      })\n\n      // client subscribed\n      client.on('subscribe', (packet: MQTTPacket) => {\n        this.onMessages(socketWrapper as any, socketWrapper.parseMessage(packet) as Message[])\n      })\n\n      // client subscribed\n      client.on('unsubscribe', (packet: MQTTPacket) => {\n        this.onMessages(socketWrapper as any, socketWrapper.parseMessage(packet) as Message[])\n      })\n    })\n\n    this.server.listen(this.mqttOptions.port, this.mqttOptions.host, () => {\n      this.services.logger.info(EVENT.INFO, `Listening for MQTT ${this.mqttOptions.ssl ? 'TLS' : 'TCP' } connections on ${this.mqttOptions.host}:${this.mqttOptions.port}`)\n      this.isReady = true\n      this.emitter.emit('ready')\n    })\n\n    return this.server\n  }\n\n  public async closeWebsocketServer () {\n    this.connections.forEach((conn) => {\n      if (!conn.isClosed) {\n        conn.destroy()\n      }\n    })\n    this.connections.clear()\n    return new Promise((resolve) => this.server.close(resolve))\n  }\n\n  public onSocketWrapperClosed (socketWrapper: SocketWrapper) {\n    socketWrapper.close()\n  }\n}\n"
  },
  {
    "path": "src/connection-endpoint/mqtt/message-parser.ts",
    "content": "import { TOPIC, EVENT_ACTION, Message, PARSER_ACTION, RECORD_ACTION } from '../../constants'\n\nexport const parseMQTT = (msg: any): Message[] => {\n    let topic = TOPIC.EVENT\n    if (msg.retain) {\n        topic = TOPIC.RECORD\n    }\n    if (msg.cmd === 'subscribe') {\n        const names = msg.subscriptions.map((mqttMsg: any) => mqttMsg.topic)\n        return [{\n            topic: TOPIC.EVENT,\n            action: EVENT_ACTION.SUBSCRIBE,\n            names,\n            correlationId: msg.messageId\n        }, {\n            topic: TOPIC.RECORD,\n            action: RECORD_ACTION.SUBSCRIBE,\n            names,\n            correlationId: msg.messageId\n        }]\n    }\n    if (msg.cmd === 'unsubscribe') {\n        const names = msg.subscriptions.map((mqttMsg: any) => mqttMsg.topic)\n        return [{\n            topic: TOPIC.EVENT,\n            action: EVENT_ACTION.UNSUBSCRIBE,\n            names,\n            correlationId: msg.messageId\n        }, {\n            topic: TOPIC.RECORD,\n            action: RECORD_ACTION.UNSUBSCRIBE,\n            names,\n            correlationId: msg.messageId\n        }]\n    }\n    if (msg.cmd === 'publish') {\n        if (topic === TOPIC.EVENT) {\n            return [{\n                topic,\n                action: EVENT_ACTION.EMIT,\n                name: msg.topic,\n                parsedData: msg.payload.toString()\n            }]\n        } else if (topic === TOPIC.RECORD) {\n            return [{\n                topic,\n                action: RECORD_ACTION.CREATEANDUPDATE,\n                name: msg.topic,\n                parsedData: JSON.parse(msg.payload.toString()),\n                isWriteAck: msg.qos > 0,\n                version: -1,\n                correlationId: msg.messageId\n            }, {\n                topic: TOPIC.EVENT,\n                action: EVENT_ACTION.EMIT,\n                name: msg.topic,\n                parsedData: msg.payload.toString()\n            }]\n        }\n    }\n    return [{\n        topic: TOPIC.PARSER,\n        action: PARSER_ACTION.INVALID_MESSAGE\n    }]\n}\n"
  },
  {
    "path": "src/connection-endpoint/mqtt/socket-wrapper-factory.ts",
    "content": "import { StatefulSocketWrapper, DeepstreamServices, UnauthenticatedSocketWrapper, EVENT, NamespacedLogger } from '@deepstream/types'\nimport { TOPIC, CONNECTION_ACTION, Message, EVENT_ACTION, AUTH_ACTION, RECORD_ACTION, ParseResult } from '../../constants'\nimport { ACTIONS_BYTE_TO_KEY } from '../websocket/text/text-protocol/constants'\nimport { parseMQTT } from './message-parser'\n\n/**\n * This class wraps around a websocket\n * and provides higher level methods that are integrated\n * with deepstream's message structure\n */\nexport class MQTTSocketWrapper implements UnauthenticatedSocketWrapper {\n  public socketType = 'mqtt'\n  public userId: string | null = null\n  public serverData: object | null = null\n  public clientData: object | null = null\n\n  public isRemote: false = false\n  public isClosed: boolean = false\n  public uuid: number = Math.random()\n  public authCallback: Function | null = null\n  public authAttempts: number = 0\n\n  private closeCallbacks: Set<Function> = new Set()\n\n  constructor (\n    private socket: any,\n    private handshakeData: any,\n    private services: DeepstreamServices,\n    private logger: NamespacedLogger\n   ) {\n  }\n\n  get isOpen () {\n    return this.isClosed !== true\n  }\n\n  public flush () {\n  }\n\n  /**\n   * Sends a message based on the provided action and topic\n   */\n  public sendMessage (message: { topic: TOPIC, action: CONNECTION_ACTION } | Message, allowBuffering: boolean = true): void {\n    this.services.monitoring.onMessageSend(message)\n    this.sendBuiltMessage(message)\n  }\n\n  /**\n   * Sends a message based on the provided action and topic\n   */\n  public sendAckMessage (message: Message, allowBuffering: boolean = true): void {\n    this.services.monitoring.onMessageSend(message)\n    if (message.topic === TOPIC.EVENT) {\n      if (message.action === EVENT_ACTION.SUBSCRIBE) {\n        this.socket.suback({ granted: [0], messageId: Number(message.correlationId) })\n        return\n      }\n    }\n    if (message.topic === TOPIC.RECORD) {\n      if (message.action === RECORD_ACTION.SUBSCRIBE) {\n        this.socket.suback({ granted: [1], messageId: Number(message.correlationId) })\n        return\n      }\n    }\n    this.logger.warn(EVENT.UNKNOWN_ACTION, `Unhandled ack message for ${TOPIC[message.topic]}:${ACTIONS_BYTE_TO_KEY[message.topic][message.action]}`)\n  }\n\n  public getMessage (message: Message): Message {\n    return message\n  }\n\n  public parseData (message: Message): true | Error {\n    return true\n  }\n\n  public onMessage (messages: Message[]): void {\n  }\n\n  /**\n   * Destroys the socket. Removes all deepstream specific\n   * logic and closes the connection\n   */\n  public destroy (): void {\n    this.socket.destroy()\n  }\n\n  public close (): void {\n    this.isClosed = true\n    this.authCallback = null\n\n    this.closeCallbacks.forEach((cb) => cb(this))\n    this.services.logger.info(EVENT.CLIENT_DISCONNECTED, this.userId!)\n  }\n\n  public parseMessage (serializedMessage: any): ParseResult[] {\n    return parseMQTT(serializedMessage)\n  }\n\n  /**\n   * Returns a map of parameters that were collected\n   * during the initial http request that established the\n   * connection\n   */\n  public getHandshakeData (): any {\n    return this.handshakeData\n  }\n\n  public onClose (callback: (socketWrapper: StatefulSocketWrapper) => void): void {\n    this.closeCallbacks.add(callback)\n  }\n\n  public removeOnClose (callback: (socketWrapper: StatefulSocketWrapper) => void): void {\n    this.closeCallbacks.delete(callback)\n  }\n\n  public sendBuiltMessage (message: Message, buffer?: boolean): void {\n    if (this.isOpen) {\n        if (message.topic === TOPIC.CONNECTION) {\n          if (message.action === CONNECTION_ACTION.ACCEPT) {\n            return\n          }\n        }\n\n        if (message.topic === TOPIC.AUTH) {\n          if (message.action === AUTH_ACTION.AUTH_SUCCESSFUL) {\n            this.socket.connack({ returnCode: 0 })\n            return\n          }\n          if (message.action === AUTH_ACTION.AUTH_UNSUCCESSFUL) {\n            this.socket.connack({ returnCode: 5, reason: message.reason })\n            return\n          }\n        }\n\n        if (message.topic === TOPIC.EVENT) {\n          if (message.action === EVENT_ACTION.EMIT) {\n            let payload = message.data\n            if (!payload && message.parsedData) {\n              payload = Buffer.from(JSON.stringify(message.parsedData))\n            }\n            this.socket.publish({\n              cmd: 'publish',\n              topic: message.name,\n              payload,\n              length: payload && payload.length\n            })\n            return\n          }\n        }\n\n        if (message.topic === TOPIC.RECORD) {\n          if (message.action === RECORD_ACTION.WRITE_ACKNOWLEDGEMENT) {\n            this.socket.puback({ messageId: message.correlationId })\n            return\n          }\n\n          if (message.action === RECORD_ACTION.UPDATE) {\n            const payload = Buffer.from(JSON.stringify(message.parsedData))\n            this.socket.publish({\n              cmd: 'publish',\n              topic: message.name,\n              payload,\n              length: payload.length\n            })\n            return\n          }\n\n          if (message.action === RECORD_ACTION.PATCH) {\n            this.logger.warn(EVENT.UNSUPPORTED_ACTION, 'Patches are not currently supported via the MQTT API')\n            return\n          }\n        }\n\n        this.logger.warn(EVENT.UNKNOWN_ACTION, `Unhandled message for ${TOPIC[message.topic]}:${ACTIONS_BYTE_TO_KEY[message.topic][message.action]}`)\n    }\n  }\n}\n\nexport const createMQTTSocketWrapper = function (\n  socket: any,\n  handshakeData: any,\n  services: DeepstreamServices,\n  logger: NamespacedLogger\n) { return new MQTTSocketWrapper(socket, handshakeData, services, logger) }\n"
  },
  {
    "path": "src/connection-endpoint/websocket/binary/connection-endpoint.ts",
    "content": "import BaseWebsocketConnectionEndpoint, { WebSocketServerConfig } from '../../base/connection-endpoint'\nimport { createWSSocketWrapper } from './socket-wrapper-factory'\nimport { DeepstreamServices, DeepstreamConfig, WebSocketConnectionEndpoint } from '@deepstream/types'\n\nexport class WSBinaryConnectionEndpoint extends BaseWebsocketConnectionEndpoint implements WebSocketConnectionEndpoint {\n  public description = 'Binary WebSocket Connection Endpoint'\n  constructor (public wsOptions: WebSocketServerConfig, services: DeepstreamServices, config: DeepstreamConfig) {\n    super(wsOptions, services, config)\n  }\n\n  public async init () {\n    super.init()\n    this.services.httpService.registerWebsocketEndpoint(this.wsOptions.urlPath, createWSSocketWrapper, this)\n  }\n}\n"
  },
  {
    "path": "src/connection-endpoint/websocket/binary/socket-wrapper-factory.ts",
    "content": "import * as binaryMessageBuilder from '@deepstream/protobuf/dist/src/message-builder'\nimport * as binaryMessageParser from '@deepstream/protobuf/dist/src/message-parser'\nimport { ParseResult, Message } from '../../../constants'\nimport { WebSocketServerConfig } from '../../base/connection-endpoint'\nimport { SocketConnectionEndpoint, DeepstreamServices } from '@deepstream/types'\nimport { WSSocketWrapper } from '../../base/socket-wrapper'\n\nexport class WSBinarySocketWrapper extends WSSocketWrapper<Uint8Array> {\n  public socketType = 'wsBinary'\n\n  public getAckMessage (message: Message): Uint8Array {\n    return binaryMessageBuilder.getMessage(message, true)\n  }\n\n  public getMessage (message: Message): Uint8Array {\n    return binaryMessageBuilder.getMessage(message, false)\n  }\n\n  public parseMessage (message: ArrayBuffer): ParseResult[] {\n    if (typeof message === 'string') {\n      this.invalidTypeReceived()\n      return []\n    }\n\n    /* we copy the underlying buffer (since a shallow reference won't be safe\n     * outside of the callback)\n     * the copy could be avoided if we make sure not to store references to the\n     * raw buffer within the message\n     */\n    return binaryMessageParser.parse(Buffer.from(Buffer.from(message)))\n  }\n\n  public parseData (message: Message): true | Error {\n    return binaryMessageParser.parseData(message)\n  }\n}\n\nexport const createWSSocketWrapper = function (\n  socket: any,\n  handshakeData: any,\n  services: DeepstreamServices,\n  config: WebSocketServerConfig,\n  connectionEndpoint: SocketConnectionEndpoint\n) { return new WSBinarySocketWrapper(socket, handshakeData, services, config, connectionEndpoint, true) }\n"
  },
  {
    "path": "src/connection-endpoint/websocket/json/connection-endpoint.ts",
    "content": "import WebsocketConnectionEndpoint, { WebSocketServerConfig } from '../../base/connection-endpoint'\nimport {createWSSocketWrapper} from './socket-wrapper-factory'\nimport { DeepstreamServices, DeepstreamConfig } from '@deepstream/types'\n\nexport class WSJSONConnectionEndpoint extends WebsocketConnectionEndpoint {\n  public description = 'WS Text Connection Endpoint'\n  constructor (public wsOptions: WebSocketServerConfig, services: DeepstreamServices, config: DeepstreamConfig) {\n    super(wsOptions, services, config)\n  }\n\n  public init () {\n    super.init()\n    this.services.httpService.registerWebsocketEndpoint(this.wsOptions.urlPath, createWSSocketWrapper, this)\n  }\n}\n"
  },
  {
    "path": "src/connection-endpoint/websocket/json/socket-wrapper-factory.ts",
    "content": "import { ParseResult, Message } from '../../../constants'\nimport { WebSocketServerConfig } from '../../base/connection-endpoint'\nimport { SocketConnectionEndpoint, DeepstreamServices } from '@deepstream/types'\nimport { WSSocketWrapper } from '../../base/socket-wrapper'\n\nexport class JSONSocketWrapper extends WSSocketWrapper<string> {\n  public socketType = 'wsJSON'\n\n  public getMessage (message: Message): string {\n    return JSON.stringify(message)\n  }\n\n  public getAckMessage (message: Message): string {\n    return this.getMessage(message)\n  }\n\n  public parseMessage (message: string): ParseResult[] {\n    if (typeof message !== 'string') {\n      this.invalidTypeReceived()\n      return []\n    }\n\n    try {\n      return [JSON.parse(message)]\n    } catch (e) {\n      this.invalidTypeReceived()\n      return []\n    }\n  }\n\n  public parseData (message: Message): true | Error {\n    try {\n      if (message.data) {\n        message.parsedData = JSON.parse(message.data as string)\n      }\n      return true\n    } catch (e) {\n      if (e instanceof Error) {\n        return e\n      }\n      return new Error(`Unknown error: ${e}`)\n    }\n  }\n}\n\nexport const createWSSocketWrapper = function (\n  socket: any,\n  handshakeData: any,\n  services: DeepstreamServices,\n  config: WebSocketServerConfig,\n  connectionEndpoint: SocketConnectionEndpoint\n) { return new JSONSocketWrapper(socket, handshakeData, services, config, connectionEndpoint, false) }\n"
  },
  {
    "path": "src/connection-endpoint/websocket/text/connection-endpoint.ts",
    "content": "import { WebSocketServerConfig } from '../../base/connection-endpoint'\nimport BaseWebsocketConnectionEndpoint from '../../base/connection-endpoint'\nimport {createWSSocketWrapper} from './socket-wrapper-factory'\nimport { DeepstreamServices, DeepstreamConfig, UnauthenticatedSocketWrapper, WebSocketConnectionEndpoint } from '@deepstream/types'\nimport * as textMessageBuilder from './text-protocol/message-builder'\nimport { TOPIC, CONNECTION_ACTION } from '../../../constants'\n\nexport class WSTextConnectionEndpoint extends BaseWebsocketConnectionEndpoint implements WebSocketConnectionEndpoint {\n  public description = 'WS Text Protocol Connection Endpoint'\n  private pingMessage: string\n\n  constructor (public wsOptions: WebSocketServerConfig, services: DeepstreamServices, config: DeepstreamConfig) {\n    super(wsOptions, services, config)\n\n    this.pingMessage = textMessageBuilder.getMessage({\n      topic: TOPIC.CONNECTION,\n      action: CONNECTION_ACTION.PING\n    })\n  }\n\n  public async init () {\n    super.init()\n    this.services.httpService.registerWebsocketEndpoint(this.wsOptions.urlPath, createWSSocketWrapper, this)\n  }\n\n  public onConnection (socketWrapper: UnauthenticatedSocketWrapper) {\n    super.onConnection(socketWrapper)\n    socketWrapper.onMessage = socketWrapper.authCallback!\n    socketWrapper.sendMessage({\n      topic: TOPIC.CONNECTION,\n      action: CONNECTION_ACTION.ACCEPT\n    }, false)\n    this.sendPing(socketWrapper)\n  }\n\n  private sendPing (socketWrapper: UnauthenticatedSocketWrapper) {\n    if (!socketWrapper.isClosed) {\n      socketWrapper.sendBuiltMessage!(this.pingMessage)\n      setTimeout(this.sendPing.bind(this, socketWrapper), this.wsOptions.heartbeatInterval)\n    }\n  }\n}\n"
  },
  {
    "path": "src/connection-endpoint/websocket/text/socket-wrapper-factory.ts",
    "content": "import { ParseResult, Message } from '../../../constants'\nimport * as textMessageBuilder from './text-protocol/message-builder'\nimport * as textMessageParse from './text-protocol/message-parser'\nimport { SocketConnectionEndpoint, DeepstreamServices } from '@deepstream/types'\nimport { WebSocketServerConfig } from '../../base/connection-endpoint'\nimport { WSSocketWrapper } from '../../base/socket-wrapper'\n\nexport class TextWSSocketWrapper extends WSSocketWrapper<string> {\n  public socketType = 'wsText'\n\n  public getMessage (message: Message): string {\n    return textMessageBuilder.getMessage(message, false)\n  }\n\n  public getAckMessage (message: Message): string {\n    return textMessageBuilder.getMessage(message, true)\n  }\n\n  public parseMessage (message: string): ParseResult[] {\n    if (typeof message !== 'string') {\n      this.invalidTypeReceived()\n      return []\n    }\n\n    return textMessageParse.parse(message)\n  }\n\n  public parseData (message: Message): true | Error {\n    return textMessageParse.parseData(message)\n  }\n}\n\nexport const createWSSocketWrapper = function (\n  socket: any,\n  handshakeData: any,\n  services: DeepstreamServices,\n  config: WebSocketServerConfig,\n  connectionEndpoint: SocketConnectionEndpoint,\n) { return new TextWSSocketWrapper(socket, handshakeData, services, config, connectionEndpoint, false) }\n"
  },
  {
    "path": "src/connection-endpoint/websocket/text/text-protocol/constants.ts",
    "content": "import {\n  AUTH_ACTION as AA,\n  CONNECTION_ACTION as CA,\n  EVENT_ACTION as EA,\n  PARSER_ACTION as XA,\n  PRESENCE_ACTION as UA,\n  RECORD_ACTION as RA,\n  RPC_ACTION as PA,\n  TOPIC as T,\n} from '../../../../constants'\n\nexport const MESSAGE_SEPERATOR = String.fromCharCode(30) // ASCII Record Seperator 1E\nexport const MESSAGE_PART_SEPERATOR = String.fromCharCode(31) // ASCII Unit Separator 1F\n\nexport const PAYLOAD_ENCODING = {\n  JSON: 0x00,\n  DEEPSTREAM: 0x01,\n}\n\nexport const TOPIC = {\n  PARSER: { TEXT: 'X', BYTE: T.PARSER },\n  CONNECTION: { TEXT: 'C', BYTE: T.CONNECTION },\n  AUTH: { TEXT: 'A', BYTE: T.AUTH },\n  ERROR: { TEXT: 'X', BYTE: T.ERROR },\n  EVENT: { TEXT: 'E', BYTE: T.EVENT },\n  RECORD: { TEXT: 'R', BYTE: T.RECORD },\n  RPC: { TEXT: 'P', BYTE: T.RPC },\n  PRESENCE: { TEXT: 'U', BYTE: T.PRESENCE },\n}\n\nexport const PARSER_ACTIONS = {\n  UNKNOWN_TOPIC: { BYTE: XA.UNKNOWN_TOPIC },\n  UNKNOWN_ACTION: { BYTE: XA.UNKNOWN_ACTION },\n  INVALID_MESSAGE: { BYTE: XA.INVALID_MESSAGE },\n  INVALID_META_PARAMS: { BYTE: XA.INVALID_META_PARAMS },\n  MESSAGE_PARSE_ERROR: { BYTE: XA.MESSAGE_PARSE_ERROR },\n  MAXIMUM_MESSAGE_SIZE_EXCEEDED: { BYTE: XA.MAXIMUM_MESSAGE_SIZE_EXCEEDED },\n  ERROR: { BYTE: XA.ERROR },\n}\n\nexport const CONNECTION_ACTIONS = {\n  ERROR: { TEXT: 'E', BYTE: CA.ERROR },\n  PING: { TEXT: 'PI', BYTE: CA.PING },\n  PONG: { TEXT: 'PO', BYTE: CA.PONG },\n  ACCEPT: { TEXT: 'A', BYTE: CA.ACCEPT },\n  CHALLENGE: { TEXT: 'CH', BYTE: CA.CHALLENGE },\n  REJECTION: { TEXT: 'REJ', BYTE: CA.REJECT },\n  REDIRECT: { TEXT: 'RED', BYTE: CA.REDIRECT },\n\n  CLOSED: { BYTE: CA.CLOSED },\n  CLOSING: { BYTE: CA.CLOSING },\n\n  CONNECTION_AUTHENTICATION_TIMEOUT: { BYTE: CA.AUTHENTICATION_TIMEOUT },\n  INVALID_MESSAGE: { BYTE: CA.INVALID_MESSAGE },\n}\n\nexport const AUTH_ACTIONS = {\n  ERROR: { TEXT: 'E', BYTE: AA.ERROR },\n  REQUEST: { TEXT: 'REQ', BYTE: AA.REQUEST },\n  AUTH_SUCCESSFUL: { BYTE: AA.AUTH_SUCCESSFUL, PAYLOAD_ENCODING: PAYLOAD_ENCODING.DEEPSTREAM },\n  AUTH_UNSUCCESSFUL: { BYTE: AA.AUTH_UNSUCCESSFUL, PAYLOAD_ENCODING: PAYLOAD_ENCODING.DEEPSTREAM },\n  TOO_MANY_AUTH_ATTEMPTS: { BYTE: AA.TOO_MANY_AUTH_ATTEMPTS },\n\n  // MESSAGE_PERMISSION_ERROR: { BYTE: AA.MESSAGE_PERMISSION_ERROR },\n  // MESSAGE_DENIED: { BYTE: AA.MESSAGE_DENIED },\n  INVALID_MESSAGE_DATA: { BYTE: AA.INVALID_MESSAGE_DATA },\n  INVALID_MESSAGE: { BYTE: AA.INVALID_MESSAGE },\n}\n\nexport const EVENT_ACTIONS = {\n  ERROR: { TEXT: 'E', BYTE: EA.ERROR },\n  EMIT: { TEXT: 'EVT', BYTE: EA.EMIT, PAYLOAD_ENCODING: PAYLOAD_ENCODING.DEEPSTREAM },\n  SUBSCRIBE: { TEXT: 'S', BYTE: EA.SUBSCRIBE },\n  UNSUBSCRIBE: { TEXT: 'US', BYTE: EA.UNSUBSCRIBE },\n  LISTEN: { TEXT: 'L', BYTE: EA.LISTEN },\n  UNLISTEN: { TEXT: 'UL', BYTE: EA.UNLISTEN },\n  LISTEN_ACCEPT: { TEXT: 'LA', BYTE: EA.LISTEN_ACCEPT },\n  LISTEN_REJECT: { TEXT: 'LR', BYTE: EA.LISTEN_REJECT },\n  SUBSCRIPTION_FOR_PATTERN_FOUND: { TEXT: 'SP', BYTE: EA.SUBSCRIPTION_FOR_PATTERN_FOUND },\n  SUBSCRIPTION_FOR_PATTERN_REMOVED: { TEXT: 'SR', BYTE: EA.SUBSCRIPTION_FOR_PATTERN_REMOVED },\n\n  MESSAGE_PERMISSION_ERROR: { BYTE: EA.MESSAGE_PERMISSION_ERROR },\n  MESSAGE_DENIED: { BYTE: EA.MESSAGE_DENIED },\n  INVALID_MESSAGE_DATA: { BYTE: EA.INVALID_MESSAGE_DATA },\n  MULTIPLE_SUBSCRIPTIONS: { BYTE: EA.MULTIPLE_SUBSCRIPTIONS },\n  NOT_SUBSCRIBED: { BYTE: EA.NOT_SUBSCRIBED },\n}\n\nexport const RECORD_ACTIONS = {\n  ERROR: { TEXT: 'E', BYTE: RA.ERROR },\n  CREATE: { TEXT: 'CR', BYTE: RA.CREATE },\n  READ: { TEXT: 'R', BYTE: RA.READ },\n  READ_RESPONSE: { BYTE: RA.READ_RESPONSE, PAYLOAD_ENCODING: PAYLOAD_ENCODING.JSON },\n  HEAD: { TEXT: 'HD', BYTE: RA.HEAD },\n  HEAD_RESPONSE: { BYTE: RA.HEAD_RESPONSE },\n  CREATEANDUPDATE: { TEXT: 'CU', BYTE: RA.CREATEANDUPDATE },\n  CREATEANDPATCH: { BYTE: RA.CREATEANDPATCH, PAYLOAD_ENCODING: PAYLOAD_ENCODING.DEEPSTREAM },\n  UPDATE: { TEXT: 'U', BYTE: RA.UPDATE, PAYLOAD_ENCODING: PAYLOAD_ENCODING.JSON },\n  PATCH: { TEXT: 'P', BYTE: RA.PATCH, PAYLOAD_ENCODING: PAYLOAD_ENCODING.DEEPSTREAM },\n  ERASE: { BYTE: RA.ERASE, PAYLOAD_ENCODING: PAYLOAD_ENCODING.DEEPSTREAM },\n  WRITE_ACKNOWLEDGEMENT: { TEXT: 'WA', BYTE: RA.WRITE_ACKNOWLEDGEMENT },\n  DELETE: { TEXT: 'D', BYTE: RA.DELETE },\n  DELETE_SUCCESS: { BYTE: RA.DELETE_SUCCESS },\n  DELETED: { BYTE: RA.DELETED },\n  LISTEN_RESPONSE_TIMEOUT: { BYTE: RA.LISTEN_RESPONSE_TIMEOUT },\n\n  SUBSCRIBEANDHEAD: { BYTE: RA.SUBSCRIBEANDHEAD },\n  // SUBSCRIBEANDHEAD_RESPONSE: { BYTE: RA.SUBSCRIBEANDHEAD_RESPONSE },\n  SUBSCRIBEANDREAD: { BYTE: RA.SUBSCRIBEANDREAD },\n  // SUBSCRIBEANDREAD_RESPONSE: { BYTE: RA.SUBSCRIBEANDREAD_RESPONSE },\n  SUBSCRIBECREATEANDREAD: { TEXT: 'CR', BYTE: RA.SUBSCRIBECREATEANDREAD },\n  // SUBSCRIBECREATEANDREAD_RESPONSE: { BYTE: RA.SUBSCRIBECREATEANDREAD_RESPONSE },\n  SUBSCRIBECREATEANDUPDATE: { BYTE: RA.SUBSCRIBECREATEANDUPDATE },\n  // SUBSCRIBECREATEANDUPDATE_RESPONSE: { BYTE: RA.SUBSCRIBECREATEANDUPDATE_RESPONSE },\n  SUBSCRIBE: { TEXT: 'S', BYTE: RA.SUBSCRIBE },\n  UNSUBSCRIBE: { TEXT: 'US', BYTE: RA.UNSUBSCRIBE },\n\n  LISTEN: { TEXT: 'L', BYTE: RA.LISTEN },\n  UNLISTEN: { TEXT: 'UL', BYTE: RA.UNLISTEN },\n  LISTEN_ACCEPT: { TEXT: 'LA', BYTE: RA.LISTEN_ACCEPT },\n  LISTEN_REJECT: { TEXT: 'LR', BYTE: RA.LISTEN_REJECT },\n  SUBSCRIPTION_HAS_PROVIDER: { TEXT: 'SH', BYTE: RA.SUBSCRIPTION_HAS_PROVIDER },\n  SUBSCRIPTION_HAS_NO_PROVIDER: { BYTE: RA.SUBSCRIPTION_HAS_NO_PROVIDER },\n  SUBSCRIPTION_FOR_PATTERN_FOUND: { TEXT: 'SP', BYTE: RA.SUBSCRIPTION_FOR_PATTERN_FOUND },\n  SUBSCRIPTION_FOR_PATTERN_REMOVED: { TEXT: 'SR', BYTE: RA.SUBSCRIPTION_FOR_PATTERN_REMOVED },\n\n  CACHE_RETRIEVAL_TIMEOUT: { BYTE: RA.CACHE_RETRIEVAL_TIMEOUT },\n  STORAGE_RETRIEVAL_TIMEOUT: { BYTE: RA.STORAGE_RETRIEVAL_TIMEOUT },\n  VERSION_EXISTS: { BYTE: RA.VERSION_EXISTS },\n\n  // HAS: { TEXT: 'H', BYTE: RA.HAS },\n  // HAS_RESPONSE: { BYTE: RA.HAS_RESPONSE },\n  SNAPSHOT: { TEXT: 'SN', BYTE: RA.READ },\n\n  RECORD_LOAD_ERROR: { BYTE: RA.RECORD_LOAD_ERROR },\n  RECORD_CREATE_ERROR: { BYTE: RA.RECORD_CREATE_ERROR },\n  RECORD_UPDATE_ERROR: { BYTE: RA.RECORD_UPDATE_ERROR },\n  RECORD_DELETE_ERROR: { BYTE: RA.RECORD_DELETE_ERROR },\n  // RECORD_READ_ERROR: { BYTE: RA.RECORD_READ_ERROR },\n  RECORD_NOT_FOUND: { BYTE: RA.RECORD_NOT_FOUND },\n  INVALID_VERSION: { BYTE: RA.INVALID_VERSION },\n  INVALID_PATCH_ON_HOTPATH: { BYTE: RA.INVALID_PATCH_ON_HOTPATH },\n\n  MESSAGE_PERMISSION_ERROR: { BYTE: RA.MESSAGE_PERMISSION_ERROR },\n  MESSAGE_DENIED: { BYTE: RA.MESSAGE_DENIED },\n  INVALID_MESSAGE_DATA: { BYTE: RA.INVALID_MESSAGE_DATA },\n  MULTIPLE_SUBSCRIPTIONS: { BYTE: RA.MULTIPLE_SUBSCRIPTIONS },\n  NOT_SUBSCRIBED: { BYTE: RA.NOT_SUBSCRIBED },\n}\n\nexport const RPC_ACTIONS = {\n  ERROR: { BYTE: PA.ERROR },\n  REQUEST: { TEXT: 'REQ', BYTE: PA.REQUEST, PAYLOAD_ENCODING: PAYLOAD_ENCODING.DEEPSTREAM },\n  ACCEPT: { BYTE: PA.ACCEPT },\n  RESPONSE: { TEXT: 'RES', BYTE: PA.RESPONSE, PAYLOAD_ENCODING: PAYLOAD_ENCODING.DEEPSTREAM },\n  REJECT: { TEXT: 'REJ', BYTE: PA.REJECT },\n  REQUEST_ERROR: { TEXT: 'E', BYTE: PA.REQUEST_ERROR, PAYLOAD_ENCODING: PAYLOAD_ENCODING.DEEPSTREAM },\n  PROVIDE: { TEXT: 'S', BYTE: PA.PROVIDE },\n  UNPROVIDE: { TEXT: 'US', BYTE: PA.UNPROVIDE },\n\n  NO_RPC_PROVIDER: { BYTE: PA.NO_RPC_PROVIDER },\n  RESPONSE_TIMEOUT: { BYTE: PA.RESPONSE_TIMEOUT },\n  ACCEPT_TIMEOUT: { BYTE: PA.ACCEPT_TIMEOUT },\n  MULTIPLE_ACCEPT: { BYTE: PA.MULTIPLE_ACCEPT },\n  MULTIPLE_RESPONSE: { BYTE: PA.MULTIPLE_RESPONSE },\n  INVALID_RPC_CORRELATION_ID: { BYTE: PA.INVALID_RPC_CORRELATION_ID },\n\n  MESSAGE_PERMISSION_ERROR: { BYTE: PA.MESSAGE_PERMISSION_ERROR },\n  MESSAGE_DENIED: { BYTE: PA.MESSAGE_DENIED },\n  INVALID_MESSAGE_DATA: { BYTE: PA.INVALID_MESSAGE_DATA },\n  MULTIPLE_PROVIDERS: { BYTE: PA.MULTIPLE_PROVIDERS },\n  NOT_PROVIDED: { BYTE: PA.NOT_PROVIDED },\n}\n\nexport const PRESENCE_ACTIONS = {\n  ERROR: { TEXT: 'E', BYTE: UA.ERROR },\n  QUERY_ALL: { BYTE: UA.QUERY_ALL },\n  QUERY_ALL_RESPONSE: { BYTE: UA.QUERY_ALL_RESPONSE, PAYLOAD_ENCODING: PAYLOAD_ENCODING.JSON },\n  QUERY: { TEXT: 'Q', BYTE: UA.QUERY },\n  QUERY_RESPONSE: { BYTE: UA.QUERY_RESPONSE, PAYLOAD_ENCODING: PAYLOAD_ENCODING.JSON },\n  PRESENCE_JOIN: { TEXT: 'PNJ', BYTE: UA.PRESENCE_JOIN },\n  PRESENCE_JOIN_ALL: { TEXT: 'PNJ', BYTE: UA.PRESENCE_JOIN_ALL },\n  PRESENCE_LEAVE: { TEXT: 'PNL', BYTE: UA.PRESENCE_LEAVE },\n  PRESENCE_LEAVE_ALL: { TEXT: 'PNL', BYTE: UA.PRESENCE_LEAVE_ALL },\n  SUBSCRIBE: { TEXT: 'S', BYTE: UA.SUBSCRIBE },\n  UNSUBSCRIBE: { TEXT: 'US', BYTE: UA.UNSUBSCRIBE },\n\n  SUBSCRIBE_ALL: { BYTE: UA.SUBSCRIBE_ALL },\n  UNSUBSCRIBE_ALL: { BYTE: UA.UNSUBSCRIBE_ALL },\n\n  INVALID_PRESENCE_USERS: { BYTE: UA.INVALID_PRESENCE_USERS },\n\n  MESSAGE_PERMISSION_ERROR: { BYTE: UA.MESSAGE_PERMISSION_ERROR },\n  MESSAGE_DENIED: { BYTE: UA.MESSAGE_DENIED },\n  // INVALID_MESSAGE_DATA: { BYTE: UA.INVALID_MESSAGE_DATA },\n  MULTIPLE_SUBSCRIPTIONS: { BYTE: UA.MULTIPLE_SUBSCRIPTIONS },\n  NOT_SUBSCRIBED: { BYTE: UA.NOT_SUBSCRIBED },\n}\n\nexport const DEEPSTREAM_TYPES = {\n  STRING: 'S',\n  OBJECT: 'O',\n  NUMBER: 'N',\n  NULL: 'L',\n  TRUE: 'T',\n  FALSE: 'F',\n  UNDEFINED: 'U',\n}\n\nexport const TOPIC_BYTE_TO_TEXT = convertMap(TOPIC, 'BYTE', 'TEXT')\nexport const TOPIC_TEXT_TO_BYTE = convertMap(TOPIC, 'TEXT', 'BYTE')\nexport const TOPIC_TEXT_TO_KEY = reverseMap(specifyMap(TOPIC, 'TEXT'))\nexport const TOPIC_BYTE_TO_KEY = reverseMap(specifyMap(TOPIC, 'BYTE'))\nexport const TOPIC_BYTES = specifyMap(TOPIC, 'BYTE')\n\nexport const ACTIONS_BYTE_TO_PAYLOAD: any = {}\nexport const ACTIONS_BYTE_TO_TEXT: any  = {}\nexport const ACTIONS_TEXT_TO_BYTE: any  = {}\nexport const ACTIONS_BYTES: any  = {}\nexport const ACTIONS_TEXT_TO_KEY: any  = {}\nexport const ACTIONS_BYTE_TO_KEY: any  = {}\n\nexport const ACTIONS = {\n  [TOPIC.PARSER.BYTE]: PARSER_ACTIONS,\n  [TOPIC.CONNECTION.BYTE]: CONNECTION_ACTIONS,\n  [TOPIC.AUTH.BYTE]: AUTH_ACTIONS,\n  [TOPIC.EVENT.BYTE]: EVENT_ACTIONS,\n  [TOPIC.RECORD.BYTE]: RECORD_ACTIONS,\n  [TOPIC.RPC.BYTE]: RPC_ACTIONS,\n  [TOPIC.PRESENCE.BYTE]: PRESENCE_ACTIONS,\n}\n\nfor (const key in ACTIONS) {\n  ACTIONS_BYTE_TO_PAYLOAD[key] = convertMap(ACTIONS[key], 'BYTE', 'PAYLOAD_ENCODING')\n  ACTIONS_BYTE_TO_TEXT[key] = convertMap(ACTIONS[key], 'BYTE', 'TEXT')\n  ACTIONS_TEXT_TO_BYTE[key] = convertMap(ACTIONS[key], 'TEXT', 'BYTE')\n  ACTIONS_BYTES[key] = specifyMap(ACTIONS[key], 'BYTE')\n  ACTIONS_TEXT_TO_KEY[key] = reverseMap(specifyMap(ACTIONS[key], 'TEXT'))\n  ACTIONS_BYTE_TO_KEY[key] = reverseMap(specifyMap(ACTIONS[key], 'BYTE'))\n}\n\n/**\n * convertMap({ a: { x: 1 }, b: { x: 2 }, c: { x : 3 } }, 'x', 'y')\n *  ===\n * { a: { y: 1 }, b: { y: 2 }, c: { y : 3 } }\n */\nfunction convertMap (map: any, from: any, to: any) {\n  const result: any = {}\n\n  for (const key in map) {\n    result[map[key][from]] = map[key][to]\n  }\n\n  return result\n}\n\n/**\n * specifyMap({ a: { x: 1 }, b: { x: 2 }, c: { x : 3 } }, 'x')\n *  ===\n * { a: 1, b: 2, c: 3 }\n */\nfunction specifyMap (map: any, innerKey: any) {\n  const result: any = {}\n\n  for (const key in map) {\n    result[key] = map[key][innerKey]\n  }\n\n  return result\n}\n\n/**\n * Takes a key-value map and returns\n * a map with { value: key } of the old map\n */\nfunction reverseMap (map: any) {\n  const reversedMap: any = {}\n\n  for (const key in map) {\n    reversedMap[map[key]] = key\n  }\n\n  return reversedMap\n}\n"
  },
  {
    "path": "src/connection-endpoint/websocket/text/text-protocol/message-builder.ts",
    "content": "import {\n  ACTIONS_BYTE_TO_PAYLOAD as ABP,\n  ACTIONS_BYTE_TO_TEXT as ABT,\n  AUTH_ACTIONS as AA,\n  CONNECTION_ACTIONS as CA,\n  DEEPSTREAM_TYPES as TYPES,\n  EVENT_ACTIONS as EA,\n  MESSAGE_PART_SEPERATOR as y,\n  MESSAGE_SEPERATOR as x,\n  PRESENCE_ACTIONS as UA,\n  RECORD_ACTIONS as RA,\n  RPC_ACTIONS as PA,\n  TOPIC,\n  TOPIC_BYTE_TO_TEXT as TBT,\n  PAYLOAD_ENCODING,\n} from './constants'\nimport { Message } from '../../../../constants'\nimport { correlationIdToVersion, bulkNameToCorrelationId } from './message-parser'\nconst WA = y + JSON.stringify({ writeSuccess: true })\nconst NWA = y + '{}'\nconst A = 'A' + y\n\nconst genericError = (msg: Message) => `${TBT[msg.topic]}${y}E${y}${msg.correlationId}${y}${msg.parsedData}${x}`\nconst invalidMessageData = (msg: Message) => `${TBT[msg.topic]}${y}E${y}INVALID_MESSAGE_DATA${y}${msg.data}${x}`\nconst messagePermissionError = (msg: Message) => `${TBT[msg.topic]}${y}E${y}MESSAGE_PERMISSION_ERROR${y}${msg.name}${ABT[msg.topic][msg.action] ? y + ABT[msg.topic][msg.action] : '' }${msg.correlationId ? y + msg.correlationId : '' }${x}`\nconst messageDenied = (msg: Message) => {\n  let version\n  if (msg.topic === TOPIC.RECORD.BYTE && msg.correlationId) {\n    version = correlationIdToVersion.get(msg.correlationId!)\n    correlationIdToVersion.delete(msg.correlationId!)\n    delete msg.correlationId\n  }\n  return `${TBT[msg.topic]}${y}E${y}MESSAGE_DENIED${y}${msg.name}${ABT[msg.topic][msg.action] ? y + ABT[msg.topic][msg.action] : '' }${msg.originalAction ? y + ABT[msg.topic][msg.originalAction] : '' }${msg.correlationId ? y + msg.correlationId : '' }${version !== undefined ? y + version : '' }${x}`\n}\nconst notSubscribed = (msg: Message) => `${TBT[msg.topic]}${y}E${y}NOT_SUBSCRIBED${y}${msg.name}${x}`\nconst invalidAuth = (msg: Message) => `A${y}E${y}INVALID_AUTH_DATA${y}${msg.data ? msg.data : 'U' }${x}`\nconst recordUpdate = (msg: Message) => `R${y}U${y}${msg.name}${y}${msg.version}${y}${msg.data}${msg.isWriteAck ? WA : '' }${x}`\nconst recordPatch = (msg: Message) => `R${y}P${y}${msg.name}${y}${msg.version}${y}${msg.path}${y}${msg.data}${msg.isWriteAck ? WA : '' }${x}`\nconst subscriptionForPatternFound = (msg: Message) => `${TBT[msg.topic]}${y}SP${y}${msg.name}${y}${msg.subscription}${x}`\nconst subscriptionForPatternRemoved = (msg: Message) => `${TBT[msg.topic]}${y}SR${y}${msg.name}${y}${msg.subscription}${x}`\nconst listen = (msg: Message, isAck: boolean) => `${TBT[msg.topic]}${y}${isAck ? A : '' }L${y}${msg.name}${x}`\nconst unlisten = (msg: Message, isAck: boolean) => `${TBT[msg.topic]}${y}${isAck ? A : '' }UL${y}${msg.name}${x}`\nconst listenAccept = (msg: Message) => `${TBT[msg.topic]}${y}LA${y}${msg.name}${y}${msg.subscription}${x}`\nconst listenReject = (msg: Message) => `${TBT[msg.topic]}${y}LR${y}${msg.name}${y}${msg.subscription}${x}`\nconst multipleSubscriptions = (msg: Message) => `${TBT[msg.topic]}${y}E${y}MULTIPLE_SUBSCRIPTIONS${y}${msg.name}${x}`\n\nconst BUILDERS = {\n  [TOPIC.CONNECTION.BYTE]: {\n    [CA.ERROR.BYTE]: genericError,\n    [CA.CHALLENGE.BYTE]: (msg: Message) => `C${y}CH${x}`,\n    [CA.ACCEPT.BYTE]: (msg: Message) => `C${y}A${x}`,\n    [CA.REJECTION.BYTE]: (msg: Message) => `C${y}REJ${y}${msg.data}${x}`,\n    [CA.REDIRECT.BYTE]: (msg: Message) => `C${y}RED${y}${msg.data}${x}`,\n    [CA.PING.BYTE]: (msg: Message) => `C${y}PI${x}`,\n    [CA.PONG.BYTE]: (msg: Message) => `C${y}PO${x}`,\n    [CA.CONNECTION_AUTHENTICATION_TIMEOUT.BYTE]: (msg: Message) => `C${y}E${y}CONNECTION_AUTHENTICATION_TIMEOUT${x}`,\n  },\n  [TOPIC.AUTH.BYTE]: {\n    [AA.ERROR.BYTE]: genericError,\n    [AA.REQUEST.BYTE]: (msg: Message) => `A${y}REQ${y}${msg.data}${x}`,\n    [AA.AUTH_SUCCESSFUL.BYTE]: (msg: Message) => `A${y}A${msg.data ? y + msg.data : ''}${x}`,\n    [AA.AUTH_UNSUCCESSFUL.BYTE]: invalidAuth,\n    [AA.INVALID_MESSAGE_DATA.BYTE]: invalidAuth,\n    [AA.TOO_MANY_AUTH_ATTEMPTS.BYTE]: (msg: Message) => `A${y}E${y}TOO_MANY_AUTH_ATTEMPTS${x}`,\n  },\n  [TOPIC.EVENT.BYTE]: {\n    [EA.ERROR.BYTE]: genericError,\n    [EA.SUBSCRIBE.BYTE]: (msg: Message, isAck: boolean) => {\n      let name = msg.name\n      if (isAck) {\n        name = bulkNameToCorrelationId.get(msg.correlationId!)\n        bulkNameToCorrelationId.delete(msg.correlationId!)\n      }\n      return `E${y}${isAck ? A : '' }S${y}${name}${x}`\n    },\n    [EA.UNSUBSCRIBE.BYTE]: (msg: Message, isAck: boolean) => {\n      let name = msg.name\n      if (isAck) {\n        name = bulkNameToCorrelationId.get(msg.correlationId!)\n        bulkNameToCorrelationId.delete(msg.correlationId!)\n      }\n      return `E${y}${isAck ? A : '' }US${y}${name}${x}`\n    },\n    [EA.EMIT.BYTE]: (msg: Message) => `E${y}EVT${y}${msg.name}${y}${msg.data ? msg.data : 'U'}${x}`,\n    [EA.LISTEN.BYTE]: listen,\n    [EA.UNLISTEN.BYTE]: unlisten,\n    [EA.LISTEN_ACCEPT.BYTE]: listenAccept,\n    [EA.LISTEN_REJECT.BYTE]: listenReject,\n    [EA.SUBSCRIPTION_FOR_PATTERN_FOUND.BYTE]: subscriptionForPatternFound,\n    [EA.SUBSCRIPTION_FOR_PATTERN_REMOVED.BYTE]: subscriptionForPatternRemoved,\n    [EA.INVALID_MESSAGE_DATA.BYTE]: invalidMessageData,\n    [EA.MESSAGE_DENIED.BYTE]: messageDenied,\n    [EA.MESSAGE_PERMISSION_ERROR.BYTE]: messagePermissionError,\n    [EA.NOT_SUBSCRIBED.BYTE]: notSubscribed,\n    [EA.MULTIPLE_SUBSCRIPTIONS.BYTE]: multipleSubscriptions,\n  },\n  [TOPIC.RECORD.BYTE]: {\n    [RA.ERROR.BYTE]: genericError,\n    [RA.HEAD.BYTE]: (msg: Message) => `R${y}HD${y}${msg.name}${x}`,\n    [RA.HEAD_RESPONSE.BYTE]: (msg: Message) => `R${y}HD${y}${msg.name}${y}${msg.version}${y}null${x}`,\n    [RA.READ.BYTE]: (msg: Message) => `R${y}R${y}${msg.name}${x}`,\n    [RA.READ_RESPONSE.BYTE]: (msg: Message) => `R${y}R${y}${msg.name}${y}${msg.version}${y}${msg.data}${x}`,\n    [RA.UPDATE.BYTE]: recordUpdate,\n    [RA.PATCH.BYTE]: recordPatch,\n    [RA.ERASE.BYTE]: (msg: Message) => `R${y}P${y}${msg.name}${y}${msg.version}${y}${msg.path}${y}U${msg.isWriteAck ? WA : '' }${x}`,\n    [RA.CREATEANDUPDATE.BYTE]: (msg: Message) => `R${y}CU${y}${msg.name}${y}${msg.version}${y}${msg.data}${msg.isWriteAck ? WA : NWA }${x}`,\n    [RA.CREATEANDPATCH.BYTE]: (msg: Message) => `R${y}CU${y}${msg.name}${y}${msg.version}${y}${msg.path}${y}${msg.data}${msg.isWriteAck ? WA : NWA }${x}`,\n    [RA.DELETE.BYTE]: (msg: Message, isAck: boolean) => `R${y}${isAck ? A : '' }D${y}${msg.name}${x}`,\n    [RA.DELETED.BYTE]: (msg: Message) => `R${y}A${y}D${y}${msg.name}${x}`,\n    [RA.DELETE_SUCCESS.BYTE]: (msg: Message) => `R${y}A${y}D${y}${msg.name}${x}`,\n    [RA.SUBSCRIBECREATEANDREAD.BYTE]: (msg: Message, isAck: boolean) => {\n      if (isAck) {\n        return `R${y}A${y}S${y}${msg.name}${x}`\n      }\n      return `R${y}CR${y}${msg.name}${x}`\n    },\n    [RA.UNSUBSCRIBE.BYTE]: (msg: Message, isAck: boolean) => {\n      let name = msg.name\n      if (isAck) {\n        name = bulkNameToCorrelationId.get(msg.correlationId!)\n        bulkNameToCorrelationId.delete(msg.correlationId!)\n      }\n      return `R${y}${isAck ? A : '' }US${y}${name}${x}`\n    },\n    [RA.WRITE_ACKNOWLEDGEMENT.BYTE]: (msg: Message) => {\n      return `R${y}WA${y}${msg.name}${y}[${correlationIdToVersion.get(msg.correlationId!)}]${y}${TYPES.NULL}${x}`\n    },\n\n    [RA.LISTEN.BYTE]: listen,\n    [RA.LISTEN_RESPONSE_TIMEOUT.BYTE]: (msg: Message) => `C${y}PO${x}`,\n    [RA.UNLISTEN.BYTE]: unlisten,\n    [RA.LISTEN_ACCEPT.BYTE]: listenAccept,\n    [RA.LISTEN_REJECT.BYTE]: listenReject,\n    [RA.SUBSCRIPTION_FOR_PATTERN_FOUND.BYTE]: subscriptionForPatternFound,\n    [RA.SUBSCRIPTION_FOR_PATTERN_REMOVED.BYTE]: subscriptionForPatternRemoved,\n    [RA.SUBSCRIPTION_HAS_PROVIDER.BYTE]: (msg: Message) => `R${y}SH${y}${msg.name}${y}T${x}`,\n    [RA.SUBSCRIPTION_HAS_NO_PROVIDER.BYTE]: (msg: Message) => `R${y}SH${y}${msg.name}${y}F${x}`,\n\n    [RA.STORAGE_RETRIEVAL_TIMEOUT.BYTE]: (msg: Message) => `R${y}E${y}STORAGE_RETRIEVAL_TIMEOUT${y}${msg.name}${x}`,\n    [RA.CACHE_RETRIEVAL_TIMEOUT.BYTE]: (msg: Message) => `R${y}E${y}CACHE_RETRIEVAL_TIMEOUT${y}${msg.name}${x}`,\n    [RA.VERSION_EXISTS.BYTE]: (msg: Message) => `R${y}E${y}VERSION_EXISTS${y}${msg.name}${y}${msg.version}${y}${msg.data}${msg.isWriteAck ? WA : ''}${x}`,\n    [RA.RECORD_NOT_FOUND.BYTE]: (msg: Message) => `R${y}E${y}RECORD_NOT_FOUND${y}${msg.name}${x}`,\n\n    [RA.INVALID_MESSAGE_DATA.BYTE]: invalidMessageData,\n    [RA.MESSAGE_DENIED.BYTE]: messageDenied,\n    [RA.MESSAGE_PERMISSION_ERROR.BYTE]: messagePermissionError,\n    [RA.NOT_SUBSCRIBED.BYTE]: notSubscribed,\n    [RA.MULTIPLE_SUBSCRIPTIONS.BYTE]: multipleSubscriptions,\n  },\n  [TOPIC.RPC.BYTE]: {\n    [PA.ERROR.BYTE]: genericError,\n    [PA.PROVIDE.BYTE]: (msg: Message, isAck: boolean) => {\n      let name = msg.name\n      if (isAck) {\n        name = bulkNameToCorrelationId.get(msg.correlationId!)\n        bulkNameToCorrelationId.delete(msg.correlationId!)\n      }\n      return `P${y}${isAck ? A : '' }S${y}${name}${x}`\n    },\n    [PA.UNPROVIDE.BYTE]: (msg: Message, isAck: boolean) => {\n      let name = msg.name\n      if (isAck) {\n        name = bulkNameToCorrelationId.get(msg.correlationId!)\n        bulkNameToCorrelationId.delete(msg.correlationId!)\n      }\n      return `P${y}${isAck ? A : '' }US${y}${name}${x}`\n    },\n    [PA.REQUEST.BYTE]: (msg: Message) => `P${y}REQ${y}${msg.name}${y}${msg.correlationId}${y}${msg.data}${x}`,\n    [PA.RESPONSE.BYTE]: (msg: Message) => `P${y}RES${y}${msg.name}${y}${msg.correlationId}${y}${msg.data}${x}`,\n    [PA.REQUEST_ERROR.BYTE]: (msg: Message) => `P${y}E${y}${msg.data}${y}${msg.name}${y}${msg.correlationId}${x}`,\n    [PA.REJECT.BYTE]: (msg: Message) => `P${y}REJ${y}${msg.name}${y}${msg.correlationId}${x}`,\n    [PA.ACCEPT.BYTE]: (msg: Message) => `P${y}A${y}REQ${y}${msg.name}${y}${msg.correlationId}${x}`,\n    [PA.NO_RPC_PROVIDER.BYTE]: (msg: Message) => `P${y}E${y}NO_RPC_PROVIDER${y}${msg.name}${y}${msg.correlationId}${x}`,\n    [PA.INVALID_RPC_CORRELATION_ID.BYTE]: (msg: Message) => `P${y}E${y}INVALID_RPC_CORRELATION_ID${y}${msg.name}${y}${msg.correlationId}${x}`,\n    [PA.RESPONSE_TIMEOUT.BYTE]: (msg: Message) => `P${y}E${y}RESPONSE_TIMEOUT${y}${msg.name}${y}${msg.correlationId}${x}`,\n    [PA.MULTIPLE_RESPONSE.BYTE]: (msg: Message) => `P${y}E${y}MULTIPLE_RESPONSE${y}${msg.name}${y}${msg.correlationId}${x}`,\n    [PA.MULTIPLE_ACCEPT.BYTE]: (msg: Message) => `P${y}E${y}MULTIPLE_ACCEPT${y}${msg.name}${y}${msg.correlationId}${x}`,\n    [PA.ACCEPT_TIMEOUT.BYTE]: (msg: Message) => `P${y}E${y}ACCEPT_TIMEOUT${y}${msg.name}${y}${msg.correlationId}${x}`,\n\n    [PA.INVALID_MESSAGE_DATA.BYTE]: invalidMessageData,\n    [PA.MESSAGE_DENIED.BYTE]: messageDenied,\n    [PA.MESSAGE_PERMISSION_ERROR.BYTE]: messagePermissionError,\n    [PA.NOT_PROVIDED.BYTE]: notSubscribed,\n    [PA.MULTIPLE_PROVIDERS.BYTE]: multipleSubscriptions,\n  },\n  [TOPIC.PRESENCE.BYTE]: {\n    [UA.ERROR.BYTE]: genericError,\n    [UA.SUBSCRIBE.BYTE]: (msg: Message, isAck: boolean) => `U${y}${isAck ? A : '' }S${y}${msg.correlationId ? msg.correlationId + y : '' }${msg.name ? msg.name : msg.data}${x}`,\n    [UA.SUBSCRIBE_ALL.BYTE]: (msg: Message, isAck: boolean) => `U${y}${isAck ? A : '' }S${y}S${x}`,\n    [UA.UNSUBSCRIBE.BYTE]: (msg: Message, isAck: boolean)  => `U${y}${isAck ? A : '' }US${y}${msg.correlationId ? msg.correlationId + y : '' }${msg.name ? msg.name : msg.data}${x}`,\n    [UA.UNSUBSCRIBE_ALL.BYTE]: (msg: Message, isAck: boolean)  => `U${y}${isAck ? A : '' }US${y}US${x}`,\n    [UA.QUERY.BYTE]: (msg: Message) => `U${y}Q${y}${msg.correlationId}${y}${msg.data}${x}`,\n    [UA.QUERY_RESPONSE.BYTE]: (msg: Message) => `U${y}Q${y}${msg.correlationId}${y}${msg.data}${x}`,\n    [UA.QUERY_ALL.BYTE]: (msg: Message) => `U${y}Q${y}Q${x}`,\n    [UA.QUERY_ALL_RESPONSE.BYTE]: (msg: Message) => `U${y}Q${(msg.names as string[]).length > 0 ? y + (msg.names as string[]).join(y) : '' }${x}`,\n    [UA.PRESENCE_JOIN.BYTE]: (msg: Message) => `U${y}PNJ${y}${msg.name}${x}`,\n    [UA.PRESENCE_JOIN_ALL.BYTE]: (msg: Message) => `U${y}PNJ${y}${msg.name}${x}`,\n    [UA.PRESENCE_LEAVE.BYTE]: (msg: Message) => `U${y}PNL${y}${msg.name}${x}`,\n    [UA.PRESENCE_LEAVE_ALL.BYTE]: (msg: Message) => `U${y}PNL${y}${msg.name}${x}`,\n\n    [UA.INVALID_PRESENCE_USERS.BYTE]: (msg: Message) => `U${y}E${y}INVALID_PRESENCE_USERS${y}${msg.data}${x}`,\n\n    [UA.MESSAGE_DENIED.BYTE]: messageDenied,\n    [UA.MESSAGE_PERMISSION_ERROR.BYTE]: messagePermissionError,\n    [UA.NOT_SUBSCRIBED.BYTE]: notSubscribed,\n    [UA.MULTIPLE_SUBSCRIPTIONS.BYTE]: multipleSubscriptions,\n  },\n}\n\n/**\n * Creates a deepstream message string, based on the\n * provided parameters\n */\nexport const getMessage = (message: Message, isAck: boolean = false): string => {\n  if (!BUILDERS[message.topic] || !BUILDERS[message.topic][message.action]) {\n    console.trace('missing builder for', message, isAck)\n    return ''\n  }\n\n  const builder = BUILDERS[message.topic][message.action]\n\n  if (\n      !message.parsedData && !message.data &&\n      (\n        (message.topic === TOPIC.RPC.BYTE && (message.action === PA.RESPONSE.BYTE || message.action === PA.REQUEST.BYTE)) ||\n        (message.topic === TOPIC.RECORD.BYTE && (message.action === RA.PATCH.BYTE || message.action === RA.ERASE.BYTE))\n      )\n    ) {\n      message.data = 'U'\n    } else if (message.parsedData) {\n      if (ABP[message.topic][message.action] === PAYLOAD_ENCODING.DEEPSTREAM) {\n        message.data = typed(message.parsedData)\n      } else {\n        message.data = JSON.stringify(message.parsedData)\n      }\n    } else if (message.data && ABP[message.topic][message.action] === PAYLOAD_ENCODING.DEEPSTREAM) {\n      message.data = typed(JSON.parse(message.data.toString()))\n    }\n\n    return builder(message, isAck)\n}\n\n/**\n * Converts a serializable value into its string-representation and adds\n * a flag that provides instructions on how to deserialize it.\n *\n * Please see messageParser.convertTyped for the counterpart of this method\n */\nexport const typed = function (value: any): string {\n  const type = typeof value\n\n  if (type === 'string') {\n    return TYPES.STRING + value\n  }\n\n  if (value === null) {\n    return TYPES.NULL\n  }\n\n  if (type === 'object') {\n    return TYPES.OBJECT + JSON.stringify(value)\n  }\n\n  if (type === 'number') {\n    return TYPES.NUMBER + value.toString()\n  }\n\n  if (value === true) {\n    return TYPES.TRUE\n  }\n\n  if (value === false) {\n    return TYPES.FALSE\n  }\n\n  if (value === undefined) {\n    return TYPES.UNDEFINED\n  }\n\n  throw new Error(`Can't serialize type ${value}`)\n}\n"
  },
  {
    "path": "src/connection-endpoint/websocket/text/text-protocol/message-parser.ts",
    "content": "import {\n  ACTIONS_BYTE_TO_PAYLOAD as ABP,\n  ACTIONS_TEXT_TO_BYTE,\n  AUTH_ACTIONS as AA,\n  CONNECTION_ACTIONS as CA,\n  DEEPSTREAM_TYPES as TYPES,\n  EVENT_ACTIONS as EA,\n  MESSAGE_PART_SEPERATOR,\n  MESSAGE_SEPERATOR,\n  PRESENCE_ACTIONS as UA,\n  RECORD_ACTIONS as RA,\n  RPC_ACTIONS as PA,\n  TOPIC,\n  TOPIC_TEXT_TO_BYTE,\n  PRESENCE_ACTIONS,\n  PAYLOAD_ENCODING,\n} from './constants'\nimport { Message } from '../../../../constants'\nimport { getUid } from '../../../../utils/utils'\n\nexport const correlationIdToVersion = new Map<string, number>()\nexport const bulkNameToCorrelationId = new Map<string, string>()\n\n/**\n * This method tries to parse a value, and returns\n * an object containing the value or error.\n *\n * This is an optimization to avoid doing try/catch\n * inline since it incurs a massive performance hit\n * in most versions of node.\n */\nfunction parseJSON (text: string, reviver?: any): any {\n  try {\n    return {\n      value: JSON.parse(text, reviver),\n    }\n  } catch (err) {\n    return {\n      error: err,\n    }\n  }\n}\n\nconst writeConfig = JSON.stringify({ writeSuccess: true })\n\nexport const parse = (rawMessage: string) => {\n  const parsedMessages: any[] = []\n  const rawMessages = rawMessage.split(MESSAGE_SEPERATOR)\n\n  for (let i = 0; i < rawMessages.length; i++) {\n    if (rawMessages[i].length < 3) {\n      continue\n    }\n\n    const parts = rawMessages[i].split(MESSAGE_PART_SEPERATOR)\n\n    const topic = TOPIC_TEXT_TO_BYTE[parts[0]]\n    if (topic === undefined) {\n      console.log('unknown topic', rawMessages[i])\n      // invalid topic\n      continue\n    }\n\n    let index: number = 1\n\n    let name\n    let names\n    let data\n\n    let version\n    let path\n    let isWriteAck\n\n    let subscription\n    let correlationId\n\n    let isAck = false\n    let isErr = false\n\n    if (parts[index] === 'A') {\n      isAck = true\n      index++\n    }\n\n    if (parts[index] === 'E') {\n      isErr = true\n      index++\n    }\n\n    const A = ACTIONS_TEXT_TO_BYTE[topic]\n    const rawAction = parts[index++]\n    let action = A[rawAction]\n\n    if (action === undefined) {\n      if (\n        (isErr && topic === TOPIC.RPC.BYTE) ||\n        (topic === TOPIC.CONNECTION.BYTE && isAck) ||\n        (topic === TOPIC.AUTH.BYTE && (isErr || isAck)) ||\n        (isErr && topic === TOPIC.RECORD.BYTE)\n      ) {\n        // ignore\n      } else {\n        console.log('unknown action', parts[index - 1], rawMessages[i])\n        continue\n      }\n    }\n    if (topic === TOPIC.RECORD.BYTE) {\n    /************************\n    ***  RECORD\n    *************************/\n      name = parts[index++]\n      names = [name]\n      if (isErr) {\n        isErr = false\n        if (rawAction === 'VERSION_EXISTS') {\n          action = RA.VERSION_EXISTS.BYTE\n          version = (parts[index++] as unknown as number) * 1\n          data = parts[index++]\n          isWriteAck = parts.length - index > 1\n        } else if (rawAction === 'CACHE_RETRIEVAL_TIMEOUT') {\n          action = RA.CACHE_RETRIEVAL_TIMEOUT.BYTE\n        } else if (rawAction === 'STORAGE_RETRIEVAL_TIMEOUT') {\n          action = RA.STORAGE_RETRIEVAL_TIMEOUT.BYTE\n        }\n      } else if (\n        action === RA.CREATEANDUPDATE.BYTE ||\n        action === RA.UPDATE.BYTE ||\n        action === RA.PATCH.BYTE\n      ) {\n        isWriteAck = (parts[parts.length - 1] === writeConfig)\n        version = (parts[index++] as unknown as number) * 1\n\n        if (action === RA.CREATEANDUPDATE.BYTE && parts.length === 7) {\n          action = RA.CREATEANDPATCH.BYTE\n        }\n\n        if (action === RA.CREATEANDPATCH.BYTE || action === RA.PATCH.BYTE) {\n          path = parts[index++]\n        }\n\n        if (parts.length - index === 2) {\n          data = parts[parts.length - 2]\n        } else {\n          data = parts[index++]\n        }\n      } else if (\n        action === RA.LISTEN_ACCEPT.BYTE ||\n        action === RA.LISTEN_REJECT.BYTE ||\n        action === RA.SUBSCRIPTION_FOR_PATTERN_FOUND.BYTE ||\n        action === RA.SUBSCRIPTION_FOR_PATTERN_REMOVED.BYTE\n      ) {\n        subscription = parts[index++]\n      } else if (action === RA.SUBSCRIPTION_HAS_PROVIDER.BYTE) {\n        if (parts[index++] === 'F') {\n          action = RA.SUBSCRIPTION_HAS_NO_PROVIDER.BYTE\n        }\n      }\n    } else if (topic === TOPIC.EVENT.BYTE) {\n    /************************\n    ***  EVENT\n    *************************/\n      name = parts[index++]\n      names = [name]\n      if (\n        action === EA.LISTEN_ACCEPT.BYTE ||\n        action === EA.LISTEN_REJECT.BYTE ||\n        action === EA.SUBSCRIPTION_FOR_PATTERN_FOUND.BYTE ||\n        action === EA.SUBSCRIPTION_FOR_PATTERN_REMOVED.BYTE\n      ) {\n        subscription = parts[index++]\n      } else if (action === EA.EMIT.BYTE) {\n        data = parts[index++]\n      }\n    } else if (topic === TOPIC.RPC.BYTE) {\n    /************************\n    ***  RPC\n    *************************/\n      name = parts[index++]\n      names = [name]\n      if (isAck && action === PA.REQUEST.BYTE) {\n        isAck = false\n        action = PA.ACCEPT.BYTE\n      }\n      if (isErr) {\n        isErr = false\n        action = PA.REQUEST_ERROR.BYTE\n        data = JSON.stringify(rawAction)\n      }\n      if (action !== PA.PROVIDE.BYTE && action !== PA.UNPROVIDE.BYTE) {\n        correlationId = parts[index++]\n      }\n      if (action === PA.RESPONSE.BYTE || action === PA.REQUEST.BYTE) {\n        data = parts[index++]\n      }\n    } else if (topic === TOPIC.PRESENCE.BYTE) {\n    /************************\n    ***  Presence\n    *************************/\n      if (action === UA.QUERY.BYTE) {\n        if (parts.length === 3) {\n          action = UA.QUERY_ALL.BYTE\n        } else {\n          correlationId = parts[index++]\n          names = JSON.parse(parts[index++])\n        }\n      } else if (action === UA.SUBSCRIBE.BYTE || action === UA.UNSUBSCRIBE.BYTE) {\n        if (parts.length === 4 && !isAck) {\n          correlationId = parts[index++]\n        }\n        data = parts[index++]\n\n        if (action === UA.SUBSCRIBE.BYTE && (data === 'S' || data === 'U')) {\n          action = PRESENCE_ACTIONS.SUBSCRIBE_ALL.BYTE\n        } else if (action === UA.UNSUBSCRIBE.BYTE && data === 'US') {\n          action = PRESENCE_ACTIONS.UNSUBSCRIBE_ALL.BYTE\n        } else {\n          names = JSON.parse(data)\n        }\n      }\n    } else if (topic === TOPIC.CONNECTION.BYTE) {\n      /************************\n      ***  Connection\n      *************************/\n      if (action === CA.PONG.BYTE) {\n        continue\n      }\n      if (isAck) {\n        action = CA.ACCEPT.BYTE\n        isAck = false\n      } else if (action === CA.REDIRECT.BYTE || action === CA.REJECTION.BYTE) {\n        data = parts[index++]\n      }\n    } else if (topic === TOPIC.AUTH.BYTE) {\n      /************************\n      ***  Authentication\n      *************************/\n      if (isAck) {\n        action = AA.AUTH_SUCCESSFUL.BYTE\n      } else if (isErr) {\n        if (rawAction === 'INVALID_AUTH_DATA') {\n          isErr = false\n          action = AA.AUTH_UNSUCCESSFUL.BYTE\n        } else if (rawAction === 'TOO_MANY_AUTH_ATTEMPTS') {\n          isErr = false\n          action = AA.TOO_MANY_AUTH_ATTEMPTS.BYTE\n        }\n      }\n      if (action === AA.AUTH_SUCCESSFUL.BYTE) {\n        isAck = false\n        data = rawAction\n      } else if (action === AA.REQUEST.BYTE || action === AA.AUTH_UNSUCCESSFUL.BYTE) {\n        data = parts[index++]\n      }\n    }\n\n    if (names && !correlationId && version !== undefined) {\n      correlationId = getUid()\n      correlationIdToVersion.set(correlationId, version)\n    } else if (names && !correlationId) {\n      correlationId = getUid()\n      bulkNameToCorrelationId.set(correlationId, names[0])\n    }\n\n    const message = JSON.parse(JSON.stringify({\n      isAck,\n      isErr,\n      topic,\n      action,\n      name,\n      names,\n      data,\n\n      // rpc / presence query\n      correlationId,\n\n      // subscription by listening\n      subscription,\n\n      // record\n      path,\n      version,\n      // parsedData: null,\n      isWriteAck,\n    }))\n\n    parseData(message)\n\n    parsedMessages.push(message)\n  }\n  return parsedMessages\n}\n\nexport const parseData = (message: Message) => {\n  if (message.parsedData || !message.data) {\n    return true\n  }\n\n  if (\n      message.topic === TOPIC.PRESENCE.BYTE &&\n      (\n        message.action === PRESENCE_ACTIONS.SUBSCRIBE_ALL.BYTE ||\n        message.action === PRESENCE_ACTIONS.UNSUBSCRIBE_ALL.BYTE\n      )\n    ) {\n      // @ts-ignore\n      message.parsedData = message.data\n      message.data = JSON.stringify(message.data)\n    }\n\n  if (ABP[message.topic][message.action] === PAYLOAD_ENCODING.DEEPSTREAM) {\n    const parsedData = convertTyped(message.data as string)\n    if (parsedData instanceof Error) {\n      return parsedData\n    }\n    message.parsedData = parsedData\n    message.data = JSON.stringify(message.parsedData)\n    return true\n  } else {\n    const res = parseJSON((message.data as string)!)\n    if (res.error) {\n      return res.error\n    }\n    message.parsedData = res.value\n    return true\n  }\n}\n\n/**\n * Deserializes values created by MessageBuilder.typed to\n * their original format\n */\nexport const convertTyped = (value: string): any => {\n  if (value === 'null') {\n    return null\n  }\n\n  if (value === undefined) {\n    return undefined\n  }\n\n  const type = value.charAt(0)\n\n  if (type === TYPES.STRING) {\n    return value.substr(1)\n  }\n\n  if (type === TYPES.OBJECT) {\n    const result = parseJSON(value.substr(1))\n    if (result.value) {\n      return result.value\n    }\n    return result.error\n  }\n\n  if (type === TYPES.NUMBER) {\n    return parseFloat(value.substr(1))\n  }\n\n  if (type === TYPES.NULL || type === 'null') {\n    return null\n  }\n\n  if (type === TYPES.TRUE) {\n    return true\n  }\n\n  if (type === TYPES.FALSE) {\n    return false\n  }\n\n  if (type === TYPES.UNDEFINED) {\n    return undefined\n  }\n\n  return new Error('Unknown type')\n}\n\nexport const isError = (message: any) => false\n"
  },
  {
    "path": "src/connection-endpoint/websocket/text/text-protocol/utils.ts",
    "content": "export const isWriteAck = (action: any) => false\n\nexport const WRITE_ACK_TO_ACTION = {}\n"
  },
  {
    "path": "src/constants.ts",
    "content": "export * from '@deepstream/protobuf/dist/types/all'\nexport * from '@deepstream/protobuf/dist/types/messages'\n\nexport enum STATES {\n    CONFIG_LOADED = 'CONFIG_LOADED',\n    LOGGER_INIT = 'LOGGER_INIT',\n    SERVICE_INIT = 'SERVICE_INIT',\n    HANDLER_INIT = 'HANDLER_INIT',\n    CONNECTION_ENDPOINT_INIT = 'CONNECTION_ENDPOINT_INIT',\n    PLUGIN_INIT = 'PLUGIN_INIT',\n    RUNNING = 'RUNNING',\n\n    PLUGIN_SHUTDOWN = 'PLUGIN_SHUTDOWN',\n    CONNECTION_ENDPOINT_SHUTDOWN = 'CONNECTION_ENDPOINT_SHUTDOWN',\n    HANDLER_SHUTDOWN = 'HANDLER_SHUTDOWN',\n    SERVICE_SHUTDOWN = 'SERVICE_SHUTDOWN',\n    LOGGER_SHUTDOWN = 'LOGGER_SHUTDOWN',\n    STOPPED = 'STOPPED'\n}\n"
  },
  {
    "path": "src/deepstream.io.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport { Deepstream } from './deepstream.io'\nimport { PromiseDelay } from './utils/utils';\n\ndescribe('deepstream.io', () => {\n\n  describe('the main server class', () => {\n    it('sets a supported option', () => {\n      const server = new Deepstream()\n      expect(() => {\n        server.set('serverName', 'my lovely horse')\n      }).not.to.throw()\n    })\n\n    it.skip('sets an unsupported option', async () => {\n      const server = new Deepstream()\n      await PromiseDelay(50)\n      expect(() => {\n        server.set('gibberish', 4444)\n      }).to.throw()\n    })\n  })\n\n})\n"
  },
  {
    "path": "src/deepstream.io.ts",
    "content": "require('source-map-support').install()\n\nimport { EventEmitter } from 'events'\n\nimport * as pkg from '../package.json'\nimport { merge } from './utils/utils'\nimport { STATES, TOPIC } from './constants'\n\nimport MessageProcessor from './utils/message-processor'\nimport MessageDistributor from './utils/message-distributor'\n\nimport EventHandler from './handlers/event/event-handler'\nimport RpcHandler from './handlers/rpc/rpc-handler'\nimport PresenceHandler from './handlers/presence/presence-handler'\nimport MonitoringHandler from './handlers/monitoring/monitoring'\n\nimport { get as getDefaultOptions } from './default-options'\nimport * as configInitializer from './config/config-initialiser'\nimport * as jsYamlLoader from './config/js-yaml-loader'\n\nimport { DependencyInitialiser } from './utils/dependency-initialiser'\nimport { DeepstreamConfig, DeepstreamServices, DeepstreamPlugin, PartialDeepstreamConfig, EVENT, SocketWrapper, ConnectionListener } from '@deepstream/types'\nimport RecordHandler from './handlers/record/record-handler'\nimport { getValue, setValue } from './utils/json-path'\nimport { CombineAuthentication } from './services/authentication/combine/combine-authentication'\n\n/**\n * Sets the name of the process\n */\nprocess.title = 'deepstream server'\n\nexport class Deepstream extends EventEmitter {\n  public constants: any\n\n  private configFile!: string\n\n  private config!: DeepstreamConfig\n  private services!: DeepstreamServices\n\n  private messageProcessor: any\n  private messageDistributor: any\n\n  private eventHandler!: EventHandler\n  private rpcHandler!: RpcHandler\n  private recordHandler!: RecordHandler\n  private presenceHandler!: PresenceHandler\n  private monitoringHandler!: MonitoringHandler\n\n  private connectionListeners = new Set<ConnectionListener>()\n\n  private stateMachine: any\n  private currentState: STATES\n\n  private overrideSettings: Array<{key: string, value: any}> = []\n  private startWhenLoaded: boolean = false\n\n/**\n * Deepstream is a realtime data server that supports data-sync,\n * publish-subscribe, request-response, listening, permissions\n * and a host of other features!\n */\n  constructor (config: PartialDeepstreamConfig | string | null = null) {\n    super()\n\n    this.stateMachine = {\n      init: STATES.STOPPED,\n      transitions: [\n      { name: 'loading-config', from: STATES.STOPPED, to: STATES.CONFIG_LOADED, handler: this.configLoaded },\n      { name: 'start', from: STATES.CONFIG_LOADED, to: STATES.LOGGER_INIT, handler: this.loggerInit },\n      { name: 'logger-started', from: STATES.LOGGER_INIT, to: STATES.SERVICE_INIT, handler: this.serviceInit },\n      { name: 'services-started', from: STATES.SERVICE_INIT, to: STATES.HANDLER_INIT, handler: this.handlerInit },\n      { name: 'handlers-started', from: STATES.HANDLER_INIT, to: STATES.PLUGIN_INIT, handler: this.pluginsInit },\n      { name: 'plugins-started', from: STATES.PLUGIN_INIT, to: STATES.CONNECTION_ENDPOINT_INIT, handler: this.connectionEndpointInit },\n      { name: 'connection-endpoints-started', from: STATES.CONNECTION_ENDPOINT_INIT, to: STATES.RUNNING, handler: this.run },\n\n      { name: 'stop', from: STATES.LOGGER_INIT, to: STATES.LOGGER_SHUTDOWN, handler: this.loggerShutdown },\n      { name: 'stop', from: STATES.SERVICE_INIT, to: STATES.SERVICE_SHUTDOWN, handler: this.serviceShutdown },\n      { name: 'stop', from: STATES.CONNECTION_ENDPOINT_INIT, to: STATES.CONNECTION_ENDPOINT_SHUTDOWN, handler: this.connectionEndpointShutdown },\n      { name: 'stop', from: STATES.PLUGIN_INIT, to: STATES.PLUGIN_SHUTDOWN, handler: this.pluginsShutdown },\n\n      { name: 'stop', from: STATES.RUNNING, to: STATES.CONNECTION_ENDPOINT_SHUTDOWN, handler: this.connectionEndpointShutdown },\n      { name: 'connection-endpoints-closed', from: STATES.CONNECTION_ENDPOINT_SHUTDOWN, to: STATES.PLUGIN_SHUTDOWN, handler: this.pluginsShutdown },\n      { name: 'plugins-closed', from: STATES.PLUGIN_SHUTDOWN, to: STATES.HANDLER_SHUTDOWN, handler: this.handlerShutdown },\n      { name: 'handlers-closed', from: STATES.HANDLER_SHUTDOWN, to: STATES.SERVICE_SHUTDOWN, handler: this.serviceShutdown },\n      { name: 'services-closed', from: STATES.SERVICE_SHUTDOWN, to: STATES.LOGGER_SHUTDOWN, handler: this.loggerShutdown },\n      { name: 'logger-closed', from: STATES.LOGGER_SHUTDOWN, to: STATES.STOPPED, handler: this.stopped },\n      ]\n    }\n    this.currentState = this.stateMachine.init\n\n    this.loadConfig(config)\n    this.messageProcessor = null\n    this.messageDistributor = null\n  }\n\n/**\n * Set a deepstream option. For a list of all available options\n * please see default-options.\n */\n  public set (key: string, value: any): any {\n    if (this.currentState === STATES.STOPPED) {\n      this.overrideSettings.push({ key, value })\n      return\n    }\n\n    if (key === 'storageExclusion') {\n        throw new Error('storageExclusion has been replace with record.storageExclusionPrefixes instead, which is an array of prefixes')\n    }\n\n    if (key === 'auth') {\n      throw new Error('auth has been replaced with authentication')\n    }\n\n    if (key === 'authentication') {\n      this.services.authentication = new CombineAuthentication(value instanceof Array ? value : [value])\n      return\n    }\n\n    if ((this.services as any)[key] !== undefined) {\n      (this.services as any)[key] = value\n    } else if (getValue(this.config, key) !== undefined) {\n      setValue(this.config, key, value)\n    } else {\n      throw new Error(`Unknown option or service \"${key}\"`)\n    }\n    return this\n  }\n\n/**\n * Returns true if the deepstream server is running, otherwise false\n */\n  public isRunning (): boolean {\n    return this.currentState === STATES.RUNNING\n  }\n\n/**\n * Starts up deepstream. The startup process has three steps:\n *\n * - First of all initialize the logger and wait for it (ready event)\n * - Then initialize all other dependencies (cache connector, message connector, storage connector)\n * - Instantiate the messaging pipeline and record-, rpc- and event-handler\n * - Start WS server\n */\n  public start (): void {\n    if (this.currentState !== STATES.CONFIG_LOADED) {\n      this.startWhenLoaded = true\n      return\n    }\n    this.transition('start')\n  }\n\n/**\n * Stops the server and closes all connections. Will emit a 'stopped' event once done\n */\n  public stop (): void {\n    if (this.currentState === STATES.STOPPED) {\n      throw new Error('The server is already stopped.')\n    }\n\n    if ([STATES.CONNECTION_ENDPOINT_SHUTDOWN, STATES.SERVICE_SHUTDOWN, STATES.PLUGIN_SHUTDOWN, STATES.LOGGER_SHUTDOWN].indexOf(this.currentState) !== -1) {\n      this.services.logger.info(EVENT.INFO, `Server is currently shutting down, currently in state ${STATES[this.currentState]}`)\n      return\n    }\n\n    this.transition('stop')\n  }\n\n  public getServices (): Readonly<DeepstreamServices> {\n    return this.services\n  }\n\n  public getConfig (): Readonly<DeepstreamConfig> {\n    return this.config\n  }\n\n/* ======================================================================= *\n * ========================== State Transitions ========================== *\n * ======================================================================= */\n\n/**\n * Try to perform a state change\n */\n  private transition (transitionName: string): void {\n    let transition\n    for (let i = 0; i < this.stateMachine.transitions.length; i++) {\n      transition = this.stateMachine.transitions[i]\n      if (transitionName === transition.name && this.currentState === transition.from) {\n        // found transition\n        this.onTransition(transition)\n        this.currentState = transition.to\n        transition.handler.call(this)\n        this.emit(EVENT.DEEPSTREAM_STATE_CHANGED, this.currentState)\n        return\n      }\n    }\n    const details = JSON.stringify({ transition: transitionName, state: this.currentState })\n    throw new Error(`Invalid state transition: ${details}`)\n  }\n\n/**\n * Log state transitions for debugging.\n */\n  private onTransition (transition: { from: STATES, to: STATES, name: string }): void {\n    const logger = this.services.logger\n    if (logger && STATES[transition.to] !== STATES.CONFIG_LOADED) {\n      logger.debug(\n        EVENT.INFO,\n        `State transition (${transition.name}): ${STATES[transition.from]} -> ${STATES[transition.to]}`\n      )\n    }\n  }\n\n  private configLoaded (): void {\n    if (this.startWhenLoaded) {\n      this.overrideSettings.forEach((setting) => this.set(setting.key, setting.value))\n      this.start()\n    }\n  }\n\n/**\n * First stage in the Deepstream initialization sequence. Initialises the logger.\n */\n  private async loggerInit (): Promise<void> {\n    const logger = this.services.logger\n    const loggerInitialiser = new DependencyInitialiser(this.config, this.services, logger, 'logger')\n    await loggerInitialiser.whenReady()\n\n    const infoLogger = (message: string) => this.services.logger.info(EVENT.INFO, message)\n    infoLogger(`server name: ${this.config.serverName}`)\n    infoLogger(`deepstream version: ${pkg.version}`)\n\n    // otherwise (no configFile) deepstream was invoked by API\n    if (this.configFile != null) {\n      infoLogger(`configuration file loaded from ${this.configFile}`)\n    }\n\n    // @ts-ignore\n    if (global.deepstreamLibDir) {\n      // @ts-ignore\n      infoLogger(`library directory set to: ${global.deepstreamLibDir}`)\n    }\n\n    this.transition('logger-started')\n  }\n\n  /**\n   * Invoked once the logger is initialised. Initialises all deepstream services.\n  */\n  private async serviceInit () {\n    const readyPromises = Object.keys(this.services).reduce((promises, serviceName) => {\n      if (['connectionEndpoints', 'plugins', 'notifyFatalException', 'logger'].includes(serviceName)) {\n        return promises\n      }\n      const service = (this.services as any)[serviceName] as DeepstreamPlugin\n      const initialiser = new DependencyInitialiser(this.config, this.services, service, serviceName)\n      promises.push(initialiser.whenReady())\n      return promises\n    }, [] as Array<Promise<void>>)\n\n    await Promise.all(readyPromises)\n\n    this.messageProcessor = new MessageProcessor(this.config, this.services)\n    this.messageDistributor = new MessageDistributor(this.config, this.services)\n    this.services.messageDistributor = this.messageDistributor\n\n    this.transition('services-started')\n  }\n\n/**\n * Invoked once all plugins are initialised. Instantiates the messaging pipeline and\n * the various handlers.\n */\n  private async handlerInit () {\n    if (this.config.enabledFeatures.event) {\n      this.eventHandler = new EventHandler(this.config, this.services)\n      this.messageDistributor.registerForTopic(\n        TOPIC.EVENT,\n        this.eventHandler.handle.bind(this.eventHandler)\n      )\n    }\n\n    if (this.config.enabledFeatures.rpc) {\n      this.rpcHandler = new RpcHandler(this.config, this.services)\n      this.messageDistributor.registerForTopic(\n        TOPIC.RPC,\n        this.rpcHandler.handle.bind(this.rpcHandler)\n      )\n    }\n\n    if (this.config.enabledFeatures.record) {\n      this.recordHandler = new RecordHandler(this.config, this.services)\n      this.messageDistributor.registerForTopic(\n        TOPIC.RECORD,\n        this.recordHandler.handle.bind(this.recordHandler)\n      )\n    }\n\n    if (this.config.enabledFeatures.presence) {\n      this.presenceHandler = new PresenceHandler(this.config, this.services)\n      this.messageDistributor.registerForTopic(\n        TOPIC.PRESENCE,\n        this.presenceHandler.handle.bind(this.presenceHandler)\n      )\n      this.connectionListeners.add(this.presenceHandler as ConnectionListener)\n    }\n\n    if (this.config.enabledFeatures.monitoring) {\n      this.monitoringHandler = new MonitoringHandler(this.config, this.services)\n      this.messageDistributor.registerForTopic(\n        TOPIC.MONITORING,\n        this.monitoringHandler.handle.bind(this.monitoringHandler)\n      )\n    }\n\n    this.messageProcessor.onAuthenticatedMessage =\n      this.messageDistributor.distribute.bind(this.messageDistributor)\n\n    if (this.services.permission.setRecordHandler) {\n      this.services.permission.setRecordHandler(this.recordHandler)\n    }\n\n    this.transition('handlers-started')\n  }\n\n  private async pluginsInit () {\n    const readyPromises = Object.keys(this.services.plugins).reduce((promises, pluginName) => {\n      const plugin = this.services.plugins[pluginName]\n      if (isConnectionListener(plugin)) {\n        this.connectionListeners.add(plugin)\n      }\n      const initialiser = new DependencyInitialiser(this.config, this.services, plugin, pluginName)\n      promises.push(initialiser.whenReady())\n      return promises\n    }, [] as Array<Promise<void>>)\n\n    await Promise.all(readyPromises)\n\n    this.transition('plugins-started')\n  }\n\n/**\n * Invoked once all dependencies and services are initialised.\n * The startup sequence will be complete once the connection endpoint is started and listening.\n */\n  private async connectionEndpointInit (): Promise<void> {\n    const endpoints = this.services.connectionEndpoints\n    const readyPromises: Array<Promise<void>> = []\n\n    for (let i = 0; i < endpoints.length; i++) {\n      const connectionEndpoint = endpoints[i]\n      const dependencyInitialiser = new DependencyInitialiser(\n        this.config,\n        this.services,\n        connectionEndpoint,\n        'connectionEndpoint'\n      )\n\n      connectionEndpoint.onMessages = this.messageProcessor.process.bind(this.messageProcessor)\n      if (connectionEndpoint.setConnectionListener) {\n        connectionEndpoint.setConnectionListener({\n          onClientConnected: this.onClientConnected.bind(this),\n          onClientDisconnected: this.onClientDisconnected.bind(this)\n        })\n      }\n      readyPromises.push(dependencyInitialiser.whenReady())\n    }\n\n    await Promise.all(readyPromises)\n    this.transition('connection-endpoints-started')\n  }\n\n/**\n * Initialization complete - Deepstream is up and running.\n */\n  private run (): void {\n    this.services.logger.info(EVENT.INFO, 'Deepstream started')\n    this.emit('started')\n  }\n\n  /**\n * Close any (perhaps partially initialised) plugins.\n */\nprivate async pluginsShutdown () {\n  const shutdownPromises = Object.keys(this.services.plugins).reduce((promises, pluginName) => {\n    const plugin = this.services.plugins[pluginName]\n    if (plugin.close) {\n      promises.push(plugin.close())\n    }\n    return promises\n  }, [] as Array<Promise<void>> )\n  await Promise.all(shutdownPromises)\n  this.transition('plugins-closed')\n}\n\n/**\n * Begin deepstream shutdown.\n * Closes the (perhaps partially initialised) connectionEndpoints.\n */\n  private async connectionEndpointShutdown (): Promise<void> {\n    const closeCallbacks = this.services.connectionEndpoints.map((endpoint) => endpoint.close())\n    await Promise.all(closeCallbacks)\n    this.transition('connection-endpoints-closed')\n  }\n\n  private async handlerShutdown () {\n    if (this.config.enabledFeatures.event) {\n      await this.eventHandler.close()\n    }\n    if (this.config.enabledFeatures.rpc) {\n      await this.rpcHandler.close()\n    }\n    if (this.config.enabledFeatures.record) {\n      await this.recordHandler.close()\n    }\n    if (this.config.enabledFeatures.presence) {\n      await this.presenceHandler.close()\n    }\n    if (this.config.enabledFeatures.monitoring) {\n      await this.monitoringHandler.close()\n    }\n    this.transition('handlers-closed')\n  }\n\n  /**\n   * Shutdown the services.\n   */\n  private async serviceShutdown (): Promise<void> {\n    const shutdownPromises = Object.keys(this.services).reduce((promises, serviceName) => {\n      const service = (this.services as any)[serviceName]\n      if (service.close) {\n        promises.push(service.close())\n      }\n      return promises\n    }, [] as Array<Promise<void>> )\n    await Promise.all(shutdownPromises)\n    this.transition('services-closed')\n  }\n\n/**\n * Close the (perhaps partially initialised) logger.\n */\n  private async loggerShutdown () {\n    const logger = this.services.logger as any\n    await logger.close()\n    this.transition('logger-closed')\n  }\n\n/**\n * Final stop state.\n * Deepstream can now be started again.\n */\n  private stopped (): void {\n    this.emit('stopped')\n  }\n\n/**\n * Synchronously loads a configuration file\n * Initialization of plugins and logger will be triggered by the\n * configInitialiser, but it should not block. Instead the ready events of\n * those plugins are handled through the DependencyInitialiser in this instance.\n */\n  private async loadConfig (config: PartialDeepstreamConfig | string | null): Promise<void> {\n    let result\n    if (config === null || typeof config === 'string') {\n      result = await jsYamlLoader.loadConfig(this, config)\n      this.configFile = result.file\n    } else {\n      configInitializer.mergeConnectionOptions(config)\n      const rawConfig = merge(getDefaultOptions(), config) as DeepstreamConfig\n      result = configInitializer.initialize(this, rawConfig)\n    }\n    this.config = result.config\n    this.services = result.services\n    this.transition('loading-config')\n  }\n\n  private onClientConnected (socketWrapper: SocketWrapper): void {\n    this.connectionListeners.forEach((connectionListener) => connectionListener.onClientConnected(socketWrapper))\n  }\n\n  private onClientDisconnected (socketWrapper: SocketWrapper): void {\n    this.connectionListeners.forEach((connectionListener) => connectionListener.onClientDisconnected(socketWrapper))\n  }\n}\n\nfunction isConnectionListener (object: any): object is ConnectionListener {\n  return 'onClientConnected' in object && 'onClientDisconnected' in object\n}\n\nexport default Deepstream\n"
  },
  {
    "path": "src/default-options.ts",
    "content": "import { getUid } from './utils/utils'\nimport { DeepstreamConfig, LOG_LEVEL } from '@deepstream/types'\n\nconst WebSocketDefaultOptions = {\n  urlPath: '/deepstream',\n  heartbeatInterval: 30000,\n  outgoingBufferTimeout: 0,\n  maxBufferByteSize: 100000,\n  headers: [],\n\n  /*\n   * Security\n   */\n  unauthenticatedClientTimeout: 180000,\n  maxAuthAttempts: 3,\n  logInvalidAuthData: false,\n  maxMessageSize: 1048576\n}\n\nexport function get (): DeepstreamConfig {\n  return {\n    /*\n     * General\n     */\n    libDir: null,\n    serverName: getUid(),\n    showLogo: false,\n    logLevel: LOG_LEVEL.INFO,\n    dependencyInitializationTimeout: 2000,\n    // defaults to false as the event is captured via commander when run via binary or standalone\n    exitOnFatalError: false,\n\n    /*\n     * Connection Endpoints\n     */\n    connectionEndpoints: [\n    {\n        type: 'ws-binary',\n        options:  { ...WebSocketDefaultOptions, urlPath: '/deepstream' }\n    },\n    {\n        type: 'ws-text',\n        options: { ...WebSocketDefaultOptions, urlPath: '/deepstream-v3' }\n    },\n    {\n        type: 'ws-json',\n        options: { ...WebSocketDefaultOptions, urlPath: '/deepstream-json' }\n      },\n      {\n        type: 'http',\n        options: {\n          allowAuthData: true,\n          enableAuthEndpoint: true,\n          authPath: '/api/auth',\n          postPath: '/api',\n          getPath: '/api'\n        }\n      },\n      {\n        type: 'mqtt',\n        options: {\n          port: 1883,\n          host: '0.0.0.0',\n          idleTimeout: 180000,\n\n          /*\n           * Security\n           */\n          unauthenticatedClientTimeout: 180000,\n        }\n      }\n    ],\n\n    logger: {\n      type: 'default',\n      options: {}\n    },\n\n    httpServer: {\n      type: 'default',\n      options: {\n        host: '0.0.0.0',\n        port: 6020,\n        healthCheckPath: '/health-check',\n        allowAllOrigins: true,\n        origins: [],\n        headers: [],\n        maxMessageSize: 1048576\n      }\n    },\n\n    subscriptions: {\n      type: 'default',\n      options: {\n        subscriptionsSanityTimer: 10000\n      }\n    },\n\n    auth: [{\n      type: 'none',\n      options: {}\n    }],\n\n    permission: {\n      type: 'none',\n      options: {\n        maxRuleIterations: 3,\n        cacheEvacuationInterval: 60000\n      }\n    },\n\n    cache: {\n      type: 'default',\n      options: {}\n    },\n\n    storage: {\n      type: 'default',\n      options: {}\n    },\n\n    monitoring: {\n      type: 'none',\n      options: {}\n    },\n\n    telemetry: {\n      type: 'deepstreamIO',\n      options: {\n        enabled: false,\n      }\n    },\n\n    locks: {\n      type: 'default',\n      options: {\n        holdTimeout: 1000,\n        requestTimeout: 1000\n      }\n    },\n\n    clusterNode: {\n      type: 'default',\n      options: {\n      }\n    },\n\n    clusterRegistry: {\n      type: 'default',\n      options: {\n        keepAliveInterval: 5000,\n        activeCheckInterval: 1000,\n        nodeInactiveTimeout: 6000\n      }\n    },\n\n    clusterStates: {\n      type: 'default',\n      options: {\n        reconciliationTimeout: 500\n      }\n    },\n\n    plugins: {\n    },\n\n    rpc: {\n      /**\n       * Don't send requestorName by default.\n       */\n      provideRequestorName: false,\n      /**\n       * Don't send requestorData by default.\n       */\n      provideRequestorData: false,\n\n      ackTimeout: 1000,\n      responseTimeout: 10000,\n    },\n\n    record: {\n      storageHotPathPrefixes: [],\n      storageExclusionPrefixes: [],\n      cacheRetrievalTimeout: 1000,\n      storageRetrievalTimeout: 2000,\n    },\n\n    listen: {\n      shuffleProviders: true,\n      responseTimeout: 500,\n      rematchInterval: 30000,\n      matchCooldown: 10000\n    },\n\n    enabledFeatures: {\n      record: true,\n      event: true,\n      rpc: true,\n      presence: true,\n      monitoring: false\n    },\n  }\n\n}\n"
  },
  {
    "path": "src/handlers/event/event-handler.spec.ts",
    "content": "import 'mocha'\n\nimport * as C from '../../constants'\nimport EventHandler from './event-handler'\n\nimport * as testHelper from '../../test/helper/test-helper'\nimport { getTestMocks } from '../../test/helper/test-mocks'\n\nconst options = testHelper.getDeepstreamOptions()\nconst config = options.config\nconst services = options.services\n\ndescribe('the eventHandler routes events correctly', () => {\n  let testMocks\n  let eventHandler\n  let socketWrapper\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    eventHandler = new EventHandler(\n      config, services, testMocks.subscriptionRegistry, testMocks.listenerRegistry\n    )\n    socketWrapper = testMocks.getSocketWrapper().socketWrapper\n  })\n\n  afterEach(() => {\n    testMocks.subscriptionRegistryMock.verify()\n    testMocks.listenerRegistryMock.verify()\n  })\n\n  it('subscribes to events', () => {\n    const subscriptionMessage = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.SUBSCRIBE,\n      names: ['someEvent']\n    }\n    testMocks.subscriptionRegistryMock\n      .expects('subscribeBulk')\n      .once()\n      .withExactArgs(subscriptionMessage, socketWrapper)\n\n    eventHandler.handle(socketWrapper, subscriptionMessage)\n  })\n\n  it('unsubscribes to events', () => {\n    const unSubscriptionMessage = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.UNSUBSCRIBE,\n      names: ['someEvent']\n    }\n    testMocks.subscriptionRegistryMock\n      .expects('unsubscribeBulk')\n      .once()\n      .withExactArgs(unSubscriptionMessage, socketWrapper)\n\n    eventHandler.handle(socketWrapper, unSubscriptionMessage)\n  })\n\n  it('triggers event without data', () => {\n    const eventMessage = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'someEvent'\n    }\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs('someEvent', eventMessage, false, socketWrapper)\n\n    eventHandler.handle(socketWrapper, eventMessage)\n  })\n\n  it('triggers event with data', () => {\n    const eventMessage = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'someEvent',\n      data: JSON.stringify({ data: 'payload' })\n    }\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs('someEvent', eventMessage, false, socketWrapper)\n\n    eventHandler.handle(socketWrapper, eventMessage)\n  })\n\n  it('registers a listener', () => {\n    const listenMessage = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.LISTEN,\n      name: 'event/.*'\n    }\n    testMocks.listenerRegistryMock\n      .expects('handle')\n      .once()\n      .withExactArgs(socketWrapper, listenMessage)\n\n    eventHandler.handle(socketWrapper, listenMessage)\n  })\n\n  it('removes listeners', () => {\n    const unlistenMessage = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.UNLISTEN,\n      name: 'event/.*'\n    }\n    testMocks.listenerRegistryMock\n      .expects('handle')\n      .once()\n      .withExactArgs(socketWrapper, unlistenMessage)\n\n    eventHandler.handle(socketWrapper, unlistenMessage)\n  })\n\n  it('processes listen accepts', () => {\n    const listenAcceptMessage = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.LISTEN_ACCEPT,\n      name: 'event/.*',\n      subscription: 'event/A'\n    }\n    testMocks.listenerRegistryMock\n      .expects('handle')\n      .once()\n      .withExactArgs(socketWrapper, listenAcceptMessage)\n\n    eventHandler.handle(socketWrapper, listenAcceptMessage)\n  })\n\n  it('processes listen rejects', () => {\n    const listenRejectMessage = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.LISTEN_REJECT,\n      name: 'event/.*',\n      subscription: 'event/A'\n    }\n    testMocks.listenerRegistryMock\n      .expects('handle')\n      .once()\n      .withExactArgs(socketWrapper, listenRejectMessage)\n\n    eventHandler.handle(socketWrapper, listenRejectMessage)\n  })\n})\n"
  },
  {
    "path": "src/handlers/event/event-handler.ts",
    "content": "import { EVENT_ACTION, TOPIC, EventMessage, ListenMessage, STATE_REGISTRY_TOPIC, BulkSubscriptionMessage } from '../../constants'\nimport { ListenerRegistry } from '../../listen/listener-registry'\nimport { DeepstreamConfig, DeepstreamServices, SocketWrapper, Handler, SubscriptionRegistry, EVENT } from '@deepstream/types'\n\nexport default class EventHandler implements Handler<EventMessage> {\n  private subscriptionRegistry: SubscriptionRegistry\n  private listenerRegistry: ListenerRegistry\n\n  /**\n   * Handles incoming and outgoing messages for the EVENT topic.\n   */\n\n  constructor (config: DeepstreamConfig, private services: DeepstreamServices, subscriptionRegistry?: SubscriptionRegistry, listenerRegistry?: ListenerRegistry) {\n    this.subscriptionRegistry =\n      subscriptionRegistry || services.subscriptions.getSubscriptionRegistry(TOPIC.EVENT, STATE_REGISTRY_TOPIC.EVENT_SUBSCRIPTIONS)\n    this.listenerRegistry =\n      listenerRegistry || new ListenerRegistry(TOPIC.EVENT, config, services, this.subscriptionRegistry, null)\n    this.subscriptionRegistry.setSubscriptionListener(this.listenerRegistry)\n  }\n\n  public async close () {\n    this.listenerRegistry.close()\n  }\n\n  /**\n   * The main distribution method. Routes messages to functions\n   * based on the provided action parameter of the message\n   */\n  public handle (socketWrapper: SocketWrapper | null, message: EventMessage) {\n\n    if (message.action === EVENT_ACTION.EMIT) {\n      this.triggerEvent(socketWrapper, message)\n      return\n    }\n\n    if (socketWrapper === null) {\n      this.services.logger.error(EVENT.ERROR, 'missing socket wrapper')\n      return\n    }\n\n    if (message.action === EVENT_ACTION.SUBSCRIBE) {\n      this.subscriptionRegistry.subscribeBulk(message as BulkSubscriptionMessage, socketWrapper)\n      return\n    }\n\n    if (message.action === EVENT_ACTION.UNSUBSCRIBE) {\n      this.subscriptionRegistry.unsubscribeBulk(message as BulkSubscriptionMessage, socketWrapper)\n      return\n    }\n\n    if (message.action === EVENT_ACTION.LISTEN ||\n      message.action === EVENT_ACTION.UNLISTEN ||\n      message.action === EVENT_ACTION.LISTEN_ACCEPT ||\n      message.action === EVENT_ACTION.LISTEN_REJECT) {\n      this.listenerRegistry.handle(socketWrapper, message as ListenMessage)\n      return\n    }\n\n    console.log('unknown action', message)\n  }\n\n  /**\n   * Notifies subscribers of events. This method is invoked for the EVENT action. It can\n   * be triggered by messages coming in from both clients and the message connector.\n   */\n  public triggerEvent (socket: SocketWrapper | null, message: EventMessage) {\n    this.services.logger.debug(EVENT_ACTION[EVENT_ACTION.EMIT], `event: ${message.name} with data: ${message.data || message.parsedData}`)\n    this.subscriptionRegistry.sendToSubscribers(message.name, message, false, socket)\n  }\n}\n"
  },
  {
    "path": "src/handlers/monitoring/monitoring.ts",
    "content": "import { DeepstreamConfig, DeepstreamServices, SubscriptionRegistry, SocketWrapper, Handler } from '@deepstream/types'\nimport { MonitoringMessage } from '../../constants'\n\nexport default class MonitoringHandler extends Handler<MonitoringMessage> {\n  // private subscriptionRegistry: SubscriptionRegistry\n\n  /**\n   * Handles incoming and outgoing messages for the EVENT topic.\n   */\n  constructor (config: DeepstreamConfig, services: DeepstreamServices, subscriptionRegistry?: SubscriptionRegistry) {\n    super()\n    // this.subscriptionRegistry =\n    // subscriptionRegistry || services.subscriptions.getSubscriptionRegistry(TOPIC.MONITORING, TOPIC.MONITORING_SUBSCRIPTIONS)\n  }\n\n  /**\n   * The main distribution method. Routes messages to functions\n   * based on the provided action parameter of the message\n   */\n  public handle (socket: SocketWrapper, message: MonitoringMessage) {\n    console.log('unknown action', message)\n  }\n}\n"
  },
  {
    "path": "src/handlers/presence/presence-handler.spec.ts",
    "content": "import 'mocha'\n\nimport PresenceHandler from './presence-handler'\n\nconst EVERYONE = '%_EVERYONE_%'\n\nimport * as C from '../../constants'\nimport * as testHelper from '../../test/helper/test-helper'\nimport { getTestMocks } from '../../test/helper/test-mocks'\nimport { PresenceMessage } from '../../../../client/dist/constants'\n\nconst { config, services } = testHelper.getDeepstreamOptions()\n\ndescribe('presence handler', () => {\n  let testMocks\n  let presenceHandler: PresenceHandler\n  let userOne\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    presenceHandler = new PresenceHandler(\n      config, services, testMocks.subscriptionRegistry, testMocks.stateRegistry\n    )\n    userOne = testMocks.getSocketWrapper('Marge')\n  })\n\n  afterEach(() => {\n    testMocks.subscriptionRegistryMock.verify()\n    testMocks.listenerRegistryMock.verify()\n    userOne.socketWrapperMock.verify()\n  })\n\n  it('subscribes to client logins and logouts', () => {\n    const subscriptionMessage = {\n      topic: C.TOPIC.PRESENCE,\n      action: C.PRESENCE_ACTION.SUBSCRIBE_ALL,\n    } as PresenceMessage\n\n    testMocks.subscriptionRegistryMock\n      .expects('subscribe')\n      .once()\n      .withExactArgs(EVERYONE, {\n        topic: C.TOPIC.PRESENCE,\n        action: C.PRESENCE_ACTION.SUBSCRIBE_ALL,\n        name: EVERYONE\n      }, userOne.socketWrapper, true)\n\n    presenceHandler.handle(userOne.socketWrapper, subscriptionMessage)\n  })\n\n  it('unsubscribes to client logins and logouts', () => {\n    const unsubscriptionMessage = {\n      topic: C.TOPIC.PRESENCE,\n      action: C.PRESENCE_ACTION.UNSUBSCRIBE_ALL,\n    } as PresenceMessage\n\n    testMocks.subscriptionRegistryMock\n      .expects('unsubscribe')\n      .once()\n      .withExactArgs(EVERYONE, {\n        topic: C.TOPIC.PRESENCE,\n        action: C.PRESENCE_ACTION.UNSUBSCRIBE_ALL,\n        name: EVERYONE\n      }, userOne.socketWrapper, true)\n\n    presenceHandler.handle(userOne.socketWrapper, unsubscriptionMessage)\n  })\n\n  it('does not return own name when queried and only user', () => {\n    const queryMessage = {\n      topic: C.TOPIC.PRESENCE,\n      action: C.PRESENCE_ACTION.QUERY_ALL\n    } as PresenceMessage\n\n    testMocks.stateRegistryMock\n      .expects('getAll')\n      .once()\n      .withExactArgs()\n      .returns(['Marge'])\n\n    userOne.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.PRESENCE,\n        action: C.PRESENCE_ACTION.QUERY_ALL_RESPONSE,\n        names: []\n      })\n\n    presenceHandler.handle(userOne.socketWrapper, queryMessage)\n  })\n\n  it('client joining gets added to state registry', () => {\n    testMocks.stateRegistryMock\n      .expects('add')\n      .once()\n      .withExactArgs(userOne.socketWrapper.userId)\n\n    presenceHandler.onClientConnected(userOne.socketWrapper)\n  })\n\n  it('client joining multiple times gets added once state registry', () => {\n    testMocks.stateRegistryMock\n      .expects('add')\n      .once()\n      .withExactArgs(userOne.socketWrapper.userId)\n\n    presenceHandler.onClientConnected(userOne.socketWrapper)\n    presenceHandler.onClientConnected(userOne.socketWrapper)\n  })\n\n  it('a duplicate client logs out does not remove from state', () => {\n    testMocks.stateRegistryMock\n      .expects('add')\n      .once()\n      .withExactArgs(userOne.socketWrapper.userId)\n\n    testMocks.stateRegistryMock\n      .expects('remove')\n      .never()\n\n    presenceHandler.onClientConnected(userOne.socketWrapper)\n    presenceHandler.onClientConnected(userOne.socketWrapper)\n    presenceHandler.onClientDisconnected(userOne.socketWrapper)\n  })\n\n  it('a client logging out removes from state', () => {\n    testMocks.stateRegistryMock\n      .expects('add')\n      .once()\n      .withExactArgs(userOne.socketWrapper.userId)\n\n    testMocks.stateRegistryMock\n      .expects('remove')\n      .once()\n      .withExactArgs(userOne.socketWrapper.userId)\n\n    presenceHandler.onClientConnected(userOne.socketWrapper)\n    presenceHandler.onClientDisconnected(userOne.socketWrapper)\n  })\n\n  it('returns one user when queried', () => {\n    const queryMessage = {\n      topic: C.TOPIC.PRESENCE,\n      action: C.PRESENCE_ACTION.QUERY_ALL,\n    } as PresenceMessage\n\n    testMocks.stateRegistryMock\n      .expects('getAll')\n      .once()\n      .withExactArgs()\n      .returns(['Bart'])\n\n    userOne.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.PRESENCE,\n        action: C.PRESENCE_ACTION.QUERY_ALL_RESPONSE,\n        names: ['Bart']\n      })\n\n    presenceHandler.handle(userOne.socketWrapper, queryMessage)\n  })\n\n  it('returns mutiple user when queried', () => {\n    const queryMessage = {\n      topic: C.TOPIC.PRESENCE,\n      action: C.PRESENCE_ACTION.QUERY_ALL\n    } as PresenceMessage\n\n    testMocks.stateRegistryMock\n      .expects('getAll')\n      .once()\n      .withExactArgs()\n      .returns(['Bart', 'Homer', 'Maggie'])\n\n    userOne.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.PRESENCE,\n        action: C.PRESENCE_ACTION.QUERY_ALL_RESPONSE,\n        names: ['Bart', 'Homer', 'Maggie']\n      })\n\n    presenceHandler.handle(userOne.socketWrapper, queryMessage)\n  })\n\n  it.skip('notifies subscribed users when user added to state', () => {\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(EVERYONE, {\n        topic: C.TOPIC.PRESENCE,\n        action: C.PRESENCE_ACTION.PRESENCE_JOIN_ALL,\n        name: 'Bart'\n      }, false, null, false)\n\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs('Bart', {\n        topic: C.TOPIC.PRESENCE,\n        action: C.PRESENCE_ACTION.PRESENCE_JOIN,\n        name: 'Bart'\n      }, false, null, false)\n\n      // This needs extra work\n    testMocks.stateRegistry.add('Bart')\n  })\n\n  it.skip('notifies subscribed users when user removed from state', () => {\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(EVERYONE, {\n        topic: C.TOPIC.PRESENCE,\n        action: C.PRESENCE_ACTION.PRESENCE_LEAVE_ALL,\n        name: 'Bart'\n      }, false, null, false)\n\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs('Bart', {\n        topic: C.TOPIC.PRESENCE,\n        action: C.PRESENCE_ACTION.PRESENCE_LEAVE,\n        name: 'Bart'\n      }, false, null, false)\n\n    testMocks.stateRegistry.emit('remove', 'Bart')\n  })\n})\n"
  },
  {
    "path": "src/handlers/presence/presence-handler.ts",
    "content": "import { PARSER_ACTION, PRESENCE_ACTION, TOPIC, PresenceMessage, Message, BulkSubscriptionMessage, STATE_REGISTRY_TOPIC } from '../../constants'\nimport { DeepstreamConfig, DeepstreamServices, SocketWrapper, StateRegistry, Handler, SubscriptionRegistry, ConnectionListener } from '@deepstream/types'\nimport { Dictionary } from 'ts-essentials'\n\nconst EVERYONE = '%_EVERYONE_%'\n\n/**\n * This class handles incoming and outgoing messages in relation\n * to deepstream presence. It provides a way to inform clients\n * who else is logged into deepstream\n */\nexport default class PresenceHandler extends Handler<PresenceMessage> implements ConnectionListener {\n  private localClients: Map<string, number> = new Map()\n  private subscriptionRegistry: SubscriptionRegistry\n  private connectedClients: StateRegistry\n\n  constructor (config: DeepstreamConfig, private services: DeepstreamServices, subscriptionRegistry?: SubscriptionRegistry, stateRegistry?: StateRegistry, private metaData?: any) {\n    super()\n\n    this.subscriptionRegistry =\n      subscriptionRegistry || services.subscriptions.getSubscriptionRegistry(TOPIC.PRESENCE, STATE_REGISTRY_TOPIC.PRESENCE_SUBSCRIPTIONS)\n\n    this.connectedClients =\n      stateRegistry || this.services.clusterStates.getStateRegistry(STATE_REGISTRY_TOPIC.ONLINE_USERS)\n\n    this.connectedClients.onAdd(this.onClientAdded.bind(this))\n    this.connectedClients.onRemove(this.onClientRemoved.bind(this))\n  }\n\n  /**\n  * The main entry point to the presence handler class.\n  *\n  * Handles subscriptions, unsubscriptions and queries\n  */\n  public handle (socketWrapper: SocketWrapper, message: PresenceMessage): void {\n    if (message.action === PRESENCE_ACTION.QUERY_ALL) {\n      this.handleQueryAll(message.correlationId, socketWrapper)\n      return\n    }\n\n    if (message.action === PRESENCE_ACTION.SUBSCRIBE_ALL) {\n      this.subscriptionRegistry.subscribe(EVERYONE, {\n        topic: TOPIC.PRESENCE,\n        action: PRESENCE_ACTION.SUBSCRIBE_ALL,\n        name: EVERYONE\n      }, socketWrapper, true)\n      socketWrapper.sendAckMessage({\n        topic: message.topic,\n        action: message.action\n      })\n      return\n    }\n\n    if (message.action === PRESENCE_ACTION.UNSUBSCRIBE_ALL) {\n      this.subscriptionRegistry.unsubscribe(EVERYONE, {\n        topic: TOPIC.PRESENCE,\n        action: PRESENCE_ACTION.UNSUBSCRIBE_ALL,\n        name: EVERYONE\n      }, socketWrapper, true)\n      socketWrapper.sendAckMessage({\n        topic: message.topic,\n        action: message.action\n      })\n      return\n    }\n\n    const users = message.names\n    if (!users) {\n      this.services.logger.error(\n        PARSER_ACTION[PARSER_ACTION.INVALID_MESSAGE],\n        `invalid presence names parameter ${PRESENCE_ACTION[message.action]}`\n      )\n      return\n    }\n\n    if (message.action === PRESENCE_ACTION.SUBSCRIBE) {\n      this.subscriptionRegistry.subscribeBulk(message as BulkSubscriptionMessage, socketWrapper)\n      return\n    }\n\n    if (message.action === PRESENCE_ACTION.UNSUBSCRIBE) {\n      this.subscriptionRegistry.unsubscribeBulk(message as BulkSubscriptionMessage, socketWrapper)\n      return\n    }\n\n    if (message.action === PRESENCE_ACTION.QUERY) {\n      this.handleQuery(users, message.correlationId, socketWrapper)\n      return\n    }\n\n    this.services.logger.warn(PARSER_ACTION[PARSER_ACTION.UNKNOWN_ACTION], PRESENCE_ACTION[message.action], this.metaData)\n  }\n\n  /**\n  * Called whenever a client has succesfully logged in with a username\n  */\n  public onClientConnected (socketWrapper: SocketWrapper): void {\n    if (socketWrapper.userId === 'OPEN') {\n      return\n    }\n    const currentCount = this.localClients.get(socketWrapper.userId)\n    if (currentCount === undefined) {\n      this.localClients.set(socketWrapper.userId, 1)\n      this.connectedClients.add(socketWrapper.userId)\n    } else {\n      this.localClients.set(socketWrapper.userId, currentCount + 1)\n    }\n  }\n\n  /**\n  * Called whenever a client has disconnected\n  */\n  public onClientDisconnected (socketWrapper: SocketWrapper): void {\n    if (socketWrapper.userId === 'OPEN') {\n      return\n    }\n    const currentCount = this.localClients.get(socketWrapper.userId)\n    if (!currentCount) {\n      // TODO: Log Error\n    } else if (currentCount === 1) {\n      this.localClients.delete(socketWrapper.userId)\n      this.connectedClients.remove(socketWrapper.userId)\n    } else {\n      this.localClients.set(socketWrapper.userId, currentCount - 1)\n    }\n  }\n\n  private handleQueryAll (correlationId: string, socketWrapper: SocketWrapper): void {\n    const clients = this.connectedClients.getAll()\n    const index = clients.indexOf(socketWrapper.userId)\n    if (index !== -1) {\n      clients.splice(index, 1)\n    }\n    socketWrapper.sendMessage({\n      topic: TOPIC.PRESENCE,\n      action: PRESENCE_ACTION.QUERY_ALL_RESPONSE,\n      names: clients\n    })\n  }\n\n  /**\n  * Handles finding clients who are connected and splicing out the client\n  * querying for users\n  */\n  private handleQuery (users: string[], correlationId: string, socketWrapper: SocketWrapper): void {\n    const result: Dictionary<boolean> = {}\n    const clients = this.connectedClients.getAll()\n    for (let i = 0; i < users.length; i++) {\n      result[users[i]] = clients.includes(users[i])\n    }\n    socketWrapper.sendMessage({\n      topic: TOPIC.PRESENCE,\n      action: PRESENCE_ACTION.QUERY_RESPONSE,\n      correlationId,\n      parsedData: result,\n    })\n  }\n\n  /**\n  * Alerts all clients who are subscribed to\n  * PRESENCE_JOIN that a new client has been added.\n  */\n  private onClientAdded (username: string): void {\n    const individualMessage: Message = {\n      topic: TOPIC.PRESENCE,\n      action: PRESENCE_ACTION.PRESENCE_JOIN,\n      name : username,\n    }\n\n    const allMessage: Message = {\n      topic: TOPIC.PRESENCE,\n      action: PRESENCE_ACTION.PRESENCE_JOIN_ALL,\n      name: username\n    }\n\n    this.subscriptionRegistry.sendToSubscribers(EVERYONE, allMessage, false, null, true)\n    this.subscriptionRegistry.sendToSubscribers(username, individualMessage, false, null, true)\n  }\n\n  /**\n  * Alerts all clients who are subscribed to\n  * PRESENCE_LEAVE that the client has left.\n  */\n  private onClientRemoved (username: string): void {\n    const individualMessage: Message = {\n      topic: TOPIC.PRESENCE,\n      action: PRESENCE_ACTION.PRESENCE_LEAVE,\n      name : username,\n    }\n\n    const allMessage: Message = {\n      topic: TOPIC.PRESENCE,\n      action: PRESENCE_ACTION.PRESENCE_LEAVE_ALL,\n      name: username\n    }\n\n    this.subscriptionRegistry.sendToSubscribers(EVERYONE, allMessage, false, null)\n    this.subscriptionRegistry.sendToSubscribers(username, individualMessage, false, null)\n  }\n}\n"
  },
  {
    "path": "src/handlers/record/record-deletion.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport {spy} from 'sinon'\n\nimport RecordDeletion from './record-deletion'\n\nimport * as M from './test-messages'\nimport * as C from '../../../src/constants'\nimport * as testHelper from '../../test/helper/test-helper'\nimport { getTestMocks } from '../../test/helper/test-mocks'\n\ndescribe('record deletion', () => {\n  let testMocks\n  let recordDeletion\n  let client\n  let config\n  let services\n  let callback\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    client = testMocks.getSocketWrapper()\n    const options = testHelper.getDeepstreamOptions()\n    config = options.config\n    services = options.services\n    callback = spy()\n  })\n\n  afterEach(() => {\n    client.socketWrapperMock.verify()\n  })\n\n  it('deletes records - happy path', () => {\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs(M.deletionSuccessMsg)\n\n    recordDeletion = new RecordDeletion(\n      config, services, client.socketWrapper, M.deletionMsg, callback\n    )\n\n    expect(services.cache.completedDeleteOperations).to.equal(1)\n    expect(services.storage.completedDeleteOperations).to.equal(1)\n\n    expect(recordDeletion.isDestroyed).to.equal(true)\n    expect(callback).to.have.callCount(1)\n  })\n\n  it('encounters an error during record deletion', (done) => {\n    services.cache.nextOperationWillBeSuccessful = false\n    services.cache.nextOperationWillBeSynchronous = false\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.RECORD_DELETE_ERROR,\n        name: 'someRecord'\n      })\n\n    recordDeletion = new RecordDeletion(\n      config, services, client.socketWrapper, M.deletionMsg, callback\n    )\n\n    setTimeout(() => {\n      expect(recordDeletion.isDestroyed).to.equal(true)\n      expect(callback).to.have.callCount(0)\n      expect(services.logger.logSpy.firstCall.args).to.deep.equal([3, C.RECORD_ACTION[C.RECORD_ACTION.RECORD_DELETE_ERROR], 'storageError'])\n      done()\n    }, 20)\n  })\n\n  it('encounters an ack delete timeout', (done) => {\n    config.record.cacheRetrievalTimeout = 10\n    services.cache.nextOperationWillBeSuccessful = false\n    services.cache.nextOperationWillBeSynchronous = false\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.RECORD_DELETE_ERROR,\n        name: 'someRecord'\n      })\n\n    recordDeletion = new RecordDeletion(\n      config, services, client.socketWrapper, M.deletionMsg, callback\n    )\n\n    setTimeout(() => {\n      expect(recordDeletion.isDestroyed).to.equal(true)\n      expect(callback).to.have.callCount(0)\n      expect(services.logger.logSpy.firstCall.args).to.deep.equal([3, C.RECORD_ACTION[C.RECORD_ACTION.RECORD_DELETE_ERROR], 'cache timeout'])\n      done()\n    }, 100)\n  })\n\n  it('doesn\\'t delete excluded messages from storage', () => {\n    config.record.storageExclusionPrefixes = ['no-storage/']\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs(M.anotherDeletionSuccessMsg)\n\n    recordDeletion = new RecordDeletion(\n      config, services, client.socketWrapper, M.anotherDeletionMsg, callback\n    )\n\n    expect(services.cache.completedDeleteOperations).to.equal(1)\n    expect(services.storage.completedDeleteOperations).to.equal(0)\n    expect(recordDeletion.isDestroyed).to.equal(true)\n    expect(callback).to.have.callCount(1)\n  })\n})\n"
  },
  {
    "path": "src/handlers/record/record-deletion.ts",
    "content": "import { DeepstreamConfig, DeepstreamServices, SocketWrapper } from '@deepstream/types'\nimport { Message, RecordMessage, RECORD_ACTION, TOPIC } from '../../constants'\nimport { isExcluded } from '../../utils/utils'\n\nexport default class RecordDeletion {\n  private metaData: any\n  private config: DeepstreamConfig\n  private services: DeepstreamServices\n  private socketWrapper: SocketWrapper\n  private message: Message\n  private successCallback: Function\n  private recordName: string\n  private completed: 0\n  private isDestroyed: boolean\n  private cacheTimeout: any\n  private storageTimeout: any\n\n/**\n * This class represents the deletion of a single record. It handles it's removal\n * from cache and storage and handles errors and timeouts\n */\n  constructor (config: DeepstreamConfig, services: DeepstreamServices, socketWrapper: SocketWrapper, message: RecordMessage, successCallback: Function, metaData: any = {}) {\n    this.metaData = metaData\n    this.config = config\n    this.services = services\n    this.socketWrapper = socketWrapper\n    this.message = message\n    this.successCallback = successCallback\n    this.recordName = message.name\n    this.completed = 0\n    this.isDestroyed = false\n\n    this.onCacheDelete = this.onCacheDelete.bind(this)\n    this.onStorageDelete = this.onStorageDelete.bind(this)\n\n    this.cacheTimeout = setTimeout(\n      this.handleError.bind(this, 'cache timeout'),\n      this.config.record.cacheRetrievalTimeout,\n    )\n    this.services.cache.delete(\n      this.recordName,\n      this.onCacheDelete.bind(this),\n      metaData,\n    )\n\n    if (!isExcluded(this.config.record.storageExclusionPrefixes, this.recordName)) {\n      this.storageTimeout = setTimeout(\n        this.handleError.bind(this, 'storage timeout'),\n        this.config.record.storageRetrievalTimeout,\n      )\n      this.services.storage.delete(\n        this.recordName,\n        this.onStorageDelete,\n        metaData,\n      )\n    } else {\n      this.onStorageDelete(null)\n    }\n  }\n\n/**\n * Callback for completed cache and storage interactions. Will invoke\n * _done() once both are completed\n */\n  private onCacheDelete (error: string | null): void {\n    clearTimeout(this.cacheTimeout)\n    this.stageComplete(error)\n  }\n\n  private onStorageDelete (error: string | null) {\n    clearTimeout(this.storageTimeout)\n    this.stageComplete(error)\n  }\n\n  private stageComplete (error: string | null) {\n    this.completed++\n\n    if (this.isDestroyed) {\n      return\n    }\n\n    if (error) {\n      this.handleError(error.toString())\n      return\n    }\n\n    if (this.completed === 2) {\n      this.done()\n    }\n  }\n\n/**\n * Callback for successful deletions. Notifies the original sender and calls\n * the callback to allow the recordHandler to broadcast the deletion\n */\n  private done (): void {\n    this.services.logger.info(RECORD_ACTION[RECORD_ACTION.DELETE], this.recordName, this.metaData)\n    this.socketWrapper.sendMessage({ topic: TOPIC.RECORD, action: RECORD_ACTION.DELETE_SUCCESS, name: this.message.name })\n    this.message = Object.assign({}, this.message, { action: RECORD_ACTION.DELETED })\n    this.successCallback(this.recordName, this.message, this.socketWrapper)\n    this.destroy()\n  }\n\n/**\n * Destroyes the class and null down its dependencies\n */\n  private destroy (): void {\n    clearTimeout(this.cacheTimeout)\n    clearTimeout(this.storageTimeout)\n    this.isDestroyed = true\n    // this.options = null\n    // this.socketWrapper = null\n    // this.message = null\n  }\n\n/**\n * Handle errors that occured during deleting the record\n */\n  private handleError (errorMsg: string) {\n    this.socketWrapper.sendMessage({\n      topic: TOPIC.RECORD,\n      action: RECORD_ACTION.RECORD_DELETE_ERROR,\n      name: this.recordName\n    })\n    this.services.logger.error(RECORD_ACTION[RECORD_ACTION.RECORD_DELETE_ERROR], errorMsg, this.metaData)\n    this.destroy()\n  }\n}\n"
  },
  {
    "path": "src/handlers/record/record-handler-permission.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as C from '../../../src/constants'\n\nimport RecordHandler from './record-handler'\n\nimport * as M from './test-messages'\nimport * as testHelper from '../../test/helper/test-helper'\nimport { getTestMocks } from '../../test/helper/test-mocks'\n\ndescribe('record handler handles messages', () => {\n  let testMocks\n  let recordHandler\n  let client\n  let config\n  let services\n\n  beforeEach(() => {\n    ({config, services} = testHelper.getDeepstreamOptions())\n    recordHandler = new RecordHandler(config, services)\n\n    testMocks = getTestMocks()\n    client = testMocks.getSocketWrapper()\n  })\n\n  afterEach(() => {\n    client.socketWrapperMock.verify()\n  })\n\n  it('triggers create and read actions if record doesnt exist', () => {\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs(M.readResponseMessage)\n\n    recordHandler.handle(client.socketWrapper, M.subscribeCreateAndReadMessage)\n\n    expect(services.permission.lastArgs.length).to.equal(2)\n    expect(services.permission.lastArgs[0][1].action).to.equal(C.RECORD_ACTION.CREATE)\n    expect(services.permission.lastArgs[1][1].action).to.equal(C.RECORD_ACTION.READ)\n  })\n\n  it('triggers only read action if record does exist', () => {\n    services.cache.set('some-record', 0, {}, () => {})\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs(M.readResponseMessage)\n\n    recordHandler.handle(client.socketWrapper, M.subscribeCreateAndReadMessage)\n\n    expect(services.permission.lastArgs.length).to.equal(1)\n    expect(services.permission.lastArgs[0][1].action).to.equal(C.RECORD_ACTION.READ)\n  })\n\n  it('rejects a create', () => {\n    services.permission.nextResult = false\n\n    const { names, ...msg } = M.subscribeCreateAndReadDeniedMessage\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({ ...msg, name: 'some-record', isError: true })\n\n    recordHandler.handle(client.socketWrapper, M.subscribeCreateAndReadMessage)\n\n    expect(services.permission.lastArgs.length).to.equal(1)\n    expect(services.permission.lastArgs[0][1].action).to.equal(C.RECORD_ACTION.CREATE)\n  })\n\n  it('rejects a read', () => {\n    services.cache.set('some-record', 0, {}, () => {})\n    services.permission.nextResult = false\n\n    const { names, ...msg } = M.subscribeCreateAndReadDeniedMessage\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({ ...msg, name: 'some-record', isError: true })\n\n    recordHandler.handle(client.socketWrapper, M.subscribeCreateAndReadMessage)\n\n    expect(services.permission.lastArgs.length).to.equal(1)\n    expect(services.permission.lastArgs[0][1].action).to.equal(C.RECORD_ACTION.READ)\n  })\n\n  it('handles a permission error', () => {\n    services.permission.nextError = 'XXX'\n    services.permission.nextResult = false\n\n    const { names, ...msg } = M.subscribeCreateAndReadPermissionErrorMessage\n    client.socketWrapperMock\n    .expects('sendMessage')\n    .once()\n    .withExactArgs({ ...msg, name: 'some-record', isError: true })\n\n    recordHandler.handle(client.socketWrapper, M.subscribeCreateAndReadMessage)\n\n    expect(services.permission.lastArgs.length).to.equal(1)\n    expect(services.permission.lastArgs[0][1].action).to.equal(C.RECORD_ACTION.CREATE)\n  })\n})\n"
  },
  {
    "path": "src/handlers/record/record-handler.spec.ts",
    "content": "import 'mocha'\n\nimport * as C from '../../../src/constants'\nimport { expect } from 'chai'\n\nimport RecordHandler from './record-handler'\n\nimport * as M from './test-messages'\n\nimport * as testHelper from '../../test/helper/test-helper'\nimport { getTestMocks } from '../../test/helper/test-mocks'\n\ndescribe('record handler handles messages', () => {\n  let testMocks\n  let recordHandler\n  let client\n  let config\n  let services\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    client = testMocks.getSocketWrapper('someUser')\n    const options = testHelper.getDeepstreamOptions()\n    config = options.config\n    services = options.services\n    recordHandler = new RecordHandler(\n      config, services, testMocks.subscriptionRegistry, testMocks.listenerRegistry\n    )\n  })\n\n  afterEach(() => {\n    client.socketWrapperMock.verify()\n    testMocks.subscriptionRegistryMock.verify()\n  })\n\n  it('creates a non existing record', () => {\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs(M.readResponseMessage)\n\n    recordHandler.handle(client.socketWrapper, M.subscribeCreateAndReadMessage)\n\n    expect(services.cache.lastSetKey).to.equal('some-record')\n    expect(services.cache.lastSetVersion).to.equal(0)\n    expect(services.cache.lastSetValue).to.deep.equal({})\n\n    expect(services.storage.lastSetKey).to.equal('some-record')\n    expect(services.storage.lastSetVersion).to.equal(0)\n    expect(services.storage.lastSetValue).to.deep.equal({})\n  })\n\n  it('tries to create a non existing record, but receives an error from the cache', () => {\n    services.cache.failNextSet = true\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.RECORD_CREATE_ERROR,\n        originalAction: C.RECORD_ACTION.SUBSCRIBECREATEANDREAD,\n        name: M.subscribeCreateAndReadMessage.names[0]\n      })\n\n    recordHandler.handle(client.socketWrapper, M.subscribeCreateAndReadMessage)\n    // expect(options.logger.lastLogMessage).to.equal('storage:storageError')\n  })\n\n  it('does not store new record when excluded', () => {\n    config.record.storageExclusionPrefixes = ['some-record']\n\n    recordHandler.handle(client.socketWrapper, M.subscribeCreateAndReadMessage)\n\n    expect(services.storage.lastSetKey).to.equal(null)\n    expect(services.storage.lastSetVersion).to.equal(null)\n    expect(services.storage.lastSetValue).to.equal(null)\n  })\n\n  it('returns an existing record', () => {\n    services.cache.set('some-record', M.recordVersion, M.recordData, () => {})\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.READ_RESPONSE,\n        name: 'some-record',\n        version: M.recordVersion,\n        parsedData: M.recordData\n      })\n\n    recordHandler.handle(client.socketWrapper, M.subscribeCreateAndReadMessage)\n  })\n\n  it('returns a snapshot of the data that exists with version number and data', () => {\n    services.cache.set('some-record', M.recordVersion, M.recordData, () => {})\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.READ_RESPONSE,\n        name: 'some-record',\n        parsedData: M.recordData,\n        version: M.recordVersion\n      })\n\n    recordHandler.handle(client.socketWrapper, M.recordSnapshotMessage)\n  })\n\n  it('returns an error for a snapshot of data that doesn\\'t exists', () => {\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.RECORD_NOT_FOUND,\n        originalAction: M.recordSnapshotMessage.action,\n        name: M.recordSnapshotMessage.name,\n        isError: true\n      })\n\n    recordHandler.handle(client.socketWrapper, M.recordSnapshotMessage)\n  })\n\n  it('returns an error for a snapshot if message error occurs with record retrieval', () => {\n    services.cache.nextOperationWillBeSuccessful = false\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.RECORD_LOAD_ERROR,\n        originalAction: M.recordSnapshotMessage.action,\n        name: M.recordSnapshotMessage.name,\n        isError: true\n      })\n\n    recordHandler.handle(client.socketWrapper, M.recordSnapshotMessage)\n  })\n\n  it('returns a version of the data that exists with version number', () => {\n    ['record/1', 'record/2', 'record/3'].forEach((name) => {\n      const version = Math.floor(Math.random() * 100)\n      const data = { firstname: 'Wolfram' }\n      services.cache.set(name, version, data, () => {})\n\n      client.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs(Object.assign({}, M.recordHeadResponseMessage, { name, version }))\n\n      recordHandler.handle(client.socketWrapper, Object.assign({}, M.recordHeadMessage, { name }))\n    })\n  })\n\n  it('returns an version of -1 for head request of data that doesn\\'t exist', () => {\n    ['record/1', 'record/2', 'record/3'].forEach((name) => {\n      client.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs(Object.assign({}, {\n          topic: C.TOPIC.RECORD,\n          action: C.RECORD_ACTION.HEAD_RESPONSE,\n          name: M.recordHeadMessage.name,\n          version: -1\n        }, { name }))\n\n      recordHandler.handle(client.socketWrapper, Object.assign({}, M.recordHeadMessage, { name }))\n    })\n  })\n\n  it('returns an error for a version if message error occurs with record retrieval', () => {\n    services.cache.nextOperationWillBeSuccessful = false\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.RECORD_LOAD_ERROR,\n        originalAction: M.recordHeadMessage.action,\n        name: M.recordHeadMessage.name,\n        isError: true\n      })\n\n    recordHandler.handle(client.socketWrapper, M.recordHeadMessage)\n  })\n\n  it('patches a record', () => {\n    const recordPatch = Object.assign({}, M.recordPatch)\n    services.cache.set('some-record', M.recordVersion, Object.assign({}, M.recordData), () => {})\n\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(M.recordPatch.name, recordPatch, false, client.socketWrapper)\n\n    recordHandler.handle(client.socketWrapper, recordPatch)\n\n    services.cache.get('some-record', (error, version, record) => {\n      expect(version).to.equal(version)\n      expect(record).to.deep.equal({ name: 'Kowalski', lastname: 'Egon' })\n    })\n  })\n\n  it('updates a record', () => {\n    services.cache.set('some-record', M.recordVersion, M.recordData, () => {})\n\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(M.recordUpdate.name, M.recordUpdate, false, client.socketWrapper)\n\n    recordHandler.handle(client.socketWrapper, M.recordUpdate)\n\n    services.cache.get('some-record', (error, version, result) => {\n      expect(version).to.equal(6)\n      expect(result).to.deep.equal({ name: 'Kowalski' })\n    })\n  })\n\n  it('rejects updates for existing versions', () => {\n    services.cache.set(M.recordUpdate.name, M.recordVersion, M.recordData, () => {})\n    const ExistingVersion = Object.assign({}, M.recordUpdate, { version: M.recordVersion })\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.VERSION_EXISTS,\n        originalAction: ExistingVersion.action,\n        name: ExistingVersion.name,\n        version: ExistingVersion.version,\n        parsedData: M.recordData,\n        isWriteAck: false,\n        correlationId: undefined\n      })\n\n    recordHandler.handle(client.socketWrapper, ExistingVersion)\n\n    expect(services.logger.lastLogMessage).to.equal('someUser tried to update record some-record to version 5 but it already was 5')\n  })\n\n  describe('notifies when db/cache remotely changed', () => {\n    beforeEach(() => {\n      services.storage.nextGetWillBeSynchronous = true\n      services.cache.nextGetWillBeSynchronous = true\n    })\n\n    it ('notifies users when record changes', () => {\n      M.notify.names.forEach(name => {\n        services.storage.set(name, 123, { name }, () => {})\n\n        testMocks.subscriptionRegistryMock\n          .expects('sendToSubscribers')\n          .once()\n          .withExactArgs(name, {\n            topic: C.TOPIC.RECORD,\n            action: C.RECORD_ACTION.UPDATE,\n            name,\n            parsedData: { name },\n            version: 123\n          }, true, null)\n      })\n\n      recordHandler.handle(client.socketWrapper, M.notify)\n    })\n\n    it('notifies users when records deleted', () => {\n      M.notify.names.forEach(name => {\n        testMocks.subscriptionRegistryMock\n          .expects('sendToSubscribers')\n          .once()\n          .withExactArgs(name, {\n            topic: C.TOPIC.RECORD,\n            action: C.RECORD_ACTION.DELETED,\n            name\n          }, true, null)\n      })\n\n      recordHandler.handle(client.socketWrapper, M.notify)\n    })\n\n    it('notifies users when records updated and deleted combined', () => {\n      services.storage.set(M.notify.names[0], 1, { name: M.notify.names[0] }, () => {})\n\n      testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(M.notify.names[0], {\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.UPDATE,\n        name: M.notify.names[0],\n        parsedData: { name: M.notify.names[0] },\n        version: 1\n      }, true, null)\n\n      testMocks.subscriptionRegistryMock\n        .expects('sendToSubscribers')\n        .once()\n        .withExactArgs(M.notify.names[1], {\n          topic: C.TOPIC.RECORD,\n          action: C.RECORD_ACTION.DELETED,\n          name: M.notify.names[1]\n        }, true, null)\n\n      recordHandler.handle(client.socketWrapper, M.notify)\n    })\n  })\n\n  describe('subscription registry', () => {\n    it('handles unsubscribe messages', () => {\n      testMocks.subscriptionRegistryMock\n        .expects('unsubscribeBulk')\n        .once()\n        .withExactArgs(M.unsubscribeMessage, client.socketWrapper)\n\n      recordHandler.handle(client.socketWrapper, M.unsubscribeMessage)\n    })\n  })\n\n  it('updates a record via same client to the same version', (done) => {\n    config.record.cacheRetrievalTimeout = 50\n    services.cache.nextGetWillBeSynchronous = false\n    services.cache.set(M.recordUpdate.name, M.recordVersion, M.recordData, () => {})\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .twice()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.VERSION_EXISTS,\n        originalAction: C.RECORD_ACTION.UPDATE,\n        version: M.recordVersion,\n        parsedData: M.recordData,\n        name: M.recordUpdate.name,\n        isWriteAck: false,\n        correlationId: undefined\n      })\n\n    recordHandler.handle(client.socketWrapper, M.recordUpdate)\n    recordHandler.handle(client.socketWrapper, M.recordUpdate)\n    recordHandler.handle(client.socketWrapper, M.recordUpdate)\n\n    setTimeout(() => {\n      /**\n      * Important to note this is a race condition since version exists errors are sent as soon as record is retrieved,\n      * which means it hasn't yet been written to cache.\n      */\n      done()\n    }, 50)\n  })\n\n  it('handles deletion messages', () => {\n    services.cache.nextGetWillBeSynchronous = false\n    services.cache.set(M.recordDelete.name, 1, {}, () => {})\n\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(M.recordDelete.name, {\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.DELETED,\n        name: M.recordDelete.name\n      }, true, client.socketWrapper)\n\n    testMocks.subscriptionRegistryMock\n      .expects('getLocalSubscribers')\n      .once()\n      .returns(new Set())\n\n    recordHandler.handle(client.socketWrapper, M.recordDelete)\n\n    services.cache.get(M.recordDelete.name, (error, version, data) => {\n      expect(version).to.deep.equal(-1)\n      expect(data).to.equal(null)\n    })\n  })\n\n  it('updates a record with a -1 version number', () => {\n    services.cache.set(M.recordUpdate.name, 5, Object.assign({}, M.recordData), () => {})\n\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(M.recordUpdate.name, Object.assign({}, M.recordUpdate, { version: 6 }), false, client.socketWrapper)\n\n    recordHandler.handle(client.socketWrapper, Object.assign({}, M.recordUpdate, { version: -1 }))\n\n    services.cache.get(M.recordUpdate.name, (error, version, data) => {\n      expect(data).to.deep.equal(M.recordUpdate.parsedData)\n      expect(version).to.equal(6)\n    })\n  })\n\n  it('updates multiple updates with an -1 version number', () => {\n    const data = Object.assign({}, M.recordData)\n    services.cache.set(M.recordUpdate.name, 5, data, () => {})\n\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(M.recordUpdate.name, Object.assign({}, M.recordUpdate, { version: 6 }), false, client.socketWrapper)\n\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(M.recordUpdate.name, Object.assign({}, M.recordUpdate, { version: 7 }), false, client.socketWrapper)\n\n    recordHandler.handle(client.socketWrapper, Object.assign({}, M.recordUpdate, { version: -1 }))\n    recordHandler.handle(client.socketWrapper, Object.assign({}, M.recordUpdate, { version: -1 }))\n\n    services.cache.get(M.recordUpdate.name, (error, version, result) => {\n      expect(result).to.deep.equal(M.recordUpdate.parsedData)\n    })\n  })\n\n  it.skip('creates records when using CREATEANDUPDATE', () => {\n    testMocks.subscriptionRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(\n        M.createAndUpdate.name,\n        Object.assign({}, M.createAndUpdate, { action: C.RECORD_ACTION.UPDATE, version: 1 }),\n        false,\n        client.socketWrapper\n      )\n\n    recordHandler.handle(client.socketWrapper, M.createAndUpdate)\n\n    services.cache.get(M.createAndUpdate.name, (error, version, data) => {\n      expect(version).to.deep.equal(1)\n      expect(data).to.deep.equal(M.recordData)\n    })\n  })\n\n  it('registers a listener', () => {\n    testMocks.listenerRegistryMock\n      .expects('handle')\n      .once()\n      .withExactArgs(client.socketWrapper, M.listenMessage)\n\n    recordHandler.handle(client.socketWrapper, M.listenMessage)\n  })\n\n  it('removes listeners', () => {\n    testMocks.listenerRegistryMock\n      .expects('handle')\n      .once()\n      .withExactArgs(client.socketWrapper, M.unlistenMessage)\n\n    recordHandler.handle(client.socketWrapper, M.unlistenMessage)\n  })\n\n  it('processes listen accepts', () => {\n    testMocks.listenerRegistryMock\n      .expects('handle')\n      .once()\n      .withExactArgs(client.socketWrapper, M.listenAcceptMessage)\n\n    recordHandler.handle(client.socketWrapper, M.listenAcceptMessage)\n  })\n\n  it('processes listen rejects', () => {\n    testMocks.listenerRegistryMock\n      .expects('handle')\n      .once()\n      .withExactArgs(client.socketWrapper, M.listenRejectMessage)\n\n    recordHandler.handle(client.socketWrapper, M.listenRejectMessage)\n  })\n})\n"
  },
  {
    "path": "src/handlers/record/record-handler.ts",
    "content": "import RecordDeletion from './record-deletion'\nimport { recordRequestBinding } from './record-request'\nimport { RecordTransition } from './record-transition'\nimport { SubscriptionRegistry, Handler, DeepstreamConfig, DeepstreamServices, SocketWrapper, EVENT } from '@deepstream/types'\nimport { ListenerRegistry } from '../../listen/listener-registry'\nimport { isExcluded } from '../../utils/utils'\nimport { STATE_REGISTRY_TOPIC, RecordMessage, TOPIC, RecordWriteMessage, BulkSubscriptionMessage, ListenMessage, PARSER_ACTION, RECORD_ACTION as RA, JSONObject, Message, RECORD_ACTION, ALL_ACTIONS } from '../../constants'\n\nexport default class RecordHandler extends Handler<RecordMessage> {\n  private subscriptionRegistry: SubscriptionRegistry\n  private listenerRegistry: ListenerRegistry\n  private transitions = new Map<string, RecordTransition>()\n  private recordRequestsInProgress = new Map<string, Function[]>()\n  private recordRequest: Function\n\n/**\n * The entry point for record related operations\n */\n  constructor (private readonly config: DeepstreamConfig, private readonly services: DeepstreamServices, subscriptionRegistry?: SubscriptionRegistry, listenerRegistry?: ListenerRegistry, private readonly metaData?: any) {\n    super()\n\n    this.subscriptionRegistry =\n      subscriptionRegistry || services.subscriptions.getSubscriptionRegistry(TOPIC.RECORD, STATE_REGISTRY_TOPIC.RECORD_SUBSCRIPTIONS)\n    this.listenerRegistry =\n      listenerRegistry || new ListenerRegistry(TOPIC.RECORD, config, services, this.subscriptionRegistry, null)\n    this.subscriptionRegistry.setSubscriptionListener(this.listenerRegistry)\n    this.recordRequest = recordRequestBinding(config, services, this, metaData)\n\n    this.onDeleted = this.onDeleted.bind(this)\n    this.create = this.create.bind(this)\n    this.onPermissionResponse = this.onPermissionResponse.bind(this)\n  }\n\n  public async close () {\n    this.listenerRegistry.close()\n  }\n\n  /**\n * Handles incoming record requests.\n *\n * Please note that neither CREATE nor READ is supported as a\n * client send action. Instead the client sends CREATEORREAD\n * and deepstream works which one it will be\n */\n  public handle (socketWrapper: SocketWrapper | null, message: RecordMessage): void {\n    const action = message.action\n\n    if (socketWrapper === null) {\n      this.handleClusterUpdate(message)\n      return\n    }\n\n    if (action === RA.SUBSCRIBE) {\n      this.subscriptionRegistry.subscribeBulk(message as BulkSubscriptionMessage, socketWrapper)\n      return\n    }\n\n    if (action === RA.SUBSCRIBECREATEANDREAD || action === RA.SUBSCRIBEANDREAD) {\n      const onSuccess = action === RA.SUBSCRIBECREATEANDREAD ? this.onSubscribeCreateAndRead : this.onSubscribeAndRead\n      const l = message.names!.length\n      for (let i = 0; i < l; i++) {\n        this.recordRequest(\n          message.names![i],\n          socketWrapper,\n          onSuccess,\n          onRequestError,\n          { ...message, name: message.names![i] }\n        )\n      }\n      socketWrapper.sendAckMessage(message)\n      return\n    }\n\n    if (\n      action === RA.CREATEANDUPDATE ||\n      action === RA.CREATEANDPATCH\n    ) {\n    /*\n     * Allows updates to the record without being subscribed, creates\n     * the record if it doesn't exist\n     */\n      this.createAndUpdate(socketWrapper!, message as RecordWriteMessage)\n      return\n    }\n\n    if (action === RA.READ) {\n    /*\n     * Return the current state of the record in cache or db\n     */\n      this.recordRequest(message.name, socketWrapper, onSnapshotComplete, onRequestError, message)\n      return\n    }\n\n    if (action === RA.HEAD) {\n    /*\n     * Return the current version of the record or -1 if not found\n     */\n      this.head(socketWrapper!, message)\n      return\n    }\n\n    if (action === RA.SUBSCRIBEANDHEAD) {\n    /*\n     * Return the current version of the record or -1 if not found, subscribing either way\n     */\n      this.subscribeAndHeadBulk(socketWrapper!, message)\n      return\n    }\n\n    if (action === RA.UPDATE || action === RA.PATCH || action === RA.ERASE) {\n    /*\n     * Handle complete (UPDATE) or partial (PATCH/ERASE) updates\n     */\n      this.update(socketWrapper, message as RecordWriteMessage, message.isWriteAck || false)\n      return\n    }\n\n    if (action === RA.DELETE) {\n    /*\n     * Deletes the record\n     */\n      this.delete(socketWrapper!, message)\n      return\n    }\n\n    if (action === RA.UNSUBSCRIBE) {\n    /*\n    * Unsubscribes (discards) a record that was previously subscribed to\n    * using read()\n    */\n      this.subscriptionRegistry.unsubscribeBulk(message as BulkSubscriptionMessage, socketWrapper!)\n      return\n    }\n\n    if (action === RA.LISTEN || action === RA.UNLISTEN || action === RA.LISTEN_ACCEPT || action === RA.LISTEN_REJECT) {\n        /*\n    * Listen to requests for a particular record or records\n    * whose names match a pattern\n    */\n      this.listenerRegistry.handle(socketWrapper!, message as ListenMessage)\n      return\n    }\n\n    if (message.action === RA.NOTIFY) {\n      this.recordUpdatedWithoutDeepstream(message, socketWrapper)\n      return\n    }\n\n    this.services.logger.error(PARSER_ACTION[PARSER_ACTION.UNKNOWN_ACTION], RA[action], this.metaData)\n  }\n\n  private handleClusterUpdate (message: RecordMessage) {\n    if (message.action === RA.DELETED) {\n      this.remoteDelete(message)\n      return\n    }\n\n    if (message.action === RA.NOTIFY) {\n      this.recordUpdatedWithoutDeepstream(message)\n      return\n    }\n\n    this.broadcastUpdate(message.name, {\n      topic: message.topic,\n      action: message.action,\n      name: message.name,\n      path: message.path,\n      version: message.version,\n      data: message.data,\n      parsedData: message.parsedData\n    }, false, null)\n  }\n\n  private async recordUpdatedWithoutDeepstream (message: RecordMessage, socketWrapper: SocketWrapper | null = null) {\n    if (socketWrapper) {\n      if (this.services.cache.deleteBulk === undefined) {\n        const errorMessage = 'Cache needs to implement deleteBulk in order for it to work correctly'\n        this.services.logger.error(EVENT.PLUGIN_ERROR, errorMessage)\n        socketWrapper.sendMessage({\n          topic: TOPIC.RECORD,\n          action: RECORD_ACTION.RECORD_NOTIFY_ERROR,\n          isError: true,\n          parsedData: errorMessage,\n          correlationId: message.correlationId\n        })\n        return\n      }\n\n      try {\n        await new Promise<void>((resolve, reject) => this.services.cache.deleteBulk(message.names!, (error) => {\n          error ? reject(error) : resolve()\n        }))\n      } catch (error) {\n        const errorMessage = 'Error deleting messages in bulk when attempting to notify of remote changes'\n        this.services.logger.error(EVENT.ERROR, `${errorMessage}: ${error?.toString()}`, { message })\n        socketWrapper.sendMessage({\n          topic: TOPIC.RECORD,\n          action: RECORD_ACTION.RECORD_NOTIFY_ERROR,\n          isError: true,\n          parsedData: errorMessage,\n          correlationId: message.correlationId\n        })\n        return\n      }\n    }\n\n    let completed = 0\n    message.names!.forEach((recordName, index, names) => {\n      if (this.subscriptionRegistry.hasLocalSubscribers(recordName)) {\n        this.recordRequest(recordName, socketWrapper, (name: string, version: number, data: JSONObject) => {\n          if (version === -1) {\n            this.remoteDelete({\n              topic: TOPIC.RECORD,\n              action: RECORD_ACTION.DELETED,\n              name\n            })\n          } else {\n            this.subscriptionRegistry.sendToSubscribers(name, {\n              topic: TOPIC.RECORD,\n              action: RECORD_ACTION.UPDATE,\n              name,\n              version,\n              parsedData: data\n            }, true, null)\n          }\n\n          completed++\n          if (completed === names.length && socketWrapper) {\n            socketWrapper.sendAckMessage(message)\n            this.services.clusterNode.send(message)\n          }\n        }, (event: RA, errorMessage: string, name: string, socket: SocketWrapper, msg: Message) => {\n          completed++\n          if (socket) {\n            onRequestError(event, errorMessage, recordName, socket, msg)\n          }\n          if (completed === names.length && socket) {\n            socket.sendAckMessage(message)\n            this.services.clusterNode.send(message)\n          }\n        }, message)\n      } else {\n        completed++\n        if (completed === names.length && socketWrapper) {\n          socketWrapper.sendAckMessage(message)\n          this.services.clusterNode.send(message)\n        }\n      }\n    })\n  }\n\n  /**\n   * Returns just the current version number of a record\n   * Results in a HEAD_RESPONSE\n   * If the record is not found, the version number will be -1\n   */\n  private head (socketWrapper: SocketWrapper, message: RecordMessage, name: string = message.name): void {\n    this.recordRequest(name, socketWrapper, onHeadComplete, onRequestError, message)\n  }\n\n  private subscribeAndHeadBulk (socketWrapper: SocketWrapper, message: RecordMessage): void {\n    this.services.cache.headBulk(message.names!, (error, versions, missing) => {\n      if (error) {\n        this.services.logger.error(EVENT.ERROR, `Error subscribing and head bulk for ${message.correlationId}`)\n        return\n      }\n\n      if (Object.keys(versions!).length > -1) {\n        socketWrapper.sendMessage({\n          topic: TOPIC.RECORD,\n          action: RA.HEAD_RESPONSE_BULK,\n          versions\n        })\n      }\n\n      this.subscriptionRegistry.subscribeBulk(message as BulkSubscriptionMessage, socketWrapper)\n\n      const l = missing!.length\n      for (let i = 0; i < l; i++) {\n        if (versions![missing![i]] === undefined) {\n\n          this.head(socketWrapper, message, missing![i])\n        }\n      }\n    })\n  }\n\n  private onSubscribeCreateAndRead (recordName: string, version: number, data: JSONObject | null, socketWrapper: SocketWrapper, message: RecordMessage) {\n    if (data) {\n      this.readAndSubscribe(message, version, data, socketWrapper)\n    } else {\n      this.permissionAction(\n        RA.CREATE,\n        message,\n        message.action,\n        socketWrapper,\n        this.create,\n      )\n    }\n  }\n\n  private onSubscribeAndRead (recordName: string, version: number, data: JSONObject | null, socketWrapper: SocketWrapper, message: RecordMessage) {\n    if (data) {\n      this.readAndSubscribe(message, version, data, socketWrapper)\n    } else {\n      this.permissionAction(RA.READ, message, message.action, socketWrapper, () => {\n        this.subscriptionRegistry.subscribe(message.name, { ...message, action: RA.SUBSCRIBE }, socketWrapper, message.names !== undefined)\n        socketWrapper.sendMessage({\n          topic: TOPIC.RECORD,\n          action: RECORD_ACTION.READ_RESPONSE,\n          name: message.name,\n          version: -1,\n          parsedData: {},\n        })\n      })\n    }\n  }\n\n/**\n * An upsert operation where the record will be created and written to\n * with the data in the message. Important to note that each operation,\n * the create and the write are permissioned separately.\n *\n * This method also takes note of the storageHotPathPatterns option, when a record\n * with a name that matches one of the storageHotPathPatterns is written to with\n * the CREATEANDUPDATE action, it will be permissioned for both CREATE and UPDATE, then\n * inserted into the cache and storage.\n */\n  private createAndUpdate (socketWrapper: SocketWrapper, message: RecordWriteMessage): void {\n    const recordName = message.name\n    const isPatch = message.path !== undefined\n    const originalAction = message.action\n    message = { ...message, action: isPatch ? RA.PATCH : RA.UPDATE }\n\n    // allow writes on the hot path to bypass the record transition\n    // and be written directly to cache and storage\n    for (let i = 0; i < this.config.record.storageHotPathPrefixes.length; i++) {\n      const pattern = this.config.record.storageHotPathPrefixes[i]\n      if (recordName.indexOf(pattern) === 0) {\n        if (isPatch) {\n          const errorMessage = {\n            topic: TOPIC.RECORD,\n            action: RA.INVALID_PATCH_ON_HOTPATH,\n            originalAction,\n            name: recordName\n          } as RecordMessage\n          if (message.correlationId) {\n            errorMessage.correlationId = message.correlationId\n          }\n          socketWrapper.sendMessage(errorMessage)\n          return\n        }\n\n        this.permissionAction(RA.CREATE, message, originalAction, socketWrapper, () => {\n          this.permissionAction(RA.UPDATE, message, originalAction, socketWrapper, () => {\n            this.forceWrite(recordName, message, socketWrapper)\n          })\n        })\n        return\n      }\n    }\n\n    const transition = this.transitions.get(recordName)\n    if (transition) {\n      this.permissionAction(message.action, message, originalAction, socketWrapper, () => {\n        transition.add(socketWrapper, message)\n      })\n      return\n    }\n\n    this.permissionAction(RA.CREATE, message, originalAction, socketWrapper, () => {\n      this.permissionAction(RA.UPDATE, message, originalAction, socketWrapper, () => {\n        this.update(socketWrapper, message, true)\n      })\n    })\n  }\n\n/**\n * Forcibly writes to the cache and storage layers without going via\n * the RecordTransition. Usually updates and patches will go via the\n * transition which handles write acknowledgements, however in the\n * case of a hot path write acknowledgement we need to handle that\n * case here.\n */\n  private forceWrite (recordName: string, message: RecordWriteMessage, socketWrapper: SocketWrapper): void {\n    socketWrapper.parseData(message)\n    const writeAck = message.isWriteAck\n    let cacheResponse = false\n    let storageResponse = false\n    let writeError: string | null\n    this.services.storage.set(recordName, 0, message.parsedData, (error) => {\n      if (writeAck) {\n        storageResponse = true\n        writeError = writeError || error || null\n        this.handleForceWriteAcknowledgement(\n          socketWrapper, message, cacheResponse, storageResponse, writeError,\n        )\n      }\n    }, this.metaData)\n\n    this.services.cache.set(recordName, 0, message.parsedData, (error) => {\n      if (!error) {\n        this.broadcastUpdate(recordName, message, false, socketWrapper)\n      }\n      if (writeAck) {\n        cacheResponse = true\n        writeError = writeError || error || null\n        this.handleForceWriteAcknowledgement(\n          socketWrapper, message, cacheResponse, storageResponse, writeError,\n        )\n      }\n    }, this.metaData)\n  }\n\n/**\n * Handles write acknowledgements during a force write. Usually\n * this case is handled via the record transition.\n */\n  public handleForceWriteAcknowledgement (\n    socketWrapper: SocketWrapper, message: RecordWriteMessage, cacheResponse: boolean, storageResponse: boolean, error: Error | string | null,\n  ): void {\n    if (storageResponse && cacheResponse) {\n      socketWrapper.sendMessage({\n        topic: TOPIC.RECORD,\n        action: RA.WRITE_ACKNOWLEDGEMENT,\n        name: message.name,\n        correlationId: message.correlationId\n      }, true)\n    }\n  }\n\n  /**\n   * Creates a new, empty record and triggers a read operation once done\n   */\n  private create (socketWrapper: SocketWrapper, message: RecordMessage, originalAction: RECORD_ACTION): void {\n    const recordName = message.name\n\n    // store the records data in the cache and wait for the result\n    this.services.cache.set(recordName, 0, {}, (error) => {\n      if (error) {\n        this.services.logger.error(RA[RA.RECORD_CREATE_ERROR], recordName, this.metaData)\n        socketWrapper.sendMessage({\n          topic: TOPIC.RECORD,\n          action: RA.RECORD_CREATE_ERROR,\n          originalAction,\n          name: message.name\n        })\n        return\n      }\n\n      // this isn't really needed, can subscribe and send empty data immediately\n      this.readAndSubscribe({ ...message, action: originalAction }, 0, {}, socketWrapper)\n    }, this.metaData)\n\n    if (!isExcluded(this.config.record.storageExclusionPrefixes, message.name)) {\n      // store the record data in the persistant storage independently and don't wait for the result\n      this.services.storage.set(recordName, 0, {}, (error) => {\n        if (error) {\n          this.services.logger.error(RA[RA.RECORD_CREATE_ERROR], `storage:${error}`, this.metaData)\n        }\n      }, this.metaData)\n    }\n  }\n\n/**\n * Subscribes to updates for a record and sends its current data once done\n */\n  private readAndSubscribe (message: RecordMessage, version: number, data: any, socketWrapper: SocketWrapper): void {\n    this.permissionAction(RA.READ, message, message.action, socketWrapper, () => {\n      this.subscriptionRegistry.subscribe(message.name, { ...message, action: RA.SUBSCRIBE }, socketWrapper, message.names !== undefined)\n\n      this.recordRequest(message.name, socketWrapper, (_: string, newVersion: number, latestData: any) => {\n        if (latestData) {\n          if (newVersion !== version) {\n            this.services.logger.info(\n              EVENT.INFO, `BUG CAUGHT! ${message.name} was version ${version} for readAndSubscribe, ` +\n              `but updated during permission to ${message.version}`\n            )\n          }\n          sendRecord(message.name, version, latestData, socketWrapper)\n        } else {\n          this.services.logger.error(\n            EVENT.ERROR,\n            `BUG? ${message.name} was version ${version} for readAndSubscribe, ` +\n            'but was removed during permission check',\n            { message }\n          )\n          onRequestError(\n            message.action, `\"${message.name}\" was removed during permission check`,\n            message.name, socketWrapper, message\n          )\n        }\n      }, onRequestError, message)\n    })\n  }\n\n /**\n * Applies both full and partial updates. Creates a new record transition that will live as\n * long as updates are in flight and new updates come in\n */\n  private update (socketWrapper: SocketWrapper | null, message: RecordWriteMessage, upsert: boolean): void {\n    const recordName = message.name\n    const version = message.version\n\n    /*\n    * If the update message is received from the message bus, rather than from a client,\n    * assume that the original deepstream node has already updated the record in cache and\n    * storage and only broadcast the message to subscribers\n    */\n    if (socketWrapper === null) {\n      this.broadcastUpdate(recordName, message, false, socketWrapper)\n      return\n    }\n\n    const isPatch = message.path !== undefined\n    message = { ...message, action: isPatch ? RA.PATCH : RA.UPDATE }\n\n    let transition = this.transitions.get(recordName)\n    if (transition && transition.hasVersion(version)) {\n      transition.sendVersionExists({ message, sender: socketWrapper })\n      return\n    }\n\n    if (!transition) {\n      transition = new RecordTransition(recordName, this.config, this.services, this, this.metaData)\n      this.transitions.set(recordName, transition)\n    }\n    transition.add(socketWrapper, message, upsert)\n  }\n\n/**\n * Invoked by RecordTransition. Notifies local subscribers and other deepstream\n * instances of record updates\n */\n  public broadcastUpdate (name: string, message: RecordMessage, noDelay: boolean, originalSender: SocketWrapper | null): void {\n      this.subscriptionRegistry.sendToSubscribers(name, message, noDelay, originalSender)\n  }\n\n/**\n * Called by a RecordTransition, either if it is complete or if an error occured. Removes\n * the transition from the registry\n */\n  public transitionComplete (recordName: string): void {\n    this.transitions.delete(recordName)\n  }\n\n/**\n * Executes or schedules a callback function once all transitions are complete\n *\n * This is called from the PermissionHandler destroy method, which\n * could occur in cases where 'runWhenRecordStable' is never called,\n * such as when no cross referencing or data loading is used.\n */\n  public removeRecordRequest (recordName: string): void {\n    const recordRequests = this.recordRequestsInProgress.get(recordName)\n\n    if (!recordRequests) {\n      return\n    }\n\n    if (recordRequests.length === 0) {\n      this.recordRequestsInProgress.delete(recordName)\n      return\n    }\n\n    const callback = recordRequests.splice(0, 1)[0]\n    callback(recordName)\n  }\n\n/**\n * Executes or schedules a callback function once all record requests are removed.\n * This is critical to block reads until writes have occured for a record, which is\n * only from permissions when a rule is required to be run and the cache has not\n * verified it has the latest version\n */\n  public runWhenRecordStable (recordName: string, callback: Function): void {\n    const recordRequests = this.recordRequestsInProgress.get(recordName)\n    if (!recordRequests || recordRequests.length === 0) {\n      this.recordRequestsInProgress.set(recordName, [])\n      callback(recordName)\n    } else {\n      recordRequests.push(callback)\n    }\n  }\n\n/**\n * Deletes a record. If a transition is in progress it will be stopped. Once the deletion is\n * complete, an ACK is returned to the sender and broadcast to the message bus.\n */\n  private delete (socketWrapper: SocketWrapper, message: RecordMessage) {\n    const recordName = message.name\n\n    const transition = this.transitions.get(recordName)\n    if (transition) {\n      transition.destroy()\n      this.transitions.delete(recordName)\n    }\n\n    // tslint:disable-next-line\n    new RecordDeletion(this.config, this.services, socketWrapper, message, this.onDeleted, this.metaData)\n  }\n\n/**\n * Handle a remote record deletion from the message bus. We assume that the original deepstream node\n * has already deleted the record from cache and storage and we only need to broadcast the message\n * to subscribers.\n *\n * If a transition is in progress it will be stopped.\n */\n  private remoteDelete (message: RecordMessage) {\n    const recordName = message.name\n\n    const transition = this.transitions.get(recordName)\n    if (transition) {\n      transition.destroy()\n      this.transitions.delete(recordName)\n    }\n\n    this.onDeleted(recordName, message, null)\n  }\n\n/*\n * Callback for completed deletions. Notifies subscribers of the delete and unsubscribes them\n */\n  private onDeleted (name: string, message: RecordMessage, originalSender: SocketWrapper | null) {\n    this.broadcastUpdate(name, message, true, originalSender)\n\n    for (const subscriber of this.subscriptionRegistry.getLocalSubscribers(name)) {\n      this.subscriptionRegistry.unsubscribe(name, message, subscriber, true)\n    }\n  }\n\n/**\n * A secondary permissioning step that is performed once we know if the record exists (READ)\n * or if it should be created (CREATE)\n */\n  private permissionAction (actionToPermission: RA, message: Message, originalAction: RA, socketWrapper: SocketWrapper, successCallback: Function) {\n    const copyWithAction = {...message, action: actionToPermission }\n    this.services.permission.canPerformAction(\n      socketWrapper,\n      copyWithAction,\n      this.onPermissionResponse,\n      { originalAction, successCallback }\n    )\n  }\n\n  /*\n  * Callback for complete permissions. Important to note that only compound operations like\n  * CREATE_AND_UPDATE will end up here.\n  */\n  private onPermissionResponse (\n    socketWrapper: SocketWrapper, message: Message, { originalAction, successCallback }: { originalAction: RA, successCallback: Function }, error: string | Error | ALL_ACTIONS | null, canPerformAction: boolean,\n  ): void {\n    if (error || !canPerformAction) {\n      let action\n      if (error) {\n        this.services.logger.error(RA[RA.MESSAGE_PERMISSION_ERROR], error.toString())\n        action = RA.MESSAGE_PERMISSION_ERROR\n      } else {\n        action = RA.MESSAGE_DENIED\n      }\n      const msg = {\n        topic: TOPIC.RECORD,\n        action,\n        originalAction,\n        name: message.name,\n        isError: true\n      } as RecordMessage\n      if (message.correlationId) {\n        msg.correlationId = message.correlationId\n      }\n      if (message.isWriteAck) {\n        msg.isWriteAck = true\n      }\n      socketWrapper.sendMessage(msg)\n    } else {\n      successCallback(socketWrapper, message, originalAction)\n    }\n  }\n}\n\nfunction onRequestError (event: RA, errorMessage: string, recordName: string, socket: SocketWrapper, message: Message) {\n  const msg = {\n    topic: TOPIC.RECORD,\n    action: event,\n    originalAction: message.action,\n    name: recordName,\n    isError: true,\n  } as Message\n  if (message.isWriteAck) {\n    msg.isWriteAck = true\n  }\n  socket.sendMessage(msg)\n}\n\nfunction onSnapshotComplete (recordName: string, version: number, data: JSONObject, socket: SocketWrapper, message: Message) {\n  if (data) {\n    sendRecord(recordName, version, data, socket)\n  } else {\n    socket.sendMessage({\n      topic: TOPIC.RECORD,\n      action: RA.RECORD_NOT_FOUND,\n      originalAction: message.action,\n      name: message.name,\n      isError: true\n    })\n  }\n}\n\nfunction onHeadComplete (name: string, version: number, data: never, socketWrapper: SocketWrapper) {\n  socketWrapper.sendMessage({\n    topic: TOPIC.RECORD,\n    action: RA.HEAD_RESPONSE,\n    name,\n    version\n  })\n}\n\n/**\n* Sends the records data current data once done\n*/\nfunction sendRecord (recordName: string, version: number, data: any, socketWrapper: SocketWrapper) {\n  socketWrapper.sendMessage({\n    topic: TOPIC.RECORD,\n    action: RA.READ_RESPONSE,\n    name: recordName,\n    version,\n    parsedData: data,\n  })\n}\n"
  },
  {
    "path": "src/handlers/record/record-request.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport {spy} from 'sinon'\n\nimport * as C from '../../constants'\nimport { recordRequest } from './record-request'\n\nimport { getTestMocks } from '../../test/helper/test-mocks'\nimport { RECORD_ACTION } from '../../constants'\nimport * as testHelper from '../../test/helper/test-helper'\nimport { PromiseDelay } from '../../utils/utils';\n\ndescribe('record request', () => {\n  const completeCallback = spy()\n  const errorCallback = spy()\n\n  let testMocks\n  let client\n  let config\n  let services\n\n  const cacheData = { cache: true }\n  const storageData = { storage: true }\n\n  beforeEach(() => {\n    const options = testHelper.getDeepstreamOptions()\n    services = options.services\n    config = Object.assign({}, options.config, {\n      record: {\n        cacheRetrievalTimeout: 100,\n        storageRetrievalTimeout: 100,\n        storageExclusionPrefixes: ['dont-save']\n      }\n    })\n    services.cache.set('existingRecord', 1, cacheData, () => {})\n    services.storage.set('onlyExistsInStorage', 1, storageData, () => {})\n\n    testMocks = getTestMocks()\n    client = testMocks.getSocketWrapper('someUser')\n\n    completeCallback.resetHistory()\n    errorCallback.resetHistory()\n  })\n\n  describe('records are requested from cache and storage sequentially', () => {\n    it('requests a record that exists in a synchronous cache', () => {\n      services.cache.nextOperationWillBeSynchronous = true\n\n      recordRequest(\n        'existingRecord',\n        config,\n        services,\n        client.socketWrapper,\n        completeCallback,\n        errorCallback,\n        null\n      )\n\n      expect(services.cache.lastRequestedKey).to.equal('existingRecord')\n      expect(services.storage.lastRequestedKey).to.equal(null)\n\n      expect(completeCallback).to.have.been.calledWith(\n        'existingRecord', 1, cacheData, client.socketWrapper\n      )\n      expect(errorCallback).to.have.callCount(0)\n    })\n\n    it('requests a record that exists in an asynchronous cache', async () => {\n      services.cache.nextGetWillBeSynchronous = false\n\n      recordRequest(\n        'existingRecord',\n        config,\n        services,\n        client.socketWrapper,\n        completeCallback,\n        errorCallback,\n        null\n        )\n\n      await PromiseDelay(30)\n\n      expect(completeCallback).to.have.been.calledWith(\n        'existingRecord',\n        1,\n        cacheData,\n        client.socketWrapper\n        )\n      expect(errorCallback).to.have.callCount(0)\n      expect(services.cache.lastRequestedKey).to.equal('existingRecord')\n      expect(services.storage.lastRequestedKey).to.equal(null)\n    })\n\n    it('requests a record that doesn\\'t exists in a synchronous cache, but in storage', () => {\n      services.cache.nextGetWillBeSynchronous = true\n\n      recordRequest(\n        'onlyExistsInStorage',\n        config,\n        services,\n        client.socketWrapper,\n        completeCallback,\n        errorCallback,\n        null\n        )\n\n      expect(services.cache.lastRequestedKey).to.equal('onlyExistsInStorage')\n      expect(services.storage.lastRequestedKey).to.equal('onlyExistsInStorage')\n\n      expect(completeCallback).to.have.been.calledWith('onlyExistsInStorage', 1, storageData, client.socketWrapper)\n      expect(errorCallback).to.have.callCount(0)\n    })\n\n    it('requests a record that doesn\\'t exists in an asynchronous cache, but in asynchronous storage', async () => {\n      services.cache.nextGetWillBeSynchronous = false\n      services.storage.nextGetWillBeSynchronous = false\n\n      recordRequest(\n        'onlyExistsInStorage',\n        config,\n        services,\n        client.socketWrapper,\n        completeCallback,\n        errorCallback,\n        null\n        )\n\n      await PromiseDelay(75)\n\n      expect(services.cache.lastRequestedKey).to.equal('onlyExistsInStorage')\n      expect(services.storage.lastRequestedKey).to.equal('onlyExistsInStorage')\n\n      expect(errorCallback).to.have.callCount(0)\n      expect(completeCallback).to.have.been.calledWith(\n        'onlyExistsInStorage', 1, storageData, client.socketWrapper\n      )\n    })\n\n    it('returns null for non existent records', () => {\n      services.cache.nextGetWillBeSynchronous = true\n\n      recordRequest(\n        'doesNotExist',\n        config,\n        services,\n        client.socketWrapper,\n        completeCallback,\n        errorCallback,\n        null\n        )\n\n      expect(completeCallback).to.have.been.calledWith('doesNotExist', -1, null, client.socketWrapper)\n      expect(errorCallback).to.have.callCount(0)\n\n      expect(services.cache.lastRequestedKey).to.equal('doesNotExist')\n      expect(services.storage.lastRequestedKey).to.equal('doesNotExist')\n    })\n\n    it('handles cache errors', () => {\n      services.cache.nextGetWillBeSynchronous = true\n      services.cache.nextOperationWillBeSuccessful = false\n\n      recordRequest(\n        'cacheError',\n        config,\n        services,\n        client.socketWrapper,\n        completeCallback,\n        errorCallback,\n        null\n        )\n\n      expect(errorCallback).to.have.been.calledWith(\n        RECORD_ACTION.RECORD_LOAD_ERROR,\n        'error while loading cacheError from cache:storageError',\n        'cacheError',\n        client.socketWrapper\n        )\n      expect(completeCallback).to.have.callCount(0)\n\n      expect(services.logger.logSpy).to.have.been.calledWith(\n        3, RECORD_ACTION[RECORD_ACTION.RECORD_LOAD_ERROR], 'error while loading cacheError from cache:storageError'\n      )\n      // expect(client.socketWrapper.socket.lastSendMessage).to.equal(\n      //   msg('R|E|RECORD_LOAD_ERROR|error while loading cacheError from cache:storageError+'\n      // ))\n    })\n\n    it('handles storage errors', () => {\n      services.cache.nextGetWillBeSynchronous = true\n      services.cache.nextOperationWillBeSuccessful = true\n      services.storage.nextGetWillBeSynchronous = true\n      services.storage.nextOperationWillBeSuccessful = false\n\n      recordRequest(\n        'storageError',\n        config,\n        services,\n        client.socketWrapper,\n        completeCallback,\n        errorCallback,\n        null\n        )\n\n      expect(errorCallback).to.have.been.calledWith(\n        RECORD_ACTION.RECORD_LOAD_ERROR,\n        'error while loading storageError from storage:storageError',\n        'storageError',\n        client.socketWrapper\n        )\n      expect(completeCallback).to.have.callCount(0)\n\n      expect(services.logger.logSpy).to.have.been.calledWith(3, RECORD_ACTION[RECORD_ACTION.RECORD_LOAD_ERROR], 'error while loading storageError from storage:storageError')\n      // expect(socketWrapper.socket.lastSendMessage).to.equal(msg('R|E|RECORD_LOAD_ERROR|error while loading storageError from storage:storageError+'))\n    })\n\n    describe('handles cache timeouts', () => {\n      beforeEach(() => {\n        config.record.cacheRetrievalTimeout = 1\n        services.cache.nextGetWillBeSynchronous = false\n        services.cache.nextOperationWillBeSuccessful = true\n      })\n\n      afterEach(() => {\n        config.cacheRetrievalTimeout = 10\n      })\n\n      it('sends a CACHE_RETRIEVAL_TIMEOUT message when cache times out', (done) => {\n        recordRequest(\n          'willTimeoutCache',\n          config,\n          services,\n          client.socketWrapper,\n          completeCallback,\n          errorCallback,\n          null\n        )\n\n        setTimeout(() => {\n          expect(errorCallback).to.have.been.calledWith(\n            C.RECORD_ACTION.CACHE_RETRIEVAL_TIMEOUT,\n            'willTimeoutCache',\n            'willTimeoutCache',\n            client.socketWrapper\n          )\n          expect(completeCallback).to.have.callCount(0)\n\n          // ignores update from cache that may occur afterwards\n          services.cache.triggerLastGetCallback(null, '{ data: \"value\" }')\n          expect(completeCallback).to.have.callCount(0)\n\n          done()\n        }, 1)\n      })\n    })\n\n    describe('handles storage timeouts', () => {\n      beforeEach(() => {\n        config.record.storageRetrievalTimeout = 1\n        services.cache.nextGetWillBeSynchronous = true\n        services.cache.nextOperationWillBeSuccessful = true\n        services.storage.nextGetWillBeSynchronous = false\n        services.storage.nextOperationWillBeSuccessful = true\n      })\n\n      it('sends a STORAGE_RETRIEVAL_TIMEOUT message when storage times out', async () => {\n        recordRequest(\n          'willTimeoutStorage',\n          config,\n          services,\n          client.socketWrapper,\n          completeCallback,\n          errorCallback,\n          null\n        )\n\n        await PromiseDelay(1)\n\n        expect(errorCallback).to.have.been.calledWith(\n          C.RECORD_ACTION.STORAGE_RETRIEVAL_TIMEOUT,\n          'willTimeoutStorage',\n          'willTimeoutStorage',\n          client.socketWrapper\n        )\n        expect(completeCallback).to.have.callCount(0)\n\n        // ignores update from storage that may occur afterwards\n        services.storage.triggerLastGetCallback(null, '{ data: \"value\" }')\n        expect(completeCallback).to.have.callCount(0)\n      })\n    })\n  })\n\n  describe('excluded records are not put into storage', () => {\n    beforeEach(() => {\n      services.cache.nextGetWillBeSynchronous = true\n      services.storage.nextGetWillBeSynchronous = true\n      services.storage.delete = spy()\n      services.storage.set('dont-save/1', 1, {}, () => {})\n    })\n\n    it('returns null when requesting a record that doesn\\'t exists in a synchronous cache, and is excluded from storage', () => {\n      recordRequest(\n        'dont-save/1',\n        config,\n        services,\n        client.socketWrapper,\n        completeCallback,\n        errorCallback,\n        null\n      )\n\n      expect(completeCallback).to.have.been.calledWith(\n        'dont-save/1',\n        -1,\n        null,\n        client.socketWrapper\n      )\n      expect(errorCallback).to.have.callCount(0)\n      expect(services.storage.lastRequestedKey).to.equal(null)\n    })\n  })\n\n  describe('promoting to cache can be disabled', () => {\n    beforeEach(() => {\n      services.cache.nextGetWillBeSynchronous = true\n      services.storage.nextGetWillBeSynchronous = true\n      services.cache.set = spy()\n      services.storage.set('dont-save/1', 1, {}, () => {})\n    })\n\n    it('doesnt call set on cache if promoteToCache is disabled', () => {\n      recordRequest(\n        'onlyExistsInStorage',\n        config,\n        services,\n        client.socketWrapper,\n        completeCallback,\n        errorCallback,\n        this,\n        null,\n        null,\n        false\n      )\n\n      expect(completeCallback).to.have.been.calledWith(\n        'onlyExistsInStorage',\n        1,\n        { storage: true },\n        client.socketWrapper\n      )\n      expect(services.cache.set).to.have.callCount(0)\n\n      expect(errorCallback).to.have.callCount(0)\n      expect(services.cache.lastRequestedKey).to.equal('onlyExistsInStorage')\n      expect(services.storage.lastRequestedKey).to.equal('onlyExistsInStorage')\n    })\n  })\n})\n"
  },
  {
    "path": "src/handlers/record/record-request.ts",
    "content": "import { SocketWrapper, DeepstreamServices, DeepstreamConfig } from '@deepstream/types'\nimport { Message, RECORD_ACTION } from '../../constants'\nimport { isExcluded } from '../../utils/utils'\n\ntype onCompleteCallback = (recordName: string, version: number, data: any, socket: SocketWrapper | null, message?: Message) => void\ntype onErrorCallback = (event: any, errorMessage: string, recordName: string, socket: SocketWrapper | null, message?: Message) => void\n\n/**\n * Sends an error to the socketWrapper that requested the\n * record\n */\nfunction sendError (\n  event: RECORD_ACTION, errorMessage: string, recordName: string, socketWrapper: SocketWrapper | null,\n  onError: onErrorCallback, services: DeepstreamServices, context: any, metaData?: any, message?: Message,\n): void {\n  services.logger.error(RECORD_ACTION[event], errorMessage, metaData)\n  if (message) {\n    onError.call(context, event, errorMessage, recordName, socketWrapper, message)\n  } else {\n    onError.call(context, event, errorMessage, recordName, socketWrapper)\n  }\n}\n\n/**\n * Callback for responses returned by the storage connector. The request will complete or error\n * here, if the record couldn't be found in storage no further attempts to retrieve it will be made\n */\nfunction onStorageResponse (\n  error: string | null, recordName: string, version: number, data: any, socketWrapper: SocketWrapper | null,\n  onComplete: onCompleteCallback, onError: onErrorCallback, services: DeepstreamServices, context: any,\n  metaData: any, promoteToCache: boolean, message?: Message\n): void {\n  if (error) {\n    sendError(\n      RECORD_ACTION.RECORD_LOAD_ERROR,\n      `error while loading ${recordName} from storage:${error}`,\n      recordName, socketWrapper, onError, services, context,\n      metaData, message\n    )\n  } else {\n    if (message) {\n      onComplete.call(context, recordName, version, data || null, socketWrapper, message)\n    } else {\n      onComplete.call(context, recordName, version, data || null, socketWrapper)\n    }\n\n    // Promote to cache is disabled when coming from the record transition\n    // since that might override the last set\n    if (data && promoteToCache) {\n      services.cache.set(recordName, version, data, () => {}, metaData)\n    }\n  }\n}\n\n/**\n * Callback for responses returned by the cache connector\n */\nfunction onCacheResponse (\n  error: string | null, recordName: string, version: number, data: any, socketWrapper: SocketWrapper | null,\n  onComplete: onCompleteCallback, onError: onErrorCallback, config: DeepstreamConfig, services: DeepstreamServices,\n  context: any, metaData: any, promoteToCache: boolean, message?: Message\n): void {\n  if (error) {\n    sendError(\n      RECORD_ACTION.RECORD_LOAD_ERROR,\n      `error while loading ${recordName} from cache:${error}`,\n      recordName, socketWrapper, onError, services, context,\n      metaData, message\n    )\n  } else if (data) {\n    if (message) {\n      onComplete.call(context, recordName, version, data, socketWrapper, message)\n    } else {\n      onComplete.call(context, recordName, version, data, socketWrapper)\n    }\n  } else if (!isExcluded(config.record.storageExclusionPrefixes, recordName)) {\n    let storageTimedOut = false\n    const storageTimeout = setTimeout(() => {\n      storageTimedOut = true\n      sendError(\n        RECORD_ACTION.STORAGE_RETRIEVAL_TIMEOUT,\n        recordName, recordName, socketWrapper,\n        onError, services, context, metaData,\n        message\n      )\n    }, config.record.storageRetrievalTimeout)\n\n    // tslint:disable-next-line:no-shadowed-variable\n    services.storage.get(recordName, (storageError, version, result) => {\n      if (!storageTimedOut) {\n        clearTimeout(storageTimeout)\n        onStorageResponse(\n          storageError, recordName, version!, result, socketWrapper, onComplete,\n          onError, services, context, metaData, promoteToCache, message\n        )\n      }\n    }, metaData)\n  } else {\n    if (message) {\n      onComplete.call(context, recordName, version, data, socketWrapper, message)\n    } else {\n      onComplete.call(context, recordName, version, data, socketWrapper)\n    }\n  }\n}\n\n/**\n * This function retrieves a single record from the cache or - if it isn't in the\n * cache - from storage. If it isn't there either it will notify its initiator\n * by passing null to onComplete (but not call onError).\n *\n * It also handles all the timeout and destruction steps around this operation\n */\nexport function recordRequest (\n  recordName: string,\n  config: DeepstreamConfig,\n  services: DeepstreamServices,\n  socketWrapper: SocketWrapper | null,\n  onComplete: onCompleteCallback,\n  onError: onErrorCallback,\n  context: any,\n  metaData?: any,\n  message?: Message,\n  promoteToCache: boolean = true\n): void {\n  let cacheTimedOut = false\n\n  const cacheTimeout = setTimeout(() => {\n    cacheTimedOut = true\n    sendError(\n      RECORD_ACTION.CACHE_RETRIEVAL_TIMEOUT,\n      recordName, recordName, socketWrapper,\n      onError, services, context, metaData,\n      message\n    )\n  }, config.record.cacheRetrievalTimeout)\n\n  services.cache.get(recordName, (error, version, data) => {\n    if (!cacheTimedOut) {\n      clearTimeout(cacheTimeout)\n      onCacheResponse(\n        error, recordName, version!, data!,\n        socketWrapper, onComplete, onError,\n        config, services, context, metaData,\n        promoteToCache, message\n      )\n    }\n  }, metaData)\n}\n\nexport function recordRequestBinding (config: DeepstreamConfig, services: DeepstreamServices, context: any, metaData: any) {\n  return function (recordName: string, socketWrapper: SocketWrapper, onComplete: onCompleteCallback, onError: onErrorCallback, message?: Message) {\n    recordRequest (recordName, config, services, socketWrapper, onComplete, onError, context, metaData, message)\n  }\n}\n"
  },
  {
    "path": "src/handlers/record/record-transition.spec.ts",
    "content": "import 'mocha'\n\nimport * as M from './test-messages'\nimport * as C from '../../constants'\nimport { getTestMocks } from '../../test/helper/test-mocks'\nimport * as testHelper from '../../test/helper/test-helper'\nimport { RecordTransition } from './record-transition'\nimport { PromiseDelay } from '../../utils/utils';\n\ndescribe('RecordTransition', () => {\n  let services\n  let config\n  // let socketWrapper\n  let recordTransition\n  let testMocks\n  let client\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    client = testMocks.getSocketWrapper()\n\n    const options = testHelper.getDeepstreamOptions()\n    services = options.services\n    config = options.config\n\n    recordTransition = new RecordTransition(M.recordUpdate.name, config, services, testMocks.recordHandler)\n\n  })\n\n  afterEach(() => {\n    client.socketWrapperMock.verify()\n    testMocks.recordHandlerMock.verify()\n  })\n\n  it('sends write acknowledgement with sync cache and async storage', async () => {\n    const message: C.RecordWriteMessage = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'random-name',\n      correlationId: '30',\n      data: 'somedata',\n      isWriteAck: true,\n      version: -1,\n      parsedData: { name: 'somedata' }\n    }\n\n    services.storage.nextOperationWillBeSuccessful = true\n    services.storage.nextOperationWillBeSynchronous = false\n\n    services.cache.nextOperationWillBeSuccessful = true\n    services.cache.nextOperationWillBeSynchronous = true\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.RECORD_ACTION.WRITE_ACKNOWLEDGEMENT,\n        name: message.name,\n        correlationId: message.correlationId,\n        isWriteAck: true\n      })\n\n    recordTransition.add(client.socketWrapper, message, true)\n\n    // Wait for the async callback to fire\n    await PromiseDelay(60)\n  })\n})\n\n/*\ndescribe.skip('record transitions', () => {\n  let services\n  let config\n  let socketWrapper\n  let recordTransition\n  let testMocks\n  let client\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    client = testMocks.getSocketWrapper()\n\n    const options = testHelper.getDeepstreamOptions()\n    services = options.services\n    config = options.config\n\n    recordTransition = new RecordTransition(M.recordUpdate.name, config, services, testMocks.recordHandler)\n\n    services.cache.set('some-record', M.recordData, () => {})\n  })\n\n  afterEach(() => {\n    client.socketWrapperMock.verify()\n    testMocks.recordHandlerMock.verify()\n  })\n\n  it('retrieves the empty record', () => {\n    testMocks.recordHandlerMock\n      .expects('broadcastUpdate')\n      .once()\n      .withExactArgs(M.recordUpdate.name, M.recordUpdate, false, client.socketWrapper)\n\n    testMocks.recordHandlerMock\n      .expects('transitionComplete')\n      .once()\n      .withExactArgs(M.recordUpdate.name)\n\n    recordTransition.add(client.socketWrapper, Object.assign({}, M.recordUpdate))\n  })\n\n  it('adds an update to the queue', () => {\n    services.cache.nextGetWillBeSynchronous = false\n\n    expect(recordTransition.steps.length).to.equal(0)\n    recordTransition.add(client.socketWrapper, M.recordUpdate)\n    expect(recordTransition.steps.length).to.equal(1)\n  })\n\n  it('adds a message with invalid data to the queue', () => {\n    const invalidMessage = {\n      topic: C.TOPIC.RECORD,\n      action: C.ACTIONS.UPDATE,\n      name: 'bob',\n      version: 1,\n      data: '{ b ]'\n    }\n\n    client.socketWrapperMock\n      .expects('sendError')\n      .once()\n      .withExactArgs(invalidMessage, C.EVENT.INVALID_MESSAGE_DATA)\n\n    recordTransition.add(client.socketWrapper, invalidMessage)\n  })\n\n  it('adds a message with null data to the queue', () => {\n    const invalidMessage = {\n      topic: C.TOPIC.RECORD,\n      action: C.ACTIONS.UPDATE,\n      name: 'bob',\n      version: 1,\n      data: 'null'\n    }\n\n    client.socketWrapperMock\n      .expects('sendError')\n      .once()\n      .withExactArgs(invalidMessage, C.EVENT.INVALID_MESSAGE_DATA)\n\n    recordTransition.add(client.socketWrapper, invalidMessage)\n  })\n\n  it('adds a message with string data to the queue', () => {\n    const invalidMessage = {\n      topic: C.TOPIC.RECORD,\n      action: C.ACTIONS.UPDATE,\n      name: 'bob',\n      version: 1,\n      data: 'This is a string'\n    }\n\n    client.socketWrapperMock\n      .expects('sendError')\n      .once()\n      .withExactArgs(invalidMessage, C.EVENT.INVALID_MESSAGE_DATA)\n\n    recordTransition.add(client.socketWrapper, invalidMessage)\n  })\n\n  it('adds a message with numeric data to the queue', () => {\n    const invalidMessage = {\n      topic: C.TOPIC.RECORD,\n      action: C.ACTIONS.UPDATE,\n      name: 'bob',\n      version: 1,\n      data: '1234'\n    }\n\n    client.socketWrapperMock\n      .expects('sendError')\n      .once()\n      .withExactArgs(invalidMessage, C.EVENT.INVALID_MESSAGE_DATA)\n\n    recordTransition.add(client.socketWrapper, invalidMessage)\n  })\n\n  it.skip('retrieves the empty record', (done) => {\n    recordRequestMockCallback({ _v: 0, _d: { firstname: 'Egon' } })\n\n    expect(recordTransition._record).to.deep.equal({ _v: 1, _d: { firstname: 'Egon' } })\n    expect(services.cache.completedSetOperations).to.equal(0)\n\n    const check = setInterval(() => {\n      if (services.cache.completedSetOperations === 1) {\n        expect(recordHandlerMock._$broadcastUpdate).to.have.been.calledWith('recordName', patchMessage, false, socketWrapper)\n        expect(recordHandlerMock._$transitionComplete).to.have.callCount(0)\n        expect(recordTransition._record).to.deep.equal({ _v: 2, _d: { lastname: 'Peterson' } })\n        clearInterval(check)\n        done()\n      }\n    }, 1)\n  })\n\n  it.skip('receives a patch message whilst the transition is in progress', () => {\n    expect(recordHandlerMock._$transitionComplete).to.have.callCount(0)\n    recordTransition.add(socketWrapper, 3, patchMessage2)\n  })\n\n  it('returns hasVersion for 1,2 and 3', () => {\n    services.cache.nextOperationWillBeSynchronous = false\n\n    recordTransition.add(client.socketWrapper, M.recordUpdate)\n\n    expect(recordTransition.hasVersion(0)).to.equal(true)\n    expect(recordTransition.hasVersion(1)).to.equal(true)\n    expect(recordTransition.hasVersion(2)).to.equal(true)\n    expect(recordTransition.hasVersion(3)).to.equal(true)\n    expect(recordTransition.hasVersion(4)).to.equal(true)\n    expect(recordTransition.hasVersion(5)).to.equal(true)\n    expect(recordTransition.hasVersion(6)).to.equal(true)\n    expect(recordTransition.hasVersion(7)).to.equal(false)\n    expect(recordTransition.hasVersion(8)).to.equal(false)\n  })\n\n  it.skip('processes a queue', (done) => {\n    testMocks.recordHandlerMock\n      .expects('broadcastUpdate')\n      .once()\n      .withExactArgs(M.recordPatch.name, M.recordPatch, false, client.socketWrapper)\n\n    testMocks.recordHandlerMock\n      .expects('broadcastUpdate')\n      .once()\n      .withExactArgs(M.recordUpdate.name, M.recordUpdate, false, client.socketWrapper)\n\n    testMocks.recordHandlerMock\n      .expects('transitionComplete')\n      .once()\n      .withExactArgs(M.recordPatch.name)\n\n    client.socketWrapperMock\n      .expects('sendError')\n      .never()\n\n    recordTransition.add(client.socketWrapper, M.recordPatch)\n    recordTransition.add(client.socketWrapper, Object.assign({}, M.recordUpdate, { version: M.recordUpdate.version + 1 }))\n    recordTransition.add(client.socketWrapper, Object.assign({}, M.recordUpdate, { version: M.recordUpdate.version + 2 }))\n  })\n\n  describe('does not store excluded data', () => {\n\n    it('retrieves the empty record', () => {\n      expect(recordHandlerMock._$broadcastUpdate).to.have.callCount(0)\n      expect(recordHandlerMock._$transitionComplete).to.have.callCount(0)\n      recordRequestMockCallback()\n      expect(recordHandlerMock._$broadcastUpdate).to.have.been.calledWith('no-storage/1', patchMessage, false, socketWrapper)\n      expect(recordHandlerMock._$transitionComplete).to.have.been.calledWith('no-storage/1')\n    })\n\n    it('does not store transition in storage', (done) => {\n      const check = setInterval(() => {\n        if (services.storage.completedSetOperations === 0) {\n          clearInterval(check)\n          done()\n        }\n      }, 1)\n    })\n  })\n\n  describe('destroys a transition between steps', () => {\n    const secondPatchMessage = { topic: 'RECORD', action: 'P', data: ['recordName', 2, 'firstname', 'SEgon'] }\n\n    before(() => {\n      createRecordTransition()\n      services.cache.nextOperationWillBeSynchronous = false\n    })\n\n    it('adds a patch to the queue', () => {\n      expect(() => {\n        recordTransition.add(socketWrapper, 2, secondPatchMessage)\n        expect(recordTransition.hasVersion(2)).to.equal(true)\n      }).not.to.throw()\n    })\n  })\n\n  describe('tries to set a record, but both cache and storage fail', () => {\n    before(() => {\n      createRecordTransition()\n      services.cache.nextOperationWillBeSynchronous = true\n      services.cache.nextOperationWillBeSuccessful = false\n      services.storage.nextOperationWillBeSuccessful = false\n      recordRequestMockCallback()\n    })\n\n    it('logged an error', () => {\n      expect(services.logger.logSpy).to.have.been.calledWith(3, 'RECORD_UPDATE_ERROR', 'storageError')\n    })\n  })\n\n  describe('destroys the transition', () => {\n    before(() => {\n      createRecordTransition()\n      services.cache.nextOperationWillBeSynchronous = false\n    })\n\n    it('destroys the transition', (done) => {\n      recordTransition.destroy()\n      expect(recordTransition.isDestroyed).to.equal(true)\n      expect(recordTransition.steps).to.equal(null)\n      setTimeout(() => {\n        // just leave this here to make sure no error is thrown when the\n        // record request returns after 30ms\n        done()\n      }, 50)\n    })\n\n    it('calls destroy a second time without causing problems', () => {\n      recordTransition.destroy()\n      expect(recordTransition.isDestroyed).to.equal(true)\n      expect(recordTransition.steps).to.equal(null)\n    })\n  })\n\n  describe('recordRequest returns an error', () => {\n    before(() => {\n      createRecordTransition()\n      services.cache.nextOperationWillBeSynchronous = false\n    })\n\n    it('receives an error', () => {\n      expect(socketWrapper.socket.lastSendMessage).to.equal(null)\n      recordRequestMockCallback('errorMsg', true)\n      expect(services.logger.logSpy).to.have.been.calledWith(3, 'RECORD_UPDATE_ERROR', 'errorMsg')\n      expect(socketWrapper.socket.lastSendMessage).to.equal(msg('R|E|RECORD_UPDATE_ERROR|1+'))\n    })\n  })\n\n  describe('recordRequest returns null', () => {\n    before(() => {\n      createRecordTransition()\n      services.cache.nextOperationWillBeSynchronous = false\n    })\n\n    it('receives a non existant error', () => {\n      expect(socketWrapper.socket.lastSendMessage).to.equal(null)\n      recordRequestMockCallback(null)\n      expect(services.logger.logSpy).to.have.been.calledWith(3, 'RECORD_UPDATE_ERROR', 'Received update for non-existant record recordName')\n      expect(socketWrapper.socket.lastSendMessage).to.equal(msg('R|E|RECORD_UPDATE_ERROR|1+'))\n    })\n  })\n\n  describe('handles invalid message data', () => {\n    const invalidPatchMessage = { topic: 'RECORD', action: 'P', data: ['recordName', 2, 'somepath', 'O{\"invalid\":\"json'] }\n\n    before(() => {\n      createRecordTransition('recordName', invalidPatchMessage)\n      services.cache.nextOperationWillBeSynchronous = false\n    })\n\n    it('receives an error', () => {\n      expect(socketWrapper.socket.lastSendMessage).to.contain(msg('R|E|INVALID_MESSAGE_DATA|'))\n    })\n  })\n\n  describe.skip('transition version conflicts', () => {\n    const socketMock1 = new SocketMock()\n    const socketMock2 = new SocketMock()\n    const socketMock3 = new SocketMock()\n    const socketWrapper1 = new SocketWrapper(socketMock1)\n    const socketWrapper2 = new SocketWrapper(socketMock2)\n    const socketWrapper3 = new SocketWrapper(socketMock3)\n    const patchMessage2 = { topic: 'RECORD', action: 'P', data: ['recordName', 2, 'firstname', 'SEgon'] }\n\n    before(() => {\n      createRecordTransition('recordName')\n      services.cache.nextOperationWillBeSynchronous = false\n    })\n\n    it('gets a version exist error on two seperate updates but does not send error', () => {\n      recordTransition.add(socketWrapper1, 2, patchMessage2)\n\n      recordTransition.sendVersionExists({ sender: socketWrapper1, version: 1, message: patchMessage })\n      recordTransition.sendVersionExists({ sender: socketWrapper2, version: 1, message: patchMessage2 })\n\n      expect(socketMock1.lastSendMessage).to.equal(null)\n      expect(socketMock2.lastSendMessage).to.equal(null)\n      expect(socketMock3.lastSendMessage).to.equal(null)\n    })\n\n    it('sends version exists error once record request is completed is retrieved', () => {\n      recordRequestMockCallback({ _v: 1, _d: { lastname: 'Kowalski' } })\n\n      expect(socketMock1.lastSendMessage).to.equal(msg('R|E|VERSION_EXISTS|recordName|1|{\"lastname\":\"Kowalski\"}+'))\n      expect(socketMock2.lastSendMessage).to.equal(msg('R|E|VERSION_EXISTS|recordName|1|{\"lastname\":\"Kowalski\"}+'))\n      expect(socketMock3.lastSendMessage).to.equal(null)\n    })\n\n    it('immediately sends version exists when record is already loaded', () => {\n      socketMock1.lastSendMessage = null\n      socketMock2.lastSendMessage = null\n      socketMock3.lastSendMessage = null\n\n      recordTransition.sendVersionExists({ sender: socketWrapper3, version: 1, message: patchMessage })\n\n      expect(socketMock1.lastSendMessage).to.equal(null)\n      expect(socketMock2.lastSendMessage).to.equal(null)\n      expect(socketMock3.lastSendMessage).to.equal(msg('R|E|VERSION_EXISTS|recordName|2|{\"lastname\":\"Kowalski\",\"firstname\":\"Egon\"}+'))\n    })\n\n    it('destroys the transition', (done) => {\n      recordTransition.destroy()\n      expect(recordTransition.isDestroyed).to.equal(true)\n      expect(recordTransition.steps).to.equal(null)\n      setTimeout(() => {\n        // just leave this here to make sure no error is thrown when the\n        // record request returns after 30ms\n        done()\n      }, 50)\n    })\n  })\n})\n*/\n"
  },
  {
    "path": "src/handlers/record/record-transition.ts",
    "content": "import { setValue as setPathValue } from '../../utils/json-path'\nimport RecordHandler from './record-handler'\nimport { recordRequest } from './record-request'\nimport { RecordWriteMessage, TOPIC, RECORD_ACTION, Message } from '../../constants'\nimport { SocketWrapper, DeepstreamConfig, DeepstreamServices, MetaData, EVENT } from '@deepstream/types'\nimport { isOfType, isExcluded } from '../../utils/utils'\n\ninterface Step {\n  message: RecordWriteMessage\n  sender: SocketWrapper\n}\n\nexport class RecordTransition {\n/**\n * This class manages one or more simultanious updates to the data of a record.\n * But: Why does that need to be so complicated and why does this class even exist?\n *\n * In short: Cross-network concurrency. If your record is written to by a single datasource\n * and consumed by many clients, this class is admittably overkill, but if deepstream is used to\n * build an app that allows many users to collaboratively edit the same dataset, sooner or later\n * two of them will do so at the same time and clash.\n *\n * Every deepstream record therefor has a  number that's incremented with every change.\n * Every client sends this version number along with the changed data. If no other update has\n * been received for the same version in the meantime, the update is accepted and not much more\n * happens.\n *\n * If, however, another clients was able to send its updated version before this update was\n * processed, the second (later) update for the same version number is rejected and the issuing\n * client is notified of the change.\n *\n * The client is then expected to merge its changes on top of the new version and re-issue the\n * update message.\n *\n * Please note: For performance reasons, succesful updates are not explicitly acknowledged.\n *\n * It's this class' responsibility to manage this. It will be created when an update arrives and\n * only exist as long as it takes to apply it and make sure that no subsequent updates for the\n * same version are requested.\n *\n * Once the update is applied it will notify the record-handler to broadcast the\n * update and delete the instance of this class.\n */\n public isDestroyed: boolean = false\n\n private steps: Step[] = []\n private version: number = -1\n private data: any = null\n private currentStep: Step | null = null\n private recordRequestMade: boolean = false\n private existingVersions: Step[] = []\n private lastVersion: number | null = null\n private readonly writeAckSockets = new Map<SocketWrapper, { [correlationId: string]: number }>()\n private pendingStorageWrites: number = 0\n private pendingCacheWrites: number = 0\n\n  constructor (private name: string, private config: DeepstreamConfig, private services: DeepstreamServices, private recordHandler: RecordHandler, private readonly metaData: MetaData) {\n    this.onCacheSetResponse = this.onCacheSetResponse.bind(this)\n    this.onStorageSetResponse = this.onStorageSetResponse.bind(this)\n    this.onRecord = this.onRecord.bind(this)\n    this.onFatalError = this.onFatalError.bind(this)\n  }\n\n/**\n * Checks if a specific version number is already processed or\n * queued for processing\n */\n  public hasVersion (version: number): boolean {\n    if (this.lastVersion === null) {\n      return false\n    }\n    return version !== -1 && version <= this.lastVersion\n  }\n\n/**\n * Send version exists error if the record has been already loaded, else\n * store the version exists error to send to the sockerWrapper once the\n * record is loaded\n */\n  public sendVersionExists (step: Step): void {\n    const socketWrapper = step.sender\n    if (this.data) {\n      socketWrapper.sendMessage({\n        topic: TOPIC.RECORD,\n        action: RECORD_ACTION.VERSION_EXISTS,\n        originalAction: step.message.action,\n        name: this.name,\n        version: this.version,\n        parsedData: this.data,\n        isWriteAck: step.message.isWriteAck,\n        correlationId: step.message.correlationId\n      })\n\n      this.services.logger.warn(\n        RECORD_ACTION[RECORD_ACTION.VERSION_EXISTS],\n        `${socketWrapper.userId} tried to update record ${this.name} to version ${step.message.version} but it already was ${this.version}`,\n        this.metaData,\n      )\n    } else {\n      this.existingVersions.push({\n        sender: socketWrapper,\n        message: step.message,\n      })\n    }\n  }\n\n/**\n * Adds a new step (either an update or a patch) to the record. The step\n * will be queued or executed immediatly if the queue is empty\n *\n * This method will also retrieve the current record's data when called\n * for the first time\n */\n  public add (socketWrapper: SocketWrapper, message: RecordWriteMessage, upsert: boolean = false): void {\n    const version = message.version\n    const update = {\n      message,\n      sender: socketWrapper,\n    }\n\n    const result = socketWrapper.parseData(message)\n    if (result instanceof Error) {\n      socketWrapper.sendMessage({\n        topic: TOPIC.RECORD,\n        action: RECORD_ACTION.INVALID_MESSAGE_DATA,\n        data: message.data\n      })\n      return\n    }\n\n    if (message.action === RECORD_ACTION.UPDATE) {\n      if (!isOfType(message.parsedData, 'object') && !isOfType(message.parsedData, 'array')) {\n        socketWrapper.sendMessage({ ...message,\n          action: RECORD_ACTION.INVALID_MESSAGE_DATA,\n          originalAction: message.action,\n        })\n        return\n      }\n    }\n\n    if (this.lastVersion !== null && version > this.lastVersion + 1) {\n      socketWrapper.sendMessage({ ...message,\n        action: RECORD_ACTION.INVALID_VERSION,\n        originalAction: message.action,\n      })\n      return\n    }\n\n    if (this.lastVersion !== null && this.lastVersion !== version - 1) {\n      this.sendVersionExists(update)\n      return\n    }\n\n    if (version !== -1) {\n      this.lastVersion = version\n    }\n    this.steps.push(update)\n\n    if (this.recordRequestMade === false) {\n      this.recordRequestMade = true\n      recordRequest(\n        this.name,\n        this.config,\n        this.services,\n        socketWrapper,\n        (r: string, v: number, d: any) => this.onRecord(v, d, upsert),\n        this.onCacheRequestError,\n        this,\n        this.metaData,\n        undefined,\n        false\n      )\n    } else if (this.steps.length === 1) {\n      this.next()\n    }\n  }\n\n/**\n * Destroys the instance\n */\n  public destroy (error?: string | null): void {\n    if (this.isDestroyed) {\n      return\n    }\n\n    if (error) {\n      this.sendWriteAcknowledgementErrors(error.toString())\n\n      // send message in order to alert current message sender that the operation failed\n      if (this.currentStep && this.currentStep.sender && !this.currentStep.sender.isRemote) {\n        this.currentStep.sender.sendMessage({\n            topic: TOPIC.RECORD,\n            action: RECORD_ACTION.RECORD_UPDATE_ERROR,\n            name: this.currentStep.message.name,\n            isError: true\n        })\n      }\n    }\n\n    this.recordHandler.transitionComplete(this.name)\n    this.isDestroyed = true\n  }\n\n/**\n * Callback for successfully retrieved records\n */\n  private onRecord (version: number, data: any, upsert: boolean) {\n    if (data === null) {\n      if (!upsert) {\n        this.onFatalError(`Received update for non-existant record ${this.name}`)\n        return\n      }\n      this.data = {}\n      this.version = 0\n    } else {\n      this.version = version\n      this.data = data\n    }\n    this.flushVersionExists()\n    this.next()\n  }\n\n/**\n * Once the record is loaded this method is called recoursively\n * for every step in the queue of pending updates.\n *\n * It will apply every patch or update and - once done - either\n * call itself to process the next one or destroy the RecordTransition\n * of the queue has been drained\n */\n  private next (): void {\n    if (this.isDestroyed === true) {\n      return\n    }\n\n    if (this.data === null) {\n      return\n    }\n\n    const currentStep = this.steps.shift()\n    if (!currentStep) {\n      this.destroy(null)\n      return\n    }\n\n    this.currentStep = currentStep\n    let message = currentStep.message\n\n    if (message.version === -1) {\n      message = Object.assign({}, message, { version: this.version + 1 })\n      currentStep.message = message\n    }\n\n    if (message.version > this.version + 1) {\n      currentStep.sender.sendMessage({ ...message,\n        action: RECORD_ACTION.INVALID_VERSION,\n        originalAction: currentStep.message.action,\n        version: this.version\n      })\n      return\n    }\n\n    if (this.version !== message.version - 1) {\n      this.sendVersionExists(currentStep)\n      this.next()\n      return\n    }\n\n    this.version = message.version\n\n    if (message.path) {\n      setPathValue(this.data, message.path, message.parsedData)\n    } else {\n      this.data = message.parsedData\n    }\n\n    /*\n   * Please note: saving to storage is called first to allow for synchronous cache\n   * responses to destroy the transition, it is however not on the critical path\n   * and the transition will continue straight away, rather than wait for the storage response\n   * to be returned.\n   *\n   * If the storage response is asynchronous and write acknowledgement is enabled, the transition\n   * will not be destroyed until writing to storage is finished\n   */\n    if (!isExcluded(this.config.record.storageExclusionPrefixes, this.name)) {\n      this.pendingStorageWrites++\n      if (message.isWriteAck) {\n        this.setUpWriteAcknowledgement(message, this.currentStep.sender)\n        this.services.storage.set(this.name, this.version, this.data, (error) => this.onStorageSetResponse(error, this.currentStep!.sender, message), this.metaData)\n      } else {\n        this.services.storage.set(this.name, this.version, this.data, this.onStorageSetResponse, this.metaData)\n      }\n    }\n\n    this.pendingCacheWrites++\n    if (message.isWriteAck) {\n      this.setUpWriteAcknowledgement(message, this.currentStep.sender)\n      this.services.cache.set(this.name, this.version, this.data, (error) => this.onCacheSetResponse(error, this.currentStep!.sender, message), this.metaData)\n    } else {\n      this.services.cache.set(this.name, this.version, this.data, this.onCacheSetResponse, this.metaData)\n    }\n  }\n\n  private setUpWriteAcknowledgement (message: Message, socketWrapper: SocketWrapper) {\n    const correlationId = message.correlationId as string\n    const response = this.writeAckSockets.get(socketWrapper)\n    if (!response) {\n      this.writeAckSockets.set(socketWrapper, { [correlationId]: 1 })\n      return\n    }\n    response[correlationId] = response[correlationId] ? response[correlationId] + 1 : 1\n    this.writeAckSockets.set(socketWrapper, response)\n  }\n\n/**\n * Send all the stored version exists errors once the record has been loaded.\n */\n  private flushVersionExists (): void {\n    for (let i = 0; i < this.existingVersions.length; i++) {\n      this.sendVersionExists(this.existingVersions[i])\n    }\n    this.existingVersions = []\n  }\n\n  private handleWriteAcknowledgement (error: string | null, socketWrapper: SocketWrapper, originalMessage: Message) {\n    const correlationId = originalMessage.correlationId as string\n    const response = this.writeAckSockets.get(socketWrapper)\n    if (!response) {\n      return\n    }\n\n    response[correlationId]--\n    if (response[correlationId] === 0) {\n      socketWrapper.sendMessage({\n        topic: TOPIC.RECORD,\n        action: RECORD_ACTION.WRITE_ACKNOWLEDGEMENT,\n        name: originalMessage.name,\n        correlationId,\n        isWriteAck: true\n      })\n      delete response[correlationId]\n    }\n\n    if (Object.keys(response).length === 0) {\n      this.writeAckSockets.delete(socketWrapper)\n    }\n  }\n\n  private onCacheRequestError (error: string) {\n    const errorMessage = `Cache retrieval error, nuking record transition for ${this.name}, ${error}`\n    this.services.logger.error(EVENT.ERROR, errorMessage)\n    this.destroy(errorMessage)\n  }\n\n/**\n * Callback for responses returned by cache.set(). If an error\n * is returned the queue will be destroyed, otherwise\n * the update will be broadcast to other subscribers and the\n * next step invoked\n */\n  private onCacheSetResponse (error: string | null, socketWrapper?: SocketWrapper, message?: Message): void {\n    if (this.currentStep === null) {\n      const errorMessage = `Cache results received without a valid step in record transition for ${this.name}`\n      this.services.logger.error(EVENT.ERROR, errorMessage)\n      this.destroy(errorMessage)\n      return\n    }\n\n    if (message && socketWrapper) {\n      this.handleWriteAcknowledgement(error, socketWrapper, message)\n    }\n    if (error) {\n      this.onFatalError(error)\n    } else if (this.isDestroyed === false) {\n      const { isWriteAck, correlationId } = this.currentStep.message\n\n      // // Delete values that should not be broadcast\n      this.currentStep.message.isWriteAck = false\n      delete this.currentStep.message.correlationId // TODO: Optimise\n      this.recordHandler.broadcastUpdate(\n          this.name,\n          this.currentStep.message,\n          false,\n          this.currentStep.sender,\n      )\n      // Restore the message for other callers to use\n      this.currentStep.message.isWriteAck = isWriteAck\n      this.currentStep.message.correlationId = correlationId\n      this.next()\n\n    } else if (this.steps.length === 0 && this.pendingCacheWrites === 0 && this.pendingStorageWrites === 0) {\n      this.destroy(null)\n    }\n  }\n\n/**\n * Callback for responses returned by storage.set()\n */\n  private onStorageSetResponse (error: string | null, socketWrapper?: SocketWrapper, message?: Message): void {\n    if (message && socketWrapper) {\n      this.handleWriteAcknowledgement(error, socketWrapper, message)\n    }\n\n    if (error) {\n      this.onFatalError(error)\n    } else if (\n      this.steps.length === 0 && this.pendingCacheWrites === 0 && this.pendingStorageWrites === 0\n    ) {\n      this.destroy(null)\n    }\n  }\n\n/**\n * Sends all write acknowledgement messages at the end of a transition\n */\n  private sendWriteAcknowledgementErrors (errorMessage: string) {\n    for (const [socketWrapper, pendingWrites] of this.writeAckSockets) {\n      for (const correlationId in pendingWrites) {\n        socketWrapper.sendMessage({\n          topic: TOPIC.RECORD, action: RECORD_ACTION.RECORD_UPDATE_ERROR, correlationId, isWriteAck: true, isError: true\n        })\n      }\n    }\n    this.writeAckSockets.clear()\n  }\n\n/**\n * Generic error callback. Will destroy the queue and notify the senders of all pending\n * transitions\n */\n  private onFatalError (error: string): void {\n    if (this.isDestroyed === true) {\n      return\n    }\n    this.services.logger.error(RECORD_ACTION[RECORD_ACTION.RECORD_UPDATE_ERROR], error.toString(), this.metaData)\n\n    for (let i = 0; i < this.steps.length; i++) {\n      if (!this.steps[i].sender.isRemote) {\n        this.steps[i].sender.sendMessage({\n          topic: TOPIC.RECORD,\n          action: RECORD_ACTION.RECORD_UPDATE_ERROR,\n          name: this.steps[i].message.name,\n          isError: true,\n          isWriteAck: this.steps[i].message.isWriteAck,\n          correlationId: this.steps[i].message.correlationId\n        })\n      }\n    }\n\n    this.destroy(error)\n  }\n\n}\n"
  },
  {
    "path": "src/handlers/record/record-write-acknowledgement.spec.ts",
    "content": "/*\n'use strict'\n\nconst M = require('./messages')\nimport * as C from '../../src/constants'\nimport { getTestMocks } from '../test-helper/test-mocks'\nconst testHelper = require('../test-helper/test-helper')\n\nconst RecordTransition = require('../../src/record/record-transition').default\n\nconst sinon = require('sinon')\n\ndescribe('record write acknowledgement', () => {\n  let config\n  let services\n  let socketWrapper\n  let recordTransition\n  let testMocks\n  let client\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    client = testMocks.getSocketWrapper()\n\n    const options = testHelper.getDeepstreamOptions()\n    config = options.config\n    services = options.services\n\n    recordTransition = new RecordTransition(M.recordUpdate.name, config, services, testMocks.recordHandler)\n  })\n\n  afterEach(() => {\n    client.socketWrapperMock.verify()\n  })\n\n  it('sends write success to socket', () => {\n    client.socketWrapperMock\n      .expects('sendError')\n      .never()\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs(M.writeAck, true)\n\n    recordTransition.add(client.socketWrapper, M.recordUpdateWithAck, true)\n  })\n\n  it('sends write failure to socket', () => {\n    services.storage.nextOperationWillBeSuccessful = false\n\n    client.socketWrapperMock\n      .expects('sendError')\n      .once()\n      .withExactArgs(M.recordUpdateWithAck, C.EVENT.RECORD_UPDATE_ERROR)\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: C.TOPIC.RECORD,\n        action: C.ACTIONS.WRITE_ACKNOWLEDGEMENT,\n        name: M.recordUpdateWithAck.name,\n        data: [[-1], C.EVENT.RECORD_LOAD_ERROR]\n      }, true)\n\n    recordTransition.add(client.socketWrapper, M.recordUpdateWithAck, true)\n  })\n\n  it.skip('multiple write acknowledgements', () => {\n      // processes the next step in the queue\n    const check = setInterval(() => {\n      if (services.storage.completedSetOperations === 2) {\n        expect(recordHandlerMock._$broadcastUpdate).to.have.been.calledWith('recordName', patchMessage2, false, socketWrapper2)\n        expect(recordHandlerMock._$transitionComplete).to.have.callCount(0)\n        expect(recordTransition._record).to.deep.equal({ _v: 3, _d: { firstname: 'Lana', lastname: 'Kowalski' } })\n        clearInterval(check)\n        done()\n      }\n    }, 1)\n\n    // processes the final step in the queue\n    if (services.storage.completedSetOperations === 3) {\n      expect(recordHandlerMock._$broadcastUpdate).to.have.been.calledWith('recordName', patchMessage3, false, socketWrapper)\n      expect(recordHandlerMock._$transitionComplete).to.have.callCount(1)\n    }\n\n    // stored each transition in storage\n    // services.storage.completedSetOperations === 3\n\n    // sent write acknowledgement to each client\n    expect(socketWrapper.socket.lastSendMessage).to.equal(msg('R|WA|recordName|[1,3]|L+'))\n    expect(socketWrapper2.socket.lastSendMessage).to.equal(msg('R|WA|recordName|[2]|L+'))\n  })\n\n  it.skip('transition version conflicts gets a version exist error on record retrieval', () => {\n  //   services.storage.nextOperationWillBeSynchronous = false\n  //   recordTransition.add(socketWrapper, 2, updateMessage)\n    expect(socketWrapper.socket.lastSendMessage).to.equal(null)\n    recordRequestMockCallback({ _v: 1, _d: { lastname: 'Kowalski' } })\n    expect(socketWrapper.socket.lastSendMessage).to.equal(msg('R|E|VERSION_EXISTS|recordName|1|{\"lastname\":\"Kowalski\"}|{\"writeSuccess\":true}+'))\n  })\n})\n*/\n"
  },
  {
    "path": "src/handlers/record/test-messages.ts",
    "content": "import * as C from '../../constants'\n\nexport const deletionMsg = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.DELETE,\n  name: 'someRecord'\n}\n\nexport const deletionSuccessMsg = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.DELETE_SUCCESS,\n  name: 'someRecord'\n}\n\nexport const anotherDeletionMsg = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.DELETE,\n  name: 'no-storage/1'\n}\n\nexport const anotherDeletionSuccessMsg = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.DELETE_SUCCESS,\n  name: 'no-storage/1'\n}\n\nexport const subscribeCreateAndReadMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.SUBSCRIBECREATEANDREAD,\n  names: ['some-record']\n}\n\nexport const readResponseMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.READ_RESPONSE,\n  name: 'some-record',\n  version: 0,\n  parsedData: {}\n}\n\nexport const subscribeCreateAndReadPermissionErrorMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.MESSAGE_PERMISSION_ERROR,\n  originalAction: C.RECORD_ACTION.SUBSCRIBECREATEANDREAD,\n  names: ['some-record']\n}\n\nexport const subscribeCreateAndReadDeniedMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.MESSAGE_DENIED,\n  originalAction: C.RECORD_ACTION.SUBSCRIBECREATEANDREAD,\n  names: ['some-record']\n}\n\nexport const subscribeMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.SUBSCRIBE,\n  names: ['some-record']\n}\n\nexport const unsubscribeMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.UNSUBSCRIBE,\n  names: ['some-record']\n}\n\nexport const recordSnapshotMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.READ,\n  name: 'some-record'\n}\n\nexport const recordHeadMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.HEAD,\n  name: 'some-record'\n}\n\nexport const recordHeadResponseMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.HEAD_RESPONSE,\n  name: 'some-record'\n}\n\nexport const recordData = { name: 'Kowalski' }\nexport const recordVersion = 5\n\nexport const recordUpdate = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.UPDATE,\n  name: 'some-record',\n  version: recordVersion + 1,\n  parsedData: recordData,\n  isWriteAck: false\n}\n\nexport const recordUpdateWithAck = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.UPDATE,\n  name: 'some-record',\n  version: -1,\n  parsedData: recordData,\n  isWriteAck: true\n}\n\nexport const recordPatch = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.PATCH,\n  name: 'some-record',\n  version: recordVersion + 1,\n  path: 'lastname',\n  parsedData: 'Egon',\n  isWriteAck: false\n}\n\nexport const recordPatchWithAck = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.PATCH,\n  name: 'some-record',\n  version: 4,\n  path: 'lastname',\n  parsedData: 'Egon',\n  isWriteAck: true\n}\n\nexport const recordDelete = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.DELETE,\n  name: 'some-record'\n}\n\nexport const createAndUpdate = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.CREATEANDUPDATE,\n  name: 'some-record',\n  version:  -1,\n  parsedData: recordData,\n  isWriteAck: false\n}\n\nexport const listenAcceptMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.LISTEN_ACCEPT,\n  name: 'record/.*',\n  subscription: 'record/A'\n}\n\nexport const listenRejectMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.LISTEN_REJECT,\n  name: 'record/.*',\n  subscription: 'record/A'\n}\n\nexport const unlistenMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.UNLISTEN,\n  name: 'record/.*'\n}\n\nexport const listenMessage = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.LISTEN,\n  name: 'record/.*'\n}\n\nexport const writeAck = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.WRITE_ACKNOWLEDGEMENT,\n  name: 'some-record',\n  data: [[-1], null]\n}\n\nexport const notify = {\n  topic: C.TOPIC.RECORD,\n  action: C.RECORD_ACTION.NOTIFY,\n  names: ['record1', 'record2']\n}\n"
  },
  {
    "path": "src/handlers/rpc/rpc-handler.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as C from '../../constants'\nimport RpcHandler from './rpc-handler'\n\nimport * as testHelper from '../../test/helper/test-helper'\nimport { getTestMocks } from '../../test/helper/test-mocks'\nimport { RpcProxy } from './rpc-proxy'\n\nconst options = testHelper.getDeepstreamOptions()\nconst config = options.config\nconst services = options.services\n\ndescribe('the rpcHandler routes events correctly', () => {\n  let testMocks\n  let rpcHandler\n\n  let requestor\n  let provider\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    rpcHandler = new RpcHandler(config, services, testMocks.subscriptionRegistry)\n    requestor = testMocks.getSocketWrapper('requestor', {}, { color: 'blue' })\n    provider = testMocks.getSocketWrapper('provider')\n  })\n\n  afterEach(() => {\n    testMocks.subscriptionRegistryMock.verify()\n    requestor.socketWrapperMock.verify()\n    provider.socketWrapperMock.verify()\n  })\n\n  it('routes subscription messages', () => {\n    const subscriptionMessage = {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.PROVIDE,\n      names: ['someRPC'],\n      correlationId: '123'\n    }\n    testMocks.subscriptionRegistryMock\n      .expects('subscribeBulk')\n      .once()\n      .withExactArgs(subscriptionMessage, provider.socketWrapper)\n\n    rpcHandler.handle(provider.socketWrapper, subscriptionMessage)\n  })\n\n  describe('when receiving a request', () => {\n    const requestMessage = {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.REQUEST,\n      name: 'addTwo',\n      correlationId: 1234,\n      data: '{\"numA\":5, \"numB\":7}'\n    }\n\n    const acceptMessage = {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.ACCEPT,\n      name: 'addTwo',\n      correlationId: 1234\n    }\n\n    const responseMessage = {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.RESPONSE,\n      name: 'addTwo',\n      correlationId: 1234,\n      data: '12'\n    }\n\n    const errorMessage = {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.REQUEST_ERROR,\n      isError: true,\n      name: 'addTwo',\n      correlationId: 1234,\n      data: 'ErrorOccured'\n    }\n\n    beforeEach(() => {\n      testMocks.subscriptionRegistryMock\n        .expects('getLocalSubscribers')\n        .once()\n        .withExactArgs('addTwo')\n        .returns([provider.socketWrapper])\n    })\n\n    it('forwards it to a provider', () => {\n      provider.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs(Object.assign({\n          requestorData: { color: 'blue' },\n          requestorName: 'requestor'\n        }, requestMessage))\n\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n    })\n\n    it('accepts first accept', () => {\n      requestor.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs(acceptMessage)\n\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n      rpcHandler.handle(provider.socketWrapper, acceptMessage)\n    })\n\n    it('errors when recieving more than one ack', () => {\n      provider.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.RPC,\n          action: C.RPC_ACTION.MULTIPLE_ACCEPT,\n          name: requestMessage.name,\n          correlationId: requestMessage.correlationId\n        })\n\n      provider.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs(Object.assign({\n          requestorData: { color: 'blue' },\n          requestorName: 'requestor'\n        }, requestMessage))\n\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n      rpcHandler.handle(provider.socketWrapper, acceptMessage)\n      rpcHandler.handle(provider.socketWrapper, acceptMessage)\n    })\n\n    it('gets a response', () => {\n      requestor.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs(responseMessage)\n\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n      rpcHandler.handle(provider.socketWrapper, responseMessage)\n    })\n\n    it('replies with an error to additonal responses', () => {\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n      rpcHandler.handle(provider.socketWrapper, responseMessage)\n\n      provider.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.RPC,\n          action: C.RPC_ACTION.INVALID_RPC_CORRELATION_ID,\n          originalAction: responseMessage.action,\n          name: responseMessage.name,\n          correlationId: responseMessage.correlationId,\n          isError: true\n        })\n\n      rpcHandler.handle(provider.socketWrapper, responseMessage)\n    })\n\n    it('gets an error', () => {\n      requestor.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs(errorMessage)\n\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n      rpcHandler.handle(provider.socketWrapper, errorMessage)\n    })\n\n    it('replies with an error after the first message', () => {\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n      rpcHandler.handle(provider.socketWrapper, errorMessage)\n\n      provider.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.RPC,\n          action: C.RPC_ACTION.INVALID_RPC_CORRELATION_ID,\n          originalAction: errorMessage.action,\n          name: errorMessage.name,\n          correlationId: errorMessage.correlationId,\n          isError: true\n        })\n\n      rpcHandler.handle(provider.socketWrapper, errorMessage)\n    })\n\n    it('supports multiple RPCs in quick succession', () => {\n      testMocks.subscriptionRegistryMock\n        .expects('getLocalSubscribers')\n        .exactly(49)\n        .withExactArgs('addTwo')\n        .returns([provider.socketWrapper])\n\n      expect(() => {\n        for (let i = 0; i < 50; i++) {\n          rpcHandler.handle(requestor.socketWrapper, requestMessage)\n        }\n      }).not.to.throw()\n    })\n\n    it('times out if no ack is received', (done) => {\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n\n      requestor.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.RPC,\n          action: C.RPC_ACTION.ACCEPT_TIMEOUT,\n          name: requestMessage.name,\n          correlationId: requestMessage.correlationId\n        })\n\n      setTimeout(done, config.rpc.ackTimeout * 2)\n    })\n\n    it('times out if response is not received in time', (done) => {\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n      rpcHandler.handle(provider.socketWrapper, acceptMessage)\n\n      requestor.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.RPC,\n          action: C.RPC_ACTION.RESPONSE_TIMEOUT,\n          name: requestMessage.name,\n          correlationId: requestMessage.correlationId\n        })\n\n      setTimeout(done, config.rpc.responseTimeout * 2)\n    })\n\n    // Should an Ack for a non existant rpc should error?\n    it.skip('ignores ack message if it arrives after response', (done) => {\n      provider.socketWrapperMock\n        .expects('sendMessage')\n        .twice()\n\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n      rpcHandler.handle(provider.socketWrapper, responseMessage)\n\n      setTimeout(() => {\n        rpcHandler.handle(provider.socketWrapper, acceptMessage)\n        done()\n      }, 30)\n    })\n\n    it('doesn\\'t throw error on response after timeout', (done) => {\n      rpcHandler.handle(requestor.socketWrapper, requestMessage)\n      rpcHandler.handle(provider.socketWrapper, acceptMessage)\n\n      requestor.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.RPC,\n          action: C.RPC_ACTION.RESPONSE_TIMEOUT,\n          name: requestMessage.name,\n          correlationId: requestMessage.correlationId\n        })\n\n      setTimeout(() => {\n        provider.socketWrapperMock\n          .expects('sendMessage')\n          .once()\n          .withExactArgs({\n            topic: C.TOPIC.RPC,\n            action: C.RPC_ACTION.INVALID_RPC_CORRELATION_ID,\n            originalAction: responseMessage.action,\n            name: responseMessage.name,\n            correlationId: responseMessage.correlationId,\n            isError: true\n          })\n        rpcHandler.handle(provider.socketWrapper, responseMessage)\n        done()\n      }, 30)\n    })\n  })\n\n  describe.skip('rpc handler returns alternative providers for the same rpc', () => {\n    let providerForA1\n    let providerForA2\n    let providerForA3\n    let usedProviders\n\n    before(() => {\n      testMocks = getTestMocks()\n      providerForA1 = testMocks.getSocketWrapper('a1')\n      providerForA2 = testMocks.getSocketWrapper('a2')\n      providerForA3 = testMocks.getSocketWrapper('a3')\n\n      rpcHandler = new RpcHandler(config, services, testMocks.subscriptionRegistry)\n\n      testMocks.subscriptionRegistryMock\n          .expects('getLocalSubscribers')\n          .once()\n          .withExactArgs('rpcA')\n          .returns([\n            providerForA1.socketWrapper\n          ])\n\n      rpcHandler.handle(providerForA1.socketWrapper, {\n        topic: C.TOPIC.RPC,\n        action: C.RPC_ACTION.REQUEST,\n        name: 'rpcA',\n        correlationId: '1234',\n        data: 'U'\n      })\n      usedProviders = [providerForA1.socketWrapper]\n\n      testMocks.subscriptionRegistryMock\n          .expects('getLocalSubscribers')\n          .exactly(5)\n          .withExactArgs('rpcA')\n          .returns([\n            providerForA2.socketWrapper,\n            providerForA3.socketWrapper\n          ])\n\n      testMocks.subscriptionRegistryMock\n          .expects('getAllRemoteServers')\n          .thrice()\n          .withExactArgs('rpcA')\n          .returns(['random-server-1', 'random-server-2'])\n    })\n\n    after(() => {\n      testMocks.subscriptionRegistryMock.verify()\n    })\n\n    it('gets alternative rpc providers', () => {\n      let alternativeProvider\n      // first proxy\n      alternativeProvider = rpcHandler.getAlternativeProvider('rpcA', '1234')\n      expect(alternativeProvider).not.to.equal(null)\n      expect(alternativeProvider instanceof RpcProxy).to.equal(false)\n      expect(usedProviders.indexOf(alternativeProvider)).to.equal(-1)\n      // second provider\n      alternativeProvider = rpcHandler.getAlternativeProvider('rpcA', '1234')\n      expect(alternativeProvider).not.to.equal(null)\n      expect(alternativeProvider instanceof RpcProxy).to.equal(false)\n      expect(usedProviders.indexOf(alternativeProvider)).to.equal(-1)\n      // remote provider\n      alternativeProvider = rpcHandler.getAlternativeProvider('rpcA', '1234')\n      expect(alternativeProvider).not.to.equal(null)\n      expect(alternativeProvider instanceof RpcProxy).to.equal(true)\n      expect(usedProviders.indexOf(alternativeProvider)).to.equal(-1)\n      // remote alternative provider\n      alternativeProvider = rpcHandler.getAlternativeProvider('rpcA', '1234')\n      expect(alternativeProvider).not.to.equal(null)\n      expect(alternativeProvider instanceof RpcProxy).to.equal(true)\n      expect(usedProviders.indexOf(alternativeProvider)).to.equal(-1)\n      // null\n      alternativeProvider = rpcHandler.getAlternativeProvider('rpcA', '1234')\n      expect(alternativeProvider).to.equal(null)\n    })\n  })\n\n  describe('the rpcHandler uses requestor fields correctly', () => {\n    beforeEach(() => {\n      testMocks = getTestMocks()\n      requestor = testMocks.getSocketWrapper('requestor', {}, { bestLanguage: 'not BF' })\n      provider = testMocks.getSocketWrapper('provider')\n      testMocks.subscriptionRegistryMock\n          .expects('getLocalSubscribers')\n          .once()\n          .withExactArgs('addTwo')\n          .returns([provider.socketWrapper])\n    })\n\n    afterEach(() => {\n      testMocks.subscriptionRegistryMock.verify()\n      requestor.socketWrapperMock.verify()\n      provider.socketWrapperMock.verify()\n    })\n\n    const requestMessage = {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.REQUEST,\n      name: 'addTwo',\n      correlationId: 1234,\n      data: '{\"numA\":5, \"numB\":7}'\n    }\n\n    for (const nameAvailable of [true, false]) {\n      for (const dataAvailable of [true, false]) {\n        const name = `name=${nameAvailable} data=${dataAvailable}`\n        it(name, () => {\n          config.rpc.provideRequestorName = nameAvailable\n          config.rpc.provideRequestorData = dataAvailable\n\n          const expectedMessage = Object.assign({}, requestMessage)\n          if (nameAvailable) {\n            Object.assign(expectedMessage, { requestorName: 'requestor' })\n          }\n          if (dataAvailable) {\n            Object.assign(expectedMessage, { requestorData: { bestLanguage: 'not BF' } })\n          }\n          provider.socketWrapperMock\n              .expects('sendMessage')\n              .once()\n              .withExactArgs(expectedMessage)\n\n          rpcHandler = new RpcHandler(config, services, testMocks.subscriptionRegistry)\n          rpcHandler.handle(requestor.socketWrapper, requestMessage)\n        })\n    }\n  }\n\n    // it ('overwrites fake requestorName and fake requestorData', () => {\n    //   config.provideRPCRequestorDetails = true\n    //   config.RPCRequestorNameTerm = null\n    //   config.RPCRequestorDataTerm = null\n    //\n    //   provider.socketWrapperMock\n    //     .expects('sendMessage')\n    //     .once()\n    //     .withExactArgs(Object.assign({\n    //       requestorName: 'requestor',\n    //       requestorData: { bestLanguage: 'not BF' }\n    //     }, requestMessage))\n    //\n    //   const fakeRequestMessage = Object.assign({\n    //     requestorName: 'evil-requestor',\n    //     requestorData: { bestLanguage: 'malbolge' }\n    //   }, requestMessage)\n    //   rpcHandler = new RpcHandler(config, services, testMocks.subscriptionRegistry)\n    //   rpcHandler.handle(requestor.socketWrapper, fakeRequestMessage)\n    // })\n\n  })\n\n})\n"
  },
  {
    "path": "src/handlers/rpc/rpc-handler.ts",
    "content": "import { PARSER_ACTION, RPC_ACTION, TOPIC, RPCMessage, BulkSubscriptionMessage, STATE_REGISTRY_TOPIC } from '../../constants'\nimport { getRandomIntInRange } from '../../utils/utils'\nimport { Rpc } from './rpc'\nimport { RpcProxy } from './rpc-proxy'\nimport { SimpleSocketWrapper, DeepstreamConfig, DeepstreamServices, SocketWrapper, SubscriptionRegistry, Handler } from '@deepstream/types'\n\ninterface RpcData {\n  providers: Set<SimpleSocketWrapper>,\n  servers: Set<string> | null,\n  rpc: Rpc\n}\n\nexport default class RpcHandler extends Handler<RPCMessage> {\n  private subscriptionRegistry: SubscriptionRegistry\n  private rpcs: Map<string, RpcData> = new Map()\n\n  /**\n  * Handles incoming messages for the RPC Topic.\n  */\n  constructor (private config: DeepstreamConfig, private services: DeepstreamServices, subscriptionRegistry?: SubscriptionRegistry, private metaData?: any) {\n    super()\n\n    this.subscriptionRegistry =\n      subscriptionRegistry || services.subscriptions.getSubscriptionRegistry(TOPIC.RPC, STATE_REGISTRY_TOPIC.RPC_SUBSCRIPTIONS)\n\n    this.subscriptionRegistry.setAction('NOT_SUBSCRIBED', RPC_ACTION.NOT_PROVIDED)\n    this.subscriptionRegistry.setAction('MULTIPLE_SUBSCRIPTIONS', RPC_ACTION.MULTIPLE_PROVIDERS)\n    this.subscriptionRegistry.setAction('SUBSCRIBE', RPC_ACTION.PROVIDE)\n    this.subscriptionRegistry.setAction('UNSUBSCRIBE', RPC_ACTION.UNPROVIDE)\n  }\n\n  /**\n  * Main interface. Handles incoming messages\n  * from the message distributor\n  */\n  public handle (socketWrapper: SocketWrapper, message: RPCMessage, originServerName: string): void {\n    if (socketWrapper === null) {\n      this.onRemoteRPCMessage(message, originServerName)\n      return\n    }\n\n    if (message.action === RPC_ACTION.REQUEST) {\n      this.makeRpc(socketWrapper, message, false)\n      return\n   }\n\n    if (message.action === RPC_ACTION.PROVIDE) {\n      this.subscriptionRegistry.subscribeBulk(message as BulkSubscriptionMessage, socketWrapper)\n      return\n    }\n\n    if (message.action === RPC_ACTION.UNPROVIDE) {\n      this.subscriptionRegistry.unsubscribeBulk(message as BulkSubscriptionMessage, socketWrapper)\n      return\n    }\n\n    if (\n      message.action === RPC_ACTION.RESPONSE ||\n      message.action === RPC_ACTION.REJECT ||\n      message.action === RPC_ACTION.ACCEPT ||\n      message.action === RPC_ACTION.REQUEST_ERROR\n    ) {\n      const rpcData = this.rpcs.get(message.correlationId)\n      if (rpcData) {\n        this.services.logger.debug(\n          RPC_ACTION[message.action],\n          `name: ${message.name} with correlation id: ${message.correlationId} from ${socketWrapper.userId}`,\n          this.metaData\n        )\n        rpcData.rpc.handle(message)\n        return\n      }\n      this.services.logger.warn(\n        RPC_ACTION[RPC_ACTION.INVALID_RPC_CORRELATION_ID],\n        `name: ${message.name} with correlation id: ${message.correlationId}`,\n        this.metaData\n      )\n      socketWrapper.sendMessage({\n        topic: TOPIC.RPC,\n        action: RPC_ACTION.INVALID_RPC_CORRELATION_ID,\n        originalAction: message.action,\n        name: message.name,\n        correlationId: message.correlationId,\n        isError: true\n      })\n      return\n    }\n\n    /*\n    *  RESPONSE-, ERROR-, REJECT- and ACK messages from the provider are processed\n    * by the Rpc class directly\n    */\n    this.services.logger.warn(PARSER_ACTION[PARSER_ACTION.UNKNOWN_ACTION], message.action.toString(), this.metaData)\n  }\n\n  /**\n  * This method is called by Rpc to reroute its request\n  *\n  * If a provider is temporarily unable to service a request, it can reject it. Deepstream\n  * will then try to reroute it to an alternative provider. Finding an alternative provider\n  * happens in this method.\n  *\n  * Initially, deepstream will look for a local provider that hasn't been used by the RPC yet.\n  * If non can be found, it will go through the currently avaiblable remote providers and try\n  * find one that hasn't been used yet.\n  *\n  * If a remote provider couldn't be found or all remote-providers have been tried already\n  * this method will return null - which in turn will prompt the RPC to send a NO_RPC_PROVIDER\n  * error to the client\n  */\n  public getAlternativeProvider (rpcName: string, correlationId: string): SimpleSocketWrapper | null {\n    const rpcData =  this.rpcs.get(correlationId)\n\n    if (!rpcData) {\n      // log error\n      return null\n    }\n\n    const subscribers = Array.from(this.subscriptionRegistry.getLocalSubscribers(rpcName))\n    let index = getRandomIntInRange(0, subscribers.length)\n\n    for (let n = 0; n < subscribers.length; ++n) {\n      if (!rpcData.providers.has(subscribers[index])) {\n        rpcData.providers.add(subscribers[index])\n        return subscribers[index]\n      }\n      index = (index + 1) % subscribers.length\n    }\n\n    if (!rpcData.servers) {\n      return null\n    }\n\n    const servers = this.subscriptionRegistry.getAllRemoteServers(rpcName)\n\n    index = getRandomIntInRange(0, servers.length)\n    for (let n = 0; n < servers.length; ++n) {\n      if (!rpcData.servers.has(servers[index])) {\n        rpcData.servers.add(servers[index])\n        return new RpcProxy(this.config, this.services, servers[index], this.metaData)\n      }\n      index = (index + 1) % servers.length\n    }\n\n    return null\n  }\n\n  /**\n  * Executes a RPC. If there are clients connected to\n  * this deepstream instance that can provide the rpc, it\n  * will be routed to a random one of them, otherwise it will be routed\n  * to the message connector\n  */\n  private makeRpc (socketWrapper: SimpleSocketWrapper, message: RPCMessage, isRemote: boolean): void {\n    const rpcName = message.name\n    const correlationId = message.correlationId\n\n    this.services.logger.debug(\n      RPC_ACTION[RPC_ACTION.REQUEST],\n      `name: ${rpcName} with correlation id: ${correlationId} from ${socketWrapper.userId}`,\n      this.metaData\n    )\n\n    const subscribers = Array.from(this.subscriptionRegistry.getLocalSubscribers(rpcName))\n    const provider = subscribers[getRandomIntInRange(0, subscribers.length)]\n\n    if (provider) {\n      this.rpcs.set(correlationId, {\n        providers: new Set([provider]),\n        servers: isRemote ? null : new Set(),\n        rpc: new Rpc(this, socketWrapper, provider, this.config, message),\n      })\n      return\n    }\n\n    if (isRemote) {\n      socketWrapper.sendMessage({\n        topic: TOPIC.RPC,\n        action: RPC_ACTION.NO_RPC_PROVIDER,\n        name: rpcName,\n        correlationId\n      })\n      return\n    }\n\n    this.makeRemoteRpc(socketWrapper, message)\n  }\n\n  /**\n  * Callback to remoteProviderRegistry.getProviderProxy()\n  *\n  * If a remote provider is available this method will route the rpc to it.\n  *\n  * If no remote provider could be found this class will return a\n  * NO_RPC_PROVIDER error to the requestor. The RPC won't continue from\n  * thereon\n  */\n  public makeRemoteRpc (requestor: SimpleSocketWrapper, message: RPCMessage): void {\n    const rpcName = message.name\n    const correlationId = message.correlationId\n\n    const servers = this.subscriptionRegistry.getAllRemoteServers(rpcName)\n    const server = servers[getRandomIntInRange(0, servers.length)]\n\n    if (server) {\n      const rpcProxy = new RpcProxy(this.config, this.services, server, this.metaData)\n      this.rpcs.set(correlationId, {\n        providers: new Set(),\n        servers: new Set(),\n        rpc: new Rpc(this, requestor, rpcProxy, this.config, message),\n      })\n      return\n    }\n\n    this.rpcs.delete(correlationId)\n\n    this.services.logger.warn(\n      RPC_ACTION[RPC_ACTION.NO_RPC_PROVIDER],\n      `name: ${message.name} with correlation id: ${message.correlationId}`,\n      this.metaData\n    )\n\n    if (!requestor.isRemote) {\n      requestor.sendMessage({\n        topic: TOPIC.RPC,\n        action: RPC_ACTION.NO_RPC_PROVIDER,\n        name: rpcName,\n        correlationId\n      })\n    }\n  }\n\n  /**\n  * Callback for messages that are send directly to\n  * this deepstream instance.\n  *\n  * Please note: Private messages are generic, so the RPC\n  * specific ones need to be filtered out.\n  */\n  private onRemoteRPCMessage (msg: RPCMessage, originServerName: string): void {\n    if (msg.action === RPC_ACTION.REQUEST) {\n      const proxy = new RpcProxy(this.config, this.services, originServerName, this.metaData)\n      this.makeRpc(proxy, msg, true)\n      return\n    }\n\n    const rpcData = this.rpcs.get(msg.correlationId)\n\n    if (!rpcData) {\n      this.services.logger.warn(\n        RPC_ACTION[RPC_ACTION.INVALID_RPC_CORRELATION_ID],\n        `Message bus response for RPC that may have been destroyed: ${JSON.stringify(msg)}`,\n        this.metaData,\n      )\n      return\n    }\n\n    this.services.logger.debug(\n      RPC_ACTION[msg.action],\n      `name: ${msg.name} with correlation id: ${msg.correlationId} from remote server ${originServerName}`,\n      this.metaData\n    )\n\n    rpcData.rpc.handle(msg)\n  }\n\n  /**\n   * Called by the RPC with correlationId to destroy itself\n   * when lifecycle is over.\n   */\n  public onRPCDestroyed (correlationId: string): void {\n     this.rpcs.delete(correlationId)\n  }\n}\n"
  },
  {
    "path": "src/handlers/rpc/rpc-proxy.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as C from '../../constants'\nimport * as testHelper from '../../test/helper/test-helper'\nimport { RpcProxy } from './rpc-proxy'\n\nconst options = testHelper.getDeepstreamOptions()\nconst config = options.config\nconst services = options.services\n\ndescribe.skip('rpcProxy proxies calls from and to the remote receiver', () => {\n  let rpcProxy\n\n  beforeEach(() => {\n    rpcProxy = new RpcProxy(config, services, 'serverNameA')\n  })\n\n  it('manipulates the message before sending', () => {\n    rpcProxy.send({\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.ACCEPT,\n      name: 'a',\n      correlationId: 1234\n    })\n\n    expect(options.message.lastDirectSentMessage).to.deep.equal({\n      serverName: 'serverNameA',\n      topic: 'PRIVATE/P',\n      message: {\n        topic: C.TOPIC.RPC,\n        action: C.RPC_ACTION.ACCEPT,\n        name: 'a',\n        correlationId: 1234\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "src/handlers/rpc/rpc-proxy.ts",
    "content": "import { RPC_ACTION, RPCMessage, ParseResult } from '../../constants'\nimport { SimpleSocketWrapper, DeepstreamConfig, DeepstreamServices } from '@deepstream/types'\n\n/**\n * This class exposes an interface that mimicks the behaviour\n * of a SocketWrapper, connected to a local rpc provider, but\n * infact relays calls from and to the message connector - sneaky.\n */\nexport class RpcProxy implements SimpleSocketWrapper {\n  public socketType = 'RpcProxy'\n  public userId: string = 'remote server ' + this.remoteServer\n  public clientData = null\n  public serverData = null\n  public isRemote = true\n\n  constructor (config: DeepstreamConfig, private services: DeepstreamServices, private remoteServer: string, private  metaData: any) {\n  }\n\n  public sendAckMessage (message: RPCMessage): void {\n  }\n\n  /**\n  * Mimicks the SocketWrapper's send method, but expects a message object,\n  * instead of a string.\n  *\n  * Adds additional information to the message that enables the counterparty\n  * to identify the sender\n  */\n  public sendMessage (msg: RPCMessage): void {\n    this.services.clusterNode.sendDirect(this.remoteServer, msg, this.metaData)\n  }\n\n  /**\n  * Mimicks the SocketWrapper's sendError method.\n  * Sends an error on the specified topic. The\n  * action will automatically be set to ACTION.ERROR\n  */\n  public sendError (msg: RPCMessage, type: RPC_ACTION, errorMessage: string): void {\n    if (type === RPC_ACTION.RESPONSE_TIMEOUT) {\n      // by the time an RPC has timed out on this server, it has already timed out on the remote\n      // (and has been cleaned up) so no point sending\n      return\n    }\n    this.services.clusterNode.sendDirect(this.remoteServer, msg, this.metaData)\n  }\n\n  public parseMessage (serializedMessage: any): ParseResult[] {\n    throw new Error('Method not implemented.')\n  }\n}\n"
  },
  {
    "path": "src/handlers/rpc/rpc.ts",
    "content": "import { RPC_ACTION, TOPIC, RPCMessage, Message } from '../../constants'\nimport RpcHandler from './rpc-handler'\nimport { SimpleSocketWrapper, DeepstreamConfig } from '@deepstream/types'\n\n/**\n * Relays a remote procedure call from a requestor to a provider and routes\n * the providers response to the requestor. Provider might either be a locally\n * connected SocketWrapper or a RpcProviderProxy that forwards messages\n * from a remote provider within the network\n */\nexport class Rpc {\n  private message: Message\n  private correlationId: string\n  private rpcName: string\n  private isAccepted: boolean = false\n  private acceptTimeout: any\n  private responseTimeout: any\n\n  /**\n  */\n  constructor (private rpcHandler: RpcHandler, private requestor: SimpleSocketWrapper, private provider: SimpleSocketWrapper, private config: DeepstreamConfig, message: RPCMessage) {\n    this.rpcName = message.name\n    this.correlationId = message.correlationId\n    this.message = { ...message, ...this.getRequestor(requestor) }\n\n    this.setProvider(provider)\n  }\n\n  private getRequestor (requestor: SimpleSocketWrapper): any {\n    const provideAll = this.config.rpc.provideRequestorName && this.config.rpc.provideRequestorData\n    switch (true) {\n      case provideAll:\n        return {\n          requestorName: requestor.userId,\n          requestorData: requestor.clientData\n        }\n      case this.config.rpc.provideRequestorName:\n        return { requestorName: requestor.userId }\n      case this.config.rpc.provideRequestorData:\n        return { requestorData: requestor.clientData }\n      default:\n        return {}\n    }\n  }\n\n  /**\n  * Processor for incoming messages from the RPC provider. The\n  * RPC provider is expected to send two messages,\n  *\n  * RPC|A|REQ|<rpcName>|<correlationId>\n  *\n  * and\n  *\n  * RPC|RES|<rpcName>|<correlationId|[<data>]\n  *\n  * Both of these messages will just be forwarded directly\n  * to the requestor\n  */\n  public handle (message: RPCMessage): void {\n    if (message.correlationId !== this.correlationId) {\n      return\n    }\n\n    if (message.action === RPC_ACTION.ACCEPT) {\n      this.handleAccept(message)\n      return\n    }\n\n    if (message.action === RPC_ACTION.REJECT || message.action === RPC_ACTION.NO_RPC_PROVIDER) {\n      this.reroute()\n      return\n    }\n\n    if (message.action === RPC_ACTION.RESPONSE || message.action === RPC_ACTION.REQUEST_ERROR) {\n      this.requestor.sendMessage(message)\n      this.destroy()\n    }\n  }\n\n  /**\n  * Destroys this Rpc, either because its completed or because a timeout has occured\n  */\n  public destroy (): void {\n    clearTimeout(this.acceptTimeout)\n    clearTimeout(this.responseTimeout)\n    this.rpcHandler.onRPCDestroyed(this.correlationId)\n  }\n\n  /**\n  * By default, a RPC is the communication between one requestor\n  * and one provider. If the original provider however rejects\n  * the request, deepstream will try to re-route it to another provider.\n  *\n  * This happens in the reroute method. This method will query\n  * the rpc-handler for an alternative provider and - if it has\n  * found one - call this method to replace the provider and re-do\n  * the second leg of the rpc\n  */\n  private setProvider (provider: SimpleSocketWrapper): void {\n    clearTimeout(this.acceptTimeout)\n    clearTimeout(this.responseTimeout)\n\n    this.provider = provider\n    this.acceptTimeout = setTimeout(this.onAcceptTimeout.bind(this), this.config.rpc.ackTimeout)\n    this.responseTimeout = setTimeout(this.onResponseTimeout.bind(this), this.config.rpc.responseTimeout)\n    this.provider.sendMessage(this.message)\n  }\n\n  /**\n  * Handles rpc acknowledgement messages from the provider.\n  * If more than one Ack is received an error will be returned\n  * to the provider\n  */\n  private handleAccept (message: RPCMessage) {\n    if (this.isAccepted === true) {\n      this.provider.sendMessage({\n        topic: TOPIC.RPC,\n        action: RPC_ACTION.MULTIPLE_ACCEPT,\n        name: this.message.name,\n        correlationId: this.message.correlationId\n      })\n      return\n    }\n\n    clearTimeout(this.acceptTimeout)\n    this.isAccepted = true\n    this.requestor.sendMessage(message)\n  }\n\n  /**\n  * This method handles rejection messages from the current provider. If\n  * a provider is temporarily unable to serve a request, it can reject it\n  * and deepstream will try to reroute to an alternative provider\n  *\n  * If no alternative provider could be found, this method will send a NO_RPC_PROVIDER\n  * error to the client and destroy itself\n  */\n  public reroute (): void {\n    const alternativeProvider = this.rpcHandler.getAlternativeProvider(this.rpcName, this.correlationId)\n\n    if (alternativeProvider) {\n      this.setProvider(alternativeProvider)\n      return\n    }\n\n    this.requestor.sendMessage({\n      topic: TOPIC.RPC,\n      action: RPC_ACTION.NO_RPC_PROVIDER,\n      name: this.message.name,\n      correlationId: this.message.correlationId\n    })\n    this.destroy()\n  }\n\n  /**\n  * Callback if the accept message hasn't been returned\n  * in time by the provider\n  */\n  private onAcceptTimeout (): void {\n    this.requestor.sendMessage({\n      topic: TOPIC.RPC,\n      action: RPC_ACTION.ACCEPT_TIMEOUT,\n      name: this.message.name,\n      correlationId: this.message.correlationId\n    })\n    this.destroy()\n  }\n\n  /**\n  * Callback if the response message hasn't been returned\n  * in time by the provider\n  */\n  public onResponseTimeout (): void {\n    this.requestor.sendMessage({\n      topic: TOPIC.RPC,\n      action: RPC_ACTION.RESPONSE_TIMEOUT,\n      name: this.message.name,\n      correlationId: this.message.correlationId\n    })\n    this.destroy()\n  }\n}\n"
  },
  {
    "path": "src/jif/jif-handler.spec.ts",
    "content": "import { expect } from 'chai'\n\nconst JIFHandler = require('./jif-handler').default\nimport LoggerMock from '../test/mock/logger-mock'\nimport { RECORD_ACTION, TOPIC, EVENT_ACTION, RPC_ACTION, PRESENCE_ACTION } from '../constants';\n\ndescribe('JIF Handler', () => {\n  let jifHandler\n  const logger = new LoggerMock()\n  before(() => {\n    jifHandler = new JIFHandler({ logger })\n  })\n  describe('fromJIF', () => {\n\n    it('should reject an empty message', () => {\n      const jif = {}\n      const result = jifHandler.fromJIF(jif)\n\n      expect(result.success).to.equal(false)\n      expect(result.error).to.be.a('string')\n    })\n\n    it('should reject a message that is not an object', () => {\n      const jifs = [\n        [{\n          topic: 'event',\n          eventName: 'time/berlin',\n          action: 'emit',\n          data: { a: ['b', 2] }\n        }],\n        '{\"topic\":\"event\",\"eventName\":\"time/berlin\",\"action\":\"emit\",\"data\":{\"a\":[\"b\",2]}}',\n        23,\n        null,\n        undefined\n      ]\n      const results = jifs.map((jif) => jifHandler.fromJIF(jif))\n\n      results.forEach((result, i) => {\n        expect(result.success).to.equal(false, i.toString())\n        expect(result.error).to.match(/must be object/, i.toString())\n      })\n    })\n\n    describe('events', () => {\n      it('should create an event message for a well formed jit event', () => {\n        const jif = {\n          topic: 'event',\n          eventName: 'time/berlin',\n          action: 'emit',\n          data: { a: ['b', 2] }\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.done).to.equal(true)\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.EVENT)\n        expect(message.action).to.equal(EVENT_ACTION.EMIT)\n        expect(message.name).to.equal('time/berlin')\n        expect(message.parsedData).to.deep.equal({ a: ['b', 2] })\n      })\n\n      it('should support events without payloads', () => {\n        const jif = {\n          topic: 'event',\n          eventName: 'time/berlin',\n          action: 'emit'\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.EVENT)\n        expect(message.action).to.equal(EVENT_ACTION.EMIT)\n        expect(message.name).to.equal('time/berlin')\n        expect(message.parsedData).to.equal(undefined)\n      })\n\n      it('should reject malformed topics', () => {\n        const topics = [\n          null,\n          23,\n          ['event'],\n          { event: 'event' },\n          'evnt',\n          'event ',\n          'Event',\n          'EVENT',\n        ]\n        const results = topics.map((topic) => jifHandler.fromJIF(\n          { topic, action: 'emit', eventName: 'time/berlin' }\n        ))\n\n        results.forEach((result, i) => expect(result.success).to.equal(false, i.toString()))\n      })\n\n      it('should reject malformed actions', () => {\n        const actions = [\n          null,\n          23,\n          'emi',\n          'emit ',\n          'Emit',\n          'EMIT',\n        ]\n        const results = actions.map((action) => jifHandler.fromJIF(\n          { topic: 'event', action, eventName: 'time/berlin' }\n        ))\n\n        results.forEach((result, i) => expect(result.success).to.equal(false, i.toString()))\n      })\n\n      it('should not support an event without a name', () => {\n        const jif = {\n          topic: 'event',\n          action: 'emit',\n          data: ''\n        }\n        const result = jifHandler.fromJIF(jif)\n\n        expect(result.success).to.equal(false)\n        expect(result.error).to.be.a('string')\n        expect(result.error).to.match(/eventName/)\n      })\n\n      it('should reject malformed names', () => {\n        const names = [\n          null,\n          23,\n          ['foo'],\n          { name: 'john' },\n          ''\n        ]\n        const results = names.map((eventName) => jifHandler.fromJIF(\n          { topic: 'event', action: 'emit', eventName }\n        ))\n\n        results.forEach((result, i) => expect(result.success).to.equal(false, i.toString()))\n      })\n\n    })\n\n    describe('rpcs', () => {\n      it('should handle a valid rpc message', () => {\n        const jif = {\n          topic: 'rpc',\n          action: 'make',\n          rpcName: 'add-two',\n          data: { numA: 6, numB: 3 }\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.done).to.equal(false)\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.RPC)\n        expect(message.action).to.equal(RPC_ACTION.REQUEST)\n        expect(message.name).to.equal('add-two')\n        expect(message.correlationId).to.be.a('string')\n        expect(message.correlationId).to.have.length.above(12)\n        expect(message.parsedData).to.deep.equal({ numA: 6, numB: 3 })\n      })\n\n      it('should handle an rpc without data', () => {\n        const jif = {\n          topic: 'rpc',\n          action: 'make',\n          rpcName: 'add-two',\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.RPC)\n        expect(message.action).to.equal(RPC_ACTION.REQUEST)\n        expect(message.name).to.equal('add-two')\n        expect(message.correlationId).to.be.a('string')\n        expect(message.correlationId).to.have.length.above(12)\n        expect(message.parsedData).to.equal(undefined)\n      })\n    })\n\n    describe('records', () => {\n      it('should handle a record write (object type) without path', () => {\n        const jif = {\n          topic: 'record',\n          action: 'write',\n          recordName: 'car/bmw',\n          data: { tyres: 2, wheels: 4 }\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.RECORD)\n        expect(message.action).to.equal(RECORD_ACTION.CREATEANDUPDATE)\n        expect(message.name).to.equal('car/bmw')\n        expect(message.version).to.equal(-1)\n        expect(message.parsedData).to.deep.equal({ tyres: 2, wheels: 4 })\n        expect(message.isWriteAck).to.equal(true)\n      })\n\n      it('should handle a record write (array type) without path', () => {\n        const jif = {\n          topic: 'record',\n          action: 'write',\n          recordName: 'car/bmw',\n          data: [{ model: 'M6', hp: 560 }, { model: 'X6', hp: 306 }]\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.RECORD)\n        expect(message.action).to.equal(RECORD_ACTION.CREATEANDUPDATE)\n        expect(message.name).to.equal('car/bmw')\n        expect(message.version).to.equal(-1)\n        expect(message.parsedData).to.deep.equal([{ model: 'M6', hp: 560 }, { model: 'X6', hp: 306 }])\n        expect(message.isWriteAck).to.equal(true)\n      })\n\n      it('should handle a record write with path', () => {\n        const jif = {\n          topic: 'record',\n          action: 'write',\n          recordName: 'car/bmw',\n          path: 'tyres',\n          data: 3\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.RECORD)\n        expect(message.action).to.equal(RECORD_ACTION.CREATEANDPATCH)\n        expect(message.name).to.equal('car/bmw')\n        expect(message.version).to.equal(-1)\n        expect(message.path).to.equal('tyres')\n        expect(message.parsedData).to.deep.equal(3)\n        expect(message.isWriteAck).to.equal(true)\n      })\n\n      it('should handle a record read', () => {\n        const jif = {\n          topic: 'record',\n          action: 'read',\n          recordName: 'car/bmw',\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.RECORD)\n        expect(message.action).to.equal(RECORD_ACTION.READ)\n        expect(message.name).to.equal('car/bmw')\n      })\n\n      it('should handle a record delete', () => {\n        const jif = {\n          topic: 'record',\n          action: 'delete',\n          recordName: 'car/bmw',\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.RECORD)\n        expect(message.action).to.equal(RECORD_ACTION.DELETE)\n        expect(message.name).to.equal('car/bmw')\n      })\n\n      it('should handle a record notify', () => {\n        const jif = {\n          topic: 'record',\n          action: 'notify',\n          recordNames: ['car/bmw', 'car/vw'],\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.RECORD)\n        expect(message.action).to.equal(RECORD_ACTION.NOTIFY)\n        expect(message.names).to.deep.equal(['car/bmw', 'car/vw'])\n      })\n\n      it('should handle a record head', () => {\n        const jif = {\n          topic: 'record',\n          action: 'head',\n          recordName: 'car/bmw',\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.RECORD)\n        expect(message.action).to.equal(RECORD_ACTION.HEAD)\n        expect(message.name).to.equal('car/bmw')\n      })\n\n      it('should only allow writes to have a path field', () => {\n        const jifs = [\n          { topic: 'record', action: 'write', recordName: 'car/bmw', data: 'bla', path: 'wheel' },\n          { topic: 'record', action: 'read', recordName: 'car/bmw', path: 'wheel' },\n          { topic: 'record', action: 'head', recordName: 'car/bmw', path: 'wheel' },\n          { topic: 'record', action: 'delete', recordName: 'car/bmw', path: 'wheel' }\n        ]\n        const results = jifs.map((jif) => jifHandler.fromJIF(jif))\n\n        expect(results[0].success).to.equal(true)\n        expect(results[1].success).to.equal(false)\n        expect(results[2].success).to.equal(false)\n        expect(results[3].success).to.equal(false)\n      })\n\n      it('should only allow writes to have a data field', () => {\n        const jifs = [\n          { topic: 'record', action: 'write', recordName: 'car/bmw', data: { a: 123 } },\n          { topic: 'record', action: 'read', recordName: 'car/bmw', data: { a: 123 } },\n          { topic: 'record', action: 'head', recordName: 'car/bmw', data: { a: 123 } },\n          { topic: 'record', action: 'delete', recordName: 'car/bmw', data: { a: 123 } }\n        ]\n        const results = jifs.map((jif) => jifHandler.fromJIF(jif))\n\n        expect(results[0].success).to.equal(true)\n        expect(results[1].success).to.equal(false)\n        expect(results[2].success).to.equal(false)\n        expect(results[3].success).to.equal(false)\n      })\n    })\n\n    describe('presence', () => {\n      it('should handle a presence query', () => {\n        const jif = {\n          topic: 'presence',\n          action: 'query'\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.PRESENCE)\n        expect(message.action).to.equal(PRESENCE_ACTION.QUERY_ALL)\n      })\n\n      it('should handle a presence query for some users', () => {\n        const jif = {\n          topic: 'presence',\n          action: 'query',\n          names: ['one']\n        }\n        const result = jifHandler.fromJIF(jif)\n        const message = result.message\n\n        expect(result.success).to.equal(true)\n        expect(message).to.be.an('object')\n        expect(message.topic).to.equal(TOPIC.PRESENCE)\n        expect(message.action).to.equal(PRESENCE_ACTION.QUERY)\n        expect(message.names).to.deep.equal(['one'])\n      })\n  })\n\n  })\n\n  describe('toJIF', () => {\n    describe('rpcs', () => {\n      it('should build a valid rpc response', () => {\n        const result = jifHandler.toJIF({\n          topic: TOPIC.RPC,\n          action: RPC_ACTION.RESPONSE,\n          name: 'addTwo',\n          correlationId: '1234',\n          parsedData: 12\n        })\n        const jif = result.message\n        expect(result.done).to.equal(true)\n        expect(jif).to.be.an('object')\n        expect(jif).to.have.all.keys(['success', 'data'])\n        expect(jif.success).to.equal(true)\n        expect(jif.data).to.equal(12)\n      })\n      it('should ignore an rpc request ack', () => {\n        const result = jifHandler.toJIF({\n          topic: TOPIC.RPC,\n          action: RPC_ACTION.REQUEST,\n          name: 'addTwo',\n          correlationId: 1234,\n          isAck: true\n        })\n        expect(result.done).to.equal(false)\n      })\n\n      it('should build a valid rpc response', () => {\n        const result = jifHandler.toJIF({\n          topic: TOPIC.RPC,\n          action: RPC_ACTION.RESPONSE,\n          name: 'addTwo',\n          correlationId: 1234,\n          parsedData: 12\n        })\n        const jif = result.message\n        expect(result.done).to.equal(true)\n        expect(jif).to.be.an('object')\n        expect(jif).to.have.all.keys(['success', 'data'])\n        expect(jif.success).to.equal(true)\n        expect(jif.data).to.equal(12)\n      })\n    })\n\n    describe('records', () => {\n      it('should build a valid record write ack', () => {\n        const result = jifHandler.toJIF({\n          topic: TOPIC.RECORD,\n          action: RECORD_ACTION.WRITE_ACKNOWLEDGEMENT,\n          name: 'car/fiat',\n          parsedData: [[2, 3], null]\n        })\n        const jif = result.message\n        expect(result.done).to.equal(true)\n        expect(jif).to.be.an('object')\n        expect(jif).to.contain.keys(['success'])\n        expect(jif.success).to.equal(true)\n      })\n\n      it('should build a valid record delete success', () => {\n        const result = jifHandler.toJIF({\n          topic: TOPIC.RECORD,\n          action: RECORD_ACTION.DELETE_SUCCESS,\n          name: 'car/fiat'\n        })\n        const jif = result.message\n        expect(result.done).to.equal(true)\n        expect(jif).to.be.an('object')\n        expect(jif).to.have.all.keys(['success'])\n        expect(jif.success).to.equal(true)\n      })\n\n      it('should build a valid record read response', () => {\n        const result = jifHandler.toJIF({\n          topic: TOPIC.RECORD,\n          action: RECORD_ACTION.READ_RESPONSE,\n          name: 'car/fiat',\n          version: 2,\n          parsedData: { car: true }\n        })\n        const jif = result.message\n        expect(result.done).to.equal(true)\n        expect(jif).to.be.an('object')\n        expect(jif).to.contain.keys(['success'])\n        expect(jif.success).to.equal(true)\n        expect(jif.data).to.deep.equal({ car: true })\n        expect(jif.version).to.equal(2)\n      })\n\n      it('should handle a valid record head response', () => {\n        const result = jifHandler.toJIF({\n          topic: TOPIC.RECORD,\n          action: RECORD_ACTION.HEAD_RESPONSE,\n          name: 'car/fiat',\n          version: 2\n        })\n        const jif = result.message\n        expect(result.done).to.equal(true)\n        expect(jif).to.be.an('object')\n        expect(jif).to.have.all.keys(['success', 'version'])\n        expect(jif.success).to.equal(true)\n        expect(jif.version).to.equal(2)\n      })\n\n      it('should handle a valid record head error', () => {\n        const result = jifHandler.errorToJIF({\n          topic: TOPIC.RECORD,\n          action: RECORD_ACTION.HEAD,\n          name: 'car/fiat'\n        }, RECORD_ACTION.RECORD_LOAD_ERROR)\n        const jif = result.message\n        expect(result.done).to.equal(true)\n        expect(jif).to.be.an('object')\n        expect(jif).to.include.all.keys(['error', 'errorEvent', 'errorTopic', 'success'])\n        expect(jif.success).to.equal(false)\n        expect(jif.errorTopic).to.equal('record')\n        expect(jif.errorEvent).to.equal(RECORD_ACTION.RECORD_LOAD_ERROR)\n        expect(jif.errorParams).to.equal('car/fiat') // TODO: review\n      })\n    })\n\n    describe('presence', () => {\n      it('should build a valid presence response', () => {\n        const result = jifHandler.toJIF({\n          topic: TOPIC.PRESENCE,\n          action: PRESENCE_ACTION.QUERY_ALL_RESPONSE,\n          names: ['john', 'alex', 'yasser']\n        })\n        const jif = result.message\n        expect(result.done).to.equal(true)\n        expect(jif).to.be.an('object')\n        expect(jif).to.have.all.keys(['success', 'users'])\n        expect(jif.success).to.equal(true)\n        expect(jif.users).to.deep.equal(['john', 'alex', 'yasser'])\n      })\n    })\n  })\n\n})\n"
  },
  {
    "path": "src/jif/jif-handler.ts",
    "content": "import {\n  EVENT_ACTION,\n  PRESENCE_ACTION,\n  RECORD_ACTION,\n  RPC_ACTION,\n  TOPIC,\n  Message,\n  ALL_ACTIONS,\n  ACTIONS\n} from '../constants'\n\nimport {Ajv} from 'ajv'\n\nimport {\n  getUid,\n  reverseMap,\n  deepFreeze,\n} from '../utils/utils'\nimport { jifSchema } from './jif-schema'\nimport { JifMessage, DeepstreamServices, EVENT } from '@deepstream/types'\n\nconst ajv = new Ajv({strict: false})\n\nconst validateJIF: any = ajv.compile(jifSchema)\n\ntype JifInMessage = any\n\n// jif -> message lookup table\nfunction getJifToMsg () {\n  const JIF_TO_MSG: any = {}\n\n  JIF_TO_MSG.event = {}\n  JIF_TO_MSG.event.emit = (msg: JifInMessage) => ({\n    done: true,\n    message: {\n      topic: TOPIC.EVENT,\n      action: EVENT_ACTION.EMIT,\n      name: msg.eventName,\n      parsedData: msg.data,\n    },\n  })\n\n  JIF_TO_MSG.rpc = {}\n  JIF_TO_MSG.rpc.make = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.RPC,\n      action: RPC_ACTION.REQUEST,\n      name: msg.rpcName,\n      correlationId: getUid(),\n      parsedData: msg.data,\n    },\n  })\n\n  JIF_TO_MSG.record = {}\n  JIF_TO_MSG.record.read = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.RECORD,\n      action: RECORD_ACTION.READ,\n      name: msg.recordName,\n    },\n  })\n\n  JIF_TO_MSG.record.write = (msg: JifInMessage) => (\n    msg.path ? JIF_TO_MSG.record.patch(msg) : JIF_TO_MSG.record.update(msg)\n  )\n\n  JIF_TO_MSG.record.patch = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.RECORD,\n      action: RECORD_ACTION.CREATEANDPATCH,\n      name: msg.recordName,\n      version: msg.version || -1,\n      path: msg.path,\n      parsedData: msg.data,\n      isWriteAck: true,\n      correlationId: 0\n    },\n  })\n\n  JIF_TO_MSG.record.update = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.RECORD,\n      action: RECORD_ACTION.CREATEANDUPDATE,\n      name: msg.recordName,\n      version: msg.version || -1,\n      parsedData: msg.data,\n      isWriteAck: true,\n      correlationId: 0\n    },\n  })\n\n  JIF_TO_MSG.record.head = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.RECORD,\n      action: RECORD_ACTION.HEAD,\n      name: msg.recordName,\n    },\n  })\n\n  JIF_TO_MSG.record.delete = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.RECORD,\n      action: RECORD_ACTION.DELETE,\n      name: msg.recordName,\n    },\n  }),\n\n  JIF_TO_MSG.record.notify = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.RECORD,\n      action: RECORD_ACTION.NOTIFY,\n      names: msg.recordNames\n    },\n  })\n\n  JIF_TO_MSG.list = {}\n  JIF_TO_MSG.list.read = (msg: JifInMessage) => ({\n      done: false,\n      message: {\n        topic: TOPIC.RECORD,\n        action: RECORD_ACTION.READ,\n        name: msg.listName,\n      },\n  })\n\n  JIF_TO_MSG.list.write = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.RECORD,\n      action: RECORD_ACTION.CREATEANDUPDATE,\n      name: msg.listName,\n      version: msg.version || -1,\n      parsedData: msg.data,\n      isWriteAck: true,\n      correlationId: 0\n    },\n  })\n\n  JIF_TO_MSG.list.delete = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.RECORD,\n      action: RECORD_ACTION.DELETE,\n      name: msg.listName,\n    },\n  })\n\n  JIF_TO_MSG.presence = {}\n\n  JIF_TO_MSG.presence.query = (msg: JifInMessage) => (\n    msg.names ? JIF_TO_MSG.presence.queryUsers(msg) : JIF_TO_MSG.presence.queryAll(msg)\n  )\n\n  JIF_TO_MSG.presence.queryAll = () => ({\n    done: false,\n    message: {\n      topic: TOPIC.PRESENCE,\n      action: PRESENCE_ACTION.QUERY_ALL,\n    },\n  })\n\n  JIF_TO_MSG.presence.queryUsers = (msg: JifInMessage) => ({\n    done: false,\n    message: {\n      topic: TOPIC.PRESENCE,\n      action: PRESENCE_ACTION.QUERY,\n      names: msg.names,\n    },\n  })\n\n  return deepFreeze(JIF_TO_MSG)\n}\n\n// message type enumeration\nconst TYPE = { ACK: 'A', NORMAL: 'N' }\n\nfunction getMsgToJif () {\n  // message -> jif lookup table\n  const MSG_TO_JIF: any = {}\n  MSG_TO_JIF[TOPIC.RPC] = {}\n  MSG_TO_JIF[TOPIC.RPC][RPC_ACTION.RESPONSE] = {}\n  MSG_TO_JIF[TOPIC.RPC][RPC_ACTION.RESPONSE][TYPE.NORMAL] = (message: Message) => ({\n    done: true,\n    message: {\n      data: message.parsedData,\n      success: true,\n    },\n  })\n\n  MSG_TO_JIF[TOPIC.RPC][RPC_ACTION.REQUEST_ERROR] = {}\n  MSG_TO_JIF[TOPIC.RPC][RPC_ACTION.REQUEST_ERROR][TYPE.NORMAL] = (message: Message) => ({\n    done: true,\n    message: {\n      errorTopic: 'rpc',\n      error: message.parsedData,\n      success: false,\n    },\n  })\n\n  MSG_TO_JIF[TOPIC.RPC][RPC_ACTION.ACCEPT] = {}\n  MSG_TO_JIF[TOPIC.RPC][RPC_ACTION.ACCEPT][TYPE.NORMAL] = () => ({ done: false })\n\n  MSG_TO_JIF[TOPIC.RPC][RPC_ACTION.REQUEST] = {}\n  MSG_TO_JIF[TOPIC.RPC][RPC_ACTION.REQUEST][TYPE.ACK] = () => ({ done: false })\n\n  MSG_TO_JIF[TOPIC.RECORD] = {}\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.READ_RESPONSE] = {}\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.READ_RESPONSE][TYPE.NORMAL] = (message: Message) => ({\n    done: true,\n    message: {\n      version: message.version,\n      data: message.parsedData,\n      success: true,\n    },\n  })\n\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.WRITE_ACKNOWLEDGEMENT] = {}\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.WRITE_ACKNOWLEDGEMENT][TYPE.NORMAL] = (message: Message) => ({\n    done: true,\n    message: {\n      success: true,\n    },\n  })\n\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.DELETE] = {}\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.DELETE][TYPE.NORMAL] = () => ({\n    done: true,\n    message: {\n      success: true,\n    },\n  })\n\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.DELETE_SUCCESS] = {}\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.DELETE_SUCCESS][TYPE.NORMAL] = () => ({\n    done: true,\n    message: {\n      success: true,\n    },\n  })\n\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.NOTIFY] = {}\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.NOTIFY][TYPE.ACK] = (message: Message) => ({\n    done: true,\n    message: {\n      success: true,\n    },\n  })\n\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.HEAD_RESPONSE] = {}\n  MSG_TO_JIF[TOPIC.RECORD][RECORD_ACTION.HEAD_RESPONSE][TYPE.NORMAL] = (message: Message) => ({\n    done: true,\n    message: {\n      version: message.version,\n      success: true,\n    },\n  })\n\n  MSG_TO_JIF[TOPIC.PRESENCE] = {}\n  MSG_TO_JIF[TOPIC.PRESENCE][PRESENCE_ACTION.QUERY_ALL_RESPONSE] = {}\n  MSG_TO_JIF[TOPIC.PRESENCE][PRESENCE_ACTION.QUERY_ALL_RESPONSE][TYPE.NORMAL] = (message: Message) => ({\n    done: true,\n    message: {\n      users: message.names,\n      success: true,\n    },\n  })\n  MSG_TO_JIF[TOPIC.PRESENCE][PRESENCE_ACTION.QUERY_RESPONSE] = {}\n  MSG_TO_JIF[TOPIC.PRESENCE][PRESENCE_ACTION.QUERY_RESPONSE][TYPE.NORMAL] = (message: Message) => ({\n    done: true,\n    message: {\n      users: message.parsedData,\n      success: true,\n    },\n  })\n\n  return deepFreeze(MSG_TO_JIF)\n}\n\nexport default class JIFHandler {\n  private JIF_TO_MSG = getJifToMsg()\n  private MSG_TO_JIF = getMsgToJif()\n  private topicToKey = reverseMap(TOPIC)\n\n  constructor (private services: DeepstreamServices) {}\n\n  /*\n   * Validate and convert a JIF message to a deepstream message\n   */\n  public fromJIF (jifMessage: JifInMessage) {\n    if (!validateJIF(jifMessage)) {\n      let error = validateJIF.errors[0]\n      switch (error.keyword) {\n     // case 'additionalProperties':\n     //   error = `property '${error.params.additionalProperty}'\n     //   not permitted for topic '${jifMessage.topic}'`\n     //   break\n        case 'required':\n          error = `property '${error.params.missingProperty}' is required for topic '${jifMessage.topic}'`\n          break\n        case 'type':\n        case 'minLength':\n          error = `property '${error.dataPath}' ${error.message}`\n          break\n     // case 'const':\n     //   error = `value for property '${error.dataPath}' not valid for topic '${jifMessage.topic}'`\n     //   break\n        default:\n          error = null\n      }\n      return {\n        success: false,\n        error,\n        done: true,\n      }\n    }\n\n    const result = this.JIF_TO_MSG[jifMessage.topic][jifMessage.action](jifMessage)\n    result.success = true\n    return result\n  }\n\n  /*\n   * Convert a deepstream response/ack message to a JIF message response\n   * @param {Object}  message     deepstream message\n   *\n   * @returns {Object} {\n   *    {Object}  message   jif message\n   *    {Boolean} done      false iff message should await another result/acknowledgement\n   * }\n   */\n  public toJIF (message: Message): JifMessage {\n    let type\n    if (message.isAck) {\n      type = TYPE.ACK\n    } else {\n      type = TYPE.NORMAL\n    }\n\n    if (message.isError) {\n      return this.errorToJIF(message, message.action)\n    }\n\n    return this.MSG_TO_JIF[message.topic][message.action][type](message)\n  }\n\n  /*\n   * Convert a deepstream error message to a JIF message response\n   */\n  public errorToJIF (message: Message, event: ALL_ACTIONS | string) {\n    // convert topic enum to human-readable key\n    const topicKey = this.topicToKey[message.topic]\n\n    const result: any = {\n      errorTopic: topicKey && topicKey.toLowerCase(),\n      errorEvent: event,\n      success: false,\n    }\n\n    if (event === ACTIONS[message.topic].MESSAGE_DENIED) {\n      result.action = message.originalAction as number\n      result.error = `Message denied. Action \"${ACTIONS[message.topic][message.originalAction!]}\" is not permitted.`\n    } else if (message.topic === TOPIC.RECORD && event === RECORD_ACTION.VERSION_EXISTS) {\n      result.error = `Record update failed. Version ${message.version} exists for record \"${message.name}\".`\n      result.currentVersion = message.version\n      result.currentData = message.parsedData\n    } else if (message.topic === TOPIC.RECORD && event === RECORD_ACTION.INVALID_VERSION) {\n      result.error = `Record update failed. Version ${message.version} is not valid for record \"${message.name}\".`\n      result.currentVersion = message.version\n      result.currentData = message.parsedData\n    } else if (message.topic === TOPIC.RECORD && event === RECORD_ACTION.RECORD_NOT_FOUND) {\n      result.error = `Record read failed. Record \"${message.name}\" could not be found.`\n      result.errorEvent = message.action\n    } else if (message.topic === TOPIC.RPC && event === RPC_ACTION.NO_RPC_PROVIDER) {\n      result.error = `No provider was available to handle the RPC \"${message.name}\".`\n      // message.correlationId = data[1]\n    } else if (message.topic === TOPIC.RPC && message.action === RPC_ACTION.RESPONSE_TIMEOUT) {\n      result.error = 'The RPC response timeout was exceeded by the provider.'\n    } else {\n      this.services.logger.warn(\n        EVENT.INFO,\n        `Unhandled request error occurred: ${TOPIC[message.topic]} ${event} ${JSON.stringify(message)}`,\n        { message }\n      )\n      result.error = `An error occurred: ${RPC_ACTION[event as number]}.`\n      result.errorParams = message.name\n    }\n\n    return {\n      message: result,\n      done: true,\n    }\n  }\n}\n"
  },
  {
    "path": "src/jif/jif-schema.ts",
    "content": "export const jifSchema = {\n  title: 'JSON Interchange Format',\n  description: 'A JSON format for interaction with DeepstreamIO.',\n  type: 'object',\n  anyOf: [\n    {\n      properties: {\n        topic: {\n          const: 'event',\n        },\n        action: {\n          const: 'emit',\n        },\n        eventName: {\n          type: 'string',\n          minLength: 1,\n        },\n        data: {},\n      },\n      required: [\n        'topic',\n        'action',\n        'eventName',\n      ],\n      additionalProperties: false,\n    },\n    {\n      title: 'RPC',\n      description: 'Make RPC requests.',\n      properties: {\n        topic: {\n          const: 'rpc',\n        },\n        action: {\n          const: 'make',\n        },\n        rpcName: {\n          type: 'string',\n          minLength: 1,\n        },\n        data: {},\n      },\n      required: [\n        'topic',\n        'action',\n        'rpcName',\n      ],\n      additionalProperties: false,\n    },\n    {\n      title: 'Record',\n      description: 'Fetch and delete records.',\n      properties: {\n        topic: {\n          const: 'record',\n        },\n        action: {\n          enum: [\n            'read',\n            'head',\n            'delete',\n          ],\n        },\n        recordName: {\n          type: 'string',\n          minLength: 1,\n        },\n      },\n      required: [\n        'topic',\n        'action',\n        'recordName',\n      ],\n      additionalProperties: false,\n    },\n    {\n      title: 'Record Writes',\n      description: 'Create or update a record. The full object must be specified.',\n      properties: {\n        topic: {\n          const: 'record',\n        },\n        action: {\n          const: 'write',\n        },\n        recordName: {\n          type: 'string',\n          minLength: 1,\n        },\n        data: {\n          type: ['object', 'array'],\n        },\n        version: {\n          type: 'integer',\n          minimum: -1,\n        },\n      },\n      required: [\n        'topic',\n        'action',\n        'recordName',\n        'data',\n      ],\n      additionalProperties: false,\n    },\n    {\n      title: 'Record Write With Path',\n      description: 'If a path is specified, a patching update will occur.',\n      properties: {\n        topic: {\n          const: 'record',\n        },\n        action: {\n          const: 'write',\n        },\n        recordName: {\n          type: 'string',\n          minLength: 1,\n        },\n        data: {},\n        path: {\n          type: 'string',\n        },\n        version: {\n          type: 'integer',\n          minimum: -1,\n        },\n      },\n      required: [\n        'topic',\n        'action',\n        'recordName',\n        'data',\n        'path',\n      ],\n      additionalProperties: false,\n    },\n    {\n      title: 'Record Notify',\n      description: 'Notifies deepstream that a record was written to remotely',\n      properties: {\n        topic: {\n          const: 'record',\n        },\n        action: {\n          const: 'notify',\n        },\n        recordNames: {\n          type: 'array',\n          minLength: 1,\n          items: {\n            type: 'string',\n          },\n        },\n      },\n      required: [\n        'topic',\n        'action',\n        'recordNames',\n      ],\n      additionalProperties: false,\n    },\n     {\n      title: 'List',\n      description: 'Fetch and delete lists.',\n      properties: {\n        topic: {\n          const: 'list',\n        },\n        action: {\n          enum: [\n            'read',\n            'delete',\n          ],\n        },\n        listName: {\n          type: 'string',\n          minLength: 1,\n        },\n      },\n      required: [\n        'topic',\n        'action',\n        'listName',\n      ],\n      additionalProperties: false,\n    },\n    {\n      title: 'List Writes',\n      description: 'Create or write to a list.',\n      properties: {\n        topic: {\n          const: 'list',\n        },\n        action: {\n          const: 'write',\n        },\n        listName: {\n          type: 'string',\n          minLength: 1,\n        },\n        data: {\n          type: ['array'],\n          items: {\n            type: 'string',\n          },\n        },\n        version: {\n          type: 'integer',\n          minimum: -1,\n        },\n      },\n      required: [\n        'topic',\n        'action',\n        'listName',\n        'data',\n      ],\n      additionalProperties: false,\n    },\n    {\n      title: 'Presence',\n      description: 'Query presence.',\n      properties: {\n        topic: {\n          const: 'presence',\n        },\n        action: {\n          enum: [\n            'query',\n          ],\n        },\n        names: {\n          type: ['array'],\n          items: {\n            type: 'string',\n          }\n        }\n      },\n      required: [\n        'topic',\n        'action',\n      ],\n      additionalProperties: false,\n    },\n  ],\n}\n"
  },
  {
    "path": "src/listen/listener-registry.spec.ts",
    "content": "import 'mocha'\n\nimport * as C from '../constants'\nimport ListenerTestUtils from './listener-test-utils'\n\nimport 'mocha'\n\nlet tu\n\ndescribe('listener-registry', () => {\n/*\nconst ListenerRegistry = require('../../src/listen/listener-registry')\nconst testHelper = require('../test-helper/test-helper')\nimport SocketMock from '../test-mocks/socket-mock'\n\nconst options = testHelper.getDeepstreamOptions()\nconst msg = testHelper.msg\nlet listenerRegistry\n\nconst recordSubscriptionRegistryMock = {\n  getNames () {\n    return ['car/Mercedes', 'car/Abarth']\n  }\n}\n\ndescribe.skip('listener-registry errors', () => {\n  beforeEach(() => {\n    listenerRegistry = new ListenerRegistry('R', options, recordSubscriptionRegistryMock)\n    expect(typeof listenerRegistry.handle).to.equal('function')\n  })\n\n  it('adds a listener without message data', () => {\n    const socketWrapper = SocketWrapperFactory.create(new SocketMock(), options)\n    listenerRegistry.handle(socketWrapper, {\n      topic: 'R',\n      action: 'L',\n      data: []\n    })\n    expect(options.logger.lastLogArguments).to.deep.equal([3, 'INVALID_MESSAGE_DATA', undefined])\n    expect(socketWrapper.socket.lastSendMessage).to.equal(msg('R|E|INVALID_MESSAGE_DATA|undefined+'))\n  })\n\n  it('adds a listener with invalid message data message data', () => {\n    const socketWrapper = new SocketWrapper(new SocketMock(), options)\n    listenerRegistry.handle(socketWrapper, {\n      topic: 'R',\n      action: 'L',\n      data: [44]\n    })\n    expect(options.logger.lastLogArguments).to.deep.equal([3, 'INVALID_MESSAGE_DATA', 44])\n    expect(socketWrapper.socket.lastSendMessage).to.equal(msg('R|E|INVALID_MESSAGE_DATA|44+'))\n  })\n\n  it('adds a listener with an invalid regexp', () => {\n    const socketWrapper = new SocketWrapper(new SocketMock(), options)\n    listenerRegistry.handle(socketWrapper, {\n      topic: 'R',\n      action: 'L',\n      data: ['us(']\n    })\n    expect(options.logger.lastLogArguments).to.deep.equal([3, 'INVALID_MESSAGE_DATA', 'SyntaxError: Invalid regular expression: /us(/: Unterminated group'])\n    expect(socketWrapper.socket.lastSendMessage).to.equal(msg('R|E|INVALID_MESSAGE_DATA|SyntaxError: Invalid regular expression: /us(/: Unterminated group+'))\n  })\n})\n*/\n\ndescribe('listener-registry-local-load-balancing', () => {\n    beforeEach(() => {\n        tu = new ListenerTestUtils()\n    })\n\n    afterEach(() => {\n        tu.complete()\n    })\n\n    describe('with a single provider', () => {\n        it('accepts a subscription', () => {\n            // 1.  provider does listen a/.*\n            tu.providerListensTo(1, 'a/.*')\n            // 3.  provider will get a SP\n            tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n            // 2.  clients 1 request a/1\n            tu.clientSubscribesTo(1, 'a/1', true)\n            // 4.  provider responds with ACCEPT\n            tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n            tu.providerAccepts(1, 'a/.*', 'a/1')\n            // // 6.  clients 2 request a/1\n            tu.clientWillRecievePublishedUpdate(2, 'a/1', true)\n            tu.clientSubscribesTo(2, 'a/1')\n            // // 6.  clients 3 request a/1\n            tu.clientWillRecievePublishedUpdate(3, 'a/1', true)\n            tu.clientSubscribesTo(3, 'a/1')\n            // // 9.  client 1 discards a/1\n            tu.clientUnsubscribesTo(3, 'a/1')\n            // // 9.  client 2 discards a/1\n            tu.clientUnsubscribesTo(2, 'a/1')\n            // // 10.  clients discards a/1\n            tu.providerWillGetSubscriptionRemoved(1, 'a/.*', 'a/1')\n            tu.publishUpdateWillBeSentToSubscribers('a/1', false)\n            tu.clientUnsubscribesTo(1, 'a/1', true)\n            // // 13.  a/1 should have no active provider\n            tu.subscriptionHasActiveProvider('a/1', false)\n            // // 14. recieving unknown accept/reject throws an error\n            tu.acceptMessageThrowsError(1, 'a/.*', 'a/1')\n            tu.rejectMessageThrowsError(1, 'a/.*', 'a/1')\n        })\n\n        it('rejects a subscription', () => {\n            // 1.  provider does listen a/.*\n            tu.providerListensTo(1, 'a/.*')\n            // 2.  clients request a/1\n            tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n            tu.clientSubscribesTo(1, 'a/1', true)\n            // 4.  provider responds with ACCEPT\n            tu.providerRejects(1, 'a/.*', 'a/1')\n            // 5.  clients discards a/1\n            tu.clientUnsubscribesTo(1, 'a/1', true)\n            // mocks do expecations to ensure nothing else was called\n        })\n\n        it('rejects a subscription with a pattern for which subscriptions already exists', () => {\n            // 0. subscription already made for b/1\n            tu.subscriptionAlreadyMadeFor('b/1')\n            // 1. provider does listen a/.*\n            tu.providerWillGetSubscriptionFound(1, 'b/.*', 'b/1')\n            tu.providerListensTo(1, 'b/.*')\n            // 3. provider responds with REJECT\n            tu.providerRejects(1, 'b/.*', 'b/1')\n            // 4. clients discards b/1\n            tu.clientUnsubscribesTo(1, 'b/1', true)\n            // mocks do expecations to ensure nothing else was called\n        })\n\n        it('accepts a subscription with a pattern for which subscriptions already exists', () => {\n            // 0. subscription already made for b/1\n            tu.subscriptionAlreadyMadeFor('b/1')\n            // 1. provider does listen a/.*\n            tu.providerWillGetSubscriptionFound(1, 'b/.*', 'b/1')\n            tu.providerListensTo(1, 'b/.*')\n            // 3. provider responds with ACCEPT\n            tu.publishUpdateWillBeSentToSubscribers('b/1', true)\n            tu.providerAccepts(1, 'b/.*', 'b/1')\n            // 5. clients discards b/1\n            tu.providerWillGetSubscriptionRemoved(1, 'b/.*', 'b/1')\n            tu.publishUpdateWillBeSentToSubscribers('b/1', false)\n            tu.clientUnsubscribesTo(1, 'b/1', true)\n            // 7. send publishing=false to the clients\n        })\n\n        it('accepts a subscription for 2 clients', () => {\n            // 1. provider does listen a/.*\n            tu.providerListensTo(1, 'a/.*')\n            // 2.  client 1 requests a/1\n            tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n            tu.clientSubscribesTo(1, 'a/1', true)\n            // 4. provider responds with ACCEPT\n            tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n            tu.providerAccepts(1, 'a/.*', 'a/1')\n            // 5. send publishing=true to the clients\n            tu.clientWillRecievePublishedUpdate(2, 'a/1', true)\n            tu.clientSubscribesTo(2, 'a/1')\n            // 9.  client 1 discards a/1\n            tu.clientUnsubscribesTo(1, 'a/1')\n            // 11.  client 2 discards a/1\n            tu.providerWillGetSubscriptionRemoved(1, 'a/.*', 'a/1')\n            tu.publishUpdateWillBeSentToSubscribers('a/1', false)\n            tu.clientUnsubscribesTo(2, 'a/1', true)\n            // 13. a/1 should have no active provider\n            tu.subscriptionHasActiveProvider('a/1', false)\n        })\n    })\n\n    describe('with multiple providers', () => {\n        it('first rejects, seconds accepts, third does nothing', () => {\n            // 1. provider 1 does listen a/.*\n            tu.providerListensTo(1, 'a/.*')\n            // 2. provider 2 does listen a/[0-9]\n            tu.providerListensTo(2, 'a/[0-9]')\n            // 3.  client 1 requests a/1\n            tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n            tu.clientSubscribesTo(1, 'a/1', true)\n            // 5. provider 1 responds with REJECTS\n            tu.providerWillGetSubscriptionFound(2, 'a/[0-9]', 'a/1')\n            tu.providerRejects(1, 'a/.*', 'a/1')\n            // 7. provider 2 responds with ACCEPTS\n            tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n            tu.providerAccepts(2, 'a/[0-9]', 'a/1')\n            // 8. send publishing=true to the clients\n            // 9. provider 3 does listen a/[0-9]\n            tu.providerListensTo(3, 'a/[0-9]')\n            // 11. client 1 unsubscribed to a/1\n            tu.providerWillGetSubscriptionRemoved(2, 'a/[0-9]', 'a/1')\n            tu.publishUpdateWillBeSentToSubscribers('a/1', false)\n            tu.clientUnsubscribesTo(1, 'a/1', true)\n        })\n\n        it('first accepts, seconds does nothing', () => {\n            // 1. provider 1 does listen a/.*\n            tu.providerListensTo(1, 'a/.*')\n            // 2. provider 2 does listen a/[0-9]\n            tu.providerListensTo(2, 'a/[0-9]')\n            // 3.  client 1 requests a/1\n            tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n            tu.clientSubscribesTo(1, 'a/1', true)\n            // 6. provider 1 accepts\n            tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n            tu.providerAccepts(1, 'a/.*', 'a/1')\n            // 12. send publishing=true to the clients\n            // 6. client 1 unsubscribed to a/1\n            tu.providerWillGetSubscriptionRemoved(1, 'a/.*', 'a/1')\n            tu.publishUpdateWillBeSentToSubscribers('a/1', false)\n            tu.clientUnsubscribesTo(1, 'a/1', true)\n        })\n\n        it('first rejects, seconds - which start listening after first gets SP - accepts', () => {\n            // 1. provider 1 does listen a/.*\n            tu.providerListensTo(1, 'a/.*')\n            // 3.  client 1 requests a/1\n            tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n            tu.clientSubscribesTo(1, 'a/1', true)\n            // 2. provider 2 does listen a/[0-9]\n            tu.providerListensTo(2, 'a/[0-9]')\n            // 6. provider 1 rejects\n            tu.providerWillGetSubscriptionFound(2, 'a/[0-9]', 'a/1')\n            tu.providerRejects(1, 'a/.*', 'a/1')\n            // 6. provider 1 accepts\n            tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n            tu.providerAccepts(2, 'a/[0-9]', 'a/1')\n            // 12. send publishing=false to the clients\n        })\n\n        it('no messages after unlisten', () => {\n            // 1. provider 1 does listen a/.*\n            tu.providerListensTo(1, 'a/.*')\n            // 2. provider 2 does listen a/[0-9]\n            tu.providerListensTo(2, 'a/[0-9]')\n            // 3.  client 1 requests a/1\n            tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n            tu.clientSubscribesTo(1, 'a/1', true)\n            // 2. provider 2 does unlisten a/[0-9]\n            tu.providerUnlistensTo(2, 'a/[0-9]')\n            // 6. provider 1 responds with REJECTS\n            tu.providerRejects(1, 'a/.*', 'a/1')\n            // mock does remaining expecations\n        })\n\n        it.skip('provider 1 accepts a subscription and disconnects then provider 2 gets a SP', () => {\n            // 1. provider 1 does listen a/.*\n            tu.providerListensTo(1, 'a/.*')\n            // 2. provider 2 does listen a/[0-9]\n            tu.providerListensTo(2, 'a/[0-9]')\n            // 3.  client 1 requests a/1\n            tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n            tu.clientSubscribesTo(1, 'a/1', true)\n            // 5. provider 1 responds with ACCEPT\n            tu.providerAccepts(1, 'a/.*', 'a/1')\n            // 13. subscription has active provider\n            tu.subscriptionHasActiveProvider('a/1', true)\n            // 7.  client 1 requests a/1\n            tu.providerWillGetSubscriptionFound(2, 'a/[0-9]', 'a/1')\n            tu.publishUpdateWillBeSentToSubscribers('a/1', false)\n            tu.providerLosesItsConnection(1)\n            // 8. send publishing=true to the clients\n            // 9. subscription doesnt have active provider\n            tu.subscriptionHasActiveProvider('a/1', false)\n            tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n            tu.providerAccepts(2, 'a/[0-9]', 'a/1')\n            // 12. send publishing=true to the clients\n            // 13. subscription has active provider\n            tu.subscriptionHasActiveProvider('a/1', true)\n        })\n    })\n})\n\ndescribe('listener-registry-local-load-balancing does not send publishing updates for events', () => {\n    beforeEach(() => {\n        tu = new ListenerTestUtils(C.TOPIC.EVENT)\n    })\n\n    afterEach(() => {\n        tu.complete()\n    })\n\n    it('client with provider already registered', () => {\n        // 1. provider does listen a/.*\n        tu.providerListensTo(1, 'a/.*')\n        // 2.  client 1 requests a/1\n        tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n        tu.clientSubscribesTo(1, 'a/1', true)\n        // 4. provider responds with ACCEPT\n        tu.providerAccepts(1, 'a/.*', 'a/1')\n        // 6. client 2 requests a/1\n        tu.clientSubscribesTo(2, 'a/1')\n        // 9.  client 1 discards a/1\n        tu.clientUnsubscribesTo(1, 'a/1')\n        // 11.  client 2 discards a/1\n        tu.providerWillGetSubscriptionRemoved(1, 'a/.*', 'a/1')\n        tu.clientUnsubscribesTo(2, 'a/1', true)\n        // 12. provider should get a SR\n        // 13. a/1 should have no active provider\n        tu.subscriptionHasActiveProvider('a/1', false)\n    })\n})\n\ndescribe('listener-registry-local-timeouts', () => {\n    beforeEach(() => {\n        tu = new ListenerTestUtils()\n    })\n\n    afterEach(() => {\n        tu.complete()\n    })\n\n    beforeEach(() => {\n        // 1. provider 1 does listen a/.*\n        tu.providerListensTo(1, 'a/.*')\n        // 2. provider 2 does listen a/[0-9]\n        tu.providerListensTo(2, 'a/[0-9]')\n        // 3.  client 1 requests a/1\n        tu.providerWillGetSubscriptionFound(1, 'a/.*', 'a/1')\n        tu.clientSubscribesTo(1, 'a/1', true)\n    })\n\n    it('provider 1 times out, provider 2 accepts', (done) => {\n        tu.providerWillGetSubscriptionFound(2, 'a/[0-9]', 'a/1')\n        tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n        tu.providerWillGetListenTimeout(1, 'a/1')\n\n        setTimeout(() => {\n            tu.providerAccepts(1, 'a/[0-9]', 'a/1')\n            tu.subscriptionHasActiveProvider('a/1', true)\n            done()\n        }, 40)\n    })\n\n    it.skip('provider 1 times out and gets a RESPONSE_TIMEOUT, but will be ignored because provider 2 accepts as well', (done) => {\n        tu.providerWillGetSubscriptionRemoved(1, 'a/.*', 'a/1')\n        tu.providerWillGetSubscriptionFound(2, 'a/[0-9]', 'a/1')\n        tu.providerWillGetListenTimeout(1, 'a/1')\n\n        setTimeout(() => {\n            tu.providerAcceptsButIsntAcknowledged(1, 'a/.*', 'a/1')\n\n            tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n            tu.providerAccepts(2, 'a/[0-9]', 'a/1')\n\n            tu.subscriptionHasActiveProvider('a/1', true)\n            done()\n        }, 40)\n    })\n\n    it.skip('provider 1 times out, but then it accept and will be used because provider 2 rejects', (done) => {\n        tu.providerWillGetSubscriptionFound(2, 'a/[0-9]', 'a/1')\n        tu.providerWillGetListenTimeout(1, 'a/1')\n\n        setTimeout(() => {\n            tu.providerAcceptsButIsntAcknowledged(1, 'a/.*', 'a/1')\n            tu.subscriptionHasActiveProvider('a/1', false)\n\n            tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n            tu.providerRejectsAndPreviousTimeoutProviderThatAcceptedIsUsed(2, 'a/[0-9]', 'a/1')\n            tu.subscriptionHasActiveProvider('a/1', true)\n            done()\n        }, 40)\n    })\n\n    it.skip('provider 1 and 2 times out and 3 rejects, 1 rejects and 2 accepts later and 2 wins', (done) => {\n        tu.providerWillGetListenTimeout(1, 'a/1')\n        tu.providerWillGetListenTimeout(2, 'a/1')\n\n        tu.providerWillGetSubscriptionFound(2, 'a/[0-9]', 'a/1')\n\n        tu.providerListensTo(3, 'a/[1]')\n\n        setTimeout(() => {\n            tu.providerWillGetSubscriptionFound(3, 'a/[1]', 'a/1')\n            // first provider timeout\n            setTimeout(() => {\n\n                tu.providerRejects(1, 'a/.*', 'a/1')\n\n                tu.providerAcceptsButIsntAcknowledged(2, 'a/[0-9]', 'a/1')\n\n                tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n                tu.providerRejectsAndPreviousTimeoutProviderThatAcceptedIsUsed(3, 'a/[1]', 'a/1')\n                done()\n            }, 40)\n        }, 40)\n    })\n\n    it.skip('1 rejects and 2 accepts later and dies and 3 wins', (done) => {\n        tu.providerWillGetSubscriptionFound(2, 'a/[0-9]', 'a/1')\n        tu.providerListensTo(3, 'a/[1]')\n\n        setTimeout(() => {\n            tu.providerWillGetSubscriptionFound(3, 'a/[1]', 'a/1')\n            // first provider timeout\n            setTimeout(() => {\n                tu.providerRejects(1, 'a/.*', 'a/1')\n\n                tu.providerAcceptsButIsntAcknowledged(2, 'a/[0-9]', 'a/1')\n\n                tu.providerLosesItsConnection(2, 'a/[0-9]')\n\n                tu.providerRejects(3, 'a/[1]', 'a/1')\n                done()\n            }, 40)\n        }, 40)\n    })\n\n    it.skip('provider 1 and 2 times out and 3 rejects, 1 and 2 accepts later and 1 wins', (done) => {\n        tu.providerWillGetListenTimeout(1, 'a/1')\n        tu.providerWillGetListenTimeout(2, 'a/1')\n\n        tu.providerWillGetSubscriptionFound(2, 'a/[0-9]', 'a/1')\n        tu.providerListensTo(3, 'a/[1]')\n\n        setTimeout(() => {\n            tu.providerWillGetSubscriptionFound(3, 'a/[1]', 'a/1')\n            // first provider\n            setTimeout(() => {\n                // 10. provider 1 responds with ACCEPT\n                tu.providerAcceptsButIsntAcknowledged(1, 'a/.*', 'a/1')\n                // 11. provider 2 responds with ACCEPT\n                tu.providerAcceptsAndIsSentSubscriptionRemoved(2, 'a/[0-9]', 'a/1')\n                // 12. provider 3 responds with reject\n                tu.publishUpdateWillBeSentToSubscribers('a/1', true)\n                tu.providerRejectsAndPreviousTimeoutProviderThatAcceptedIsUsed(3, 'a/[1]', 'a/1')\n                done()\n            }, 40)\n        }, 40)\n    })\n})\n})\n"
  },
  {
    "path": "src/listen/listener-registry.ts",
    "content": "import { EVENT_ACTION, RECORD_ACTION, TOPIC, ListenMessage, STATE_REGISTRY_TOPIC } from '../constants'\nimport { EVENT, SubscriptionListener, DeepstreamConfig, DeepstreamServices, Provider, SocketWrapper, StateRegistry, SubscriptionRegistry } from '@deepstream/types'\nimport { shuffleArray } from '../utils/utils'\n\ninterface ListenInProgress {\n  queryProvider: Provider,\n  remainingProviders: Provider[]\n}\n\nexport class ListenerRegistry implements SubscriptionListener {\n  private providerRegistry: SubscriptionRegistry\n  private uniqueLockName = `${this.topic}_LISTEN_LOCK`\n  private patterns = new Map<string, RegExp>()\n  private locallyProvidedRecords = new Map<string, Provider>()\n  private messageTopic: TOPIC | STATE_REGISTRY_TOPIC\n  private actions: typeof RECORD_ACTION | typeof EVENT_ACTION\n\n  private listenInProgress = new Map<string, ListenInProgress>()\n  private unsuccesfulMatches = new Map<string, number>()\n\n  private clusterProvidedRecords: StateRegistry\n  private rematchInterval!: NodeJS.Timer\n\n  /**\n  * Deepstream.io allows clients to register as listeners for subscriptions.\n  * This allows for the creation of 'active' data-providers,\n  * e.g. data providers that provide data on the fly, based on what clients\n  * are actually interested in.\n  *\n  * When a client registers as a listener, it provides a regular expression.\n  * It will then immediatly get a number of callbacks for existing record subscriptions\n  * whose names match that regular expression.\n  *\n  * After that, whenever a record with a name matching that regular expression is subscribed\n  * to for the first time, the listener is notified.\n  *\n  * Whenever the last subscription for a matching record is removed, the listener is also\n  * notified with a SUBSCRIPTION_FOR_PATTERN_REMOVED action\n  *\n  * This class manages the matching of patterns and record names. The subscription /\n  * notification logic is handled by this.providerRegistry\n  */\n  constructor (private topic: TOPIC, private config: DeepstreamConfig, private services: DeepstreamServices, private clientRegistry: SubscriptionRegistry, private metaData: any = {}) {\n    this.actions = topic === TOPIC.RECORD ? RECORD_ACTION : EVENT_ACTION\n\n    this.triggerNextProvider = this.triggerNextProvider.bind(this)\n\n    if (this.topic === TOPIC.RECORD) {\n      this.providerRegistry = this.services.subscriptions.getSubscriptionRegistry(\n        STATE_REGISTRY_TOPIC.RECORD_LISTEN_PATTERNS,\n        STATE_REGISTRY_TOPIC.RECORD_LISTEN_PATTERNS,\n      )\n      this.clusterProvidedRecords = this.services.clusterStates.getStateRegistry(STATE_REGISTRY_TOPIC.RECORD_PUBLISHED_SUBSCRIPTIONS)\n      this.messageTopic = STATE_REGISTRY_TOPIC.RECORD_LISTENING\n    } else {\n      this.providerRegistry = this.services.subscriptions.getSubscriptionRegistry(\n        STATE_REGISTRY_TOPIC.EVENT_LISTEN_PATTERNS,\n        STATE_REGISTRY_TOPIC.EVENT_LISTEN_PATTERNS,\n      )\n      this.clusterProvidedRecords = this.services.clusterStates.getStateRegistry(STATE_REGISTRY_TOPIC.EVENT_PUBLISHED_SUBSCRIPTIONS)\n      this.messageTopic = STATE_REGISTRY_TOPIC.EVENT_LISTENING\n    }\n\n    this.providerRegistry.setAction('subscribe', this.actions.LISTEN)\n    this.providerRegistry.setAction('unsubscribe', this.actions.UNLISTEN)\n    this.providerRegistry.setSubscriptionListener({\n      onLastSubscriptionRemoved: this.removeLastPattern.bind(this),\n      onSubscriptionRemoved: this.removePattern.bind(this),\n      onFirstSubscriptionMade: this.addPattern.bind(this),\n      onSubscriptionMade: this.reconcileSubscriptionsToPatterns.bind(this),\n    })\n\n    this.clusterProvidedRecords.onAdd(this.onRecordStartProvided.bind(this))\n    this.clusterProvidedRecords.onRemove(this.onRecordStopProvided.bind(this))\n\n    this.services.clusterNode.subscribe(\n      this.messageTopic,\n      this.onIncomingMessage.bind(this),\n    )\n\n    if (this.config.listen.rematchInterval > 1000) {\n      this.rematchInterval = setInterval(() => {\n        this.patterns.forEach((value, pattern) => this.reconcileSubscriptionsToPatterns(pattern))\n      }, this.config.listen.rematchInterval)\n    } else {\n      this.services.logger.warn(EVENT.INVALID_CONFIG_DATA, 'Setting listen.rematchInterval to less than a second is not permitted.')\n    }\n  }\n\n  public async close () {\n    clearInterval(this.rematchInterval)\n  }\n\n  /**\n  * Returns whether or not a provider exists for\n  * the specific subscriptionName\n  */\n  public hasActiveProvider (susbcriptionName: string): boolean {\n    return this.clusterProvidedRecords.has(susbcriptionName)\n  }\n\n  /**\n  * The main entry point to the handle class.\n  * Called on any of the following actions:\n  *\n  * 1) ACTIONS.LISTEN\n  * 2) ACTIONS.UNLISTEN\n  * 3) ACTIONS.LISTEN_ACCEPT\n  * 4) ACTIONS.LISTEN_REJECT\n  */\n  public handle (socketWrapper: SocketWrapper, message: ListenMessage): void {\n    if (message.action === this.actions.LISTEN) {\n      this.addListener(socketWrapper, message)\n      return\n    }\n\n    if (message.action === this.actions.UNLISTEN) {\n      this.providerRegistry.unsubscribe(message.name, message, socketWrapper)\n      return\n    }\n\n    if (message.action === this.actions.LISTEN_ACCEPT || message.action === this.actions.LISTEN_REJECT) {\n      this.processResponseForListenInProgress(socketWrapper, message)\n      return\n    }\n\n    this.services.logger.warn(EVENT.UNKNOWN_ACTION, `Unknown action for topic ${TOPIC[message.topic]} action ${message.action}`)\n  }\n\n  /**\n  * Handle messages that arrive via the message bus\n  */\n  private onIncomingMessage (message: ListenMessage, serverName: string): void {\n    if (message.action === this.actions.LISTEN_UNSUCCESSFUL) {\n      if (this.hasActiveProvider(message.subscription) === false) {\n        const unsuccesfulTimeStamp = this.unsuccesfulMatches.get(message.subscription)\n        if (!unsuccesfulTimeStamp || unsuccesfulTimeStamp - Date.now() > this.config.listen.matchCooldown) {\n          this.onFirstSubscriptionMade(message.subscription)\n        }\n        return\n      }\n    }\n  }\n\n  /**\n  * Process an accept or reject for a listen that is currently in progress\n  * and hasn't timed out yet.\n  */\n  private processResponseForListenInProgress (socketWrapper: SocketWrapper, message: ListenMessage): void {\n    const inProgress = this.listenInProgress.get(message.subscription)\n    if (!inProgress || !inProgress.queryProvider) {\n      // This should send a message saying response is invalid\n      return\n    }\n    clearTimeout(inProgress.queryProvider.responseTimeout!)\n\n    if (message.action === this.actions.LISTEN_ACCEPT) {\n      this.accept(socketWrapper, message)\n      return\n    }\n\n    if (message.action === this.actions.LISTEN_REJECT) {\n      this.triggerNextProvider(message.subscription)\n      return\n    }\n  }\n\n  /**\n  * Called by the record subscription registry whenever a subscription count goes down to zero\n  * Part of the subscriptionListener interface.\n  */\n  public onFirstSubscriptionMade (subscriptionName: string): void {\n    this.startProviderSearch(subscriptionName)\n  }\n\n  public onSubscriptionMade (subscriptionName: string, socketWrapper: SocketWrapper): void {\n    if (this.hasActiveProvider(subscriptionName)) {\n      this.sendHasProviderUpdateToSingleSubscriber(true, socketWrapper, subscriptionName)\n      return\n    }\n  }\n\n  public onLastSubscriptionRemoved (subscriptionName: string): void {\n    this.unsuccesfulMatches.delete(subscriptionName)\n\n    const provider = this.locallyProvidedRecords.get(subscriptionName)\n\n    if (!provider) {\n      return\n    }\n\n    this.sendSubscriptionForPatternRemoved(provider, subscriptionName)\n    this.removeActiveListener(subscriptionName)\n  }\n\n  /**\n  * Called by the record subscription registry whenever the subscription count increments.\n  * Part of the subscriptionListener interface.\n  */\n  public onSubscriptionRemoved (subscriptionName: string, socketWrapper: SocketWrapper): void {\n  }\n\n  /**\n  * Register callback for when the server recieves an accept message from the client\n  */\n  private accept (socketWrapper: SocketWrapper, message: ListenMessage): void {\n    const subscriptionName = message.subscription\n\n    const provider = {\n      socketWrapper,\n      pattern: message.name,\n      closeListener: this.removePattern.bind(this, message.name, socketWrapper)\n    }\n\n    this.locallyProvidedRecords.set(subscriptionName, provider)\n    socketWrapper.onClose(provider.closeListener)\n\n    this.clusterProvidedRecords.add(subscriptionName)\n    this.stopProviderSearch(subscriptionName)\n  }\n\n  /**\n  * Register a client as a listener for record subscriptions\n  */\n  private addListener (socketWrapper: SocketWrapper, message: ListenMessage): void {\n    const regExp = this.validatePattern(socketWrapper, message)\n\n    if (!regExp) {\n      // TODO: Send an invalid pattern here?\n      return\n    }\n\n    this.providerRegistry.subscribe(message.name, message, socketWrapper)\n  }\n\n  /**\n  * Find subscriptions that match pattern, and notify them that\n  * they can be provided.\n  *\n  * We will attempt to notify all possible providers rather than\n  * just the single provider for load balancing purposes and\n  * so that the one listener doesnt potentially get overwhelmed.\n  */\n  private reconcileSubscriptionsToPatterns (pattern: string, socketWrapper?: SocketWrapper): void {\n    const regExp = this.patterns.get(pattern)!\n    const names = this.clientRegistry.getNames()\n\n    for (let i = 0; i < names.length; i++) {\n      const subscriptionName = names[i]\n\n      if (this.locallyProvidedRecords.has(subscriptionName)) {\n        continue\n      }\n\n      if (!subscriptionName.match(regExp)) {\n        continue\n      }\n\n      const listenInProgress = this.listenInProgress.get(subscriptionName)\n\n      if (listenInProgress && socketWrapper) {\n        listenInProgress.remainingProviders.push({ socketWrapper, pattern })\n      } else if (listenInProgress) {\n        // A reconsile happened while listen is still in progress, ignore\n      } else {\n        this.startProviderSearch(subscriptionName)\n      }\n    }\n  }\n\n  /**\n  * Removes the listener if it is the currently active publisher, and retriggers\n  * another listener discovery phase\n  */\n  private removeListenerIfActive (pattern: string, socketWrapper: SocketWrapper): void {\n    for (const [subscriptionName, provider] of this.locallyProvidedRecords) {\n      if (\n        provider.socketWrapper === socketWrapper &&\n        provider.pattern === pattern\n      ) {\n        if (provider.closeListener) {\n          provider.socketWrapper.removeOnClose(provider.closeListener)\n        }\n        this.removeActiveListener(subscriptionName)\n        if (this.clientRegistry.hasLocalSubscribers(subscriptionName)) {\n          this.startProviderSearch(subscriptionName)\n        }\n      }\n    }\n  }\n\n  /**\n    */\n  private removeActiveListener (subscriptionName: string): void {\n    this.clusterProvidedRecords.remove(subscriptionName)\n    this.locallyProvidedRecords.delete(subscriptionName)\n  }\n\n  /**\n  * Start discovery phase once a lock is obtained from the leader within\n  * the cluster\n  */\n  private startProviderSearch (subscriptionName: string): void {\n    const localListenArray = this.createLocalListenArray(subscriptionName)\n\n    if (localListenArray.length === 0) {\n      return\n    }\n\n    this.services.locks.get(this.getUniqueLockName(subscriptionName), (success: boolean) => {\n      if (!success) {\n        return\n      }\n\n      if (this.hasActiveProvider(subscriptionName)) {\n        this.services.locks.release(this.getUniqueLockName(subscriptionName))\n        return\n      }\n\n      this.startLocalDiscoveryStage(subscriptionName, localListenArray)\n    })\n  }\n\n  /**\n  * Start discovery phase once a lock is obtained from the leader within\n  * the cluster\n  */\n  private startLocalDiscoveryStage (subscriptionName: string, localListenArray: Provider[]): void {\n    this.services.logger.debug(\n      EVENT.LOCAL_LISTEN,\n      `started for ${TOPIC[this.topic] || STATE_REGISTRY_TOPIC[this.topic]}:${subscriptionName}`,\n      this.metaData,\n    )\n    this.triggerNextProvider(subscriptionName, localListenArray)\n  }\n\n  /**\n  * Trigger the next provider in the map of providers capable of publishing\n  * data to the specific subscriptionName\n  */\n private triggerNextProvider (subscriptionName: string, localListenArray?: Provider[]): void {\n  let listenInProgress = this.listenInProgress.get(subscriptionName)\n  let provider: Provider\n\n  if (localListenArray) {\n    provider = localListenArray.shift()!\n    listenInProgress = {\n      queryProvider: provider,\n      remainingProviders: localListenArray\n    }\n    this.listenInProgress.set(subscriptionName, listenInProgress)\n  } else  if (listenInProgress) {\n    if (listenInProgress.remainingProviders.length === 0) {\n      this.stopProviderSearch(subscriptionName)\n      return\n    }\n\n    provider = listenInProgress.remainingProviders.shift()!\n    listenInProgress.queryProvider = provider\n  } else {\n    this.services.logger.warn('triggerNextProvider', 'no listen in progress', this.metaData)\n    return\n  }\n\n  // const subscribers = this.clientRegistry.getLocalSubscribers(subscriptionName)\n  // This stops a client from subscribing to itself, I think\n  // if (subscribers && subscribers.has(provider!.socketWrapper)) {\n  //   this.services.logger.debug(EVENT.LOCAL_LISTEN, `Ignoring socket since it would be subscribing to itself for ${subscriptionName}`)\n  //   this.triggerNextProvider(subscriptionName)\n  //   return\n  // }\n\n  provider!.responseTimeout = setTimeout(() => {\n    provider!.socketWrapper.sendMessage({\n      topic: this.topic,\n      action: this.actions.LISTEN_RESPONSE_TIMEOUT,\n      subscription: subscriptionName\n    })\n    this.triggerNextProvider(subscriptionName)\n  }, this.config.listen.responseTimeout)\n\n  this.sendSubscriptionForPatternFound(provider!, subscriptionName)\n}\n\n  /**\n  * Finalises a local listener discovery stage\n  */\n  private stopProviderSearch (subscriptionName: string): void {\n    this.services.logger.debug(\n      EVENT.LOCAL_LISTEN,\n      `stopped for ${TOPIC[this.topic] || STATE_REGISTRY_TOPIC[this.topic]}:${subscriptionName}`,\n      this.metaData,\n    )\n\n    this.services.locks.release(this.getUniqueLockName(subscriptionName))\n\n    const stoppedSearch = this.listenInProgress.delete(subscriptionName)\n    if (stoppedSearch) {\n      if (this.hasActiveProvider(subscriptionName) === false) {\n        this.unsuccesfulMatches.set(subscriptionName, Date.now())\n        this.services.clusterNode.send({\n          topic: this.messageTopic,\n          action: this.actions.LISTEN_UNSUCCESSFUL,\n          subscription: subscriptionName\n        })\n      }\n      return\n    }\n\n    this.services.logger.warn(\n      EVENT.LOCAL_LISTEN,\n      `nothing to stop for ${TOPIC[this.topic] || STATE_REGISTRY_TOPIC[this.topic]}:${subscriptionName}`,\n      this.metaData,\n    )\n  }\n\n  /**\n  * Triggered when a subscription is being provided by a node in the cluster\n  */\n  private onRecordStartProvided (subscriptionName: string): void {\n    this.sendHasProviderUpdate(true, subscriptionName)\n  }\n\n  /**\n  * Triggered when a subscription is stopped being provided by a node in the cluster\n  */\n  private onRecordStopProvided (subscriptionName: string): void {\n    this.services.logger.info(\n      'LISTEN_PROVIDER_STOPPED',\n      `listen provider has stopped for ${TOPIC[this.topic]}:${subscriptionName}`,\n      this.metaData,\n    )\n\n    this.sendHasProviderUpdate(false, subscriptionName)\n    if (!this.hasActiveProvider(subscriptionName) && this.clientRegistry.hasName(subscriptionName)) {\n      this.startProviderSearch(subscriptionName)\n    }\n  }\n\n  /**\n  * Compiles a regular expression from an incoming pattern\n  */\n  private addPattern (pattern: string): void {\n    if (!this.patterns.has(pattern)) {\n      this.patterns.set(pattern, new RegExp(pattern))\n    }\n  }\n\n  /**\n  * Deletes the pattern regex when removed\n  */\n  private removePattern (pattern: string, socketWrapper: SocketWrapper): void {\n    this.removeListenerFromInProgress(this.listenInProgress, pattern, socketWrapper)\n    this.removeListenerIfActive(pattern, socketWrapper)\n  }\n\n  private removeLastPattern (pattern: string): void {\n    this.patterns.delete(pattern)\n  }\n\n  /**\n  * Remove provider from listen in progress map if it unlistens during discovery stage\n  */\n  private removeListenerFromInProgress (listensCurrentlyInProgress: Map<string, ListenInProgress>, pattern: string, socketWrapper: SocketWrapper): void {\n    for (const [subscriptionName, listensInProgress] of listensCurrentlyInProgress) {\n      listensInProgress.remainingProviders = listensInProgress.remainingProviders.filter((provider: Provider) => {\n        return provider.socketWrapper === socketWrapper && provider.pattern === pattern\n      })\n      if (listensInProgress.remainingProviders.length === 0) {\n        this.stopProviderSearch(subscriptionName)\n      }\n    }\n  }\n\n  /**\n  * Sends a has provider update to a single subcriber\n  */\n  private sendHasProviderUpdateToSingleSubscriber (hasProvider: boolean, socketWrapper: SocketWrapper, subscriptionName: string): void {\n    if (socketWrapper && this.topic === TOPIC.RECORD) {\n      socketWrapper.sendMessage({\n        topic: this.topic,\n        action: hasProvider ? RECORD_ACTION.SUBSCRIPTION_HAS_PROVIDER : RECORD_ACTION.SUBSCRIPTION_HAS_NO_PROVIDER,\n        name: subscriptionName,\n      })\n    }\n  }\n\n  /**\n  * Sends a has provider update to all subcribers\n  */\n  private sendHasProviderUpdate (hasProvider: boolean, subscriptionName: string): void  {\n    if (this.topic !== TOPIC.RECORD) {\n      return\n    }\n    this.clientRegistry.sendToSubscribers(subscriptionName, {\n      topic: this.topic,\n      action: hasProvider ? RECORD_ACTION.SUBSCRIPTION_HAS_PROVIDER : RECORD_ACTION.SUBSCRIPTION_HAS_NO_PROVIDER,\n      name: subscriptionName,\n    }, false, null)\n  }\n\n  /**\n  * Send a subscription found to a provider\n  */\n  private sendSubscriptionForPatternFound (provider: Provider, subscriptionName: string): void  {\n    provider.socketWrapper.sendMessage({\n      topic: this.topic,\n      action: this.actions.SUBSCRIPTION_FOR_PATTERN_FOUND,\n      name: provider.pattern,\n      subscription: subscriptionName,\n    })\n  }\n\n  /**\n  * Send a subscription removed to a provider\n  */\n  private sendSubscriptionForPatternRemoved (provider: Provider, subscriptionName: string): void {\n    provider.socketWrapper.sendMessage({\n      topic: this.topic,\n      action: this.actions.SUBSCRIPTION_FOR_PATTERN_REMOVED,\n      name: provider.pattern,\n      subscription: subscriptionName,\n    })\n  }\n\n  /**\n  * Create a map of all the listeners that patterns match the subscriptionName locally\n  */\n  private createLocalListenArray (subscriptionName: string): Provider[] {\n    const providers: Provider[] = []\n    this.patterns.forEach((regex, pattern) => {\n      if (regex.test(subscriptionName)) {\n        for (const socketWrapper of this.providerRegistry.getLocalSubscribers(pattern)) {\n          providers.push({ pattern, socketWrapper })\n        }\n      }\n    })\n    if (!this.config.listen.shuffleProviders) {\n      return providers\n    }\n    return shuffleArray(providers)\n  }\n\n  /**\n  * Validates that the pattern is not empty and is a valid regular expression\n  */\n  private validatePattern (socketWrapper: SocketWrapper, message: ListenMessage): RegExp | null {\n    try {\n      return new RegExp(message.name)\n    } catch (e) {\n      socketWrapper.sendMessage({\n        topic: this.topic,\n        action: this.actions.INVALID_LISTEN_REGEX,\n        name: message.name\n      })\n      this.services.logger.warn(this.actions[this.actions.INVALID_LISTEN_REGEX], `${e}`, this.metaData)\n      return null\n    }\n  }\n\n  /**\n  * Returns the unique lock when leading a listen discovery phase\n  */\n  private getUniqueLockName (subscriptionName: string) {\n    return `${this.uniqueLockName}_${subscriptionName}`\n  }\n}\n"
  },
  {
    "path": "src/listen/listener-test-utils.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport { ListenerRegistry } from './listener-registry'\nimport * as testHelper from '../test/helper/test-helper'\nimport * as C from '../constants'\nimport { getTestMocks } from '../test/helper/test-mocks'\nimport * as sinon from 'sinon'\nimport { SocketWrapper, SubscriptionRegistry } from '@deepstream/types'\nimport { TOPIC, ListenMessage } from '../constants'\n\nexport default class ListenerTestUtils {\n  private actions: any\n  private subscribedTopics: string[] = []\n\n  private topic: TOPIC.RECORD | TOPIC.EVENT\n  private subscribers = new Set()\n  private clientRegistryMock: any\n  private providers: Array<{\n    socketWrapper: SocketWrapper,\n    socketWrapperMock: any\n  }>\n  private clients: Array<{\n    socketWrapper: SocketWrapper,\n    socketWrapperMock: any\n  }>\n  private listenerRegistry: ListenerRegistry\n  private clientRegistry: SubscriptionRegistry\n\n  constructor (listenerTopic?: TOPIC.RECORD | TOPIC.EVENT) {\n    const { config, services } = testHelper.getDeepstreamOptions()\n\n    this.topic = listenerTopic || C.TOPIC.RECORD\n\n    if (this.topic === C.TOPIC.RECORD) {\n      this.actions = C.RECORD_ACTION\n    } else {\n      this.actions = C.EVENT_ACTION\n    }\n\n    const self = this\n    this.clientRegistry = {\n      hasName (subscriptionName: string) {\n        return self.subscribedTopics.indexOf(subscriptionName) === -1\n      },\n      getNames () {\n        return self.subscribedTopics\n      },\n      getLocalSubscribers () {\n        return self.subscribers\n      },\n      hasLocalSubscribers () {\n        return self.subscribers.size > 0\n      },\n      sendToSubscribers: () => {}\n    } as never as SubscriptionRegistry\n    this.clientRegistryMock = sinon.mock(this.clientRegistry)\n\n    config.listen.responseTimeout = 30\n    config.listen.shuffleProviders = false\n    // config.stateReconciliationTimeout = 10\n\n    this.clients = [\n      // @ts-ignore\n      null, // to make tests start from 1\n      getTestMocks().getSocketWrapper('c1'),\n      getTestMocks().getSocketWrapper('c2'),\n      getTestMocks().getSocketWrapper('c3')\n    ]\n\n    this.providers = [\n      // @ts-ignore\n      null, // to make tests start from 1\n      getTestMocks().getSocketWrapper('p1'),\n      getTestMocks().getSocketWrapper('p2'),\n      getTestMocks().getSocketWrapper('p3')\n    ]\n\n    this.listenerRegistry = new ListenerRegistry(self.topic, config, services, self.clientRegistry)\n    expect(typeof self.listenerRegistry.handle).to.equal('function')\n  }\n\n  public complete () {\n    this.clients.forEach((client) => {\n      if (client) {\n        client.socketWrapperMock.verify()\n      }\n    })\n    this.providers.forEach((provider) => {\n      if (provider) {\n        provider.socketWrapperMock.verify()\n      }\n    })\n    this.clientRegistryMock.verify()\n  }\n\n  /**\n  * Provider Utils\n  */\n  public providerListensTo (provider: number, pattern: string): void {\n    this.providers[provider].socketWrapperMock\n      .expects('sendAckMessage')\n      .once()\n      .withExactArgs({\n        topic: this.topic,\n        action: this.actions.LISTEN,\n        name: pattern\n      })\n\n    this.listenerRegistry.handle(this.providers[provider].socketWrapper, {\n        topic: this.topic,\n        action: this.actions.LISTEN,\n        name: pattern\n      } as never as ListenMessage)\n  }\n\n  public providerUnlistensTo (provider: number, pattern: string) {\n    this.providers[provider].socketWrapperMock\n      .expects('sendAckMessage')\n      .once()\n      .withExactArgs({\n        topic: this.topic,\n        action: this.actions.UNLISTEN,\n        name: pattern\n      })\n\n    this.listenerRegistry.handle(this.providers[provider].socketWrapper, {\n      topic: this.topic,\n      action: this.actions.UNLISTEN,\n      name: pattern,\n    } as never as ListenMessage)\n  }\n\n  public providerWillGetListenTimeout (provider: number, subscription: string) {\n    this.providers[provider].socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: this.topic,\n        action: this.actions.LISTEN_RESPONSE_TIMEOUT,\n        subscription\n      })\n  }\n\n  public providerWillGetSubscriptionFound (provider: number, pattern: string, subscription: string) {\n    this.providers[provider].socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: this.topic,\n        action: this.actions.SUBSCRIPTION_FOR_PATTERN_FOUND,\n        name: pattern,\n        subscription\n      })\n  }\n\n  public providerWillGetSubscriptionRemoved (provider: number, pattern: string, subscription: string) {\n    this.providers[provider].socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: this.topic,\n        action: this.actions.SUBSCRIPTION_FOR_PATTERN_REMOVED,\n        name: pattern,\n        subscription\n      })\n  }\n\n  public providerAcceptsButIsntAcknowledged (provider: number, pattern: string, subscriptionName: string) {\n    this.providerAccepts(provider, pattern, subscriptionName, true)\n  }\n\n  public providerAccepts (provider: number, pattern: string, subscription: string, doesnthaveActiveProvider: boolean) {\n    this.listenerRegistry.handle(this.providers[provider].socketWrapper, {\n      topic: this.topic,\n      action: this.actions.LISTEN_ACCEPT,\n      name: pattern,\n      subscription\n    })\n    expect(this.listenerRegistry.hasActiveProvider(subscription)).to.equal(!doesnthaveActiveProvider)\n  }\n\n  public providerRejectsAndPreviousTimeoutProviderThatAcceptedIsUsed (provider: number, pattern: string, subscriptionName: string) {\n    this.providerRejects(provider, pattern, subscriptionName, true)\n  }\n\n  public providerAcceptsAndIsSentSubscriptionRemoved (provider: number, pattern: string, subscriptionName: string) {\n    this.providerWillGetSubscriptionRemoved(provider, pattern, subscriptionName)\n    this.providerAcceptsButIsntAcknowledged(provider, pattern, subscriptionName)\n  }\n\n  public providerRejects (provider: number, pattern: string, subscription: string, doNotCheckActiveProvider: boolean) {\n    this.listenerRegistry.handle(this.providers[provider].socketWrapper, {\n      topic: this.topic,\n      action: this.actions.LISTEN_REJECT,\n      name: pattern,\n      subscription\n    })\n\n    if (!doNotCheckActiveProvider) {\n      expect(this.listenerRegistry.hasActiveProvider(subscription)).to.equal(false)\n    }\n  }\n\n  public acceptMessageThrowsError (provider: number, pattern: string, subscription: string) {\n    this.listenerRegistry.handle(this.providers[provider].socketWrapper, {\n      topic: this.topic,\n      action: this.actions.LISTEN_ACCEPT,\n      name: pattern,\n      subscription\n    })\n    // verify( providers[ provider], this.actions.ERROR, [ C.EVENT.INVALID_MESSAGE, this.actions.LISTEN_ACCEPT, pattern, subscriptionName ] );\n  }\n\n  public rejectMessageThrowsError (provider: number, pattern: string, subscription: string) {\n    this.listenerRegistry.handle(this.providers[provider].socketWrapper, {\n      topic: this.topic,\n      action: this.actions.LISTEN_REJECT,\n      name: pattern,\n      subscription\n    })\n\n    // TODO\n    // verify( providers[ provider], this.actions.ERROR, [ C.EVENT.INVALID_MESSAGE, this.actions.LISTEN_REJECT, pattern, subscriptionName ] );\n  }\n\n  public providerLosesItsConnection (provider: number) {\n    // (this.providers[provider].socketWrapper as any).emit('close', this.providers[provider].socketWrapper)\n  }\n\n  /**\n  * Subscriber Utils\n  */\n  public subscriptionAlreadyMadeFor (subscriptionName: string) {\n    this.subscribedTopics.push(subscriptionName)\n  }\n\n  public clientSubscribesTo (client: number, subscriptionName: string, firstSubscription: boolean) {\n    if (firstSubscription) {\n      this.listenerRegistry.onFirstSubscriptionMade(subscriptionName)\n    }\n    this.listenerRegistry.onSubscriptionMade(subscriptionName, this.clients[client].socketWrapper)\n    this.subscribedTopics.push(subscriptionName)\n    this.subscribers.add(this.clients[client].socketWrapper)\n  }\n\n  public clientUnsubscribesTo (client: number, subscriptionName: string, lastSubscription: boolean) {\n    if (lastSubscription) {\n      this.listenerRegistry.onLastSubscriptionRemoved(subscriptionName)\n    }\n    this.listenerRegistry.onSubscriptionRemoved(subscriptionName, this.clients[client].socketWrapper)\n    this.subscribedTopics.splice(this.subscribedTopics.indexOf(subscriptionName), 1)\n    this.subscribers.delete(this.clients[client].socketWrapper)\n  }\n\n  public clientWillRecievePublishedUpdate (client: number, subscription: string, state: boolean) {\n    this.clients[client].socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: this.topic,\n        action: state ? this.actions.SUBSCRIPTION_HAS_PROVIDER : this.actions.SUBSCRIPTION_HAS_NO_PROVIDER,\n        name: subscription,\n      })\n  }\n\n  public publishUpdateWillBeSentToSubscribers (subscription: string, state: boolean) {\n    this.clientRegistryMock\n      .expects('sendToSubscribers')\n      .once()\n      .withExactArgs(subscription, {\n        topic: this.topic,\n        action: state ? this.actions.SUBSCRIPTION_HAS_PROVIDER : this.actions.SUBSCRIPTION_HAS_NO_PROVIDER,\n        name: subscription,\n      }, false, null)\n  }\n\n  public subscriptionHasActiveProvider (subscription: string, value: string) {\n    expect(this.listenerRegistry.hasActiveProvider(subscription)).to.equal(value)\n  }\n}\n"
  },
  {
    "path": "src/plugins/heap-snapshot/heap-snapshot.ts",
    "content": "import { DeepstreamPlugin, DeepstreamServices, EVENT } from '@deepstream/types'\nimport { writeHeapSnapshot } from 'v8'\nimport { existsSync, mkdirSync } from 'fs'\n\ninterface HeapSnapshotOptions {\n    interval: number,\n    outputDir: string\n}\n\n/**\n * This plugin will log the handshake data on login/logout and send a custom event to the logged-in\n * client.\n */\nexport default class HeapSnapshot extends DeepstreamPlugin {\n    public description = 'V8 Memory Analysis'\n    private logger = this.services.logger.getNameSpace('MEMORY_ANALYSIS')\n    private snapshotInterval!: NodeJS.Timer\n\n    constructor (private options: HeapSnapshotOptions, private services: Readonly<DeepstreamServices>) {\n        super()\n    }\n\n    public init () {\n        if (typeof this.options.interval !== 'number') {\n            this.logger.fatal(EVENT.ERROR, 'Invalid or missing \"interval\"')\n        }\n        if (this.options.interval < 10000) {\n            this.logger.fatal(EVENT.ERROR, 'interval must be above 10000')\n        }\n        if (typeof this.options.outputDir !== 'string') {\n            this.logger.fatal(EVENT.ERROR, 'Invalid or missing \"outputDir\"')\n        }\n        if (existsSync(this.options.outputDir) === false) {\n            mkdirSync(this.options.outputDir, 0o744)\n        }\n    }\n\n    public async whenReady (): Promise<void> {\n        this.snapshotInterval = setInterval(() => this.outputHeapSnapshot(), this.options.interval)\n    }\n\n    public async close (): Promise<void> {\n        clearInterval(this.snapshotInterval)\n    }\n\n    private outputHeapSnapshot () {\n        writeHeapSnapshot(`${this.options.outputDir}/${Date.now()}-${process.pid}.heapsnapshot`)\n        this.logger.info(EVENT.INFO, 'Taking a heap snapshot. This might affect your CPU usage drastically.')\n    }\n}"
  },
  {
    "path": "src/service/daemon.ts",
    "content": "// Handle input parameters\nimport { spawn, ChildProcess } from 'child_process'\n\nconst maxMilliseconds = 5000\n\nfunction _start (options: any) {\n  let startTime: number | null = null\n  let child!: ChildProcess\n  let attempts = 0\n  let starts = 0\n  let wait = 1\n\n  /**\n   * Monitor the process to make sure it is running\n   */\n  function monitor () {\n    if (!child.pid) {\n      // If the number of periodic starts exceeds the max, kill the process\n      if (starts >= options.maxRetries) {\n        if ((Date.now() - startTime!) > maxMilliseconds) {\n          console.error(\n            `Too many restarts within the last ${maxMilliseconds / 1000} seconds. Please check the script.`\n          )\n          process.exit(1)\n        }\n      }\n\n      setTimeout(() => {\n        wait = wait * options.growPercentage\n        attempts += 1\n        if (attempts > options.maxRestarts && options.maxRestarts >= 0) {\n          console.error(\n            `${options.name} will not be restarted because the maximum number of total restarts has been exceeded.`\n          )\n          process.exit()\n        } else {\n          launch()\n        }\n      }, wait)\n    } else {\n      attempts = 0\n      wait = options.restartDelay * 1000\n    }\n  }\n\n  /**\n   * @method launch\n   * A method to start a process.\n   */\n  function launch () {\n    // Set the start time if it's null\n    if (startTime === null) {\n      startTime = startTime || Date.now()\n      setTimeout(() => {\n        startTime = null\n        starts = 0\n      }, ( maxMilliseconds ) + 2000)\n    }\n    starts += 1\n\n    // Fork the child process piping stdin/out/err to the parent\n    // NOTE: ADDING process.pkg.entrypoint as first arg to the spawned process is a workaround to fix a pkg bug\n    // https://github.com/vercel/pkg/issues/1356\n    // @ts-ignore\n    child = spawn(options.processExec, [process.pkg.entrypoint, 'start'].concat(process.argv.slice(2)), {\n      env: process.env\n    })\n\n    child.stdout!.on('data', function (data) {\n      process.stdout.write(data.toString())\n    })\n\n    child.stderr!.on('data', function (data) {\n      process.stderr.write(data.toString())\n    })\n\n    // When the child dies, attempt to restart based on configuration\n    child.on('exit', (code) => {\n      // If an error is thrown and the process is configured to exit, then kill the parent.\n      if (code !== 0 && options.exitOnError) {\n        console.error(`${options.name} exited with error code ${code}`)\n        process.exit()\n      }\n\n      // delete child.pid\n\n      monitor()\n    })\n  }\n\n  process.on('exit', () => {\n    child.removeAllListeners('exit')\n    if (child) {\n      child.kill()\n    }\n  })\n\n  process.on('SIGTERM', () => {\n    child.removeAllListeners('exit')\n    child.kill()\n  })\n\n  process.on('SIGHUP', () => {\n    child.removeAllListeners('exit')\n    child.kill()\n  })\n\n  process.on('SIGINT', () => {\n    child.removeAllListeners('exit')\n    child.kill()\n  })\n\n  launch()\n}\n\nexport const start = (daemonOptions: any) => {\n  const options = daemonOptions || {}\n  _start({\n    name: 'deepstream',\n    exitOnError: false,\n    growPercentage: 0.25,\n    maxRetries: 10,\n    restartDelay: 1,\n    ...options\n  })\n}\n"
  },
  {
    "path": "src/service/service.ts",
    "content": "import { exec } from 'child_process'\n\nimport { existsSync, unlinkSync, chmodSync, writeFileSync } from 'fs'\n\nimport systemdTemplate from './template/systemd'\nimport initdTemplate from './template/initd'\n\n/**\n * Returns true if system support systemd daemons\n * @return {Boolean}\n */\nfunction hasSystemD () {\n  return existsSync('/usr/lib/systemd/system') || existsSync('/bin/systemctl')\n}\n\n/**\n * Returns true if system support init.d daemons\n * @return {Boolean}\n */\nfunction hasSystemV () {\n  return existsSync('/etc/init.d')\n}\n\n/**\n * Deletes a service file from /etc/systemd/system/\n */\nasync function deleteSystemD (name: string, callback: Function) {\n  const filepath = `/etc/systemd/system/${name}.service`\n  console.log(`Removing service on: ${filepath}`)\n  const exists = existsSync(filepath)\n  if (!exists) {\n    callback(\"Service doesn't exists, nothing to uninstall\")\n    return\n  }\n  try {\n    unlinkSync(filepath)\n    const cmd = 'systemctl daemon-reload'\n    console.log('Running %s...', cmd)\n    exec(cmd, (e) => {\n      callback(e, 'SystemD service removed successfully')\n    })\n  } catch (e) {\n    callback(e)\n  }\n}\n\n/**\n * Installs a service file to /etc/systemd/system/\n *\n * It deals with logs, restarts and by default points\n * to the normal system install\n */\nasync function setupSystemD (name: string, options: any, callback: Function) {\n  options.stdOut = (options.logDir && `${options.logDir}/${name}-out.log`) || null\n  options.stdErr = (options.logDir && `${options.logDir}/${name}-err.log`) || null\n\n  const filepath = `/etc/systemd/system/${name}.service`\n\n  const script = systemdTemplate(options)\n\n  if (options.dryRun) {\n    console.log(script)\n    return\n  }\n\n  console.log(`Installing service on: ${filepath}`)\n\n  const exists = existsSync(filepath)\n  if (exists) {\n    callback('Service already exists, please uninstall first')\n    return\n  }\n  try {\n    writeFileSync(filepath, script)\n    chmodSync(filepath, '755')\n    const cmd = 'systemctl daemon-reload'\n    console.log('Running %s...', cmd)\n    exec(cmd, (e2) => {\n      callback(e2, 'SystemD service registered successfully')\n    })\n  } catch (e) {\n    callback(e)\n  }\n}\n\n/**\n * Deletes a service file from /etc/init.d/\n */\nasync function deleteSystemV (name: string, callback: Function) {\n  const filepath = `/etc/init.d/${name}`\n  console.log(`Removing service on: ${filepath}`)\n\n  const exists = existsSync(filepath)\n  if (!exists) {\n    callback(\"Service doesn't exists, nothing to uninstall\")\n    return\n  }\n\n  try {\n    unlinkSync(filepath)\n    callback(null, 'SystemD service removed successfully')\n  } catch (e) {\n    callback(e)\n  }\n}\n\n/**\n * Installs a service file to /etc/init.d/\n *\n * It deals with logs, restarts and by default points\n * to the normal system install\n */\nasync function setupSystemV (name: string, options: any, callback: Function) {\n  options.stdOut = (options.logDir && `${options.logDir}/${name}-out.log`) || '/dev/null'\n  options.stdErr = (options.logDir && `${options.logDir}/${name}-err.log`) || '&1'\n\n  const script = initdTemplate(options)\n\n  if (options.dryRun) {\n    console.log(script)\n    return\n  }\n\n  const filepath = `/etc/init.d/${name}`\n  console.log(`Installing service on: ${filepath}`)\n\n  const exists = existsSync(filepath)\n  if (exists) {\n    callback('Service already exists, please uninstall first')\n    return\n  }\n\n  try {\n    writeFileSync(filepath, script)\n    chmodSync(filepath, '755')\n    callback(null, 'init.d service registered successfully')\n  } catch (e) {\n    callback(e)\n  }\n}\n\n/**\n * Adds a service, either via systemd or init.d\n * @param {String}   name the name of the service\n * @param {Object}   options  options to configure deepstream service\n * @param {Function} callback called when complete\n */\nexport const add = (name: string, options: any, callback: Function) => {\n  options.name = name\n  options.pidFile = options.pidFile || `/var/run/${name}.pid`\n\n  options.exec = options.exec\n  options.logDir = options.logDir || '/var/log/deepstream'\n  options.user = options.user || 'root'\n  options.group = options.group || 'root'\n\n  if (options && !options.runLevels) {\n    options.runLevels = [2, 3, 4, 5].join(' ')\n  } else {\n    options.runLevels = options.runLevels.join(' ')\n  }\n\n  if (!options.programArgs) {\n    options.programArgs = []\n  }\n  options.deepstreamArgs = ['daemon'].concat(options.programArgs).join(' ')\n\n  if (hasSystemD()) {\n    setupSystemD(name, options, callback)\n  } else if (hasSystemV()) {\n    setupSystemV(name, options, callback)\n  } else {\n    callback('Only systemd and init.d services are currently supported.')\n  }\n}\n\n/**\n * Delete a service, either from systemd or init.d\n * @param {String}   name the name of the service\n * @param {Function} callback called when complete\n */\nexport const remove = (name: string, callback: Function) => {\n  if (hasSystemD()) {\n    deleteSystemD(name, callback)\n  } else if (hasSystemV()) {\n    deleteSystemV(name, callback)\n  } else {\n    callback('Only systemd and init.d services are currently supported.')\n  }\n}\n\n/**\n * Start a service, either from systemd or init.d\n * @param {String}   name the name of the service\n * @param {Function} callback called when complete\n */\nexport const start = (name: string, callback: Function) => {\n  if (hasSystemD() || hasSystemV()) {\n    exec(`service ${name} start`, (err, stdOut, stdErr) => {\n      callback(err || stdErr, stdOut)\n    })\n  } else {\n    callback('Only systemd and init.d services are currently supported.')\n  }\n}\n\n/**\n * Stop a service, either from systemd or init.d\n * @param {String}   name the name of the service\n * @param {Function} callback called when complete\n */\nexport const stop = (name: string, callback: Function) => {\n  if (hasSystemD() || hasSystemV()) {\n    exec(`service ${name} stop`, (err, stdOut, stdErr) => {\n      callback(err || stdErr, stdOut)\n    })\n  } else {\n    callback('Only systemd and init.d services are currently supported.')\n  }\n}\n\n/**\n * Get the status of the service, either from systemd or init.d\n * @param {String}   name the name of the service\n * @param {Function} callback called when complete\n */\nexport const status = (name: string, callback: Function) => {\n  if (hasSystemD() || hasSystemV()) {\n    exec(`service ${name} status`, (err, stdOut, stdErr) => {\n      callback(err || stdErr, stdOut)\n    })\n  } else {\n    callback('Only systemd and init.d services are currently supported.')\n  }\n}\n\n/**\n * Restart the service, either from systemd or init.d\n * @param {String}   name the name of the service\n * @param {Function} callback called when complete\n */\nexport const restart = (name: string, callback: Function) => {\n  if (hasSystemD() || hasSystemV()) {\n    exec(`service ${name} restart`, (err, stdOut, stdErr) => {\n      callback(err || stdErr, stdOut)\n    })\n  } else {\n    callback('Only systemd and init.d services are currently supported.')\n  }\n}\n"
  },
  {
    "path": "src/service/template/initd.ts",
    "content": "export default (d: any) =>\n`#!/bin/bash\n\n### BEGIN INIT INFO\n# Provides:      ${d.name}\n# Required-Start:\n# Required-Stop:\n# Default-Start:   ${d.runLevels}\n# Default-Stop:    0 1 6\n# Short-Description: Start ${d.name} at boot time\n# Description: Enable ${d.name} service.\n### END INIT INFO\n\n# chkconfig:   ${d.runLevels} 99 1\n# description: ${d.name}\n\nset_pid () {\n    unset PID\n    _PID=\\`head -1 \"${d.pidFile}\" 2>/dev/null\\`\n    if [ $_PID ]; then\n    kill -0 $_PID 2>/dev/null && PID=$_PID\n    fi\n}\n\nrestart () {\n    stop\n    start\n}\n\nstart () {\n    CNT=5\n\n    set_pid\n\n    if [ -z \"$PID\" ]; then\n    echo starting ${d.name}\n\n    if [ -e \"/var/deepstream/DEEPSTREAM_SETUP\" ]; then\n        bash \"/var/deepstream/DEEPSTREAM_SETUP\"\n    fi\n\n    if [ -e \"/var/deepstream/DEEPSTREAM_ENV_VARS\" ]; then\n        source \"/var/deepstream/DEEPSTREAM_ENV_VARS\"\n    fi\n\n    mkdir -p ${d.logDir}\n    \"${d.exec}\" ${d.deepstreamArgs} >> ${d.stdOut} 2>> ${d.stdErr} &\n\n    echo $! > \"${d.pidFile}\"\n\n    while [ : ]; do\n        set_pid\n\n        if [ -n \"$PID\" ]; then\n        echo started ${d.name}\n        break\n        else\n        if [ $CNT -gt 0 ]; then\n            sleep 1\n            CNT=\\`expr $CNT - 1\\`\n        else\n            echo ERROR - failed to start ${d.name}\n            break\n        fi\n        fi\n    done\n    else\n    echo ${d.name} is already started\n    fi\n}\n\nstatus () {\n    set_pid\n\n    if [ -z \"$PID\" ]; then\n    echo ${d.name} is not running\n    exit 1\n    else\n    echo ${d.name} is running\n    exit 0\n    fi\n}\n\nstop () {\n    CNT=30\n\n    set_pid\n\n    if [ -n \"$PID\" ]; then\n    echo stopping ${d.name}\n\n    kill $PID\n\n    while [ : ]; do\n    set_pid\n\n    if [ -z \"$PID\" ]; then\n    rm \"${d.pidFile}\"\n    echo stopped ${d.name}\n    break\n    else\n    if [ $CNT -gt 0 ]; then\n        sleep 1\n        CNT=\\`expr $CNT - 1\\`\n    else\n        echo ERROR - failed to stop ${d.name}\n        break\n    fi\n    fi\n    done\n    else\n    echo ${d.name} is already stopped\n    fi\n}\n\ncase $1 in\n    restart)\n    restart\n    ;;\n    start)\n    start\n    ;;\n    status)\n    status\n    ;;\n    stop)\n    stop\n    ;;\n    *)\n    echo \"usage: $0 <restart|start|status|stop>\"\n    exit 1\n    ;;\nesac\n\n`\n"
  },
  {
    "path": "src/service/template/systemd.ts",
    "content": "export default (d: any) =>\n`[Unit]\nDescription=${d.name}\nAfter=network.target\n\n[Service]\nType=simple\nStandardOutput=${d.stdOut}\nStandardError=${d.stdErr}\nExecStart=${d.exec} ${d.deepstreamArgs}\nRestart=always\nUser=${d.user}\nGroup=${d.group}\nEnvironment=\n\n[Install]\nWantedBy=multi-user.target\n\n`\n"
  },
  {
    "path": "src/services/authentication/combine/combine-authentication.ts",
    "content": "import { DeepstreamPlugin, DeepstreamAuthenticationCombiner, DeepstreamAuthentication, UserAuthenticationCallback } from '@deepstream/types'\nimport { JSONObject } from '../../../constants'\n\n/**\n * The open authentication handler allows every client to connect.\n * If the client specifies a username as part of its authentication\n * data, it will be used to identify the user internally\n */\nexport class CombineAuthentication extends DeepstreamPlugin implements DeepstreamAuthenticationCombiner {\n  public description: string = ''\n\n  constructor (private auths: DeepstreamAuthentication[]) {\n    super()\n    if (auths.length === 1) {\n      this.description = auths[0].description\n    } else {\n      this.description = auths.map((auth, index) => `\\n\\t${index}) ${auth.description}`).join('')\n    }\n  }\n\n  public async whenReady () {\n    await Promise.all(this.auths.map((auth) => auth.whenReady()))\n  }\n\n  public async close () {\n    await Promise.all(this.auths.map((auth) => auth.close()))\n  }\n\n  public async isValidUser (connectionData: JSONObject, authData: JSONObject, callback: UserAuthenticationCallback) {\n    for (const auth of this.auths) {\n      const result = await auth.isValidUser(connectionData, authData)\n      if (result) {\n        callback(result.isValid, result)\n        return\n      }\n    }\n    callback(false)\n  }\n\n  public onClientDisconnect (userId: string): void {\n    for (const auth of this.auths) {\n      if (auth.onClientDisconnect) {\n        auth.onClientDisconnect(userId)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/services/authentication/file/file-based-authentication.spec.ts",
    "content": "import { spy, assert } from 'sinon'\nimport { expect } from 'chai'\nimport { FileBasedAuthentication } from './file-based-authentication'\nimport { DeepstreamServices, EVENT, MetaData } from '@deepstream/types'\nimport { PromiseDelay } from '../../../utils/utils'\n\nimport * as users from '../../../test/config/users.json'\nimport * as usersUnhashed from '../../../test/config/users-unhashed.json'\nimport * as invalidUsersConfig from '../../../test/config/invalid-user-config.json'\nimport * as emptyUsersMap from '../../../test/config/empty-map-config.json'\n\nconst createServices = () => {\n  return {\n    logger: { fatal: spy() } as never as Logger\n  } as DeepstreamServices\n}\n\nconst testAuthentication = async ({ username, password, handler, notFound, isValid, clientData, serverData }) => {\n  const result = await handler.isValidUser(null, { username, password })\n  if (notFound) {\n    expect(result).to.equal(null)\n    return\n  }\n  expect(result.isValid).to.eq(isValid)\n\n  if (isValid) {\n    expect(result.id).to.equal(username)\n    expect(result.clientData).to.deep.equal(clientData)\n    expect(result.serverData).to.deep.equal(serverData)\n  } else {\n    expect(result).to.deep.equal({ isValid })\n  }\n}\n\ndescribe('file based authentication', () => {\n\n  describe('does authentication for cleartext passwords', () => {\n    let authenticationHandler\n\n    beforeEach(async () => {\n      authenticationHandler = new FileBasedAuthentication({\n        users: usersUnhashed,\n        hash: false\n      }, createServices())\n      await authenticationHandler.whenReady()\n      expect(authenticationHandler.description).to.eq('File Authentication')\n    })\n\n    it('confirms userC with valid password', async () => {\n      await testAuthentication({\n        handler: authenticationHandler,\n        username: 'userC',\n        password: 'userCPass',\n        isValid: true,\n        serverData: { some: 'values' },\n        clientData: { all: 'othervalue' }\n      })\n    })\n\n    it('confirms userD with valid password', async () => {\n      await testAuthentication({\n        username: 'userD',\n        password: 'userDPass',\n        isValid: true,\n        serverData: null,\n        clientData: { all: 'client data' },\n        handler: authenticationHandler\n      })\n    })\n\n    it('rejects userC with invalid password', async () => {\n      await testAuthentication({\n        username: 'userC',\n        password: 'userDPass',\n        isValid: false,\n        serverData: null,\n        clientData: null,\n        handler: authenticationHandler\n      })\n    })\n  })\n\n  describe('does authentication for hashed passwords', () => {\n    let authenticationHandler\n\n    beforeEach(async () => {\n      authenticationHandler = new FileBasedAuthentication({\n        users,\n        hash: 'md5',\n        iterations: 100,\n        keyLength: 32,\n        reportInvalidParameters: true\n      }, createServices())\n      await authenticationHandler.whenReady()\n    })\n\n    it('confirms userA with valid password', async () => {\n      await testAuthentication({\n        handler: authenticationHandler,\n        username: 'userA',\n        password: 'userAPass',\n        isValid: true,\n        serverData: { some: 'values' },\n        clientData: { all: 'othervalue' }\n      })\n    })\n\n    it('rejects userA with an invalid password', async () => {\n      await testAuthentication({\n        username: 'userA',\n        password: 'wrongPassword',\n        isValid: false,\n        handler: authenticationHandler\n      })\n    })\n\n    it('rejects userA with user B\\'s password', async () => {\n      await testAuthentication({\n        username: 'userA',\n        password: 'userBPass',\n        isValid: false,\n        handler: authenticationHandler\n      })\n    })\n\n    it('accepts userB with user B\\'s password', async () => {\n      await testAuthentication({\n        username: 'userB',\n        password: 'userBPass',\n        isValid: true,\n        serverData: null,\n        clientData: { all: 'client data' },\n        handler: authenticationHandler\n      })\n    })\n\n    it('returns null for userQ', async () => {\n      await testAuthentication({\n        handler: authenticationHandler,\n        username: 'userQ',\n        password: 'userBPass',\n        notFound: true,\n      })\n    })\n  })\n\n  describe('errors for invalid settings', () => {\n    const getSettings = function () {\n      return {\n        users,\n        hash: 'md5',\n        iterations: 100,\n        keyLength: 32\n      }\n    }\n\n    it('accepts settings with hash = false', () => {\n      const settings = {\n        users: usersUnhashed,\n        hash: false\n      }\n\n      expect(() => {\n        // tslint:disable-next-line:no-unused-expression\n        new FileBasedAuthentication(settings as any, createServices())\n      }).not.to.throw()\n    })\n\n    it('fails for settings with hash=string that miss hashing parameters', () => {\n      const settings = {\n        usersUnhashed,\n        hash: 'md5'\n      }\n\n      expect(() => {\n        // tslint:disable-next-line:no-unused-expression\n        new FileBasedAuthentication(settings as any)\n      }).to.throw()\n    })\n\n    it('fails for settings with non-existing hash algorithm', () => {\n      const settings = getSettings()\n      settings.hash = 'does-not-exist'\n\n      expect(() => {\n        // tslint:disable-next-line:no-unused-expression\n        new FileBasedAuthentication(settings)\n      }).to.throw()\n    })\n  })\n\n  describe('errors for invalid configs', () => {\n    const test = async (settings: any, errorMessage: string, expectedMetaData?: MetaData) => {\n      const services = createServices()\n      // tslint:disable-next-line: no-unused-expression\n      new FileBasedAuthentication(settings, services)\n      await PromiseDelay(10)\n      assert.calledOnce(services.logger.fatal)\n\n      if (!expectedMetaData) {\n        assert.calledWithExactly(services.logger.fatal, EVENT.PLUGIN_INITIALIZATION_ERROR, errorMessage)\n        return\n      }\n\n      assert.calledWith(services.logger.fatal, EVENT.PLUGIN_INITIALIZATION_ERROR, errorMessage)\n      const actualMetadata = services.logger.fatal.getCall(0).args[2]\n      if (expectedMetaData.error) {\n        expect(actualMetadata.error.message).to.equal(expectedMetaData.error.message)\n      }\n\n      const actualMetadataWithoutError = {\n        ...actualMetadata,\n        error: null\n      }\n      const expectedMetadataWithoutError = {\n        ...expectedMetaData,\n        error: null\n      }\n      expect(actualMetadataWithoutError).to.deep.equal(expectedMetadataWithoutError)\n    }\n\n    it('loads a user config without password field',async () => {\n      await test({\n        users: invalidUsersConfig,\n        hash: false\n      }, 'missing password for userB')\n    })\n\n    it('loads a user config without users', async() => {\n      await test({\n        users: emptyUsersMap,\n        hash: false\n      }, 'no users present in user file')\n    })\n\n    it('loads a user config with invalid hashing parameters', async() => {\n        await test(\n            {\n            users: users,\n            hash: 'md5',\n            iterations: '100',\n            keyLength: 32\n        },\n            'Validating settings failed for file auth',\n            {\n              error: new Error('Invalid type string for iterations')\n            }\n        )\n    })\n  })\n\n  describe('errors for invalid auth-data', () => {\n    let authenticationHandler\n    const settings = {\n      users,\n      hash: 'md5',\n      iterations: 100,\n      keyLength: 32,\n      reportInvalidParameters: true\n    }\n\n    beforeEach(async () => {\n      authenticationHandler = new FileBasedAuthentication(settings, createServices())\n      await authenticationHandler.whenReady()\n    })\n\n    it('returns null for authData without username', async () => {\n      const result = await authenticationHandler.isValidUser(null, {\n        password: 'some password'\n      })\n      expect(result.isValid).to.eq(false)\n      expect(result.clientData).to.deep.eq({ error: 'missing authentication parameter: username or/and password' })\n    })\n\n    it('returns an error for authData without password', async () => {\n      const result = await authenticationHandler.isValidUser(null, {\n        username: 'some user'\n      })\n      expect(result.isValid).to.eq(false)\n      expect(result.clientData).to.deep.eq({ error: 'missing authentication parameter: username or/and password' })\n    })\n  })\n})\n"
  },
  {
    "path": "src/services/authentication/file/file-based-authentication.ts",
    "content": "import { DeepstreamPlugin, DeepstreamAuthentication, DeepstreamServices, EVENT } from '@deepstream/types'\nimport { validateMap, createHash, validateHashingAlgorithm } from '../../../utils/utils'\n\ninterface FileAuthConfig {\n  users: any\n  // the name of a HMAC digest algorithm, a.g. 'sha512'\n  hash: string | false\n  // the amount of times the algorithm should be applied\n  iterations: number\n  // the length of the resulting key\n  keyLength: number,\n  // fail authentication process if invalid login parameters are used\n  reportInvalidParameters: boolean\n}\n\n/**\n * This authentication handler reads a list of users and their associated password (either\n * hashed or in cleartext ) from a json file. This can be useful to authenticate smaller amounts\n * of clients with static credentials, e.g. backend provider that write to publicly readable records\n */\nexport class FileBasedAuthentication extends DeepstreamPlugin implements DeepstreamAuthentication {\n  public description: string = 'File Authentication'\n  private base64KeyLength: number\n  private hashSettings = {\n    iterations: this.settings.iterations,\n    keyLength: this.settings.keyLength,\n    algorithm: this.settings.hash\n  }\n\n  /**\n  * Creates the class, reads and validates the users.json file\n  */\n  constructor (private settings: FileAuthConfig, private services: DeepstreamServices) {\n    super()\n    this.validateSettings(settings)\n    this.base64KeyLength = 4 * Math.ceil(this.settings.keyLength / 3)\n    if (this.settings.reportInvalidParameters === undefined) {\n      this.settings.reportInvalidParameters = true\n    }\n  }\n\n  public async whenReady (): Promise<void> {\n  }\n\n  /**\n  * Main interface. Authenticates incoming connections\n  */\n  public async isValidUser (connectionData: any, authData: any) {\n    const missingUsername = typeof authData.username !== 'string'\n    const missingPassword = typeof authData.password !== 'string'\n\n    if (missingPassword || missingUsername) {\n      if (this.settings.reportInvalidParameters) {\n        return {\n          isValid: false,\n          clientData: { error: 'missing authentication parameter: username or/and password' }\n        }\n      } else {\n        return null\n      }\n    }\n\n    const userData = this.settings.users[authData.username]\n\n    if (!userData) {\n      return null\n    }\n\n    const actualPassword = this.settings.hash ? userData.password.substr(0, this.base64KeyLength) : userData.password\n    let expectedPassword = authData.password\n\n    if (typeof this.settings.hash === 'string') {\n      ({ hash: expectedPassword} = await createHash(authData.password, this.hashSettings as any, userData.password.substr(this.base64KeyLength)))\n      expectedPassword = expectedPassword.toString('base64')\n    }\n\n    if (actualPassword === expectedPassword) {\n      return {\n        isValid: true,\n        id: authData.username,\n        serverData: typeof userData.serverData === 'undefined' ? null : userData.serverData,\n        clientData: typeof userData.clientData === 'undefined' ? null : userData.clientData,\n      }\n    }\n    if (this.settings.reportInvalidParameters) {\n      return { isValid: false }\n    }\n    return null\n  }\n\n  /**\n  * Called initially to validate the user provided settings\n  */\n  private validateSettings (settings: FileAuthConfig) {\n    try {\n      if (settings.hash) {\n        validateMap(settings, true, {\n          hash: 'string',\n          iterations: 'number',\n          keyLength: 'number',\n        })\n        validateHashingAlgorithm(settings.hash)\n      }\n    } catch (e) {\n      this.services.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, 'Validating settings failed for file auth', { error: e })\n    }\n\n    if (Object.keys(settings.users).length === 0) {\n      this.services.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, 'no users present in user file')\n      return\n    }\n\n    for (const username in this.settings.users) {\n      if (typeof settings.users[username].password !== 'string') {\n        this.services.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, `missing password for ${username}`)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/services/authentication/http/http-authentication.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport TestHttpServer from '../../../test/helper/test-http-server'\nimport MockLogger from '../../../test/mock/logger-mock'\nimport { PromiseDelay } from '../../../utils/utils';\nimport * as testHelper from '../../../test/helper/test-helper'\nimport { HttpAuthentication } from './http-authentication';\nimport { EVENT } from '@deepstream/types'\n\ndescribe('it forwards authentication attempts as http post requests to a specified endpoint', () => {\n  let authenticationHandler\n  let server\n  const port = TestHttpServer.getRandomPort()\n  const { config, services} = testHelper.getDeepstreamOptions()\n  let logSpy\n\n  before((done) => {\n    server = new TestHttpServer(port, done)\n    logSpy = (services.logger as MockLogger).logSpy\n    logSpy.resetHistory()\n  })\n\n  after((done) => {\n    server.close(done)\n  })\n\n  before (() => {\n    const endpointUrl = `http://localhost:${port}`\n\n    authenticationHandler = new HttpAuthentication({\n      endpointUrl,\n      permittedStatusCodes: [200],\n      requestTimeout: 60,\n      promoteToHeader: ['token'],\n      retryAttempts: 2,\n      retryInterval: 30,\n      retryStatusCodes: [404, 504]\n    }, services, config)\n    expect(authenticationHandler.description).to.equal(`http webhook to ${endpointUrl}`)\n  })\n\n  it('issues a request when isValidUser is called and receives 200 in return', async () => {\n    const connectionData = { connection: 'data' }\n    const authData = { username: 'userA' }\n\n    server.once('request-received', () => {\n      expect(server.lastRequestData).to.deep.equal({\n        connectionData: { connection: 'data' },\n        authData: { username: 'userA' }\n      })\n      expect(server.lastRequestMethod).to.equal('POST')\n      expect(server.lastRequestHeaders['content-type']).to.contain('application/json')\n      server.respondWith(200, { serverData: { extra: 'data' }, clientData: { color: 'red' }, id: \"123\" })\n    })\n\n    const result = await authenticationHandler.isValidUser(connectionData, authData)\n    expect(result.isValid).to.equal(true)\n    expect(result.id).to.equal(\"123\")\n    expect(result.serverData).to.deep.equal({ extra: 'data' })\n    expect(result.clientData).to.deep.equal({ color: 'red' })\n  })\n\n  it('issues a request when isValidUser is called and receives 401 (denied) in return', async () => {\n    const connectionData = { connection: 'data' }\n    const authData = { username: 'userA' }\n\n    server.once('request-received', () => {\n      expect(server.lastRequestData).to.deep.equal({\n        connectionData: { connection: 'data' },\n        authData: { username: 'userA' }\n      })\n      expect(server.lastRequestMethod).to.equal('POST')\n      expect(server.lastRequestHeaders['content-type']).to.contain('application/json')\n      server.respondWith(401)\n    })\n\n    const result = await authenticationHandler.isValidUser(connectionData, authData)\n    expect(result.isValid).to.equal(false)\n    expect(result.serverData).to.equal(undefined)\n    expect(result.clientData).to.equal(undefined)\n    expect(logSpy).to.have.callCount(0)\n  })\n\n  it('receives a positive response without data', async () => {\n    const connectionData = { connection: 'data' }\n    const authData = { username: 'userA' }\n\n    server.once('request-received', () => {\n      expect(server.lastRequestData).to.deep.equal({\n        connectionData: { connection: 'data' },\n        authData: { username: 'userA' }\n      })\n      expect(server.lastRequestMethod).to.equal('POST')\n      expect(server.lastRequestHeaders['content-type']).to.contain('application/json')\n      server.respondWith(200, '')\n    })\n\n    const result = await authenticationHandler.isValidUser(connectionData, authData)\n    expect(result.isValid).to.equal(true)\n    expect(result.serverData).to.equal(undefined)\n    expect(result.clientData).to.equal(undefined)\n  })\n\n  it('receives a positive response with only a string', async () => {\n    const connectionData = { connection: 'data' }\n    const authData = { username: 'userA' }\n\n    server.once('request-received', () => {\n      expect(server.lastRequestData).to.deep.equal({\n        connectionData: { connection: 'data' },\n        authData: { username: 'userA' }\n      })\n      expect(server.lastRequestMethod).to.equal('POST')\n      expect(server.lastRequestHeaders['content-type']).to.contain('application/json')\n      server.respondWith(200, 'userA')\n    })\n\n    const result = await authenticationHandler.isValidUser(connectionData, authData)\n    expect(result.isValid).to.equal(true)\n    expect(result.id).to.deep.equal('userA')\n  })\n\n  it('receives a server error as response', async () => {\n    const connectionData = { connection: 'data' }\n    const authData = { username: 'userA' }\n\n    server.once('request-received', () => {\n      server.respondWith(500, 'oh dear')\n    })\n\n    const result = await authenticationHandler.isValidUser(connectionData, authData)\n    expect(result.isValid).to.equal(false)\n    expect(logSpy).to.have.been.calledWith(2, EVENT.AUTH_ERROR, 'http auth server error: \"oh dear\"')\n    expect(result.clientData).to.deep.equal({ error: 'oh dear' })\n  })\n\n  it('promotes headers from body if provides', async () => {\n    const connectionData = { connection: 'data' }\n    const authData = { token: 'a-token' }\n\n    server.once('request-received', () => {\n      server.respondWith(200, {})\n    })\n\n    const result = await authenticationHandler.isValidUser(connectionData, authData)\n    expect(result.isValid).to.equal(true)\n    expect(result.clientData).to.equal(undefined)\n    expect(result.serverData).to.equal(undefined)\n    expect(server.getRequestHeader('token')).to.equal('a-token')\n  })\n\n  describe('retries', () => {\n    const connectionData = { connection: 'data' }\n    const authData = { token: 'a-token' }\n\n    beforeEach(() => {\n      server.once('request-received', () => server.respondWith(404, {}))\n    })\n\n    it ('doesn\\'t fail if the response returned is retry code', async () => {\n      let called = false\n      authenticationHandler.isValidUser(connectionData, authData, (result, data) => {\n        called = true\n      })\n      await PromiseDelay(20)\n      expect(called).to.equal(false)\n    })\n\n    it.skip ('returns true if the second attempt is valid', async () => {\n      let done\n      const result = new Promise((resolve) => done = resolve)\n\n      authenticationHandler.isValidUser(connectionData, authData, (result, data) => {\n        expect(result).to.equal(true)\n        expect(data).to.deep.equal({ what: '2nd-attempt' })\n        done()\n      })\n\n      await PromiseDelay(30)\n      server.once('request-received', () => server.respondWith(200, { what: '2nd-attempt' }))\n\n      await result\n    })\n\n    // TODO: Always passing\n    it ('returns invalid if retry attempts are exceeded', async () => {\n      const isValidUser = authenticationHandler.isValidUser(connectionData, authData)\n\n      await PromiseDelay(30)\n      server.once('request-received', () => server.respondWith(404, {}))\n\n      await PromiseDelay(30)\n      server.once('request-received', () => server.respondWith(504, {}))\n\n      const result = await isValidUser\n      expect(result.isValid).to.equal(false)\n      expect(result.clientData).to.deep.equal({ error: EVENT.AUTH_RETRY_ATTEMPTS_EXCEEDED })\n    })\n  })\n\n  it('times out', async () => {\n    const connectionData = { connection: 'data' }\n    const authData = { username: 'userA' }\n\n    const response = await authenticationHandler.isValidUser(connectionData, authData)\n    expect(response.isValid).to.equal(false)\n    expect(logSpy).to.have.been.calledWith(2, EVENT.AUTH_ERROR, 'http auth error: Error: socket hang up')\n    expect(response.clientData).to.deep.equal({ error: EVENT.AUTH_RETRY_ATTEMPTS_EXCEEDED })\n    server.respondWith(200)\n  })\n})\n"
  },
  {
    "path": "src/services/authentication/http/http-authentication.ts",
    "content": "import { post } from 'needle'\nimport { EVENT, DeepstreamPlugin, DeepstreamServices, DeepstreamConfig, DeepstreamAuthentication, DeepstreamAuthenticationResult } from '@deepstream/types'\nimport { JSONObject } from '../../../constants'\nimport { validateMap } from '../../../utils/utils'\n\ninterface HttpAuthenticationHandlerSettings {\n  // http(s) endpoint that will receive post requests\n  endpointUrl: string\n  // an array of http status codes that qualify as permitted\n  permittedStatusCodes: number[]\n  // time in milliseconds before the request times out if no reply is received\n  requestTimeout: number\n  // fields to copy from authData to header, useful for when endpoints authenticate using middleware\n  promoteToHeader: string[],\n  // any array of status codes that should be retries, useful if the server is down during a deploy\n  // or generally unresponsive\n  retryStatusCodes: number[],\n  // the maximum amount of retries before returning a false login\n  retryAttempts: number,\n  // the time in milliseconds between retries\n  retryInterval: number,\n  // fail authentication process if invalid login parameters are used\n  reportInvalidParameters: boolean\n}\n\nexport class HttpAuthentication extends DeepstreamPlugin implements DeepstreamAuthentication {\n  public description: string = `http webhook to ${this.settings.endpointUrl}`\n  private retryAttempts = new Map<number, { connectionData: any, authData: any, callback: (result: DeepstreamAuthenticationResult | null) => void, attempts: number } >()\n  private requestId = 0\n\n  constructor (private settings: HttpAuthenticationHandlerSettings, private services: DeepstreamServices, config: DeepstreamConfig) {\n    super()\n    this.validateSettings()\n    if (this.settings.promoteToHeader === undefined) {\n      this.settings.promoteToHeader = []\n    }\n    if (this.settings.reportInvalidParameters === undefined) {\n      this.settings.reportInvalidParameters = true\n    }\n  }\n\n  public async isValidUser (connectionData: JSONObject, authData: JSONObject) {\n    return new Promise((resolve: (result: DeepstreamAuthenticationResult | null) => void) => {\n      this.validate(this.requestId++, connectionData, authData, resolve)\n    })\n  }\n\n  private validate (id: number, connectionData: JSONObject, authData: JSONObject, callback: (result: DeepstreamAuthenticationResult | null) => void): void {\n    const options = {\n      read_timeout: this.settings.requestTimeout,\n      open_timeout: this.settings.requestTimeout,\n      response_timeout: this.settings.requestTimeout,\n      follow_max: 2,\n      json: true,\n      headers: {}\n    }\n\n    if (this.settings.promoteToHeader.length > 0) {\n      options.headers = this.settings.promoteToHeader.reduce(\n        (result, property) => {\n          if (authData[property]) {\n            result[property] = authData[property]\n          }\n          return result\n        },\n        {} as JSONObject\n      )\n    }\n\n    post(this.settings.endpointUrl, { connectionData, authData }, options, (error, response) => {\n      if (error) {\n        this.services.logger.warn(EVENT.AUTH_ERROR, `http auth error: ${error}`)\n        this.retry(id, connectionData, authData, callback)\n        return\n      }\n\n      if (!response.statusCode) {\n        this.services.logger.warn(EVENT.AUTH_ERROR, 'http auth server error: missing status code!')\n        this.retryAttempts.delete(id)\n        if (this.settings.reportInvalidParameters) {\n          callback({ isValid: false })\n        } else {\n          callback(null)\n        }\n\n        return\n      }\n\n      if (response.statusCode >= 500 && response.statusCode < 600) {\n        this.services.logger.warn(EVENT.AUTH_ERROR, `http auth server error: ${JSON.stringify(response.body)}`)\n      }\n\n      if (this.settings.retryStatusCodes.includes(response.statusCode)) {\n        this.retry(id, connectionData, authData, callback)\n        return\n      }\n\n      this.retryAttempts.delete(id)\n\n      if (this.settings.permittedStatusCodes.indexOf(response.statusCode) === -1) {\n        if (this.settings.reportInvalidParameters) {\n          if (response.body) {\n            if (typeof response.body === 'string') {\n              callback({ isValid: false, clientData: { error: response.body }})\n            } else if (typeof response.body === 'object' && Object.keys(response.body).length > 0) {\n              callback({ isValid: false, clientData: {...response.body} })\n            } else {\n              callback({ isValid: false })\n            }\n          } else {\n            callback({ isValid: false })\n          }\n        } else {\n          callback(null)\n        }\n        return\n      }\n\n      if (response.body && typeof response.body === 'string') {\n        callback({ isValid: true, id: response.body })\n        return\n      }\n\n      callback({ isValid: true, ...response.body })\n    })\n  }\n\n  private retry (id: number, connectionData: JSONObject, authData: JSONObject, callback: (result: DeepstreamAuthenticationResult | null) => void) {\n    let retryAttempt = this.retryAttempts.get(id)\n    if (!retryAttempt) {\n      retryAttempt = {\n        connectionData,\n        authData,\n        callback,\n        attempts: 0\n      }\n      this.retryAttempts.set(id, retryAttempt)\n    } else {\n      retryAttempt.attempts++\n    }\n    if (retryAttempt.attempts < this.settings.retryAttempts) {\n      setTimeout(() => this.validate(id, connectionData, authData, callback), this.settings.retryInterval)\n    } else {\n      this.retryAttempts.delete(id)\n      if (this.settings.reportInvalidParameters) {\n        callback({\n          isValid: false,\n          clientData: {\n            error: EVENT.AUTH_RETRY_ATTEMPTS_EXCEEDED\n          }\n        })\n      } else {\n        this.services.logger.warn(EVENT.AUTH_ERROR, EVENT.AUTH_RETRY_ATTEMPTS_EXCEEDED)\n        callback(null)\n      }\n    }\n  }\n\n  private validateSettings (): void {\n    validateMap(this.settings, true, {\n      endpointUrl: 'url',\n      permittedStatusCodes: 'array',\n      requestTimeout: 'number',\n      retryStatusCodes: 'array',\n      retryAttempts: 'number',\n      retryInterval: 'number'\n    })\n  }\n}\n"
  },
  {
    "path": "src/services/authentication/open/open-authentication.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport { spy } from 'sinon'\n\nimport { OpenAuthentication } from './open-authentication'\n\ndescribe('open authentication handler', () => {\n  let authenticationHandler\n\n  it('creates the handler', () => {\n    authenticationHandler = new OpenAuthentication()\n    expect(typeof authenticationHandler.isValidUser).to.equal('function')\n    expect(authenticationHandler.description).to.equal('Open Authentication')\n  })\n\n  it('permissions users without auth data', async () => {\n    const result = await authenticationHandler.isValidUser(null, {})\n    expect(result.isValid).to.equal(true)\n    expect(result.id).to.equal('open')\n  })\n\n  it('permissions users with a username', async () => {\n    const result = await authenticationHandler.isValidUser(null, { username: 'Wolfram' })\n    expect(result.isValid).to.equal(true)\n    expect(result.id).to.equal('Wolfram')\n  })\n})\n"
  },
  {
    "path": "src/services/authentication/open/open-authentication.ts",
    "content": "import { DeepstreamPlugin, DeepstreamAuthentication } from '@deepstream/types'\nimport { JSONObject } from '../../../constants'\n\n/**\n * Used for users that don't provide a username\n */\nconst OPEN: string = 'open'\n\n/**\n * The open authentication handler allows every client to connect.\n * If the client specifies a username as part of its authentication\n * data, it will be used to identify the user internally\n */\nexport class OpenAuthentication extends DeepstreamPlugin implements DeepstreamAuthentication {\n  public description: string  = 'Open Authentication'\n\n  /**\n  * Grants access to any user. Registers them with username or open\n  */\n  public async isValidUser (connectionData: JSONObject, authData: JSONObject) {\n    return {\n      isValid: true,\n      id: (authData.username && authData.username.toString()) || OPEN\n    }\n  }\n}\n"
  },
  {
    "path": "src/services/authentication/storage/storage-based-authentication.ts",
    "content": "import { DeepstreamPlugin, DeepstreamAuthentication, DeepstreamServices, EVENT, DeepstreamAuthenticationResult } from '@deepstream/types'\nimport { v4 as uuid } from 'uuid'\nimport { Dictionary } from 'ts-essentials'\nimport { createHash } from '../../../utils/utils'\n\nconst STRING = 'string'\n\ninterface StorageAuthConfig {\n  // fail authentication process if invalid login parameters are used\n  reportInvalidParameters: boolean,\n  // the table to store and lookup the users in\n  table: string,\n  // upsert the user if it doesn't exist in db\n  createUser: boolean,\n  // the name of a HMAC digest algorithm, a.g. 'sha512'\n  hash: string\n  // the amount of times the algorithm should be applied\n  iterations: number\n  // the length of the resulting key\n  keyLength: number\n}\n\ntype UserData = DeepstreamAuthenticationResult & {\n  password: string,\n  clientData: { [index: string]: any, id: string },\n  serverData: Dictionary<string>\n}\n\nexport class StorageBasedAuthentication extends DeepstreamPlugin implements DeepstreamAuthentication {\n  public description: string = `Storage using table: ${this.settings.table}`\n\n  private logger = this.services.logger.getNameSpace('STORAGE_AUTH')\n  private hashSettings = {\n    iterations: this.settings.iterations,\n    keyLength: this.settings.keyLength,\n    algorithm: this.settings.hash\n  }\n  private base64KeyLength = 4 * Math.ceil(this.settings.keyLength / 3)\n\n  /**\n  * Creates the class, reads and validates the users.json file\n  */\n  constructor (private settings: StorageAuthConfig, private services: DeepstreamServices) {\n    super()\n    if (this.settings.reportInvalidParameters === undefined) {\n      this.settings.reportInvalidParameters = true\n    }\n  }\n\n  public async whenReady (): Promise<void> {\n    await this.services.storage.whenReady()\n  }\n\n  /**\n  * Main interface. Authenticates incoming connections\n  */\n  public async isValidUser (connectionData: any, authData: any): Promise<DeepstreamAuthenticationResult | null> {\n    const missingUsername = typeof authData.username !== STRING\n    const missingPassword = typeof authData.password !== STRING\n\n    if (missingPassword || missingUsername) {\n      if (this.settings.reportInvalidParameters) {\n        return {\n          isValid: false,\n          clientData: { error: `missing authentication parameters: ${missingUsername && 'username'} ${missingPassword && 'password'}` }\n        }\n      } else {\n        return null\n      }\n    }\n\n    let userData: UserData\n    const storageId = `${this.settings.table}/${authData.username}`\n    try {\n      userData = await new Promise((resolve, reject) => this.services.storage.get(storageId, (err, version, data) => err ? reject(err) : resolve(data)))\n    } catch (err) {\n      this.logger.error(EVENT.ERROR, `Error retrieving user from storage ${JSON.stringify(err)}`)\n      return {\n        isValid: false,\n        clientData: { error: 'Error retrieving user from storage' }\n      }\n    }\n\n    if (userData === null) {\n      if (this.settings.createUser) {\n        this.logger.info(EVENT.REGISTERING_USER, `Adding new user ${authData.username}`)\n        const { hash, salt } = await createHash(authData.password, this.hashSettings)\n        const clientData = {\n          id: uuid(),\n        }\n        const serverData = {\n          created: Date.now()\n        }\n        return await new Promise((resolve, reject) => this.services.storage.set(storageId, 1, {\n          username: authData.username,\n          password: hash.toString('base64') + salt,\n          clientData,\n          serverData\n        }, (err) => {\n          if (err) {\n            this.logger.error(EVENT.ERROR, `Error creating user ${JSON.stringify(err)}`)\n            return resolve({\n              isValid: false,\n              clientData: { error: 'Error creating user' }\n            })\n          }\n          resolve({\n            isValid: true,\n            id: clientData.id,\n            clientData,\n            serverData\n          })\n        }\n        ))\n      }\n      return null\n    }\n\n    const expectedHash = userData.password.substr(0, this.base64KeyLength)\n    const { hash: actualHash } = await createHash(authData.password, this.hashSettings, userData.password.substr(this.base64KeyLength))\n\n    if (expectedHash === actualHash.toString('base64')) {\n      return {\n        isValid: true,\n        id: userData.clientData.id,\n        serverData: userData.serverData || null,\n        clientData: userData.clientData || null,\n      }\n    }\n\n    if (this.settings.reportInvalidParameters) {\n      return { isValid: false }\n    } else {\n      return null\n    }\n\n  }\n}\n"
  },
  {
    "path": "src/services/cache/local-cache.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport {spy} from 'sinon'\nimport { LocalCache } from './local-cache'\n\ndescribe('it saves values in memory', () => {\n  let localCache\n\n  before(() => {\n    localCache = new LocalCache()\n  })\n\n  it('has created the Local Cache', async () => {\n    await localCache.whenReady()\n  })\n\n  it('sets a value in the cache', (done) => {\n    const successCallback = spy()\n    localCache.set('firstname', 1, 'Wolfram', successCallback)\n    setTimeout(() => {\n      expect(successCallback).to.have.callCount(1)\n      expect(successCallback).to.have.been.calledWith(null)\n      done()\n    }, 1)\n  })\n\n  it('retrieves an existing value from the cache', (done) => {\n    const successCallback = spy()\n    localCache.get('firstname', successCallback)\n    setTimeout(() => {\n      expect(successCallback).to.have.callCount(1)\n      expect(successCallback).to.have.been.calledWith(null, 1, 'Wolfram')\n      done()\n    }, 1)\n  })\n\n  it('deletes a value from the cache', (done) => {\n    const successCallback = spy()\n    localCache.delete('firstname', successCallback)\n    setTimeout(() => {\n      expect(successCallback).to.have.callCount(1)\n      expect(successCallback).to.have.been.calledWith(null)\n      done()\n    }, 1)\n  })\n\n  it('tries to retrieve a non-existing value from the cache', (done) => {\n    const successCallback = spy()\n    localCache.get('firstname', successCallback)\n    setTimeout(() => {\n      expect(successCallback).to.have.callCount(1)\n      expect(successCallback).to.have.been.calledWith(null, -1, null)\n      done()\n    }, 1)\n  })\n})\n"
  },
  {
    "path": "src/services/cache/local-cache.ts",
    "content": "import { DeepstreamPlugin, DeepstreamCache, StorageWriteCallback, StorageReadCallback, StorageHeadBulkCallback, StorageHeadCallback } from '@deepstream/types'\nimport { JSONValue } from '../../constants'\n\nexport class LocalCache extends DeepstreamPlugin implements DeepstreamCache {\n  public description = 'Local Cache'\n\n  private data = new Map<string, { version: number, data: JSONValue }>()\n\n  public head (recordName: string, callback: StorageHeadCallback) {\n    const data = this.data.get(recordName)\n    process.nextTick(() => callback(null, data ? data.version : -1))\n  }\n\n  public headBulk (recordNames: string[], callback: StorageHeadBulkCallback) {\n    const versions: any = {}\n    const missing: any = []\n    for (const name of recordNames) {\n      const data = this.data.get(name)\n      if (data) {\n        versions[name] = data.version\n      } else {\n        missing.push(name)\n      }\n    }\n    process.nextTick(() => callback(null, versions, missing))\n  }\n\n  public set (key: string, version: number, data: any, callback: StorageWriteCallback) {\n    this.data.set(key, { version, data })\n    process.nextTick(() => callback(null))\n  }\n\n  public get (key: string, callback: StorageReadCallback) {\n    const data = this.data.get(key)\n    if (!data) {\n      process.nextTick(() => callback(null, -1, null))\n    } else {\n      process.nextTick(() => callback(null, data.version, data.data))\n    }\n  }\n\n  public delete (key: string, callback: StorageWriteCallback) {\n    this.data.delete(key)\n    process.nextTick(() => callback(null))\n  }\n\n  public deleteBulk (keys: string[], callback: StorageWriteCallback) {\n    keys.forEach((key) => this.data.delete(key))\n    process.nextTick(() => callback(null))\n  }\n}\n\nexport default LocalCache\n"
  },
  {
    "path": "src/services/cluster-node/single-cluster-node.ts",
    "content": "import { TOPIC, Message } from '../../constants'\nimport { DeepstreamClusterNode, DeepstreamPlugin } from '@deepstream/types'\n\nexport class SingleClusterNode extends DeepstreamPlugin implements DeepstreamClusterNode {\n  public description = 'Single Cluster Node'\n\n  public sendDirect (serverName: string, message: Message, metaData?: any) {}\n\n  public send (message: Message, metaData?: any) {}\n\n  public subscribe (stateRegistryTopic: TOPIC, callback: Function) {}\n\n  public async close (): Promise<void> {}\n}\n"
  },
  {
    "path": "src/services/cluster-node/vertical-cluster-node.ts",
    "content": "import { Message, TOPIC } from '../../constants'\nimport * as cluster from 'cluster'\nimport { EventEmitter } from 'events'\nimport { DeepstreamClusterNode, DeepstreamPlugin, DeepstreamServices, DeepstreamConfig, EVENT } from '@deepstream/types'\n\nif (cluster.isMaster) {\n    cluster.on('message', (worker, serializedMessage: string, handle) => {\n        for (const id in cluster.workers) {\n            const toWorker = cluster.workers[id]!\n            if (toWorker !== worker) {\n                toWorker.send(serializedMessage)\n            }\n        }\n    })\n}\n\nexport class VerticalClusterNode extends DeepstreamPlugin implements DeepstreamClusterNode {\n    public description: string = 'Vertical Cluster Message Bus'\n    private isReady: boolean = false\n    public static emitter = new EventEmitter()\n    private callbacks = new Map<string, any>()\n\n    constructor (pluginConfig: any, private services: DeepstreamServices, private config: DeepstreamConfig) {\n        super()\n    }\n\n    public init () {\n        if (cluster.isWorker) {\n            process.on('message', (serializedMessage) => {\n                if (this.isReady) {\n                    const { fromServer, message } = JSON.parse(serializedMessage)\n\n                    VerticalClusterNode.emitter.emit(TOPIC[message.topic], message, fromServer)\n\n                    const callbacks = this.callbacks.get(TOPIC[message.topic])\n                        if (!callbacks || callbacks.size === 0) {\n                            this.services.logger.warn(EVENT.UNKNOWN_ACTION, `Received message for unknown topic ${TOPIC[message.topic]}`)\n                            return\n                        }\n                        callbacks.forEach((callback: Function) => callback(message, fromServer))\n                }\n            })\n        }\n    }\n\n    async whenReady (): Promise<void> {\n        this.isReady = true\n    }\n\n    public send (message: Message, metaData?: any): void {\n        process.send!(JSON.stringify({ message, fromServer: this.config.serverName }))\n    }\n\n    public sendDirect (serverName: string, message: Message, metaData?: any): void {\n        process.send!(JSON.stringify({ toServer: serverName, fromServer: this.config.serverName, message }))\n    }\n\n    public subscribe<SpecificMessage> (stateRegistryTopic: TOPIC, callback: (message: SpecificMessage, originServerName: string) => void): void {\n        VerticalClusterNode.emitter.on(TOPIC[stateRegistryTopic], callback)\n\n        let callbacks = this.callbacks.get(TOPIC[stateRegistryTopic])\n        if (!callbacks) {\n            callbacks = new Set()\n            this.callbacks.set(TOPIC[stateRegistryTopic], callbacks)\n        }\n        callbacks.add(callback)\n    }\n\n    public async close (): Promise<void> {\n        for (const [topic, callbacks] of this.callbacks) {\n            for (const callback of callbacks) {\n                VerticalClusterNode.emitter.off(topic, callback)\n            }\n        }\n        this.callbacks.clear()\n        this.isReady = false\n    }\n}\n"
  },
  {
    "path": "src/services/cluster-registry/distributed-cluster-registry.ts",
    "content": "import { DeepstreamServices, DeepstreamConfig, ClusterRegistry, DeepstreamPlugin, EVENT } from '@deepstream/types'\nimport { TOPIC, ClusterMessage, CLUSTER_ACTION } from '../../constants'\nimport { EventEmitter } from 'events'\n\n/**\n * This class maintains a list of all nodes that are\n * currently present within the cluster.\n *\n * It provides status messages on a predefined interval\n * and keeps track of incoming status messages.\n */\nexport class DistributedClusterRegistry extends DeepstreamPlugin implements ClusterRegistry {\n    public description: string = 'Distributed Cluster Registry'\n    private inCluster: boolean = false\n    private nodes = new Map<string, { lastStatusTime: number, leaderScore: number }>()\n    private leaderScore = Math.random()\n    private publishInterval!: NodeJS.Timeout\n    private checkInterval!: NodeJS.Timeout\n    private role: string\n    private emitter = new EventEmitter()\n\n    /**\n     * Creates the class, initialises all intervals and publishes the\n     * initial status message that notifies other nodes within this\n     * cluster of its presence.\n     */\n    constructor (private pluginOptions: any, private services: Readonly<DeepstreamServices>, private config: Readonly<DeepstreamConfig>) {\n        super()\n        this.role = this.pluginOptions.role || 'deepstream'\n    }\n\n    public init () {\n        this.services.clusterNode.subscribe(TOPIC.CLUSTER, this.onMessage.bind(this))\n        this.leaveCluster = this.leaveCluster.bind(this)\n\n        this.publishStatus()\n\n        this.publishInterval = setInterval(\n            this.publishStatus.bind(this),\n            this.pluginOptions.keepAliveInterval\n        )\n\n        this.checkInterval = setInterval(\n            this.checkNodes.bind(this),\n            this.pluginOptions.activeCheckInterval\n        )\n    }\n\n    public async close (): Promise<void> {\n      return new Promise((resolve) => {\n        this.emitter.once('close', resolve)\n        this.leaveCluster()\n      })\n    }\n\n    public onServerAdded (callback: (serverName: string) => void) {\n        this.emitter.on('server-added', callback)\n    }\n\n    public onServerRemoved (callback: (serverName: string) => void) {\n        this.emitter.on('server-removed', callback)\n    }\n\n    /**\n     * Returns the serverNames of all nodes currently present within the cluster\n     */\n    public getAll (): string[] {\n        return [...this.nodes.keys()]\n    }\n\n    /**\n     * Returns true if this node is the cluster leader\n     */\n    public isLeader (): boolean {\n        return this.config.serverName === this.getLeader()\n    }\n\n    /**\n     * Returns the name of the current leader\n     */\n    public getLeader () {\n        let maxScore = 0\n        let leader = this.config.serverName\n\n        for (const [serverName, node] of this.nodes) {\n            if (node.leaderScore > maxScore) {\n                maxScore = node.leaderScore\n                leader = serverName\n            }\n        }\n\n        return leader\n    }\n\n    /**\n     * Distributes incoming messages on the cluster topic\n     */\n    private onMessage (message: ClusterMessage) {\n        if (message.action === CLUSTER_ACTION.STATUS) {\n            this.updateNode(message)\n            return\n        }\n\n        if (message.action === CLUSTER_ACTION.REMOVE) {\n            this.removeNode(message.serverName)\n            return\n        }\n\n        this.services.logger.error(EVENT.UNKNOWN_ACTION, `TOPIC: ${TOPIC[TOPIC.CLUSTER]} ${message.action}`)\n    }\n\n    /**\n     * Called on an interval defined by clusterActiveCheckInterval to check if all nodes\n     * within the cluster are still alive.\n     *\n     * Being alive is defined as having received a status message from that node less than\n     * <clusterNodeInactiveTimeout> milliseconds ago.\n     */\n    private checkNodes () {\n        // Never remove a single node instance\n        if (this.nodes.size === 1) {\n            return\n        }\n        const now = Date.now()\n        for (const [serverName, node] of this.nodes) {\n            if (now - node.lastStatusTime > this.pluginOptions.nodeInactiveTimeout) {\n                this.removeNode(serverName)\n            }\n        }\n    }\n\n    /**\n     * Updates the status of a node with incoming status data and resets its lastStatusTime.\n     *\n     * If the remote node doesn't exist yet, it is added and an add event is emitted / logged\n     */\n    private updateNode (message: ClusterMessage) {\n        const node = this.nodes.get(message.serverName)\n\n        this.nodes.set(message.serverName, {\n            lastStatusTime: Date.now(),\n            leaderScore: message.leaderScore!\n        })\n\n        if (node) {\n            return\n        }\n\n        this.services.logger.info(EVENT.CLUSTER_JOIN, message.serverName)\n        this.services.logger.info(EVENT.CLUSTER_SIZE, `The cluster size is now ${this.nodes.size}`)\n        this.emitter.emit('server-added', message.serverName)\n    }\n\n    /**\n     * Removes a remote node from this registry if it exists.\n     * Logs/emits remove\n     */\n    private removeNode (serverName: string) {\n        const deleted = this.nodes.delete(serverName)\n        if (deleted) {\n            this.services.logger.info(EVENT.CLUSTER_LEAVE, serverName)\n            this.services.logger.info(EVENT.CLUSTER_SIZE, `The cluster size is now ${this.nodes.size}`)\n            this.emitter.emit('server-removed', serverName)\n        }\n    }\n\n    /**\n     * Publishes this node's status on the message bus\n     */\n    private publishStatus (): void {\n        this.inCluster = true\n        const message = {\n            topic: TOPIC.CLUSTER,\n            action: CLUSTER_ACTION.STATUS,\n            serverName: this.config.serverName,\n            leaderScore: this.leaderScore,\n            role: this.role\n        } as ClusterMessage\n        this.updateNode(message)\n        this.services.clusterNode.send(message)\n    }\n\n    /**\n     * Prompts this node to leave the cluster, either as a result of a server.close()\n     * call or due to the process exiting.\n     * This sends out a leave message to all other nodes and destroys this class.\n     */\n    private leaveCluster () {\n        if (this.inCluster === false) {\n            this.emitter.emit('close')\n            return\n        }\n\n        this.services.logger.info(EVENT.CLUSTER_LEAVE, this.config.serverName)\n        this.services.clusterNode.send({\n            topic: TOPIC.CLUSTER,\n            action: CLUSTER_ACTION.REMOVE,\n            name: this.config.serverName\n        })\n\n        // TODO: If a message connector doesn't close this is required to avoid an error\n        // being thrown during shutdown\n        // this._options.messageConnector.unsubscribe( C.TOPIC.CLUSTER, this._onMessageFn );\n\n        process.removeListener('beforeExit', this.leaveCluster)\n        process.removeListener('exit', this.leaveCluster)\n        clearInterval(this.publishInterval)\n        clearInterval(this.checkInterval)\n        this.nodes.clear()\n        this.inCluster = false\n\n        this.emitter.emit('close')\n    }\n\n}\n"
  },
  {
    "path": "src/services/cluster-registry/distributed-state-registry-factory.ts",
    "content": "import { DeepstreamPlugin, DeepstreamServices, DeepstreamConfig, StateRegistryFactory, StateRegistry } from '@deepstream/types'\nimport { TOPIC } from '../../constants'\nimport { DistributedStateRegistry, DistributedStateRegistryOptions } from '../cluster-state/distributed-state-registry'\n\nexport class DistributedStateRegistryFactory extends DeepstreamPlugin implements StateRegistryFactory {\n    public description: string = 'Distributed State Registry'\n    private stateRegistries = new Map<TOPIC, StateRegistry>()\n\n    constructor (private pluginConfig: DistributedStateRegistryOptions, private services: Readonly<DeepstreamServices>, private config: Readonly<DeepstreamConfig>) {\n        super()\n    }\n\n    public getStateRegistry = (topic: TOPIC) => {\n      let stateRegistry = this.stateRegistries.get(topic)\n      if (!stateRegistry) {\n        stateRegistry = new DistributedStateRegistry(topic, this.pluginConfig, this.services, this.config)\n        this.stateRegistries.set(topic, stateRegistry)\n      }\n      return stateRegistry\n  }\n\n  public getStateRegistries (): Map<TOPIC, StateRegistry> {\n      return this.stateRegistries\n  }\n}\n"
  },
  {
    "path": "src/services/cluster-state/distributed-state-registry-factory.ts",
    "content": "import { DeepstreamPlugin, DeepstreamServices, DeepstreamConfig, StateRegistryFactory, StateRegistry } from '@deepstream/types'\nimport { TOPIC } from '../../constants'\nimport { DistributedStateRegistry, DistributedStateRegistryOptions } from './distributed-state-registry'\n\nexport class DistributedStateRegistryFactory extends DeepstreamPlugin implements StateRegistryFactory {\n    public description: string = 'Distributed State Registry'\n    private stateRegistries = new Map<TOPIC, StateRegistry>()\n\n    constructor (private pluginConfig: DistributedStateRegistryOptions, private services: Readonly<DeepstreamServices>, private config: Readonly<DeepstreamConfig>) {\n        super()\n    }\n\n    public getStateRegistry = (topic: TOPIC) => {\n      let stateRegistry = this.stateRegistries.get(topic)\n      if (!stateRegistry) {\n        stateRegistry = new DistributedStateRegistry(topic, this.pluginConfig, this.services, this.config)\n        this.stateRegistries.set(topic, stateRegistry)\n      }\n      return stateRegistry\n  }\n\n  public getStateRegistries (): Map<TOPIC, StateRegistry> {\n      return this.stateRegistries\n  }\n}\n"
  },
  {
    "path": "src/services/cluster-state/distributed-state-registry.ts",
    "content": "import { TOPIC, STATE_ACTION, StateMessage } from '../../constants'\nimport { DeepstreamServices, StateRegistry, StateRegistryCallback, DeepstreamConfig, EVENT } from '@deepstream/types'\nimport { Dictionary } from 'ts-essentials'\nimport { EventEmitter } from 'events'\n\nexport type DistributedStateRegistryOptions = any\n\n/**\n * This class provides a generic mechanism that allows to maintain\n * a distributed state amongst the nodes of a cluster. The state is an\n * array of unique strings in arbitrary order.\n *\n * Whenever a string is added by any node within the cluster for the first time,\n * an 'add' event is emitted. Whenever its removed by the last node within the cluster,\n * a 'remove' event is emitted.\n */\nexport class DistributedStateRegistry implements StateRegistry {\n  private isReady: boolean = false\n  private data = new Map<string, {\n    localCount: number,\n    nodes: Set<string>,\n    checkSum: number\n  }>()\n  private reconciliationTimeouts = new Map<string, NodeJS.Timeout>()\n  private checkSumTimeouts = new Map<string, any[]>()\n  private fullStateSent: boolean = false\n  private initialServers = new Set<string>()\n  private emitter = new EventEmitter()\n  private logger = this.services.logger.getNameSpace('DISTRIBUTED_STATE_REGISTRY')\n\n  /**\n   * Initializes the DistributedStateRegistry and subscribes to the provided cluster topic\n   */\n  constructor (private topic: TOPIC, private stateOptions: any, private services: Readonly<DeepstreamServices>, private config: Readonly<DeepstreamConfig>) {\n    this.resetFullStateSent = this.resetFullStateSent.bind(this)\n    this.services.clusterNode.subscribe(TOPIC.STATE_REGISTRY, this.processIncomingMessage.bind(this))\n\n    const serverNames = this.services.clusterRegistry.getAll()\n    this.initialServers = new Set(serverNames)\n    if (this.initialServers.size === 0) {\n      this.isReady = true\n      this.emitter.emit('ready')\n    }\n\n    this.initialServers.forEach((serverName) => {\n      if (serverName !== this.config.serverName) {\n        this.onServerAdded(serverName)\n      }\n    })\n\n    this.services.clusterRegistry.onServerAdded(this.onServerAdded.bind(this))\n    this.services.clusterRegistry.onServerRemoved(this.onServerRemoved.bind(this))\n  }\n\n  public async whenReady () {\n    if (!this.isReady) {\n      await new Promise((resolve) => this.emitter.once('ready', resolve))\n    }\n  }\n\n  public onAdd (callback: StateRegistryCallback): void {\n    this.emitter.on('add', callback)\n  }\n\n  public onRemove (callback: StateRegistryCallback): void {\n    this.emitter.on('remove', callback)\n  }\n\n  /**\n   * Checks if a given entry exists within the registry\n   */\n  public has (name: string) {\n    return this.data.has(name)\n  }\n\n  /**\n   * Add a name/entry to the registry. If the entry doesn't exist yet,\n   * this will notify the other nodes within the cluster\n   */\n  public add (name: string) {\n    const data = this.data.get(name)\n    if (!data || !data.nodes.has(this.config.serverName)) {\n      this.addToServer(name, this.config.serverName)\n      this.sendMessage(name, STATE_ACTION.ADD)\n    } else {\n      data.localCount++\n    }\n  }\n\n  /**\n   * Removes a name/entry from the registry. If the entry doesn't exist,\n   * this will exit silently\n   */\n  public remove (name: string) {\n    const data = this.data.get(name)\n    if (data) {\n      data.localCount--\n      if (data.localCount === 0) {\n        this.removeFromServer(name, this.config.serverName)\n        this.sendMessage(name, STATE_ACTION.REMOVE)\n      }\n    }\n  }\n\n  public removeAll (serverName: string): void {\n    throw new Error('Method not implemented.')\n  }\n\n  /**\n   * Informs the distributed state registry a server has been added to the cluster\n   */\n  public onServerAdded (serverName: string) {\n    this._requestFullState(serverName)\n  }\n\n  /**\n   * Removes all entries for a given serverName. This is intended to be called\n   * whenever a node is removed from the cluster\n   */\n  public onServerRemoved (serverName: string) {\n    for (const [name, value] of this.data) {\n      if (value.nodes.has(serverName)) {\n        this.removeFromServer(name, serverName)\n      }\n    }\n  }\n\n  /**\n   * Returns all the servers that hold a given state\n   */\n  public getAllServers (name: string) {\n    const data = this.data.get(name)\n    if (data) {\n      return [...data.nodes.keys()]\n    }\n    return []\n  }\n\n  /**\n   * Returns all currently registered entries\n   */\n  public getAll (serverName: string): string[] {\n    if (!serverName) {\n      return [...this.data.keys()]\n    }\n    const entries: string[] = []\n    for (const [name, value] of this.data) {\n      if (value.nodes.has(serverName)) {\n        entries.push(name)\n      }\n    }\n    return entries\n  }\n\n  /**\n   * Removes an entry for a given serverName. If the serverName\n   * was the last node that held the entry, the entire entry will\n   * be removed and a `remove` event will be emitted\n   */\n  private removeFromServer (name: string, serverName: string) {\n    const data = this.data.get(name)\n    if (!data) {\n      return\n    }\n    data.nodes.delete(serverName)\n\n    const exists = data.nodes.size !== 0\n\n    if (exists === false) {\n      this.data.delete(name)\n      this.emitter.emit('remove', name)\n    }\n\n    this.emitter.emit('server-removed', name, serverName)\n  }\n\n  /**\n   * Adds a new entry to this registry, either as a result of a remote or\n   * a local addition. Will emit an `add` event if the entry wasn't present before\n   */\n  private addToServer (name: string, serverName: string) {\n    let data = this.data.get(name)\n\n    if (!data) {\n      data = {\n        localCount: 1,\n        nodes: new Set(),\n        checkSum: this.createCheckSum(name)\n      }\n      this.data.set(name, data)\n\n      this.emitter.emit('add', name)\n    }\n\n    data.nodes.add(serverName)\n    this.emitter.emit('server-added', name, serverName)\n  }\n\n  /**\n   * Generic messaging function for add and remove messages\n   */\n  private sendMessage (name: string, action: STATE_ACTION) {\n    this.services.clusterNode.send({\n      topic: TOPIC.STATE_REGISTRY,\n      registryTopic: this.topic,\n      action,\n      name\n    })\n\n    this.getCheckSumTotal(this.config.serverName, (checksum) =>\n      this.services.clusterNode.send({\n        topic: TOPIC.STATE_REGISTRY,\n        registryTopic: this.topic,\n        action: STATE_ACTION.CHECKSUM,\n        checksum\n      })\n    )\n  }\n\n  /**\n   * This method calculates the total checkSum for all local entries of\n   * a given serverName\n   */\n  private getCheckSumTotal (serverName: string, callback: (checksum: number) => void): void {\n    const callbacks = this.checkSumTimeouts.get(serverName)\n    if (callbacks) {\n      callbacks.push(callback)\n    } else {\n      this.checkSumTimeouts.set(serverName, [callback])\n\n      setTimeout(() => {\n        let totalCheckSum = 0\n\n        for (const [, value] of this.data) {\n          if (value.nodes.has(serverName)) {\n            totalCheckSum += value.checkSum\n          }\n        }\n\n        this.checkSumTimeouts.get(serverName)!.forEach((cb: (checksum: number) => void) => cb(totalCheckSum))\n        this.checkSumTimeouts.delete(serverName)\n      }, this.stateOptions.checkSumBuffer)\n    }\n  }\n\n  /**\n   * Calculates a simple checkSum for a given name. This is done up-front and cached\n   * to increase performance for local add and remove operations. Arguably this is a generic\n   * method and might be moved to the utils class if we find another usecase for it.\n   */\n  private createCheckSum (name: string) {\n    let checkSum = 0\n    let i\n\n    for (i = 0; i < name.length; i++) {\n      // tslint:disable-next-line:no-bitwise\n      checkSum = ((checkSum << 5) - checkSum) + name.charCodeAt(i) // eslint-disable-line\n    }\n\n    return checkSum\n  }\n\n  /**\n   * Checks a remote checkSum for a given serverName against the\n   * actual checksum for all local entries for the given name.\n   *\n   * - If the checksums match, it removes all possibly pending\n   *   reconciliationTimeouts\n   *\n   * - If the checksums don't match, it schedules a reconciliation request. If\n   *   another message from the remote server arrives before the reconciliation request\n   *   is send, it will be cancelled.\n   */\n  private verifyCheckSum (serverName: string, remoteCheckSum: number) {\n    this.getCheckSumTotal(serverName, (checksum: number) => {\n      if (checksum !== remoteCheckSum) {\n        this.reconciliationTimeouts.set(serverName, setTimeout(\n            this._requestFullState.bind(this, serverName),\n            this.stateOptions.stateReconciliationTimeout\n        ))\n        return\n      }\n\n      const timeout = this.reconciliationTimeouts.get(serverName)\n      if (timeout) {\n        clearTimeout(timeout)\n        this.reconciliationTimeouts.delete(serverName)\n      }\n    })\n  }\n\n  /**\n   * Sends a reconciliation request for a server with a given name (technically, its send to\n   * every node within the cluster, but will be ignored by all but the one with a matching name)\n   *\n   * The matching node will respond with a DISTRIBUTED_STATE_FULL_STATE message\n   */\n  private _requestFullState (serverName: string) {\n    this.services.clusterNode.sendDirect(serverName, {\n      topic: TOPIC.STATE_REGISTRY,\n      registryTopic: this.topic,\n      action: STATE_ACTION.REQUEST_FULL_STATE,\n    })\n  }\n\n  /**\n   * Creates a full state message containing an array of all local entries that\n   * will be used to reconcile compromised states as well as provide the full state\n   * for new nodes that joined the cluster\n   *\n   * When a state gets compromised, more than one remote registry might request a full state update.\n   * This method will  schedule a timeout in which no additional full state messages are sent to\n   * make sure only a single full state message is sent in reply.\n   */\n  public sendFullState (serverName: string): void {\n    const localState: string[] = []\n\n    for (const [name, value] of this.data) {\n      if (value.nodes.has(this.config.serverName)) {\n        localState.push(name)\n      }\n    }\n    this.services.clusterNode.sendDirect(serverName, {\n      topic: TOPIC.STATE_REGISTRY,\n      registryTopic: this.topic,\n      action: STATE_ACTION.FULL_STATE,\n      fullState: localState\n    })\n\n    this.fullStateSent = true\n    setTimeout(this.resetFullStateSent, this.stateOptions.stateReconciliationTimeout)\n  }\n\n  /**\n   * This will apply the data from an incoming full state message. Entries that are not within\n   * the incoming array will be removed for that node from the local registry and new entries will\n   * be added.\n   */\n  private applyFullState (serverName: string, names: string[]) {\n    const namesMap: Dictionary<boolean> = {}\n    for (let i = 0; i < names.length; i++) {\n      namesMap[names[i]] = true\n    }\n\n    Object.keys(this.data).forEach((name) => {\n      // please note: only checking if the name exists is sufficient as the registry will just\n      // set node[serverName] to false if the entry exists, but not for the remote server.\n      if (!namesMap[name]) {\n        this.removeFromServer(name, serverName)\n      }\n    })\n\n    names.forEach((name) => this.addToServer(name, serverName))\n\n    this.initialServers.delete(serverName)\n    if (this.initialServers.size === 0) {\n      this.isReady = true\n      this.emitter.emit('ready')\n    }\n  }\n\n  /**\n   * Will be called after a full state message has been sent and\n   * stateReconciliationTimeout has passed. This will allow further reconciliation\n   * messages to be sent again.\n   */\n  private resetFullStateSent (): void {\n    this.fullStateSent = false\n  }\n\n  /**\n   * This is the main routing point for messages coming in from\n   * the message connector.\n   */\n  private processIncomingMessage (message: StateMessage, serverName: string): void {\n    if (message.registryTopic !== this.topic) {\n      return\n    }\n\n    if (message.action === STATE_ACTION.ADD) {\n      this.addToServer(message.name!, serverName)\n      return\n    }\n\n    if (message.action === STATE_ACTION.REMOVE) {\n      this.removeFromServer(message.name!, serverName)\n      return\n    }\n\n    if (message.action === STATE_ACTION.REQUEST_FULL_STATE) {\n      if (!message.data || this.fullStateSent === false) {\n        this.sendFullState(serverName)\n      } else {\n        this.logger.error(EVENT.ERROR, `Ignoring a request for full state from ${serverName}`)\n      }\n      return\n    }\n\n    if (message.action === STATE_ACTION.FULL_STATE) {\n      this.applyFullState(serverName, message.fullState!)\n    }\n\n    if (message.action === STATE_ACTION.CHECKSUM) {\n      this.verifyCheckSum(serverName, message.checksum!)\n    }\n  }\n}\n"
  },
  {
    "path": "src/services/cluster-state/single-state-registry.ts",
    "content": "import { StateRegistry, DeepstreamPlugin, StateRegistryCallback } from '@deepstream/types'\nimport { EventEmitter } from 'events'\n\n/**\n * This class provides a generic mechanism that allows to maintain\n * a distributed state amongst the nodes of a cluster.\n */\nexport class SingleStateRegistry extends DeepstreamPlugin implements StateRegistry {\n  public description: string = 'Single State Registry'\n  private readonly data = new Map<string, number>()\n  private emitter = new EventEmitter()\n\n  /**\n  * Checks if a given entry exists within the registry\n  */\n  public has (name: string): boolean {\n    return this.data.has(name)\n  }\n\n  public onAdd (callback: StateRegistryCallback): void {\n    this.emitter.on('add', callback)\n  }\n\n  public onRemove (callback: StateRegistryCallback): void {\n    this.emitter.on('remove', callback)\n  }\n\n  /**\n  * Add a name/entry to the registry. If the entry doesn't exist yet,\n  * this will notify the other nodes within the cluster\n  */\n  public add (name: string): void {\n    const current = this.data.get(name)\n    if (!current) {\n      this.data.set(name, 1)\n      this.emitter.emit('add', name)\n    } else {\n      this.data.set(name, current + 1)\n    }\n  }\n\n  /**\n  * Removes a name/entry from the registry. If the entry doesn't exist,\n  * this will exit silently\n  */\n  public remove (name: string): void {\n    const current = this.data.get(name)! - 1\n    if (current === 0) {\n      this.data.delete(name)\n      this.emitter.emit('remove', name)\n    } else {\n      this.data.set(name, current)\n    }\n  }\n\n  /**\n  * Returns all currently registered entries\n  */\n  public getAll (): string[] {\n    return [ ...this.data.keys() ]\n  }\n\n  /**\n   * Returns all the servers that hold a given state\n   */\n  public getAllServers (subscriptionName: string): string[] {\n    return []\n  }\n\n  /**\n   * Removes all entries for a given serverName. This is intended to be called\n   * whenever a node leaves the cluster\n   */\n  public removeAll (serverName: string): void {\n  }\n}\n"
  },
  {
    "path": "src/services/http/node/node-http.ts",
    "content": "import { DeepstreamPlugin, DeepstreamHTTPService, EVENT, PostRequestHandler, GetRequestHandler, DeepstreamHTTPMeta, DeepstreamHTTPResponse, SocketHandshakeData, DeepstreamServices, DeepstreamConfig, SocketWrapper, WebSocketConnectionEndpoint, SocketWrapperFactory } from '@deepstream/types'\n// @ts-ignore\nimport * as httpShutdown from 'http-shutdown'\nimport * as http from 'http'\nimport * as https from 'https'\nimport * as HTTPStatus from 'http-status'\nimport * as contentType from 'content-type'\nimport * as bodyParser from 'body-parser'\nimport { EventEmitter } from 'events'\nimport * as WebSocket from 'ws'\nimport { Socket } from 'net'\nimport { Dictionary } from 'ts-essentials'\ninterface NodeHTTPInterface {\n    healthCheckPath: string,\n    host: string,\n    port: number,\n    allowAllOrigins: boolean,\n    origins?: string[],\n    maxMessageSize: number,\n    hostUrl: string,\n    headers: string[],\n    ssl?: {\n      key: string,\n      cert: string,\n      ca?: string\n    }\n}\n\nexport class NodeHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {\n  public description: string = 'NodeJS HTTP Service'\n  private server!: http.Server | https.Server\n  private isReady: boolean = false\n\n  private methods: string[] = ['GET', 'POST', 'OPTIONS']\n  private methodsStr: string = this.methods.join(', ')\n  private headers: string[] = ['X-Requested-With', 'X-HTTP-Method-Override', 'Content-Type', 'Accept']\n  private headersLower: string[] = this.headers.map((header) => header.toLowerCase())\n  private headersStr: string = this.headers.join(', ')\n  private jsonBodyParser: any\n\n  private postPaths = new Map<string, PostRequestHandler<any>>()\n  private getPaths = new Map<string, GetRequestHandler>()\n  private upgradePaths = new Map<string, WebSocket.Server>()\n\n  private sortedPostPaths: string[] = []\n  private sortedGetPaths: string[] = []\n  private sortedUpgradePaths: string[] = []\n\n  private connections = new Map<WebSocket, SocketWrapper>()\n  private emitter = new EventEmitter()\n\n  constructor (private pluginOptions: NodeHTTPInterface, private services: DeepstreamServices, config: DeepstreamConfig) {\n    super()\n\n    if (this.pluginOptions.allowAllOrigins === false) {\n        if (this.pluginOptions.origins?.length === 0) {\n          this.services.logger.fatal(EVENT.INVALID_CONFIG_DATA, 'HTTP allowAllOrigins set to false but no origins provided')\n        }\n    }\n\n    this.jsonBodyParser = bodyParser.json({\n        inflate: true,\n        limit: `${pluginOptions.maxMessageSize}b`\n    })\n  }\n\n  public async whenReady (): Promise<void> {\n    if (this.isReady) {\n        return\n    }\n    if (!this.server) {\n      const server: http.Server = this.createHttpServer()\n      this.server = httpShutdown(server)\n      this.server.on('request', this.onRequest.bind(this))\n      this.server.on('upgrade', this.onUpgrade.bind(this))\n      this.server.listen(this.pluginOptions.port, this.pluginOptions.host, () => {\n          const serverAddress = this.server.address() as WebSocket.AddressInfo\n          const address = serverAddress.address\n          const port = serverAddress.port\n          this.services.logger.info(EVENT.INFO, `Listening for http connections on ${address}:${port}`)\n          this.services.logger.info(EVENT.INFO, `Listening for health checks on path ${this.pluginOptions.healthCheckPath}`)\n          this.registerGetPathPrefix(this.pluginOptions.healthCheckPath, (meta: DeepstreamHTTPMeta, response: DeepstreamHTTPResponse) => {\n            response(null)\n          })\n          this.isReady = true\n          this.emitter.emit('ready')\n      })\n    }\n    return new Promise((resolve) => this.emitter.once('ready', resolve))\n  }\n\n  public async close (): Promise<void> {\n    const closePromises: Array<Promise<void>> = []\n    this.connections.forEach((conn) => {\n      if (!conn.isClosed) {\n        closePromises.push(new Promise((resolve) => conn.onClose(resolve)))\n        conn.destroy()\n      }\n    })\n    await Promise.all(closePromises)\n    this.connections.clear()\n\n    // @ts-ignore\n    return new Promise((resolve) => this.server.shutdown(resolve))\n  }\n\n  public sendWebsocketMessage (socket: WebSocket, message: any, isBinary: boolean) {\n    socket.send(message, (err) => {\n      if (err) {\n        // message was not sent\n        const socketWrapper = this.connections.get(socket)!\n        this.services.logger.warn(EVENT.ERROR, `Failed to deliver message to ${socketWrapper.userId}, error: ${err.message}`)\n      }\n    })\n  }\n\n  public getSocketWrappersForUserId (userId: string) {\n    return [...this.connections.values()].filter((socketWrapper) => socketWrapper.userId === userId)\n  }\n\n  public registerPostPathPrefix<DataInterface> (prefix: string, handler: PostRequestHandler<DataInterface>) {\n    this.postPaths.set(prefix, handler)\n    this.sortedPostPaths = [...this.postPaths.keys()].sort().reverse()\n  }\n\n  public registerGetPathPrefix (prefix: string, handler: GetRequestHandler) {\n    this.getPaths.set(prefix, handler)\n    this.sortedGetPaths = [...this.getPaths.keys()].sort().reverse()\n  }\n\n  public registerWebsocketEndpoint (path: string, createSocketWrapper: SocketWrapperFactory, webSocketConnectionEndpoint: WebSocketConnectionEndpoint) {\n    const server = new WebSocket.Server({ noServer: true, maxPayload:  webSocketConnectionEndpoint.wsOptions.maxMessageSize})\n    server.on('connection', (websocket: WebSocket, handshakeData: SocketHandshakeData) => {\n      websocket.on('error', (error) => {\n        this.services.logger.error(EVENT.ERROR, `Error on websocket: ${error.message}`)\n      })\n\n      const socketWrapper = createSocketWrapper(websocket, handshakeData, this.services, webSocketConnectionEndpoint.wsOptions, webSocketConnectionEndpoint)\n      socketWrapper.lastMessageRecievedAt = Date.now()\n      this.connections.set(websocket, socketWrapper)\n\n      const interval = setInterval(() => {\n        if ((Date.now() - socketWrapper.lastMessageRecievedAt) > webSocketConnectionEndpoint.wsOptions.heartbeatInterval * 2) {\n          this.services.logger.error(EVENT.INFO, 'Heartbeat missing on websocket, terminating connection')\n          socketWrapper.destroy()\n        }\n      }, webSocketConnectionEndpoint.wsOptions.heartbeatInterval)\n\n      websocket.on('close', () => {\n        clearInterval(interval)\n        webSocketConnectionEndpoint.onSocketClose.call(webSocketConnectionEndpoint, socketWrapper)\n        this.connections.delete(websocket)\n      })\n\n      websocket.on('message', (msg: string) => {\n        socketWrapper.lastMessageRecievedAt = Date.now()\n        const messages = socketWrapper.parseMessage(msg)\n        if (messages.length > 0) {\n          socketWrapper.onMessage(messages)\n        }\n      })\n\n      webSocketConnectionEndpoint.onConnection.call(webSocketConnectionEndpoint, socketWrapper)\n    })\n    this.upgradePaths.set(path, server)\n    this.sortedUpgradePaths = [...this.upgradePaths.keys()].sort().reverse()\n  }\n\n  private createHttpServer () {\n    if (this.pluginOptions.ssl) {\n      const { key, cert, ca } = this.pluginOptions.ssl\n      if (!key || !cert) {\n        this.services.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, 'To enable HTTP please provide a key and cert')\n      }\n      return new https.Server({ key, cert, ca })\n    }\n    return new http.Server()\n  }\n\n  private onUpgrade (\n    request: http.IncomingMessage,\n    socket: Socket,\n    head: Buffer\n   ): void {\n    for (const path of this.sortedUpgradePaths) {\n      if (request.url === path) {\n        const wss = this.upgradePaths.get(path)!\n        wss.handleUpgrade(request, socket, head, (ws) => {\n          wss.emit('connection', ws, {\n            remoteAddress: request.headers['x-forwarded-for'] || request.connection.remoteAddress,\n            headers: request.headers,\n            referer: request.headers.referer\n          })\n        })\n        return\n      }\n    }\n    socket.destroy()\n   }\n\n  private onRequest (\n    request: http.IncomingMessage,\n    response: http.ServerResponse\n   ): void {\n     if (!this.pluginOptions.allowAllOrigins) {\n       if (!this.verifyOrigin(request, response)) {\n         return\n       }\n     } else {\n       response.setHeader('Access-Control-Allow-Origin', '*')\n     }\n\n     switch (request.method) {\n       case 'POST':\n         this.handlePost(request, response)\n         break\n       case 'GET':\n         this.handleGet(request, response)\n         break\n       case 'OPTIONS':\n         this.handleOptions(request, response)\n         break\n       default:\n         this.terminateResponse(\n           response,\n           HTTPStatus.METHOD_NOT_ALLOWED,\n           `Unsupported method. Supported methods: ${this.methodsStr}`\n         )\n     }\n   }\n\n   private handlePost (request: http.IncomingMessage, response: http.ServerResponse): void {\n    let parsedContentType\n    try {\n      parsedContentType = contentType.parse(request)\n    } catch (typeError) {\n      parsedContentType = { type: null }\n    }\n    if (parsedContentType.type !== 'application/json') {\n      this.terminateResponse(\n        response,\n        HTTPStatus.UNSUPPORTED_MEDIA_TYPE,\n        'Invalid \"Content-Type\" header. Supported media types: \"application/json\"'\n      )\n      return\n    }\n\n    this.jsonBodyParser(request, response, (err: Error | null) => {\n      if (err) {\n        this.terminateResponse(\n          response,\n          HTTPStatus.BAD_REQUEST,\n          `Failed to parse body of request: ${err.message}`\n        )\n        return\n      }\n\n      for (const path of this.sortedPostPaths) {\n        if (request.url!.startsWith(path)) {\n          this.postPaths.get(path)!(\n            (request as any).body,\n            { headers: request.headers as Dictionary<string>, url: request.url! },\n            this.sendResponse.bind(this, response)\n          )\n          return\n        }\n      }\n      this.terminateResponse(response, HTTPStatus.NOT_FOUND, 'Endpoint not found.')\n    })\n  }\n\n  private handleGet (request: http.IncomingMessage, response: http.ServerResponse) {\n    for (const path of this.sortedGetPaths) {\n      if (request.url!.startsWith(path)) {\n        this.getPaths.get(path)!(\n          { headers: request.headers as Dictionary<string>, url: request.url! },\n          this.sendResponse.bind(this, response)\n        )\n        return\n      }\n    }\n    this.terminateResponse(response, HTTPStatus.NOT_FOUND, 'Endpoint not found.')\n  }\n\n   private handleOptions (\n    request: http.IncomingMessage,\n    response: http.ServerResponse\n  ): void {\n    const requestMethod = request.headers['access-control-request-method'] as string | undefined\n    if (!requestMethod) {\n      this.terminateResponse(\n        response,\n        HTTPStatus.BAD_REQUEST,\n        'Missing header \"Access-Control-Request-Method\".'\n      )\n      return\n    }\n    if (this.methods.indexOf(requestMethod) === -1) {\n      this.terminateResponse(\n        response,\n        HTTPStatus.FORBIDDEN,\n        `Method ${requestMethod} is forbidden. Supported methods: ${this.methodsStr}`\n      )\n      return\n    }\n\n    const requestHeadersRaw = request.headers['access-control-request-headers'] as string | undefined\n    if (!requestHeadersRaw) {\n      this.terminateResponse(\n        response,\n        HTTPStatus.BAD_REQUEST,\n        'Missing header \"Access-Control-Request-Headers\".'\n      )\n      return\n    }\n    const requestHeaders = requestHeadersRaw.split(',')\n    for (let i = 0; i < requestHeaders.length; i++) {\n      if (this.headersLower.indexOf(requestHeaders[i].trim().toLowerCase()) === -1) {\n        this.terminateResponse(\n          response,\n          HTTPStatus.FORBIDDEN,\n          `Header ${requestHeaders[i]} is forbidden. Supported headers: ${this.headersStr}`\n        )\n        return\n      }\n    }\n\n    response.setHeader('Access-Control-Allow-Methods', this.methodsStr)\n    response.setHeader('Access-Control-Allow-Headers', this.headersStr)\n    this.terminateResponse(response, HTTPStatus.NO_CONTENT)\n  }\n\n  private verifyOrigin (\n    request: http.IncomingMessage,\n    response: http.ServerResponse\n  ): boolean {\n    const requestOriginUrl = request.headers.origin as string || request.headers.referer as string\n    const requestHostUrl = request.headers.host\n    if (this.pluginOptions.hostUrl && requestHostUrl !== this.pluginOptions.hostUrl) {\n      this.terminateResponse(response, HTTPStatus.FORBIDDEN, 'Forbidden Host.')\n      return false\n    }\n    if (this.pluginOptions.origins!.indexOf(requestOriginUrl) === -1) {\n      if (!requestOriginUrl) {\n        this.terminateResponse(\n          response,\n          HTTPStatus.FORBIDDEN,\n          'CORS is configured for this. All requests must set a valid \"Origin\" header.'\n        )\n      } else {\n        this.terminateResponse(\n          response,\n          HTTPStatus.FORBIDDEN,\n          `Origin \"${requestOriginUrl}\" is forbidden.`\n        )\n      }\n      return false\n    }\n\n    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin\n    response.setHeader('Access-Control-Allow-Origin', requestOriginUrl)\n    response.setHeader('Access-Control-Allow-Credentials', 'true')\n    response.setHeader('Vary', 'Origin')\n\n    return true\n  }\n\n  private terminateResponse (response: http.ServerResponse, code: number, message?: string) {\n    response.setHeader('Content-Type', 'text/plain; charset=utf-8')\n    response.writeHead(code)\n    if (message) {\n      response.end(`${message}\\r\\n\\r\\n`)\n    } else {\n      response.end()\n    }\n  }\n\n  private sendResponse (\n    response: http.ServerResponse,\n    err: { statusCode: number, message: string } | null,\n    data: { result: string, body: object }\n  ): void {\n    if (err) {\n      const statusCode = err.statusCode || HTTPStatus.BAD_REQUEST\n      this.terminateResponse(response, statusCode, err.message)\n      return\n    }\n    response.setHeader('Content-Type', 'application/json; charset=utf-8')\n    response.writeHead(HTTPStatus.OK)\n    if (data) {\n      response.end(`${JSON.stringify(data)}\\r\\n\\r\\n`)\n    } else {\n      response.end()\n    }\n  }\n}\n"
  },
  {
    "path": "src/services/http/uws/uws-http.ts",
    "content": "import { DeepstreamPlugin, DeepstreamHTTPService, PostRequestHandler, GetRequestHandler, DeepstreamServices, DeepstreamConfig, SocketWrapper, WebSocketConnectionEndpoint, SocketWrapperFactory, EVENT, DeepstreamHTTPMeta, DeepstreamHTTPResponse } from '@deepstream/types'\nimport { Dictionary } from 'ts-essentials'\nimport { STATES } from '../../../constants'\nimport { PromiseDelay } from '../../../utils/utils'\nimport * as fileUtils from '../../../config/file-utils'\nimport * as HTTPStatus from 'http-status'\n\ninterface UWSHTTPInterface extends uws.AppOptions {\n  healthCheckPath: string,\n  host: string,\n  port: number,\n  allowAllOrigins: boolean,\n  origins?: string[],\n  maxMessageSize: number,\n  maxBackpressure?: number,\n  headers: string[],\n  hostUrl: string\n}\n\ninterface UserData {\n  url: string,\n  headers: Dictionary<string>,\n  referer: string\n}\n\nexport class UWSHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {\n  public description: string = 'UWS HTTP Service'\n  private server!: uws.TemplatedApp\n  private isReady: boolean = false\n  private uWS: typeof uws\n  private connections = new Map<uws.WebSocket<UserData>, SocketWrapper>()\n  private listenSocket!: uws.us_listen_socket\n  private isGettingReady: boolean = false\n  private maxBackpressure?: number = 1024 * 1024\n  private methods: string[] = ['GET', 'POST', 'OPTIONS']\n  private methodsStr: string = this.methods.join(', ')\n  private headers: string[] = ['X-Requested-With', 'X-HTTP-Method-Override', 'Content-Type', 'Accept']\n  private headersLower: string[] = this.headers.map((header) => header.toLowerCase())\n  private headersStr: string = this.headers.join(', ')\n\n  constructor (private pluginOptions: UWSHTTPInterface, private services: DeepstreamServices, config: DeepstreamConfig) {\n    super()\n\n    if (this.pluginOptions.allowAllOrigins === false) {\n      if (this.pluginOptions.origins?.length === 0) {\n        this.services.logger.fatal(EVENT.INVALID_CONFIG_DATA, 'HTTP allowAllOrigins set to false but no origins provided')\n      }\n    }\n    // set maxBackpressure if defined, default is 1024*1024\n    if (this.pluginOptions.maxBackpressure) {\n      this.maxBackpressure = this.pluginOptions.maxBackpressure\n    }\n\n    // alias require to trick nexe from bundling it\n    const req = require\n    try {\n      this.uWS = req('uWebSockets.js')\n    } catch (e) {\n      this.uWS = req(fileUtils.lookupLibRequirePath('uWebSockets.js'))\n    }\n\n    const sslParams = this.getSLLParams(pluginOptions)\n    if (sslParams) {\n      this.server = this.uWS.SSLApp({\n        ...pluginOptions,\n        ...sslParams\n      })\n    } else {\n      this.server = this.uWS.App(pluginOptions)\n    }\n  }\n\n  public async whenReady (): Promise<void> {\n    if (this.isReady || this.isGettingReady) {\n      return\n    }\n    this.isGettingReady = true\n    return new Promise((resolve) => {\n      this.server.listen(this.pluginOptions.host, this.pluginOptions.port, (token) => {\n        /* Save the listen socket for later shut down */\n        this.listenSocket = token\n        // handle options requests\n        this.server.options('/*', (response: uws.HttpResponse, request: uws.HttpRequest) => {\n          const baseHeaders: Dictionary<string> = {}\n          if (!this.pluginOptions.allowAllOrigins) {\n            const corsValidationHeaders = this.getVerifiedOriginHeaders(response, request)\n            if (!corsValidationHeaders) { // Verification failed and response terminated\n              return\n            }\n            Object.assign(baseHeaders, corsValidationHeaders)\n            this.handleOptions(response, request, baseHeaders)\n          } else {\n            baseHeaders['Access-Control-Allow-Origin'] = '*'\n            this.handleOptions(response, request, baseHeaders)\n          }\n        })\n\n        // Health check path uses GET, so CORS headers will be applied by registerGetPathPrefix\n        // The handler calls response(null), which will use sendResponse.\n        // sendResponse will correctly order writeStatus and add Content-Type: application/json.\n        this.registerGetPathPrefix(this.pluginOptions.healthCheckPath, (meta: DeepstreamHTTPMeta, response: DeepstreamHTTPResponse) => {\n          response(null)\n        })\n\n        if (!!token) {\n          resolve()\n          return\n        }\n\n        this.services.logger.fatal(\n          STATES.SERVICE_INIT,\n          `Failed to listen to port: ${this.pluginOptions.port}`\n        )\n      })\n    })\n  }\n\n  public async close (): Promise<void> {\n    const closePromises: Array<Promise<void>> = []\n    this.connections.forEach((conn) => {\n      if (!conn.isClosed) {\n        closePromises.push(new Promise((resolve) => conn.onClose(resolve)))\n        conn.destroy()\n      }\n    })\n    await Promise.all(closePromises)\n    this.connections.clear()\n    this.uWS.us_listen_socket_close(this.listenSocket)\n    await PromiseDelay(2000)\n  }\n\n  public registerPostPathPrefix<DataInterface> (prefix: string, handler: PostRequestHandler<any>) {\n    this.server.post(prefix, (response: uws.HttpResponse, request: uws.HttpRequest) => {\n      /* Register error cb */\n      response.onAborted(() => {\n        this.services.logger.warn(EVENT.ERROR, 'post request aborted')\n      })\n\n      const meta = { headers: this.getHeaders(request), url: request.getUrl() }\n\n      const accumulatedHeaders: Dictionary<string> = {}\n\n      if (!this.pluginOptions.allowAllOrigins) {\n        const corsHeaders = this.getVerifiedOriginHeaders(response, request)\n        if (!corsHeaders) {\n          return // Response already terminated\n        }\n        Object.assign(accumulatedHeaders, corsHeaders)\n      } else {\n        accumulatedHeaders['Access-Control-Allow-Origin'] = '*'\n      }\n\n      readJson(response, (body: any) => {\n        handler(\n          body,\n          meta,\n          (err, data) => this.sendResponse(response, err, data, accumulatedHeaders)\n        )\n      }, (code: number) => {\n        this.terminateResponse(\n          response,\n          code,\n          HTTPStatus[`${code}_MESSAGE` as keyof typeof HTTPStatus] as string,\n          accumulatedHeaders\n        )\n      }, this.pluginOptions.maxMessageSize)\n    })\n  }\n\n  public registerGetPathPrefix (prefix: string, handler: GetRequestHandler) {\n    this.server.get(prefix, (response: uws.HttpResponse, request: uws.HttpRequest) => {\n      /* Register error cb */\n      response.onAborted(() => {\n        this.services.logger.warn(EVENT.ERROR, 'get request aborted')\n      })\n\n      const accumulatedHeaders: Dictionary<string> = {}\n\n      if (!this.pluginOptions.allowAllOrigins) {\n        const corsHeaders = this.getVerifiedOriginHeaders(response, request)\n        if (!corsHeaders) {\n          return // Response already terminated\n        }\n        Object.assign(accumulatedHeaders, corsHeaders)\n      } else {\n        accumulatedHeaders['Access-Control-Allow-Origin'] = '*'\n      }\n\n      handler(\n        { headers: this.getHeaders(request), url: request.getUrl() },\n        // Ensure the bound sendResponse uses these accumulatedHeaders\n        (err, data) => this.sendResponse(response, err, data, accumulatedHeaders)\n      )\n    })\n  }\n\n  public sendWebsocketMessage (socket: uws.WebSocket<UserData>, message: Uint8Array | string, isBinary: boolean) {\n    const sentStatus = socket.send(message, isBinary)\n    if (sentStatus === 2) {\n      // message was not sent\n      const socketWrapper = this.connections.get(socket)!\n      this.services.logger.error(EVENT.ERROR, `Failed to deliver message to userId ${socketWrapper.userId}, current socket backpressure ${socket.getBufferedAmount()}`)\n    }\n  }\n\n  public getSocketWrappersForUserId (userId: string) {\n    return [...this.connections.values()].filter((socketWrapper) => socketWrapper.userId === userId)\n  }\n\n  public registerWebsocketEndpoint (path: string, createSocketWrapper: SocketWrapperFactory, webSocketConnectionEndpoint: WebSocketConnectionEndpoint) {\n    // uws idleTimeout is in seconds and requires it to be > 8\n    const idleTimeout = webSocketConnectionEndpoint.wsOptions.heartbeatInterval * 2 / 1000 > 8 ? webSocketConnectionEndpoint.wsOptions.heartbeatInterval * 2 / 1000 : 8\n\n    this.server.ws(path, {\n      /* Options */\n      compression: 0,\n      /* Maximum length of received message. If a client tries to send you a message larger than this, the connection is immediately closed.*/\n      maxPayloadLength: webSocketConnectionEndpoint.wsOptions.maxMessageSize,\n      /* Maximum length of allowed backpressure per socket when sending messages. Slow receivers with too high backpressure will not receive messages */\n      maxBackpressure: this.maxBackpressure,\n      idleTimeout,\n      upgrade: (response: uws.HttpResponse, request: uws.HttpRequest, context: any) => {\n        /* This immediately calls open handler, you must not use response after this call */\n        response.upgrade({\n          url: request.getUrl(),\n          headers: this.getHeaders(request),\n          referer: request.getHeader('referer')\n        },\n          /* Spell these correctly */\n          request.getHeader('sec-websocket-key'), request.getHeader('sec-websocket-protocol'), request.getHeader('sec-websocket-extensions'), context)\n      },\n      open: (websocket: uws.WebSocket<UserData>) => {\n        const handshakeData = {\n          remoteAddress: new Uint8Array(websocket.getRemoteAddress()).join('.'),\n          headers: websocket.getUserData().headers,\n          referer: websocket.getUserData().referer\n        }\n        const socketWrapper = createSocketWrapper(websocket, handshakeData, this.services, webSocketConnectionEndpoint.wsOptions, webSocketConnectionEndpoint)\n        this.connections.set(websocket, socketWrapper)\n        webSocketConnectionEndpoint.onConnection.call(webSocketConnectionEndpoint, socketWrapper)\n      },\n      message: (ws: uws.WebSocket<UserData>, message: ArrayBuffer, isBinary: boolean) => {\n        const socketWrapper = this.connections.get(ws)!\n        const messages = socketWrapper.parseMessage(isBinary ? new Uint8Array(message) : Buffer.from(message).toString())\n        if (messages.length > 0) {\n          socketWrapper.onMessage(messages)\n        }\n      },\n      drain: (socket: uws.WebSocket<UserData>) => {\n        const socketWrapper = this.connections.get(socket)!\n        this.services.logger.warn(EVENT.INFO, `Socket backpressure drained for userId ${socketWrapper.userId}, current socket backpressure ${socket.getBufferedAmount()}`)\n      },\n      close: (ws: uws.WebSocket<UserData>) => {\n        webSocketConnectionEndpoint.onSocketClose.call(webSocketConnectionEndpoint, this.connections.get(ws)!)\n        this.connections.delete(ws)\n      }\n    } as any)\n  }\n\n  private terminateResponse (response: uws.HttpResponse, code: number, message?: string, additionalHeaders: Dictionary<string> = {}) {\n    response.cork(() => {\n      response.writeStatus(code.toString())\n      for (const key in additionalHeaders) {\n        if (additionalHeaders.hasOwnProperty(key)) {\n          response.writeHeader(key, additionalHeaders[key])\n        }\n      }\n      // Only set Content-Type if there's a message body, and not for 204/304\n      if (message && code !== HTTPStatus.NO_CONTENT) {\n        response.writeHeader('Content-Type', 'text/plain; charset=utf-8')\n        response.end(`${message}\\r\\n\\r\\n`)\n      } else {\n        // For 204 NO_CONTENT or other cases without a message, just end.\n        // uWS requires end() to be called.\n        response.end()\n      }\n    })\n  }\n\n  private sendResponse (\n    response: uws.HttpResponse,\n    err: { statusCode: number, message: string } | null,\n    data: { result: string, body: object },\n    additionalHeaders: Dictionary<string> = {}\n  ): void {\n    if (err) {\n      const statusCode = err.statusCode || HTTPStatus.BAD_REQUEST\n      this.terminateResponse(response, statusCode, err.message, additionalHeaders)\n      return\n    }\n\n    response.cork(() => {\n      response.writeStatus(HTTPStatus.OK.toString())\n      for (const key in additionalHeaders) {\n        if (additionalHeaders.hasOwnProperty(key)) {\n          response.writeHeader(key, additionalHeaders[key])\n        }\n      }\n      response.writeHeader('Content-Type', 'application/json; charset=utf-8')\n      if (data) {\n        response.end(`${JSON.stringify(data)}\\r\\n\\r\\n`)\n      } else {\n        response.end()\n      }\n    })\n  }\n  public getHeaders (req: uws.HttpRequest) {\n    const headers: Dictionary<string> = {}\n    for (const wantedHeader of this.pluginOptions.headers) {\n      headers[wantedHeader] = req.getHeader(wantedHeader).toString()\n    }\n    return headers\n  }\n  private getSLLParams (options: any) {\n    if (!options.ssl) {\n      return null\n    }\n    // tslint:disable-next-line: variable-name\n    const { key: key_file_name, cert: cert_file_name, dhParams: dh_params_file_name, passphrase } = options.ssl\n    if (key_file_name || cert_file_name) {\n      if (!key_file_name) {\n        this.services.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, 'Must also include ssl key in order to use SSL')\n      }\n      if (!cert_file_name) {\n        this.services.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, 'Must also include ssl cert in order to use SSL')\n      }\n      return {\n        key_file_name,\n        cert_file_name,\n        dh_params_file_name,\n        passphrase,\n      }\n    }\n    return null\n  }\n\n  // This method now either terminates the response or returns headers for the caller to use.\n  private getVerifiedOriginHeaders (response: uws.HttpResponse, request: uws.HttpRequest): Dictionary<string> | null {\n    const requestOriginUrl = request.getHeader('origin') as string || request.getHeader('referer') as string\n    const requestHostUrl = request.getHeader('host')\n\n    if (this.pluginOptions.hostUrl && requestHostUrl !== this.pluginOptions.hostUrl) {\n      this.terminateResponse(response, HTTPStatus.FORBIDDEN, 'Forbidden Host.')\n      return null\n    }\n\n    if (this.pluginOptions.origins!.indexOf(requestOriginUrl) === -1) {\n      if (!requestOriginUrl) {\n        this.terminateResponse(\n          response,\n          HTTPStatus.FORBIDDEN,\n          'CORS is configured for this. All requests must set a valid \"Origin\" header.',\n          // No additional headers known at this point for the error response itself\n        )\n      } else {\n        this.terminateResponse(\n          response,\n          HTTPStatus.FORBIDDEN,\n          `Origin \"${requestOriginUrl}\" is forbidden.`,\n          // No additional headers known at this point for the error response itself\n        )\n      }\n      return null\n    }\n\n    // If verification is successful, return the headers to be set by the caller.\n    return {\n      'Access-Control-Allow-Origin': requestOriginUrl,\n      'Access-Control-Allow-Credentials': 'true', // Typically needed with specific origins\n      'Vary': 'Origin' // Good practice when Access-Control-Allow-Origin is dynamic\n    }\n  }\n\n  private handleOptions (response: uws.HttpResponse, request: uws.HttpRequest, baseCorsHeaders: Dictionary<string>): void {\n    const allHeadersForResponse = { ...baseCorsHeaders }\n\n    const requestMethod = request.getHeader('access-control-request-method') as string | undefined\n    if (!requestMethod) {\n      this.terminateResponse(\n        response,\n        HTTPStatus.BAD_REQUEST,\n        'Missing header \"Access-Control-Request-Method\".',\n        allHeadersForResponse // Pass along already determined CORS headers\n      )\n      return\n    }\n    if (this.methods.indexOf(requestMethod) === -1) {\n      this.terminateResponse(\n        response,\n        HTTPStatus.FORBIDDEN,\n        `Method ${requestMethod} is forbidden. Supported methods: ${this.methodsStr}`,\n        allHeadersForResponse\n      )\n      return\n    }\n\n    const requestHeadersRaw = request.getHeader('access-control-request-headers') as string | undefined\n    if (!requestHeadersRaw) {\n      // Some browsers might not send this for simple requests, but for preflight it's expected.\n      // Depending on strictness, this could be an error or allowed.\n      // For now, let's assume it's required for a preflight OPTIONS.\n      this.terminateResponse(\n        response,\n        HTTPStatus.BAD_REQUEST,\n        'Missing header \"Access-Control-Request-Headers\".'\n      )\n      return\n    }\n    const requestHeaders = requestHeadersRaw.split(',')\n    for (let i = 0; i < requestHeaders.length; i++) {\n      if (this.headersLower.indexOf(requestHeaders[i].trim().toLowerCase()) === -1) {\n        this.terminateResponse(\n          response,\n          HTTPStatus.FORBIDDEN,\n          `Header ${requestHeaders[i]} is forbidden. Supported headers: ${this.headersStr}`,\n          allHeadersForResponse\n        )\n        return\n      }\n    }\n\n    allHeadersForResponse['Access-Control-Allow-Methods'] = this.methodsStr\n    allHeadersForResponse['Access-Control-Allow-Headers'] = this.headersStr\n    this.terminateResponse(response, HTTPStatus.NO_CONTENT, undefined, allHeadersForResponse)\n  }\n}\n\n/* Helper function for reading a posted JSON body */\nfunction readJson (res: uws.HttpResponse, cb: Function, err: (code: number) => void, limit: number) {\n  let buffer: Buffer\n  let received: number = 0\n\n  res.onData((ab, isLast) => {\n    const chunk = Buffer.from(ab)\n    received += chunk.length\n    // check max length\n    if (received > limit) {\n      err(HTTPStatus.REQUEST_ENTITY_TOO_LARGE)\n      return\n    }\n\n    if (isLast) {\n      let json\n      if (buffer) {\n        try {\n          json = JSON.parse(Buffer.concat([buffer, chunk]).toString())\n        } catch (e) {\n          err(HTTPStatus.BAD_REQUEST)\n          return\n        }\n        cb(json)\n      } else {\n        try {\n          json = JSON.parse(chunk.toString())\n        } catch (e) {\n          err(HTTPStatus.BAD_REQUEST)\n          return\n        }\n        cb(json)\n      }\n    } else {\n      if (buffer) {\n        buffer = Buffer.concat([buffer, chunk])\n      } else {\n        buffer = Buffer.concat([chunk])\n      }\n    }\n  })\n}\n"
  },
  {
    "path": "src/services/lock/distributed-lock-registry.ts",
    "content": "import {EventEmitter} from 'events'\nimport Timeout = NodeJS.Timeout\nimport { DeepstreamPlugin, DeepstreamLockRegistry, DeepstreamServices, DeepstreamConfig, LockCallback, EVENT } from '@deepstream/types'\nimport { TOPIC, LOCK_ACTION, LockMessage } from '../../constants'\n\n/**\n * The lock registry is responsible for maintaing a single source of truth\n * within the cluster, used mainly for issuing cluster wide locks when an operation\n * that stretches over multiple nodes are required.\n *\n * For example, distributed listening requires a leader to drive the nodes in sequence,\n * so issuing a lock prevents multiple nodes from assuming the lead.\n *\n */\nexport class DistributedLockRegistry extends DeepstreamPlugin implements DeepstreamLockRegistry {\n  public description: string = 'Distributed Lock Registry'\n  private locks = new Set<string>()\n  private timeouts = new Map<string, Timeout>()\n  private responseEventEmitter = new EventEmitter()\n\n  /**\n   * The unique registry is a singleton and is only created once\n   * within deepstream.io. It is passed via\n   * via the options object.\n   */\n  constructor (private pluginOptions: any, private services: Readonly<DeepstreamServices>, private config: Readonly<DeepstreamConfig>) {\n    super()\n    this.onPrivateMessage =  this.onPrivateMessage.bind(this)\n  }\n\n  public init () {\n    this.services.clusterNode.subscribe(TOPIC.LOCK, this.onPrivateMessage)\n  }\n\n  /**\n   * Requests a lock, if the leader ( whether local or distributed ) has the lock availble\n   * it will invoke the callback with true, otherwise false.\n   */\n  public get (lockName: string, callback: LockCallback) {\n    if (this.services.clusterRegistry.isLeader()) {\n      callback(this.getLock(lockName))\n    } else if (!this.timeouts.has(lockName)) {\n       this.getRemoteLock(lockName, callback)\n    } else {\n      callback(false)\n    }\n  }\n\n  /**\n   * Release a lock, allowing other resources to request it again\n   */\n  public release (lockName: string) {\n    if (this.services.clusterRegistry.isLeader()) {\n       this.releaseLock(lockName)\n    } else {\n       this.releaseRemoteLock(lockName)\n    }\n  }\n\n  /**\n   * Called when the current node is not the leader, issuing a lock request\n   * via the message bus\n   */\n  private getRemoteLock (lockName: string, callback: LockCallback) {\n    const leaderServerName = this.services.clusterRegistry.getLeader()\n\n    this.timeouts.set(lockName, setTimeout(\n      this.onLockRequestTimeout.bind(this, lockName),\n      this.pluginOptions.requestTimeout\n    ))\n\n    this.responseEventEmitter.once(lockName, callback)\n\n    this.services.clusterNode.sendDirect(leaderServerName, {\n      topic: TOPIC.LOCK,\n      action: LOCK_ACTION.REQUEST,\n      name: lockName\n    })\n  }\n\n  /**\n   * Notifies a remote leader keeping a lock that said lock is no longer required\n   */\n  private releaseRemoteLock (lockName: string) {\n    const leaderServerName = this.services.clusterRegistry.getLeader()\n    this.services.clusterNode.sendDirect(leaderServerName, {\n      topic:  TOPIC.LOCK,\n      action: LOCK_ACTION.RELEASE,\n      name: lockName\n    })\n  }\n\n  /**\n   * Called when a message is received on the message bus.\n   * This could mean the leader responded to a request or that you're currently\n   * the leader and received a request.\n   */\n  private onPrivateMessage (message: LockMessage, remoteServerName: string) {\n    if (message.action === LOCK_ACTION.RESPONSE) {\n        this.handleRemoteLockResponse(message.name!, message.locked)\n        return\n    }\n\n    if (this.services.clusterRegistry.isLeader() === false) {\n      this.services.logger.warn(\n          EVENT.INVALID_LEADER_REQUEST,\n          `server ${remoteServerName} assumes this node '${this.config.serverName}' is the leader`\n      )\n      return\n    }\n\n    if (message.action === LOCK_ACTION.REQUEST) {\n      this.handleRemoteLockRequest(message.name, remoteServerName)\n      return\n    }\n\n    if (message.action === LOCK_ACTION.RELEASE) {\n       this.releaseLock(message.name!)\n       return\n    }\n  }\n\n  /**\n   * Called when a remote lock request is received\n   */\n  private handleRemoteLockRequest (lockName: string, remoteServerName: string) {\n    this.services.clusterNode.sendDirect(remoteServerName, {\n      topic: TOPIC.LOCK,\n      action: LOCK_ACTION.RESPONSE,\n      name: lockName,\n      locked: this.getLock(lockName)\n    })\n  }\n\n  /**\n   * Called when a remote lock response is received\n   */\n  private handleRemoteLockResponse (lockName: string, result: boolean) {\n    clearTimeout(this.timeouts.get(lockName)!)\n    this.timeouts.delete(lockName)\n    this.responseEventEmitter.emit(lockName, result)\n  }\n\n  /**\n   * Returns true if reserving lock was possible otherwise returns false\n   */\n  private getLock (lockName: string) {\n    if (this.locks.has(lockName)) {\n      return false\n    }\n\n    this.timeouts.set(lockName, setTimeout(\n        this.onLockTimeout.bind(this, lockName),\n        this.pluginOptions.holdTimeout\n    ))\n    this.locks.add(lockName)\n    return true\n  }\n\n  /**\n   * Called when a lock is no longer required and can be released. This is triggered either by\n   * a timeout if a remote release message wasn't received in time or when release was called\n   * locally.\n   *\n   * Important note: Anyone can release a lock. It is assumed that the cluster is trusted\n   * so maintaining who has the lock is not required. This may need to change going forward.\n   */\n  private releaseLock (lockName: string) {\n    clearTimeout(this.timeouts.get(lockName)!)\n    this.timeouts.delete(lockName)\n    this.locks.delete(lockName)\n  }\n\n  /**\n   * Called when a timeout occurs on a lock that has been reserved for too long\n   */\n  private onLockTimeout (lockName: string) {\n    this.releaseLock(lockName)\n    this.services.logger.warn(EVENT.LOCK_RELEASE_TIMEOUT, `lock ${lockName} released due to timeout`, { lockName })\n  }\n\n  /**\n   * Called when a remote request has timed out, resulting in notifying the client that\n   * the lock wasn't able to be reserved\n   */\n  private onLockRequestTimeout (lockName: string) {\n    this.handleRemoteLockResponse(lockName, false)\n    this.services.logger.warn(EVENT.LOCK_REQUEST_TIMEOUT, `request for lock ${lockName} timed out`, { lockName })\n  }\n}\n"
  },
  {
    "path": "src/services/logger/pino/pino-logger.ts",
    "content": "import pino, { LoggerOptions } from 'pino'\nimport { LOG_LEVEL, DeepstreamPlugin, DeepstreamLogger, DeepstreamConfig, DeepstreamServices, NamespacedLogger, EVENT } from '@deepstream/types'\n\nconst DSToPino: { [index: number]: pino.LevelWithSilent } = {\n    [LOG_LEVEL.DEBUG]: 'debug',\n    [LOG_LEVEL.FATAL]: 'fatal',\n    [LOG_LEVEL.ERROR]: 'error',\n    [LOG_LEVEL.WARN]: 'warn',\n    [LOG_LEVEL.INFO]: 'info',\n    [LOG_LEVEL.OFF]: 'silent',\n}\n\nexport class PinoLogger extends DeepstreamPlugin implements DeepstreamLogger {\n    public description = 'Pino Logger'\n    private logger: pino.Logger\n\n    constructor (pluginOptions: LoggerOptions, private services: DeepstreamServices, config: DeepstreamConfig) {\n        super()\n        this.logger = pino(pluginOptions)\n        this.setLogLevel(config.logLevel)\n    }\n\n    /**\n     * Return true if logging is enabled. This is used in deepstream to stop generating useless complex strings\n     * that we know will never be logged.\n     */\n    public shouldLog (logLevel: LOG_LEVEL): boolean {\n        return this.logger.isLevelEnabled(DSToPino[logLevel])\n    }\n\n    /**\n     * Set the log level desired by deepstream. Since deepstream uses LOG_LEVEL this needs to be mapped\n     * to whatever your libary uses (this is usually just conversion stored in a static map)\n     */\n    public setLogLevel (logLevel: LOG_LEVEL): void {\n        this.logger.level = DSToPino[logLevel]\n    }\n\n    /**\n     * Log as info\n     */\n    public info (event: EVENT, message?: string, metaData?: any): void {\n        if (metaData) {\n            this.logger.info({ event, message, ...metaData })\n        } else {\n            this.logger.info({ event, message })\n        }\n    }\n\n    /**\n     * Log as debug\n     */\n    public debug (event: EVENT, message?: string, metaData?: any): void {\n        if (metaData) {\n            this.logger.debug({ event, message, ...metaData })\n        } else {\n            this.logger.debug({ event, message })\n        }\n    }\n\n    /**\n     * Log as warn\n     */\n    public warn (event: EVENT, message?: string, metaData?: any): void {\n        if (metaData) {\n            this.logger.warn({ event, message, ...metaData })\n        } else {\n            this.logger.warn({ event, message })\n        }\n    }\n\n    /**\n     * Log as error\n     */\n    public error (event: EVENT, message?: string, metaData?: any): void {\n        this.services.monitoring.onErrorLog(LOG_LEVEL.ERROR, event, message!, metaData!)\n        if (metaData) {\n            this.logger.error({ event, message, ...metaData })\n        } else {\n            this.logger.error({ event, message })\n        }\n    }\n\n    /**\n     * Log as fatal\n     */\n    public fatal (event: EVENT, message?: string, metaData?: any): void {\n        this.services.monitoring.onErrorLog(LOG_LEVEL.FATAL, event, message!, metaData!)\n        if (metaData) {\n            this.logger.fatal({ event, message, ...metaData })\n        } else {\n            this.logger.fatal({ event, message })\n        }\n        this.services.notifyFatalException()\n    }\n\n    /**\n     * Create a namespaced logger, used by plugins. This could either be a new instance of a logger\n     * or just a thin wrapper to add the namespace at the beginning of the log method.\n     */\n    public getNameSpace (namespace: string): NamespacedLogger {\n        return {\n          shouldLog: this.shouldLog.bind(this),\n          fatal: this.log.bind(this, DSToPino[LOG_LEVEL.FATAL], namespace),\n          error: this.log.bind(this, DSToPino[LOG_LEVEL.ERROR], namespace),\n          warn: this.log.bind(this, DSToPino[LOG_LEVEL.WARN], namespace),\n          info: this.log.bind(this, DSToPino[LOG_LEVEL.INFO], namespace),\n          debug: this.log.bind(this, DSToPino[LOG_LEVEL.DEBUG], namespace),\n        }\n    }\n\n    private log (logLevel: pino.LevelWithSilent, namespace: string, event: EVENT, message: string, metaData?: any ) {\n        this.logger[logLevel]({ namespace, event, message, ...metaData })\n    }\n}\n"
  },
  {
    "path": "src/services/logger/std/std-out-logger.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport {spy} from 'sinon'\n\nimport { StdOutLogger } from './std-out-logger'\nimport { LOG_LEVEL, EVENT, DeepstreamConfig } from '@deepstream/types';\n\ndescribe('logs to stdout and stderr', () => {\n  const logger = new StdOutLogger({ color: false }, undefined, { logLevel: LOG_LEVEL.DEBUG } as DeepstreamConfig)\n  const originalStdOut = process.stdout\n  const originalStdErr = process.stderr\n  const stdout = spy()\n  const stderr = spy()\n  const comp = function (std, exp) {\n    return std.lastCall.args[0].indexOf(exp) !== -1\n  }\n\n  before(() => {\n    Object.defineProperty(process, 'stdout', {\n      value: { write: stdout }\n    })\n    Object.defineProperty(process, 'stderr', {\n      value: { write: stderr }\n    })\n  })\n\n  after(() => {\n    Object.defineProperty(process, 'stdout', {\n      value: originalStdOut\n    })\n    Object.defineProperty(process, 'stderr', {\n      value: originalStdErr\n    })\n  })\n\n  it('creates the logger', async () => {\n    await logger.whenReady()\n    logger.info(EVENT.INFO, 'b')\n    expect(comp(stdout, 'INFO | b')).to.equal(true)\n  })\n\n  it('logs to stderr', () => {\n    stdout.resetHistory()\n    logger.error(EVENT.INFO, 'e')\n    expect(stdout).to.have.callCount(0)\n    expect(stderr).to.have.callCount(1)\n  })\n\n  it('logs above log level', () => {\n    logger.setLogLevel(LOG_LEVEL.DEBUG)\n    stdout.resetHistory()\n    logger.info(EVENT.INFO, 'e')\n    expect(stdout).to.have.callCount(1)\n    logger.setLogLevel(LOG_LEVEL.WARN)\n    stdout.resetHistory()\n    logger.info(EVENT.INFO, 'e')\n    expect(stdout).to.have.callCount(0)\n  })\n})\n"
  },
  {
    "path": "src/services/logger/std/std-out-logger.ts",
    "content": "import * as chalk from 'chalk'\nimport { DeepstreamPlugin, DeepstreamLogger, DeepstreamServices, DeepstreamConfig, LOG_LEVEL, NamespacedLogger, EVENT, MetaData } from '@deepstream/types'\nimport { EOL } from 'os'\n\nexport class StdOutLogger extends DeepstreamPlugin implements DeepstreamLogger {\n  public description = 'std out/err'\n\n  private useColors: boolean\n  private currentLogLevel!: LOG_LEVEL\n  private logLevelColors: string[] = [\n    'white',\n    'green',\n    'yellow',\n    'red',\n    'blue'\n  ]\n\n  /**\n   * Logs to the operatingsystem's standard-out and standard-error streams.\n   *\n   * Consoles / Terminals as well as most log-managers and logging systems\n   * consume messages from these streams\n   */\n  constructor (private options: any = {}, private services: DeepstreamServices, config: DeepstreamConfig) {\n    super()\n    this.useColors = this.options.colors === undefined ? true : this.options.colors\n    this.setLogLevel(config.logLevel)\n  }\n\n  public async whenReady (): Promise<void> {\n    this.description = `${this.description} at level ${LOG_LEVEL[this.currentLogLevel]}`\n  }\n\n  public shouldLog (logLevel: number): boolean {\n    return this.currentLogLevel >= logLevel\n  }\n\n  public debug (event: EVENT, logMessage: string): void {\n    this.log(LOG_LEVEL.DEBUG, '', event, logMessage)\n  }\n\n  public info (event: EVENT, logMessage: string): void {\n    this.log(LOG_LEVEL.INFO, '', event, logMessage)\n  }\n\n  public warn (event: EVENT, logMessage: string, metaData?: MetaData): void {\n    this.log(LOG_LEVEL.WARN, '', event, logMessage, metaData)\n  }\n\n  public error (event: EVENT, logMessage: string, metaData?: MetaData): void {\n    this.log(LOG_LEVEL.ERROR, '', event, logMessage, metaData)\n  }\n\n  public fatal (event: EVENT, logMessage: string, metaData?: MetaData): void {\n    this.log(LOG_LEVEL.FATAL, '', event, logMessage, metaData)\n    this.services.notifyFatalException()\n  }\n\n  public getNameSpace (namespace: string): NamespacedLogger {\n    return {\n      shouldLog: this.shouldLog.bind(this),\n      fatal: this.log.bind(this, LOG_LEVEL.FATAL, namespace),\n      error: this.log.bind(this, LOG_LEVEL.ERROR, namespace),\n      warn: this.log.bind(this, LOG_LEVEL.WARN, namespace),\n      info: this.log.bind(this, LOG_LEVEL.INFO, namespace),\n      debug: this.log.bind(this, LOG_LEVEL.DEBUG, namespace),\n    }\n  }\n\n  /**\n   * Sets the log-level. This can be called at runtime.\n   */\n  public setLogLevel (logLevel: LOG_LEVEL) {\n    this.currentLogLevel = logLevel\n  }\n\n  /**\n   * Logs a line\n   */\n  private log (logLevel: LOG_LEVEL, namespace: string, event: EVENT, logMessage: string, metaData: MetaData | null = null): void {\n    if (logLevel >= LOG_LEVEL.WARN && this.services && this.services.monitoring) {\n      this.services.monitoring.onErrorLog(logLevel, event, logMessage, metaData!)\n    }\n\n    if (this.currentLogLevel > logLevel) {\n      return\n    }\n\n    const msg = `${namespace ? `${namespace} | ` : '' }${event} | ${logMessage}`\n    let outputStream\n\n    if (logLevel === LOG_LEVEL.ERROR || logLevel === LOG_LEVEL.WARN) {\n      outputStream = 'stderr'\n    } else {\n      outputStream = 'stdout'\n    }\n\n    if (this.useColors) {\n      (process as any)[outputStream].write((chalk as any)[this.logLevelColors[logLevel]](msg) + EOL)\n    } else {\n      (process as any)[outputStream].write(msg + EOL)\n    }\n\n    if (logLevel === LOG_LEVEL.FATAL) {\n      this.services.notifyFatalException()\n    }\n  }\n}\n"
  },
  {
    "path": "src/services/monitoring/combine-monitoring.ts",
    "content": "import { DeepstreamPlugin, DeepstreamMonitoring, SocketData, LOG_LEVEL, EVENT, MetaData } from '@deepstream/types'\nimport { Message } from '../../constants'\n\n/**\n * The combine monitoring handler allows multiple monitoring plugins,\n * this allows to develop plugins that handle independantly multiple aspects of the monitoring: audit logs, user behaviour, more complex presence logic, etc\n * */\nexport class CombineMonitoring extends DeepstreamPlugin implements DeepstreamMonitoring {\n  public description: string = ''\n\n  constructor (private monitorings: DeepstreamMonitoring[]) {\n    super()\n    if (monitorings.length === 1) {\n      this.description = monitorings[0].description\n    } else {\n      this.description = monitorings.map((monitoring, index) => `\\n\\t${index}) ${monitoring.description}`).join('')\n    }\n  }\n\n  public async whenReady () {\n    await Promise.all(this.monitorings.map((monitoring) => monitoring.whenReady()))\n  }\n\n  public async close () {\n    await Promise.all(this.monitorings.map((monitoring) => monitoring.close()))\n  }\n\n  public init () {\n    this.monitorings.forEach((monitoring) => monitoring.init ? monitoring.init() : null)\n  }\n\n  public onErrorLog (loglevel: LOG_LEVEL, event: EVENT, logMessage: string, metaData: MetaData): void {\n    // NOTE: If using another logger service other than std-out or pino, the logger service must call this endpoint when logging.\n    this.monitorings.forEach((monitoring) => monitoring.onErrorLog(loglevel, event, logMessage, metaData))\n  }\n\n  public onLogin (allowed: boolean, endpointType: string): void {\n    this.monitorings.forEach((monitoring) => monitoring.onLogin(allowed, endpointType))\n  }\n\n  public onMessageReceived (message: Message, socketData: SocketData): void {\n    this.monitorings.forEach((monitoring) => monitoring.onMessageReceived(message, socketData))\n  }\n\n  public onMessageSend (message: Message): void {\n    this.monitorings.forEach((monitoring) => monitoring.onMessageSend(message))\n  }\n\n  public onBroadcast (message: Message, count: number): void {\n    this.monitorings.forEach((monitoring) => monitoring.onBroadcast(message, count))\n  }\n}\n"
  },
  {
    "path": "src/services/monitoring/http/monitoring-http.spec.ts",
    "content": "describe('stub', () => {\n    it ('does nothing yet', () => {\n    })\n})\n"
  },
  {
    "path": "src/services/monitoring/http/monitoring-http.ts",
    "content": "import { DeepstreamServices, DeepstreamHTTPMeta, DeepstreamHTTPResponse, EVENT } from '@deepstream/types'\nimport { MonitoringBase } from '../monitoring-base'\n\ninterface HTTPMonitoringOptions {\n    url: string,\n    headerKey: string,\n    headerValue: string,\n    allowOpenPermissions: boolean\n}\n\nexport default class HTTPMonitoring extends MonitoringBase {\n    public description = 'HTTP Monitoring'\n    private logger = this.services.logger.getNameSpace('HTTP_MONITORING')\n\n    constructor (private pluginOptions: HTTPMonitoringOptions, services: DeepstreamServices) {\n        super(services)\n        if (typeof pluginOptions.url !== 'string') {\n            this.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, 'Missing \"url\" for HTTP Monitoring')\n        }\n        if (this.pluginOptions.allowOpenPermissions) {\n            this.logger.warn(EVENT.PLUGIN_INITIALIZATION_ERROR, '\"allowOpenPermissions\" is set. Try not to deploy to production')\n        } else if (!pluginOptions.headerKey || !pluginOptions.headerValue) {\n            this.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, 'Missing \"headerKey\" and/or \"headerValue\"')\n        }\n        this.description +=  ` on ${this.pluginOptions.url}`\n    }\n\n    public async whenReady (): Promise<void> {\n        await this.services.httpService.whenReady()\n\n        let from = Date.now()\n        this.services.httpService.registerGetPathPrefix(this.pluginOptions.url, (metaData: DeepstreamHTTPMeta, response: DeepstreamHTTPResponse) => {\n            if (this.pluginOptions.allowOpenPermissions !== true) {\n                if (metaData.headers[this.pluginOptions.headerKey] !== this.pluginOptions.headerValue) {\n                    this.logger.warn(EVENT.AUTH_ERROR, 'Invalid monitoring data due to missing or invalid header values')\n                    return response({\n                        statusCode: 404,\n                        message: 'Endpoint not found.'\n                    })\n                }\n            }\n            const to = Date.now()\n            response(null, {\n                from,\n                to,\n                ...this.getAndResetMonitoringStats()\n            })\n            from = to\n        })\n    }\n\n    public async close (): Promise<void> {\n    }\n}\n"
  },
  {
    "path": "src/services/monitoring/log/monitoring-log.spec.ts",
    "content": "describe('stub', () => {\n    it ('does nothing yet', () => {\n    })\n})\n"
  },
  {
    "path": "src/services/monitoring/log/monitoring-log.ts",
    "content": "import { DeepstreamServices, NamespacedLogger } from '@deepstream/types'\nimport { MonitoringBase } from '../monitoring-base'\n\ninterface HTTPMonitoringOptions {\n    logInterval: number,\n    monitoringKey: string\n}\n\nexport default class LogMonitoring extends MonitoringBase {\n    public description = 'Log Monitoring'\n    private logInterval!: NodeJS.Timeout\n    private logger: NamespacedLogger\n\n    constructor (private pluginOptions: HTTPMonitoringOptions, services: DeepstreamServices) {\n        super(services)\n        this.pluginOptions.monitoringKey = pluginOptions.monitoringKey || 'LOG_MONITORING'\n        this.logger = this.services.logger.getNameSpace(this.pluginOptions.monitoringKey)\n        this.pluginOptions.logInterval = pluginOptions.logInterval || 15000\n        this.description += ` every ${this.pluginOptions.logInterval / 1000} seconds`\n    }\n\n    public async whenReady (): Promise<void> {\n        let lastDate = Date.now()\n        this.logInterval = setInterval(() => {\n            const newDate = Date.now()\n            this.logger.info(`Monitoring stats for ${lastDate} to ${newDate}`, JSON.stringify({\n                [this.pluginOptions.monitoringKey]: this.getAndResetMonitoringStats(),\n                from: lastDate,\n                to: newDate\n            }))\n            lastDate = newDate\n        }, this.pluginOptions.logInterval)\n    }\n\n    public async close (): Promise<void> {\n        clearInterval(this.logInterval)\n    }\n}\n"
  },
  {
    "path": "src/services/monitoring/monitoring-base.ts",
    "content": "import { Message, ACTIONS } from '@deepstream/protobuf/dist/types/messages'\nimport { TOPIC, STATE_REGISTRY_TOPIC } from '@deepstream/protobuf/dist/types/all'\nimport { DeepstreamMonitoring, DeepstreamPlugin, DeepstreamServices, LOG_LEVEL, EVENT } from '@deepstream/types'\n\nexport abstract class MonitoringBase extends DeepstreamPlugin implements DeepstreamMonitoring {\n    private errorLogs: { [index: string]: number } = {}\n    private receiveStats: { [index: string]: { [index: string]: number } } = {}\n    private sendStats: { [index: string]: { [index: string]: number } } = {}\n    private loginStats: { [index: string]: {\n        allowed: number,\n        declined: number\n    } } = {}\n\n    constructor (protected services: DeepstreamServices) {\n        super()\n    }\n\n    public onErrorLog (loglevel: LOG_LEVEL, event: EVENT, logMessage: string): void {\n        const count = this.errorLogs[event]\n        if (!count) {\n            this.errorLogs[event] = 1\n        } else {\n            this.errorLogs[event] = count + 1\n        }\n    }\n\n    /**\n     * Called whenever a login attempt is tried and whether or not it succeeded, as well\n     * as the connection-endpoint type, which is provided from the connection endpoint\n     * itself\n     */\n    public onLogin (allowed: boolean, endpointType: string): void {\n        let stats = this.loginStats[endpointType]\n        if (!stats) {\n            stats = { allowed: 0, declined: 0 }\n            this.loginStats[endpointType] = stats\n        }\n        allowed ? stats.allowed++ : stats.declined++\n    }\n\n    public onMessageReceived (message: Message): void {\n        let actionsMap = this.receiveStats[TOPIC[message.topic]]\n        if (!actionsMap) {\n            actionsMap = {}\n            this.receiveStats[TOPIC[message.topic]] = actionsMap\n        }\n        const actionName = ACTIONS[message.topic][message.action!]\n        actionsMap[actionName] = actionsMap[actionName] ? actionsMap[actionName] + 1 : 1\n    }\n\n    public onMessageSend (message: Message): void {\n        let actionsMap = this.sendStats[TOPIC[message.topic]]\n        if (!actionsMap) {\n            actionsMap = {}\n            this.sendStats[TOPIC[message.topic]] = actionsMap\n        }\n        const actionName = ACTIONS[message.topic][message.action!]\n        actionsMap[actionName] = actionsMap[actionName] ? actionsMap[actionName] + 1 : 1\n    }\n\n    public onBroadcast (message: Message, count: number): void {\n        let actionsMap = this.receiveStats[TOPIC[message.topic]]\n        if (!actionsMap) {\n            actionsMap = {}\n            this.sendStats[TOPIC[message.topic]] = actionsMap\n        }\n        const actionName = ACTIONS[message.topic][message.action!]\n        actionsMap[actionName] = actionsMap[actionName] ? actionsMap[actionName] + count : count\n    }\n\n    public getAndResetMonitoringStats () {\n        const results = {\n            clusterSize: this.services.clusterRegistry.getAll().length,\n            stateMetrics: this.getStateMetrics(),\n            errors: this.errorLogs,\n            received: this.receiveStats,\n            send: this.sendStats,\n            logins: this.loginStats\n        }\n        this.errorLogs = {}\n        this.receiveStats = {}\n        this.sendStats = {}\n        this.loginStats = {}\n        return results\n    }\n\n    private getStateMetrics () {\n        const result: any = {}\n        const stateRegistries = this.services.clusterStates.getStateRegistries()\n        for (const [topic, stateRegistry] of stateRegistries) {\n            result[TOPIC[topic] || STATE_REGISTRY_TOPIC[topic]] = stateRegistry.getAll().length\n        }\n        return result\n    }\n\n}\n"
  },
  {
    "path": "src/services/monitoring/noop-monitoring.ts",
    "content": "import { DeepstreamPlugin, DeepstreamMonitoring, SocketData, LOG_LEVEL, EVENT } from '@deepstream/types'\nimport { Message } from '../../constants'\n\nexport class NoopMonitoring extends DeepstreamPlugin implements DeepstreamMonitoring {\n  public description: string = 'Noop Monitoring'\n\n  public onErrorLog (loglevel: LOG_LEVEL, event: EVENT, logMessage: string): void {\n  }\n\n  public onLogin (allowed: boolean, endpointType: string): void {\n  }\n\n  public onMessageReceived (message: Message, socketData: SocketData): void {\n  }\n\n  public onMessageSend (message: Message): void {\n  }\n\n  public onBroadcast (message: Message, count: number): void {\n  }\n}\n"
  },
  {
    "path": "src/services/permission/open/open-permission.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport { OpenPermission } from './open-permission'\n\ndescribe('open permission handler', () => {\n  let permission\n\n  it('allows any action', (done) => {\n    permission = new OpenPermission()\n\n    const message = {\n      topic: 'This doesnt matter',\n      action: 'Since it allows anything',\n      data: ['anything']\n    }\n    permission.canPerformAction('someone', message, (socketWrapper, msg, passItOn, error, success) => {\n      expect(error).to.equal(null)\n      expect(success).to.equal(true)\n      done()\n    }, {})\n  })\n})\n"
  },
  {
    "path": "src/services/permission/open/open-permission.ts",
    "content": "import { Message } from '../../../constants'\nimport { DeepstreamPlugin, DeepstreamPermission, PermissionCallback, SocketWrapper } from '@deepstream/types'\n\n/**\n * The open permission handler allows any action to occur without applying\n * any permissions.\n */\nexport class OpenPermission extends DeepstreamPlugin implements DeepstreamPermission {\n  public description: string = 'none'\n\n  /**\n  * Allows any action by an user\n  */\n  public canPerformAction (socketWrapper: SocketWrapper, message: Message, callback: PermissionCallback, passItOn: any) {\n    callback(socketWrapper, message, passItOn, null, true)\n  }\n}\n"
  },
  {
    "path": "src/services/permission/valve/config-compiler.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nconst configCompiler = require('./config-compiler')\n\ndescribe('compiles user entered config specs into an optimized format', () => {\n  it('exposes a compile method', () => {\n    expect(typeof configCompiler.compile).to.equal('function')\n  })\n\n  it('compiles a basic config', () => {\n    const conf = {\n      record: {\n        'user/$userId': {\n          write: '$userId === user.id'\n        }\n      }\n    }\n\n    const compiledConf = configCompiler.compile(conf)\n\n    expect(Array.isArray(compiledConf.record)).to.equal(true)\n    expect(compiledConf.record.length).to.equal(1)\n    expect(compiledConf.record[0].regexp).to.not.equal(undefined)\n    expect(compiledConf.record[0].variables).to.deep.equal(['$userId'])\n    expect(compiledConf.record[0].rules.write.fn).to.not.equal(undefined)\n    expect(compiledConf.record[0].rules.write.hasOldData).to.equal(false)\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/config-compiler.ts",
    "content": "import * as pathParser from './path-parser'\nimport * as ruleParser from './rule-parser'\nimport { ValveSchema } from '@deepstream/types'\n\n/**\n * Compiles a pre-validated config into a format that allows for quicker access\n * and execution\n */\nexport const compile = function (config: ValveSchema) {\n  const compiledConfig: any = {}\n  let compiledRuleset\n  let section\n  let path\n\n  for (section in config) {\n    compiledConfig[section] = []\n\n    for (path in config[section]) {\n      compiledRuleset = compileRuleset(path, config[section][path])\n      compiledConfig[section].push(compiledRuleset)\n    }\n  }\n\n  return compiledConfig\n}\n\n/**\n * Compiles an individual ruleset\n */\nfunction compileRuleset (path: string, rules: any) {\n  const ruleset = pathParser.parse(path)\n\n  ruleset.rules = {}\n\n  for (const ruleType in rules) {\n    ruleset.rules[ruleType] = ruleParser.parse(\n      rules[ruleType], ruleset.variables,\n    )\n  }\n\n  return ruleset\n}\n"
  },
  {
    "path": "src/services/permission/valve/config-permission-basic.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as C from '../../../constants'\nimport { getBasePermissions } from '../../../test/helper/test-helper'\nimport * as testHelper from '../../../test/helper/test-helper'\nimport { ConfigPermission } from './config-permission'\n\nconst options = testHelper.getDeepstreamPermissionOptions()\nconst config = options.config\nconst services = options.services\n\nconst testPermission = testHelper.testPermission(options)\n\nconst lastError = function () {\n  return services.logger.logSpy.lastCall.args[2]\n}\n\ndescribe('permission handler applies basic permissions to incoming messages', () => {\n  it('allows everything for a basic permission set', () => {\n    const permissions = getBasePermissions()\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'someRecord'\n    }\n    expect(testPermission(permissions, message)).to.equal(true)\n  })\n\n  it('denies reading of a private record', () => {\n    const permissions = getBasePermissions()\n\n    permissions.record['private/$userId'] = {\n      read: 'user.id === $userId'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'private/userA'\n    }\n\n    expect(testPermission(permissions, message, 'userB')).to.equal(false)\n  })\n\n  it('allows actions that dont need permissions for a private record', () => {\n    const permissions = getBasePermissions()\n\n    permissions.record['private/$userId'] = {\n      read: 'user.id === $userId'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UNSUBSCRIBE,\n      name: 'private/userA'\n    }\n\n    expect(testPermission(permissions, message, 'userB')).to.equal(true)\n  })\n\n  it('allows reading of a private record for the right user', () => {\n    const permissions = getBasePermissions()\n\n    permissions.record['private/$userId'] = {\n      read: 'user.id === $userId'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'private/userA'\n    }\n\n    expect(testPermission(permissions, message, 'userA')).to.equal(true)\n  })\n\n  it('can reference the name', () => {\n    const permissions = getBasePermissions()\n\n    permissions.record['private/userA'] = {\n      read: 'name === \"private/userA\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'private/userA'\n    }\n\n    expect(testPermission(permissions, message, 'userA')).to.equal(true)\n  })\n\n  it('denies snapshot of a private record', () => {\n    const permissions = getBasePermissions()\n\n    permissions.record['private/$userId'] = {\n      read: 'user.id === $userId'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'private/userA'\n    }\n\n    expect(testPermission(permissions, message, 'userB')).to.equal(false)\n  })\n})\n\ndescribe('permission handler applies basic permissions referencing their own data', () => {\n  it('checks incoming data against a value for events', () => {\n    const permissions = getBasePermissions()\n\n    permissions.event['some-event'] = {\n      publish: 'data.price < 10'\n    }\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event',\n      data: '{\"price\":15}'\n    })).to.equal(false)\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event',\n      data: '{\"price\":5}'\n    })).to.equal(true)\n  })\n\n  it('can reference data for events without a payload and fail normally', () => {\n    const permissions = getBasePermissions()\n    permissions.event['some-event'] = {\n      publish: 'data.price < 10'\n    }\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event'\n    })).to.equal(false)\n  })\n\n  it('checks incoming data against a value for rpcs', () => {\n    const permissions = getBasePermissions()\n\n    permissions.rpc['*'] = {\n      request: false\n    }\n\n    permissions.rpc['trade/book'] = {\n      request: 'user.data.role === \"fx-trader\" && data.assetClass === \"fx\"'\n    }\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.REQUEST,\n      name: 'trade/book',\n      correlationId: '1234',\n      data: '{\"assetClass\": \"equity\"}'\n    }, null, { role: 'eq-trader' })).to.equal(false)\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.REQUEST,\n      name: 'trade/book',\n      correlationId: '1234',\n      data: '{\"assetClass\": \"fx\"}'\n    }, null, { role: 'fx-trader' })).to.equal(true)\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.REQUEST,\n      name: 'trade/book',\n      correlationId: '1234',\n      data: '{\"assetClass\": \"fx\"}'\n    }, null, { role: 'eq-trader' })).to.equal(false)\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.RPC,\n      action: C.RPC_ACTION.REQUEST,\n      name: 'trade/cancel',\n      correlationId: '1234',\n      data: '{\"assetClass\": \"fx\"}'\n    }, null, { role: 'fx-trader' })).to.equal(false)\n  })\n\n  it('checks incoming data against a value for record updates', () => {\n    const permissions = getBasePermissions()\n\n    permissions.record['cars/mercedes'] = {\n      write: 'data.manufacturer === \"mercedes-benz\"'\n    }\n\n    permissions.record['cars/porsche/$model'] = {\n      write: 'data.price > 50000 && data.model === $model'\n    }\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'cars/mercedes',\n      version: 1,\n      data: '{\"manufacturer\":\"mercedes-benz\"}'\n    })).to.equal(true)\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'cars/mercedes',\n      version: 1,\n      data: '{\"manufacturer\":\"BMW\"}'\n    })).to.equal(false)\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'cars/porsche/911',\n      version: 1,\n      data: '{\"model\": \"911\", \"price\": 60000 }'\n    })).to.equal(true)\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'cars/porsche/911',\n      version: 1,\n      data: '{\"model\": \"911\", \"price\": 40000 }'\n    })).to.equal(false)\n\n    expect(testPermission(permissions, {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'cars/porsche/911',\n      version: 1,\n      data: '{\"model\": \"Boxter\", \"price\": 70000 }'\n    })).to.equal(false)\n  })\n\n  it.skip('checks against existing data for non-existant record reads', (next) => {\n    const permissions = getBasePermissions()\n\n    permissions.record['non-Existing-Record'] = {\n      read: 'oldData.xyz === \"hello\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'non-Existing-Record',\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('Cannot read property \\'xyz\\' of null')\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, 'user', null, callback)\n  })\n\n  it.skip('checks against existing data for non-existant list reads', (next) => {\n    const permissions = getBasePermissions()\n\n    permissions.record['non-Existing-Record'] = {\n      read: 'oldData.indexOf(\"hello\") !== -1'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'non-Existing-Record',\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('Cannot read property \\'indexOf\\' of null')\n      expect(error).to.equal(C.RECORD_ACTION.MESSAGE_PERMISSION_ERROR)\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, 'user', null, callback)\n\n  })\n\n  it('deals with broken messages', (next) => {\n    const permissions = getBasePermissions()\n\n    permissions.record['cars/mercedes'] = {\n      write: 'data.manufacturer === \"mercedes-benz\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'cars/mercedes',\n      version: 1,\n      data: '{\"manufacturer\":\"mercedes-benz\"'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('error when converting message data')\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, 'user', null, callback)\n  })\n\n  it('deals with messages with invalid types', (next) => {\n    const permissions = getBasePermissions()\n\n    permissions.event['some-event'] = {\n      publish: 'data.manufacturer === \"mercedes-benz\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event',\n      data: 'xxx'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('error when converting message data')\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, 'user', null, callback)\n  })\n})\n\ndescribe('loads permissions repeatedly', () => {\n  let permission\n\n  it('creates the permission', async () => {\n    permission = new ConfigPermission({ permissions: getBasePermissions() }, services, config)\n    permission.setRecordHandler({\n      removeRecordRequest: () => {},\n      runWhenRecordStable: (r, c) => { c(r) }\n    })\n    await permission.whenReady()\n  })\n\n  it('requests permissions initially, causing a lookup', (next) => {\n    const message = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event',\n      data: 'some-data'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      next()\n    }\n\n    permission.canPerformAction('some-user', message, callback)\n  })\n\n  it('requests permissions a second time, causing a cache retrieval', (next) => {\n    const message = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event',\n      data: 'some-data'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      next()\n    }\n\n    permission.canPerformAction('some-user', message, callback)\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/config-permission-create.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as C from '../../../constants'\n\nimport { getBasePermissions } from '../../../test/helper/test-helper'\nimport * as testHelper from '../../../test/helper/test-helper'\n\nconst options = testHelper.getDeepstreamPermissionOptions()\nconst services = options.services\nconst testPermission = testHelper.testPermission(options)\n\ndescribe('allows to create a record without providing data, but denies updating it', () => {\n  const permissions = getBasePermissions()\n  permissions.record['some/*'] = {\n    write: 'data.name === \"Wolfram\"'\n  }\n\n  beforeEach(() => {\n    services.cache.set('some/tests', 0, {}, () => {})\n  })\n\n  it('allows creating the record', () => {\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.SUBSCRIBECREATEANDREAD,\n      name: 'some/tests'\n    }\n\n    expect(testPermission(permissions, message)).to.equal(true)\n  })\n\n  it('denies update', () => {\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'some/tests',\n      version: 2,\n      data: '{\"other\":\"data\"}'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(false)\n    }\n\n    testPermission(permissions, message, 'some-user', null, callback)\n  })\n\n  it('denies patch', () => {\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.PATCH,\n      name: 'some/tests',\n      version: 2,\n      path: 'apath',\n      data: '\"aValue\"'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(false)\n    }\n\n    testPermission(permissions, message, 'some-user', null, callback)\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/config-permission-cross-reference.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as C from '../../../constants'\n\nimport { getBasePermissions } from '../../../test/helper/test-helper'\nimport * as testHelper from '../../../test/helper/test-helper'\n\nconst noop = function () {}\nconst options = testHelper.getDeepstreamPermissionOptions()\nconst services = options.services\nconst testPermission = testHelper.testPermission(options)\n\nconst lastError = function () {\n  return services.logger.logSpy.lastCall.args[2]\n}\n\ndescribe('permission handler loads data for cross referencing', () => {\n  before((next) => {\n    services.cache.set('item/doesExist', 0, { isInStock: true }, next)\n  })\n\n  it('retrieves an existing record from a synchronous cache', (next) => {\n    const permissions = getBasePermissions()\n    services.cache.nextGetWillBeSynchronous = true\n\n    permissions.record['purchase/$itemId'] = {\n      read: '_(\"item/\" + $itemId).isInStock === true'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'purchase/doesExist'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      expect(services.cache.lastRequestedKey).to.equal('item/doesExist')\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('retrieves two records from the cache for cross referencing purposes', (next) => {\n    const permissions = getBasePermissions()\n\n    services.cache.set('item/itemA', 0, { isInStock: true }, noop)\n    services.cache.set('item/itemB', 0, { isInStock: false }, noop)\n\n    services.cache.nextGetWillBeSynchronous = false\n    permissions.record['purchase/$itemId'] = {\n      read: '_(\"item/\" + $itemId).isInStock === true && _(\"item/itemB\").isInStock === false'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'purchase/itemA'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('retrieves and expects a non existing record', (next) => {\n    const permissions = getBasePermissions()\n\n    services.cache.nextGetWillBeSynchronous = false\n    permissions.record['purchase/$itemId'] = {\n      read: '_(\"doesNotExist\") !== null && _(\"doesNotExist\").isInStock === true'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'purchase/itemA'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('gets a non existant record thats not expected', (next) => {\n    const permissions = getBasePermissions()\n\n    services.cache.nextGetWillBeSynchronous = false\n    permissions.record['purchase/$itemId'] = {\n      read: '_(\"doesNotExist\").isInStock === true'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'purchase/itemA'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('TypeError')\n      .and.contain('null')\n      .and.contain('isInStock')\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('mixes old data and cross references', (next) => {\n    const permissions = getBasePermissions()\n    services.cache.reset()\n    services.cache.set('userA', 0, { firstname: 'Egon' }, noop)\n    services.cache.set('userB', 0, { firstname: 'Mike' }, noop)\n    services.cache.nextGetWillBeSynchronous = false\n    permissions.record.userA = {\n      read: 'oldData.firstname === \"Egon\" && _(\"userB\").firstname === \"Mike\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'userA'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      expect(services.cache.getCalls.length).to.equal(2)\n      expect(services.cache.hadGetFor('userA')).to.equal(true)\n      expect(services.cache.hadGetFor('userB')).to.equal(true)\n      setTimeout(next, 200)\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('retrieves keys from name', (next) => {\n    const permissions = getBasePermissions()\n\n    services.cache.set('some-event', 0, { firstname: 'Joe' }, noop)\n\n    permissions.event['some-event'] = {\n      publish: '_(name).firstname === \"Joe\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      next()\n    }\n\n    testPermission(permissions, message, 'username', null, callback)\n  })\n\n  it('retrieves keys from variables', (next) => {\n    const permissions = getBasePermissions()\n\n    services.cache.set('userX', 0, { firstname: 'Joe' }, noop)\n\n    permissions.event['some-event'] = {\n      publish: '_(data.owner).firstname === \"Joe\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event',\n      data: '{\"owner\":\"userX\"}'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      next()\n    }\n\n    testPermission(permissions, message, 'username', null, callback)\n  })\n\n  it('retrieves keys from variables again', (next) => {\n    const permissions = getBasePermissions()\n\n    services.cache.set('userX', 0, { firstname: 'Mike' }, noop)\n\n    permissions.event['some-event'] = {\n      publish: '_(data.owner).firstname === \"Joe\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event',\n      data: '{\"owner\":\"userX\"}'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, 'username', null, callback)\n  })\n\n  it('handles load errors', (next) => {\n    const permissions = getBasePermissions()\n\n    permissions.event['some-event'] = {\n      publish: '_(\"bla\") < 10'\n    }\n    services.cache.nextOperationWillBeSuccessful = false\n\n    const message = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.EMIT,\n      name: 'some-event',\n      data: '{\"price\":15}'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(C.RECORD_ACTION.RECORD_LOAD_ERROR)\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, 'username', null, callback)\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/config-permission-load.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport {spy, assert} from 'sinon'\nimport * as C from '../../../constants'\nimport { ConfigPermission } from './config-permission'\nimport * as testHelper from '../../../test/helper/test-helper'\nimport { PromiseDelay } from '../../../utils/utils'\nimport { EVENT } from '@deepstream/types'\n\nimport * as invalidPermissionConfig from '../../../test/config/invalid-permission-conf.json'\nimport * as noPrivateEventsConfig from '../../../test/config/no-private-events-permission-config.json'\n\nconst { config, services } = testHelper.getDeepstreamPermissionOptions()\n\ndescribe('permission handler loading', () => {\n  beforeEach(() => {\n    services.logger.fatal = spy()\n  })\n\n  describe('permission handler is initialised correctly', () => {\n    it('loads a valid config file upon initialization', async () => {\n      const permission = new ConfigPermission({\n        permissions: testHelper.getBasePermissions(),\n        cacheEvacuationInterval: 60000,\n        maxRuleIterations: 10\n      }, services, config)\n      assert.notCalled(services.logger.fatal)\n      await permission.whenReady()\n    })\n\n    it('fails to load maxRuleIterations less than zero initialization', async () => {\n      const permission = new ConfigPermission({\n        permissions: testHelper.getBasePermissions(),\n        cacheEvacuationInterval: 60000,\n        maxRuleIterations: 0\n      }, services, config)\n\n      assert.calledOnce(services.logger.fatal)\n      assert.calledWithExactly(services.logger.fatal, EVENT.PLUGIN_INITIALIZATION_ERROR, 'Maximum rule iteration has to be at least one')\n    })\n\n    it('fails when loading an invalid config file upon initialization', async () => {\n      // tslint:disable-next-line: no-unused-expression\n      new ConfigPermission({\n        permissions: invalidPermissionConfig,\n        cacheEvacuationInterval: 60000,\n        maxRuleIterations: 10\n      }, services, config)\n\n      await PromiseDelay(10)\n\n      assert.calledOnce(services.logger.fatal)\n      assert.calledWithExactly(services.logger.fatal, EVENT.PLUGIN_INITIALIZATION_ERROR, 'invalid permission config - empty section \"record\"')\n    })\n  })\n\n  describe('it loads a new config during runtime', () => {\n    let permission: ConfigPermission\n    const onError = spy()\n\n    it('loads a valid config file upon initialization', async () => {\n      permission = new ConfigPermission({\n        permissions: testHelper.getBasePermissions(),\n        cacheEvacuationInterval: 60000,\n        maxRuleIterations: 10\n      }, services, config)\n\n      await permission.whenReady()\n    })\n\n    it('allows publishing of a private event', (next) => {\n      const message = {\n        topic: C.TOPIC.EVENT,\n        action: C.EVENT_ACTION.EMIT,\n        name: 'private/event',\n        data: 'somedata'\n      }\n\n      const callback = function (socketWrapper, msg, passItOn, error, result) {\n        expect(error).to.equal(null)\n        expect(result).to.equal(true)\n        next()\n      }\n\n      permission.canPerformAction('some-user', message, callback)\n    })\n\n    it('denies publishing of a private event', (next) => {\n      expect(onError).to.have.callCount(0)\n\n      const message = {\n        topic: C.TOPIC.EVENT,\n        action: C.EVENT_ACTION.EMIT,\n        name: 'private/event',\n        data: 'somedata'\n      }\n\n      const callback = function (socketWrapper, msg, passItOn, error, result) {\n        expect(error).to.equal(null)\n        expect(result).to.equal(false)\n        next()\n      }\n\n      permission.useConfig(noPrivateEventsConfig)\n      permission.canPerformAction('some-user', message, callback, {})\n    })\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/config-permission-nested-cross-reference.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as testHelper from '../../../test/helper/test-helper'\nimport * as C from '../../../constants'\n\nconst noop = function () {}\nconst options = testHelper.getDeepstreamPermissionOptions()\nconst services = options.services\nconst testPermission = testHelper.testPermission(options)\n\nconst lastError = function () {\n  return services.logger.logSpy.lastCall.args[2]\n}\n\ndescribe('permission handler loads data for cross referencing', () => {\n  it('retrieves data for a nested cross references', (next) => {\n    const permissions = testHelper.getBasePermissions()\n\n    services.cache.set('thing/x', 0, { ref: 'y' }, noop)\n    services.cache.set('thing/y', 0, { is: 'it' }, noop)\n\n    services.cache.nextGetWillBeSynchronous = false\n    permissions.record['test-record'] = {\n      read: '_( \"thing/\" + _( \"thing/x\" ).ref ).is === \"it\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'test-record'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      expect(services.cache.getCalls.length).to.equal(2)\n      expect(services.cache.hadGetFor('thing/x')).to.equal(true)\n      expect(services.cache.hadGetFor('thing/y')).to.equal(true)\n      expect(services.cache.hadGetFor('thing/z')).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('erors for undefined fields in crossreferences', (next) => {\n    const permissions = testHelper.getBasePermissions()\n\n    services.cache.set('thing/x', 0, { ref: 'y' }, noop)\n    services.cache.set('thing/y', 0, { is: 'it' }, noop)\n\n    services.cache.nextGetWillBeSynchronous = false\n    permissions.record['test-record'] = {\n      read: '_( \"thing/\" + _( \"thing/x\" ).doesNotExist ).is === \"it\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'test-record'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('TypeError')\n      .and.contain('undefined')\n      .and.contain('is')\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('can use the same cross reference multiple times', (next) => {\n    const permissions = testHelper.getBasePermissions()\n\n    services.cache.reset()\n    services.cache.set('user', 0, { firstname: 'Wolfram', lastname: 'Hempel' }, noop)\n    services.cache.nextGetWillBeSynchronous = false\n\n    permissions.record['test-record'] = {\n      read: '_(\"user\").firstname === \"Wolfram\" && _(\"user\").lastname === \"Hempel\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'test-record'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      expect(services.cache.getCalls.length).to.equal(1)\n      expect(services.cache.hadGetFor('user')).to.equal(true)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('supports nested references to the same record', (next) => {\n    const permissions = testHelper.getBasePermissions()\n\n    services.cache.reset()\n    services.cache.set('user', 0, { ref: 'user', firstname: 'Egon' }, noop)\n    services.cache.nextGetWillBeSynchronous = false\n\n    permissions.record['test-record'] = {\n      read: '_(_(\"user\").ref).firstname === \"Egon\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'test-record'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      expect(services.cache.getCalls.length).to.equal(1)\n      expect(services.cache.hadGetFor('user')).to.equal(true)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('errors for objects as cross reference arguments', (next) => {\n    const permissions = testHelper.getBasePermissions()\n\n    services.cache.reset()\n    services.cache.set('user', 0, { ref: { bla: 'blub' } }, noop)\n    services.cache.nextGetWillBeSynchronous = false\n\n    permissions.record['test-record'] = {\n      read: '_(_(\"user\").ref).firstname === \"Egon\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'test-record'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('crossreference got unsupported type object')\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('prevents nesting beyond limit', (next) => {\n    const permissions = testHelper.getBasePermissions()\n\n    services.cache.reset()\n    services.cache.set('a', 0, 'a', noop)\n    services.cache.set('ab', 0, 'b', noop)\n    services.cache.set('abc', 0, 'c', noop)\n    services.cache.set('abcd', 0, 'd', noop)\n    services.cache.set('abcde', 0, 'e', noop)\n    services.cache.nextGetWillBeSynchronous = false\n    permissions.record['test-record'] = {\n      read: '_(_(_(_(_(\"a\")+\"b\")+\"c\")+\"d\")+\"e\")'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.READ,\n      name: 'test-record'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('Exceeded max iteration count')\n      expect(result).to.equal(false)\n      expect(services.cache.getCalls.length).to.equal(3)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/config-permission-other.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport { getBasePermissions } from '../../../test/helper/test-helper'\nimport * as C from '../../../constants'\nimport * as testHelper from '../../../test/helper/test-helper'\nimport { ConfigPermission } from './config-permission';\n\nconst { config, services } = testHelper.getDeepstreamPermissionOptions()\nconst testPermission = testHelper.testPermission({ config, services })\n\ndescribe('supports spaces after variables and escaped quotes', () => {\n  it('errors for read with data', () => {\n    const permissions = getBasePermissions()\n    permissions.record.someUser = {\n      read: 'data.firstname === \"Yasser\"',\n      write: 'data .firstname === \"Yasser\"'\n    }\n\n    try {\n      // tslint:disable-next-line:no-unused-expression\n      new ConfigPermission(config, services, permissions)\n    } catch (e) {\n      expect(e.toString()).to.contain('invalid permission config - rule read for record does not support data')\n    }\n  })\n\n  it('allows yasser', (next) => {\n    const permissions = getBasePermissions()\n    permissions.record.someUser = {\n      write: 'data .firstname === \"Yasser\"'\n    }\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'someUser',\n      version: 1,\n      data: '{\"firstname\":\"Yasser\"}'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      next()\n    }\n\n    testPermission(permissions, message, 'Yasser', null, callback)\n  })\n\n  it('denies Wolfram', (next) => {\n    const permissions = getBasePermissions()\n    permissions.record.someUser = {\n      write: 'data .firstname === \"Yasser\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.UPDATE,\n      name: 'someUser',\n      version: 1,\n      data: '{\"firstname\":\"Wolfram\"}'\n    }\n\n    const callback = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, 'Yasser', null, callback)\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/config-permission-record-patch.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as C from '../../../constants'\nimport * as testHelper from '../../../test/helper/test-helper'\n\nconst noop = function () {}\n\nconst options = testHelper.getDeepstreamPermissionOptions()\nconst services = options.services\nconst testPermission = testHelper.testPermission(options)\n\nconst lastError = function () {\n  return services.logger.logSpy.lastCall.args[2]\n}\n\ndescribe('constructs data for patch message validation', () => {\n  it('fails to set incorrect data', (next) => {\n    const permissions = testHelper.getBasePermissions()\n    services.cache.nextGetWillBeSynchronous = false\n\n    permissions.record['user/wh'] = {\n      write: 'data.firstname === \"Wolfram\" && data.lastname === \"Hempel\"'\n    }\n\n    services.cache.set('user/wh', 0, { firstname: 'Wolfram', lastname: 'Something Else' }, noop)\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.PATCH,\n      name: 'user/wh',\n      version: 123,\n      path: 'lastname',\n      data: '\"Miller\"'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('succeeds if both old and new data is correct', (next) => {\n    const permissions = testHelper.getBasePermissions()\n    services.cache.nextGetWillBeSynchronous = false\n\n    permissions.record['user/wh'] = {\n      write: 'data.firstname === \"Wolfram\" && data.lastname === \"Hempel\"'\n    }\n\n    services.cache.set('user/wh', 1, { firstname: 'Wolfram', lastname: 'Something Else' }, noop)\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.PATCH,\n      name: 'user/wh',\n      version: 123,\n      path: 'lastname',\n      data: '\"Hempel\"'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(error).to.equal(null)\n      expect(result).to.equal(true)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('errors if the patch message has data with an invalid json', (next) => {\n    const permissions = testHelper.getBasePermissions()\n    services.cache.nextGetWillBeSynchronous = false\n\n    permissions.record['user/wh'] = {\n      write: 'data.firstname === \"Wolfram\" && data.lastname === \"Hempel\"'\n    }\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.PATCH,\n      name: 'user/wh',\n      version: 123,\n      path: 'lastname',\n      data: '['\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('SyntaxError: Unexpected end of JSON input')\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n\n  it('returns false if patch if for a non existing record', (next) => {\n    const permissions = testHelper.getBasePermissions()\n    services.cache.nextGetWillBeSynchronous = false\n\n    permissions.record['*'].write = 'data.lastname === \"Blob\"'\n\n    const message = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.PATCH,\n      name: 'somerecord',\n      version: 1,\n      path: 'lastname',\n      data: '\"Hempel\"'\n    }\n\n    const onDone = function (socketWrapper, msg, passItOn, error, result) {\n      expect(lastError()).to.contain('Tried to apply patch to non-existant record somerecord')\n      expect(result).to.equal(false)\n      next()\n    }\n\n    testPermission(permissions, message, null, null, onDone)\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/config-permission.ts",
    "content": "import * as configCompiler from './config-compiler'\nimport * as configValidator from './config-validator'\nimport RuleApplication from './rule-application'\nimport RuleCache from './rule-cache'\nimport * as rulesMap from './rules-map'\nimport { Message, RECORD_ACTION, EVENT_ACTION, RPC_ACTION, PRESENCE_ACTION } from '../../../constants'\nimport RecordHandler from '../../../handlers/record/record-handler'\nimport { DeepstreamPlugin, DeepstreamPermission, ValveConfig, DeepstreamServices, DeepstreamConfig, PermissionCallback, SocketWrapper, EVENT, ValveSchema } from '@deepstream/types'\n\nconst UNDEFINED = 'undefined'\n\nexport type RuleType = string\nexport type ValveSection = string\n\nexport class ConfigPermission extends DeepstreamPlugin implements DeepstreamPermission {\n  public description = 'Valve Permissions'\n\n  private ruleCache: RuleCache\n  private permissions: any\n  private recordHandler: RecordHandler | null = null\n  private logger = this.services.logger.getNameSpace('PERMISSION')\n\n  /**\n   * A permission handler that reads a rules config YAML or JSON, validates\n   * its contents, compiles it and executes the permissions that it contains\n   * against every incoming message.\n   *\n   * This is the standard permission handler that deepstream exposes, in conjunction\n   * with the default permission.yml it allows everything, but at the same time provides\n   * a convenient starting point for permission declarations.\n   */\n  constructor (private permissionOptions: ValveConfig, private services: Readonly<DeepstreamServices>, private config: Readonly<DeepstreamConfig>) {\n    super()\n    this.ruleCache = new RuleCache(this.permissionOptions)\n\n    const maxRuleIterations = permissionOptions.maxRuleIterations\n    if (maxRuleIterations !== undefined && maxRuleIterations < 1) {\n      this.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, 'Maximum rule iteration has to be at least one')\n    }\n    this.useConfig(permissionOptions.permissions)\n  }\n\n  public async whenReady (): Promise<void> {\n  }\n\n  public async close () {\n    this.ruleCache.close()\n  }\n\n  /**\n   * Will be invoked with the initialized recordHandler instance by deepstream.io\n   */\n  public setRecordHandler (recordHandler: RecordHandler): void {\n    this.recordHandler = recordHandler\n  }\n\n  /**\n   * Validates and compiles a loaded config. This can be called as the result\n   * of a config being passed to the permission service upon initialization,\n   * as a result of loadConfig or at runtime\n   *\n   * CLI useConfig <config>\n   */\n  public useConfig (permissions: ValveSchema): void {\n    const validationResult = configValidator.validate(permissions)\n\n    if (validationResult !== true) {\n      this.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, `invalid permission config - ${validationResult}`)\n      return\n    }\n\n    this.permissions = configCompiler.compile(permissions)\n    this.ruleCache.reset()\n  }\n\n  /**\n   * Implements the permission service's canPerformAction interface\n   * method\n   *\n   * This is the main entry point for permissionOperations and will\n   * be called for every incoming message. This method executes four steps\n   *\n   * - Check if the incoming message conforms to basic specs\n   * - Check if the incoming message requires permissions\n   * - Load the applicable permissions\n   * - Apply them\n   */\n  public canPerformAction (socketWrapper: SocketWrapper, message: Message, callback: PermissionCallback, passItOn: any) {\n    const ruleSpecification = rulesMap.getRulesForMessage(message)\n\n    if (ruleSpecification === null) {\n      callback(socketWrapper, message, passItOn, null, true)\n      return\n    }\n\n    const ruleData = this.getCompiledRulesForName(message.name!, ruleSpecification)\n    if (!ruleData) {\n      callback(socketWrapper, message, passItOn, null, false)\n      return\n    }\n\n    // tslint:disable-next-line\n    new RuleApplication({\n      recordHandler: this.recordHandler!,\n      socketWrapper,\n      userId: socketWrapper.userId,\n      serverData: socketWrapper.serverData,\n      path: ruleData,\n      ruleSpecification,\n      message,\n      action: ruleSpecification.action as (RECORD_ACTION | EVENT_ACTION | RPC_ACTION | PRESENCE_ACTION),\n      regexp: ruleData.regexp,\n      rule: ruleData.rule,\n      name: message.name!,\n      callback,\n      passItOn,\n      logger: this.logger,\n      permissionOptions: this.permissionOptions,\n      config: this.config,\n      services: this.services,\n    })\n  }\n\n  /**\n   * Evaluates the rules within a section and returns the matching rule for a path.\n   * Takes basic specificity (as deduced from the path length) into account and\n   * caches frequently used rules for faster access\n   */\n  private getCompiledRulesForName (name: string, ruleSpecification: any): any {\n    const compiledRules = this.ruleCache.get(ruleSpecification.section, name, ruleSpecification.type)\n    if (compiledRules) {\n      return compiledRules\n    }\n\n    const sections = this.permissions[ruleSpecification.section]\n    let pathLength = 0\n    let result: any = null\n\n    for (let i = 0; i < sections.length; i++) {\n      const { rules, path, regexp } = sections[i]\n      if (typeof rules[ruleSpecification.type] !== UNDEFINED && path.length >= pathLength && regexp.test(name)) {\n        pathLength = path.length\n        result = {\n          path,\n          regexp,\n          rule: rules[ruleSpecification.type],\n        }\n      }\n    }\n\n    if (result) {\n      this.ruleCache.set(ruleSpecification.section, name, ruleSpecification.type, result)\n    }\n\n    return result\n  }\n}\n"
  },
  {
    "path": "src/services/permission/valve/config-schema.ts",
    "content": "import { ConfigSchema } from '@deepstream/types'\n\nexport const SCHEMA: ConfigSchema = {\n  record: {\n    write: true,\n    read: true,\n    create: true,\n    delete: true,\n    listen: true,\n    notify: true,\n  },\n  event: {\n    publish: true,\n    subscribe: true,\n    listen: true,\n  },\n  rpc: {\n    provide: true,\n    request: true,\n  },\n  presence: {\n    allow: true,\n  },\n}\n"
  },
  {
    "path": "src/services/permission/valve/config-validator.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as configValidator from './config-validator'\nimport * as testHelper from '../../../test/helper/test-helper'\n\ndescribe('it validates permission.json files', () => {\n  it('exposes a validate method', () => {\n    expect(typeof configValidator.validate).to.equal('function')\n  })\n\n  it('validates a basic configuration', () => {\n    expect(configValidator.validate(testHelper.getBasePermissions())).to.equal(true)\n  })\n\n  it('validates the type of the configuration', () => {\n    expect(configValidator.validate()).to.equal('config should be an object literal, but was of type undefined')\n    expect(configValidator.validate('bla')).to.equal('config should be an object literal, but was of type string')\n    expect(configValidator.validate(testHelper.getBasePermissions())).to.equal(true)\n  })\n\n  it('fails if a top level key is missing', () => {\n    const conf = testHelper.getBasePermissions()\n    delete conf.record\n    expect(configValidator.validate(conf)).to.equal('missing configuration section \"record\"')\n  })\n\n  it('fails if an unknown top level key is added', () => {\n    const conf = testHelper.getBasePermissions()\n    conf.bogus = {}\n    expect(configValidator.validate(conf)).to.equal('unexpected configuration section \"bogus\"')\n  })\n\n  it('fails for empty sections', () => {\n    const conf = testHelper.getBasePermissions()\n    conf.rpc = {}\n    expect(configValidator.validate(conf)).to.equal('empty section \"rpc\"')\n  })\n\n  it('fails if no root permissions are specified', () => {\n    const conf = testHelper.getBasePermissions()\n    conf.rpc = { bla: {\n      request: 'user.id === $userId'\n    } }\n    expect(configValidator.validate(conf)).to.equal('missing root entry \"*\" for section rpc')\n  })\n\n  it('fails for invalid paths', () => {\n    const conf = testHelper.getBasePermissions()\n    conf.record.a$$x = {}\n    expect(configValidator.validate(conf)).to.equal('invalid variable name $$ for path a$$x in section record')\n  })\n\n  it('fails for invalid rule types', () => {\n    const conf = testHelper.getBasePermissions()\n    conf.rpc.somepath = { write: 'a === b' }\n    expect(configValidator.validate(conf)).to.equal('unknown rule type write in section rpc')\n  })\n\n  it('fails for invalid rules', () => {\n    const conf = testHelper.getBasePermissions()\n    conf.record.somepath = { write: 'process.exit()' }\n    expect(configValidator.validate(conf)).to.equal('function exit is not supported')\n  })\n\n  // it( 'fails for rules referencing data that dont support it', function(){\n  //  var conf = testHelper.getBasePermissions();\n  //  conf.record.somepath = { read: 'data.firstname === \"Egon\"' };\n  //  expect( configValidator.validate( conf ) ).to.equal(\n  //    'data is not supported for record read - did you mean \"oldData\"?' \\\n  //  );\n  // });\n})\n"
  },
  {
    "path": "src/services/permission/valve/config-validator.ts",
    "content": "import { SCHEMA } from './config-schema'\nimport * as pathParser from './path-parser'\nimport * as ruleParser from './rule-parser'\nimport { Dictionary } from 'ts-essentials'\nimport { DeepstreamConfig } from '@deepstream/types'\n\nconst validationSteps: Dictionary<(config: DeepstreamConfig) => boolean | string> = {}\n\n/**\n * Validates a configuration object. This method runs through multiple\n * individual validation steps. If any of them returns false,\n * the validation fails\n */\nexport const validate = function (config: any) {\n  let validationStepResult\n  let key\n\n  for (key in validationSteps) {\n    validationStepResult = validationSteps[key](config)\n\n    if (validationStepResult !== true) {\n      return validationStepResult\n    }\n  }\n\n  return true\n}\n\n/**\n * Checks if the configuration is an object\n */\nvalidationSteps.isValidType = function (config: any) {\n  if (typeof config === 'object') {\n    return true\n  }\n\n  return `config should be an object literal, but was of type ${typeof config}`\n}\n\n/**\n * Makes sure all sections (record, event, rpc) are present\n */\nvalidationSteps.hasRequiredTopLevelKeys = function (config: any) {\n  for (const key in SCHEMA) {\n    if (typeof config[key] !== 'object') {\n      return `missing configuration section \"${key}\"`\n    }\n  }\n\n  return true\n}\n\n/**\n * Makes sure no unsupported sections were added\n */\nvalidationSteps.doesNotHaveAdditionalTopLevelKeys = function (config: any) {\n  for (const key in config) {\n    if (typeof SCHEMA[key] === 'undefined') {\n      return `unexpected configuration section \"${key}\"`\n    }\n  }\n\n  return true\n}\n\n/**\n * Checks if the configuration contains valid path definitions\n */\nvalidationSteps.doesOnlyContainValidPaths = function (config: any) {\n  let key\n  let path\n  let result\n\n  for (key in SCHEMA) {\n    // Check empty\n    if (Object.keys(config[key]).length === 0) {\n      return `empty section \"${key}\"`\n    }\n\n    // Check valid\n    for (path in config[key]) {\n      result = pathParser.validate(path)\n      if (result !== true) {\n        return `${result} for path ${path} in section ${key}`\n      }\n    }\n  }\n\n  return true\n}\n\n/**\n * Each section must specify a generic permission (\"*\") that\n * will be applied if no other permission is applicable\n */\nvalidationSteps.doesHaveRootEntries = function (config: any) {\n  let sectionName\n\n  for (sectionName in SCHEMA) {\n    if (!config[sectionName]['*']) {\n      return `missing root entry \"*\" for section ${sectionName}`\n    }\n  }\n\n  return true\n}\n\n/**\n * Runs the rule validator against every rule in each section\n */\nvalidationSteps.hasValidRules = function (config: any) {\n  let path\n  let ruleType\n  let section\n  let validationResult\n\n  for (section in config) {\n    for (path in config[section]) {\n      for (ruleType in config[section][path]) {\n        if (SCHEMA[section][ruleType] !== true) {\n          return `unknown rule type ${ruleType} in section ${section}`\n        }\n\n        validationResult = ruleParser.validate(config[section][path][ruleType], section, ruleType)\n        if (validationResult !== true) {\n          return validationResult\n        }\n      }\n    }\n  }\n\n  return true\n}\n"
  },
  {
    "path": "src/services/permission/valve/path-parser.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport * as pathParser from './path-parser'\n\nconst isRegExp = function (val) {\n  return typeof val === 'object' && typeof val.test === 'function'\n}\n\ndescribe('validates paths in permission.json files', () => {\n  it('exposes a validate method', () => {\n    expect(typeof pathParser.validate).to.equal('function')\n  })\n\n  it('accepts a valid path', () => {\n    expect(pathParser.validate('game-comment/$gameId/*')).to.equal(true)\n  })\n\n  it('rejects none strings', () => {\n    expect(pathParser.validate(3 as any)).to.equal('path must be a string')\n  })\n\n  it('rejects empty strings', () => {\n    expect(pathParser.validate('')).to.equal('path can\\'t be empty')\n  })\n\n  it('rejects paths starting with /', () => {\n    expect(pathParser.validate('/bla')).to.equal('path can\\'t start with /')\n  })\n\n  it('rejects paths with invalid variable names', () => {\n    expect(pathParser.validate('bla/$-')).to.equal('invalid variable name $-')\n    expect(pathParser.validate('bla/$$aa')).to.equal('invalid variable name $$')\n  })\n})\n\ndescribe('parses valid paths in permission.json files', () => {\n  it('exposes a parse method', () => {\n    expect(typeof pathParser.parse).to.equal('function')\n  })\n\n  it('parses a simple, valid path', () => {\n    const result = pathParser.parse('i-am-valid')\n    expect(isRegExp(result.regexp)).to.equal(true)\n    expect(result.regexp.toString()).to.equal('/^i-am-valid$/')\n    expect(result.variables.length).to.equal(0)\n  })\n\n  it('parses a valid path with a wildcard', () => {\n    const result = pathParser.parse('i-am-valid/*')\n    expect(isRegExp(result.regexp)).to.equal(true)\n    expect(result.regexp.toString()).to.equal('/^i-am-valid\\\\/.*$/')\n    expect(result.variables.length).to.equal(0)\n  })\n\n  it('parses a valid path with a variable', () => {\n    const result = pathParser.parse('game-score/$gameId')\n    expect(isRegExp(result.regexp)).to.equal(true)\n    expect(result.regexp.toString()).to.equal('/^game-score\\\\/([^\\/]+)$/')\n    expect(result.variables).to.deep.equal(['$gameId'])\n  })\n\n  it('parses a valid path with multiple variables', () => {\n    const result = pathParser.parse('game-comment/$gameId/$userId/$commentId')\n    expect(isRegExp(result.regexp)).to.equal(true)\n    expect(result.regexp.toString()).to.equal('/^game-comment\\\\/([^/]+)\\\\/([^/]+)\\\\/([^/]+)$/')\n    expect(result.variables).to.deep.equal(['$gameId', '$userId', '$commentId'])\n  })\n\n  it('parses a path with a mix of variables and wildcards', () => {\n    const result = pathParser.parse('$recordName/*')\n    expect(isRegExp(result.regexp)).to.equal(true)\n    expect(result.regexp.toString()).to.equal('/^([^/]+)\\\\/.*$/')\n    expect(result.variables).to.deep.equal(['$recordName'])\n  })\n})\n\ndescribe('applies regexp to paths', () => {\n  it('applies a regexp to a simple path', () => {\n    const path = 'public/*'\n    const result = pathParser.parse(path)\n    expect(result.regexp.test('public/details/info')).to.equal(true)\n    expect(result.regexp.test('private/details/info')).to.equal(false)\n  })\n\n  it('applies a regexp and extracts a variable from a simple path', () => {\n    const path = 'private/$userId'\n    const name = 'private/userA'\n    const result = pathParser.parse(path)\n    expect(result.regexp.test(name)).to.equal(true)\n\n    const r = name.match(result.regexp) || false\n    expect(r[1]).to.equal('userA')\n  })\n\n  it('applies a regexp and extracts variables from a more complex path', () => {\n    const path = 'private/$userId/*/$anotherId'\n    const name = 'private/userA/blabla/14'\n    const result = pathParser.parse(path)\n    expect(result.regexp.test(name)).to.equal(true)\n\n    const r = name.match(result.regexp) || []\n    expect(r.join(',')).to.deep.equal('private/userA/blabla/14,userA,14')\n\n    const reject = name.match(result.regexp) || false\n    expect(reject[1]).to.equal('userA')\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/path-parser.ts",
    "content": "const WILDCARD_REGEXP = /\\*/g\nconst WILDCARD_STRING = '.*'\nconst VARIABLE_REGEXP = /(\\$[a-zA-Z0-9]+)/g\nconst VARIABLE_STRING = '([^/]+)'\nconst INVALID_VARIABLE_REGEXP = /(\\$[^a-zA-Z0-9])/\n\n/**\n * Checks a path for type and basic syntax errors\n *\n * @param   {String} path The path as specified in permission.json\n *\n * @public\n * @returns {String|Boolean} true if path is valid, string error message if not\n */\nexport const validate = (path: string): string | boolean => {\n  if (typeof path !== 'string') {\n    return 'path must be a string'\n  }\n\n  if (path.length === 0) {\n    return 'path can\\'t be empty'\n  }\n\n  if (path[0] === '/') {\n    return 'path can\\'t start with /'\n  }\n\n  const invalidVariableNames = path.match(INVALID_VARIABLE_REGEXP)\n\n  if (invalidVariableNames !== null) {\n    return `invalid variable name ${invalidVariableNames[0]}`\n  }\n\n  return true\n}\n\n/**\n * Parses a path and returns a regexp matcher with capture groups for\n * variable names and a list of variable names in the same order.\n * The path is assumed to be valid when its passed to this method\nt */\nexport const parse = (path: string): any => {\n  const variables: string[] = []\n  let regExp = path.replace(WILDCARD_REGEXP, WILDCARD_STRING)\n\n  regExp = regExp.replace(VARIABLE_REGEXP, (variableName) => {\n    variables.push(variableName)\n    return VARIABLE_STRING\n  })\n\n  return {\n    variables,\n    path,\n    regexp: new RegExp(`^${regExp}$`),\n  }\n}\n"
  },
  {
    "path": "src/services/permission/valve/rule-application.ts",
    "content": "import { EOL } from 'os'\nimport { Message, RECORD_ACTION, PRESENCE_ACTION, EVENT_ACTION, RPC_ACTION, RecordData, TOPIC, RecordWriteMessage } from '../../../constants'\nimport { PermissionCallback, ValveConfig, SocketWrapper, DeepstreamConfig, DeepstreamServices, NamespacedLogger, EVENT } from '@deepstream/types'\nimport { recordRequest } from '../../../handlers/record/record-request'\nimport RecordHandler from '../../../handlers/record/record-handler'\nimport { setValue } from '../../../utils/json-path'\n\nconst OPEN = 'open'\nconst LOADING = 'loading'\nconst ERROR = 'error'\n\nconst UNDEFINED = 'undefined'\nconst STRING = 'string'\n\ninterface RuleApplicationParams {\n   userId: string\n   serverData: any\n   path: string\n   ruleSpecification: any\n   message: Message\n   action: RECORD_ACTION | PRESENCE_ACTION | EVENT_ACTION | RPC_ACTION\n   regexp: RegExp\n   rule: any\n   name: string\n   permissionOptions: ValveConfig\n   logger: NamespacedLogger\n   recordHandler: RecordHandler\n   socketWrapper: SocketWrapper\n   config: DeepstreamConfig\n   services: DeepstreamServices,\n   callback: PermissionCallback,\n   passItOn: any,\n}\n\nexport default class RuleApplication {\n  private isDestroyed: boolean = false\n  private runScheduled: boolean = false\n  private pathVars: any\n  private user: any\n\n  private readonly maxIterationCount: number\n  private readonly recordsData = new Map<string, RecordData | string>()\n\n  private iterations: number\n\n  /**\n   * This class handles the evaluation of a single rule. It creates\n   * the required variables, injects them into the rule function and\n   * runs the function recoursively until either all cross-references,\n   * references to old or new data is loaded, it errors or the maxIterationCount\n   * limit is exceeded\n   */\n  constructor (private params: RuleApplicationParams) {\n    this.maxIterationCount = this.params.permissionOptions.maxRuleIterations\n\n    this.run = this.run.bind(this)\n    this.crossReference = this.crossReference.bind(this)\n    this.createNewRecordRequest = this.createNewRecordRequest.bind(this)\n\n    this.pathVars = this.getPathVars()\n    this.user = this.getUser()\n    this.iterations = 0\n\n    this.run()\n  }\n\n  /**\n   * Runs the rule function. This method is initially called when this class\n   * is constructed and recoursively from thereon whenever the loading of a record\n   * is completed\n   */\n  private run (): void {\n    this.runScheduled = false\n    this.iterations++\n\n    if (this.isDestroyed) {\n      return\n    }\n\n    if (this.iterations > this.maxIterationCount) {\n      this.onRuleError('Exceeded max iteration count')\n      return\n    }\n\n    if (this.isDestroyed) {\n      return\n    }\n\n    const args = this.getArguments()\n    let result\n\n    try {\n      result = this.params.rule.fn.apply({}, args)\n    } catch (error) {\n      if (this.isReady()) {\n        this.onRuleError(`${error}`)\n        return\n      }\n    }\n\n    if (this.isReady()) {\n      this.params.callback(this.params.socketWrapper, this.params.message, this.params.passItOn, null, result)\n      this.destroy()\n    }\n  }\n\n  /**\n   * Callback if a rule has irrecoverably errored. Rule errors due to unresolved\n   * crossreferences are allowed as long as a loading step is in progress\n   */\n  private onRuleError (error: string): void {\n    if (this.isDestroyed === true) {\n      return\n    }\n    const errorMsg = `error when executing ${this.params.rule.fn.toString()}${EOL}for ${this.params.name}: ${error.toString()}`\n\n    this.params.logger.warn(EVENT_ACTION[EVENT_ACTION.MESSAGE_PERMISSION_ERROR], errorMsg, { recordName: this.params.name })\n    this.params.callback(this.params.socketWrapper, this.params.message, this.params.passItOn, EVENT_ACTION.MESSAGE_PERMISSION_ERROR, false)\n    this.destroy()\n  }\n\n  /**\n   * Called either asynchronously when data is successfully retrieved from the\n   * cache or synchronously if its already present\n   */\n  private onLoadComplete (recordName: string, version: number, data: any): void {\n    this.recordsData.set(recordName, data)\n\n    if (this.isReady()) {\n      this.runScheduled = true\n      process.nextTick(this.run)\n    }\n  }\n\n  /**\n   * Called whenever a storage or cache retrieval fails. Any kind of error during the\n   * permission process is treated as a denied permission\n   */\n  private onLoadError (event: any, errorMessage: string, recordName: string, socket: SocketWrapper | null) {\n    this.recordsData.set(recordName, ERROR)\n    const errorMsg = `failed to load record ${this.params.name} for permissioning:${errorMessage}`\n    this.params.logger.error(RECORD_ACTION[RECORD_ACTION.RECORD_LOAD_ERROR], errorMsg, { recordName, socketWrapper: socket })\n    this.params.callback(this.params.socketWrapper, this.params.message, this.params.passItOn, RECORD_ACTION.RECORD_LOAD_ERROR, false)\n    this.destroy()\n  }\n\n  /**\n   * Destroys this class and nulls down values to avoid\n   * memory leaks\n   */\n  private destroy () {\n    this.params.recordHandler.removeRecordRequest(this.params.name)\n    this.isDestroyed = true\n    this.runScheduled = false\n    this.recordsData.clear()\n    // this.params = null\n    // this.crossReference = null\n    // this.currentData = null\n    this.pathVars = null\n    this.user = null\n  }\n\n  /**\n   * If data.someValue is used in the rule, this method retrieves or loads the\n   * current data. This can mean different things, depending on the type of message\n   *\n   * the data arguments is supported for record read & write,\n   * event publish and rpc request\n   *\n   * for event publish, record update and rpc request, the data is already provided\n   * in the message and doesn't need to be loaded\n   *\n   * for record.patch, only a delta is part of the message. For the full data, the current value\n   * is loaded and the patch applied on top\n   */\n  private getCurrentData (): any {\n    if (this.params.rule.hasData === false) {\n      return null\n    }\n\n    const msg = this.params.message\n    let result: any = false\n\n    if (\n      (msg.topic === TOPIC.RPC) ||\n      (msg.topic === TOPIC.EVENT && msg.data) ||\n      (msg.topic === TOPIC.RECORD && msg.action === RECORD_ACTION.UPDATE)\n    ) {\n      result = this.params.socketWrapper.parseData(msg)\n      if (result instanceof Error) {\n        this.onRuleError(`error when converting message data ${result.toString()}`)\n      } else {\n        return msg.parsedData\n      }\n    } else if (msg.topic === TOPIC.RECORD && msg.action === RECORD_ACTION.PATCH) {\n      result = this.getRecordPatchData(msg as RecordWriteMessage)\n      if (result instanceof Error) {\n        this.onRuleError(`error when converting message data ${result.toString()}`)\n      } else {\n        return result\n      }\n    }\n\n  }\n\n  /**\n   * Loads the records current data and applies the patch data onto it\n   * to avoid users having to distuinguish between patches and updates\n   */\n  private getRecordPatchData (msg: RecordWriteMessage): any {\n    if (!this.recordsData) {\n      return\n    }\n\n    if (!msg.path) {\n      this.params.logger.error(EVENT.ERROR, `Missing path for record patch ${msg.name}`, { message: msg })\n      return\n    }\n\n    const currentData = this.recordsData.get(this.params.name)\n    const parseResult = this.params.socketWrapper.parseData(msg)\n    let data\n\n    if (parseResult instanceof Error) {\n      return parseResult\n    }\n\n    if (currentData === null) {\n      return new Error(`Tried to apply patch to non-existant record ${msg.name}`)\n    }\n\n    if (typeof currentData !== UNDEFINED && currentData !== LOADING) {\n      data = JSON.parse(JSON.stringify(currentData))\n      setValue(data, msg.path, msg.parsedData)\n      return data\n    }\n    this.loadRecord(this.params.name)\n  }\n\n  /**\n   * Returns or loads the record's previous value. Only supported for record\n   * write and read operations\n   *\n   * If getData encounters an error, the rule application might already be destroyed\n   * at this point\n   */\n  private getOldData (): any {\n    if (this.isDestroyed === true || this.params.rule.hasOldData === false) {\n      return\n    }\n    if (this.recordsData.has(this.params.name)) {\n      return this.recordsData.get(this.params.name)\n    }\n    this.loadRecord(this.params.name)\n  }\n\n  /**\n   * Compile the list of arguments that will be injected\n   * into the permission function. This method is called\n   * everytime the permission is run. This allows it to merge\n   * patches and update the now timestamp\n   */\n  private getArguments (): any[] {\n    return [\n      this.crossReference,\n      this.user,\n      this.getCurrentData(),\n      this.getOldData(),\n      Date.now(),\n      this.params ? this.params.action : null,\n      this.params ? this.params.name : null\n    ].concat(this.pathVars)\n  }\n\n  /**\n   * Returns the data for the user variable. This is only done once\n   * per rule as the user is not expected to change\n   */\n  private getUser (): any {\n    return {\n      isAuthenticated: this.params.userId !== OPEN,\n      id: this.params.userId,\n      data: this.params.serverData,\n    }\n  }\n\n  /**\n   * Applies the compiled regexp for the path and extracts\n   * the variables that will be made available as $variableName\n   * within the rule\n   *\n   * This is only done once per rule as the path is not expected\n   * to change\n   */\n  private getPathVars (): string[] {\n    if (!this.params.name) {\n      return []\n    }\n    const matches = this.params.name.match(this.params.regexp)\n    if (matches) {\n      return matches.slice(1)\n    } else {\n      return []\n    }\n  }\n\n  /**\n   * Returns true if all loading operations that are in progress have finished\n   * and no run has been scheduled yet\n   */\n  private isReady (): boolean {\n    let isLoading = false\n\n    // @ts-ignore\n    for (const [key, value] of this.recordsData) {\n      if (value === LOADING) {\n        isLoading = true\n        break\n      }\n    }\n\n    return isLoading === false && this.runScheduled === false\n  }\n\n  /**\n   * Loads a record with a given name. This will either result in\n   * a onLoadComplete or onLoadError call. This method should only\n   * be called if the record is not already being loaded or present,\n   * but I'll leave the additional safeguards in until absolutely sure.\n   */\n  private loadRecord (recordName: string): void {\n    const recordData = this.recordsData.get(recordName)\n\n    if (recordData === LOADING) {\n      return\n    }\n\n    if (typeof recordData !== UNDEFINED) {\n      this.onLoadComplete(recordName, -1, recordData)\n      return\n    }\n\n    this.recordsData.set(recordName, LOADING)\n\n    this.params.recordHandler.runWhenRecordStable(\n      recordName,\n      this.createNewRecordRequest,\n    )\n  }\n\n  /**\n   * Load the record data from the cache for permissioning. This method should be\n   * called once the record is stable – meaning there are no remaining writes\n   * waiting to be written to the cache.\n   */\n  private createNewRecordRequest (recordName: string): void {\n    recordRequest(\n      recordName,\n      this.params.config,\n      this.params.services,\n      null,\n      this.onLoadComplete,\n      this.onLoadError,\n      this\n    )\n  }\n\n  /**\n   * This method is passed to the rule function as _ to allow crossReferencing\n   * of other records. Cross-references can be nested, leading to this method\n   * being recoursively called until the either all cross references are loaded\n   * or the rule has finally failed\n   */\n  private crossReference (recordName: string): any | null {\n    const type = typeof recordName\n    const recordData = this.recordsData.get(recordName)\n\n    if (type !== UNDEFINED && type !== STRING) {\n      this.onRuleError(`crossreference got unsupported type ${type}`)\n    } else if (type === UNDEFINED || recordName.indexOf(UNDEFINED) !== -1) {\n      return\n    } else if (recordData === LOADING) {\n      return\n    } else if (recordData === null) {\n      return null\n    } else if (typeof recordData === UNDEFINED) {\n      this.loadRecord(recordName)\n    } else {\n      return recordData\n    }\n  }\n}\n"
  },
  {
    "path": "src/services/permission/valve/rule-cache.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nconst RuleCache = require('./rule-cache').default\n\ndescribe('loads and retrieves values from the rule cache', () => {\n  let ruleCache\n\n  it('creates the rule cache', () => {\n    ruleCache = new RuleCache({ cacheEvacuationInterval: 10 })\n    expect(ruleCache.has('record', '*', 'write')).to.equal(false)\n  })\n\n  it('sets a value', () => {\n    ruleCache.set('event', '*', 'write', 'ah')\n    expect(ruleCache.has('event', '*', 'write')).to.equal(true)\n    expect(ruleCache.get('event', '*', 'write')).to.equal('ah')\n  })\n\n  it('sets another value', (next) => {\n    ruleCache.set('record', '*', 'write', 'yup')\n    expect(ruleCache.has('record', '*', 'write')).to.equal(true)\n    expect(ruleCache.get('record', '*', 'write')).to.equal('yup')\n    setTimeout(next, 40)\n  })\n\n  it('sets two values for different actions', () => {\n    ruleCache.set('record', 'somepath', 'write', true)\n    ruleCache.set('record', 'somepath', 'read', 'bla')\n\n    expect(ruleCache.has('record', 'somepath', 'write')).to.equal(true)\n    expect(ruleCache.get('record', 'somepath', 'write')).to.equal(true)\n\n    expect(ruleCache.has('record', 'somepath', 'read')).to.equal(true)\n    expect(ruleCache.get('record', 'somepath', 'read')).to.equal('bla')\n  })\n\n  it('has purged the cache in the meantime', () => {\n    expect(ruleCache.has('record', '*', 'write')).to.equal(false)\n  })\n\n  it('does not remove an entry thats repeatedly requested', (next) => {\n    ruleCache.set('record', '*', 'write', 'yeah')\n    let count = 0\n    const interval = setInterval(() => {\n      count++\n      expect(ruleCache.has('record', '*', 'write')).to.equal(true)\n      expect(ruleCache.get('record', '*', 'write')).to.equal('yeah')\n      if (count >= 10) {\n        clearInterval(interval)\n        next()\n      }\n    }, 10)\n  })\n\n  it('removes the entry once it stops being requested', (next) => {\n    expect(ruleCache.has('record', '*', 'write')).to.equal(true)\n    expect(ruleCache.get('record', '*', 'write')).to.equal('yeah')\n    setTimeout(() => {\n      expect(ruleCache.has('record', '*', 'write')).to.equal(false)\n      next()\n    }, 40)\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/rule-cache.ts",
    "content": "import { ValveConfig } from '@deepstream/types'\n\ninterface CachedRule {\n  rule: string,\n  isUsed: boolean,\n}\n\nexport default class RuleCache {\n  private data = new Map<string, CachedRule>()\n  private purgeInterval: NodeJS.Timer\n\n  /**\n   * This cache stores rules that are frequently used. It removes\n   * unused rules after a preset interval\n   */\n  constructor (config: ValveConfig) {\n    this.purgeInterval = setInterval(this.purge.bind(this), config.cacheEvacuationInterval)\n  }\n\n  public close () {\n    clearInterval(this.purgeInterval)\n  }\n\n  /**\n   * Empties the rulecache completely\n   */\n  public reset (): void {\n    this.data.clear()\n  }\n\n  /**\n   * Checks if an entry for a specific rule in a specific section is\n   * present\n   */\n  public has (section: string, name: string, type: string): boolean {\n    return this.data.has(toKey(section, name, type))\n  }\n\n  /**\n   * Resets the usage flag and returns an entry from the cache\n   */\n  public get (section: string, name: string, type: string): string | undefined {\n    const cache = this.data.get(toKey(section, name, type))\n    if (cache) {\n      cache.isUsed = true\n      return cache.rule\n    }\n    return undefined\n  }\n\n  /**\n   * Adds an entry to the cache\n   */\n  public set (section: string, name: string, type: string, rule: string): void {\n    this.data.set(toKey(section, name, type), {\n      rule,\n      isUsed: true,\n    })\n  }\n\n  /**\n   * This method is called repeatedly on an interval, defined by\n   * cacheEvacuationInterval.\n   *\n   * If a rule in the cache has been used in the last interval, it sets its isUsed flag to false.\n   * Whenever the rule is used, the isUsed flag will be set to true\n   * Any rules that haven't been used in the next cycle will be removed from the cache\n   */\n  private purge () {\n    for (const [key, cache] of this.data) {\n      if (cache.isUsed === true) {\n        cache.isUsed = false\n      } else {\n        this.data.delete(key)\n      }\n    }\n  }\n}\n\n/**\n * Creates a key from the various set parameters\n */\nfunction toKey (section: string, name: string, type: string): string {\n  return `${section}_${name}_${type}`\n}\n"
  },
  {
    "path": "src/services/permission/valve/rule-parser.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nconst ruleParser = require('./rule-parser')\n\ndescribe('validates rule strings from permissions.json', () => {\n  it('exposes a validate method', () => {\n    expect(typeof ruleParser.validate).to.equal('function')\n  })\n\n  it('accepts valid rules', () => {\n    expect(ruleParser.validate('user.id === $userId')).to.equal(true)\n  })\n\n  it('rejects non-strings', () => {\n    expect(ruleParser.validate(3)).to.equal('rule must be a string')\n  })\n\n  it('rejects empty strings', () => {\n    expect(ruleParser.validate('')).to.equal('rule can\\'t be empty')\n  })\n\n  it('rejects rules that contain new as a keyword', () => {\n    expect(ruleParser.validate('a new SomeClass')).to.equal('rule can\\'t contain the new keyword')\n    expect(ruleParser.validate('a=new SomeClass')).to.equal('rule can\\'t contain the new keyword')\n    expect(ruleParser.validate('new SomeClass')).to.equal('rule can\\'t contain the new keyword')\n    expect(ruleParser.validate(' new SomeClass')).to.equal('rule can\\'t contain the new keyword')\n    expect(ruleParser.validate('16-new Number(3)')).to.equal('rule can\\'t contain the new keyword')\n    expect(ruleParser.validate('~new SomeClass')).to.equal('rule can\\'t contain the new keyword')\n  })\n\n  it('accepts rules that contain new as part of another string or object name', () => {\n    expect(ruleParser.validate('newData.firstname')).to.equal(true)\n    expect(ruleParser.validate('$new = \"foo\"')).to.equal(true) // TODO also unicode in identifiers\n    // expect(ruleParser.validate('a == \"new\"')).to.equal(true) // TODO\n  })\n\n  it('rejects rules that define user functions', () => {\n    expect(ruleParser.validate('(function (foo) { return foo + 1; })(20)'))\n      .to.equal('rule can\\'t contain user functions')\n    expect(ruleParser.validate('(foo => foo + 1)(20)')).to.equal('rule can\\'t contain user functions')\n  })\n\n  it('rejects rules that call unsupported functions', () => {\n    expect(ruleParser.validate('data.lastname.toUpperCase()', 'record', 'write')).to.equal(true)\n    expect(ruleParser.validate('alert(\"bobo\")')).to.equal('function alert is not supported')\n    expect(ruleParser.validate('alert  (\"whoops\") && console.log(\"nope\")'))\n      .to.equal('function alert is not supported')\n    expect(ruleParser.validate('alert\\t(\"whoops\")')).to.equal('function alert is not supported')\n    expect(ruleParser.validate('alert\\n(\"whoops\")')).to.equal('function alert is not supported')\n    expect(ruleParser.validate('console[\"log\"](\"whoops\")'))\n      .to.equal('function log\"] is not supported')\n    expect(ruleParser.validate('global[\"con\"+\"sole\"][\"lo\" + `g`] (\"whoops\")'))\n      .to.equal('function g`] is not supported')\n    expect(ruleParser.validate('data.lastname.toUpperCase() && data.lastname.substr(0,3)', 'record', 'write')).to.equal('function substr is not supported')\n  })\n\n  it('rejects invalid cross references', () => {\n    expect(ruleParser.validate('_(\"another-record\" + data.userId) === $userId', 'record', 'write')).to.equal(true)\n  })\n\n  it('rejects rules that are syntactically invalid', () => {\n    expect(ruleParser.validate('a b')).to.match(/^SyntaxError: Unexpected identifier/)\n    expect(ruleParser.validate('user.id.toUpperCase(')).to.equal(\"SyntaxError: Unexpected token '}'\")\n  })\n\n  it('rejects rules that reference old data without it being supported', () => {\n    expect(ruleParser.validate('data.price === 500 && oldData.price < 500', 'event', 'publish')).to.equal('rule publish for event does not support oldData')\n  })\n\n  it('rejects rules that reference data without it being supported', () => {\n    expect(ruleParser.validate('user.id === $userId && data.price === 500', 'rpc', 'provide')).to.equal('rule provide for rpc does not support data')\n  })\n\n  it('validates a rule referencing data as a property for a type (read) where the injected (root) data is not available', () => {\n    const validatedRule = ruleParser.validate('user.id !== user.data.someUser', 'record', 'read')\n    expect(typeof validatedRule).to.equal('boolean')\n    expect(validatedRule).to.equal(true)\n  })\n})\n\ndescribe('compiles rules into usable objects', () => {\n  it('compiles boolean false', () => {\n    const compiledRule = ruleParser.parse(false, [])\n    expect(compiledRule.fn()).to.equal(false)\n    expect(typeof compiledRule.fn).to.equal('function')\n    expect(compiledRule.hasOldData).to.equal(false)\n    expect(compiledRule.hasData).to.equal(false)\n  })\n\n  it('compiles boolean true', () => {\n    const compiledRule = ruleParser.parse(true, [])\n    expect(compiledRule.fn()).to.equal(true)\n    expect(typeof compiledRule.fn).to.equal('function')\n    expect(compiledRule.hasOldData).to.equal(false)\n    expect(compiledRule.hasData).to.equal(false)\n  })\n\n  it('creates executable functions', () => {\n    expect(ruleParser.parse('\"bobo\"', []).fn()).to.equal('bobo')\n    expect(ruleParser.parse('2+2', []).fn()).to.equal(4)\n  })\n\n  it('compiles a simple rule', () => {\n    const compiledRule = ruleParser.parse('user.id !== \"open\"', [])\n    expect(typeof compiledRule.fn).to.equal('function')\n    expect(compiledRule.hasOldData).to.equal(false)\n    expect(compiledRule.hasData).to.equal(false)\n  })\n\n  it('compiles a rule referencing data', () => {\n    const compiledRule = ruleParser.parse('user.id !== data.someUser', [])\n    expect(typeof compiledRule.fn).to.equal('function')\n    expect(compiledRule.hasOldData).to.equal(false)\n    expect(compiledRule.hasData).to.equal(true)\n  })\n\n  it('compiles a rule referencing data followed by a space', () => {\n    const compiledRule = ruleParser.parse('data .firstname === \"Yasser\"', [])\n    expect(typeof compiledRule.fn).to.equal('function')\n    expect(compiledRule.hasOldData).to.equal(false)\n    expect(compiledRule.hasData).to.equal(true)\n  })\n\n  it('compiles a rule referencing oldData', () => {\n    const compiledRule = ruleParser.parse('user.id !== oldData.someUser', [])\n    expect(typeof compiledRule.fn).to.equal('function')\n    expect(compiledRule.hasOldData).to.equal(true)\n    expect(compiledRule.hasData).to.equal(false)\n  })\n\n  it('compiles a rule referencing both data and oldData', () => {\n    const compiledRule = ruleParser.parse('user.id !== data.someUser && oldData.price <= data.price', [])\n    expect(typeof compiledRule.fn).to.equal('function')\n    expect(compiledRule.hasOldData).to.equal(true)\n    expect(compiledRule.hasData).to.equal(true)\n  })\n\n  it('compiles a rule referencing both data and oldData as well as other records', () => {\n    const compiledRule = ruleParser.parse('_( \"private/\"+ user.id ) !== data.someUser && oldData.price <= data.price', [])\n    expect(typeof compiledRule.fn).to.equal('function')\n    expect(compiledRule.hasOldData).to.equal(true)\n    expect(compiledRule.hasData).to.equal(true)\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/rule-parser.ts",
    "content": "import * as rulesMap from './rules-map'\nimport { ValveSection, RuleType } from './config-permission'\n\n// TODO: any of these are fine inside a string or comment context...\nconst FUNCTION_REGEXP = /([\\w]+(?:['\"`]\\])?)\\s*\\(/g\nconst USER_FUNCTION_REGEXP = /[^\\w$]function[^\\w$]|=>/g\nconst NEW_REGEXP = /(^|[^\\w$])new[^\\w$]/\nconst OLD_DATA_REGEXP = /(^|[^\\w~])oldData[^\\w~]/\nconst DATA_REGEXP = /(^|[^\\w.~])data($|[^\\w~])/\n\nconst SUPPORTED_FUNCTIONS = [\n  '_',\n  'startsWith',\n  'endsWith',\n  'includes',\n  'indexOf',\n  'match',\n  'toUpperCase',\n  'toLowerCase',\n  'trim',\n]\n\n/**\n * Validates a rule. Makes sure that the rule is either a boolean or a string,\n * that it doesn't contain the new keyword or unsupported function invocations\n * and that it can be compiled into a javascript function\n */\nexport const validate = (rule: string | boolean, section: ValveSection, type: RuleType): boolean | string => {\n  if (typeof rule === 'boolean') {\n    return true\n  }\n\n  if (typeof rule !== 'string') {\n    return 'rule must be a string'\n  }\n\n  if (rule.length === 0) {\n    return 'rule can\\'t be empty'\n  }\n\n  if (rule.match(NEW_REGEXP)) {\n    return 'rule can\\'t contain the new keyword'\n  }\n\n  if (rule.match(USER_FUNCTION_REGEXP)) {\n    return 'rule can\\'t contain user functions'\n  }\n\n  const functions = rule.match(FUNCTION_REGEXP)\n  let functionName\n  let i\n\n  // TODO _ cross references are only supported for section record\n  if (functions) {\n    for (i = 0; i < functions.length; i++) {\n      functionName = functions[i].replace(/\\s*\\($/, '')\n      if (SUPPORTED_FUNCTIONS.indexOf(functionName) === -1) {\n        return `function ${functionName} is not supported`\n      }\n    }\n  }\n\n  try {\n    // tslint:disable-next-line\n    new Function(rule)\n  } catch (e) {\n    return `${e}`\n  }\n\n  if (!!rule.match(OLD_DATA_REGEXP) && !rulesMap.supportsOldData(type)) {\n    return `rule ${type} for ${section} does not support oldData`\n  }\n\n  if (!!rule.match(DATA_REGEXP) && !rulesMap.supportsData(type)) {\n    return `rule ${type} for ${section} does not support data`\n  }\n\n  return true\n}\n\n/**\n * Cross References:\n *\n * Cross references are denoted with an underscore function _()\n * They can take path variables: _($someId)\n * variables from data: _(data.someValue)\n * or strings: _('user/egon')\n */\nexport const parse = (rule: boolean | string, variables: any) => {\n  if (rule === true || rule === false) {\n    return {\n      fn: rule === true ? function () { return true } : function () { return false },\n      hasOldData: false,\n      hasData: false,\n    }\n  }\n  const ruleObj: any = {}\n  const args = ['_', 'user', 'data', 'oldData', 'now', 'action', 'name'].concat(variables)\n  args.push(`return ${rule};`)\n\n  ruleObj.fn = Function.apply(null, args)\n  ruleObj.hasOldData = !!rule.match(OLD_DATA_REGEXP)\n  ruleObj.hasData = !!rule.match(DATA_REGEXP)\n\n  return ruleObj\n}\n"
  },
  {
    "path": "src/services/permission/valve/rules-map.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\n\nimport { getRulesForMessage } from './rules-map'\nimport * as C from '../../../constants'\n\ndescribe('returns the applicable rule for a message', () => {\n  it('exposes a getRulesForMessage method', () => {\n    expect(typeof getRulesForMessage).to.equal('function')\n  })\n\n  it('returns null for topics without rules', () => {\n    const msg = {\n      topic: C.TOPIC.AUTH\n    }\n    expect(getRulesForMessage(msg)).to.equal(null)\n  })\n\n  it('returns null for actions without rules', () => {\n    const msg = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.UNSUBSCRIBE\n    }\n    expect(getRulesForMessage(msg)).to.equal(null)\n  })\n\n  it('returns ruletypes for event subscribe messages', () => {\n    const msg = {\n      topic: C.TOPIC.EVENT,\n      action: C.EVENT_ACTION.SUBSCRIBE\n    }\n    expect(getRulesForMessage(msg)).to.deep.equal({\n      section: 'event',\n      type: 'subscribe',\n      action: C.EVENT_ACTION.SUBSCRIBE\n    })\n  })\n\n  it('returns ruletypes for record patch messages', () => {\n    const msg = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.PATCH\n    }\n    expect(getRulesForMessage(msg)).to.deep.equal({\n      section: 'record',\n      type: 'write',\n      action: C.RECORD_ACTION.PATCH\n    })\n  })\n\n  it('returns ruletypes for record notify messages', () => {\n    const msg = {\n      topic: C.TOPIC.RECORD,\n      action: C.RECORD_ACTION.NOTIFY\n    }\n    expect(getRulesForMessage(msg)).to.deep.equal({\n      section: 'record',\n      type: 'notify',\n      action: C.RECORD_ACTION.NOTIFY\n    })\n  })\n})\n"
  },
  {
    "path": "src/services/permission/valve/rules-map.ts",
    "content": "import { Dictionary } from 'ts-essentials'\nimport { TOPIC, RECORD_ACTION, EVENT_ACTION, RPC_ACTION, PRESENCE_ACTION, Message } from '../../../constants'\n\ninterface RuleType { name: string, data: boolean, oldData: boolean }\n\n/**\n * Different rule types support different features. Generally, all rules can\n * use cross referencing _() to reference records, but only record writes, incoming events\n * or RPC requests carry data and only existing records have a concept of oldData\n */\nconst RULE_TYPES: Dictionary<RuleType> = {\n  CREATE: { name: 'create', data: false, oldData: false },\n  READ: { name: 'read', data: false, oldData: true },\n  WRITE: { name: 'write', data: true, oldData: true },\n  DELETE: { name: 'delete', data: false, oldData: true },\n  LISTEN: { name: 'listen', data: false, oldData: false },\n  NOTIFY: { name: 'notify', data: false, oldData: false },\n  PUBLISH: { name: 'publish', data: true, oldData: false },\n  SUBSCRIBE: { name: 'subscribe', data: true, oldData: false },\n  PROVIDE: { name: 'provide', data: false, oldData: false },\n  REQUEST: { name: 'request', data: true, oldData: false },\n  ALLOW: { name: 'allow', data: false, oldData: false },\n}\n\n/**\n * This class maps topic / action combinations to applicable\n * rules. It combines actions of a similar character (e.g. READ,\n * SNAPSHOT) into high level permissions (e.g. read)\n *\n * Lower level permissioning on a per action basis can still be achieved\n * by virtue of using the action variable within the rule, e.g.\n *\n * {\n *    //allow read, but not listen\n *    'read': 'user.id === $userId && action !== LISTEN'\n * }\n */\nconst RULES_MAP: Dictionary<{ section: string, actions: Dictionary<RuleType> }> = {\n  [TOPIC.RECORD]: {\n    section: 'record',\n    actions: {\n      [RECORD_ACTION.SUBSCRIBE]: RULE_TYPES.READ,\n      [RECORD_ACTION.SUBSCRIBEANDHEAD]: RULE_TYPES.READ,\n      [RECORD_ACTION.SUBSCRIBEANDREAD]: RULE_TYPES.READ,\n      [RECORD_ACTION.READ]: RULE_TYPES.READ,\n      [RECORD_ACTION.HEAD]: RULE_TYPES.READ,\n      [RECORD_ACTION.LISTEN]: RULE_TYPES.LISTEN,\n      [RECORD_ACTION.CREATE]: RULE_TYPES.CREATE,\n      [RECORD_ACTION.UPDATE]: RULE_TYPES.WRITE,\n      [RECORD_ACTION.PATCH]: RULE_TYPES.WRITE,\n      [RECORD_ACTION.NOTIFY]: RULE_TYPES.NOTIFY,\n      [RECORD_ACTION.DELETE]: RULE_TYPES.DELETE,\n      [RECORD_ACTION.ERASE]: RULE_TYPES.DELETE\n    },\n  },\n  [TOPIC.EVENT]: {\n    section: 'event',\n    actions: {\n      [EVENT_ACTION.LISTEN]: RULE_TYPES.LISTEN,\n      [EVENT_ACTION.SUBSCRIBE]: RULE_TYPES.SUBSCRIBE,\n      [EVENT_ACTION.EMIT]: RULE_TYPES.PUBLISH,\n    },\n  },\n  [TOPIC.RPC]: {\n    section: 'rpc',\n    actions: {\n      [RPC_ACTION.PROVIDE]: RULE_TYPES.PROVIDE,\n      [RPC_ACTION.REQUEST]: RULE_TYPES.REQUEST,\n    },\n  },\n  [TOPIC.PRESENCE]: {\n    section: 'presence',\n    actions: {\n      [PRESENCE_ACTION.SUBSCRIBE]: RULE_TYPES.ALLOW,\n      [PRESENCE_ACTION.SUBSCRIBE_ALL]: RULE_TYPES.ALLOW,\n      [PRESENCE_ACTION.QUERY]: RULE_TYPES.ALLOW,\n      [PRESENCE_ACTION.QUERY_ALL]: RULE_TYPES.ALLOW,\n    },\n  },\n}\n\n/**\n * Returns a map of applicable rule-types for a topic\n * action combination\n */\nexport const getRulesForMessage = (message: Message) => {\n  if (RULES_MAP[message.topic] === undefined) {\n    return null\n  }\n\n  if (RULES_MAP[message.topic].actions[message.action] === undefined) {\n    return null\n  }\n\n  return {\n    section: RULES_MAP[message.topic].section,\n    type: RULES_MAP[message.topic].actions[message.action].name,\n    action: message.action,\n  }\n}\n\n/**\n * Returns true if a given rule supports references to incoming data\n */\nexport const supportsData = function (type: string): boolean {\n  return RULE_TYPES[type.toUpperCase()].data\n}\n\n/**\n * Returns true if a given rule supports references to existing data\n */\nexport const supportsOldData = function (type: string): boolean {\n  return RULE_TYPES[type.toUpperCase()].oldData\n}\n"
  },
  {
    "path": "src/services/storage/noop-storage.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport {spy} from 'sinon'\nimport { NoopStorage } from './noop-storage'\n\ndescribe('retuns null for all values', () => {\n  let noopStorage\n\n  before(() => {\n    noopStorage = new NoopStorage()\n  })\n\n  it('has created the Noop Storage', async () => {\n    await noopStorage.whenReady()\n  })\n\n  it('tries to retrieve a non-existing value', (done) => {\n    const successCallback = spy()\n    noopStorage.get('firstname', successCallback)\n    setTimeout(() => {\n      expect(successCallback).to.have.callCount(1)\n      expect(successCallback).to.have.been.calledWith(null, -1, null)\n      done()\n    }, 1)\n  })\n\n  it('tries to delete a value', (done) => {\n    const successCallback = spy()\n    noopStorage.delete('firstname', successCallback)\n    setTimeout(() => {\n      expect(successCallback).to.have.callCount(1)\n      expect(successCallback).to.have.been.calledWith(null)\n      done()\n    }, 1)\n  })\n})\n"
  },
  {
    "path": "src/services/storage/noop-storage.ts",
    "content": "import { DeepstreamPlugin, StorageWriteCallback, StorageReadCallback, DeepstreamStorage } from '@deepstream/types'\n\nexport class NoopStorage extends DeepstreamPlugin implements DeepstreamStorage {\n  public description = 'Noop Storage'\n\n  public set (key: string, version: number, data: any, callback: StorageWriteCallback) {\n    process.nextTick(() => callback(null))\n  }\n\n  public get (key: string, callback: StorageReadCallback) {\n    process.nextTick(() => callback(null, -1, null))\n  }\n\n  public delete (key: string, callback: StorageWriteCallback) {\n    process.nextTick(() => callback(null))\n  }\n\n  public deleteBulk (key: string[], callback: StorageWriteCallback) {\n    process.nextTick(() => callback(null))\n  }\n}\n"
  },
  {
    "path": "src/services/subscription-registry/default-subscription-registry-factory.ts",
    "content": "import { DefaultSubscriptionRegistry } from './default-subscription-registry'\nimport { DeepstreamConfig, DeepstreamServices, DeepstreamPlugin, SubscriptionRegistryFactory, SubscriptionRegistry } from '@deepstream/types'\nimport { TOPIC } from '../../constants'\n\nexport class DefaultSubscriptionRegistryFactory extends DeepstreamPlugin implements SubscriptionRegistryFactory {\n    public description: string = 'Subscription Registry'\n\n    private subscriptionRegistries = new Map<TOPIC, SubscriptionRegistry>()\n\n    constructor (private pluginOptions: any, private services: Readonly<DeepstreamServices>, private config: Readonly<DeepstreamConfig>) {\n        super()\n    }\n\n    public getSubscriptionRegistry (topic: TOPIC, clusterTopic: TOPIC) {\n        let subscriptionRegistry = this.subscriptionRegistries.get(topic)\n        if (!subscriptionRegistry) {\n            subscriptionRegistry = new DefaultSubscriptionRegistry(this.pluginOptions, this.services, this.config, topic, clusterTopic)\n            this.subscriptionRegistries.set(topic, subscriptionRegistry)\n        }\n        return subscriptionRegistry\n    }\n\n    public getSubscriptionRegistries () {\n        return this.subscriptionRegistries\n    }\n}\n"
  },
  {
    "path": "src/services/subscription-registry/default-subscription-registry.spec.ts",
    "content": "import 'mocha'\nimport * as sinon from 'sinon'\nimport {expect} from 'chai'\nimport * as C from '../../constants'\nimport * as testHelper from '../../test/helper/test-helper'\nimport { getTestMocks } from '../../test/helper/test-mocks'\nimport { SocketWrapper } from '@deepstream/types'\nimport { DefaultSubscriptionRegistry } from './default-subscription-registry';\n\nconst options = testHelper.getDeepstreamOptions()\nconst services = options.services\nconst config = options.config\n\nconst subscriptionListener = {\n  onSubscriptionMade: () => {},\n  onSubscriptionRemoved: () => {},\n  onLastSubscriptionRemoved: () => {},\n  onFirstSubscriptionMade: () => {},\n}\n\nlet subscriptionRegistry: DefaultSubscriptionRegistry\nlet subscriptionListenerMock\n\nlet clientA: { socketWrapper: SocketWrapper }\nlet clientB: { socketWrapper: SocketWrapper }\n\nlet testMocks\n\ndescribe('subscription registry', () => {\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n\n    subscriptionListenerMock = sinon.mock(subscriptionListener)\n    subscriptionRegistry = new DefaultSubscriptionRegistry({}, services, config, C.TOPIC.EVENT, C.TOPIC.EVENT)\n    subscriptionRegistry.setSubscriptionListener(subscriptionListener)\n\n    clientA = testMocks.getSocketWrapper('client a')\n    clientB = testMocks.getSocketWrapper('client b')\n  })\n\n  afterEach(() => {\n    subscriptionListenerMock.verify()\n    clientA.socketWrapperMock.verify()\n    clientB.socketWrapperMock.verify()\n  })\n\n  const subscribeMessage = {\n    topic: C.TOPIC.EVENT,\n    action: C.EVENT_ACTION.SUBSCRIBE,\n    name: 'someName'\n  }\n\n  const unsubscribeMessage = {\n    topic: C.TOPIC.EVENT,\n    action: C.EVENT_ACTION.UNSUBSCRIBE,\n    name: 'someName'\n  }\n\n  const eventMessage = {\n    topic: C.TOPIC.EVENT,\n    action: C.EVENT_ACTION.EMIT,\n    name: 'someName'\n  }\n\n  describe('subscription-registry manages subscriptions', () => {\n    it('subscribes to names', () => {\n      clientA.socketWrapperMock\n        .expects('sendAckMessage')\n        .once()\n        .withExactArgs(subscribeMessage)\n\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n\n      // expect(socketWrapperA.socket.lastSendMessage).to.equal(_msg('E|A|S|someName+'))\n\n      // subscriptionRegistry.sendToSubscribers('someName', fakeEvent('someName', 'SsomeString'))\n      // expect(socketWrapperA.socket.lastSendMessage).to.equal(_msg('E|EVT|someName|SsomeString+'))\n    })\n\n    it('doesn\\'t subscribe twice to the same name', () => {\n      clientA.socketWrapperMock\n        .expects('sendAckMessage')\n        .once()\n        .withExactArgs(subscribeMessage)\n\n      clientA.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.EVENT,\n          action: C.EVENT_ACTION.MULTIPLE_SUBSCRIPTIONS,\n          originalAction: C.EVENT_ACTION.SUBSCRIBE,\n          name: 'someName'\n        })\n\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n      expect(services.logger.lastLogEvent).to.equal(C.EVENT_ACTION[C.EVENT_ACTION.MULTIPLE_SUBSCRIPTIONS])\n    })\n\n    it('returns the subscribed socket', () => {\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n      expect(subscriptionRegistry.getLocalSubscribers('someName')).to.deep.equal(new Set([clientA.socketWrapper]))\n    })\n\n    it('determines if it has subscriptions', () => {\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n      expect(subscriptionRegistry.hasLocalSubscribers('someName')).to.equal(true)\n      expect(subscriptionRegistry.hasLocalSubscribers('someOtherName')).to.equal(false)\n    })\n\n    it('distributes messages to multiple subscribers', () => {\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientB.socketWrapper)\n\n      clientA.socketWrapperMock\n        .expects('sendBuiltMessage')\n        .once()\n\n      clientB.socketWrapperMock\n        .expects('sendBuiltMessage')\n        .once()\n\n      subscriptionRegistry.sendToSubscribers('someName', eventMessage, true, null)\n    })\n\n    it('doesn\\'t send message to sender', () => {\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientB.socketWrapper)\n\n      clientA.socketWrapperMock\n        .expects('sendBuiltMessage')\n        .never()\n\n      clientB.socketWrapperMock\n        .expects('sendBuiltMessage')\n        .once()\n\n      subscriptionRegistry.sendToSubscribers('someName', eventMessage, false, clientA.socketWrapper)\n    })\n\n    it('unsubscribes', () => {\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n\n      clientA.socketWrapperMock\n        .expects('sendAckMessage')\n        .once()\n        .withExactArgs(unsubscribeMessage)\n\n      subscriptionRegistry.unsubscribe(subscribeMessage.name, unsubscribeMessage, clientA.socketWrapper)\n    })\n\n    it('handles unsubscribes for non existant topics', () => {\n      clientA.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.EVENT,\n          action: C.EVENT_ACTION.NOT_SUBSCRIBED,\n          originalAction: C.EVENT_ACTION.UNSUBSCRIBE,\n          name: 'someName'\n        })\n\n      subscriptionRegistry.unsubscribe(subscribeMessage.name, unsubscribeMessage, clientA.socketWrapper)\n    })\n\n    it.skip('removes all subscriptions on socket.close', () => {\n      subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n      subscriptionRegistry.subscribe(subscribeMessage.name, Object.assign({}, subscribeMessage, { name: 'eventB' }), clientA.socketWrapper)\n\n      clientA.socketWrapperMock\n        .expects('sendMessage')\n        .never()\n\n      subscriptionRegistry.sendToSubscribers('nameA', eventMessage, true, null)\n      subscriptionRegistry.sendToSubscribers('nameB', eventMessage, true, null)\n    })\n  })\n\n  describe('subscription-registry allows custom actions to be set', () => {\n    beforeEach(() => {\n      subscriptionRegistry.setAction('subscribe', 'make-aware')\n      subscriptionRegistry.setAction('unsubscribe', 'be-unaware')\n      subscriptionRegistry.setAction('multiple_subscriptions', 'too-aware')\n      subscriptionRegistry.setAction('not_subscribed', 'unaware')\n    })\n\n    it('subscribes to names', () => {\n      clientA.socketWrapperMock\n        .expects('sendAckMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.EVENT,\n          action: 'make-aware',\n          name: 'someName'\n        })\n\n      subscriptionRegistry.subscribe(\n        'someName', {\n        topic: C.TOPIC.EVENT,\n        action: 'make-aware',\n        name: 'someName'\n      }, clientA.socketWrapper)\n    })\n\n    it('doesn\\'t subscribe twice to the same name', () => {\n      clientA.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.EVENT,\n          action: 'too-aware',\n          originalAction: 'make-aware',\n          name: 'someName'\n        })\n\n      subscriptionRegistry.subscribe('someName', {\n        topic: C.TOPIC.EVENT,\n        action: 'make-aware',\n        name: 'someName'\n      }, clientA.socketWrapper)\n      subscriptionRegistry.subscribe('someName', {\n        topic: C.TOPIC.EVENT,\n        action: 'make-aware',\n        name: 'someName'\n      }, clientA.socketWrapper)\n    })\n\n    it('unsubscribes', () => {\n      subscriptionRegistry.subscribe('someName', {\n        topic: C.TOPIC.EVENT,\n        action: 'make-aware',\n        name: 'someName'\n      }, clientA.socketWrapper)\n\n      clientA.socketWrapperMock\n        .expects('sendAckMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.EVENT,\n          action: 'be-unaware',\n          name: 'someName'\n        })\n\n      subscriptionRegistry.unsubscribe('someName', {\n        topic: C.TOPIC.EVENT,\n        action: 'be-unaware',\n        name: 'someName'\n      }, clientA.socketWrapper)\n    })\n\n    it('handles unsubscribes for non existant subscriptions', () => {\n      clientA.socketWrapperMock\n        .expects('sendMessage')\n        .once()\n        .withExactArgs({\n          topic: C.TOPIC.EVENT,\n          action: 'unaware',\n          originalAction: 'be-unaware',\n          name: 'someName'\n        })\n\n      subscriptionRegistry.unsubscribe('someName', {\n        topic: C.TOPIC.EVENT,\n        action: 'be-unaware',\n        name: 'someName'\n      }, clientA.socketWrapper)\n    })\n  })\n\n  describe('subscription-registry unbinds all events on unsubscribe', () => {\n    it('subscribes and unsubscribes 30 times', () => {\n      for (let i = 0; i < 30; i++) {\n        subscriptionRegistry.subscribe(subscribeMessage.name, subscribeMessage, clientA.socketWrapper)\n        subscriptionRegistry.unsubscribe(unsubscribeMessage.name, unsubscribeMessage, clientA.socketWrapper)\n      }\n    })\n  })\n\n})\n"
  },
  {
    "path": "src/services/subscription-registry/default-subscription-registry.ts",
    "content": "import {\n  EVENT_ACTION,\n  PRESENCE_ACTION,\n  RECORD_ACTION,\n  RPC_ACTION,\n  TOPIC,\n  MONITORING_ACTION,\n  Message,\n  BulkSubscriptionMessage,\n  STATE_REGISTRY_TOPIC\n} from '../../constants'\nimport { SocketWrapper, DeepstreamConfig, DeepstreamServices, SubscriptionListener, StateRegistry, SubscriptionRegistry, LOG_LEVEL, EVENT, NamespacedLogger } from '@deepstream/types'\n\ninterface SubscriptionActions {\n  MULTIPLE_SUBSCRIPTIONS: RECORD_ACTION.MULTIPLE_SUBSCRIPTIONS | EVENT_ACTION.MULTIPLE_SUBSCRIPTIONS | RPC_ACTION.MULTIPLE_PROVIDERS | PRESENCE_ACTION.MULTIPLE_SUBSCRIPTIONS\n  NOT_SUBSCRIBED: RECORD_ACTION.NOT_SUBSCRIBED | EVENT_ACTION.NOT_SUBSCRIBED | RPC_ACTION.NOT_PROVIDED | PRESENCE_ACTION.NOT_SUBSCRIBED\n  SUBSCRIBE: RECORD_ACTION.SUBSCRIBE | EVENT_ACTION.SUBSCRIBE | RPC_ACTION.PROVIDE | PRESENCE_ACTION.SUBSCRIBE\n  UNSUBSCRIBE: RECORD_ACTION.UNSUBSCRIBE | EVENT_ACTION.UNSUBSCRIBE | RPC_ACTION.UNPROVIDE | PRESENCE_ACTION.UNSUBSCRIBE\n}\n\ninterface Subscription {\n  name: string\n  sockets: Set<SocketWrapper>\n}\n\nexport class DefaultSubscriptionRegistry implements SubscriptionRegistry {\n  private sockets = new Map<SocketWrapper, Set<Subscription>>()\n  private subscriptions = new Map<string, Subscription>()\n  private subscriptionListener: SubscriptionListener | null = null\n  private constants: SubscriptionActions\n  private clusterSubscriptions: StateRegistry\n  private actions: any\n  private logger: NamespacedLogger = this.services.logger.getNameSpace('SUBSCRIPTION_REGISTRY')\n  private invalidSockets = new Set<SocketWrapper>()\n  /**\n   * A generic mechanism to handle subscriptions from sockets to topics.\n   * A bit like an event-hub, only that it registers SocketWrappers rather\n   * than functions\n   */\n  constructor (private pluginConfig: any, private services: Readonly<DeepstreamServices>, private config: Readonly<DeepstreamConfig>, private topic: TOPIC | STATE_REGISTRY_TOPIC, clusterTopic: TOPIC) {\n    switch (topic) {\n      case TOPIC.RECORD:\n      case STATE_REGISTRY_TOPIC.RECORD_LISTEN_PATTERNS:\n        this.actions = RECORD_ACTION\n        break\n      case TOPIC.EVENT:\n      case STATE_REGISTRY_TOPIC.EVENT_LISTEN_PATTERNS:\n        this.actions = EVENT_ACTION\n        break\n      case TOPIC.RPC:\n        this.actions = RPC_ACTION\n        break\n      case TOPIC.PRESENCE:\n        this.actions = PRESENCE_ACTION\n        break\n      case TOPIC.MONITORING:\n        this.actions = MONITORING_ACTION\n        break\n    }\n\n    this.constants = {\n      MULTIPLE_SUBSCRIPTIONS: this.actions.MULTIPLE_SUBSCRIPTIONS,\n      NOT_SUBSCRIBED: this.actions.NOT_SUBSCRIBED,\n      SUBSCRIBE: this.actions.SUBSCRIBE,\n      UNSUBSCRIBE: this.actions.UNSUBSCRIBE,\n    }\n\n    this.onSocketClose = this.onSocketClose.bind(this)\n\n    this.clusterSubscriptions = this.services.clusterStates.getStateRegistry(clusterTopic)\n\n    if (this.pluginConfig.subscriptionsSanityTimer > 0) {\n      setInterval(this.illegalCleanup.bind(this), this.pluginConfig.subscriptionsSanityTimer)\n      setInterval(() => this.invalidSockets.clear(), this.pluginConfig.subscriptionsSanityTimer * 100)\n    }\n  }\n\n  public async whenReady () {\n    await this.clusterSubscriptions.whenReady()\n  }\n\n  public async close () {\n    await this.clusterSubscriptions.whenReady()\n  }\n\n  /**\n   * Return all the servers that have this subscription.\n   */\n  public getAllServers (subscriptionName: string): string[] {\n    return this.clusterSubscriptions.getAllServers(subscriptionName)\n  }\n\n  /**\n   * Return all the servers that have this subscription excluding the current\n   * server name\n   */\n  public getAllRemoteServers (subscriptionName: string): string[] {\n    const serverNames = this.clusterSubscriptions.getAllServers(subscriptionName)\n    const localServerIndex = serverNames.indexOf(this.config.serverName)\n    if (localServerIndex > -1) {\n      serverNames.splice(serverNames.indexOf(this.config.serverName), 1)\n    }\n    return serverNames\n  }\n\n  /**\n   * Returns a list of all the topic this registry\n   * currently has subscribers for\n   */\n  public getNames (): string[] {\n    return this.clusterSubscriptions.getAll()\n  }\n\n  /**\n   * Returns true if the subscription exists somewhere\n   * in the cluster\n   */\n  public hasName (subscriptionName: string): boolean {\n    return this.clusterSubscriptions.has(subscriptionName)\n  }\n\n  /**\n  * This method allows you to customise the SubscriptionRegistry so that it can send\n  * custom events and ack messages back.\n  * For example, when using the ACTIONS.LISTEN, you would override SUBSCRIBE with\n  * ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE\n  */\n  public setAction (name: string, value: EVENT_ACTION | RECORD_ACTION | RPC_ACTION): void {\n    (this.constants as any)[name.toUpperCase()] = value\n  }\n\n  /**\n   * Enqueues a message string to be broadcast to all subscribers. Broadcasts will potentially\n   * be reordered in relation to *other* subscription names, but never in relation to the same\n   * subscription name.\n   */\n  public sendToSubscribers (name: string, message: Message, noDelay: boolean, senderSocket: SocketWrapper | null, suppressRemote: boolean = false): void {\n    // If the senderSocket is null it means it was received via the message bus\n    if (senderSocket !== null && suppressRemote === false) {\n      this.services.clusterNode.send(message)\n    }\n\n    const subscription = this.subscriptions.get(name)\n\n    if (!subscription) {\n      return\n    }\n\n    const subscribers = subscription.sockets\n\n    this.services.monitoring.onBroadcast(message, subscribers.size)\n\n    const serializedMessages: { [index: string]: any } = {}\n    for (const socket of subscribers) {\n      if (socket === senderSocket) {\n        continue\n      }\n      if (!serializedMessages[socket.socketType]) {\n        if (message.parsedData) {\n          delete message.data\n        }\n        this.logger.debug('SEND_TO_SUBSCRIBERS', `encoding ${name} with protocol ${socket.socketType} with data ${JSON.stringify(message)}`)\n        serializedMessages[socket.socketType] = socket.getMessage(message)\n      }\n      this.logger.debug('SEND_TO_SUBSCRIBERS', `sending ${socket.socketType} payload of ${serializedMessages[socket.socketType]}`)\n      socket.sendBuiltMessage!(serializedMessages[socket.socketType], !noDelay)\n    }\n  }\n\n  /**\n   * Adds a SocketWrapper as a subscriber to a topic\n   */\n  public subscribeBulk (message: BulkSubscriptionMessage, socket: SocketWrapper, silent?: boolean): void {\n    const length = message.names.length\n    for (let i = 0; i < length; i++) {\n      this.subscribe(message.names[i], message, socket, true)\n    }\n    if (!silent) {\n      socket.sendAckMessage({\n        topic: message.topic,\n        action: message.action,\n        correlationId: message.correlationId\n      })\n    }\n  }\n\n  /**\n   * Adds a SocketWrapper as a subscriber to a topic\n   */\n  public unsubscribeBulk (message: BulkSubscriptionMessage, socket: SocketWrapper, silent?: boolean): void {\n    message.names!.forEach((name) => {\n      this.unsubscribe(name, message, socket, true)\n    })\n    if (!silent) {\n      socket.sendAckMessage({\n        topic: message.topic,\n        action: message.action,\n        correlationId: message.correlationId\n      })\n    }\n  }\n\n  /**\n   * Adds a SocketWrapper as a subscriber to a topic\n   */\n  public subscribe (name: string, message: Message, socket: SocketWrapper, silent?: boolean): void {\n    const subscription = this.subscriptions.get(name) || {\n      name,\n      sockets: new Set()\n    }\n\n    if (subscription.sockets.size === 0) {\n      this.subscriptions.set(name, subscription)\n    } else if (subscription.sockets.has(socket)) {\n      if (this.logger.shouldLog(LOG_LEVEL.WARN)) {\n        const msg = `repeat subscription to \"${name}\" by ${socket.userId}`\n        this.logger.warn(EVENT_ACTION[this.constants.MULTIPLE_SUBSCRIPTIONS], msg, { message, socketWrapper: socket })\n      }\n      socket.sendMessage({\n        topic: this.topic,\n        action: this.constants.MULTIPLE_SUBSCRIPTIONS,\n        originalAction: message.action,\n        name\n      })\n      return\n    }\n\n    subscription.sockets.add(socket)\n\n    this.addSocket(subscription, socket)\n\n    if (!silent) {\n      if (this.logger.shouldLog(LOG_LEVEL.DEBUG)) {\n        const logMsg = `for ${TOPIC[this.topic] || STATE_REGISTRY_TOPIC[this.topic]}:${name} by ${socket.userId}`\n        this.logger.debug(this.actions[this.constants.SUBSCRIBE], logMsg)\n      }\n      socket.sendAckMessage(message)\n    }\n  }\n\n  /**\n   * Removes a SocketWrapper from the list of subscriptions for a topic\n   */\n  public unsubscribe (name: string, message: Message, socket: SocketWrapper, silent?: boolean): void {\n    const subscription = this.subscriptions.get(name)\n\n    if (!subscription || !subscription.sockets.delete(socket)) {\n      if (!silent) {\n        if (this.logger.shouldLog(LOG_LEVEL.WARN)) {\n          const msg = `${socket.userId} is not subscribed to ${name}`\n          this.logger.warn(this.actions[this.constants.NOT_SUBSCRIBED], msg, { socketWrapper: socket, message})\n        }\n        if (STATE_REGISTRY_TOPIC[this.topic]) {\n          // This isn't supported for STATE_REGISTRY_TOPIC/s\n          return\n        }\n        socket.sendMessage({\n          topic: this.topic,\n          action: this.constants.NOT_SUBSCRIBED,\n          originalAction: message.action,\n          name\n        })\n      }\n      return\n    }\n\n    this.removeSocket(subscription, socket)\n\n    if (!silent) {\n      if (this.logger.shouldLog(LOG_LEVEL.DEBUG)) {\n        const logMsg = `for ${this.topic}:${name} by ${socket.userId}`\n        this.logger.debug(this.actions[this.constants.UNSUBSCRIBE], logMsg)\n      }\n      socket.sendAckMessage(message)\n    }\n  }\n\n  /**\n   * Returns an array of SocketWrappers that are subscribed\n   * to <name> or null if there are no subscribers\n   */\n  public getLocalSubscribers (name: string): Set<SocketWrapper> {\n    const subscription = this.subscriptions.get(name)\n    return subscription ? subscription.sockets : new Set()\n  }\n\n  /**\n   * Returns true if there are SocketWrappers that\n   * are subscribed to <name> or false if there\n   * aren't any subscribers\n   */\n  public hasLocalSubscribers (name: string): boolean {\n    return this.subscriptions.has(name)\n  }\n\n  /**\n   * Allows to set a subscriptionListener after the class had been instantiated\n   */\n  public setSubscriptionListener (listener: SubscriptionListener): void {\n    this.subscriptionListener = listener\n    this.clusterSubscriptions.onAdd(listener.onFirstSubscriptionMade.bind(listener))\n    this.clusterSubscriptions.onRemove(listener.onLastSubscriptionRemoved.bind(listener))\n  }\n\n  private addSocket (subscription: Subscription, socket: SocketWrapper): void {\n    const subscriptions = this.sockets.get(socket) || new Set()\n    if (subscriptions.size === 0) {\n      this.sockets.set(socket, subscriptions)\n      socket.onClose(this.onSocketClose)\n    }\n    subscriptions.add(subscription)\n\n    this.clusterSubscriptions!.add(subscription.name)\n\n    if (this.subscriptionListener) {\n      this.subscriptionListener.onSubscriptionMade(subscription.name, socket)\n    }\n  }\n\n  private removeSocket (subscription: Subscription, socket: SocketWrapper): void {\n    if (subscription.sockets.size === 0) {\n      this.subscriptions.delete(subscription.name)\n    }\n\n    if (this.subscriptionListener) {\n      this.subscriptionListener.onSubscriptionRemoved(subscription.name, socket)\n    }\n    this.clusterSubscriptions!.remove(subscription.name)\n\n    const subscriptions = this.sockets.get(socket)\n    if (subscriptions) {\n      subscriptions.delete(subscription)\n\n      if (subscriptions.size === 0) {\n        this.sockets.delete(socket)\n        socket.removeOnClose(this.onSocketClose)\n      }\n    } else {\n      this.logger.error(EVENT.ERROR, 'Attempting to delete a subscription that doesn\\'t exist')\n    }\n  }\n\n  /**\n  * Called whenever a socket closes to remove all of its subscriptions\n  */\n  private onSocketClose (socket: SocketWrapper): void {\n    const subscriptions = this.sockets.get(socket)\n    if (!subscriptions) {\n      this.logger.error(\n        EVENT_ACTION[this.constants.NOT_SUBSCRIBED],\n        'A socket has an illegal registered close callback',\n        { socketWrapper: socket }\n      )\n      return\n    }\n    for (const subscription of subscriptions) {\n      subscription.sockets.delete(socket)\n      this.removeSocket(subscription, socket)\n    }\n    this.sockets.delete(socket)\n  }\n\n  private illegalCleanup () {\n    this.sockets.forEach((subscriptions, socket) => {\n      if (socket.isClosed) {\n        if (!this.invalidSockets.has(socket)) {\n          this.logger.error(\n            EVENT.CLOSED_SOCKET,\n            `Socket ${socket.uuid} is closed but still in registry. Currently there are ${this.invalidSockets.size} sockets. If you see this please raise a github issue!`\n          )\n          this.invalidSockets.add(socket)\n        }\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "src/services/telemetry/deepstreamio-telemetry.ts",
    "content": "import { DeepstreamTelemetry, DeepstreamPlugin, DeepstreamServices, EVENT, DeepstreamConfig } from '@deepstream/types'\nimport { getDSInfo } from '../../config/ds-info'\nimport { v4 as uuid } from 'uuid'\nimport { validateUUID } from '../../utils/utils'\nimport { Dictionary } from 'ts-essentials'\nimport { post } from 'needle'\n\nconst TELEMETRY_URL = process.env.TELEMETRY_URL || 'http://telemetry.deepstream.io:8080/api/v1/startup'\nconst DEFAULT_UUID = '00000000-0000-0000-0000-000000000000'\n\nexport interface DeepstreamIOTelemetryOptions {\n    enabled: boolean\n    debug: boolean\n    deploymentId: string\n}\n\nexport class DeepstreamIOTelemetry extends DeepstreamPlugin implements DeepstreamTelemetry {\n    public description = 'Deepstream Telemetry'\n    private logger = this.services.logger.getNameSpace('TELEMETRY')\n\n    constructor (private pluginOptions: DeepstreamIOTelemetryOptions, private services: DeepstreamServices, private config: DeepstreamConfig) {\n        super()\n    }\n\n    public init () {\n        if (this.pluginOptions.enabled === false) {\n            this.logger.info(\n                EVENT.INFO,\n                'Telemetry disabled'\n            )\n            return\n        }\n        if (this.pluginOptions.deploymentId === undefined || !validateUUID(this.pluginOptions.deploymentId)) {\n            this.logger.error(\n                EVENT.ERROR,\n                `Invalid deployment id, must be uuid format. Feel free to use this one \"${uuid()}\"`\n            )\n            this.pluginOptions.deploymentId = DEFAULT_UUID\n        }\n    }\n\n    public async whenReady (): Promise<void> {\n        if (this.pluginOptions.enabled === false) {\n            return\n        }\n        const info = getDSInfo()\n        const enabledFeatures = this.config.enabledFeatures\n        const config: any = this.config\n        const services = Object.keys(this.config).reduce((result, key) => {\n            if (!config[key]) {\n                return result\n            }\n            if (config[key].type) {\n                result[key] = config[key].type\n            } else if (config[key].name) {\n                result[key] = {\n                    name: config[key].name\n                }\n            } else if (config[key].path) {\n                result[key] = 'custom'\n            }\n            return result\n        }, {} as Dictionary<any>)\n\n        const analytics = {\n            deploymentId: this.pluginOptions.deploymentId,\n            ...info,\n            enabledFeatures,\n            services\n        }\n\n        if (this.pluginOptions.debug) {\n            this.logger.info(EVENT.TELEMETRY_DEBUG, `We would have sent the following: ${JSON.stringify(analytics)}`)\n        } else {\n            this.sendReport(analytics)\n        }\n    }\n\n    public async close (): Promise<void> {\n    }\n\n    private sendReport (data: any): void {\n        post(TELEMETRY_URL, data, { content_type: 'application/json' }, (error: any) => {\n          if (error) {\n            if (error.code === 'ECONNREFUSED') {\n                this.logger.warn(EVENT.TELEMETRY_UNREACHABLE, \"Can't reach telemetry endpoint\")\n            } else {\n                console.log(error)\n                this.logger.error(EVENT.ERROR, `Telemetry error: ${error}`)\n            }\n          }\n        })\n    }\n}"
  },
  {
    "path": "src/test/common.ts",
    "content": "import * as chai from 'chai'\nimport * as sinonChai from 'sinon-chai'\nchai.use(sinonChai)\n"
  },
  {
    "path": "src/test/config/basic-permission-config.json",
    "content": "{\n    \"presence\": {\n        \"*\": {\n            \"allow\": true\n        }\n    },\n    \"record\": {\n        \"*\": {\n            \"write\": true,\n            \"read\": true\n        }\n    },\n    \"event\": {\n        \"*\": {\n            \"publish\": true,\n            \"subscribe\": true\n        }\n    },\n    \"rpc\": {\n        \"*\": {\n            \"provide\": true,\n            \"request\": true\n        }\n    }\n}"
  },
  {
    "path": "src/test/config/basic-valid-json.json",
    "content": "{\n    \"pet\": \"pug\"\n}"
  },
  {
    "path": "src/test/config/blank-config.json",
    "content": ""
  },
  {
    "path": "src/test/config/config-broken.js",
    "content": "/* eslint-disable */\nfoobarBreaksIt\n"
  },
  {
    "path": "src/test/config/config-broken.yml",
    "content": "asdsad: ooops:\n"
  },
  {
    "path": "src/test/config/config.js",
    "content": "module.exports = {\n  port: 1002\n}\n"
  },
  {
    "path": "src/test/config/config.yml",
    "content": "serverName: UUID\nport: 1337\nhost: 1.2.3.4\ncolors: false\nshowLogo: false\nlogLevel: ERROR\n\nmultipleenvs: '${EXAMPLE_HOST}:${EXAMPLE_PORT}'\nthisenvironmentdoesntexist: ${DOESNT_EXIST}\nenvironmentvariable: ${ENVIRONMENT_VARIABLE_TEST_1}\nanother:\n  environmentvariable: ${ENVIRONMENT_VARIABLE_TEST_2}"
  },
  {
    "path": "src/test/config/empty-map-config.json",
    "content": "{}"
  },
  {
    "path": "src/test/config/exists-test/a-file.js",
    "content": "module.exports = {}\n"
  },
  {
    "path": "src/test/config/exists-test/a-file.yml",
    "content": "---\nyup: thats right"
  },
  {
    "path": "src/test/config/exists-test/a-json-file.json",
    "content": "{\n    \"I\": \"exist\"\n}"
  },
  {
    "path": "src/test/config/invalid-permission-conf.json",
    "content": "{\n    \"presence\": {\n        \"*\": {\n            \"allow\": true\n        }\n    },\n    \"record\": {},\n    \"event\": {\n        \"*\": {\n            \"publish\": true,\n            \"subscribe\": true\n        }\n    },\n    \"rpc\": {\n        \"*\": {\n            \"provide\": true,\n            \"request\": true\n        }\n    }\n}"
  },
  {
    "path": "src/test/config/invalid-user-config.json",
    "content": "{\n    \"userA\": {\n        \"password\": \"tsA+yfWGoEk9uEU/GX1JokkzteayLj6YFTwmraQrO7k=75KQ2Mzm\",\n        \"serverData\": {}\n    },\n    \"userB\": {\n        \"serverData\": {}\n    }\n}"
  },
  {
    "path": "src/test/config/json-with-env-variables.json",
    "content": "{\n    \"multipleenvs\": \"${EXAMPLE_HOST}:${EXAMPLE_PORT}\",\n    \"environmentvariable\": \"${ENVIRONMENT_VARIABLE_TEST_1}\",\n    \"another\": {\n        \"environmentvariable\": \"${ENVIRONMENT_VARIABLE_TEST_2}\"\n    },\n    \"thisenvironmentdoesntexist\": \"${DOESNT_EXIST}\"\n}"
  },
  {
    "path": "src/test/config/no-private-events-permission-config.json",
    "content": "{\n    \"presence\": {\n        \"*\": {\n            \"allow\": true\n        }\n    },\n    \"record\": {\n        \"*\": {\n            \"write\": true,\n            \"read\": true\n        }\n    },\n    \"event\": {\n        \"*\": {\n            \"publish\": true,\n            \"subscribe\": true\n        },\n        \"private/*\": {\n            \"publish\": false,\n            \"subscribe\": false\n        }\n    },\n    \"rpc\": {\n        \"*\": {\n            \"provide\": true,\n            \"request\": true\n        }\n    }\n}"
  },
  {
    "path": "src/test/config/sslKey.pem",
    "content": "I'm a key"
  },
  {
    "path": "src/test/config/users-unhashed.json",
    "content": "{\n    \"userC\": {\n        \"password\": \"userCPass\",\n        \"serverData\": { \"some\": \"values\" },\n        \"clientData\": { \"all\": \"othervalue\" }\n    },\n    \"userD\": {\n        \"password\": \"userDPass\",\n        \"clientData\": { \"all\": \"client data\" }\n    }\n}"
  },
  {
    "path": "src/test/config/users.json",
    "content": "{\n    \"userA\": {\n        \"password\": \"CKgFpPLJX1+FezZR8bMsP+8wQR+WG0z7AZYRy9nz5KY=DzI79/e3yJ0Y0UvNENMXaQ==\",\n        \"serverData\": { \"some\": \"values\" },\n        \"clientData\": { \"all\": \"othervalue\" }\n    },\n    \"userB\": {\n        \"password\": \"jHPg24EDs9SKHALytrfaoEvDyz7wJgSVEY0ANaw/LgA=m5Ilfg/3+yN5j38tx8cBfA==\",\n        \"clientData\": { \"all\": \"client data\" }\n    }\n}"
  },
  {
    "path": "src/test/helper/start-test-server.ts",
    "content": "const TestServer = require('./test-http-server')\nconst testServer = new TestServer(6004, () => {}, true)\n\ntestServer.on(\n  'request-received',\n  testServer.respondWith.bind(\n  testServer,\n    501,\n    {\n      serverData: {},\n      clientData: { name: 'bob'}\n    }\n  )\n)\n"
  },
  {
    "path": "src/test/helper/test-helper.ts",
    "content": "import * as SocketWrapperFactoryMock from '../mock/socket-wrapper-factory-mock'\nimport AuthenticationHandler from '../mock/authentication-handler-mock'\nimport {get} from '../../default-options'\nimport MessageConnectorMock from '../mock/message-connector-mock'\nimport LoggerMock from '../mock/logger-mock'\nimport StorageMock from '../mock/storage-mock'\nimport { DeepstreamConfig, DeepstreamServices, SocketWrapper, DeepstreamMonitoring, DeepstreamPlugin, PermissionCallback, LOG_LEVEL, EVENT, ValveSchema } from '@deepstream/types'\nimport { Message } from '../../constants'\nimport { DefaultSubscriptionRegistryFactory } from '../../services/subscription-registry/default-subscription-registry-factory'\nimport { DistributedStateRegistryFactory } from '../../services/cluster-state/distributed-state-registry-factory'\nimport { DistributedClusterRegistry } from '../../services/cluster-registry/distributed-cluster-registry'\n\nexport const getBasePermissions = function (): ValveSchema {\n  return {\n    presence: {\n      '*': {\n        allow: true\n      }\n    },\n    record: {\n      '*': {\n        write: true,\n        read: true\n      }\n    },\n    event: {\n      '*': {\n        publish: true,\n        subscribe: true\n      }\n    },\n    rpc: {\n      '*': {\n        provide: true,\n        request: true\n      }\n    }\n  }\n}\n\nexport const getDeepstreamOptions = (serverName?: string): { config: DeepstreamConfig, services: DeepstreamServices } => {\n  const config = { ...get(), ...{\n    serverName: serverName || 'server-name-a',\n\n    cluster: {\n      state: {\n        options: {\n          reconciliationTimeout: 50\n        }\n      }\n    },\n    permission: {\n      options: {\n        cacheEvacuationInterval: 60000,\n        maxRuleIterations: 3\n      }\n    },\n    rpc: {\n      provideRequestorData: true,\n      provideRequestorName: true,\n      ackTimeout: 10,\n      responseTimeout: 20,\n    },\n    record: {\n      cacheRetrievalTimeout: 30,\n      storageRetrievalTimeout: 50,\n      storageExclusionPrefixes: ['no-storage'],\n      storageHotPathPrefixes: [],\n    }\n  }} as never as DeepstreamConfig\n\n  class PermissionHandler extends DeepstreamPlugin implements PermissionHandler {\n    public lastArgs: any[]\n    public description: string\n    public nextResult: boolean\n    public nextError: string | null\n\n    constructor () {\n      super()\n      this.description = 'Test Permission Handler'\n      this.nextResult = true\n      this.nextError = null\n      this.lastArgs = []\n    }\n\n    public canPerformAction (socketWrapper: SocketWrapper, message: Message, callback: PermissionCallback, passItOn: any) {\n      this.lastArgs.push([socketWrapper.userId, message, callback])\n      callback(socketWrapper, message, passItOn, this.nextError, this.nextResult)\n    }\n  }\n\n// tslint:disable-next-line: max-classes-per-file\n  class MonitoringMock extends DeepstreamPlugin implements DeepstreamMonitoring {\n    public description = 'monitoring mock'\n    public onErrorLog (loglevel: LOG_LEVEL, event: EVENT, logMessage: string): void {\n    }\n    public onLogin (allowed: boolean, endpointType: string): void {\n    }\n    public onMessageReceived (message: Message): void {\n    }\n    public onMessageSend (message: Message): void {\n    }\n    public onBroadcast (message: Message, count: number): void {\n    }\n  }\n\n  const services: Partial<DeepstreamServices> = {\n    logger: new LoggerMock(),\n    cache: new StorageMock(),\n    storage: new StorageMock(),\n    clusterNode: new MessageConnectorMock(config),\n    // @ts-ignore\n    locks: {\n      get (name, cb) { cb(true) },\n      release () {}\n    },\n    monitoring: new MonitoringMock(),\n    authenticationHandler: new AuthenticationHandler(),\n    permission: new PermissionHandler(),\n    connectionEndpoints: [],\n  }\n  services.subscriptions = new DefaultSubscriptionRegistryFactory({}, services as DeepstreamServices, config)\n  services.clusterStates = new DistributedStateRegistryFactory({}, services as DeepstreamServices, config)\n  services.clusterRegistry = new DistributedClusterRegistry({}, services as DeepstreamServices, config)\n  return { config, services } as { config: DeepstreamConfig, services: DeepstreamServices}\n}\n\nexport const getDeepstreamPermissionOptions = function () {\n  const options = exports.getDeepstreamOptions()\n  options.config = Object.assign(options.config, {\n    cacheRetrievalTimeout: 500,\n  })\n  return { config: options.config, services: options.services }\n}\n\nconst ConfigPermission = require('../../services/permission/valve/config-permission').ConfigPermission\n\nexport const testPermission = function (options: { config: DeepstreamConfig, services: DeepstreamServices }) {\n  return function (permissions: any, message: Message, username?: string, userdata?: any, callback?: PermissionCallback) {\n    options.config.permission.options.permissions = permissions\n    const permission = new ConfigPermission(options.config.permission.options, options.services, options.config)\n    permission.setRecordHandler({\n      removeRecordRequest: () => {},\n      runWhenRecordStable: (r: any, c: any) => { c(r) }\n    })\n    let permissionResult\n\n    const socketWrapper = SocketWrapperFactoryMock.createSocketWrapper()\n    socketWrapper.userId = username || 'someUser'\n    socketWrapper.serverData = userdata\n    callback = callback || function (sw: SocketWrapper, msg: Message, passItOn: any, error: any, result: boolean) {\n      permissionResult = result\n    }\n    permission.canPerformAction(socketWrapper, message, callback)\n    return permissionResult\n  }\n}\n"
  },
  {
    "path": "src/test/helper/test-http-server.ts",
    "content": "import * as http from 'http'\nimport { EventEmitter } from 'events'\n\nexport default class TestHttpServer extends EventEmitter {\n  public server: any\n  public lastRequestData: any = null\n  public hasReceivedRequest: boolean = false\n  public lastRequestHeaders: any = null\n  public lastRequestMethod: any = null\n  private response: any = null\n  private request: any = null\n\n  constructor (private port: number, private callback: Function, private doLog: boolean = false) {\n    super()\n    this.server = http.createServer(this.onRequest.bind(this))\n    this.server.listen(port, this.onListen.bind(this))\n  }\n\n  public static getRandomPort () {\n    return 1000 + Math.floor(Math.random() * 9000)\n  }\n\n  public getRequestHeader (key: string) {\n    return this.request.headers[key]\n  }\n\n  public reset () {\n    this.lastRequestData = null\n    this.hasReceivedRequest = false\n    this.lastRequestHeaders = null\n  }\n\n  public respondWith (statusCode: number, data: any) {\n    if (typeof data === 'object') {\n      data = JSON.stringify(data) // eslint-disable-line\n    }\n    this.response.setHeader('content-type', 'application/json')\n    this.response.writeHead(statusCode)\n    this.response.end(data)\n  }\n\n  public close (callback: Function) {\n    this.server.close(callback)\n  }\n\n  private onListen () {\n    this.log(`server listening on port ${this.port}`)\n    this.callback()\n  }\n\n  private log (msg: string) {\n    if (this.doLog) {\n      console.log(msg)\n    }\n  }\n\n  private onRequest (request: http.IncomingMessage, response: http.OutgoingMessage) {\n    let postData = ''\n    request.setEncoding('utf8')\n    request.on('data', (chunk) => {\n      postData += chunk\n    })\n    request.on('end', () => {\n      this.lastRequestData = JSON.parse(postData)\n      this.lastRequestHeaders = request.headers\n      this.lastRequestMethod = request.method\n      this.emit('request-received')\n      this.log(`received data ${postData}`)\n    })\n    this.request = request\n    this.response = response\n  }\n}\n"
  },
  {
    "path": "src/test/helper/test-mocks.ts",
    "content": "import { EventEmitter } from 'events'\nimport { Message, JSONObject } from '../../constants'\nimport { SocketWrapper } from '@deepstream/types'\nconst sinon = require('sinon')\n\nexport const getTestMocks = () => {\n\n  const subscriptionRegistry = {\n    subscribe: () => {},\n    unsubscribe: () => {},\n    sendToSubscribers: () => {},\n    setSubscriptionListener: () => {},\n    getLocalSubscribers: () => new Set(),\n    getAllRemoteServers: () => {},\n    setAction: () => {},\n    hasLocalSubscribers: () => true,\n    subscribeBulk: () => {},\n    unsubscribeBulk: () => {}\n  }\n\n  const listenerRegistry = {\n    handle: () => {}\n  }\n\n  const emitter = new EventEmitter()\n  const stateRegistry = {\n    add: () => {},\n    remove: () => {},\n    on: () => {},\n    emit: () => {},\n    getAll: () => {},\n    onAdd: () => {},\n    onRemove: () => {}\n  }\n  stateRegistry.on = emitter.on as any\n  stateRegistry.emit = emitter.emit as any\n\n  const recordHandler = {\n    broadcastUpdate: () => {},\n    transitionComplete: () => {}\n  }\n\n  const subscriptionRegistryMock = sinon.mock(subscriptionRegistry)\n  const listenerRegistryMock = sinon.mock(listenerRegistry)\n  const stateRegistryMock = sinon.mock(stateRegistry)\n  const recordHandlerMock = sinon.mock(recordHandler)\n\n  function getSocketWrapper (userId: string, authData: JSONObject = {}, clientData: JSONObject = {}) {\n    const socketWrapper = {\n      authAttempts: 0,\n      userId,\n      authData,\n      clientData,\n      sendMessage: () => {},\n      sendBuiltMessage: () => {},\n      sendAckMessage: () => {},\n      uuid: Math.random(),\n      parseData: (message: Message) => {\n        if (message.parsedData) {\n          return true\n        }\n        try {\n          message.parsedData = JSON.parse(message.data!.toString())\n          return true\n        } catch (e) {\n          return e\n        }\n      },\n      getMessage: (message: Message) => message,\n      parseMessage: (message: Message) => message,\n      destroy: () => {},\n      getHandshakeData: () => ({}),\n      close: () => {},\n      onClose: () => {},\n      removeOnClose: () => {}\n    } as never as SocketWrapper\n\n    return {\n      socketWrapper,\n      socketWrapperMock: sinon.mock(socketWrapper)\n    }\n  }\n\n  return {\n    subscriptionRegistry,\n    listenerRegistry,\n    stateRegistry,\n    recordHandler,\n    subscriptionRegistryMock,\n    listenerRegistryMock,\n    stateRegistryMock,\n    recordHandlerMock,\n    getSocketWrapper,\n  }\n}\n"
  },
  {
    "path": "src/test/mock/authentication-handler-mock.ts",
    "content": "import { DeepstreamPlugin, DeepstreamAuthentication, DeepstreamAuthenticationResult } from '@deepstream/types'\n\nexport default class AuthenticationMock extends DeepstreamPlugin implements DeepstreamAuthentication {\n  public onClientDisconnectCalledWith: string | null = null\n  public sendNextValidAuthWithData: boolean = false\n  public lastUserValidationQueryArgs: IArguments | null = null\n  public nextUserValidationResult: boolean = true\n  public nextUserIsAnonymous: boolean = false\n  public description: string = 'Authentication Mock'\n\n  constructor () {\n    super()\n    this.reset()\n  }\n\n  public reset () {\n    this.nextUserIsAnonymous = false\n    this.nextUserValidationResult = true\n    this.lastUserValidationQueryArgs = null\n    this.sendNextValidAuthWithData = false\n    this.onClientDisconnectCalledWith = null\n  }\n\n  public async isValidUser (handshakeData: any, authData: any): Promise<DeepstreamAuthenticationResult> {\n    this.lastUserValidationQueryArgs = arguments\n    if (this.nextUserValidationResult === true) {\n      if (this.sendNextValidAuthWithData === true) {\n        return {\n          isValid: true,\n          id: 'test-user',\n          clientData: { value: 'test-data' }\n        }\n      }\n      if (this.nextUserIsAnonymous) {\n        return {\n          isValid: true,\n          id: 'open'\n        }\n      }\n      return {\n        isValid: true,\n        id: 'test-user'\n      }\n    }\n\n    return {\n      isValid: false,\n      clientData: { error: 'Invalid User' }\n    }\n  }\n\n  public onClientDisconnect (username: string) {\n    this.onClientDisconnectCalledWith = username\n  }\n}\n"
  },
  {
    "path": "src/test/mock/http-mock.ts",
    "content": "import { EventEmitter } from 'events'\n\nexport class HttpServerMock extends EventEmitter {\n  public listening: boolean  = false\n  public closed: boolean = false\n  private port: any\n  private host: any\n\n  public listen (port: string, host: string, callback: Function) {\n    this.port = port\n    this.host = host\n    const server = this\n    process.nextTick(() => {\n      server.listening = true\n      server.emit('listening')\n      if (callback) {\n        callback()\n      }\n    })\n  }\n\n  public close (callback: Function) {\n    this.closed = true\n    this.emit('close')\n    if (callback) {\n      callback()\n    }\n  }\n\n  public address () {\n    return {\n      address: this.host || 'localhost',\n      port: this.port || 8080\n    }\n  }\n\n  public _simulateUpgrade (socket: any) {\n    const head = {}\n    const request = {\n      url: 'https://deepstream.io/?ds=foo',\n      headers: {\n        'origin': '',\n        'sec-websocket-key': 'xxxxxxxxxxxxxxxxxxxxxxxx'\n      },\n      connection: {\n        authorized: true\n      }\n    }\n    this.emit('upgrade', request, socket, head)\n  }\n}\n\n// tslint:disable-next-line:max-classes-per-file\nexport default class HttpMock {\n  public nextServerIsListening: boolean\n  constructor () {\n    this.nextServerIsListening = false\n  }\n\n  public createServer () {\n    const server = new HttpServerMock()\n    server.listening = this.nextServerIsListening\n    return server\n  }\n}\n"
  },
  {
    "path": "src/test/mock/logger-mock.ts",
    "content": "import {spy, SinonSpy} from 'sinon'\nimport { DeepstreamLogger, DeepstreamPlugin, LOG_LEVEL, NamespacedLogger, EVENT } from '@deepstream/types'\n\nexport default class LoggerMock extends DeepstreamPlugin implements DeepstreamLogger {\n  public description: string = 'mock logger'\n  public lastLogLevel: any\n  public lastLogEvent: any\n  public lastLogMessage: any\n  public lastLogArguments: any\n  public logSpy: SinonSpy\n\n  constructor () {\n    super()\n    this.lastLogLevel = null\n    this.lastLogEvent = null\n    this.lastLogMessage = null\n    this.lastLogArguments = null\n\n    this.logSpy = spy()\n  }\n\n  public shouldLog (logLevel: LOG_LEVEL): boolean {\n    return true\n  }\n\n  public warn (event: EVENT | string, message?: string, metaData?: any) {\n    this.log(LOG_LEVEL.WARN, event, message)\n    this.logSpy(LOG_LEVEL.WARN, event, message)\n  }\n\n  public debug (event: EVENT | string, message?: string, metaData?: any) {\n    this.log(LOG_LEVEL.DEBUG, event, message)\n    this.logSpy(LOG_LEVEL.DEBUG, event, message)\n  }\n\n  public info (event: EVENT | string, message?: string, metaData?: any) {\n    this.log(LOG_LEVEL.INFO, event, message)\n    this.logSpy(LOG_LEVEL.INFO, event, message)\n  }\n\n  public error (event: EVENT | string, message?: string, metaData?: any) {\n    this.log(LOG_LEVEL.ERROR, event, message)\n    this.logSpy(LOG_LEVEL.ERROR, event, message)\n  }\n\n  public fatal (event: string, message?: string | undefined, metaData?: any): void {\n    this.log(LOG_LEVEL.FATAL, event, message)\n    this.logSpy(LOG_LEVEL.FATAL, event, message)\n  }\n\n  public getNameSpace (namespace: string): NamespacedLogger {\n    return this\n  }\n\n  private log (level: LOG_LEVEL, event: EVENT | string, message?: string, metaData?: any) {\n    this.lastLogLevel = level\n    this.lastLogEvent = event\n    this.lastLogMessage = message\n    this.lastLogArguments = Array.from(arguments)\n  }\n\n  public setLogLevel () {\n  }\n}\n"
  },
  {
    "path": "src/test/mock/message-connector-mock.ts",
    "content": "import { EventEmitter } from 'events'\nimport { DeepstreamPlugin, DeepstreamClusterNode } from '@deepstream/types'\nimport { TOPIC, Message, STATE_REGISTRY_TOPIC } from '../../constants'\n\nexport default class MessageConnectorMock extends DeepstreamPlugin implements DeepstreamClusterNode {\n  public description = 'Message Connector Mock'\n  public lastPublishedTopic: TOPIC | STATE_REGISTRY_TOPIC | null = null\n  public lastPublishedMessage: Message | null = null\n  public lastSubscribedTopic: TOPIC | null = null\n  public publishedMessages: Message[] = []\n  public all: string[] = ['server-name-a', 'server-name-b', 'server-name-c']\n  public lastDirectSentMessage: any\n  public currentLeader: string = 'server-name-a'\n  public eventEmitter = new EventEmitter()\n\n  constructor (private options: any) {\n    super()\n    this.eventEmitter.setMaxListeners(0)\n  }\n\n  public reset () {\n    this.publishedMessages = []\n    this.lastPublishedTopic = null\n    this.lastPublishedMessage = null\n    this.lastSubscribedTopic = null\n\n    this.all = ['server-name-a', 'server-name-b', 'server-name-c']\n    this.currentLeader = 'server-name-a'\n  }\n\n  public subscribe <MessageType> (topic: TOPIC, callback: (message: MessageType, originServerName: string) => void) {\n    this.lastSubscribedTopic = topic\n    this.eventEmitter.on(TOPIC[topic], callback)\n  }\n\n  public sendBroadcast () {\n  }\n\n  public send (message: Message, metaData?: any) {\n    this.publishedMessages.push(message)\n    this.lastPublishedTopic = message.topic\n    this.lastPublishedMessage = message\n  }\n\n  public sendDirect (serverName: string, message: Message) {\n    this.lastDirectSentMessage = {\n      serverName,\n      message\n    }\n  }\n\n  public unsubscribe (topic: TOPIC, callback: (message: Message) => void) {\n    this.eventEmitter.removeListener(TOPIC[topic], callback)\n  }\n\n  public simulateIncomingMessage (topic: TOPIC, msg: Message, serverName: string) {\n    this.eventEmitter.emit(TOPIC[topic], msg, serverName)\n  }\n\n  public getAll () {\n    return this.all\n  }\n\n  public isLeader () {\n    return this.currentLeader === this.options.serverName\n  }\n\n  public getLeader () {\n    return this.currentLeader\n  }\n\n  public getCurrentLeader () {\n    return this.currentLeader\n  }\n\n  public subscribeServerDisconnect () {\n\n  }\n}\n"
  },
  {
    "path": "src/test/mock/permission-handler-mock.ts",
    "content": "import { PermissionCallback, SocketWrapper, DeepstreamPlugin } from '@deepstream/types'\nimport { Message } from '../../constants'\n\nexport default class PermissionHandlerMock extends DeepstreamPlugin {\n  public nextCanPerformActionResult: any\n  public lastCanPerformActionQueryArgs: any\n  public description = 'PermissionHandlerMock'\n\n  constructor () {\n    super()\n    this.reset()\n  }\n\n  public reset () {\n    this.nextCanPerformActionResult = true\n    this.lastCanPerformActionQueryArgs = null\n  }\n\n  public canPerformAction (socketWrapper: SocketWrapper, message: Message, callback: PermissionCallback, passItOn: any) {\n    this.lastCanPerformActionQueryArgs = arguments\n    if (typeof this.nextCanPerformActionResult === 'string') {\n      callback(socketWrapper, message, passItOn, this.nextCanPerformActionResult, false)\n    } else {\n      callback(socketWrapper, message, passItOn, null, this.nextCanPerformActionResult)\n    }\n  }\n}\n"
  },
  {
    "path": "src/test/mock/plugin-mock.ts",
    "content": "import { DeepstreamPlugin, DeepstreamServices, DeepstreamConfig } from '@deepstream/types'\nimport { EventEmitter } from 'events'\n\nexport default class PluginMock extends DeepstreamPlugin {\n  public isReady: boolean = false\n  public description: string = this.options.name || 'mock-plugin'\n  private emitter = new EventEmitter()\n\n  constructor (private options: any, services: DeepstreamServices, config: DeepstreamConfig) {\n    super()\n  }\n\n  public setReady () {\n    this.isReady = true\n    this.emitter.emit('ready')\n  }\n\n  public async whenReady () {\n    if (!this.isReady) {\n      await new Promise((resolve) => {\n        this.emitter.once('ready', resolve)\n        setTimeout(resolve, 20)\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "src/test/mock/socket-mock.ts",
    "content": "import { Message } from '../../constants'\n\nexport default class SocketMock {\n  public lastSendMessage: any\n  public isDisconnected: any\n  public sendMessages: any\n  public autoClose: any\n  public readyState: any\n  public ssl: any\n  // tslint:disable-next-line:variable-name\n  public _handle: any\n\n  constructor () {\n    this.lastSendMessage = null\n    this.isDisconnected = false\n    this.sendMessages = []\n    this.autoClose = true\n    this.readyState = ''\n    this.ssl = null\n    this._handle = {}\n}\n\npublic send (message: Message) {\n  this.lastSendMessage = message\n  this.sendMessages.push(message)\n}\n\npublic end () {\n}\n\npublic getMsg (i: number) {\n  return this.sendMessages[this.sendMessages.length - (i + 1)]\n}\n\npublic getMsgSize () {\n  return this.sendMessages.length\n}\n\npublic close () {\n  if (this.autoClose === true) {\n    this.doClose()\n  }\n}\n\npublic destroy () {\n  this.doClose()\n}\n\npublic doClose () {\n  this.isDisconnected = true\n  this.readyState = 'closed'\n}\n\n}\n"
  },
  {
    "path": "src/test/mock/socket-wrapper-factory-mock.ts",
    "content": "import { EventEmitter } from 'events'\nimport { Message } from '../../constants'\n\nclass SocketWrapperMock extends EventEmitter {\n  public static lastPreparedMessage: any\n  public isClosed: boolean = false\n  public authCallBack: any = null\n  public authAttempts: number = 0\n  public uuid: number = Math.random()\n  public lastSendMessage: any\n\n  public userId: any = null\n  public serverData: any\n\n  constructor (private handshakeData: any) {\n    super()\n  }\n\n  public sendAckMessage (message: Message) {\n    this.lastSendMessage = message\n  }\n\n  public getHandshakeData () {\n    return this.handshakeData\n  }\n\n  public sendError (/* topic, type, msg */) {\n  }\n\n  public sendMessage (message: Message) {\n    this.lastSendMessage = message\n  }\n\n  public parseData (message: Message) {\n    if (message.parsedData || !message.data) {\n      return null\n    }\n    try {\n      message.parsedData = JSON.parse(message.data.toString())\n      return true\n    } catch (e) {\n      return e\n    }\n  }\n\n  public send (/* message */) {\n  }\n\n  public destroy () {\n    this.authCallBack = null\n    this.isClosed = true\n    this.emit('close', this)\n  }\n\n  public close () {\n    this.destroy()\n  }\n\n  public setUpHandshakeData () {\n    this.handshakeData = {\n      remoteAddress: 'remote@address'\n    }\n\n    return this.handshakeData\n  }\n}\n\nexport const createSocketWrapper = (options?: any) => new SocketWrapperMock(options)\n"
  },
  {
    "path": "src/test/mock/storage-mock.ts",
    "content": "import { DeepstreamStorage, DeepstreamCache, StorageWriteCallback, StorageReadCallback, DeepstreamPlugin } from '@deepstream/types'\nimport { JSONObject } from '../../constants'\n\nexport default class StorageMock extends DeepstreamPlugin implements DeepstreamStorage, DeepstreamCache  {\n  public values = new Map<string, { version: number, value: JSONObject }>()\n  public failNextSet: boolean = false\n  public nextOperationWillBeSuccessful: boolean = true\n  public nextOperationWillBeSynchronous: boolean = true\n  public nextGetWillBeSynchronous: boolean = true\n  public lastGetCallback: Function | null = null\n  public lastRequestedKey: string | null = null\n  public lastSetKey: string | null = null\n  public lastSetVersion: number | null = null\n  public lastSetValue: object | null = null\n  public completedSetOperations: any\n  public completedDeleteOperations: any\n  public getCalls: any\n  public setTimeout: any\n  public getTimeout: any\n  public description: string = 'Mock Storage'\n\nconstructor () {\n    super()\n    this.reset()\n  }\n\n  public reset () {\n    this.values.clear()\n    this.failNextSet = false\n    this.nextOperationWillBeSuccessful = true\n    this.nextOperationWillBeSynchronous = true\n    this.nextGetWillBeSynchronous = true\n    this.lastGetCallback = null\n    this.lastRequestedKey = null\n    this.lastSetKey = null\n    this.lastSetVersion = null\n    this.lastSetValue = null\n    this.completedSetOperations = 0\n    this.completedDeleteOperations = 0\n    this.getCalls = []\n    clearTimeout(this.getTimeout)\n    clearTimeout(this.setTimeout)\n  }\n\n  public head (recordName: string, callback: any): void {\n    throw new Error('Method not implemented.')\n  }\n\n  public headBulk (recordNames: string[], callback: any): void {\n    throw new Error('Method not implemented.')\n  }\n\n  public deleteBulk (keys: string[], callback: StorageWriteCallback) {\n    if (this.nextOperationWillBeSynchronous) {\n      this.completedDeleteOperations++\n      if (this.nextOperationWillBeSuccessful) {\n        keys.forEach((key) => this.values.delete(key))\n        callback(null)\n      } else {\n        callback('storageError')\n        return\n      }\n    } else {\n      setTimeout(() => {\n        this.completedDeleteOperations++\n        callback(this.nextOperationWillBeSuccessful ? null : 'storageError')\n      }, 10)\n    }\n  }\n\n  public delete (key: string, callback: StorageWriteCallback) {\n    if (this.nextOperationWillBeSynchronous) {\n      this.completedDeleteOperations++\n      if (this.nextOperationWillBeSuccessful) {\n        this.values.delete(key)\n        callback(null)\n      } else {\n        callback('storageError')\n        return\n      }\n    } else {\n      setTimeout(() => {\n        this.completedDeleteOperations++\n        callback(this.nextOperationWillBeSuccessful ? null : 'storageError')\n      }, 10)\n    }\n  }\n\n  public hadGetFor (key: string) {\n    for (let i = 0; i < this.getCalls.length; i++) {\n      if (this.getCalls[i][0] === key) {\n        return true\n      }\n    }\n\n    return false\n  }\n\n  public triggerLastGetCallback (errorMessage: string, value: JSONObject) {\n    if (this.lastGetCallback) {\n      this.lastGetCallback(errorMessage, value)\n    }\n  }\n\n  public get (key: string, callback: StorageReadCallback) {\n    this.getCalls.push(arguments)\n    this.lastGetCallback = callback\n    this.lastRequestedKey = key\n    const set = this.values.get(key) || {\n      version: -1,\n      value: null\n    }\n\n    if (this.nextGetWillBeSynchronous === true) {\n      callback(this.nextOperationWillBeSuccessful ? null : 'storageError', set.version !== undefined ? set.version : -1, set.value ? Object.assign({}, set.value) : null)\n    } else {\n      this.getTimeout = setTimeout(() => {\n        callback(this.nextOperationWillBeSuccessful ? null : 'storageError', set.version !== undefined ? set.version : -1, set.value ? Object.assign({}, set.value) : null)\n      }, 25)\n    }\n  }\n\n  public set (key: string, version: number, value: JSONObject, callback: StorageWriteCallback) {\n    const set = { version, value }\n\n    this.lastSetKey = key\n    this.lastSetVersion = version\n    this.lastSetValue = value\n\n    if (this.nextOperationWillBeSuccessful) {\n      this.values.set(key, set)\n    }\n\n    if (this.nextOperationWillBeSynchronous) {\n      this.completedSetOperations++\n      if (this.failNextSet) {\n        this.failNextSet = false\n        callback('storageError')\n        return\n      }\n      callback(this.nextOperationWillBeSuccessful ? null : 'storageError')\n    } else {\n      this.setTimeout = setTimeout(() => {\n        this.completedSetOperations++\n        callback(this.nextOperationWillBeSuccessful ? null : 'storageError')\n      }, 50)\n    }\n  }\n}\n"
  },
  {
    "path": "src/utils/dependency-initialiser.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport { spy } from 'sinon'\nimport { DependencyInitialiser } from './dependency-initialiser'\nimport PluginMock from '../test/mock/plugin-mock'\nimport LoggerMock from '../test/mock/logger-mock'\nimport { LOG_LEVEL, EVENT } from '@deepstream/types';\nimport { PromiseDelay } from './utils';\n\nconst services = {\n  logger: new LoggerMock()\n}\n\ndescribe('dependency-initialiser', () => {\n  let dependencyBInitialiser: DependencyInitialiser\n  let config: any\n\n  beforeEach(() => {\n    config = {\n      pluginA: new PluginMock({ name:'A' }),\n      pluginB: new PluginMock({ name: 'B'}),\n      pluginC: new PluginMock({ name: 'C'}),\n      brokenPlugin: {},\n      dependencyInitializationTimeout: 50\n    }\n    services.logger.lastLogEvent = null\n  })\n\n  it ('sets description', () => {\n    dependencyBInitialiser = new DependencyInitialiser(config as any, services as any, config.pluginB, 'pluginB')\n    expect(dependencyBInitialiser.getDependency().description).to.equal('B')\n    expect(services.logger.lastLogEvent).to.equal(null)\n  })\n\n  it('throws an error if dependency doesnt implement emitter or has isReady', () => {\n    expect(() => {\n      // tslint:disable-next-line:no-unused-expression\n      new DependencyInitialiser(config as any, services as any, {} as any, 'brokenPlugin')\n    }).to.throw()\n    expect(services.logger.lastLogEvent).to.equal(EVENT.PLUGIN_INITIALIZATION_ERROR)\n  })\n\n  it('notifies when the plugin is ready with when already ready', async () => {\n    config.pluginB.isReady = true\n    dependencyBInitialiser = new DependencyInitialiser(config as any, services as any, config.pluginB, 'pluginB')\n    await dependencyBInitialiser.whenReady()\n    expect(services.logger.lastLogEvent).to.equal(EVENT.INFO)\n  })\n\n  it('notifies when the plugin is ready with when not ready', (done) => {\n    dependencyBInitialiser = new DependencyInitialiser(config as any, services as any, config.pluginB, 'pluginB')\n    dependencyBInitialiser.whenReady().then(() => {\n      expect(services.logger.lastLogEvent).to.equal(EVENT.INFO)\n      done()\n    })\n    config.pluginB.setReady()\n  })\n})\n\ndescribe('encounters timeouts and errors during dependency initialisations', () => {\n  let dependencyInitialiser\n  const onReady = spy()\n  const originalConsoleLog = console.log\n  const config = {\n    plugin: new PluginMock('A'),\n    dependencyInitializationTimeout: 1,\n  }\n\n  it('disables console.error', () => {\n    Object.defineProperty(console, 'error', {\n      value: services.logger.log\n    })\n  })\n\n  it(\"creates a dependency initialiser and doesn't initialize a plugin in time\", async () => {\n    services.logger.logSpy.resetHistory()\n    dependencyInitialiser = new DependencyInitialiser(config as any, services as any, config.plugin, 'plugin')\n\n    await PromiseDelay(20)\n\n    dependencyInitialiser.whenReady().then(onReady)\n    expect(config.plugin.isReady).to.equal(false)\n    expect(onReady).to.have.callCount(0)\n\n    // expect(services.logger.logSpy).to.have.been.calledOnce // another test isn't async and bleeds into this one\n    expect(services.logger.logSpy).to.have.been.calledWith(LOG_LEVEL.FATAL, EVENT.PLUGIN_INITIALIZATION_TIMEOUT, 'plugin wasn\\'t initialised in time')\n  })\n\n  it.skip('creates another depdendency initialiser with a plugin error', async () => {\n    process.once('uncaughtException', () => {\n      expect(onReady).to.have.callCount(0)\n      expect(services.logger.logSpy).to.have.been.calledWith('Error while initialising dependency')\n      expect(services.logger.logSpy).to.have.been.calledWith('Error while initialising plugin: something went wrong')\n      next()\n    })\n    dependencyInitialiser = new DependencyInitialiser({}, config as any, services as any, config.plugin, 'plugin')\n    dependencyInitialiser.on('ready', onReady)\n    try {\n      config.plugin.emit('error', 'something went wrong')\n      next('Fail')\n    } catch (err) {}\n  })\n\n  it('enable console.error', () => {\n    Object.defineProperty(console, 'error', {\n      value: originalConsoleLog\n    })\n  })\n})\n"
  },
  {
    "path": "src/utils/dependency-initialiser.ts",
    "content": "import { DeepstreamConfig, DeepstreamServices, DeepstreamPlugin, EVENT } from '@deepstream/types'\nimport { EventEmitter } from 'events'\n\nexport class DependencyInitialiser {\n  private isReady: boolean = false\n  private timeout: NodeJS.Timeout | null = null\n  private emitter = new EventEmitter()\n\n/**\n * This class is used to track the initialization of an individual service or plugin\n */\n  constructor (private config: DeepstreamConfig, private services: DeepstreamServices, private dependency: DeepstreamPlugin, private name: string) {\n    if (typeof this.dependency.whenReady !== 'function') {\n      const errorMessage = `${this.name} needs to implement async whenReady and close, please look at the DeepstreamPlugin API here` // TODO: Insert link\n      this.services.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, errorMessage)\n      this.services.notifyFatalException()\n      return\n    }\n\n    this.timeout = setTimeout(\n      this.onTimeout.bind(this),\n      this.config.dependencyInitializationTimeout,\n    )\n\n    if (this.dependency.init) {\n      this.dependency.init()\n    }\n\n    this.dependency\n      .whenReady()\n      .then(this.onReady.bind(this))\n  }\n\n  public async whenReady (): Promise<void> {\n    if (!this.isReady) {\n      return new Promise((resolve) => this.emitter.once('ready', resolve))\n    }\n  }\n\n/**\n * Returns the underlying dependency (e.g. the Logger, StorageConnector etc.)\n */\n  public getDependency (): DeepstreamPlugin {\n    return this.dependency\n  }\n\n/**\n * Callback for succesfully initialised dependencies\n */\n  private onReady (): void {\n    if (this.timeout) {\n      clearTimeout(this.timeout)\n    }\n\n    this.dependency.description = this.dependency.description || (this.dependency as any).type\n    const dependencyType = this.dependency.description ? `: ${this.dependency.description}` : ': no dependency description provided'\n    this.services.logger.info(EVENT.INFO, `${this.name} ready${dependencyType}`)\n\n    this.isReady = true\n    this.emitter.emit('ready')\n  }\n\n/**\n * Callback for dependencies that weren't initialised in time\n */\n  private onTimeout (): void {\n    const message = `${this.name} wasn't initialised in time`\n\n    if (this.name === 'logger') {\n      console.error('Error while initialising log dependency dependency')\n      console.error(message)\n      this.services.notifyFatalException()\n    }\n\n    this.services.logger.fatal(EVENT.PLUGIN_INITIALIZATION_TIMEOUT, message)\n  }\n}\n"
  },
  {
    "path": "src/utils/json-path.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport * as jsonPath from './json-path'\n\ndescribe('objects are created from paths and their value is set correctly', () => {\n  it('sets simple values', () => {\n    const record = {}\n    jsonPath.setValue(record, 'firstname', 'Wolfram')\n    expect(record).to.deep.equal({ firstname: 'Wolfram' })\n  })\n\n  it('setting a value to undefined deletes it', () => {\n    const record = { a: 1, b: 2 }\n    jsonPath.setValue(record, 'a', undefined)\n    expect(record).to.deep.equal({ b: 2 })\n  })\n\n\n  it('sets values for nested objects', () => {\n    const record = {}\n    jsonPath.setValue(record, 'address.street', 'someStreet')\n\n    expect(record).to.deep.equal({\n      address: {\n        street: 'someStreet'\n      }\n    })\n  })\n\n  it('sets values for nested objects with numeric field names', () => {\n    const record = {}\n    jsonPath.setValue(record, 'address.street.1', 'someStreet')\n\n    expect(record).to.deep.equal({\n      address: {\n        street: {\n          1: 'someStreet'\n        }\n      }\n    })\n  })\n\n  it('sets values for nested objects with multiple numeric field names', () => {\n    const record = {}\n    jsonPath.setValue(record, 'address.99.street.1', 'someStreet')\n\n    expect(record).to.deep.equal({\n      address: {\n        99 : {\n          street: {\n            1: 'someStreet'\n          }\n        }\n      }\n    })\n  })\n\n  it('sets values for nested objects with multiple mixed array and numeric field names', () => {\n    const record = {}\n    jsonPath.setValue(record, 'address[2].99.street[2].1', 'someStreet')\n\n    expect(record).to.deep.equal({\n      address: [\n        undefined,\n        undefined,\n        {\n          99 : {\n            street: [\n              undefined,\n              undefined,\n              {\n                1: 'someStreet'\n              }\n            ]\n          }\n        }\n      ]\n    })\n  })\n\n  it('sets first value of array', () => {\n    const record = {}\n    jsonPath.setValue(record, 'items[0]', 51)\n\n    expect(JSON.stringify(record)).to.deep.equal(JSON.stringify({\n      items: [\n        51\n      ]\n    }))\n  })\n\n  it('sets numeric obj member name of 0 (zero)', () => {\n    const record = {}\n    jsonPath.setValue(record, 'items.0', 51)\n\n    expect(JSON.stringify(record)).to.deep.equal(JSON.stringify({\n      items: {\n        0 : 51\n      }\n    }))\n  })\n\n  it('sets values for arrays', () => {\n    const record = {}\n    jsonPath.setValue(record, 'pastAddresses[1].street', 'someStreet')\n\n    expect(JSON.stringify(record)).to.deep.equal(JSON.stringify({\n      pastAddresses: [\n        undefined,\n        {\n          street: 'someStreet'\n        }\n      ]\n    }))\n  })\n\n  it('sets value AS arrays of arrays', () => {\n    const record = {\n      addresses: undefined\n    }\n    const arrOfArr = [\n      undefined,\n      [\n        'new-Street1', 'road1', 'blvd1'\n      ],\n      [\n        'street2', 'road2', 'blvd2'\n      ]\n    ]\n\n    jsonPath.setValue(record, 'addresses', arrOfArr)\n\n    expect(JSON.stringify(record)).to.deep.equal(JSON.stringify({\n      addresses: [\n        undefined,\n        [\n          'new-Street1', 'road1', 'blvd1'\n        ],\n        [\n          'street2', 'road2', 'blvd2'\n        ]\n      ]\n    }))\n  })\n\n  it('sets value IN arrays of arrays', () => {\n    const record = {\n      addresses: [\n        undefined,\n        [\n          'street1', 'road1', 'blvd1'\n        ],\n        [\n          'street2', 'road2', 'blvd2'\n        ]\n      ]\n    }\n    jsonPath.setValue(record, 'addresses[1][0]', 'new-Street1')\n\n    expect(JSON.stringify(record)).to.deep.equal(JSON.stringify({\n      addresses: [\n        undefined,\n        [\n          'new-Street1', 'road1', 'blvd1'\n        ],\n        [\n          'street2', 'road2', 'blvd2'\n        ]\n      ]\n    }))\n  })\n\n  it('sets value IN deeper nested multi-dimensional arrays of arrays', () => {\n    const record = {\n      obj: {\n        101 : {\n          addresses: [\n            [\n              undefined,\n              [\n                undefined,\n                ['street1', 'road1', 'blvd1'],\n                ['street2', 'road2', 'blvd2']\n              ],\n              [\n                undefined,\n                { a: 'street1', b: 'road1', c: 'blvd1' },\n                { 1: 'street2', 2: 'road2', 3: 'blvd2' }\n              ]\n            ],\n            undefined,\n            [[0, 1, 2, 3], [9, 8, 7, 6], [2, 4, 6, 8]]\n          ]\n        }\n      }\n    }\n    jsonPath.setValue(record, 'obj.101.addresses[0][1][1][0]', 'new-Street1')\n\n    expect(JSON.stringify(record)).to.deep.equal(JSON.stringify({\n      obj: {\n        101 : {\n          addresses: [\n            [\n              undefined,\n              [\n                undefined,\n                  ['new-Street1', 'road1', 'blvd1'],\n                  ['street2', 'road2', 'blvd2']\n              ],\n              [\n                undefined,\n                  { a: 'street1', b: 'road1', c: 'blvd1' },\n                  { 1: 'street2', 2: 'road2', 3: 'blvd2' }\n              ]\n            ],\n            undefined,\n              [[0, 1, 2, 3], [9, 8, 7, 6], [2, 4, 6, 8]]\n          ]\n        }\n      }\n    }))\n  })\n\n  it('extends existing objects', () => {\n    const record = { firstname: 'Wolfram' }\n    jsonPath.setValue(record, 'lastname', 'Hempel')\n\n    expect(record).to.deep.equal({\n      firstname: 'Wolfram',\n      lastname: 'Hempel'\n    } as any)\n  })\n\n  it('extends existing arrays', () => {\n    const record = {\n      firstname: 'Wolfram',\n      animals: ['Bear', 'Cow', 'Ostrich']\n    }\n    jsonPath.setValue(record, 'animals[ 1 ]', 'Emu')\n\n    expect(record).to.deep.equal({\n      firstname: 'Wolfram',\n      animals: ['Bear', 'Emu', 'Ostrich']\n    })\n  })\n\n  it('extends existing arrays with empty slot assigned a primitive', () => {\n    const record = {\n      firstname: 'Wolfram',\n      animals: [undefined, 'Cow', 'Ostrich']\n    }\n    jsonPath.setValue(record, 'animals[0]', 'Emu')\n\n    expect(record).to.deep.equal({\n      firstname: 'Wolfram',\n      animals: ['Emu', 'Cow', 'Ostrich']\n    })\n  })\n\n  it('extends existing arrays with objects', () => {\n    const record = {\n      firstname: 'Wolfram',\n      animals: [undefined, 'Cow', 'Ostrich']\n    }\n    jsonPath.setValue(record, 'animals[0].xxx', 'Emu')\n\n    expect(record).to.deep.equal({\n      firstname: 'Wolfram',\n      animals: [{ xxx: 'Emu' }, 'Cow', 'Ostrich']\n    } as any)\n  })\n\n  it('treats numbers with the path such as .0. as a key value', () => {\n    const record = {}\n    jsonPath.setValue(record, 'animals.0.name', 'Emu')\n\n    expect(record).to.deep.equal({\n      animals: {\n        0: {\n          name: 'Emu'\n        }\n      }\n    })\n  })\n\n  it('treats numbers with the path such as [0] as an index value', () => {\n    const record = {}\n    jsonPath.setValue(record, 'animals[0].name', 'Emu')\n\n    expect(record).to.deep.equal({\n      animals: [{\n        name: 'Emu'\n      }]\n    })\n  })\n\n  it('handles .xyz paths into non-objects', () => {\n    const record = { animals: 3 }\n    jsonPath.setValue(record, 'animals.name', 'Emu')\n\n    expect(record).to.deep.equal({\n      animals: {\n        name: 'Emu'\n      }\n    } as any)\n  })\n\n  it('handles .xyz paths through non-objects', () => {\n    const record = { animals: 3 }\n    jsonPath.setValue(record, 'animals.name.length', 7)\n\n    expect(record).to.deep.equal({\n      animals: {\n        name: {\n          length: 7\n        }\n      }\n    } as any)\n  })\n\n  it('handles [0] paths into non-objects', () => {\n    const record = { animals: 3 }\n    jsonPath.setValue(record, 'animals[0]', 7)\n\n    expect(record).to.deep.equal({\n      animals: [7]\n    } as any)\n  })\n\n})\n"
  },
  {
    "path": "src/utils/json-path.ts",
    "content": "const SPLIT_REG_EXP = /[[\\]]/g\n\n/**\n* Returns the value of the path or\n* undefined if the path can't be resolved\n*/\nexport function getValue (data: any, path: string): any {\n  const tokens = tokenize(path)\n  let value = data\n  for (let i = 0; i < tokens.length; i++) {\n    if (value === undefined) {\n      return undefined\n    }\n    if (typeof value !== 'object') {\n      throw new Error('invalid data or path')\n    }\n    value = value[tokens[i]]\n  }\n\n  return value\n }\n\n/**\n * This class allows to set or get specific\n * values within a json data structure using\n * string-based paths\n */\nexport function setValue (root: any, path: string, value: any): void {\n  const tokens = tokenize(path)\n  let node = root\n\n  let i\n  for (i = 0; i < tokens.length - 1; i++) {\n    const token = tokens[i]\n\n    if (node[token] !== undefined && typeof node[token] === 'object') {\n      node = node[token]\n    } else if (typeof tokens[i + 1] === 'number') {\n      node = node[token] = []\n    } else {\n      node = node[token] = {}\n    }\n  }\n\n  if (value === undefined) {\n    delete node[tokens[i]]\n  } else {\n    node[tokens[i]] = value\n  }\n}\n\n/**\n * Parses the path. Splits it into\n * keys for objects and indices for arrays.\n */\nfunction tokenize (path: string): Array<string | number> {\n  const tokens: Array<string | number> = []\n\n  const parts = path.split('.')\n\n  for (let i = 0; i < parts.length; i++) {\n    const part = parts[i].trim()\n\n    if (part.length === 0) {\n      continue\n    }\n\n    const arrayIndexes: string[] = part.split(SPLIT_REG_EXP)\n\n    if (arrayIndexes.length === 0) {\n      // TODO\n      continue\n    }\n\n    tokens.push(arrayIndexes[0])\n\n    for (let j = 1; j < arrayIndexes.length; j++) {\n      if (arrayIndexes[j].length === 0) {\n        continue\n      }\n\n      tokens.push(Number(arrayIndexes[j]))\n    }\n  }\n  return tokens\n}\n"
  },
  {
    "path": "src/utils/message-distributor.spec.ts",
    "content": "import {spy} from 'sinon'\nimport {expect} from 'chai'\n\nimport MessageDistributor from './message-distributor'\nimport * as testHelper from '../test/helper/test-helper'\nimport { getTestMocks } from '../test/helper/test-mocks'\n\nconst options = testHelper.getDeepstreamOptions()\nconst config = options.config\nconst services = options.services\n\ndescribe('message connector distributes messages to callbacks', () => {\n  let messageDistributor\n  let testMocks\n  let client\n  let testCallback\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    client = testMocks.getSocketWrapper()\n    testCallback = spy()\n\n    messageDistributor = new MessageDistributor(config, services)\n  })\n\n  afterEach(() => {\n    client.socketWrapperMock.verify()\n  })\n\n  it('makes remote connection', () => {\n    expect(services.clusterNode.lastSubscribedTopic).to.equal(null)\n\n    messageDistributor.registerForTopic('someTopic', testCallback)\n\n    expect(services.clusterNode.lastSubscribedTopic).to.equal('someTopic')\n  })\n\n  it('makes local connection', () => {\n    messageDistributor.registerForTopic('someTopic', testCallback)\n\n    messageDistributor.distribute(client.socketWrapper, { topic: 'someTopic' })\n\n    expect(testCallback).to.have.callCount(1)\n  })\n\n  it.skip('routes messages from the message connector', () => {\n    messageDistributor.registerForTopic('topicB', testCallback)\n\n    services.message.simulateIncomingMessage('topicB', { topic: 'topicB' })\n\n    expect(testCallback).to.have.callCount(1)\n  })\n\n  it('only routes matching topics', () => {\n    messageDistributor.registerForTopic('aTopic', testCallback)\n    messageDistributor.registerForTopic('anotherTopic', testCallback)\n\n    messageDistributor.distribute(client.socketWrapper, { topic: 'aTopic' })\n\n    expect(testCallback).to.have.callCount(1)\n  })\n\n  it('throws an error for multiple registrations to the same topic', () => {\n    let hasErrored = false\n\n    try {\n      messageDistributor.registerForTopic('someTopic', testCallback)\n      messageDistributor.registerForTopic('someTopic', testCallback)\n    } catch (e) {\n      hasErrored = true\n    }\n\n    expect(hasErrored).to.equal(true)\n  })\n})\n"
  },
  {
    "path": "src/utils/message-distributor.ts",
    "content": "import { PARSER_ACTION, TOPIC, Message, STATE_REGISTRY_TOPIC } from '../constants'\nimport { SocketWrapper, DeepstreamServices, DeepstreamConfig } from '@deepstream/types'\n\n/**\n * The MessageDistributor routes valid and permissioned messages to\n * various, previously registered handlers, e.g. event-, rpc- or recordHandler\n */\nexport default class MessageDistributor {\n  private callbacks = new Map<TOPIC | STATE_REGISTRY_TOPIC, Function>()\n\n  constructor (options: DeepstreamConfig, private services: DeepstreamServices) {}\n\n  /**\n   * Accepts a socketWrapper and a parsed message as input and distributes\n   * it to its subscriber, based on the message's topic\n   */\n  public distribute (socketWrapper: SocketWrapper, message: Message) {\n    const callback = this.callbacks.get(message.topic)\n    if (callback === undefined) {\n      this.services.logger.warn(PARSER_ACTION[PARSER_ACTION.UNKNOWN_TOPIC], TOPIC[message.topic], { message })\n      socketWrapper.sendMessage({\n        topic: TOPIC.PARSER,\n        action: PARSER_ACTION.UNKNOWN_TOPIC,\n        originalTopic: message.topic\n      })\n      return\n    }\n    this.services.monitoring.onMessageReceived(message, socketWrapper)\n    callback(socketWrapper, message)\n  }\n\n  /**\n   * Allows handlers (event, rpc, record) to register for topics. Subscribes them\n   * to both messages passed to the distribute method as well as messages received\n   * from the messageConnector\n   */\n  public registerForTopic (topic: TOPIC, callback: (message: Message) => void) {\n    if (this.callbacks.has(topic)) {\n      throw new Error(`Callback already registered for topic ${topic}`)\n    }\n\n    this.callbacks.set(topic, callback)\n    this.services.clusterNode.subscribe(\n      topic,\n      this.onMessageConnectorMessage.bind(this, callback),\n    )\n  }\n\n  /**\n   * Whenever a message from the messageConnector is received it is passed\n   * to the relevant handler, but with null instead of\n   * a socketWrapper as sender\n   */\n  private onMessageConnectorMessage (callback: Function, message: Message, originServer: string) {\n    callback(null, message, originServer)\n  }\n}\n"
  },
  {
    "path": "src/utils/message-processor.spec.ts",
    "content": "import {expect} from 'chai'\nimport PermissionHandlerMock from '../test/mock/permission-handler-mock'\nimport MessageProcessor from './message-processor'\nimport LoggerMock from '../test/mock/logger-mock'\nimport { getTestMocks } from '../test/helper/test-mocks'\nimport { TOPIC, CONNECTION_ACTION, RPC_ACTION, RECORD_ACTION } from '../constants';\n\nlet messageProcessor\nlet log\nlet lastAuthenticatedMessage = null\n\ndescribe('the message processor only forwards valid, authorized messages', () => {\n  let testMocks\n  let client\n  let permissionMock\n\n  const message = {\n    topic: TOPIC.RECORD,\n    action: RECORD_ACTION.READ,\n    name: 'record/name'\n  }\n\n  beforeEach(() => {\n    testMocks = getTestMocks()\n    client = testMocks.getSocketWrapper('someUser')\n    permissionMock  = new PermissionHandlerMock()\n    const loggerMock = new LoggerMock()\n    log = loggerMock.logSpy\n    messageProcessor = new MessageProcessor({}, {\n      permission: permissionMock,\n      logger: loggerMock\n    })\n    messageProcessor.onAuthenticatedMessage = function (socketWrapper, authenticatedMessage) {\n      lastAuthenticatedMessage = authenticatedMessage\n    }\n  })\n\n  afterEach(() => {\n    client.socketWrapperMock.verify()\n  })\n\n  it('handles permission errors', () => {\n    permissionMock.nextCanPerformActionResult = 'someError'\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: TOPIC.RECORD,\n        action: RECORD_ACTION.MESSAGE_PERMISSION_ERROR,\n        originalAction: RECORD_ACTION.READ,\n        name: message.name,\n        isError: true\n      })\n\n    messageProcessor.process(client.socketWrapper, [message])\n\n    expect(log).to.have.callCount(1)\n    expect(log).to.have.been.calledWith(2, RECORD_ACTION[RECORD_ACTION.MESSAGE_PERMISSION_ERROR], 'someError')\n  })\n\n  it('rpc permission errors have a correlation id', () => {\n    permissionMock.nextCanPerformActionResult = 'someError'\n    const rpcMessage = {\n      topic: TOPIC.RPC,\n      action: RPC_ACTION.REQUEST,\n      name: 'myRPC',\n      correlationId: '1234567890',\n      data: Buffer.from('{}', 'utf8'),\n      parsedData: {}\n    }\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: TOPIC.RPC,\n        action: RPC_ACTION.MESSAGE_PERMISSION_ERROR,\n        originalAction: rpcMessage.action,\n        name: rpcMessage.name,\n        correlationId: rpcMessage.correlationId,\n        isError: true\n      })\n\n    messageProcessor.process(client.socketWrapper, [rpcMessage])\n\n    expect(log).to.have.callCount(1)\n    expect(log).to.have.been.calledWith(2, RPC_ACTION[RPC_ACTION.MESSAGE_PERMISSION_ERROR], 'someError')\n  })\n\n  it('handles denied messages', () => {\n    permissionMock.nextCanPerformActionResult = false\n\n    client.socketWrapperMock\n      .expects('sendMessage')\n      .once()\n      .withExactArgs({\n        topic: TOPIC.RECORD,\n        action: RECORD_ACTION.MESSAGE_DENIED,\n        originalAction: RECORD_ACTION.READ,\n        name: message.name,\n        isError: true\n      })\n\n    messageProcessor.process(client.socketWrapper, [message])\n  })\n\n  it('provides the correct arguments to canPerformAction', () => {\n    permissionMock.nextCanPerformActionResult = false\n\n    messageProcessor.process(client.socketWrapper, [message])\n\n    expect(permissionMock.lastCanPerformActionQueryArgs.length).to.equal(4)\n    expect(permissionMock.lastCanPerformActionQueryArgs[0]).to.equal(client.socketWrapper)\n    expect(permissionMock.lastCanPerformActionQueryArgs[1]).to.deep.equal(message)\n    expect(permissionMock.lastCanPerformActionQueryArgs[3]).to.deep.equal({})\n  })\n\n  it('forwards validated and permissioned messages', () => {\n    permissionMock.nextCanPerformActionResult = true\n\n    messageProcessor.process(client.socketWrapper, [message])\n\n    expect(lastAuthenticatedMessage).to.equal(message as any)\n  })\n})\n"
  },
  {
    "path": "src/utils/message-processor.ts",
    "content": "import { TOPIC, CONNECTION_ACTION, Message, ALL_ACTIONS, ACTIONS, RECORD_ACTION  } from '../constants'\nimport { SocketWrapper, DeepstreamConfig, DeepstreamServices, EVENT } from '@deepstream/types'\nimport { getUid } from './utils'\n\n/**\n * The MessageProcessor consumes blocks of parsed messages emitted by the\n * ConnectionEndpoint, checks if they are permissioned and - if they\n * are - forwards them.\n */\nexport default class MessageProcessor {\n  private bulkResults = new Map<string, { total: number, completed: number }>()\n\n  constructor (config: DeepstreamConfig, private services: DeepstreamServices) {\n    this.onPermissionResponse = this.onPermissionResponse.bind(this)\n    this.onBulkPermissionResponse = this.onBulkPermissionResponse.bind(this)\n  }\n\n  /**\n   * There will only ever be one consumer of forwarded messages. So rather than using\n   * events - and their performance overhead - the messageProcessor exposes\n   * this method that's expected to be overwritten.\n   */\n  public onAuthenticatedMessage (socketWrapper: SocketWrapper, message: Message) {\n  }\n\n  /**\n   * This method is the way the message processor accepts input. It receives arrays\n   * of parsed messages, iterates through them and issues permission requests for\n   * each individual message\n   *\n   * @todo The responses from the permission service might arrive in any arbitrary order - order them\n   * @todo Handle permission handler timeouts\n   */\n  public process (socketWrapper: SocketWrapper, parsedMessages: Message[]): void {\n    const length = parsedMessages.length\n    for (let i = 0; i < length; i++) {\n      const message = parsedMessages[i]\n\n      if (message.topic === TOPIC.CONNECTION && message.action === CONNECTION_ACTION.PING) {\n        // respond to PING message\n        socketWrapper.sendMessage({ topic: TOPIC.CONNECTION, action: CONNECTION_ACTION.PONG })\n        continue\n      }\n\n      if (message.names && message.names.length > 0) {\n        const uuid = getUid()\n\n        if (this.bulkResults.has(uuid)) {\n          this.services.logger.error(EVENT.NOT_VALID_UUID, `Invalid uuid used twice ${uuid}`, { uuid })\n        }\n\n        this.bulkResults.set(uuid, {\n          total: message.names!.length,\n          completed: 0\n        })\n        const l = message.names!.length\n        for (let j = 0; j < l; j++) {\n          this.services.permission.canPerformAction(\n            socketWrapper,\n            { ...message, name: message.names![j] },\n            this.onBulkPermissionResponse,\n            { originalMessage: message, uuid }\n          )\n        }\n        continue\n      }\n\n      this.services.permission.canPerformAction(\n        socketWrapper,\n        message,\n        this.onPermissionResponse,\n        {}\n      )\n    }\n  }\n\n  private onBulkPermissionResponse (socketWrapper: SocketWrapper, message: Message, passItOn: any, error: ALL_ACTIONS | Error | string | null, result: boolean) {\n    const bulkResult = this.bulkResults.get(passItOn.uuid)!\n\n    if (error !== null || result === false) {\n      passItOn.originalMessage.names!.splice(passItOn.originalMessage.names!.indexOf(passItOn.originalMessage.name!), 1)\n      this.processInvalidResponse(socketWrapper, message, error, result)\n    }\n\n    if (bulkResult.total !== bulkResult.completed + 1) {\n      bulkResult.completed = bulkResult.completed + 1\n      return\n    }\n\n    this.bulkResults.delete(passItOn.uuid)\n\n    if (message.names!.length > 0) {\n      this.onAuthenticatedMessage(socketWrapper, passItOn.originalMessage)\n    }\n  }\n\n  /**\n   * Processes the response that's returned by the permission service.\n   */\n  private onPermissionResponse (socketWrapper: SocketWrapper, message: Message, passItOn: any, error: ALL_ACTIONS | Error | string | null, result: boolean): void {\n    if (error !== null || result === false) {\n      this.processInvalidResponse(socketWrapper, message, error, result)\n    } else {\n      this.onAuthenticatedMessage(socketWrapper, message)\n    }\n  }\n\n  private processInvalidResponse (socketWrapper: SocketWrapper, message: Message, error: ALL_ACTIONS | Error | string | null, result: boolean) {\n    if (error !== null) {\n      this.services.logger.warn(RECORD_ACTION[RECORD_ACTION.MESSAGE_PERMISSION_ERROR], error.toString(), { message })\n      const permissionErrorMessage: Message = {\n        topic: message.topic,\n        action: ACTIONS[message.topic].MESSAGE_PERMISSION_ERROR,\n        originalAction: message.action,\n        name: message.name,\n        isError: true\n      }\n      if (message.correlationId) {\n        permissionErrorMessage.correlationId = message.correlationId\n      }\n      if (message.isWriteAck) {\n        permissionErrorMessage.isWriteAck = true\n      }\n\n      socketWrapper.sendMessage(permissionErrorMessage)\n      return\n    }\n\n    if (result !== true) {\n      const permissionDeniedMessage: Message = {\n        topic: message.topic,\n        action: ACTIONS[message.topic].MESSAGE_DENIED,\n        originalAction: message.action,\n        name: message.name,\n        isError: true\n      }\n      if (message.correlationId) {\n        permissionDeniedMessage.correlationId = message.correlationId\n      }\n      if (message.isWriteAck) {\n        permissionDeniedMessage.isWriteAck = true\n      }\n\n      socketWrapper.sendMessage(permissionDeniedMessage)\n      return\n    }\n  }\n}\n"
  },
  {
    "path": "src/utils/utils.spec.ts",
    "content": "import 'mocha'\nimport {spy} from 'sinon'\nimport {expect} from 'chai'\nimport { EventEmitter } from 'events'\nimport { createHash } from './utils';\nconst utils = require('./utils')\n\ndescribe('utils', () => {\n  it('receives a different value everytime getUid is called', () => {\n    const uidA = utils.getUid()\n    const uidB = utils.getUid()\n    const uidC = utils.getUid()\n\n    expect(uidA).not.to.equal(uidB)\n    expect(uidB).not.to.equal(uidC)\n    expect(uidA).not.to.equal(uidC)\n  })\n\n  it('reverses maps', () => {\n    const user = {\n      firstname: 'Wolfram',\n      lastname: 'Hempel'\n    }\n\n    expect(utils.reverseMap(user)).to.deep.equal({\n      Wolfram: 'firstname',\n      Hempel: 'lastname'\n    })\n  })\n\n  describe('isOfType', () => {\n    it('checks basic types', () => {\n      expect(utils.isOfType('bla', 'string')).to.equal(true)\n      expect(utils.isOfType(42, 'string')).to.equal(false)\n      expect(utils.isOfType(42, 'number')).to.equal(true)\n      expect(utils.isOfType(true, 'number')).to.equal(false)\n      expect(utils.isOfType(true, 'boolean')).to.equal(true)\n      expect(utils.isOfType({}, 'object')).to.equal(true)\n      expect(utils.isOfType(null, 'null')).to.equal(true)\n      expect(utils.isOfType(null, 'object')).to.equal(false)\n      expect(utils.isOfType([], 'object')).to.equal(true)\n    })\n\n    it('checks urls', () => {\n      expect(utils.isOfType('bla', 'url')).to.equal(false)\n      expect(utils.isOfType('bla:22', 'url')).to.equal(true)\n      expect(utils.isOfType('https://deepstream.io/', 'url')).to.equal(true)\n    })\n\n    it('checks arrays', () => {\n      expect(utils.isOfType([], 'array')).to.equal(true)\n      expect(utils.isOfType({}, 'array')).to.equal(false)\n    })\n  })\n\n  describe('validateMap', () => {\n    function _map () {\n      return {\n        'a-string': 'bla',\n        'a number': 42,\n        'an array': ['yup']\n      }\n    }\n\n    function _schema () {\n      return {\n        'a-string': 'string',\n        'a number': 'number',\n        'an array': 'array'\n      }\n    }\n\n    it('validates basic maps', () => {\n      const map = _map()\n      const schema = _schema()\n      expect(utils.validateMap(map, false, schema)).to.equal(true)\n    })\n\n    it('fails validating an incorrect map', () => {\n      const map = _map()\n      const schema = _schema()\n      schema['an array'] = 'number'\n      const returnValue = utils.validateMap(map, false, schema)\n      expect(returnValue instanceof Error).to.equal(true)\n    })\n\n    it('fails validating an incomplete map', () => {\n      const map = _map()\n      const schema = _schema()\n      delete map['an array']\n      const returnValue = utils.validateMap(map, false, schema)\n      expect(returnValue instanceof Error).to.equal(true)\n    })\n\n    it('throws errors', () => {\n      const map = _map()\n      const schema = _schema()\n      schema['an array'] = 'number'\n      expect(() => {\n        utils.validateMap(map, true, schema)\n      }).to.throw()\n    })\n  })\n\n  describe('merges recoursively', () => {\n    it('merges two simple objects', () => {\n      const objA = {\n        firstname: 'Homer',\n        lastname: 'Simpson'\n      }\n\n      const objB = {\n        firstname: 'Marge'\n      }\n\n      expect(utils.merge(objA, objB)).to.deep.equal({\n        firstname: 'Marge',\n        lastname: 'Simpson'\n      })\n    })\n\n    it('merges two nested objects', () => {\n      const objA = {\n        firstname: 'Homer',\n        lastname: 'Simpson',\n        children: {\n          Bart: {\n            lastname: 'Simpson'\n          }\n        }\n      }\n\n      const objB = {\n        firstname: 'Marge',\n        children: {\n          Bart: {\n            firstname: 'Bart'\n          }\n        }\n      }\n\n      expect(utils.merge(objA, objB)).to.deep.equal({\n        firstname: 'Marge',\n        lastname: 'Simpson',\n        children: {\n          Bart: {\n            firstname: 'Bart',\n            lastname: 'Simpson'\n          }\n        }\n      })\n    })\n\n    it('merges multiple objects ', () => {\n      const objA = {\n        pets: {\n          birds: ['parrot', 'dove']\n        }\n\n      }\n\n      const objB = {\n        jobs: {\n          hunter: false\n        }\n      }\n\n      const objC = {\n        firstname: 'Egon'\n      }\n\n      expect(utils.merge(objA, objB, {}, objC)).to.deep.equal({\n        pets: {\n          birds: ['parrot', 'dove']\n        },\n        jobs: {\n          hunter: false\n        },\n        firstname: 'Egon'\n      })\n    })\n\n    it('handles null and undefined values', () => {\n      const objA = {\n        pets: {\n          dog: 1,\n          cat: 2,\n          ape: 3\n        }\n\n      }\n\n      const objB = {\n        pets: {\n          cat: null,\n          ape: undefined,\n          zebra: 9\n        }\n      }\n\n      expect(utils.merge(objA, objB)).to.deep.equal({\n        pets: {\n          dog: 1,\n          cat: null,\n          ape: 3,\n          zebra: 9\n        }\n      })\n    })\n  })\n\n  it('creates a hash', async() => {\n    const password = 'userAPass'\n    const settings = {\n      algorithm: 'md5',\n      iterations: 100,\n      keyLength: 32\n    }\n    const { hash, salt } = await createHash(password, settings)\n    const { hash: hashCheck } = await createHash(password, settings, salt)\n    expect(hash.toString('base64')).to.eq(hashCheck.toString('base64'))\n  })\n})\n"
  },
  {
    "path": "src/utils/utils.ts",
    "content": "import * as url from 'url'\nimport * as crypto from 'crypto'\n\n/**\n * Returns a unique identifier\n */\nexport let getUid = function (): string {\n  return `${Date.now().toString(36)}-${(Math.random() * 10000000000000000000).toString(36)}`\n}\n\n/**\n * Takes a key-value map and returns\n * a map with { value: key } of the old map\n */\nexport let reverseMap = function (map: any): any {\n  const reversedMap = {}\n\n  for (const key in map) {\n    // @ts-ignore\n    reversedMap[map[key]] = key\n  }\n\n  return reversedMap\n}\n\n/**\n * Extended version of the typeof operator. Also supports 'array'\n * and 'url' to check for valid URL schemas\n */\nexport let isOfType = function (input: any, expectedType: string): boolean {\n  if (input === null) {\n    return expectedType === 'null'\n  } else if (expectedType === 'array') {\n    return Array.isArray(input)\n  } else if (expectedType === 'url') {\n    return !!url.parse(input).host\n  }\n  return typeof input === expectedType\n}\n\n/**\n * Takes a map and validates it against a basic\n * json schema in the form { key: type }\n * @returns {Boolean|Error}\n */\nexport let validateMap = function (map: any, throwError: boolean, schema: any): any {\n  let error\n  let key\n\n  for (key in schema) {\n    if (typeof map[key] === 'undefined') {\n      error = new Error(`Missing key ${key}`)\n      break\n    }\n\n    if (!isOfType(map[key], schema[key])) {\n      error = new Error(`Invalid type ${typeof map[key]} for ${key}`)\n      break\n    }\n  }\n\n  if (error) {\n    if (throwError) {\n      throw error\n    } else {\n      return error\n    }\n  } else {\n    return true\n  }\n}\n\n/**\n * Multi Object recursive merge\n * @param {Object} multiple objects to be merged into each other recursively\n */\nexport let merge = function (...args: any[]) {\n  const result = {}\n  const objs = Array.prototype.slice.apply(arguments)\n  let i\n\n  const internalMerge = (objA: any, objB: any) => {\n    let key\n\n    for (key in objB) {\n      if (objB[key] && objB[key].constructor === Object) {\n        objA[key] = objA[key] || {}\n        internalMerge(objA[key], objB[key])\n      } else if (objB[key] !== undefined) {\n        objA[key] = objB[key]\n      }\n    }\n  }\n\n  for (i = 0; i < objs.length; i++) {\n    internalMerge(result, objs[i])\n  }\n\n  return result\n}\n\nexport let getRandomIntInRange = function (min: number, max: number): number {\n  return min + Math.floor(Math.random() * (max - min))\n}\n\nexport let spliceRandomElement = function (array: any[]): any {\n  const randomIndex = getRandomIntInRange(0, array.length)\n  return array.splice(randomIndex, 1)[0]\n}\n\n/**\n * Randomize array element order in-place.\n * Using Durstenfeld shuffle algorithm.\n */\nexport let shuffleArray = function (array: any[]): any[] {\n  for (let i = array.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1))\n    const temp = array[i]\n    array[i] = array[j]\n    array[j] = temp\n  }\n  return array\n}\n\n/*\n * Recursively freeze a deeply nested object\n * https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze\n */\nexport let deepFreeze = function (obj: any): any {\n\n  // Retrieve the property names defined on obj\n  const propNames = Object.getOwnPropertyNames(obj)\n\n  // Freeze properties before freezing self\n  propNames.forEach((name) => {\n    const prop = obj[name]\n\n    // Freeze prop if it is an object\n    if (typeof prop === 'object' && prop !== null) {\n      deepFreeze(prop)\n    }\n  })\n\n  // Freeze self (no-op if already frozen)\n  return Object.freeze(obj)\n}\n\n/**\n * Check whether a record name should be excluded from storage\n */\nexport const isExcluded = function (exclusionPrefixes: string[], recordName: string): boolean {\n  if (!exclusionPrefixes) {\n    return false\n  }\n\n  for (const exclusionPrefix of exclusionPrefixes) {\n    if (recordName.startsWith(exclusionPrefix)) {\n      return true\n    }\n  }\n\n  return false\n}\n\nexport const PromiseDelay = (timeout: number) => {\n  return new Promise((resolve) => setTimeout(resolve, timeout))\n}\n\n /**\n  * Utility method for creating hashes including salts based on\n  * the provided parameters\n  */\nexport const createHash = (password: string, settings: { iterations: number, keyLength: number, algorithm: string }, salt: string = crypto.randomBytes(16).toString('base64')): Promise<{ hash: Buffer, salt: string}> => {\n  return new Promise((resolve, reject) => {\n    crypto.pbkdf2(\n      password,\n      salt,\n      settings.iterations,\n      settings.keyLength,\n      settings.algorithm,\n      (err, hash) => {\n        err ? reject(err) : resolve({ hash, salt })\n      }\n    )\n  })\n}\n\nexport const validateHashingAlgorithm = (hash: string): void => {\n  if (crypto.getHashes().indexOf(hash) === -1) {\n    throw new Error(`Unknown Hash ${hash}`)\n  }\n}\n\nconst uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i\nexport const validateUUID = (uuid: string): boolean => {\n  return uuidPattern.test(uuid.toLowerCase())\n}"
  },
  {
    "path": "telemetry-server/package.json",
    "content": "{\n  \"name\": \"@deepstream/telemetry-server\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A server that simply gathers anonymous data and inserts it into postgres\",\n  \"scripts\": {\n    \"start\": \"ts-node telemetry-server.ts\"\n  },\n  \"keywords\": [\n    \"deepstream\",\n    \"telemetry\"\n  ],\n  \"author\": \"yasserf\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"body-parser\": \"^1.19.0\",\n    \"express\": \"^4.17.1\",\n    \"pg\": \"^8.1.0\",\n    \"typescript\": \"^3.8.3\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^4.17.6\",\n    \"@types/pg\": \"^7.14.3\",\n    \"ts-node\": \"^8.10.1\"\n  }\n}\n"
  },
  {
    "path": "telemetry-server/telemetry-server.config.js",
    "content": "module.exports = {\n    apps : [\n        {\n          name: \"telemetry-server\",\n          script: \"npm run start\",\n          env: {\n            PGUSER:\"postgres\",\n            PGDATABASE:\"deepstream\",\n            PGHOST:\"localhost\",\n            PGPASSWORD:\"secretpassword\"\n          }\n        }\n    ]\n  }"
  },
  {
    "path": "telemetry-server/telemetry-server.ts",
    "content": "const port = process.env.TELEMETRY_PORT || 8080\n\nimport * as express from 'express'\nimport * as bodyParser from 'body-parser'\nimport { Pool } from 'pg'\n\nconst app = express()\nconst pool = new Pool()\n\nasync function insertStats (stats: any) {\n  const { record, event, presence, rpc } = stats.enabledFeatures\n  const valueStrings  = `('${stats.deploymentId}','${stats.deepstreamVersion}','${stats.nodeVersion}','${stats.platform}','${record}','${event}','${presence}','${rpc}','${JSON.stringify(stats).replace(/'/g, \"''\")}')`\n\n  await pool.query(`\n      INSERT INTO \"public\".\"telemetry\" (id, version, nodeVersion, platform, record, event, presence, rpc, json)\n      VALUES ${valueStrings}\n      ON CONFLICT (id)\n      DO UPDATE SET version = EXCLUDED.version, nodeVersion = EXCLUDED.nodeVersion, platform = EXCLUDED.platform, record = EXCLUDED.record, event = EXCLUDED.event, presence = EXCLUDED.presence, rpc = EXCLUDED.rpc, json = EXCLUDED.json, revision = telemetry.revision + 1;\n  `)\n\n  console.log(`Updated ${stats.deploymentId}`)\n}\n\n// parse application/json\napp.use(bodyParser.json())\n\napp.post('/api/v1/startup', async (req, res) => {\n  await insertStats(req.body)\n  res.json({ success: true })\n})\n\napp.listen(port, () => console.log(`Telemetry listening on port ${port}`))"
  },
  {
    "path": "test-e2e/config/permissions-complex.json",
    "content": "{\n\t\"presence\": {\n\t\t\"*\": {\n\t\t\t\"allow\": \"user.id === 'B'\"\n\t\t}\n\t},\n\t\"record\": {\n\t\t\"*\": {\n\t\t\t\"create\": true,\n\t\t\t\"delete\": true,\n\t\t\t\"write\": true,\n\t\t\t\"read\": true,\n\t\t\t\"listen\": true,\n\t\t\t\"notify\": \"user.id === 'B'\"\n\t\t},\n\t\t\"d/*\": {\n\t\t\t\"create\": \"_('perm/JohnDoe').boolean\",\n\t\t\t\"read\": \"_('perm/JohnDoe').boolean\"\n\t\t},\n\t\t\"public-read-private-write/$userid\": {\n\t\t\t\"read\": true,\n\t\t\t\"create\": \"user.id === $userid\",\n\t\t\t\"write\": \"user.id === $userid\"\n\t\t},\n\t\t\"only-increment\": {\n\t\t\t\"write\": \"!oldData.value || data.value > oldData.value\",\n\t\t\t\"create\": true,\n\t\t\t\"read\": true\n\t\t},\n\t\t\"only-decrement\": {\n\t\t\t\"write\": \"!oldData.value || data.value < oldData.value\",\n\t\t\t\"create\": true,\n\t\t\t\"read\": true\n\t\t},\n\t\t\"only-delete-egon-miller/$firstname/$lastname\": {\n\t\t\t\"delete\": \"$firstname.toLowerCase() === 'egon' && $lastname.toLowerCase() === 'miller'\"\n\t\t},\n\t\t\"only-allows-purchase-of-products-in-stock/$purchaseId\": {\n\t\t\t\"create\": true,\n\t\t\t\"write\": \"_('item/' + data.itemId ).stock > 0\"\n\t\t},\n\t\t\"only-a-can-read-and-create\": {\n\t\t\t\"create\": \"user.id === 'A'\",\n\t\t\t\"read\": \"user.id === 'A'\"\n\t\t},\n\t\t\"forbidden\": {\n\t\t\t\"create\": false,\n\t\t\t\"delete\": false,\n\t\t\t\"write\": false,\n\t\t\t\"read\": false,\n\t\t\t\"listen\": false\n\t\t},\n\t\t\t\"read-only\": {\n\t\t\t\"write\": false,\n\t\t\t\"read\": true\n\t\t},\n\t\t\"deny-read\":{\n\t\t\t\"create\": false,\n\t\t\t\"write\": false,\n\t\t\t\"read\": false,\n\t\t\t\"delete\": false,\n\t\t\t\"listen\": false\n\t\t},\n\t\t\"deny-write\":{\n\t\t\t\"create\": true,\n\t\t\t\"write\": false,\n\t\t\t\"read\": true,\n\t\t\t\"delete\": false,\n\t\t\t\"listen\": false\n\t\t }\n\t},\n\t\"event\": {\n\t\t\"*\": {\n\t\t\t\"listen\": true,\n\t\t\t\"publish\": true,\n\t\t\t\"subscribe\": true\n\t\t},\n\t\t\"open/*\": {\n\t\t\t\"listen\": true,\n\t\t\t\"publish\": true,\n\t\t\t\"subscribe\": true\n\t\t},\n\t\t\"forbidden/*\": {\n\t\t\t\"publish\": false,\n\t\t\t\"subscribe\": false\n\t\t},\n\t\t\"a-to-b/*\": {\n\t\t\t\"publish\": \"user.id === 'A'\",\n\t\t\t\"subscribe\": \"user.id === 'B'\"\n\t\t},\n\t\t\"news/$topic\": {\n\t\t\t\"publish\": \"$topic === 'tea-cup-pigs'\"\n\t\t},\n\t\t\"number\": {\n\t\t\t\"publish\": \"data > 10\"\n\t\t},\n\t\t\"place/$city\": {\n\t\t\t\"publish\": \"$city.toLowerCase() === data.address.city.toLowerCase()\"\n\t\t},\n\t\t\"deny-this*\":{\n\t\t\t\"publish\": false,\n\t\t\t\"subscribe\": false,\n\t\t\t\"listen\": false\n\t\t},\n\t\t\"admin-publish\":{\n\t\t\t\"publish\": \"user.data.role === 'admin'\"\n\t\t}\n\t},\n\t\"rpc\": {\n\t\t\"*\": {\n\t\t\t\"provide\": true,\n\t\t\t\"request\": true\n\t\t},\n\t\t\"a-provide-b-request\": {\n\t\t\t\"provide\": \"user.id === 'A'\",\n\t\t\t\"request\": \"user.id === 'B'\"\n\t\t},\n\t\t\"only-full-user-data\": {\n\t\t\t\"request\": \"typeof data.firstname === 'string' && typeof data.lastname === 'string'\"\n\t\t},\n\t\t\"deny\": {\n\t\t\t\"provide\": false,\n\t\t\t\"request\": false\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "test-e2e/config/permissions-open.json",
    "content": "{\n\t\"presence\": {\n\t\t\"*\": {\n\t\t\t\"allow\": true\n\t\t}\n\t},\n\t\"record\": {\n\t\t\"*\": {\n\t\t\t\"create\": true,\n\t\t\t\"delete\": true,\n\t\t\t\"write\": true,\n\t\t\t\"read\": true,\n\t\t\t\"listen\": true,\n\t\t\t\"notify\": true\n\t\t}\n\t},\n\t\"event\": {\n\t\t\"*\": {\n\t\t\t\"listen\": true,\n\t\t\t\"publish\": true,\n\t\t\t\"subscribe\": true\n\t\t}\n\t},\n\t\"rpc\": {\n\t\t\"*\": {\n\t\t\t\"provide\": true,\n\t\t\t\"request\": true\n\t\t}\n\t}\n}"
  },
  {
    "path": "test-e2e/framework/client-handler.ts",
    "content": "import { DeepstreamClient } from '@deepstream/client'\nimport * as sinon from 'sinon'\nimport { Message } from '../../src/constants'\n\nexport interface E2EClient {\n  name: string,\n  client: DeepstreamClient,\n  [index: string]: any\n}\n\nconst clients: { [index: string]: E2EClient } = {}\n\nfunction createClient (clientName: string, server: string, options?: any) {\n  const deepstreamUrl = global.e2eHarness.getUrl(server)\n  // @ts-ignore\n  const client = new DeepstreamClient(`${deepstreamUrl}-v4`, {\n    ...options,\n    subscriptionInterval: 5,\n    maxReconnectInterval: 300,\n    maxReconnectAttempts: 20,\n    rpcAcceptTimeout: 100,\n    rpcResponseTimeout: 300,\n    subscriptionTimeout: 100,\n    recordReadAckTimeout: 100,\n    recordReadTimeout: 50,\n    recordDeleteTimeout: 100,\n    recordDiscardTimeout: 100,\n    intervalTimerResolution: 1,\n    offlineEnabled: false,\n    offlineBufferTimeout: 10000,\n    nativeTimerRegistry: false,\n    initialRecordVersion: 1,\n    socketOptions: {\n      jsonTransportMode: false\n    }\n  })\n  clients[clientName] = {\n    name: clientName,\n    client,\n    login: sinon.spy(),\n    error: {},\n    connectionStateChanged: sinon.spy(),\n    clientDataChanged: sinon.spy(),\n    reauthenticationFailure: sinon.spy(),\n    event: {\n      callbacks: {},\n      callbacksListeners: {},\n      callbacksListenersSpies: {},\n      callbacksListenersResponse: {},\n    },\n    record: {\n      records: {\n        // Creates a similar structure when record is requests\n        xxx: {\n          record: null,\n          discardCallback: null,\n          deleteCallback: null,\n          callbackError: null,\n          subscribeCallback: null,\n          subscribePathCallbacks: {}\n        }\n      },\n      lists: {\n        xxx: {\n          list: null,\n          discardCallback: null,\n          deleteCallback: null,\n          callbackError: null,\n          subscribeCallback: null,\n          addedCallback: null,\n          removedCallback: null,\n          movedCallback: null\n        }\n      },\n      anonymousRecord: null,\n      snapshotCallback: sinon.spy(),\n      hasCallback: sinon.spy(),\n      headCallback: sinon.spy(),\n      callbacksListeners: {},\n      callbacksListenersSpies: {},\n      callbacksListenersResponse: {},\n    },\n    rpc: {\n      callbacks: {},\n      provides: {},\n      callbacksListeners: {},\n      callbacksListenersSpies: {},\n      callbacksListenersResponse: {},\n    },\n    presence: {\n      callbacks: {}\n    }\n  }\n\n  clients[clientName].client.on('error', (message: Message, event: string, topic: number) => {\n    if (process.env.DEBUG_LOG) {\n      console.log('An Error occured on', clientName, message, event, topic)\n    }\n\n    if (!clients[clientName]) {\n      return\n    }\n    const clientErrors = clients[clientName].error\n    clientErrors[topic]          = clientErrors[topic] || {}\n    clientErrors[topic][event] = clientErrors[topic][event] || sinon.spy()\n    clients[clientName].error[topic][event](message)\n  })\n\n  clients[clientName].client.on('connectionStateChanged', (state: string) => {\n    if (!clients[clientName]) {\n      return\n    }\n    clients[clientName].connectionStateChanged(state)\n  })\n\n  clients[clientName].client.on('clientDataChanged', (clientData: any) => {\n    if (!clients[clientName]) {\n      return\n    }\n    clients[clientName].clientDataChanged(clientData)\n  })\n\n  clients[clientName].client.on('reauthenticationFailure', (reason: string) => {\n    if (!clients[clientName]) {\n      return\n    }\n    clients[clientName].reauthenticationFailure(reason)\n  })\n\n  return clients[clientName]\n}\n\nfunction getClientNames (expression: string) {\n  const clientExpression = /all clients|(?:subscriber|publisher|clients?) ([^\\s']*)(?:'s)?/\n  const result = clientExpression.exec(expression)!\n  if (result[0] === 'all clients') {\n    return Object.keys(clients)\n  } else if (result.length === 2 && result[1].indexOf(',') > -1) {\n    return result[1].replace(/\"/g, '').split(',')\n  } else if (result.length === 2) {\n    return [result[1].replace(/\"/g, '')]\n  }\n\n  throw new Error(`Invalid expression: ${expression}`)\n}\n\nfunction getClients (expression: string) {\n  return getClientNames(expression).map((client) => clients[client])\n}\n\nfunction assertNoErrors (client: string) {\n  const clientErrors = clients[client].error\n  for (const topic in clientErrors) {\n    for (const event in clientErrors[topic]) {\n      sinon.assert.notCalled(clientErrors[topic][event])\n    }\n  }\n}\n\nexport const clientHandler = {\n  clients,\n  createClient,\n  getClientNames,\n  getClients,\n  assertNoErrors\n}\n"
  },
  {
    "path": "test-e2e/framework/client.ts",
    "content": "// tslint:disable:no-shadowed-variable\nimport * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\nimport { TOPIC, AUTH_ACTION, CONNECTION_ACTION } from '../../src/constants'\n\nexport const client = {\n  logsOut (clientExpression: string, done: Function) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.close()\n    })\n    // current sync since protocol doesn't yet support async\n    done()\n  },\n\n  connect (clientExpression: string, server: string) {\n    clientHandler.getClientNames(clientExpression).forEach((clientName) => {\n      clientHandler.createClient(clientName, server)\n    })\n  },\n\n  connectAndLogin (clientExpression: string, server: string, done: Function) {\n    clientHandler.getClientNames(clientExpression).forEach((clientName) => {\n      const client = clientHandler.createClient(clientName, server)\n      client.client.login({ username: clientName, password: 'abcdefgh' }, (success, data) => {\n        client.login(success, data)\n        client.user = clientName\n        done()\n      })\n    })\n  },\n\n  login (clientExpression: string, username: string, password: string, done: Function) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.login({\n        username,\n        password\n      }, (success, data) => {\n        client.login(success, data)\n        client.user = username\n        done()\n      })\n    })\n  },\n\n  attemptLogin (clientExpression: string, username: string, password: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.login({\n        username,\n        password\n      })\n    })\n  },\n\n  receivedTooManyLoginAttempts (clientExpression: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const errorSpy = client.error[TOPIC[TOPIC.AUTH]][AUTH_ACTION[AUTH_ACTION.TOO_MANY_AUTH_ATTEMPTS]]\n      sinon.assert.calledOnce(errorSpy)\n      errorSpy.resetHistory()\n    })\n  },\n\n  recievesNoLoginResponse (clientExpression: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      sinon.assert.notCalled(client.login)\n    })\n  },\n\n  recievesLoginResponse (clientExpression: string, loginFailed: boolean, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const loginSpy = client.login\n      if (!loginFailed) {\n        sinon.assert.calledOnce(loginSpy)\n        if (data) {\n          sinon.assert.calledWith(loginSpy, true, JSON.parse(data))\n        } else {\n          sinon.assert.calledWith(loginSpy, true)\n        }\n      } else {\n        sinon.assert.calledOnce(loginSpy)\n        sinon.assert.calledWith(loginSpy, false)\n      }\n      loginSpy.resetHistory()\n    })\n  },\n\n  connectionTimesOut (clientExpression: string, done: Function) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      setTimeout(() => {\n        const errorSpy = client.error[TOPIC[TOPIC.CONNECTION]][CONNECTION_ACTION[CONNECTION_ACTION.AUTHENTICATION_TIMEOUT]]\n        sinon.assert.calledOnce(errorSpy)\n        errorSpy.resetHistory()\n        done()\n      }, 1000)\n    })\n  },\n\n  receivedErrorOnce (clientExpression: string, topicName: string, eventName: string) {\n    const topic = topicName.toUpperCase()\n\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const errorSpy = client.error[topic][eventName]\n      sinon.assert.called(errorSpy)\n      errorSpy.resetHistory()\n    })\n  },\n\n  receivedOneError (clientExpression: string, topicName: string, eventName: string) {\n    // @ts-ignore\n    const topic = TOPIC[TOPIC[topicName.toUpperCase()]]\n    const event = eventName.toUpperCase()\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const errorSpy = client.error[topic][event]\n      sinon.assert.calledOnce(errorSpy)\n      errorSpy.resetHistory()\n    })\n  },\n\n  callbackCalled (clientExpression: string, eventName: string, notCalled: boolean, once: boolean, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const spy = client[eventName]\n      if (notCalled) {\n        sinon.assert.notCalled(spy)\n      } else {\n        if (once) {\n          sinon.assert.calledOnce(spy)\n        } else {\n          sinon.assert.called(spy)\n        }\n        if (data !== null) {\n          sinon.assert.calledWith(spy, JSON.parse(data))\n        }\n      }\n\n      spy.resetHistory()\n    })\n  },\n\n  receivedNoErrors (clientExpression: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      clientHandler.assertNoErrors(client.name)\n    })\n  },\n\n  hadConnectionState (clientExpression: string, had: boolean, state: boolean) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      if (had) {\n        sinon.assert.calledWith(client.connectionStateChanged, state)\n      } else {\n        sinon.assert.neverCalledWith(client.connectionStateChanged, state)\n      }\n    })\n  },\n\n}\n"
  },
  {
    "path": "test-e2e/framework/event.ts",
    "content": "import * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\nimport { parseData } from './utils'\n\nconst assert = {\n  received (clientExpression: string, doesReceive: boolean, subscriptionName: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const eventSpy = client.event.callbacks[subscriptionName]\n      if (doesReceive) {\n        sinon.assert.calledOnce(eventSpy)\n        sinon.assert.calledWith(eventSpy, parseData(data))\n        eventSpy.resetHistory()\n      } else {\n        sinon.assert.notCalled(eventSpy)\n      }\n    })\n  },\n}\n\nexport const event = {\n  assert,\n\n  publishes (clientExpression: string, subscriptionName: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.event.emit(subscriptionName, parseData(data))\n    })\n  },\n\n  subscribes (clientExpression: string, subscriptionName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.event.callbacks[subscriptionName] = sinon.spy()\n      client.client.event.subscribe(subscriptionName, client.event.callbacks[subscriptionName])\n    })\n  },\n\n  unsubscribes (clientExpression: string, subscriptionName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.event.unsubscribe(subscriptionName, client.event.callbacks[subscriptionName])\n      client.event.callbacks[subscriptionName].isSubscribed = false\n    })\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework/listening.ts",
    "content": "import * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\nimport { ListenResponse } from '@deepstream/client/dist/src/util/listener'\n\nconst clients = clientHandler.clients\n\ntype ListenType = 'record' | 'event'\n\nexport const assert = {\n  doesNotRecieveMatch (client: string, type: ListenType, match: boolean, pattern: string) {\n    const listenCallbackSpy = clients[client][type].callbacksListenersSpies[pattern].start\n    sinon.assert.neverCalledWith(listenCallbackSpy, match)\n  },\n\n  recievesMatch (client: string, count: number, type: ListenType, subscriptionName: string, pattern: string) {\n    const listenCallbackSpy = clients[client][type].callbacksListenersSpies[pattern].start\n    sinon.assert.callCount(listenCallbackSpy.withArgs(subscriptionName), Number(count))\n  },\n\n  receivedUnMatch (client: string, count: number, type: ListenType, subscriptionName: string, pattern: string) {\n    const listenCallbackSpy = clients[client][type].callbacksListenersSpies[pattern].stop\n    sinon.assert.callCount(listenCallbackSpy.withArgs(subscriptionName), Number(count))\n  }\n}\n\nexport const listening = {\n  assert,\n\n  setupListenResponse (client: string, accepts: boolean, type: ListenType, subscriptionName: string, pattern: string) {\n    clients[client][type].callbacksListenersSpies[pattern].start.withArgs(subscriptionName)\n    clients[client][type].callbacksListenersSpies[pattern].stop.withArgs(subscriptionName)\n    clients[client][type].callbacksListenersResponse[pattern] = accepts\n  },\n\n  listens (client: string, type: ListenType, pattern: string) {\n    if (!clients[client][type].callbacksListenersSpies[pattern]) {\n      clients[client][type].callbacksListenersSpies[pattern] = { start: sinon.spy(), stop: sinon.spy() }\n    }\n\n    clients[client][type].callbacksListeners[pattern] = (subscriptionName: string, response: ListenResponse) => {\n      if (clients[client][type].callbacksListenersResponse[pattern]) {\n        response.accept()\n      } else {\n        response.reject()\n      }\n      response.onStop(clients[client][type].callbacksListenersSpies[pattern].stop)\n      clients[client][type].callbacksListenersSpies[pattern].start(subscriptionName)\n    }\n    clients[client].client[type].listen(pattern, clients[client][type].callbacksListeners[pattern])\n  },\n\n  unlistens (client: string, type: ListenType, pattern: string) {\n    clients[client].client[type].unlisten(pattern)\n    clients[client][type].callbacksListeners[pattern].isListening = false\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework/presence.ts",
    "content": "import * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\nimport { Dictionary } from 'ts-essentials'\n\nconst subscribeEvent = 'subscribe'\nconst queryEvent = 'query'\n\nexport const assert = {\n  notifiedUserStateChanged (notifeeExpression: string, not: boolean, notiferExpression: string, event: string) {\n    clientHandler.getClients(notifeeExpression).forEach((notifee) => {\n      clientHandler.getClients(notiferExpression).forEach((notifier) => {\n        if (not) {\n          sinon.assert.neverCalledWith(notifee.presence.callbacks[subscribeEvent], notifier.user, event === 'in')\n        } else {\n          sinon.assert.calledWith(notifee.presence.callbacks[subscribeEvent], notifier.user, event === 'in')\n        }\n      })\n      notifee.presence.callbacks[subscribeEvent].resetHistory()\n    })\n  },\n\n  globalQueryResult (clientExpression: string, error: null | string, users?: string[]) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      sinon.assert.calledOnce(client.presence.callbacks[queryEvent])\n      if (users) {\n        sinon.assert.calledWith(client.presence.callbacks[queryEvent], error, users)\n      } else {\n        sinon.assert.calledWith(client.presence.callbacks[queryEvent], error)\n      }\n      client.presence.callbacks[queryEvent].resetHistory()\n    })\n  },\n\n  queryResult (clientExpression: string, users: string[], online: boolean) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const result = users.reduce((r, user) => {\n        r[user] = online\n        return r\n      }, {} as Dictionary<boolean>)\n      sinon.assert.calledOnce(client.presence.callbacks[queryEvent])\n      sinon.assert.calledWith(client.presence.callbacks[queryEvent], null, result)\n      client.presence.callbacks[queryEvent].resetHistory()\n    })\n  }\n}\n\nexport const presence = {\n  assert,\n  subscribe (clientExpression: string, user?: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      if (!client.presence.callbacks[subscribeEvent]) {\n        client.presence.callbacks[subscribeEvent] = sinon.spy()\n      }\n      if (user) {\n        client.client.presence.subscribe(user, client.presence.callbacks[subscribeEvent])\n      } else {\n        client.client.presence.subscribe(client.presence.callbacks[subscribeEvent])\n      }\n    })\n  },\n\n  unsubscribe (clientExpression: string, user?: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      if (user) {\n        client.client.presence.unsubscribe(user)\n      } else {\n        client.client.presence.unsubscribe()\n      }\n    })\n  },\n\n  getAll (clientExpression: string, users?: string[]) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.presence.callbacks[queryEvent] = sinon.spy()\n      if (users) {\n        client.client.presence.getAll(users, client.presence.callbacks[queryEvent])\n      } else {\n        client.client.presence.getAll(client.presence.callbacks[queryEvent])\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework/record.ts",
    "content": "import * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\nimport * as utils from './utils'\nimport * as assert from 'assert'\n\nfunction getRecordData (clientExpression: string, recordName: string) {\n  return clientHandler.getClients(clientExpression).map((client) => client.record.records[recordName])\n}\n\nfunction getListData (clientExpression: string, listName: string) {\n  return clientHandler.getClients(clientExpression).map((client) => client.record.lists[listName])\n}\n\nconst assert2 = {\n    deleted (clientExpression: string, recordName: string, called: boolean) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            if (called) {\n              sinon.assert.calledOnce(recordData.deleteCallback)\n              recordData.deleteCallback.resetHistory()\n            } else {\n              sinon.assert.notCalled(recordData.deleteCallback)\n            }\n            recordData.deleteCallback.resetHistory()\n        })\n    },\n\n    discarded (clientExpression: string, recordName: string, called: boolean) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            if (called) {\n                sinon.assert.calledOnce(recordData.discardCallback)\n                recordData.discardCallback.resetHistory()\n            } else {\n                sinon.assert.notCalled(recordData.discardCallback)\n            }\n        })\n    },\n\n    receivedUpdate (clientExpression: string, recordName: string, data: string) {\n        data = utils.parseData(data)\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            sinon.assert.calledOnce(recordData.subscribeCallback)\n            sinon.assert.calledWith(recordData.subscribeCallback, data)\n            recordData.subscribeCallback.resetHistory()\n        })\n    },\n\n    receivedUpdateForPath (clientExpression: string, recordName: string, path: string, data: string) {\n        data = utils.parseData(data)\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            sinon.assert.calledOnce(recordData.subscribePathCallbacks[path])\n            sinon.assert.calledWith(recordData.subscribePathCallbacks[path], data)\n            recordData.subscribePathCallbacks[path].resetHistory()\n        })\n    },\n\n    receivedNoUpdate (clientExpression: string, recordName: string) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            sinon.assert.notCalled(recordData.subscribeCallback)\n        })\n    },\n\n    receivedNoUpdateForPath (clientExpression: string, recordName: string, path: string) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            sinon.assert.notCalled(recordData.subscribePathCallbacks[path])\n        })\n    },\n\n    receivedRecordError (clientExpression: string, error: string, recordName: string) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            sinon.assert.calledWith(recordData.errorCallback, error)\n            recordData.errorCallback.resetHistory()\n        })\n    },\n\n    hasData (clientExpression: string, recordName: string, data: string) {\n        data = utils.parseData(data)\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            assert.deepEqual(recordData.record.get(), data)\n        })\n    },\n\n    hasProviders (clientExpression: string, recordName: string, without: boolean) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            assert.deepEqual(recordData.record.hasProvider, !without)\n        })\n    },\n\n    hasDataAtPath (clientExpression: string, recordName: string, path: string, data: string) {\n        data = utils.parseData(data)\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            assert.deepEqual(recordData.record.get(path), data)\n        })\n    },\n\n    writeAckSuccess (clientExpression: string, recordName: string) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            if (!recordData) { return }\n            sinon.assert.calledOnce(recordData.setCallback)\n            sinon.assert.calledWith(recordData.setCallback, null)\n            recordData.setCallback.resetHistory()\n        })\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            if (!client.record.writeAcks) { return }\n            sinon.assert.calledOnce(client.record.writeAcks[recordName])\n            sinon.assert.calledWith(client.record.writeAcks[recordName], null)\n            client.record.writeAcks[recordName].resetHistory()\n        })\n    },\n\n    writeAckError (clientExpression: string, recordName: string, errorMessage: string) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            if (!recordData) { return }\n            sinon.assert.calledOnce(recordData.setCallback)\n            sinon.assert.calledWith(recordData.setCallback, errorMessage)\n            recordData.setCallback.resetHistory()\n        })\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            if (!client.record.writeAcks) { return }\n            sinon.assert.calledOnce(client.record.writeAcks[recordName])\n            sinon.assert.calledWith(client.record.writeAcks[recordName], errorMessage)\n            client.record.writeAcks[recordName].resetHistory()\n        })\n    },\n\n    snapshotSuccess (clientExpression: string, recordName: string, data: string) {\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            sinon.assert.calledOnce(client.record.snapshotCallback)\n            sinon.assert.calledWith(client.record.snapshotCallback, null, utils.parseData(data))\n            client.record.snapshotCallback.resetHistory()\n        })\n    },\n\n    snapshotError (clientExpression: string, recordName: string, data: string) {\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            sinon.assert.calledOnce(client.record.snapshotCallback)\n            sinon.assert.calledWith(client.record.snapshotCallback, data.replace(/\"/g, ''))\n            client.record.snapshotCallback.resetHistory()\n        })\n    },\n\n    headSuccess (clientExpression: string, recordName: string, data: number) {\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            sinon.assert.calledOnce(client.record.headCallback)\n            sinon.assert.calledWith(client.record.headCallback, null, data)\n            client.record.headCallback.resetHistory()\n        })\n    },\n\n    headError (clientExpression: string, recordName: string, data: string) {\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            sinon.assert.calledOnce(client.record.headCallback)\n            sinon.assert.calledWith(client.record.headCallback, data.replace(/\"/g, ''))\n            client.record.snapshotCallback.resetHistory()\n        })\n    },\n\n    has (clientExpression: string, recordName: string, expected: boolean) {\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            sinon.assert.calledOnce(client.record.hasCallback)\n            sinon.assert.calledWith(client.record.hasCallback, null, expected)\n            client.record.hasCallback.resetHistory()\n        })\n    },\n\n    hasEntries (clientExpression: string, listName: string, data: string) {\n        data = utils.parseData(data)\n        getListData(clientExpression, listName).forEach((listData) => {\n            assert.deepEqual(listData.list.getEntries(), data)\n        })\n    },\n\n    addedNotified (clientExpression: string, listName: string, entryName: string) {\n        getListData(clientExpression, listName).forEach((listData) => {\n            sinon.assert.calledWith(listData.addedCallback, entryName)\n        })\n    },\n\n    removedNotified (clientExpression: string, listName: string, entryName: string) {\n        getListData(clientExpression, listName).forEach((listData) => {\n            sinon.assert.calledWith(listData.removedCallback, entryName)\n        })\n    },\n\n    movedNotified (clientExpression: string, listName: string, entryName: string) {\n        getListData(clientExpression, listName).forEach((listData) => {\n            sinon.assert.calledWith(listData.movedNotified, entryName)\n        })\n    },\n\n    listChanged (clientExpression: string, listName: string, data: string) {\n        data = utils.parseData(data)\n        getListData(clientExpression, listName).forEach((listData) => {\n            // sinon.assert.calledOnce( listData.subscribeCallback );\n            sinon.assert.calledWith(listData.subscribeCallback, data)\n        })\n    },\n\n    anonymousRecordContains (clientExpression: string, data: string) {\n        data = utils.parseData(data)\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            assert.deepEqual(client.record.anonymousRecord.get(), data)\n        })\n    }\n}\n\nexport const record = {\n  assert: assert2,\n\n  getRecord (clientExpression: string, recordName: string) {\n    const clients = clientHandler.getClients(clientExpression)\n    clients.forEach((client) => {\n      const recordData = {\n        record: client.client.record.getRecord(recordName),\n        discardCallback: sinon.spy(),\n        deleteSuccessCallback: sinon.spy(),\n        deleteCallback: sinon.spy(),\n        callbackError: sinon.spy(),\n        subscribeCallback: sinon.spy(),\n        errorCallback: sinon.spy(),\n        setCallback: undefined,\n        subscribePathCallbacks: {}\n      }\n      recordData.record.on('delete', recordData.deleteCallback)\n      recordData.record.on('error', recordData.errorCallback)\n      recordData.record.on('discard', recordData.discardCallback)\n      client.record.records[recordName] = recordData\n    })\n  },\n\n  subscribe (clientExpression: string, recordName: string, immediate: boolean) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.subscribe(recordData.subscribeCallback, !!immediate)\n    })\n  },\n\n  unsubscribe (clientExpression: string, recordName: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.unsubscribe(recordData.subscribeCallback)\n    })\n  },\n\n  subscribeWithPath (clientExpression: string, recordName: string, path: string, immediate: boolean) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.subscribePathCallbacks[path] = sinon.spy()\n      recordData.record.subscribe(path, recordData.subscribePathCallbacks[path], !!immediate)\n    })\n  },\n\n  unsubscribeFromPath (clientExpression: string, recordName: string, path: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.unsubscribe(path, recordData.subscribePathCallbacks[path])\n    })\n  },\n\n  discard (clientExpression: string, recordName: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.discard()\n    })\n  },\n\n  delete (clientExpression: string, recordName: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.delete(recordData.deleteSuccessCallback)\n    })\n  },\n\n  setupWriteAck (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.record.records[recordName].setCallback = sinon.spy()\n    })\n  },\n\n  set (clientExpression: string, recordName: string, data: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      if (recordData.setCallback) {\n        recordData.record.set(utils.parseData(data), recordData.setCallback)\n      } else {\n        recordData.record.set(utils.parseData(data))\n      }\n    })\n  },\n\n  setWithPath (clientExpression: string, recordName: string, path: string, data: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      if (recordData.setCallback) {\n        recordData.record.set(path, utils.parseData(data), recordData.setCallback)\n      } else {\n        recordData.record.set(path, utils.parseData(data))\n      }\n    })\n  },\n\n  erase (clientExpression: string, recordName: string, path: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      if (recordData.setCallback) {\n        recordData.record.erase(path, recordData.setCallback)\n      } else {\n        recordData.record.erase(path)\n      }\n    })\n  },\n\n  setData (clientExpression: string, recordName: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.setData(recordName, utils.parseData(data))\n    })\n  },\n\n  setDataWithWriteAck (clientExpression: string, recordName: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      if (!client.record.writeAcks) {\n        client.record.writeAcks = {}\n      }\n      client.record.writeAcks[recordName] = sinon.spy()\n      client.client.record.setData(recordName, utils.parseData(data), client.record.writeAcks[recordName])\n    })\n  },\n\n  setDataWithPath (clientExpression: string, recordName: string, path: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.setData(recordName, path, utils.parseData(data))\n    })\n  },\n\n  snapshot (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.snapshot(recordName, client.record.snapshotCallback)\n    })\n  },\n\n  has (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.has(recordName, client.record.hasCallback)\n    })\n  },\n\n  head (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.head(recordName, client.record.headCallback)\n    })\n  },\n\n  /** ******************************************************************************************************************************\n   *********************************************************** Lists ************************************************************\n   ********************************************************************************************************************************/\n\n   getList (clientExpression: string, listName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const listData = {\n        list: client.client.record.getList(listName),\n        discardCallback: sinon.spy(),\n        deleteCallback: sinon.spy(),\n        callbackError: sinon.spy(),\n        subscribeCallback: sinon.spy(),\n        addedCallback: sinon.spy(),\n        removedCallback: sinon.spy(),\n        movedCallback: sinon.spy()\n      }\n      listData.list.on('discard', listData.discardCallback)\n      listData.list.on('delete', listData.deleteCallback)\n      listData.list.on('entry-added', listData.addedCallback)\n      listData.list.on('entry-removed', listData.removedCallback)\n      listData.list.on('entry-moved', listData.movedCallback)\n      listData.list.subscribe(listData.subscribeCallback)\n      client.record.lists[listName] = listData\n    })\n   },\n\n   setEntries (clientExpression: string, listName: string, data: string) {\n      const entries = utils.parseData(data)\n      getListData(clientExpression, listName).forEach((listData) => {\n        listData.list.setEntries(entries)\n      })\n   },\n\n   addEntry (clientExpression: string, listName: string, entryName: string) {\n    getListData(clientExpression, listName).forEach((listData) => {\n      listData.list.addEntry(entryName)\n    })\n   },\n\n   removeEntry (clientExpression: string, listName: string, entryName: string) {\n    getListData(clientExpression, listName).forEach((listData) => {\n      listData.list.removeEntry(entryName)\n    })\n   },\n\n  /** ******************************************************************************************************************************\n   *********************************************************** ANONYMOUS RECORDS ************************************************************\n   ********************************************************************************************************************************/\n\n   getAnonymousRecord (clientExpression: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.record.anonymousRecord = client.client.record.getAnonymousRecord()\n    })\n   },\n\n   setName (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.record.anonymousRecord.setName(recordName)\n    })\n   }\n}\n"
  },
  {
    "path": "test-e2e/framework/rpc.ts",
    "content": "// tslint:disable:no-shadowed-variable\n\nimport * as sinon from 'sinon'\nimport { clientHandler, E2EClient } from './client-handler'\nimport { RPCResponse } from '@deepstream/client/dist/src/rpc/rpc-response'\n\nlet rejected = false\n\nconst rpcs: { [index: string]: (client: E2EClient, data: any, response: RPCResponse) => void } = {\n  'addTwo': (client, data, response) => {\n    client.rpc.provides.addTwo()\n    response.send(data.numA + data.numB)\n  },\n  'double': (client, data, response) => {\n    client.rpc.provides.double()\n    response.send(data * 2)\n  },\n  'stringify': (client, data, response) => {\n    client.rpc.provides.stringify()\n    response.send(typeof data === 'object' ? JSON.stringify(data) : String(data))\n  },\n  'a-provide-b-request': (client, data, response) => {\n    client.rpc.provides['a-provide-b-request']()\n    response.send(data * 3)\n  },\n  'only-full-user-data': (client, data, response) => {\n    client.rpc.provides['only-full-user-data']()\n    response.send('ok')\n  },\n  'alwaysReject': (client, data, response) => {\n    client.rpc.provides.alwaysReject()\n    response.reject()\n  },\n  'alwaysError': (client, data, response) => {\n    client.rpc.provides.alwaysError()\n    response.error('always errors')\n  },\n  'neverRespond': (client) => {\n    client.rpc.provides.neverRespond()\n  },\n  'clientBRejects': (client, data, response) => {\n    client.rpc.provides.clientBRejects()\n    if (client.name === 'B') {\n      response.reject()\n    } else {\n      response.send(data.root * data.root)\n    }\n  },\n  'deny': (client, data, response) => {\n    // permissions always deny\n  },\n  'rejectOnce': (client, data, response) => {\n    client.rpc.provides.rejectOnce(data)\n    if (rejected) {\n      response.send('ok')\n      rejected = false\n    } else {\n      response.reject()\n      rejected = true\n    }\n  }\n}\n\nconst assert = {\n  recievesResponse (clientExpression: string, rpc: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      sinon.assert.calledOnce(client.rpc.callbacks[rpc])\n      sinon.assert.calledWith(client.rpc.callbacks[rpc], null, JSON.parse(data).toString())\n      client.rpc.callbacks[rpc].resetHistory()\n    })\n  },\n\n  recievesResponseWithError (clientExpression: string, eventually: boolean, rpc: string, error: string, done: Function) {\n    setTimeout(() => {\n      clientHandler.getClients(clientExpression).forEach((client) => {\n        sinon.assert.calledOnce(client.rpc.callbacks[rpc])\n        sinon.assert.calledWith(client.rpc.callbacks[rpc], error)\n        client.rpc.callbacks[rpc].resetHistory()\n        done()\n      })\n    }, eventually ? 150 : 0)\n  },\n\n  providerCalled (clientExpression: string, rpc: string, timesCalled: number, data?: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      sinon.assert.callCount(client.rpc.provides[rpc], timesCalled)\n      if (data) {\n        sinon.assert.calledWith(client.rpc.provides[rpc], JSON.parse(data))\n      }\n      client.rpc.provides[rpc].resetHistory()\n    })\n  }\n}\n\nexport const rpc = {\n  assert,\n\n  provide (clientExpression: string, rpc: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.rpc.provides[rpc] = sinon.spy()\n      client.client.rpc.provide(rpc, rpcs[rpc].bind(null, client))\n    })\n  },\n\n  unprovide (clientExpression: string, rpc: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.rpc.unprovide(rpc)\n    })\n  },\n\n  make (clientExpression: string, rpc: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const callback = client.rpc.callbacks[rpc] = sinon.spy()\n      client.client.rpc.make(rpc, JSON.parse(data), (error, result) => {\n        callback(error, result && result.toString())\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework/utils.ts",
    "content": "export const defaultDelay: number = Number(process.env.DEFAULT_DELAY) || 10\n\nexport const parseData = (data: string) => {\n  if (data === undefined || data === 'undefined') {\n    return undefined\n  } else if (data === 'null') {\n    return null\n  }\n  try {\n    return JSON.parse(data)\n  } catch (e) {\n    return data\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework/world.ts",
    "content": "// tslint:disable:no-shadowed-variable\n\nimport { clientHandler } from './client-handler'\n\nexport const world = {\n  endTest (done: (...args: any[]) => void) {\n    const clients = clientHandler.clients\n    for (const client in clients) {\n      clientHandler.assertNoErrors(client)\n\n      for (const event in clients[client].event.callbacks) {\n        if (clients[client].event.callbacks[event].isSubscribed !== false) {\n          clients[client].client.event.unsubscribe(event, clients[client].event.callbacks[event])\n        }\n      }\n\n      setTimeout(function (client: string) {\n        for (const pattern in clients[client].event.callbacksListeners) {\n          if (clients[client].event.callbacksListeners[pattern].isListening !== false) {\n            clients[client].client.event.unlisten(pattern)\n          }\n        }\n      }.bind(null, client), 1)\n\n      setTimeout(function (client: string) {\n        clients[client].client.close()\n        delete clients[client]\n      }.bind(null, client), 50)\n    }\n\n    setTimeout(done, 100)\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework-v3/client-handler.ts",
    "content": "import * as deepstream from 'deepstream.io-client-js'\nimport * as sinon from 'sinon'\nimport { Message } from '../../src/constants'\n\nexport interface E2EClient {\n  name: string,\n  client: any,\n  [index: string]: any\n}\n\nconst clients: { [index: string]: E2EClient } = {}\n\nfunction createClient (clientName: string, server: string, options?: any) {\n  const deepstreamUrl = global.e2eHarness.getUrl(server)\n  // @ts-ignore\n  const client = deepstream(`${deepstreamUrl}-v3`, {\n    ...options,\n    silentDeprecation: true,\n    subscriptionInterval: 5,\n    maxReconnectInterval: 300,\n    maxReconnectAttempts: 20,\n    rpcAcceptTimeout: 100,\n    rpcResponseTimeout: 300,\n    subscriptionTimeout: 100,\n    recordReadAckTimeout: 100,\n    recordReadTimeout: 50,\n    recordDeleteTimeout: 100,\n    recordDiscardTimeout: 100,\n    timerResolution: 1,\n    offlineEnabled: false,\n    offlineBufferTimeout: 10000\n  })\n  clients[clientName] = {\n    name: clientName,\n    client,\n    login: sinon.spy(),\n    error: {},\n    connectionStateChanged: sinon.spy(),\n    clientDataChanged: sinon.spy(),\n    reauthenticationFailure: sinon.spy(),\n    event: {\n      callbacks: {},\n      callbacksListeners: {},\n      callbacksListenersSpies: {},\n      callbacksListenersResponse: {},\n    },\n    record: {\n      records: {\n        // Creates a similar structure when record is requests\n        xxx: {\n          record: null,\n          discardCallback: null,\n          deleteCallback: null,\n          callbackError: null,\n          subscribeCallback: null,\n          subscribePathCallbacks: {}\n        }\n      },\n      lists: {\n        xxx: {\n          list: null,\n          discardCallback: null,\n          deleteCallback: null,\n          callbackError: null,\n          subscribeCallback: null,\n          addedCallback: null,\n          removedCallback: null,\n          movedCallback: null\n        }\n      },\n      anonymousRecord: null,\n      snapshotCallback: sinon.spy(),\n      hasCallback: sinon.spy(),\n      headCallback: sinon.spy(),\n      callbacksListeners: {},\n      callbacksListenersSpies: {},\n      callbacksListenersResponse: {},\n    },\n    rpc: {\n      callbacks: {},\n      provides: {},\n      callbacksListeners: {},\n      callbacksListenersSpies: {},\n      callbacksListenersResponse: {},\n    },\n    presence: {\n      callbacks: {}\n    }\n  }\n\n  clients[clientName].client.on('error', (message: Message, event: string, topic: number) => {\n    if (process.env.DEBUG_LOG) {\n      console.log('An Error occured on', clientName, message, event, topic)\n    }\n\n    if (event === 'MULTIPLE_SUBSCRIPTIONS' || event === 'UNSOLICITED_MESSAGE') {\n      return\n    }\n\n    if (!clients[clientName]) {\n      return\n    }\n    const clientErrors = clients[clientName].error\n    clientErrors[topic] = clientErrors[topic] || {}\n    clientErrors[topic][event] = clientErrors[topic][event] || sinon.spy()\n    clients[clientName].error[topic][event](message)\n  })\n\n  clients[clientName].client.on('connectionStateChanged', (state: string) => {\n    if (!clients[clientName]) {\n      return\n    }\n    clients[clientName].connectionStateChanged(state)\n  })\n\n  clients[clientName].client.on('clientDataChanged', (clientData: any) => {\n    if (!clients[clientName]) {\n      return\n    }\n    clients[clientName].clientDataChanged(clientData)\n  })\n\n  clients[clientName].client.on('reauthenticationFailure', (reason: string) => {\n    if (!clients[clientName]) {\n      return\n    }\n    clients[clientName].reauthenticationFailure(reason)\n  })\n\n  return clients[clientName]\n}\n\nfunction getClientNames (expression: string) {\n  const clientExpression = /all clients|(?:subscriber|publisher|clients?) ([^\\s']*)(?:'s)?/\n  const result = clientExpression.exec(expression)!\n  if (result[0] === 'all clients') {\n    return Object.keys(clients)\n  } else if (result.length === 2 && result[1].indexOf(',') > -1) {\n    return result[1].replace(/\"/g, '').split(',')\n  } else if (result.length === 2) {\n    return [result[1].replace(/\"/g, '')]\n  }\n\n  throw new Error(`Invalid expression: ${expression}`)\n}\n\nfunction getClients (expression: string) {\n  return getClientNames(expression).map((client) => clients[client])\n}\n\nfunction assertNoErrors (client: string) {\n  const clientErrors = clients[client].error\n  for (const topic in clientErrors) {\n    for (const event in clientErrors[topic]) {\n      sinon.assert.notCalled(clientErrors[topic][event])\n    }\n  }\n}\n\nexport const clientHandler = {\n  clients,\n  createClient,\n  getClientNames,\n  getClients,\n  assertNoErrors\n}\n"
  },
  {
    "path": "test-e2e/framework-v3/client.ts",
    "content": "// tslint:disable:no-shadowed-variable\nimport * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\n\nconst compat: any = {\n  CONNECTION: 'C',\n  EVENT: 'E',\n  RECORD: 'R',\n  PRESENCE: 'U',\n  RPC: 'P',\n  AUTH: 'A',\n  CONNECTION_ERROR: 'connectionError',\n  RECORD_NOT_FOUND: 'RECORD_NOT_FOUND'\n}\n\nexport const client = {\n  logsOut (clientExpression: string, done: Function) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.close()\n    })\n    // current sync since protocol doesn't yet support async\n    done()\n  },\n\n  connect (clientExpression: string, server: string) {\n    clientHandler.getClientNames(clientExpression).forEach((clientName) => {\n      clientHandler.createClient(clientName, server)\n    })\n  },\n\n  connectAndLogin (clientExpression: string, server: string, done: Function) {\n    clientHandler.getClientNames(clientExpression).forEach((clientName) => {\n      const client = clientHandler.createClient(clientName, server)\n      client.client.login({ username: clientName, password: 'abcdefgh' }, (success: boolean, data: any) => {\n        client.login(success, data)\n        client.user = clientName\n        done()\n      })\n    })\n  },\n\n  login (clientExpression: string, username: string, password: string, done: Function) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.login({\n        username,\n        password\n      }, (success: boolean, data: any) => {\n        client.login(success, data)\n        client.user = username\n        done()\n      })\n    })\n  },\n\n  attemptLogin (clientExpression: string, username: string, password: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.login({\n        username,\n        password\n      })\n    })\n  },\n\n  receivedTooManyLoginAttempts (clientExpression: string) {\n    // Not a < V4 feature\n    // clientHandler.getClients(clientExpression).forEach((client) => {\n    //   const errorSpy = client.error.A.TOO_MANY_AUTH_ATTEMPTS\n    //   sinon.assert.calledOnce(errorSpy)\n    //   errorSpy.resetHistory()\n    // })\n  },\n\n  recievesNoLoginResponse (clientExpression: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      sinon.assert.notCalled(client.login)\n    })\n  },\n\n  recievesLoginResponse (clientExpression: string, loginFailed: boolean, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const loginSpy = client.login\n      if (!loginFailed) {\n        sinon.assert.calledOnce(loginSpy)\n        if (data) {\n          sinon.assert.calledWith(loginSpy, true, JSON.parse(data))\n        } else {\n          sinon.assert.calledWith(loginSpy, true)\n        }\n      } else {\n        sinon.assert.called(loginSpy)\n        sinon.assert.calledWith(loginSpy, false)\n      }\n      loginSpy.resetHistory()\n    })\n  },\n\n  connectionTimesOut (clientExpression: string, done: Function) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      setTimeout(() => {\n        const errorSpy = client.error.C.CONNECTION_AUTHENTICATION_TIMEOUT\n        sinon.assert.calledOnce(errorSpy)\n        errorSpy.resetHistory()\n        done()\n      }, 1000)\n    })\n  },\n\n  receivedErrorOnce (clientExpression: string, topicName: string, eventName: string) {\n    const topic = topicName.toUpperCase()\n\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const errorSpy = client.error[compat[topic]][compat[eventName]]\n      sinon.assert.called(errorSpy)\n      errorSpy.resetHistory()\n    })\n  },\n\n  receivedOneError (clientExpression: string, topicName: string, eventName: string) {\n    let topic = compat[topicName]\n    const event = eventName.toUpperCase()\n\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      if (event === 'IS_CLOSED') {\n        topic = 'X'\n      }\n      const errorSpy = client.error[topic][event]\n      sinon.assert.calledOnce(errorSpy)\n      errorSpy.resetHistory()\n    })\n  },\n\n  callbackCalled (clientExpression: string, eventName: string, notCalled: boolean, once: boolean, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const spy = client[eventName]\n      if (notCalled) {\n        sinon.assert.notCalled(spy)\n      } else {\n        if (once) {\n          sinon.assert.calledOnce(spy)\n        } else {\n          sinon.assert.called(spy)\n        }\n        if (data !== undefined) {\n          sinon.assert.calledWith(spy, JSON.parse(data))\n        }\n      }\n\n      spy.resetHistory()\n    })\n  },\n\n  receivedNoErrors (clientExpression: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      clientHandler.assertNoErrors(client.name)\n    })\n  },\n\n  hadConnectionState (clientExpression: string, had: boolean, state: boolean) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      if (had) {\n        sinon.assert.calledWith(client.connectionStateChanged, state)\n      } else {\n        sinon.assert.neverCalledWith(client.connectionStateChanged, state)\n      }\n    })\n  },\n\n}\n"
  },
  {
    "path": "test-e2e/framework-v3/event.ts",
    "content": "import * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\nimport { parseData } from './utils'\n\nconst assert = {\n  received (clientExpression: string, doesReceive: boolean, subscriptionName: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const eventSpy = client.event.callbacks[subscriptionName]\n      if (doesReceive) {\n        sinon.assert.calledOnce(eventSpy)\n        sinon.assert.calledWith(eventSpy, parseData(data))\n        eventSpy.resetHistory()\n      } else {\n        sinon.assert.notCalled(eventSpy)\n      }\n    })\n  },\n}\n\nexport const event = {\n  assert,\n\n  publishes (clientExpression: string, subscriptionName: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.event.emit(subscriptionName, parseData(data))\n    })\n  },\n\n  subscribes (clientExpression: string, subscriptionName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.event.callbacks[subscriptionName] = sinon.spy()\n      client.client.event.subscribe(subscriptionName, client.event.callbacks[subscriptionName])\n    })\n  },\n\n  unsubscribes (clientExpression: string, subscriptionName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.event.unsubscribe(subscriptionName, client.event.callbacks[subscriptionName])\n      client.event.callbacks[subscriptionName].isSubscribed = false\n    })\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework-v3/listening.ts",
    "content": "import * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\n\nconst clients = clientHandler.clients\n\ntype ListenType = 'record' | 'event'\n\nexport const assert = {\n  doesNotRecieveMatch (client: string, type: ListenType, match: boolean, pattern: string) {\n    const listenCallbackSpy = clients[client][type].callbacksListenersSpies[pattern].start\n    sinon.assert.neverCalledWith(listenCallbackSpy, match)\n  },\n\n  recievesMatch (client: string, count: number, type: ListenType, subscriptionName: string, pattern: string) {\n    const listenCallbackSpy = clients[client][type].callbacksListenersSpies[pattern].start\n    sinon.assert.callCount(listenCallbackSpy.withArgs(subscriptionName), Number(count))\n  },\n\n  receivedUnMatch (client: string, count: number, type: ListenType, subscriptionName: string, pattern: string) {\n    const listenCallbackSpy = clients[client][type].callbacksListenersSpies[pattern].stop\n    sinon.assert.callCount(listenCallbackSpy.withArgs(subscriptionName), Number(count))\n  }\n}\n\nexport const listening = {\n  assert,\n\n  setupListenResponse (client: string, accepts: boolean, type: ListenType, subscriptionName: string, pattern: string) {\n    clients[client][type].callbacksListenersSpies[pattern].start.withArgs(subscriptionName)\n    clients[client][type].callbacksListenersSpies[pattern].stop.withArgs(subscriptionName)\n    clients[client][type].callbacksListenersResponse[pattern] = accepts\n  },\n\n  listens (client: string, type: ListenType, pattern: string) {\n    if (!clients[client][type].callbacksListenersSpies[pattern]) {\n      clients[client][type].callbacksListenersSpies[pattern] = { start: sinon.spy(), stop: sinon.spy() }\n    }\n\n    clients[client][type].callbacksListeners[pattern] = (subscriptionName: string, isSubscribed: boolean, response: any) => {\n      if (isSubscribed) {\n        if (clients[client][type].callbacksListenersResponse[pattern]) {\n          response.accept()\n        } else {\n          response.reject()\n        }\n        clients[client][type].callbacksListenersSpies[pattern].start(subscriptionName)\n      } else {\n        clients[client][type].callbacksListenersSpies[pattern].stop(subscriptionName)\n      }\n    }\n    clients[client].client[type].listen(pattern, clients[client][type].callbacksListeners[pattern])\n  },\n\n  unlistens (client: string, type: ListenType, pattern: string) {\n    clients[client].client[type].unlisten(pattern)\n    clients[client][type].callbacksListeners[pattern].isListening = false\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework-v3/presence.ts",
    "content": "import * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\nimport { client as cl } from './client'\nimport { Dictionary } from 'ts-essentials'\n\nconst subscribeEvent = 'subscribe'\nconst queryEvent = 'query'\n\nexport const assert = {\n  notifiedUserStateChanged (notifeeExpression: string, not: boolean, notiferExpression: string, event: string) {\n    clientHandler.getClients(notifeeExpression).forEach((notifee) => {\n      clientHandler.getClients(notiferExpression).forEach((notifier) => {\n        if (not) {\n          sinon.assert.neverCalledWith(notifee.presence.callbacks[subscribeEvent], notifier.user, event === 'in')\n        } else {\n          sinon.assert.calledWith(notifee.presence.callbacks[subscribeEvent], notifier.user, event === 'in')\n        }\n      })\n      notifee.presence.callbacks[subscribeEvent].resetHistory()\n    })\n  },\n\n  globalQueryResult (clientExpression: string, error: null | string, users?: string[]) {\n    if (error) {\n      cl.receivedOneError(clientExpression, 'PRESENCE', error)\n      return\n    }\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      sinon.assert.calledOnce(client.presence.callbacks[queryEvent])\n      if (users) {\n        sinon.assert.calledWith(client.presence.callbacks[queryEvent], users)\n      } else {\n        sinon.assert.calledWith(client.presence.callbacks[queryEvent])\n      }\n      client.presence.callbacks[queryEvent].resetHistory()\n    })\n  },\n\n  queryResult (clientExpression: string, users: string[], online: boolean) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const result = users.reduce((r, user) => {\n        r[user] = online\n        return r\n      }, {} as Dictionary<boolean>)\n      sinon.assert.calledOnce(client.presence.callbacks[queryEvent])\n      sinon.assert.calledWith(client.presence.callbacks[queryEvent], result)\n      client.presence.callbacks[queryEvent].resetHistory()\n    })\n  }\n}\n\nexport const presence = {\n  assert,\n  subscribe (clientExpression: string, user?: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      if (!client.presence.callbacks[subscribeEvent]) {\n        client.presence.callbacks[subscribeEvent] = sinon.spy()\n      }\n      if (user) {\n        client.client.presence.subscribe(user, (...args: any[]) => {\n          client.presence.callbacks[subscribeEvent](args[1], args[0])\n        })\n      } else {\n        client.client.presence.subscribe((...args: any[]) => {\n          client.presence.callbacks[subscribeEvent](...args)\n        })\n      }\n    })\n  },\n\n  unsubscribe (clientExpression: string, user?: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      if (user) {\n        client.client.presence.unsubscribe(user)\n      } else {\n        client.client.presence.unsubscribe()\n      }\n    })\n  },\n\n  getAll (clientExpression: string, users?: string[]) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.presence.callbacks[queryEvent] = sinon.spy()\n      if (users) {\n        client.client.presence.getAll(users, (...args: any[]) => {\n          client.presence.callbacks[queryEvent](...args)\n        })\n      } else {\n        client.client.presence.getAll((...args: any[]) => {\n          client.presence.callbacks[queryEvent](...args)\n        })\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework-v3/record.ts",
    "content": "import * as sinon from 'sinon'\nimport { clientHandler } from './client-handler'\nimport { client as cl } from './client'\nimport * as utils from './utils'\nimport * as assert from 'assert'\n\nfunction getRecordData (clientExpression: string, recordName: string) {\n  return clientHandler.getClients(clientExpression).map((client) => client.record.records[recordName])\n}\n\nfunction getListData (clientExpression: string, listName: string) {\n  return clientHandler.getClients(clientExpression).map((client) => client.record.lists[listName])\n}\n\nconst assert2 = {\n    deleted (clientExpression: string, recordName: string, called: boolean) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            if (called) {\n              sinon.assert.calledOnce(recordData.deleteCallback)\n              recordData.deleteCallback.resetHistory()\n            } else {\n              sinon.assert.notCalled(recordData.deleteCallback)\n            }\n            recordData.deleteCallback.resetHistory()\n        })\n    },\n\n    discarded (clientExpression: string, recordName: string, called: boolean) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            if (called) {\n                sinon.assert.calledOnce(recordData.discardCallback)\n                recordData.discardCallback.resetHistory()\n            } else {\n                sinon.assert.notCalled(recordData.discardCallback)\n            }\n        })\n    },\n\n    receivedUpdate (clientExpression: string, recordName: string, data: string) {\n        data = utils.parseData(data)\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            sinon.assert.calledOnce(recordData.subscribeCallback)\n            sinon.assert.calledWith(recordData.subscribeCallback, data)\n            recordData.subscribeCallback.resetHistory()\n        })\n    },\n\n    receivedUpdateForPath (clientExpression: string, recordName: string, path: string, data: string) {\n        data = utils.parseData(data)\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            sinon.assert.calledOnce(recordData.subscribePathCallbacks[path])\n            sinon.assert.calledWith(recordData.subscribePathCallbacks[path], data)\n            recordData.subscribePathCallbacks[path].resetHistory()\n        })\n    },\n\n    receivedNoUpdate (clientExpression: string, recordName: string) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            sinon.assert.notCalled(recordData.subscribeCallback)\n        })\n    },\n\n    receivedNoUpdateForPath (clientExpression: string, recordName: string, path: string) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            sinon.assert.notCalled(recordData.subscribePathCallbacks[path])\n        })\n    },\n\n    receivedRecordError (clientExpression: string, error: string, recordName: string) {\n        cl.receivedOneError(clientExpression, 'RECORD', error)\n\n        // getRecordData(clientExpression, recordName).forEach((recordData) => {\n            // sinon.assert.calledWith(recordData.errorCallback, error)\n            // recordData.errorCallback.resetHistory()\n        // })\n    },\n\n    hasData (clientExpression: string, recordName: string, data: string) {\n        data = utils.parseData(data)\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            assert.deepEqual(recordData.record.get(), data)\n        })\n    },\n\n    hasProviders (clientExpression: string, recordName: string, without: boolean) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            assert.deepEqual(recordData.record.hasProvider, !without)\n        })\n    },\n\n    hasDataAtPath (clientExpression: string, recordName: string, path: string, data: string) {\n        data = utils.parseData(data)\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            assert.deepEqual(recordData.record.get(path), data)\n        })\n    },\n\n    writeAckSuccess (clientExpression: string, recordName: string) {\n        getRecordData(clientExpression, recordName).forEach((recordData) => {\n            if (!recordData) { return }\n            sinon.assert.calledOnce(recordData.setCallback)\n            sinon.assert.calledWith(recordData.setCallback, null)\n            recordData.setCallback.resetHistory()\n        })\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            if (!client.record.writeAcks) { return }\n            sinon.assert.calledOnce(client.record.writeAcks[recordName])\n            sinon.assert.calledWith(client.record.writeAcks[recordName], null)\n            client.record.writeAcks[recordName].resetHistory()\n        })\n    },\n\n    writeAckError (clientExpression: string, recordName: string, errorMessage: string) {\n        cl.receivedOneError(clientExpression, 'RECORD', errorMessage)\n\n        // getRecordData(clientExpression, recordName).forEach((recordData) => {\n        //     if (!recordData) { return }\n        //     sinon.assert.calledOnce(recordData.setCallback)\n        //     sinon.assert.calledWith(recordData.setCallback, errorMessage)\n        //     recordData.setCallback.resetHistory()\n        // })\n        // clientHandler.getClients(clientExpression).forEach((client) => {\n        //     if (!client.record.writeAcks) { return }\n        //     sinon.assert.calledOnce(client.record.writeAcks[recordName])\n        //     sinon.assert.calledWith(client.record.writeAcks[recordName], errorMessage)\n        //     client.record.writeAcks[recordName].resetHistory()\n        // })\n    },\n\n    snapshotSuccess (clientExpression: string, recordName: string, data: string) {\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            sinon.assert.calledOnce(client.record.snapshotCallback)\n            sinon.assert.calledWith(client.record.snapshotCallback, null, utils.parseData(data))\n            client.record.snapshotCallback.resetHistory()\n        })\n    },\n\n    snapshotError (clientExpression: string, recordName: string, data: string) {\n        cl.receivedOneError(clientExpression, 'RECORD', data)\n\n        // clientHandler.getClients(clientExpression).forEach((client) => {\n        //     sinon.assert.calledOnce(client.record.snapshotCallback)\n        //     sinon.assert.calledWith(client.record.snapshotCallback, data.replace(/\"/g, ''))\n        //     client.record.snapshotCallback.resetHistory()\n        // })\n    },\n\n    headSuccess (clientExpression: string, recordName: string, data: number) {\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            sinon.assert.calledOnce(client.record.headCallback)\n            sinon.assert.calledWith(client.record.headCallback, null, data)\n            client.record.headCallback.resetHistory()\n        })\n    },\n\n    headError (clientExpression: string, recordName: string, data: string) {\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            sinon.assert.calledOnce(client.record.headCallback)\n            sinon.assert.calledWith(client.record.headCallback, data.replace(/\"/g, ''))\n            client.record.snapshotCallback.resetHistory()\n        })\n    },\n\n    has (clientExpression: string, recordName: string, expected: boolean) {\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            sinon.assert.calledOnce(client.record.hasCallback)\n            sinon.assert.calledWith(client.record.hasCallback, null, expected)\n            client.record.hasCallback.resetHistory()\n        })\n    },\n\n    hasEntries (clientExpression: string, listName: string, data: string) {\n        data = utils.parseData(data)\n        getListData(clientExpression, listName).forEach((listData) => {\n            assert.deepEqual(listData.list.getEntries(), data)\n        })\n    },\n\n    addedNotified (clientExpression: string, listName: string, entryName: string) {\n        getListData(clientExpression, listName).forEach((listData) => {\n            sinon.assert.calledWith(listData.addedCallback, entryName)\n        })\n    },\n\n    removedNotified (clientExpression: string, listName: string, entryName: string) {\n        getListData(clientExpression, listName).forEach((listData) => {\n            sinon.assert.calledWith(listData.removedCallback, entryName)\n        })\n    },\n\n    movedNotified (clientExpression: string, listName: string, entryName: string) {\n        getListData(clientExpression, listName).forEach((listData) => {\n            sinon.assert.calledWith(listData.movedNotified, entryName)\n        })\n    },\n\n    listChanged (clientExpression: string, listName: string, data: string) {\n        data = utils.parseData(data)\n        getListData(clientExpression, listName).forEach((listData) => {\n            // sinon.assert.calledOnce( listData.subscribeCallback );\n            sinon.assert.calledWith(listData.subscribeCallback, data)\n        })\n    },\n\n    anonymousRecordContains (clientExpression: string, data: string) {\n        data = utils.parseData(data)\n        clientHandler.getClients(clientExpression).forEach((client) => {\n            assert.deepEqual(client.record.anonymousRecord.get(), data)\n        })\n    }\n}\n\nexport const record = {\n  assert: assert2,\n\n  getRecord (clientExpression: string, recordName: string) {\n    const clients = clientHandler.getClients(clientExpression)\n    clients.forEach((client) => {\n      const recordData = {\n        record: client.client.record.getRecord(recordName),\n        discardCallback: sinon.spy(),\n        deleteSuccessCallback: sinon.spy(),\n        deleteCallback: sinon.spy(),\n        callbackError: sinon.spy(),\n        subscribeCallback: sinon.spy(),\n        errorCallback: sinon.spy(),\n        setCallback: undefined,\n        subscribePathCallbacks: {}\n      }\n      recordData.record.on('delete', recordData.deleteCallback)\n      recordData.record.on('error', recordData.errorCallback)\n      recordData.record.on('discard', recordData.discardCallback)\n      client.record.records[recordName] = recordData\n    })\n  },\n\n  subscribe (clientExpression: string, recordName: string, immediate: boolean) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.subscribe(recordData.subscribeCallback, !!immediate)\n    })\n  },\n\n  unsubscribe (clientExpression: string, recordName: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.unsubscribe(recordData.subscribeCallback)\n    })\n  },\n\n  subscribeWithPath (clientExpression: string, recordName: string, path: string, immediate: boolean) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.subscribePathCallbacks[path] = sinon.spy()\n      recordData.record.subscribe(path, recordData.subscribePathCallbacks[path], !!immediate)\n    })\n  },\n\n  unsubscribeFromPath (clientExpression: string, recordName: string, path: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.unsubscribe(path, recordData.subscribePathCallbacks[path])\n    })\n  },\n\n  discard (clientExpression: string, recordName: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.discard()\n    })\n  },\n\n  delete (clientExpression: string, recordName: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      recordData.record.delete(recordData.deleteSuccessCallback)\n    })\n  },\n\n  setupWriteAck (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.record.records[recordName].setCallback = sinon.spy()\n    })\n  },\n\n  set (clientExpression: string, recordName: string, data: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      if (recordData.setCallback) {\n        recordData.record.set(utils.parseData(data), recordData.setCallback)\n      } else {\n        recordData.record.set(utils.parseData(data))\n      }\n    })\n  },\n\n  setWithPath (clientExpression: string, recordName: string, path: string, data: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      if (recordData.setCallback) {\n        recordData.record.set(path, utils.parseData(data), recordData.setCallback)\n      } else {\n        recordData.record.set(path, utils.parseData(data))\n      }\n    })\n  },\n\n  erase (clientExpression: string, recordName: string, path: string) {\n    getRecordData(clientExpression, recordName).forEach((recordData) => {\n      if (recordData.setCallback) {\n        recordData.record.set(path, undefined, recordData.setCallback)\n      } else {\n        recordData.record.set(path, undefined)\n      }\n    })\n  },\n\n  setData (clientExpression: string, recordName: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.setData(recordName, utils.parseData(data))\n    })\n  },\n\n  setDataWithWriteAck (clientExpression: string, recordName: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      if (!client.record.writeAcks) {\n        client.record.writeAcks = {}\n      }\n      client.record.writeAcks[recordName] = sinon.spy()\n      client.client.record.setData(recordName, utils.parseData(data), client.record.writeAcks[recordName])\n    })\n  },\n\n  setDataWithPath (clientExpression: string, recordName: string, path: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.setData(recordName, path, utils.parseData(data))\n    })\n  },\n\n  snapshot (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.snapshot(recordName, client.record.snapshotCallback)\n    })\n  },\n\n  has (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.has(recordName, client.record.hasCallback)\n    })\n  },\n\n  head (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.record.head(recordName, client.record.headCallback)\n    })\n  },\n\n  /** ******************************************************************************************************************************\n   *********************************************************** Lists ************************************************************\n   ********************************************************************************************************************************/\n\n   getList (clientExpression: string, listName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const listData = {\n        list: client.client.record.getList(listName),\n        discardCallback: sinon.spy(),\n        deleteCallback: sinon.spy(),\n        callbackError: sinon.spy(),\n        subscribeCallback: sinon.spy(),\n        addedCallback: sinon.spy(),\n        removedCallback: sinon.spy(),\n        movedCallback: sinon.spy()\n      }\n      listData.list.on('discard', listData.discardCallback)\n      listData.list.on('delete', listData.deleteCallback)\n      listData.list.on('entry-added', listData.addedCallback)\n      listData.list.on('entry-removed', listData.removedCallback)\n      listData.list.on('entry-moved', listData.movedCallback)\n      listData.list.subscribe(listData.subscribeCallback)\n      client.record.lists[listName] = listData\n    })\n   },\n\n   setEntries (clientExpression: string, listName: string, data: string) {\n      const entries = utils.parseData(data)\n      getListData(clientExpression, listName).forEach((listData) => {\n        listData.list.setEntries(entries)\n      })\n   },\n\n   addEntry (clientExpression: string, listName: string, entryName: string) {\n    getListData(clientExpression, listName).forEach((listData) => {\n      listData.list.addEntry(entryName)\n    })\n   },\n\n   removeEntry (clientExpression: string, listName: string, entryName: string) {\n    getListData(clientExpression, listName).forEach((listData) => {\n      listData.list.removeEntry(entryName)\n    })\n   },\n\n  /** ******************************************************************************************************************************\n   *********************************************************** ANONYMOUS RECORDS ************************************************************\n   ********************************************************************************************************************************/\n\n   getAnonymousRecord (clientExpression: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.record.anonymousRecord = client.client.record.getAnonymousRecord()\n    })\n   },\n\n   setName (clientExpression: string, recordName: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.record.anonymousRecord.setName(recordName)\n    })\n   }\n}\n"
  },
  {
    "path": "test-e2e/framework-v3/rpc.ts",
    "content": "// tslint:disable:no-shadowed-variable\n\nimport * as sinon from 'sinon'\nimport { clientHandler, E2EClient } from './client-handler'\nimport { RPCResponse } from '@deepstream/client/dist/src/rpc/rpc-response'\n\nlet rejected = false\n\nconst rpcs: { [index: string]: (client: E2EClient, data: any, response: RPCResponse) => void } = {\n  'addTwo': (client, data, response) => {\n    client.rpc.provides.addTwo()\n    response.send(data.numA + data.numB)\n  },\n  'double': (client, data, response) => {\n    client.rpc.provides.double()\n    response.send(data * 2)\n  },\n  'stringify': (client, data, response) => {\n    client.rpc.provides.stringify()\n    response.send(typeof data === 'object' ? JSON.stringify(data) : String(data))\n  },\n  'a-provide-b-request': (client, data, response) => {\n    client.rpc.provides['a-provide-b-request']()\n    response.send(data * 3)\n  },\n  'only-full-user-data': (client, data, response) => {\n    client.rpc.provides['only-full-user-data']()\n    response.send('ok')\n  },\n  'alwaysReject': (client, data, response) => {\n    client.rpc.provides.alwaysReject()\n    response.reject()\n  },\n  'alwaysError': (client, data, response) => {\n    client.rpc.provides.alwaysError()\n    response.error('always errors')\n  },\n  'neverRespond': (client) => {\n    client.rpc.provides.neverRespond()\n  },\n  'clientBRejects': (client, data, response) => {\n    client.rpc.provides.clientBRejects()\n    if (client.name === 'B') {\n      response.reject()\n    } else {\n      response.send(data.root * data.root)\n    }\n  },\n  'deny': (client, data, response) => {\n    // permissions always deny\n  },\n  'rejectOnce': (client, data, response) => {\n    client.rpc.provides.rejectOnce(data)\n    if (rejected) {\n      response.send('ok')\n      rejected = false\n    } else {\n      response.reject()\n      rejected = true\n    }\n  }\n}\n\nconst assert = {\n  recievesResponse (clientExpression: string, rpc: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      sinon.assert.calledOnce(client.rpc.callbacks[rpc])\n      sinon.assert.calledWith(client.rpc.callbacks[rpc], null, JSON.parse(data).toString())\n      client.rpc.callbacks[rpc].resetHistory()\n    })\n  },\n\n  recievesResponseWithError (clientExpression: string, eventually: boolean, rpc: string, error: string, done: Function) {\n    setTimeout(() => {\n      clientHandler.getClients(clientExpression).forEach((client) => {\n        sinon.assert.calledOnce(client.rpc.callbacks[rpc])\n        sinon.assert.calledWith(client.rpc.callbacks[rpc], error)\n        client.rpc.callbacks[rpc].resetHistory()\n        done()\n      })\n    }, eventually ? 150 : 0)\n  },\n\n  providerCalled (clientExpression: string, rpc: string, timesCalled: number, data?: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      sinon.assert.callCount(client.rpc.provides[rpc], timesCalled)\n      if (data) {\n        sinon.assert.calledWith(client.rpc.provides[rpc], JSON.parse(data))\n      }\n      client.rpc.provides[rpc].resetHistory()\n    })\n  }\n}\n\nexport const rpc = {\n  assert,\n\n  provide (clientExpression: string, rpc: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.rpc.provides[rpc] = sinon.spy()\n      client.client.rpc.provide(rpc, rpcs[rpc].bind(null, client))\n    })\n  },\n\n  unprovide (clientExpression: string, rpc: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      client.client.rpc.unprovide(rpc)\n    })\n  },\n\n  make (clientExpression: string, rpc: string, data: string) {\n    clientHandler.getClients(clientExpression).forEach((client) => {\n      const callback = client.rpc.callbacks[rpc] = sinon.spy()\n      client.client.rpc.make(rpc, JSON.parse(data), (error: any, result: any) => {\n        callback(error, result && result.toString())\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework-v3/utils.ts",
    "content": "export const defaultDelay: number = Number(process.env.DEFAULT_DELAY) || 10\n\nexport const parseData = (data: string) => {\n  if (data === undefined || data === 'undefined') {\n    return undefined\n  } else if (data === 'null') {\n    return null\n  }\n  try {\n    return JSON.parse(data)\n  } catch (e) {\n    return data\n  }\n}\n"
  },
  {
    "path": "test-e2e/framework-v3/world.ts",
    "content": "// tslint:disable:no-shadowed-variable\n\nimport { clientHandler } from './client-handler'\n\nexport const world = {\n  endTest (done: (...args: any[]) => void) {\n    const clients = clientHandler.clients\n    for (const client in clients) {\n      clientHandler.assertNoErrors(client)\n\n      for (const event in clients[client].event.callbacks) {\n        if (clients[client].event.callbacks[event].isSubscribed !== false) {\n          clients[client].client.event.unsubscribe(event, clients[client].event.callbacks[event])\n        }\n      }\n\n      setTimeout(function (client: string) {\n        for (const pattern in clients[client].event.callbacksListeners) {\n          if (clients[client].event.callbacksListeners[pattern].isListening !== false) {\n            clients[client].client.event.unlisten(pattern)\n          }\n        }\n      }.bind(null, client), 1)\n\n      setTimeout(function (client: string) {\n        clients[client].client.close()\n        delete clients[client]\n      }.bind(null, client), 50)\n    }\n\n    setTimeout(done, 100)\n  }\n}\n"
  },
  {
    "path": "test-e2e/steps/client/client-definition-step.ts",
    "content": "import {Before, After} from 'cucumber'\nconst { world } = require(`../../framework${process.env.V3 ? '-v3' : ''}/world`)\n\nBefore((/* scenario*/) => {\n  // client are connecting via \"Background\" explictly\n})\n\nAfter((scenario, done) => {\n  world.endTest(done!)\n})\n"
  },
  {
    "path": "test-e2e/steps/client/connection-steps.ts",
    "content": "import { defaultDelay } from '../../framework/utils'\nimport {When, Then, Given} from 'cucumber'\nconst { client } = require(`../../framework${process.env.V3 ? '-v3' : ''}/client`)\n\nThen(/^(.+) receives? at least one \"([^\"]*)\" error \"([^\"]*)\"$/, client.receivedErrorOnce)\nThen(/^(.+) receives? \"([^\"]*)\" error \"([^\"]*)\"$/, client.receivedOneError)\nThen(/^(.+) received? no errors$/, client.receivedNoErrors)\n\nGiven(/^(.+) logs? out$/, (clientExpression: string, done) => {\n  client.logsOut(clientExpression, () => {})\n  setTimeout(done, defaultDelay)\n})\n\nGiven(/^(.+) connects? to server (\\d+)$/, client.connect)\n\nThen(/^(.+) connections? times? out$/, client.connectionTimesOut)\n\n// Then(/^(.+) has connection state \"([^\"]*)\"$/, (clientExpression: string, state) =>\n//   client.hasConnectionState(clientExpression: string, state))\n\nThen(/^(.+) had a connection state change to \"([^\"]*)\"$/, (clientExpression: string, state) =>\n  client.hadConnectionState(clientExpression, true, state))\n\nThen(/^(.+) did not have a connection state change to \"([^\"]*)\"$/, (clientExpression: string, state) =>\n  client.hadConnectionState(clientExpression, false, state))\n\nGiven(/^(.+) connects? and logs? into server (\\d+)$/, (clientExpression: string, server, done) => {\n  client.connectAndLogin(clientExpression, server, () => {\n    setTimeout(done, defaultDelay)\n  })\n})\n\nGiven(/^(.+) logs? in with username \"([^\"]*)\" and password \"([^\"]*)\"$/, (clientExpression: string, username, password, done) => {\n  client.login(clientExpression, username, password, () => {\n    setTimeout(done, defaultDelay)\n  })\n})\n\nWhen(/^(.+) attempts? to login with username \"([^\"]*)\" and password \"([^\"]*)\"$/, client.attemptLogin)\n\nThen(/^(.+) (?:is|are) notified of too many login attempts$/, client.receivedTooManyLoginAttempts)\n\nThen(/^(.+) receives? no login response$/, (clientExpression) => {\n  client.recievesNoLoginResponse(clientExpression)\n})\n\nThen(/^(.+) receives? an (un)?authenticated login response(?: with data ({.*}))?$/, (clientExpression: string, unauth, data) => {\n  client.recievesLoginResponse(clientExpression, unauth, data)\n})\n\nThen(/^(.+) \"([^\"]*)\" callback was( not)? called( once)?( with ({.*}))?$/, (clientExpression: string, eventName, notCalled, once, data) => {\n  if (eventName !== 'clientDataChanged' && eventName !== 'reauthenticationFailure') {\n    client.callbackCalled(clientExpression, eventName, notCalled, false, data)\n  }\n})\n"
  },
  {
    "path": "test-e2e/steps/client/event-steps.ts",
    "content": "import { defaultDelay } from '../../framework/utils'\nimport {When, Then, Given} from 'cucumber'\nconst { event } = require(`../../framework${process.env.V3 ? '-v3' : ''}/event`)\n\nWhen(/^(.+) publishes? (?:an|the) event \"([^\"]*)\"(?: with data (\"[^\"]*\"|\\d+|{.*}))?$/, (clientExpression: string, subscriptionName, data, done) => {\n  event.publishes(clientExpression, subscriptionName, data)\n  setTimeout(done, defaultDelay)\n})\n\nThen(/^(.+) receives? (the|no) event \"([^\"]*)\"(?: with data (.+))?$/, (clientExpression: string, theNo, subscriptionName, data) => {\n  event.assert.received(clientExpression, !theNo.match(/^no$/), subscriptionName, data)\n})\n\nGiven(/^(.+) subscribes? to (?:an|the) event \"([^\"]*)\"$/, (clientExpression: string, subscriptionName, done) => {\n  event.subscribes(clientExpression, subscriptionName)\n  setTimeout(done, defaultDelay * 10)\n})\n\nWhen(/^(.+) unsubscribes from (?:an|the) event \"([^\"]*)\"$/, (clientExpression: string, subscriptionName, done) => {\n  event.unsubscribes(clientExpression, subscriptionName)\n  setTimeout(done, defaultDelay)\n})\n"
  },
  {
    "path": "test-e2e/steps/client/listening-steps.ts",
    "content": "import { defaultDelay } from '../../framework/utils'\nimport {When, Then} from 'cucumber'\nconst { listening } = require(`../../framework${process.env.V3 ? '-v3' : ''}/listening`)\n\nWhen(/^publisher (\\S*) (accepts|rejects) (?:a|an) (event|record) match \"([^\"]*)\" for pattern \"([^\"]*)\"$/, (client, action, type, subscriptionName, pattern) => {\n  listening.setupListenResponse(client, action === 'accepts', type, subscriptionName, pattern)\n})\n\nWhen(/^publisher (\\S*) listens to (?:a|an) (event|record) with pattern \"([^\"]*)\"$/, (client, type, pattern, done) => {\n  listening.listens(client, type, pattern)\n  setTimeout(done, defaultDelay)\n})\n\nWhen(/^publisher (\\S*) unlistens to the (event|record) pattern \"([^\"]*)\"$/, (client, type, pattern, done) => {\n  listening.unlistens(client, type, pattern)\n  setTimeout(done, defaultDelay)\n})\n\nThen(/^publisher (\\S*) does not receive (?:a|an) (event|record) match \"([^\"]*)\" for pattern \"([^\"]*)\"$/, listening.assert.doesNotRecieveMatch)\n\nThen(/^publisher (\\S*) receives (\\d+) (event|record) (?:match|matches) \"([^\"]*)\" for pattern \"([^\"]*)\"$/, listening.assert.recievesMatch)\n\nThen(/^publisher (\\S*) removed (\\d+) (event|record) (?:match|matches) \"([^\"]*)\" for pattern \"([^\"]*)\"$/, listening.assert.receivedUnMatch)\n"
  },
  {
    "path": "test-e2e/steps/client/presence-steps.ts",
    "content": "import { defaultDelay } from '../../framework/utils'\nimport {When, Then, Given} from 'cucumber'\nconst { presence } = require(`../../framework${process.env.V3 ? '-v3' : ''}/presence`)\n\nGiven(/^(.+) subscribes to presence events$/, (clientExpression: string, done) => {\n  presence.subscribe(clientExpression)\n  setTimeout(done, defaultDelay)\n})\n\nGiven(/^(.+) unsubscribes to presence events$/, (clientExpression: string, done) => {\n  presence.unsubscribe(clientExpression)\n  setTimeout(done, defaultDelay)\n})\n\nGiven(/^(.+) subscribes to presence events for \"([^\"]*)\"$/, (clientExpression: string, users: string, done) => {\n  users.split(',').forEach((user) => presence.subscribe(clientExpression, user))\n  setTimeout(done, defaultDelay * 3)\n})\n\nGiven(/^(.+) unsubscribes to presence events for \"([^\"]*)\"$/, (clientExpression: string, users: string, done) => {\n  users.split(',').forEach((user) => presence.unsubscribe(clientExpression, user))\n  setTimeout(done, defaultDelay)\n})\n\nWhen(/^(.+) queries for connected clients$/, (clientExpression: string, done) => {\n  presence.getAll(clientExpression)\n  setTimeout(done, defaultDelay)\n})\n\nWhen(/^(.+) queries for clients \"([^\"]*)\"$/, (clientExpression: string, clients: string, done) => {\n  presence.getAll(clientExpression, clients.split(','))\n  setTimeout(done, defaultDelay)\n})\n\nThen(/^(.+) (?:is|are) (not )?notified that (.+) logged ([^\"]*)$/, presence.assert.notifiedUserStateChanged)\n\nThen(/^(.+) is notified that (?:clients|client) \"([^\"]*)\" (?:are|is) connected$/, (clientExpression: string, connectedClients) => {\n  presence.assert.globalQueryResult(clientExpression, null, connectedClients.split(','))\n})\n\nThen(/^(.+) receives a \"([^\"]*)\" error on their query$/, (clientExpression: string, error) => {\n  presence.assert.globalQueryResult(clientExpression, error)\n})\n\nThen(/^(.+) (?:is|are) notified that (?:clients|client) \"([^\"]*)\" (?:are|is) online$/, (clientExpression: string, clients) => {\n  presence.assert.queryResult(clientExpression, clients.split(','), true)\n})\n\nThen(/^(.+) (?:is|are) notified that (?:clients|client) \"([^\"]*)\" (?:are|is) offline$/, (clientExpression: string, clients) => {\n  presence.assert.queryResult(clientExpression, clients.split(','), false)\n})\n\nThen(/^(.+) is notified that no clients are connected$/, (clientExpression: string) => {\n  presence.assert.globalQueryResult(clientExpression, null, [])\n})\n"
  },
  {
    "path": "test-e2e/steps/client/record-steps.ts",
    "content": "import { defaultDelay } from '../../framework/utils'\nimport {When, Then, Given} from 'cucumber'\nconst { record } = require(`../../framework${process.env.V3 ? '-v3' : ''}/record`)\nconst { client } = require(`../../framework${process.env.V3 ? '-v3' : ''}/client`)\n\nWhen(/(.+) gets? the record \"([^\"]*)\"$/, (clientExpression: string, recordName: string, done) => {\n  record.getRecord(clientExpression, recordName)\n  setTimeout(done, defaultDelay * 3)\n})\n\nWhen(/(.+) sets the merge strategy to (remote|local)$/, (clientExpression: string, recordName: string) => {\n  // not implemented\n})\n\nThen(/^(.+) (gets?|is not) notified of record \"([^\"]*)\" getting (discarded|deleted)$/, (clientExpression: string, notified, recordName, action) => {\n  const called = notified.indexOf('is not') !== -1 ? false : true\n  if (action === 'discarded') {\n    // record.assert.discarded(clientExpression, recordName, called)\n  } else {\n    record.assert.deleted(clientExpression, recordName, called)\n  }\n})\n\nThen(/^(.+) receives? an? \"([^\"]*)\" error on record \"([^\"]*)\"$/, (clientExpression: string, error: string, recordName: string, done) => {\n  record.assert.receivedRecordError(clientExpression, error, recordName)\n\n  if (recordName ==='only-a-can-read-and-create') client.receivedOneError(clientExpression, 'record', error)\n\n  setTimeout(done, defaultDelay)\n})\n\nThen(/^(.+) receives? an update for record \"([^\"]*)\" with data '([^']+)'$/, record.assert.receivedUpdate)\n\nThen(/^(.+) receives? an update for record \"([^\"]*)\" and path \"([^\"]*)\" with data '([^']+)'$/, record.assert.receivedUpdateForPath)\n\nThen(/^(.+) (?:don't|doesn't|does not) receive an update for record \"([^\"]*)\"$/, record.assert.receivedNoUpdate)\n\nThen(/^(.+) don't receive an update for record \"([^\"]*)\" and path \"([^\"]*)\"$/, record.assert.receivedNoUpdateForPath)\n\nGiven(/^(.+) subscribes? to record \"([^\"]*)\"( with immediate flag)?$/, record.subscribe)\n\nGiven(/^(.+) unsubscribes? to record \"([^\"]*)\"$/, record.unsubscribe)\n\nGiven(/^(.+) subscribes? to record \"([^\"]*)\" with path \"([^\"]*)\"( with immediate flag)?$/, record.subscribeWithPath)\nGiven(/^(.+) unsubscribes? to record \"([^\"]*)\" with path \"([^\"]*)\"$/, record.unsubscribeFromPath)\n\nThen(/^(.+) (?:have|has) record \"([^\"]*)\" with data '([^']+)'$/, record.assert.hasData)\n\nThen(/^(.+) (?:have|has) record \"([^\"]*)\" with(out)? providers$/, record.assert.hasProviders)\n\nThen(/^(.+) (?:have|has) record \"([^\"]*)\" with path \"([^\"]*)\" and data '([^']+)'$/, record.assert.hasDataAtPath)\n\nGiven(/^(.+) discards record \"([^\"]*)\"$/, (clientExpression: string, recordName: string, done) => {\n  record.discard(clientExpression, recordName)\n  setTimeout(done, defaultDelay)\n})\n\nGiven(/^(.+) deletes record \"([^\"]*)\"$/, (clientExpression: string, recordName: string, done) => {\n  record.delete(clientExpression, recordName)\n  setTimeout(done, defaultDelay)\n})\n\nWhen(/^(.+) requires? write acknowledgements for record \"([^\"]*)\"$/, (clientExpression: string, recordName: string) => {\n  record.setupWriteAck(clientExpression, recordName)\n})\n\nWhen(/^(.+) sets? the record \"([^\"]*)\" with data '([^']+)'$/, (clientExpression: string, recordName: string, data: string, done) => {\n  record.set(clientExpression, recordName, data)\n  setTimeout(done, defaultDelay)\n})\n\nWhen(/^(.+) sets? the record \"([^\"]*)\" without being subscribed with data '([^']+)'$/, (clientExpression: string, recordName: string, data: string, done) => {\n  record.setData(clientExpression, recordName, data)\n  setTimeout(done, defaultDelay)\n})\n\nWhen(/^(.+) sets? the record \"([^\"]*)\" without being subscribed with data '([^']+)' and requires write acknowledgement$/, (clientExpression: string, recordName: string, data: string, done) => {\n  record.setDataWithWriteAck(clientExpression, recordName, data)\n  setTimeout(done, defaultDelay)\n})\n\nWhen(\n    /^(.+) sets? the record \"([^\"]*)\" without being subscribed with path \"([^\"]*)\" and data '([^']+)'$/,\n(clientExpression: string, recordName: string, path, data, done) => {\n  record.setDataWithPath(clientExpression, recordName, path, data)\n  setTimeout(done, defaultDelay)\n})\n\nWhen(/^(.+) sets? the record \"([^\"]*)\" and path \"([^\"]*)\" with data '([^']+)'$/, (clientExpression: string, recordName: string, path, data, done) => {\n  record.setWithPath(clientExpression, recordName, path, data)\n  setTimeout(done, defaultDelay)\n})\n\nWhen(/^(.+) erases the path \"([^\"]*)\" on record \"([^\"]*)\"$/, (clientExpression: string, path, recordName, done) => {\n  record.erase(clientExpression, recordName, path)\n  setTimeout(done, defaultDelay)\n})\n\nThen(/^(.+) is told that the record \"([^\"]*)\" was set without error$/, record.assert.writeAckSuccess)\n\nThen(/^(.+) is told that the record \"([^\"]*)\" experienced error \"([^\"]*)\" while setting$/, (clientExpression: string, recordName: string, errorMessage, done) => {\n  setTimeout(() => {\n    record.assert.writeAckError(clientExpression, recordName, errorMessage)\n    done()\n  }, 100)\n})\n\nGiven(/^(.+) requests? a snapshot of record \"([^\"]*)\"$/, (clientExpression: string, recordName: string, done) => {\n  record.snapshot(clientExpression, recordName)\n  setTimeout(done, defaultDelay)\n})\n\nThen(/^(.+) gets? a snapshot response for \"([^\"]*)\" with data '([^']+)'$/, record.assert.snapshotSuccess)\nThen(/^(.+) gets? a snapshot response for \"([^\"]*)\" with error '([^']+)'$/, record.assert.snapshotError)\n\nGiven(/^(.+) asks? if record \"([^\"]*)\" exists$/, (clientExpression: string, recordName: string, done) => {\n  record.has(clientExpression, recordName)\n  setTimeout(done, defaultDelay)\n})\n\nThen(/^(.+) gets? told record \"([^\"]*)\" (.*)exists?$/, (clientExpression: string, recordName: string, adjective) => {\n  record.assert.has(clientExpression, recordName, (adjective || '').indexOf('not') === -1)\n})\n\nThen(/^(.+) asks? for the version of record \"([^\"]*)\"$/, (clientExpression: string, recordName: string, done) => {\n  record.head(clientExpression, recordName)\n  setTimeout(done, defaultDelay)\n})\n\nThen(/^(.+) gets? told record \"([^\"]*)\" has version (.*)$/, (clientExpression: string, recordName: string, version: string) => {\n  record.assert.headSuccess(clientExpression, recordName, Number(version))\n})\n\nThen(/^(.+) gets? a head response for \"([^\"]*)\" with error '([^']+)'$/, record.assert.headError)\n\n  /** ******************************************************************************************************************************\n   *********************************************************** Lists ************************************************************\n   ********************************************************************************************************************************/\n\nWhen(/(.+) gets? the list \"([^\"]*)\"$/, (clientExpression: string, listName, done) => {\n  record.getList(clientExpression, listName)\n  setTimeout(done, defaultDelay * 3)\n})\n\nGiven(/^(.+) sets the entries on the list \"([^\"]*)\" to '([^']*)'$/, (clientExpression: string, listName: string, data: string, done) => {\n  record.setEntries(clientExpression, listName, data)\n  setTimeout(done, defaultDelay)\n})\n\nGiven(/^(.+) (adds|removes) an entry \"([^\"]*)\" (?:to|from) \"([^\"\"]*)\"$/, (clientExpression: string, action, entryName, listName, done) => {\n  if (action === 'adds') {\n    record.addEntry(clientExpression, listName, entryName)\n  } else {\n    record.removeEntry(clientExpression, listName, entryName)\n  }\n  setTimeout(done, defaultDelay)\n})\n\nThen(/^(.+) have a list \"([^\"]*)\" with entries '([^']*)'$/, record.assert.hasEntries)\n\nThen(/^(.+) gets? notified of \"([^\"]*)\" being (added|removed|moved) (?:to|in|from) \"([^\"\"]*)\"$/, (clientExpression: string, entryName, action, listName) => {\n  if (action === 'added') {\n    record.assert.addedNotified(clientExpression, listName, entryName)\n  } else if (action === 'removed') {\n    record.assert.removedNotified(clientExpression, listName, entryName)\n  } else {\n    record.assert.movedNotified(clientExpression, listName, entryName)\n  }\n})\n\nThen(/^(.+) gets? notified of list \"([^\"]*)\" entries changing to '([^']*)'$/, record.assert.listChanged)\n\n  /** ******************************************************************************************************************************\n   *********************************************************** ANONYMOUS RECORDS ************************************************************\n   ********************************************************************************************************************************/\n\nWhen(/(.+) gets? a anonymous record$/, record.getAnonymousRecord)\n\nWhen(/(.+) sets? the underlying record to \"([^\"]*)\" on the anonymous record$/, (clientExpression: string, recordName: string, done) => {\n  record.setName(clientExpression, recordName)\n  setTimeout(done, defaultDelay)\n})\n\nThen(/(.+) anonymous record data is '([^']*)'$/, (clientExpression: string, data: string) => {\n  record.assert.anonymousRecordContains(clientExpression, data)\n})\n"
  },
  {
    "path": "test-e2e/steps/client/rpc-steps.ts",
    "content": "import { defaultDelay } from '../../framework/utils'\nimport {When, Then, Given} from 'cucumber'\nconst { rpc } = require(`../../framework${process.env.V3 ? '-v3' : ''}/rpc`)\n\nGiven(/^(.+) provides? the RPC \"([^\"]*)\"$/, (clientExpression: string, rpcName, done) => {\n  rpc.provide(clientExpression, rpcName)\n  setTimeout(done, defaultDelay)\n})\n\nGiven(/^(.+) unprovides? the RPC \"([^\"]*)\"$/, (clientExpression: string, rpcName, done) => {\n  rpc.unprovide(clientExpression, rpcName)\n  setTimeout(done, defaultDelay)\n})\n\nWhen(/^(.+) calls? the RPC \"([^\"]*)\" with arguments? (\"[^\"]*\"|\\d+|{.*})$/, (clientExpression: string, rpcName, args, done) => {\n  rpc.make(clientExpression, rpcName, args)\n  setTimeout(done, defaultDelay)\n})\n\nThen(/(.+) receives? a response for RPC \"([^\"]*)\" with data (\"[^\"]*\"|\\d+|{.*})$/, rpc.assert.recievesResponse)\n\nThen(/(.+) (eventually )?receives? a response for RPC \"([^\"]*)\" with error \"([^\"]*)\"$/, rpc.assert.recievesResponseWithError)\n\nThen(/(.+) RPCs? \"([^\"]*)\" (?:is|are) never called$/, (clientExpression: string, rpcName) => {\n  rpc.assert.providerCalled(clientExpression, rpcName, 0)\n})\n\nThen(/(.+) RPCs? \"([^\"]*)\" (?:is|are) called once( with data (\"[^\"]*\"|\\d+|{.*}))?$/, (clientExpression: string, rpcName, data) => {\n  rpc.assert.providerCalled(clientExpression, rpcName, 1, data)\n})\n\nThen(/(.+) RPCs? \"([^\"]*)\" is called (\\d+) times$/, (clientExpression: string, rpcName, numTimes) => {\n  rpc.assert.providerCalled(clientExpression, rpcName, numTimes)\n})\n"
  },
  {
    "path": "test-e2e/steps/http/http-steps.ts",
    "content": "import * as sinon from 'sinon'\nimport {Given, When, Then, After } from 'cucumber'\nimport { expect } from 'chai'\nimport * as needle from 'needle'\nimport { parseData, defaultDelay } from '../../framework/utils'\nconst { clientHandler } = require(`../../framework${process.env.V3 ? '-v3' : ''}/client-handler`)\n\nlet httpClients: { [index: string]: any } = {}\n\nGiven(/^(.+) authenticates? with http server (\\d+)$/, (clientExpression: string, server, done) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    let serverUrl\n    if (global.e2eHarness.getAuthUrl) {\n      serverUrl = global.e2eHarness.getAuthUrl(server)\n    } else {\n      serverUrl = global.e2eHarness.getHttpUrl(server)\n    }\n    const message = {\n      username: clientName,\n      password: 'abcdefgh'\n    }\n\n    needle.post(serverUrl, message, { json: true }, (err, response) => {\n      process.nextTick(done)\n      expect(err).to.equal(null)\n      expect(response.statusCode).to.be.within(200, 299)\n      expect(response.body.token).to.be.a('string')\n      httpClients[clientName] = {\n        token: response.body.token,\n        serverUrl: global.e2eHarness.getHttpUrl(server - 1, clientName),\n        queue: [],\n        lastResponse: Object.assign({}, response, { isAuthResponse: true }),\n        resultChecked: false\n      }\n    })\n  })\n})\n\nGiven(/^(.+) authenticates? with http server (\\d+) with details (\"[^\"]*\"|\\d+|{.*})?$/, (clientExpression: string, server, data, done) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    let serverUrl\n    if (global.e2eHarness.getAuthUrl) {\n      serverUrl = global.e2eHarness.getAuthUrl(server - 1, clientName)\n    } else {\n      serverUrl = global.e2eHarness.getHttpUrl(server - 1, clientName)\n    }\n    const credentials = JSON.parse(data)\n    needle.post(serverUrl, credentials, { json: true }, (err, response) => {\n      process.nextTick(done)\n      expect(err).to.equal(null)\n      httpClients[clientName] = {\n        token: response.body.token,\n        serverUrl: global.e2eHarness.getHttpUrl(server - 1, clientName),\n        queue: [],\n        lastResponse: Object.assign({}, response, { isAuthResponse: true }),\n        resultChecked: false\n      }\n    })\n  })\n})\n\nThen(/^the last response (.+) received contained the properties \"([^\"]*)\"$/, (clientExpression: string, properties) => {\n  const propertyArray = properties.split(',')\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    expect(client.lastResponse.body).to.contain.keys(propertyArray)\n    expect(Object.keys(client.lastResponse.body).length).to.equal(propertyArray.length)\n    client.resultChecked = true\n  })\n})\n\nWhen(/^(.+) queues? (?:an|the|\"(\\d+)\") events? \"([^\"]*)\"(?: with data (\"[^\"]*\"|\\d+|{.*}))?$/, (clientExpression: string, numEvents, eventName, rawData) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const jifMessage = {\n      topic: 'event',\n      action: 'emit',\n      eventName,\n      data: null\n    }\n    if (rawData !== null) {\n      jifMessage.data = parseData(rawData)\n    }\n    if (numEvents === null) {\n      client.queue.push(jifMessage)\n    } else {\n      for (let i = 0; i < numEvents; i++) {\n        client.queue.push(jifMessage)\n      }\n    }\n  })\n})\n\nWhen(/^(.+) sends the data (\"[^\"]*\"|\\d+|{.*})$/, (clientExpression: string, rawData, done) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    needle.post(`${client.serverUrl}`, JSON.parse(rawData), { json: true }, (err, response) => {\n      setTimeout(done, defaultDelay)\n      client.lastResponse = response\n    })\n  })\n})\n\nWhen(/^(.+) queues \"(\\d+)\" random messages$/, (clientExpression: string, numMessages) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    for (let i = 0; i < numMessages; i++) {\n      const r = Math.random()\n      let message\n      if (r < 0.3) {\n        message = {\n          topic: 'event',\n          action: 'emit',\n          eventName: 'eventName'\n        }\n      } else if (r < 0.5) {\n        message = {\n          topic: 'record',\n          action: 'read',\n          recordName: 'recordName'\n        }\n      } else if (r < 0.6) {\n        message = {\n          topic: 'record',\n          action: 'write',\n          recordName: 'recordName',\n          path: 'r',\n          data: r\n        }\n      } else if (r < 0.7) {\n        message = {\n          topic: 'record',\n          action: 'head',\n          recordName: 'recordName',\n        }\n      } else if (r < 0.8) {\n        message = {\n          topic: 'rpc',\n          action: 'make',\n          rpcName: 'addTwo',\n          data: {\n            numA: (r * 1011) % 77,\n            numB: (r * 9528) % 63\n          }\n        }\n      } else {\n        message = {\n          topic: 'presence',\n          action: 'query',\n        }\n      }\n\n      client.queue.push(message)\n    }\n  })\n})\n\nWhen(/^(.+) queues a presence query$/, (clientExpression) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const jifMessage = {\n      topic: 'presence',\n      action: 'query'\n    }\n\n    client.queue.push(jifMessage)\n  })\n})\n\nWhen(/^(.+) queues? (?:an|the) RPC call to \"([^\"]*)\"(?: with arguments (\"[^\"]*\"|\\d+|{.*}))?$/, (clientExpression: string, rpcName, rawData) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const jifMessage = {\n      topic: 'rpc',\n      action: 'make',\n      rpcName,\n      data: null\n    }\n    if (rawData !== null) {\n      jifMessage.data = parseData(rawData)\n    }\n\n    client.queue.push(jifMessage)\n  })\n})\n\nWhen(/^(.+) queues? a fetch for (record|list) \"([^\"]*)\"$/, (clientExpression: string, recordOrList, recordName) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const jifMessage = recordOrList === 'record'\n      ? { topic: 'record', action: 'read', recordName }\n      : { topic: 'list', action: 'read', listName: recordName }\n\n    client.queue.push(jifMessage)\n  })\n})\n\nWhen(/^(.+) queues? a write to (record|list) \"([^\"]*)\"(?: and path \"([^\"]*)\")? with data '([^']*)'(?: and version \"(-?\\d+)\")?$/, (clientExpression: string, recordOrList, recordName, path, rawData, version) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const jifMessage = recordOrList === 'record'\n      ? { topic: 'record', action: 'write', recordName }\n      : { topic: 'list', action: 'write', listName: recordName }\n\n    if (path !== null) {\n      // @ts-ignore\n      jifMessage.path = path\n    }\n    if (rawData !== null) {\n      // @ts-ignore\n      jifMessage.data = parseData(rawData)\n    }\n    if (version !== null) {\n      // @ts-ignore\n      jifMessage.version = parseInt(version, 10)\n    }\n\n    client.queue.push(jifMessage)\n  })\n})\n\nWhen(/^(.+) queues? a notify for records? '([^\"]*)'$/, (clientExpression: string, recordNames) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const jifMessage = { topic: 'record', action: 'notify', recordNames: recordNames.split(',') }\n    client.queue.push(jifMessage)\n  })\n})\n\nWhen(/^(.+) queues? a delete for (record|list) \"([^\"]*)\"$/, (clientExpression: string, recordOrList, recordName) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const jifMessage = recordOrList === 'record'\n      ? { topic: 'record', action: 'delete', recordName }\n      : { topic: 'list', action: 'delete', listName: recordName }\n\n    client.queue.push(jifMessage)\n  })\n})\n\nWhen(/^(.+) queues? a head for record \"([^\"]*)\"$/, (clientExpression: string, recordName: string) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const jifMessage = {\n      topic: 'record',\n      action: 'head',\n      recordName,\n    }\n\n    client.queue.push(jifMessage)\n  })\n})\n\nWhen(/^(.+) flushe?s? their http queues?$/, (clientExpression: string, done) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const message = {\n      token: client.token,\n      body: client.queue\n    }\n    client.queue = []\n    needle.post(`${client.serverUrl}`, message, { json: true }, (err, response) => {\n      client.lastResponse = response\n      setTimeout(done, defaultDelay)\n    })\n  })\n})\n\nThen(/^(.+) last response said that clients? \"([^\"]*)\" (?:is|are) connected(?: at index \"(\\d+)\")?$/, (clientExpression: string, connectedClients, rawIndex) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const responseIndex = rawIndex === null ? 0 : rawIndex\n    const client = httpClients[clientName]\n    const lastResponse = client.lastResponse\n\n    expect(lastResponse).not.to.equal(null)\n    expect(lastResponse.body).to.be.an('object')\n    expect(lastResponse.body.body).to.be.an('array')\n    const result = lastResponse.body.body[responseIndex]\n    expect(result).to.be.an('object')\n    expect(result.success).to.equal(true)\n    expect(result.users).to.have.members(connectedClients.split(','))\n  })\n})\n\nThen(/^(.+) last response said that no clients are connected(?: at index \"(\\d+)\")?$/, (clientExpression: string, rawIndex) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const responseIndex = rawIndex === null ? 0 : rawIndex\n    const client = httpClients[clientName]\n    const lastResponse = client.lastResponse\n\n    expect(lastResponse).not.to.equal(null)\n    expect(lastResponse.body).to.be.an('object')\n    expect(lastResponse.body.body).to.be.an('array')\n    const result = lastResponse.body.body[responseIndex]\n    expect(result).to.be.an('object')\n    expect(result.success).to.equal(true)\n    expect(result.users).to.deep.equal([])\n  })\n})\n\nThen(/^(.+) receives? an RPC response(?: with data (\"[^\"]*\"|\\d+|{.*}))?(?: at index \"(\\d+)\")?$/, (clientExpression: string, rawData, rawIndex) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const responseIndex = rawIndex === null ? 0 : rawIndex\n    const client = httpClients[clientName]\n    const lastResponse = client.lastResponse\n\n    expect(lastResponse).not.to.equal(null)\n    expect(lastResponse.body).to.be.an('object')\n    expect(lastResponse.body.body).to.be.an('array')\n    const result = lastResponse.body.body[responseIndex]\n    expect(result).to.be.an('object')\n    expect(result.success).to.equal(true)\n    if (rawData !== null && rawData !== '\"undefined\"') {\n      expect(result.data).to.deep.equal(parseData(rawData))\n    }\n  })\n})\n\nThen(/^(.+) receives? the (?:record|list) (?:head )?\"([^\"]*)\"(?: with data '([^']+)')?(?: (?:with|and) version \"(\\d+)\")?(?: at index \"(\\d+)\")?$/, (clientExpression: string, recordName: string, rawData, version, rawIndex) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const responseIndex = rawIndex === null ? 0 : rawIndex\n    const client = httpClients[clientName]\n    const lastResponse = client.lastResponse\n\n    expect(lastResponse).not.to.equal(null)\n    expect(lastResponse.body).to.be.an('object')\n    expect(lastResponse.body.body).to.be.an('array')\n    const result = lastResponse.body.body[responseIndex]\n    expect(result).to.be.an('object')\n    expect(result.success).to.equal(true)\n    if (rawData !== null) {\n      expect(result.data).to.deep.equal(parseData(rawData))\n    }\n    if (version !== null) {\n      expect(result.version).to.equal(parseInt(version, 10))\n    }\n  })\n})\n\nThen(/^(.+) last response was a \"(\\S*)\"(?: with length \"(\\d+)\")?$/, (clientExpression: string, result, length) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const lastResponse = client.lastResponse\n\n    const failures = client.lastResponse.body.body.filter((res: any) => !res.success)\n    const failuresStr = JSON.stringify(failures, null, 2)\n    expect(lastResponse.body.result).to.equal(result, failuresStr)\n\n    if (length !== null) {\n      expect(lastResponse.body.body.length).to.equal(parseInt(length, 10))\n    }\n\n      // by default, clients are expected to have a SUCCESS response last, so mark as already\n      // checked\n    client.resultChecked = true\n  })\n})\n\nThen(/^(.+) (eventually )?receives \"(\\d+)\" events? \"([^\"]*)\"(?: with data (.+))?$/, (clientExpression: string, eventually, numEvents, subscriptionName, data, done) => {\n  setTimeout(() => {\n    clientHandler.getClients(clientExpression).forEach((client: any) => {\n      const eventSpy = client.event.callbacks[subscriptionName]\n      expect(eventSpy.callCount).to.equal(parseInt(numEvents, 10))\n      sinon.assert.calledWith(eventSpy, parseData(data))\n      eventSpy.resetHistory()\n      done()\n    })\n  }, eventually ? 350 : 0)\n})\n\nThen(/^(.+) last response had a success(?: at index \"(\\d+)\")?$/, (clientExpression: string, rawIndex) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const responseIndex = rawIndex === null ? 0 : rawIndex\n    const client = httpClients[clientName]\n    const lastResponse = client.lastResponse\n\n    expect(lastResponse).not.to.equal(null)\n    expect(lastResponse.body).to.be.an('object')\n    expect(lastResponse.body.body).to.be.an('array')\n    const result = lastResponse.body.body[responseIndex]\n\n    expect(result).to.be.an('object')\n    expect(result.success).to.equal(true)\n\n    client.resultChecked = true\n  })\n})\n\nThen(/^(.+) last response had an? \"([^\"]*)\" error matching \"([^\"]*)\"(?: at index \"(\\d+)\")?$/, (clientExpression: string, topic: string, message: string, rawIndex: number) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const responseIndex = rawIndex === null ? 0 : rawIndex\n    const client = httpClients[clientName]\n    const lastResponse = client.lastResponse\n\n    expect(lastResponse).not.to.equal(null)\n    expect(lastResponse.body).to.be.an('object')\n    expect(lastResponse.body.body).to.be.an('array')\n    const result = lastResponse.body.body[responseIndex]\n\n    expect(result).to.be.an('object')\n    expect(result.success).to.equal(false)\n    expect(result.errorTopic).to.equal(topic)\n    expect(result.error).to.match(new RegExp(message, 'i'))\n\n    client.resultChecked = true\n  })\n})\n\nThen(/^(.+) last response had an error matching \"([^\"]*)\"$/, (clientExpression: string, message) => {\n  clientHandler.getClientNames(clientExpression).forEach((clientName: string) => {\n    const client = httpClients[clientName]\n    const lastResponse = client.lastResponse\n    expect(lastResponse).not.to.equal(null)\n    expect(lastResponse.body).to.be.an('string')\n    expect(lastResponse.body).to.match(new RegExp(message, 'i'))\n\n    client.resultChecked = true\n  })\n})\n\nAfter(() => {\n  for (const clientName in httpClients) {\n    const client = httpClients[clientName]\n    if (client.lastResponse && !client.resultChecked && !client.lastResponse.isAuthResponse) {\n      const failures = client.lastResponse.body.body.filter((res: any) => !res.success)\n      const failuresStr = JSON.stringify(failures, null, 2)\n      expect(client.lastResponse.body.result).to.equal('SUCCESS', failuresStr)\n    }\n    client.lastResponse = null\n  }\n  httpClients = {}\n})\n"
  },
  {
    "path": "test-e2e/steps/server/step-definition-server.ts",
    "content": "import {Given, When, Before, BeforeAll } from 'cucumber'\nimport { PromiseDelay } from '../../../src/utils/utils'\nimport { E2EHarness } from '../../tools/e2e-harness'\n\nBefore(async (scenarioResult) => {\n  await global.e2eHarness.updatePermissions('open')\n})\n\nGiven(/\"([^\"]*)\" permissions are used$/, async (permissionType) => {\n  await global.e2eHarness.updatePermissions(permissionType)\n})\n\nWhen('storage remote updates {string} to {string} with version {int}', async (recordName, data, version) => {\n  await global.e2eHarness.updateStorageDirectly(recordName, version, data)\n})\n\nWhen('storage remote deletes {string}', async (recordName) => {\n  await global.e2eHarness.deleteFromStorageDirectly(recordName)\n})\n\nWhen(/^server (\\S)* goes down$/, async (server) => {\n  if (global.e2eHarness.started === false) {\n    return\n  }\n  await global.e2eHarness.stopServer(server)\n})\n\nWhen(/^all servers go down$/, async () => {\n  if (global.e2eHarness.started === false) {\n    return\n  }\n  await global.e2eHarness.stop()\n  await PromiseDelay(200)\n})\n\nWhen(/^server (\\S)* comes back up$/, async (server) => {\n  if (global.e2eHarness.started) {\n    return\n  }\n  await global.e2eHarness.startServer(server)\n})\n\nWhen(/^all servers come back up$/, async () => {\n  if (global.e2eHarness.started === true) {\n    return\n  }\n  await global.e2eHarness.start()\n  await PromiseDelay(200)\n})\n\nGiven(/^a small amount of time passes$/, (done) => {\n  setTimeout(done, 500)\n})\n\nBeforeAll(async () => {\n  global.e2eHarness = new E2EHarness([6001, 7001, 8001], process.env.ENABLE_LOGGING === 'true')\n  await global.e2eHarness.start()\n  await PromiseDelay(200)\n})\n\n// AfterAll((callback) => {\n//   setTimeout(() => {\n//     global.e2eHarness.once('stopped', () => {\n//       callback()\n//     })\n//     global.e2eHarness.stop()\n//   }, 500)\n// })\n"
  },
  {
    "path": "test-e2e/tools/e2e-authentication.ts",
    "content": "import { DeepstreamPlugin, DeepstreamAuthentication } from '@deepstream/types'\nimport { JSONObject } from '../../src/constants'\n\ninterface AuthData {\n    token?: string,\n    username?: string,\n    password?: string\n}\n\nexport class E2EAuthentication extends DeepstreamPlugin implements DeepstreamAuthentication {\n  public description: string = 'E2E Authentication'\n  public tokens = new Map<string, {\n    id?: string,\n    token?: string,\n    clientData?: JSONObject,\n    serverData?: JSONObject\n  }>()\n  public onlyLoginOnceUser: boolean = false\n\n  public async isValidUser (headers: JSONObject, authData: AuthData) {\n    if (authData.token) {\n        if (authData.token === 'letmein') {\n            return { isValid: true, id: 'A' }\n        }\n\n        // authenticate token\n        const response = this.tokens.get(authData.token)\n        if (response && response.id) {\n            return { isValid: true, ...response }\n        }\n    }\n\n    const username = authData.username\n    const token = Math.random().toString()\n    let clientData: any = null\n    const serverData: any = {}\n    let success\n\n    // authenticate auth data\n    const users = ['A', 'B', 'C', 'D', 'E', 'F', 'W', '1', '2', '3', '4', 'OPEN']\n    if (users.indexOf(username!) !== -1 && authData.password === 'abcdefgh') {\n        success = true\n    } else if (username === 'userA' && authData.password === 'abcdefgh') {\n        success = true\n        serverData.role = 'user'\n    } else if (username === 'userB' && authData.password === '123456789') {\n        success = true\n        clientData = { 'favorite color': 'orange', 'id': username }\n        serverData.role = 'admin'\n    } else if (username === 'randomClientData') {\n        success = true\n        clientData = { value : Math.random() }\n    } else if (username === 'onlyLoginOnce' && !this.onlyLoginOnceUser) {\n        this.onlyLoginOnceUser = true\n        success = true\n    } else {\n        success = false\n    }\n\n    const authResponseData = { id: username, token, clientData, serverData }\n\n    if (success) {\n        this.tokens.set(token, authResponseData)\n        return { isValid: true, ...authResponseData }\n    } else {\n        return { isValid: false }\n    }\n  }\n}\n"
  },
  {
    "path": "test-e2e/tools/e2e-cluster-node.ts",
    "content": "import { Message, TOPIC, STATE_REGISTRY_TOPIC } from '../../src/constants'\nimport { EventEmitter } from 'events'\nimport { DeepstreamServices, DeepstreamConfig, DeepstreamPlugin, DeepstreamClusterNode } from '@deepstream/types'\n\nexport class E2EClusterNode extends DeepstreamPlugin implements DeepstreamClusterNode {\n    public description: string = 'E2EClusterNode'\n    private static emitters = new Map<string, EventEmitter>()\n\n    constructor (options: any, services: DeepstreamServices, private config: DeepstreamConfig) {\n        super()\n        E2EClusterNode.emitters.set(this.config.serverName, new EventEmitter())\n    }\n\n    public sendDirect (toServer: string, message: Message, metaData?: any): void {\n      const msg = { ...message }\n      process.nextTick(() => {\n        E2EClusterNode.emitters.get(toServer)!.emit(TOPIC[message.topic], this.config.serverName, msg)\n      })\n    }\n\n    public send (message: Message, metaData?: any): void {\n        const msg = { ...message }\n        process.nextTick(() => {\n            for (const [serverName, emitter] of E2EClusterNode.emitters) {\n                if (serverName !== this.config.serverName) {\n                    emitter.emit(TOPIC[message.topic], this.config.serverName, { ...msg })\n                }\n            }\n        })\n    }\n\n    public subscribe<SpecificMessage> (topic: STATE_REGISTRY_TOPIC, callback: (message: SpecificMessage, serverName: string) => void): void {\n        E2EClusterNode.emitters.get(this.config.serverName)!.on(TOPIC[topic], (fromServer, message) => {\n            if (fromServer === this.config.serverName) {\n                throw new Error('Cyclic message was sent!')\n            }\n            callback(message, fromServer)\n        })\n    }\n\n    public async close () {\n        E2EClusterNode.emitters.delete(this.config.serverName)\n    }\n  }\n"
  },
  {
    "path": "test-e2e/tools/e2e-harness.ts",
    "content": "import { EventEmitter } from 'events'\nimport { PromiseDelay } from '../../src/utils/utils'\nimport { Deepstream } from '../../src/deepstream.io'\nimport { E2EAuthentication } from './e2e-authentication'\nimport { getServerConfig } from './e2e-server-config'\nimport { E2ELogger } from './e2e-logger'\nimport { STATES, JSONValue } from '../../src/constants'\nimport { LocalCache } from '../../src/services/cache/local-cache'\nimport { ConfigPermission } from '../../src/services/permission/valve/config-permission'\nimport { E2EClusterNode } from './e2e-cluster-node'\nimport * as openPermissions from '../config/permissions-open.json'\nimport * as complexPermissions from '../config/permissions-complex.json'\n\nconst cache = new LocalCache()\n\nconst SERVER_STOP_OR_START_DURATION = 200\n\nconst authenticationHandler = new E2EAuthentication()\n\nexport class E2EHarness extends EventEmitter {\n  private servers: Deepstream[] = []\n\n  constructor (private ports: number[], private enableLogging: boolean = false) {\n    super()\n    this.start()\n  }\n\n  public getServerName (serverId: number) {\n    return `server-${this.ports[serverId - 1]}`\n  }\n\n  public getUrl (serverId: number) {\n    return `localhost:${this.ports[serverId - 1]}/e2e`\n  }\n\n  public getHttpUrl (serverId: number) {\n    return `localhost:${this.ports[serverId - 1]}/api`\n  }\n\n  public getAuthUrl (serverId: number) {\n    return `localhost:${this.ports[serverId - 1]}/api/auth`\n  }\n\n  public async updateStorageDirectly (recordName: string, version: number, data: JSONValue) {\n    this.servers.forEach((server) => {\n      server.getServices().storage.set(recordName, version, data, () => {})\n    })\n\n    return new Promise((resolve) => setTimeout(resolve, 10))\n  }\n\n  public async deleteFromStorageDirectly (recordName: string) {\n    this.servers.forEach((server) => {\n      server.getServices().storage.delete(recordName, () => {})\n    })\n\n    return new Promise((resolve) => setTimeout(resolve, 10))\n  }\n\n  public async start () {\n    for (let i = 1; i <= this.ports.length; i++) {\n      this.startServer(i)\n    }\n    await this.whenReady()\n  }\n\n  public async stop () {\n    this.servers.forEach((server) => server.stop())\n    await this.whenStopped()\n  }\n\n  public async updatePermissions (type: string) {\n    const promises = this.servers.map((server) => {\n      const permission = server.getServices().permission as never as ConfigPermission\n      permission.useConfig(type === 'open' ? openPermissions : complexPermissions)\n    })\n    await Promise.all(promises)\n  }\n\n  public stopServer (serverId: number) {\n    return new Promise<void>(async (resolve) => {\n      const server = this.servers[serverId - 1]\n      if (!server) {\n        throw new Error(`Server ${serverId} not found`)\n      }\n      if (server.isRunning() === false) {\n        // Single node\n        resolve()\n        return\n      }\n      server.on('stopped', async () => {\n        await PromiseDelay(SERVER_STOP_OR_START_DURATION)\n        // @ts-ignore\n        this.servers[serverId - 1] = null\n        resolve()\n      })\n      server.stop()\n    })\n  }\n\n  public async startServer (serverId: number) {\n    if (this.servers[serverId - 1]) {\n      await PromiseDelay(SERVER_STOP_OR_START_DURATION)\n      return\n    }\n\n    const server = new Deepstream(getServerConfig(this.ports[serverId - 1])) as any\n    this.servers[serverId - 1] = server\n    const startedPromise = new Promise((resolve) => server.on('started', resolve))\n    if (this.enableLogging !== true) {\n      server.set('logger', new E2ELogger())\n    }\n    server.set('cache', cache)\n    server.set('authentication', authenticationHandler)\n    // @ts-ignore\n    server.set('clusterNode', new E2EClusterNode({}, server.services, server.config))\n    server.start()\n\n    await startedPromise\n    await PromiseDelay(SERVER_STOP_OR_START_DURATION * 2)\n  }\n\n  public async whenReady () {\n    const startedPromises = this.servers.reduce((result, server) => {\n      if (!server.isRunning()) {\n        result.push(new Promise((resolve) => server.on('started', resolve)))\n      }\n      return result\n    }, [] as Array<Promise<void>>)\n    await Promise.all(startedPromises)\n    await PromiseDelay(SERVER_STOP_OR_START_DURATION)\n  }\n\n  public async whenStopped () {\n    const stopPromises = this.servers.reduce((result, server) => {\n      // @ts-ignore\n      if (server.currentState !== STATES.STOPPED) {\n        result.push(new Promise((resolve) => server.on('stopped', resolve)))\n      }\n      return result\n    }, [] as Array<Promise<void>>)\n    await Promise.all(stopPromises)\n    await PromiseDelay(SERVER_STOP_OR_START_DURATION)\n    this.servers = []\n  }\n}\n"
  },
  {
    "path": "test-e2e/tools/e2e-logger.ts",
    "content": "import { DeepstreamLogger, DeepstreamPlugin, LOG_LEVEL, NamespacedLogger, EVENT } from '@deepstream/types'\n\ninterface Log {\n  level: number,\n  event: string,\n  message: string\n}\n\nexport class E2ELogger extends DeepstreamPlugin implements DeepstreamLogger {\n  public description = 'Test Logger'\n  public logs: Log[] = []\n  public lastLog: Log | null = null\n\n  public setLogLevel (logLevel: LOG_LEVEL): void {\n    throw new Error('Method not implemented.')\n  }\n\n  public shouldLog () {\n    return true\n  }\n\n  public error (event: EVENT, logMessage: string) {\n    this.log(LOG_LEVEL.ERROR, event, logMessage)\n  }\n\n  public warn (event: EVENT, logMessage: string) {\n    this.log(LOG_LEVEL.WARN, event, logMessage)\n  }\n\n  public info (event: EVENT, logMessage: string) {\n     this.log(LOG_LEVEL.INFO, event, logMessage)\n  }\n\n  public debug (event: EVENT, logMessage: string) {\n    this.log(LOG_LEVEL.DEBUG, event, logMessage)\n  }\n\n  public fatal (event: EVENT, logMessage: string) {\n    this.log(LOG_LEVEL.FATAL, event, logMessage)\n  }\n\n  public getNameSpace (namespace: string): NamespacedLogger {\n    return {\n      shouldLog: this.shouldLog.bind(this),\n      fatal: this.fatal.bind(this),\n      error: this.error.bind(this),\n      warn: this.warn.bind(this),\n      info: this.info.bind(this),\n      debug: this.debug.bind(this),\n    }\n  }\n\n  private log (logLevel: LOG_LEVEL, event: EVENT, logMessage: string) {\n    const log = {\n      level: logLevel,\n      event,\n      message: logMessage\n    }\n\n    this.logs.push(log)\n    this.lastLog = log\n\n    switch (logLevel) {\n      case 3:\n        throw new Error(`Critical error occured on deepstream ${event} ${logMessage}`)\n        break\n      case 2:\n        // console.log('Warning:', event, logMessage)\n        break\n    }\n  }\n}\n"
  },
  {
    "path": "test-e2e/tools/e2e-server-config.ts",
    "content": "import { PartialDeepstreamConfig, LOG_LEVEL } from '@deepstream/types'\nimport * as permissions from '../config/permissions-open.json'\n\nexport const getServerConfig = (port: number): PartialDeepstreamConfig => ({\n    serverName : `server-${port}`,\n    showLogo : false,\n\n    rpc: {\n      // This shouldn't be more than response,\n      // but it solves issues in E2E tests for HTTP bulk requests for now\n      ackTimeout: 100,\n      responseTimeout: 100,\n    },\n\n    listen: {\n      shuffleProviders: false,\n      responseTimeout: 2000,\n      rematchInterval: 60000,\n      matchCooldown: 10000\n    },\n\n    permission: {\n      type    : 'config',\n      options : {\n        permissions\n      } as any\n    },\n\n    httpServer: {\n      type: process.env.uws ? 'uws' : 'default',\n      options: {\n        port\n      }\n    },\n\n    connectionEndpoints: [\n      {\n        type: 'ws-binary',\n        options: {\n          urlPath: '/e2e-v4',\n          maxAuthAttempts              : 2,\n          unauthenticatedClientTimeout : 200,\n          heartbeatInterval: 10000\n        } as any\n      },\n      {\n        type: 'ws-text',\n        options: {\n          urlPath: '/e2e-v3',\n          maxAuthAttempts              : 2,\n          unauthenticatedClientTimeout : 200,\n          heartbeatInterval: 10000\n        } as any\n      },\n      {\n        type: 'http',\n        options: {\n          allowAuthData: true,\n          enableAuthEndpoint: true,\n        } as any\n      }\n    ],\n\n    monitoring: {\n      type: 'http',\n      options: {\n        url: '/monitoring',\n        allowOpenPermissions: false,\n        headerKey: 'deepstream-password',\n        headerValue: 'deepstream-secret'\n      } as any\n    },\n\n    telemetry: {\n      type: 'deepstreamIO',\n      options: {\n        enabled: false\n      }\n    },\n\n    logger: {\n      type: 'default',\n      options: {\n        logLevel: LOG_LEVEL.WARN\n      }\n    },\n\n    locks: {\n      type: 'default',\n      options: {\n        holdTimeout            : 1500,\n        requestTimeout         : 1500,\n      } as any\n    },\n\n    clusterNode: {\n      type: 'default',\n      options: {\n      } as any\n    },\n\n    clusterRegistry: {\n      type: 'default',\n      options: {\n        keepAliveInterval: 20,\n        activeCheckInterval: 200\n      } as any\n    },\n\n    clusterStates: {\n      type: 'default',\n      options: {\n        reconciliationTimeout : 100,\n      } as any\n    },\n\n    storage: {\n      path: './src/services/cache/local-cache',\n      options: {\n      } as any\n    }\n  })\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2018\",\n    \"module\": \"commonjs\",\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"sourceMap\": true,\n    \"allowJs\": false,\n    \"resolveJsonModule\": true,\n    \"noUnusedLocals\": true,\n    \"strict\": true,\n    \"lib\": [\"es2018\"],\n    \"types\": [\n      \"node\",\n      \"mocha\"\n    ]\n  },\n  \"include\": [\n    \"protocol/**/*\",\n    \"bin/*\",\n    \"src/**/*\",\n    \"src/**/*.json\",\n    \"test-e2e/**/*\",\n    \"package.json\", \"types/uws.d.ts\"\n  ],\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"node_modules\"\n  ],\n  \"files\": [\n    \"types/global.d.ts\",\n    \"types/uws.d.ts\"\n  ],\n}\n"
  },
  {
    "path": "tslint.json",
    "content": "{\n  \"defaultSeverity\": \"error\",\n  \"extends\": [\n      \"tslint:recommended\"\n  ],\n  \"jsRules\": {},\n  \"rules\": {\n      \"arrow-parens\": [true],\n      \"ordered-imports\": false,\n      \"trailing-comma\": false,\n      \"array-type\": [true, \"array-simple\"],\n      \"prefer-const\": true,\n      \"new-parens\": true,\n      \"no-consecutive-blank-lines\": true,\n      \"no-trailing-whitespace\": true,\n      \"no-unnecessary-initializer\": true,\n      \"one-variable-per-declaration\": true,\n      \"space-before-function-paren\": [true, \"always\"],\n      \"interface-name\": [true, \"never-prefix\"],\n      \"forin\": false,\n      \"indent\": [true, \"spaces\", 2],\n      \"jsdoc-format\": false,\n      \"object-literal-sort-keys\": false,\n      \"member-ordering\": false,\n      \"prefer-for-of\": false,\n      \"max-line-length\": false,\n      \"only-arrow-functions\": false,\n      \"ban-types\": false,\n      \"no-console\": false,\n      \"no-empty\": false,\n      \"semicolon\": [true, \"never\"],\n      \"no-var-requires\": false,\n      \"quotemark\": [true, \"single\", \"avoid-escape\", \"avoid-template\"],\n      \"unified-signatures\": false\n  },\n  \"rulesDirectory\": []\n}\n"
  },
  {
    "path": "types/global.d.ts",
    "content": "declare namespace NodeJS {\n  export interface Global {\n    deepstreamCLI: any\n    deepstreamLibDir: string | null\n    deepstreamConfDir: string | null\n    require (path: string): any\n    e2eHarness: any // Used by e2e tests\n  }\n}"
  },
  {
    "path": "types/uws.d.ts",
    "content": "/*\n * Authored by Alex Hultman, 2018-2021.\n * Intellectual property of third-party.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\ndeclare namespace uws {\n/** Native type representing a raw uSockets struct us_listen_socket_t.\n * Careful with this one, it is entirely unchecked and native so invalid usage will blow up.\n */\nexport interface us_listen_socket {\n\n}\n\n/** Native type representing a raw uSockets struct us_socket_t.\n * Careful with this one, it is entirely unchecked and native so invalid usage will blow up.\n */\nexport interface us_socket {\n\n}\n\n/** Native type representing a raw uSockets struct us_socket_context_t.\n * Used while upgrading a WebSocket manually. */\nexport interface us_socket_context_t {\n\n}\n\n/** Recognized string types, things C++ can read and understand as strings.\n * \"String\" does not have to mean \"text\", it can also be \"binary\".\n *\n * Ironically, JavaScript strings are the least performant of all options, to pass or receive to/from C++.\n * This because we expect UTF-8, which is packed in 8-byte chars. JavaScript strings are UTF-16 internally meaning extra copies and reinterpretation are required.\n *\n * That's why all events pass data by ArrayBuffer and not JavaScript strings, as they allow zero-copy data passing.\n *\n * You can always do Buffer.from(arrayBuffer).toString(), but keeping things binary and as ArrayBuffer is preferred.\n */\nexport type RecognizedString = string | ArrayBuffer | Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | Float32Array | Float64Array;\n\n/** A WebSocket connection that is valid from open to close event.\n * Read more about this in the user manual.\n */\nexport interface WebSocket<UserData> {\n    /** Sends a message. Returns 1 for success, 2 for dropped due to backpressure limit, and 0 for built up backpressure that will drain over time. You can check backpressure before or after sending by calling getBufferedAmount().\n     *\n     * Make sure you properly understand the concept of backpressure. Check the backpressure example file.\n     */\n    send(message: RecognizedString, isBinary?: boolean, compress?: boolean) : number;\n\n    /** Returns the bytes buffered in backpressure. This is similar to the bufferedAmount property in the browser counterpart.\n     * Check backpressure example.\n     */\n    getBufferedAmount() : number;\n\n    /** Gracefully closes this WebSocket. Immediately calls the close handler.\n     * A WebSocket close message is sent with code and shortMessage.\n     */\n    end(code?: number, shortMessage?: RecognizedString) : void;\n\n    /** Forcefully closes this WebSocket. Immediately calls the close handler.\n     * No WebSocket close message is sent.\n     */\n    close() : void;\n\n    /** Sends a ping control message. Returns sendStatus similar to WebSocket.send (regarding backpressure). This helper function correlates to WebSocket::send(message, uWS::OpCode::PING, ...) in C++. */\n    ping(message?: RecognizedString) : number;\n\n    /** Subscribe to a topic. */\n    subscribe(topic: RecognizedString) : boolean;\n\n    /** Unsubscribe from a topic. Returns true on success, if the WebSocket was subscribed. */\n    unsubscribe(topic: RecognizedString) : boolean;\n\n    /** Returns whether this websocket is subscribed to topic. */\n    isSubscribed(topic: RecognizedString) : boolean;\n\n    /** Returns a list of topics this websocket is subscribed to. */\n    getTopics() : string[];\n\n    /** Publish a message under topic. Backpressure is managed according to maxBackpressure, closeOnBackpressureLimit settings.\n     * Order is guaranteed since v20.\n    */\n    publish(topic: RecognizedString, message: RecognizedString, isBinary?: boolean, compress?: boolean) : boolean;\n\n    /** See HttpResponse.cork. Takes a function in which the socket is corked (packing many sends into one single syscall/SSL block) */\n    cork(cb: () => void) : WebSocket<UserData>;\n\n    /** Returns the remote IP address. Note that the returned IP is binary, not text.\n     *\n     * IPv4 is 4 byte long and can be converted to text by printing every byte as a digit between 0 and 255.\n     * IPv6 is 16 byte long and can be converted to text in similar ways, but you typically print digits in HEX.\n     *\n     * See getRemoteAddressAsText() for a text version.\n     */\n    getRemoteAddress() : ArrayBuffer;\n\n    /** Returns the remote IP address as text. See RecognizedString. */\n    getRemoteAddressAsText() : ArrayBuffer;\n\n    /** Returns the UserData object. */\n    getUserData() : UserData;\n}\n\n/** An HttpResponse is valid until either onAborted callback or any of the .end/.tryEnd calls succeed. You may attach user data to this object. */\nexport interface HttpResponse {\n    /** Writes the HTTP status message such as \"200 OK\".\n     * This has to be called first in any response, otherwise\n     * it will be called automatically with \"200 OK\".\n     *\n     * If you want to send custom headers in a WebSocket\n     * upgrade response, you have to call writeStatus with\n     * \"101 Switching Protocols\" before you call writeHeader,\n     * otherwise your first call to writeHeader will call\n     * writeStatus with \"200 OK\" and the upgrade will fail.\n     *\n     * As you can imagine, we format outgoing responses in a linear\n     * buffer, not in a hash table. You can read about this in\n     * the user manual under \"corking\".\n    */\n\n    /** Pause http body streaming (throttle) */\n    pause() : void;\n\n    /** Resume http body streaming (unthrottle) */\n    resume() : void;\n\n    writeStatus(status: RecognizedString) : HttpResponse;\n    /** Writes key and value to HTTP response.\n     * See writeStatus and corking.\n    */\n    writeHeader(key: RecognizedString, value: RecognizedString) : HttpResponse;\n    /** Enters or continues chunked encoding mode. Writes part of the response. End with zero length write. Returns true if no backpressure was added. */\n    write(chunk: RecognizedString) : boolean;\n    /** Ends this response by copying the contents of body. */\n    end(body?: RecognizedString, closeConnection?: boolean) : HttpResponse;\n    /** Ends this response without a body. */\n    endWithoutBody(reportedContentLength?: number, closeConnection?: boolean) : HttpResponse;\n    /** Ends this response, or tries to, by streaming appropriately sized chunks of body. Use in conjunction with onWritable. Returns tuple [ok, hasResponded].*/\n    tryEnd(fullBodyOrChunk: RecognizedString, totalSize: number) : [boolean, boolean];\n\n    /** Immediately force closes the connection. Any onAborted callback will run. */\n    close() : HttpResponse;\n\n    /** Returns the global byte write offset for this response. Use with onWritable. */\n    getWriteOffset() : number;\n\n    /** Registers a handler for writable events. Continue failed write attempts in here.\n     * You MUST return true for success, false for failure.\n     * Writing nothing is always success, so by default you must return true.\n     */\n    onWritable(handler: (offset: number) => boolean) : HttpResponse;\n\n    /** Every HttpResponse MUST have an attached abort handler IF you do not respond\n     * to it immediately inside of the callback. Returning from an Http request handler\n     * without attaching (by calling onAborted) an abort handler is ill-use and will terminate.\n     * When this event emits, the response has been aborted and may not be used. */\n    onAborted(handler: () => void) : HttpResponse;\n\n    /** Handler for reading data from POST and such requests. You MUST copy the data of chunk if isLast is not true. We Neuter ArrayBuffers on return, making it zero length.*/\n    onData(handler: (chunk: ArrayBuffer, isLast: boolean) => void) : HttpResponse;\n\n    /** Returns the remote IP address in binary format (4 or 16 bytes). */\n    getRemoteAddress() : ArrayBuffer;\n\n    /** Returns the remote IP address as text. */\n    getRemoteAddressAsText() : ArrayBuffer;\n\n    /** Returns the remote IP address in binary format (4 or 16 bytes), as reported by the PROXY Protocol v2 compatible proxy. */\n    getProxiedRemoteAddress() : ArrayBuffer;\n\n    /** Returns the remote IP address as text, as reported by the PROXY Protocol v2 compatible proxy. */\n    getProxiedRemoteAddressAsText() : ArrayBuffer;\n\n    /** Corking a response is a performance improvement in both CPU and network, as you ready the IO system for writing multiple chunks at once.\n     * By default, you're corked in the immediately executing top portion of the route handler. In all other cases, such as when returning from\n     * await, or when being called back from an async database request or anything that isn't directly executing in the route handler, you'll want\n     * to cork before calling writeStatus, writeHeader or just write. Corking takes a callback in which you execute the writeHeader, writeStatus and\n     * such calls, in one atomic IO operation. This is important, not only for TCP but definitely for TLS where each write would otherwise result\n     * in one TLS block being sent off, each with one send syscall.\n     *\n     * Example usage:\n     *\n     * ```\n     * res.cork(() => {\n     *   res.writeStatus(\"200 OK\").writeHeader(\"Some\", \"Value\").write(\"Hello world!\");\n     * });\n     * ```\n     */\n    cork(cb: () => void) : HttpResponse;\n\n    /** Upgrades a HttpResponse to a WebSocket. See UpgradeAsync, UpgradeSync example files. */\n    upgrade<UserData>(userData : UserData, secWebSocketKey: RecognizedString, secWebSocketProtocol: RecognizedString, secWebSocketExtensions: RecognizedString, context: us_socket_context_t) : void;\n\n    /** Arbitrary user data may be attached to this object */\n    [key: string]: any;\n}\n\n/** An HttpRequest is stack allocated and only accessible during the callback invocation. */\nexport interface HttpRequest {\n    /** Returns the lowercased header value or empty string. */\n    getHeader(lowerCaseKey: RecognizedString) : string;\n    /** Returns the parsed parameter at index. Corresponds to route. */\n    getParameter(index: number) : string;\n    /** Returns the URL including initial /slash */\n    getUrl() : string;\n    /** Returns the lowercased HTTP method, useful for \"any\" routes. */\n    getMethod() : string;\n    /** Returns the HTTP method as-is. */\n    getCaseSensitiveMethod() : string;\n    /** Returns the raw querystring (the part of URL after ? sign) or empty string. */\n    getQuery() : string;\n    /** Returns a decoded query parameter value or empty string. */\n    getQuery(key: string) : string;\n    /** Loops over all headers. */\n    forEach(cb: (key: string, value: string) => void) : void;\n    /** Setting yield to true is to say that this route handler did not handle the route, causing the router to continue looking for a matching route handler, or fail. */\n    setYield(_yield: boolean) : HttpRequest;\n}\n\n/** A structure holding settings and handlers for a WebSocket URL route handler. */\nexport interface WebSocketBehavior<UserData> {\n    /** Maximum length of received message. If a client tries to send you a message larger than this, the connection is immediately closed. Defaults to 16 * 1024. */\n    maxPayloadLength?: number;\n    /** Whether or not we should automatically close the socket when a message is dropped due to backpressure. Defaults to false. */\n    closeOnBackpressureLimit?: number;\n    /** Maximum number of minutes a WebSocket may be connected before being closed by the server. 0 disables the feature. */\n    maxLifetime?: number;\n    /** Maximum amount of seconds that may pass without sending or getting a message. Connection is closed if this timeout passes. Resolution (granularity) for timeouts are typically 4 seconds, rounded to closest.\n     * Disable by using 0. Defaults to 120.\n     */\n    idleTimeout?: number;\n    /** What permessage-deflate compression to use. uWS.DISABLED, uWS.SHARED_COMPRESSOR or any of the uWS.DEDICATED_COMPRESSOR_xxxKB. Defaults to uWS.DISABLED. */\n    compression?: CompressOptions;\n    /** Maximum length of allowed backpressure per socket when publishing or sending messages. Slow receivers with too high backpressure will be skipped until they catch up or timeout. Defaults to 64 * 1024. */\n    maxBackpressure?: number;\n    /** Whether or not we should automatically send pings to uphold a stable connection given whatever idleTimeout. */\n    sendPingsAutomatically?: boolean;\n    /** Upgrade handler used to intercept HTTP upgrade requests and potentially upgrade to WebSocket.\n     * See UpgradeAsync and UpgradeSync example files.\n     */\n    upgrade?: (res: HttpResponse, req: HttpRequest, context: us_socket_context_t) => void | Promise<void>;\n    /** Handler for new WebSocket connection. WebSocket is valid from open to close, no errors. */\n    open?: (ws: WebSocket<UserData>) => void | Promise<void>;\n    /** Handler for a WebSocket message. Messages are given as ArrayBuffer no matter if they are binary or not. Given ArrayBuffer is valid during the lifetime of this callback (until first await or return) and will be neutered. */\n    message?: (ws: WebSocket<UserData>, message: ArrayBuffer, isBinary: boolean) => void | Promise<void>;\n    /** Handler for when WebSocket backpressure drains. Check ws.getBufferedAmount(). Use this to guide / drive your backpressure throttling. */\n    drain?: (ws: WebSocket<UserData>) => void;\n    /** Handler for close event, no matter if error, timeout or graceful close. You may not use WebSocket after this event. Do not send on this WebSocket from within here, it is closed. */\n    close?: (ws: WebSocket<UserData>, code: number, message: ArrayBuffer) => void;\n    /** Handler for received ping control message. You do not need to handle this, pong messages are automatically sent as per the standard. */\n    ping?: (ws: WebSocket<UserData>, message: ArrayBuffer) => void;\n    /** Handler for received pong control message. */\n    pong?: (ws: WebSocket<UserData>, message: ArrayBuffer) => void;\n    /** Handler for subscription changes. */\n    subscription?: (ws: WebSocket<UserData>, topic: ArrayBuffer, newCount: number, oldCount: number) => void;\n}\n\n/** Options used when constructing an app. Especially for SSLApp.\n * These are options passed directly to uSockets, C layer.\n */\nexport interface AppOptions {\n    key_file_name?: RecognizedString;\n    cert_file_name?: RecognizedString;\n    ca_file_name?: RecognizedString;\n    passphrase?: RecognizedString;\n    dh_params_file_name?: RecognizedString;\n    ssl_ciphers?: RecognizedString;\n    /** This translates to SSL_MODE_RELEASE_BUFFERS */\n    ssl_prefer_low_memory_usage?: boolean;\n}\n\nexport enum ListenOptions {\n  LIBUS_LISTEN_DEFAULT = 0,\n  LIBUS_LISTEN_EXCLUSIVE_PORT = 1\n}\n\n/** TemplatedApp is either an SSL or non-SSL app. See App for more info, read user manual. */\nexport interface TemplatedApp {\n    /** Listens to hostname & port. Callback hands either false or a listen socket. */\n    listen(host: RecognizedString, port: number, cb: (listenSocket: us_listen_socket | false) => void | Promise<void>) : TemplatedApp;\n    /** Listens to port. Callback hands either false or a listen socket. */\n    listen(port: number, cb: (listenSocket: us_listen_socket | false) => void | Promise<void>) : TemplatedApp;\n    /** Listens to port and sets Listen Options. Callback hands either false or a listen socket. */\n    listen(port: number, options: ListenOptions, cb: (listenSocket: us_listen_socket | false) => void | Promise<void>) : TemplatedApp;\n    /** Listens to unix socket. Callback hands either false or a listen socket. */\n    listen_unix(cb: (listenSocket: us_listen_socket) => void | Promise<void>, path: RecognizedString) : TemplatedApp;\n    /** Registers an HTTP GET handler matching specified URL pattern. */\n    get(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers an HTTP POST handler matching specified URL pattern. */\n    post(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers an HTTP OPTIONS handler matching specified URL pattern. */\n    options(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers an HTTP DELETE handler matching specified URL pattern. */\n    del(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers an HTTP PATCH handler matching specified URL pattern. */\n    patch(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers an HTTP PUT handler matching specified URL pattern. */\n    put(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers an HTTP HEAD handler matching specified URL pattern. */\n    head(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers an HTTP CONNECT handler matching specified URL pattern. */\n    connect(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers an HTTP TRACE handler matching specified URL pattern. */\n    trace(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers an HTTP handler matching specified URL pattern on any HTTP method. */\n    any(pattern: RecognizedString, handler: (res: HttpResponse, req: HttpRequest) => void | Promise<void>) : TemplatedApp;\n    /** Registers a handler matching specified URL pattern where WebSocket upgrade requests are caught. */\n    ws<UserData>(pattern: RecognizedString, behavior: WebSocketBehavior<UserData>) : TemplatedApp;\n    /** Publishes a message under topic, for all WebSockets under this app. See WebSocket.publish. */\n    publish(topic: RecognizedString, message: RecognizedString, isBinary?: boolean, compress?: boolean) : boolean;\n    /** Returns number of subscribers for this topic. */\n    numSubscribers(topic: RecognizedString) : number;\n    /** Adds a server name. */\n    addServerName(hostname: string, options: AppOptions) : TemplatedApp;\n    /** Browse to SNI domain. Used together with .get, .post and similar to attach routes under SNI domains. */\n    domain(domain: string) : TemplatedApp;\n    /** Removes a server name. */\n    removeServerName(hostname: string) : TemplatedApp;\n    /** Registers a synchronous callback on missing server names. See /examples/ServerName.js. */\n    missingServerName(cb: (hostname: string) => void) : TemplatedApp;\n}\n\n/** Constructs a non-SSL app. An app is your starting point where you attach behavior to URL routes.\n * This is also where you listen and run your app, set any SSL options (in case of SSLApp) and the like.\n */\nexport function App(options?: AppOptions) : TemplatedApp;\n\n/** Constructs an SSL app. See App. */\nexport function SSLApp(options: AppOptions) : TemplatedApp;\n\n/** Closes a uSockets listen socket. */\nexport function us_listen_socket_close(listenSocket: us_listen_socket) : void;\n\n/** Gets local port of socket (or listenSocket) or -1. */\nexport function us_socket_local_port(socket: us_socket) : number;\n\nexport interface MultipartField {\n    data: ArrayBuffer;\n    name: string;\n    type?: string;\n    filename?: string;\n}\n\n/** Takes a POSTed body and contentType, and returns an array of parts if the request is a multipart request */\nexport function getParts(body: RecognizedString, contentType: RecognizedString) : MultipartField[] | undefined;\n\n/** WebSocket compression options. Combine any compressor with any decompressor using bitwise OR. */\nexport type CompressOptions = number;\n/** No compression (always a good idea if you operate using an efficient binary protocol) */\nexport var DISABLED: CompressOptions;\n/** Zero memory overhead compression. */\nexport var SHARED_COMPRESSOR: CompressOptions;\n/** Zero memory overhead decompression. */\nexport var SHARED_DECOMPRESSOR: CompressOptions;\n/** Sliding dedicated compress window, requires 3KB of memory per socket */\nexport var DEDICATED_COMPRESSOR_3KB: CompressOptions;\n/** Sliding dedicated compress window, requires 4KB of memory per socket */\nexport var DEDICATED_COMPRESSOR_4KB: CompressOptions;\n/** Sliding dedicated compress window, requires 8KB of memory per socket */\nexport var DEDICATED_COMPRESSOR_8KB: CompressOptions;\n/** Sliding dedicated compress window, requires 16KB of memory per socket */\nexport var DEDICATED_COMPRESSOR_16KB: CompressOptions;\n/** Sliding dedicated compress window, requires 32KB of memory per socket */\nexport var DEDICATED_COMPRESSOR_32KB: CompressOptions;\n/** Sliding dedicated compress window, requires 64KB of memory per socket */\nexport var DEDICATED_COMPRESSOR_64KB: CompressOptions;\n/** Sliding dedicated compress window, requires 128KB of memory per socket */\nexport var DEDICATED_COMPRESSOR_128KB: CompressOptions;\n/** Sliding dedicated compress window, requires 256KB of memory per socket */\nexport var DEDICATED_COMPRESSOR_256KB: CompressOptions;\n/** Sliding dedicated decompress window, requires 32KB of memory per socket (plus about 23KB) */\nexport var DEDICATED_DECOMPRESSOR_32KB: CompressOptions;\n/** Sliding dedicated decompress window, requires 16KB of memory per socket (plus about 23KB) */\nexport var DEDICATED_DECOMPRESSOR_16KB: CompressOptions;\n/** Sliding dedicated decompress window, requires 8KB of memory per socket (plus about 23KB) */\nexport var DEDICATED_DECOMPRESSOR_8KB: CompressOptions;\n/** Sliding dedicated decompress window, requires 4KB of memory per socket (plus about 23KB) */\nexport var DEDICATED_DECOMPRESSOR_4KB: CompressOptions;\n/** Sliding dedicated decompress window, requires 2KB of memory per socket (plus about 23KB) */\nexport var DEDICATED_DECOMPRESSOR_2KB: CompressOptions;\n/** Sliding dedicated decompress window, requires 1KB of memory per socket (plus about 23KB) */\nexport var DEDICATED_DECOMPRESSOR_1KB: CompressOptions;\n/** Sliding dedicated decompress window, requires 512B of memory per socket (plus about 23KB) */\nexport var DEDICATED_DECOMPRESSOR_512B: CompressOptions;\n/** Sliding dedicated decompress window, requires 32KB of memory per socket (plus about 23KB) */\nexport var DEDICATED_DECOMPRESSOR: CompressOptions;\n}"
  }
]